【Linux网络编程】select函数实现TCP并发服务器

I/O多路复用

应用程序中同时处理 多路 输入输出流,若采用 阻塞模式 ,将得不到预期的目的;
若采用 非阻塞模式 ,对多个输入进行轮询,但又太浪费 CPU 时间;
若设置 多个进程 ,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂;
比较好的方法是使用 I/O 多路复用 。其基本思想是:
先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行 I/O 时函数才返回。
函数返回时告诉进程那个描述符已就绪,可以进行 I/O操作。
I/O多路复用可以不使用进程/线程,就能完成多个客户端连接一个服务器 

TCP多路复用

select()函数

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>


int select(int n, fd_set *read_fds, fd_set *write_fds, fd_set *except_fds, struct timeval *timeout);

参数:
maxfd
所有监控的文件描述符中最大的那一个加1
read_fds
所有要读的文件文件描述符的集合 
write_fds
所有要的写文件文件描述符的集合 
except_fds
其他要向我们通知的文件描述符 
timeout
超时设置. 
Null:一直阻塞,直到有文件描述符就绪或出错
时间值为0:仅仅检测文件描述符集的状态,然后立即返回
时间值不为0:在指定时间内,如果没有事件发生,则超时返回。

在我们调用select时进程会一直阻塞直到以下的一种情况发生.

–有文件可以读.

–有文件可以写.

–超时所设置的时间到.

为了设置文件描述符我们要使用几个宏:

宏的形式:
void FD_ZERO(fd_set *fdset)
void FD_SET(int fd,fd_set *fdset) 
void FD_CLR(int fd,fd_set *fdset) 
int FD_ISSET(int fd,fd_set *fdset) 

–FD_SET       将fd加入到fdset

–FD_CLR      将fd从fdset里面清除

–FD_ZERO   从fdset中清除所有的文件描述符

–FD_ISSET   判断fd是否在fdset集合中

代码详解

服务器server.c

#include "Network.h"

#define MAX_FD 10

int main(int argc, const char **argv)
{
    int fd = -1, newfd = -1, ret;
    struct sockaddr_in server_addr;
    int i = 0;
    char buf[BUFSIZ];

    /*1、创建套接字*/
    if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket");
        return -1;
    }

    /*2、填充网络信息结构体*/
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);

    //优化:允许任意IP地址的客户端连接
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    /*3、绑定=====将名称分配给套接字*/
    if(bind(fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("bind");
        return -1;
    }

    /*4、监听=====把主动套接字转换为被动套接字*/
    if(listen(fd, 5) < 0)
    {
        perror("listen");
        return -1;
    }

    printf("***Server start success***\n");
    
    
    fd_set rset, temps;
    int num;
    struct sockaddr_in client_addr;
    socklen_t AddrLen = sizeof(client_addr);
    char ipv4_addr[16];
    
    int fd_all[MAX_FD]; //保存所有描述符,用于select调用后,判断哪个可读
    memset(fd_all, -1, sizeof(fd_all));
    fd_all[0] = fd;     //第一个为监听套接字

    FD_ZERO(&rset);     //清零
    FD_SET(fd, &rset);  //监听套接字fd加进集合,监视fd

    int maxfd = fd_all[0];  //监听的最大套接字

    /*
        5、用Select函数,达到I/O多路复用,一个服务器支持多个客户端
    */
    while(1)
    {
        temps = rset;    //因为无事件发生的fd会被清空,需要重新复制

        if((num = select(maxfd + 1, &temps, NULL, NULL, NULL)) < 0)
        {
            perror("select");
            return -1;
        }
        if(num < 0)
        {
            perror("select");
            return -1;
        }
        if(num == 0)
        {
            printf("timeout\n");
        }

        if(FD_ISSET(fd, &temps))      //监听套接字如果可读,就说明有新客户端连接进来了
        {
                /* 
                    6、接受=====阻塞等待客户端连接 
                */
                if((newfd = accept(fd, (struct sockaddr *)&client_addr, &AddrLen)) < 0)
                {
                    perror("accept");
                    close(fd);
                    return -1;
                }
                //客户端连接成功后在服务器端打印出来
                if(!inet_ntop(AF_INET, (void *)&client_addr.sin_addr.s_addr, ipv4_addr, sizeof(client_addr)))
                {
                    perror("ionet_ntop");
                    close(newfd);
                    close(fd);
                    return -1;
                }

                printf("Client [%s:%d] connected!\n", ipv4_addr, htons(client_addr.sin_port));

                /* 将新连接套接字加入进fd_all集合 和 rset集合 */
                for(i = 0;i <= MAX_FD; i++)
                {
                    if(fd_all[i] == -1)
                    {
                        fd_all[i] = newfd;
                        printf("client fd_all[%d] join in.\n", i);
                        break;
                    }
                    else
                    {
                        continue;
                    }
                }
                
                if(newfd == FD_SETSIZE)
                {
                    printf("error: too many client newfd\n");
                    return -1;
                }
                if(newfd > -1 && newfd < FD_SETSIZE)
                {
                    FD_SET(newfd, &rset);
                }
                

                if(maxfd < newfd)
                {
                    maxfd = newfd;  //更新maxfd
                }
            }
    
        /*
            7、读写数据,并且将数据返回给客户端
            循环判断哪个文件描述符
        */
        for(int i = 1;i < maxfd; i++)
        {
            if(FD_ISSET(fd_all[i], &temps))
            {
                bzero(buf, BUFSIZ);

                do {
                    ret = read(fd_all[i], buf, BUFSIZ-1);
                }while(ret < 0 && EINTR == errno);

                if(ret < 0)
                {
                    perror("read");
                    return -1;
                }

                if(!ret)    //客户端退出,关闭套接字,并从监听集合清除
                {
                    FD_CLR(fd_all[i], &rset);
                    close(fd_all[i]);
                    fd_all[i] = -1;
                    
                    continue;
                }   

                if(!strncasecmp(buf, "quit", 4))
                {
                    printf("Client all_fd[%d] has left!\n", i);
                    continue;
                }

                printf("[Server]Receive data: %s\n",buf);
            }
        }
    }

    return 0;
}

