I/O复用函数的使用(select)
I/O 复用使得程序能同时监听多个文件描述符,这对于提高程序的性能至关重要。通常,网络程序在下列情况下需要使用 I/O 复用技术:
◼ TCP 服务器同时要处理监听套接字和连接套接字。
◼ 服务器要同时处理 TCP 请求和 UDP 请求。
◼ 程序要同时处理多个套接字。
◼ 客户端程序要同时处理用户输入和网络连接。
◼ 服务器要同时监听多个端口。
(同时处理多个描述符)
需要指出的是,I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依处理其中的每一个文件描述符,这使得服务器看起来好像是串行工作的。如果要提高并发处理的能力,可以配合使用多线程或多进程等编程方法。
TCP中,第一个客户端和服务器端建立连接,向服务器端不发数据,服务器端就在recv阻塞住,无法继续执行;如果有第二个客户端与服务器端建立连接,就在已完成三次握手的队列中放着,等着accept处理它,由于我们的代码阻塞在recv,没有机会去执行accept,导致第二个客户端得不到响应,我们之前的解决方法是accept之后的代码在子线程中执行。多线程多进程解决此问题。
select poll epoll都是系统调用
那么今天我们就要用I/O复用的select方法解决此问题!
I/O复用解决描述符过多的情况。(开销问题)
我们在一个线程内完成多个客户端连接服务器端。
如何做到的?在单个进程内同时处理多个描述符?
举个例子,假如学校要给你们发一本书,一种情况下,所有人到图书馆门前等,每个人就是一个线程,每个人都想要自己的数据,每个人都在等,阻塞住,如果谁的书到了,点名,谁就上去把书拿了,线程退出,线程往下执行。那么多人等,都阻塞住。还有一种情况,就留一个人在那等,谁的书发下来,那个人就给你打电话,你就去图书馆领取,这就是留一个人在那里等,就一个人阻塞住,先看有你没有数据,如果有数据,就通知你来处理这个数据,相当于代码执行recv,不会阻塞,因为你的数据已经到达了。
就是我们用I/O函数先去检测所有的描述符,挑谁上面有数据,把这些检测出来,对这些有数据的描述符来个循环,依次处理,依次recv处理,都不会阻塞,因为都有数据,select检测没有数据的描述符我们不去recv处理。
我们首先给出一个集合
把关系的描述符都加进去(3,4,6,7),然后select前去检测,谁上面有数据,select返回的n的值为-1,就是失败了。n为0,就是超时了,select可以永久阻塞也可以设置一个超时时间,时间到了提醒我们。n>0,集合中有n个元素上面有数据。接下来我们要找到是哪个,比如说n=2,说明3,4,6,7中有2个有数据。select返回只是告诉我们有n个有数据,不会告诉我们是哪个。select返回以后,我们去检测哪个有数据,比如说4,6有数据,我们把4,6处理,然后再把 这些数据重新添加到集合中,再select处理。
select 的接口介绍
select 系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件。
select 系统调用的原型如下:
存放描述符是以位来存储的!
这个集合有1024个位。比这里有描述符3,4,6,7,如果我们按一个整数去存,一个整数占4个字节,很占空间。所以我们在这里用偏移量去算(按位存)。比如描述符是0,把第一个值0置为1,如果描述符是2,把第三个0改成1。这个集合可以存放0-1023的描述符。
当我们把这些描述符添加进去,我们把集合交给select,select返回2,就说明2个有数据。原来我们把第4个,第五个,第七个,第八个位置为1,分别代表描述符,3,4,6,7,假如3和6上有数据,我们就把4和7对应的位置从1置为0。select把没有数据的描述符从1置为0。但是select不会告诉我们是哪个有数据。接下来我们用一个方法去测试,把1从头向右移,按位与,都为1为1。
测出3和6上有数据,系统提供了4个方法:FD_ZERO,把集合空间全部清空置为0,FD_SET,把描述符添加到相应的位置,FD_CLR,清除某个描述符的位置,int FD_ISSET,测试描述符在集合中有没有被设置(1按位与)。
#include <sys/select.h>
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct ti
meval *timeout);
/*
select 成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内
没有任何文件描述符就绪,select 将返回 0。select 失败是返回-1.如果在 select 等待
期间,程序接收到信号,则 select 立即返回-1,并设置 errno 为 EINTR。
maxfd 参数指定的被监听的文件描述符的总数。它通常被设置为 select 监听的所
有文件描述符中的最大值+1,就是把前面几个位关注,后面的位就不关注了
readfds、writefds 和 exceptfds 参数分别指向可读、可写和异常等事件对应的文件
描述符集合。应用程序调用 select 函数时,通过这 3 个参数传入自己感兴趣的文件
描述符。select 返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪
*/
//fd_set 结构如下:
#define __FD_SETSIZE 1024//数组有2014个位的空间
typedef long int __fd_mask;
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
typedef struct
{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];//多少个元素
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
1 # define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
//通过下列宏可以访问 fd_set 结构中的位:
FD_ZERO(fd_set *fdset); // 清除 fdset 的所有位
FD_SET(int fd, fd_set *fdset); // 设置 fdset 的位 fd
FD_CLR(int fd, fd_set *fdset); // 清除 fdset 的位 fd
int FD_ISSET(int fd, fd_set *fdset);// 测试 fdset 的位 fd 是否被设置
/*
timeout 参数用来设置 select 函数的超时时间。它是一个 timeval 结构类型的指
针,采用指针参数是因为内核将修改它以告诉应用程序 select 等待了多久。timeval
结构的定义如下:
struct timeval
{
long tv_sec; //秒数
long tv_usec; // 微秒数
};
如果给 timeout 的两个成员都是 0,则 select 将立即返回。如果 timeout 传递
NULL,则 select 将一直阻塞,直到某个文件描述符就绪
*/
读事件:一开始是没有就绪的,因为接收缓存区没有数据,对方发了才有
写事件:一开始是就绪的,因为发送缓冲区没有数据,是空闲的
select(检查键盘是否有数据)
为什么每次都要去清空,添加呢?
因为select返回会去修改fdset的内容,下次我们要关注哪些描述符就得重新添加,包括tv,select返回以后会去修改tv的值,如果5秒超时,3秒的时候数据到达了,它会返回告诉你描述符上有数据,此时会把tv的值改为2秒,因为用了3秒
运行程序
select检测有数据,才去read读。
看下图,第一次,集合中只有一个描述符,sockfd,select检测如果有数据,n只可能是1,如果sockfd有数据,我们调用accept,得到新的c,把c添加进集合,集合中有2个描述符了,如果select返回1,只有一个有数据,使用FED_ISSET,检测,发现仍然是sockfd有数据,调用accept,产生一个新的c,添加进集合,集合中有3个描述符,select返回1,检测,发现是c有数据,就调动recv处理。
但是select返回之后,会把没有数据的描述符删掉,所以我们有必要定义一个数组,事先把这些描述符先存起来!
下标就是描述符的值,方便寻找查询!或者把数组的值初始化为-1,代表空闲的。是-1就可以把描述符存进去。
select 处理多个客户端
封装好,我们书写主函数
客户端的代码在此!
运行服务器和客户端
最后关闭的时候,一定要移除,如果不移除,下一轮会把这个描述符继续添加到集合中,但是对方已经关闭了,如果我们去检测这个描述符有没有数据,但是它执行了return -1,有数据,就有问题了。所以我们select只是去检测“还在通话的”。
我们把接收数据从127改为1,一次只收1个字符
全读到了,意味着我们把hello发出去,select,有数据,recv,再一次select,缓冲区上有数据,recv。当对方发了数据,如果我们没有收完,下一次select检测,这个描述符还是有数据,再recv,不会丢数据,还会再次提醒你。
使用 select 实现的 TCP 服务器代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#define MAX_FD 128
#define DATALEN 1024
// 初始化服务器端的 sockfd 套接字
int InitSocket()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1) return -1;
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(res == -1) return -1;
res = listen(sockfd, 5);
if(res == -1) return -1;
return sockfd;
}
// 初始化记录服务器套接字的数组
void InitFds(int fds[], int n)
{
int i = 0;
for (; i < n; ++i)
{
fds[i] = -1;
}
}
// 将套接字描述符添加到数组中
void AddFdToFds(int fds[], int fd, int n)
{
int i = 0;
for (; i < n; ++i)
{
if (fds[i] == -1)
{
fds[i] = fd;
break;
}
}
}
// 删除数组中的套接字描述符
void DelFdFromFds(int fds[], int fd, int n)
{
int i = 0;
for (; i < n; ++i)
{
if (fds[i] == fd)
{
fds[i] = -1;
break;
}
}
}
//将数组中的套接字描述符设置到 fd_set 变量中,并返回当前最大的文件描述符值
int SetFdToFdset(fd_set *fdset, int fds[], int n)
{
FD_ZERO(fdset);
int i = 0, maxfd = fds[0];
for (; i < n; ++i)
{
if (fds[i] != -1)
{
FD_SET(fds[i], fdset);
if (fds[i] > maxfd)
{
maxfd = fds[i];
}
}
}
return maxfd;
}
void GetClientLink(int sockfd, int fds[], int n)
{
struct sockaddr_in caddr;
memset(&caddr, 0, sizeof(caddr));
socklen_t len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
if (c < 0)
{
return;
}
printf("A client connection was successful\n");
AddFdToFds(fds, c, n);
}
// 处理客户端数据
void DealClientData(int fds[], int n, int clifd)
{
char data[DATALEN] = { 0 };
int num = recv(clifd, data, DATALEN - 1, 0);
if (num <= 0)
{
DelFdFromFds(fds, clifd, n);
close(clifd);
printf("A client disconnected\n");
}
else
{
printf("%d: %s\n", clifd, data);
send(clifd, "OK", 2, 0);
}
}
// 处理 select 返回的就绪事件
void DealReadyEvent(int fds[], int n, fd_set *fdset, int sockfd)
{
int i = 0;
for (; i < n; ++i)
{
if (fds[i] != -1 && FD_ISSET(fds[i], fdset))
{
if (fds[i] == sockfd)
{
GetClientLink(sockfd, fds, n);
}
else
{
DealClientData(fds, n, fds[i]);
}
}
}
}
int main()
{
int sockfd = InitSocket();
assert(sockfd != -1);
fd_set readfds;
int fds[MAX_FD];
InitFds(fds, MAX_FD);
AddFdToFds(fds, sockfd, MAX_FD);
while ( 1 )
{
int maxfd = SetFdToFdset(&readfds, fds, MAX_FD);
struct timeval timeout;
timeout.tv_sec = 2; // 秒数
timeout.tv_usec = 0; //微秒数
int n = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
if (n < 0)
{
printf("select error\n");
break;
}
else if (n == 0)
{
printf("time out\n");
continue;
}
DealReadyEvent(fds, MAX_FD, &readfds, sockfd);
}
exit(0);
}
TCP客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int InitSocket()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1) return -1;
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(res == -1) return -1;
return sockfd;
}
int main()
{
int sockfd = InitSocket();
assert(sockfd != -1);
while ( 1 )
{
printf("please input: ");
char buff[128] = { 0 };
fgets(buff, 127, stdin);
if (strncmp(buff, "bye", 3) == 0)
{
break;
}
int n = send(sockfd, buff, strlen(buff) - 1, 0);
{
printf("send error\n");
break;
}
memset(buff, 0, 128);
n = recv(sockfd, buff, 127, 0);
if (n <= 0)
{
printf("send error\n");
break;
}
printf("%s\n", buff);
}
close(sockfd);
exit(0);
}