TCP I/O复用服务器模型
I/O端口复用
select系统调用的目的是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。poll和select应该被归类为这样的系统 调用,它们可以阻塞地同时探测一组支持非阻塞的IO设备,直至某一个设备触发了事件或者超过了指定的等待时间——也就是说它们的职责不是做IO,而是帮助 调用者寻找当前就绪的设备。
下面是select的原理图:
在linux中,select函数使我们可以执行I/O多路转接。
传给select的参数告诉内核:
1、我们所关心的描述符
2、对于每个描述符我们所关心的条件(读、写、异常)
3、愿意等待多长时间(可以永远等待、等待一个固定的时间或者根本不等待)
4、已准备好的描述符的总数量
从select返回时,内核告诉我们:
1、已准备好的描述符的总数量
2、对于读、写或异常这三个条件中的每一个,哪些描述符已准备好
select函数
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
//返回值, 准备就绪的描述符数目,若超时,返回0, 若出错,返回-1
参数 | 描述 |
---|---|
nfds | 指定集合中所有文件描述符的范围,所有文件描述符的最大值+1, nfds也就是定义了位图中的有效位的个数 |
readfds | 我们关心的可读的文件描述符集合 |
writefds | 我们关心的可写的文件描述符集合 |
exceptfds | 我们关心的异常的文件描述符集合 |
timeout | 我们愿意等待多长时间 |
timeout参数
指定愿意等待的时间
struct timeval {
__kernel_time_t tv_sec; /* seconds 秒数*/
__kernel_suseconds_t tv_usec; /* microseconds 微妙*/
};
1、timeout == NULL
永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的一个已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,select返回-1, errno设置为EINTR
2、timeout->tv_sec == 0 && timeout->tv_usec == 0
不等待,直接返回。
加入描述符集的描述符都会被测试,并且返回满足要求的描述符的个数。这种方法通过轮询,无阻塞地获得了多个文件描述符的状态。
3、timeout->tv_sec != 0 || timeout->tv_usec != 0
等待指定的描述和微秒数
当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返如果在超时到期时还没有一个描述符准备好,则返回0;与第一种一样,这种等待可被捕捉到的信号中断,在每次select返回之后,需要重新设置时间
readfds/writefds/execptfds三个参数
这三个描述符集说明了我们关心的可读、可写或处于异常条件的描述符集合
每个描述符集存储在一个fd_set数据类型中。这个数据类型是由实现选择的,他可以为每一个可能的描述符保持一位。我们可以认为他是一个很大的字节数组。 一般默认最大为1024bit
通过FD_SET(下面介绍)函数分别添加进readfds、writefds、 exceptfds文件描述符集,select将对这些文件描述符集中的文件描述符进行监听,如果有就绪文件描述符,select会重置readfds、writefds、exceptfds文件描述符集来通知应用程序哪些文件描述符就绪。这个特性将导致select函数返回后,再次调用select之前,必须重置我们关心的文件描述符,也就是此时的三个文件描述符集已经不是我们之前传入 的了。
对于fd_set数据类型,唯一可以进行处理的是:分配一个这种类型的变量,将这种类型的一个变量赋值给同类型的另一个变量,或对这种类型的变量使用下列4个函数中的一个
void FD_CLR(int fd, fd_set *set); //清除文件描述符集合set中相关fd的位
int FD_ISSET(int fd, fd_set *set); //用来测试文件描述符集合set中相关fd位是否为真
void FD_SET(int fd, fd_set *set); //用来设置文件描述符集合set中相关fd的位
void FD_ZERO(fd_set *set); //用来清除文件描述符集合set中的全部位
这些接口可以实现为宏或函数。
调用FD_ZERO将一个fd_set变量的所有位都设置为0。
要开启描述符集中的一位,可以调用FD_SET。
调用FD_CLR可以清除一位。
调用FD_ISSET测试描述符集中的一个指定位是否打开
对于select有3个可能的返回值
(1) 返回值-1 表示出错。这是可能发生的,例如,在所指定的描述符一个都没有准备好时捕捉到一个信号。在此种情况下,一个描述符集都不修改。
(2)返回值0 表示没有描述符准备好。 若指定的描述符一个都没有准备好,指定时间就过了,那么就会发生这种情况。此时,所有描述符都会置0。 需要重新将我们关心的描述符加入到描述符集中
(3)一个正数返回 说明了已经准备好的描述符数。该值是3个描述符集中已经准备好的描述符数之和,所以如果同一描述符已准备好,那么在返回值中会对其计两次数。在这种情况下,3个描述符集中 _仍旧打开的位_对应于已准备好的描述符
对于"准备好" 的含义
(1)若对读集(readfds)中的一个描述符进行的read操作不会阻塞,则认为此描述符是准备好的
(2)若对写集(writefds)中的一个描述符进行的write操作不会阻塞,则认为此描述符是准备好的
(3)若对异常条件集(exceptfds)中的一个描述符有一个未决异常条件,则认为此描述符是准备好的
异常条件包括:(1)在网络连接上到达带外的数据;(2)在处于数据包模式的为终端上发生了某些条件(某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息)
准备好读
满足以下四个条件中的任何一个时,一个套接字准备好读
(1) 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记(默认为1)的当前大小。对于这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)。我们可以使用SO_RCVLOWAIT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值为1
(2) 该连接读半部关闭(客户端关闭写半部shotdown(sockfd, SHUT_WR))(也就是服务器接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0 (可用该方法判断client是否断开连接)
(3) 该套接字是一个监听套接字且已完成的连接数不为0(有客户端连接)。对于这样的套接字accept通常不会阻塞
(4) 其上有一个套接字错误待处理。对于这样的套接字的读操作将不阻塞并返回-1,同时把errno设置成确切的错误条件
准备好写
(1) 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已经连接,或者该套接字不需要连接(如UDP套接字)。这意味着如果我们把这样的套接字设置成非阻塞,写操作将不阻塞并返回一个正值。 我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值通常为2048.
(2) 该连接的写半部关闭(客户端关闭其读半部(shutdown(sockfd, SHUT_RD)))。对这样的套接字的写操作将产生SIGPIPE信号
(3) 使用非阻塞式的connect的套接字已建立连接,或者connect已经以失败告终。
(4) 其上有一个套接字错误待处理。对于这样的套接字的读操作将不阻塞并返回-1,同时把errno设置成确切的错误条件
fd_set实例分析:
如果fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0010,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0010,0110
(4)执行select(6,&set,NULL,NULL,NULL)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0110。注意:没有事件发生的fd=5被清空。
select模型的特点
(1)可监控的文件描述符个数取决于fd_set变量 一般来说是1024bit 宏FD_SETSIZE,指定了大小
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。
二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
也就是说每一次select返回>=0的值,再次使用时都需要将关心的描述符加入到描述符集中,因为select返回0时会将所有的描述符集都 清零 select返回值大于0时,会将没有准备好的描述符设置为0
(3)可见select模型必须在select前循环array(FD_SET(fd, &fds),取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生)。
select模型,代码结构
记住一点即可,select返回后,如果返回值大于等于0 ,那么当前的描述符集就是已经准备好的描述符集(select 返回值> 0)或者描述符集为全0(select 返回值== 0),所以下次使用select时,如果想要正确的监听我们所关心的描述符的话,就需要重新使用FD_SET将我们关心的描述符加入到描述符集中
fd_set readfds;
int i;
int fdarray[FD_SETSIZE];
for(i = 0; i < FD_SETSIZE; i++)
{
fdarray[i] = -1;
}
struct timeval time = {1, 0};
for(;;)
{
FD_ZERO(&readfds);
for(i = 0; i < index; i++) //index arrayfd使用的最大下标
{
if (fdarray[i] != -1)
FD_SET(fdarray[i], &readfds);
}
int ret = select(maxfd + 1, &readfds, NULL, NULL, &time);
if (ret > 0)
{
for (i = 0; i < index; i++)
if (FD_ISSET(fdarray[i], &readfds))
{
xxxx
}
}
}
select 并发服务器/客户端模型
server端
/*************************************************************************
> File Name: server_select.c
> 作者:YJK
> Mail: 745506980@qq.com
> Created Time: 2021年05月15日 星期六 16时30分24秒
************************************************************************/
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/select.h>
#define SERVER_IP "192.168.51.183"
#define SERVER_PORT 5052
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define FDMAX 1024
#define BUFSIZE 2048
int server_socket(void)
{
int sock_fd = socket(AF_INET, SOCK_STREAM, 0); // IPV4 tcp
/*设置端口快速重用*/
int b_reuse = 1;
setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));
struct sockaddr_in addr;
addr.sin_family = AF_INET; //IPV4 需要与socket中的指定一致
addr.sin_addr.s_addr = inet_addr(SERVER_IP);
addr.sin_port = htons(SERVER_PORT);//大于1024
memset(addr.sin_zero, 0, sizeof(addr.sin_zero));
if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
perror("bind");
return -1;
}
if (listen(sock_fd, 5) == -1)
{
perror("bind");
return -1;
}
return sock_fd;
}
int main(int argc,char *argv[])
{
int fdarray[FDMAX] = {0};
memset(fdarray, -1, FDMAX);
int index = 0;
int i;
int fdmax; //当前最大的fd
int sock_fd = server_socket();
#if 0
int val;
val = fcntl(sock_fd, F_GETFD, 0);
if (val < 0)
{
perror("fcntl");
}
val |= FNONBLOCK;
if (fcntl(sock_fd, F_SETFD, val)< 0)
{
perror("fcntl");
return -1;
}
#endif
fdmax = sock_fd;
#if 0
struct timeval tim;
/*等待1s*/
tim.tv_sec = 1;
tim.tv_usec = 0;
#endif
fd_set readfds;
/*将sock_fd加入 readfds 当有客户端连接时,sock_fd变为可读状态*/
while (1)
{
FD_ZERO(&readfds);
FD_SET(sock_fd, &readfds); //将sock_fd,加入文件描述符集
for (i = 0; i < index; i++)
{
if (fdarray[i] != -1)
{
FD_SET(fdarray[i], &readfds);
}
}
int err = select(fdmax + 1, &readfds, NULL, NULL, NULL);
if (err < 0) //出错
{
perror("select");
return -1;
}
else if (err == 0) //超时
{
continue;
}
else //成功
{
if (FD_ISSET(sock_fd, &readfds)) //新连接
{
printf("new connect\n");
int sign = 0;
struct sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int c_sock_fd = accept(sock_fd, (struct sockaddr*)&caddr, &len); //阻塞
if (c_sock_fd == -1)
{
perror("accept");
return -1;
}
printf("client ip addr %s\n", inet_ntoa(caddr.sin_addr));
printf("client port addr %d\n", ntohs(caddr.sin_port));
/*将新建立连接的客户端fd加入到集合中*/
for (i = 0; i < index ; i++)
{ /*如果有连接已经断开,则加入到空缺位置*/
if (fdarray[i] == -1)
{
fdarray[i] = c_sock_fd;
sign = 1;
break;
}
}
if(sign == 0) //没有连接断开
{
fdarray[index] = c_sock_fd;
index++;
}
if (index == FDMAX)
{
fprintf(stderr, "client connect is full\n");
close(c_sock_fd);
continue;
}
fdmax = MAX(fdmax, c_sock_fd); //更新最大值
printf("c_sock_fd :%d\n", c_sock_fd);
}
else{
printf("client data\n");
for(i = 0;i < index; i++)
{
if (fdarray[i] == -1) //表示当前index无客户端socket
{
continue;
}
/*当前描述符有数据*/
if(FD_ISSET(fdarray[i], &readfds))
{
// printf("fdarray[%d]\n", i);
/*获取客户端发送的数据,并进行处理*/
char buf[BUFSIZE] = {0};
int ret;
do{
ret = read(fdarray[i], buf, BUFSIZE);
}while(ret == -1 && errno==EINTR);
if (ret == -1)
{
perror("read");
return -1;
}
if (ret == 0) // 客户端断开连接
{
printf("client is close------%d\n", fdarray[i]);
close(fdarray[i]);
fdarray[i] = -1; //标志为无fd
}
/*给所有客户端转发数据*/
for (int j = 0; j < index; j++)
{
if (j != i && fdarray[j] != -1)
{
printf("recv:%s\n", buf);
err = write(fdarray[j], buf, ret);
if (err == -1)
{
perror("write");
return -1;
}
}
}
memset(buf, 0, ret);
}
}
}
}
}
return 0;
}
client端
/*************************************************************************
> File Name: client_select.c
> 作者:YJK
> Mail: 745506980@qq.com
> Created Time: 2021年05月15日 星期六 20时16分59秒
************************************************************************/
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#define MAXSLEEP 128
#define SERVER_IP "192.168.51.183"
#define SERVER_PORT 5052
#define BUFSIZE 2048
int connect_try(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
{
int num_sec;
for(num_sec = 1; num_sec <= MAXSLEEP; num_sec <<= 1)
{
if (connect(sockfd, addr, addrlen) == 0)
// 成功连接
return 0;
if (num_sec <= MAXSLEEP/2) //休眠,然后再次重连
sleep(num_sec);
}
return -1; //超时, 返回 -1
}
int main(int argc,char *argv[])
{
int sock_fd = socket(AF_INET, SOCK_STREAM, 0); // IPV4 tcp
struct sockaddr_in addr;
addr.sin_family = AF_INET; //IPV4 需要与socket中的指定一致
addr.sin_addr.s_addr = inet_addr(SERVER_IP);
addr.sin_port = htons(SERVER_PORT);//大于1024
memset(addr.sin_zero, 0, sizeof(addr.sin_zero));
if (connect_try(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
{
perror("connect");
return -1;
}
printf("connect is success!\n");
fd_set readfds;
/*
struct timeval tim = {1, 0}; //最多等待1s
char buf[BUFSIZE] = {0};
*/
for(;;)
{
FD_ZERO(&readfds);// 清空readfds
FD_SET(0, &readfds); //加入stdin
FD_SET(sock_fd, &readfds); //加入socket
int ret = select(sock_fd + 1, &readfds, NULL, NULL, NULL);
if (ret < 0) //出错
{
perror("select");
return -1;
}
else if (ret == 0) //超时,再次等待
{
continue;
}
else
{
if (FD_ISSET(0, &readfds)) //stdin数据
{
fgets(buf, BUFSIZE - 1, stdin);
if (ret > 0)
{
char *str = buf;
while(*str != '\0')
{
if (*str == '\n') //去掉'\n'
*str = '\0';
str++;
}
ret = write(sock_fd, buf, (str - buf));
if (ret == -1)
{
perror("write");
return -1;
}
}
}
if (FD_ISSET(sock_fd, &readfds))
{
do{
ret = read(sock_fd, buf, sizeof(buf));
}while(ret == -1 && errno == EINTR);
if (ret == -1)
{
perror("read");
return -1;
}
if (!ret)
{
printf("server is close");
break;
}
printf("buf:%s\n", buf);
}
}
}
close(sock_fd);
return 0;
}
客服端连接服务器,然后发送数据,服务器将数据转发给非本客户端的所有客户端
select服务器优缺点
优点
(1)select()的可移植性更好;
(2)select()对于超时值提供了更好的精度(微秒级)。
(3)不需要建立多个线程、进程就可以实现一对多的通信。
(4)可以同时等待多个文件描述符,效率比起多进程多线程来说要高很多。
缺点
(1)每次调用选择,都需要把FD集合从用户态拷贝到内核态,这个开销在FD很多时会很大 ;
(2)同时每次调用选择都需要在内核遍历传递进来的所有的FD,这个开销在FD很多时也很大 ;
(3)select支持的文件描述符数量有上限,默认是1024(可以修改)。