客户端client.c

#include "Network.h"

int main(int argc, const char **argv)
{
    int fd = -1, ret;
    struct sockaddr_in server_addr;
    char buf[BUFSIZ];

    /*1、创建套接字*/
    if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket");
        return -1;
    }

    /*2、填充网络信息结构体*/
    bzero(&server_addr, sizeof(struct sockaddr_in));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
#if 0
//简单写法(只适用于IPv4)
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
#else
//复杂写法(也兼容IPv6)
    if(inet_pton(AF_INET, SERVER_IP, (void *)&server_addr.sin_addr.s_addr) != 1)
    {
        perror("inet_pton");
        return -1;
    }
#endif

    /*3、与服务器建立连接*/
    if(connect(fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("connect");
        return -1;
    }

    printf("Connect to the server!\n");


    /*4、
        用Select函数,达到I/O多路复用,一个服务器支持多个客户端

        读写=====发送信息给服务器端
    */
    fd_set rset;
    int maxfd = -1;

    while(1)
    {   
        FD_ZERO(&rset);
        FD_SET(0, &rset);
        FD_SET(fd, &rset);
        maxfd = fd;

        if(select(maxfd + 1, &rset, NULL, NULL, NULL) < 0)
        {
            perror("select");
            return -1;
        }

		if (FD_ISSET (0, &rset)) {	//标准键盘上有输入
			//读取键盘输入,发送到网络套接字fd
			bzero (buf, BUFSIZ);
			do {
				ret = read (0, buf, BUFSIZ - 1);
			} while (ret < 0 && EINTR == errno);
			if (ret < 0) {
				perror ("read");
				continue;
			}
			if (!ret)
				continue;

			if (write (fd, buf, strlen (buf)) < 0) {
				perror ("write() to socket");
				continue;
			}

			if (!strncasecmp(buf, "quit", 4)) {	//用户输入了quit字符
				printf ("Client is exiting!\n");
				break;
			}
		}
    }
    
    close(fd);

    return 0;
}

头文件Network.h

#ifndef __NETWORK_H__
#define __NEWWORK_H__


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/select.h>


#define SERVER_IP   "10.172.154.133"
#define SERVER_PORT 5520

#endif

猜你喜欢

转载自blog.csdn.net/imysy_22_/article/details/127298843