> 文章列表 > 2.1.1网络io与io多路复用select/poll/epoll

2.1.1网络io与io多路复用select/poll/epoll

2.1.1网络io与io多路复用select/poll/epoll

关于网络io,我们可以通过一个服务端-客户端的示例来了解:
这是一段TCP服务端的代码:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>int main() {//open//创建网络ioint sockfd = socket(AF_INET, SOCK_STREAM, 0); // iostruct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123servaddr.sin_family = AF_INET;//INADDR_ANY绑定任意网卡,接收任意网卡的数据servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0servaddr.sin_port = htons(9999);if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {printf("bind failed: %s", strerror(errno));return -1;}listen(sockfd, 10); 
}

值得注意的是htonlhtons都是将主机字节序转换为网络字节序
htonl表示转换四字节的无符号整数,htons表示转换两字节的无符号整数

htonl,  htons,  ntohl, ntohs - convert values between host and network byte order

运行这段程序,可以发现程序没有任何效果,直接退出,而当我们在listen后面加上
getchar();后,程序阻塞,这时通过命令netstat -anop | grep 9999查看端口状态:
2.1.1网络io与io多路复用select/poll/epoll
发现该端口正处于listen状态,这时通过网络调试助手(充当客户端)连接192.168.209.130:9999发现连接成功。
2.1.1网络io与io多路复用select/poll/epoll
服务端其实一直都处于listen状态,之后还需要通过accept接受连接

	listen(sockfd, 10); struct sockaddr_in clientaddr;socklen_t len = sizeof(struct sockaddr_in);int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);getchar();

accept接受客户端的连接,并通过传出参数返回客户端的信息,以及函数返回clientfd,后续该客户端的数据收发都通过该clientfd进行。这也就反应了一个问题,每个客户端连接的都会对应一个clientfd。这时运行程序,程序阻塞等待客户端的连接,但这次是阻塞在accept系统调用上。创建的sockfd默认是阻塞的
而关于阻塞和非阻塞的概念,简单总结就是阻塞会等待有事件发生,非阻塞则是不管有无事件都会立即返回。
我们将sockfd设为非阻塞形式,并将getchar()注释掉再看看效果:

#include <fcntl.h>...listen(sockfd, 10);//设为非阻塞int flags = fcntl(sockfd, F_GETFL, 0);flags |= O_NONBLOCK;fcntl(sockfd, F_SETFL, flags);struct sockaddr_in clientaddr;...//getchar();

调用程序发现,程序立即返回,不再阻塞!

现在思考一个问题,连接成功是在listen完成,还是在accept完成呢?
我们可以在listen后加上一个sleep(10);,在accept返回后打印一下返回值

	listen(sockfd, 10);sleep(10);...struct sockaddr_in clientaddr;socklen_t len = sizeof(struct sockaddr_in);int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("clientfd: %d\\n", clientfd);

程序运行后,立马连接,发现可以连接成功,并且10秒后,accept返回4
说明在listen连接就已经建立成功了,而clientfd为4则是因为,标准输入、标准输出、标准错误、以及sockfd已经占用了0、1、2、3再分配的文件描述符就是4。

接下来进行数据的收发(使用阻塞模式),accept之后调用recvsend

	char buffer[BUFFER_LENGTH] = {0};int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);printf("ret: %d, buffer: %s\\n", ret, buffer);send(clientfd, buffer, ret, 0);

这时收发数据只能进行一次,我们可以加上while循环实现循环收发。若想实现多个客户端连接,并支持收发数据,也把accept放入while循环??形如这样?

while (1) {int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);char buffer[BUFFER_LENGTH] = {0};int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);printf("ret: %d, buffer: %s\\n", ret, buffer);send(clientfd, buffer, ret, 0);
}

我们开多个客户端连接发现确实能连接上服务端,但连接后仍然只能进行一次数据收发。
因为一直阻塞在accept上,服务端只会服务新来的连接的一次数据收发。

那要支持多个客户端连接,并且都能进行多次数据收发该如何做呢?
我们可以将数据收发的工作放在一个线程中循环做:

#include <pthread.h>
void *client_thread(void *arg) {int clientfd = *(int*)arg;//线程中循环数据收发while (1) {char buffer[BUFFER_LENGTH] = {0};int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);//recv返回0说明对端关闭连接if (ret == 0) {close(clientfd);break;}printf("ret: %d, buffer: %s\\n", ret, buffer);send(clientfd, buffer, ret, 0);}
}
...while (1) {int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);pthread_t threadid;pthread_create(&threadid, NULL, client_thread, &clientfd);}
...

来一个连接,创建一个线程,该线程循环负责该客户端的数据收发。
但是这种模式存在一个弊端,成千上万个客户端连接,难道要创建对应个数的线程吗?有没有更好的解决办法?有,那便是IO多路复用!
Linux中有三种IO多路复用:select、poll、epoll
下面介绍使用selectpoll的方式:

#include <sys/select.h>
#define BUFFER_LENGTH 1024listen(sockfd, 10); struct sockaddr_in clientaddr;socklen_t len = sizeof(struct sockaddr_in);fd_set rfds, rset;FD_ZERO(&rfds);FD_SET(sockfd, &rfds);int maxfd = sockfd;int clientfd = 0;while (1) {rset = rfds;//这里传入文件描述符最大值加1//判断时是形如for(; i < maxfd; i++)所以要加一int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);if (FD_ISSET(sockfd, &rset)) {clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept: %d\\n", clientfd);FD_SET(clientfd, &rfds);if (clientfd > maxfd) maxfd = clientfd;if(--nready == 0) continue;}int i = 0;for (i = sockfd + 1; i <= maxfd; i++) {if (FD_ISSET(i, &rset)) {char buffer[BUFFER_LENGTH] = {0};int ret = recv(i, buffer, BUFFER_LENGTH, 0);if (ret == 0) {close(i);break;}printf("ret: %d, buffer: %s\\n", ret, buffer);send(i, buffer, ret, 0);}}}getchar();

值得注意的是select是通过判断fd_set中的某些位,从而判断是否发生事件,因此,select所能处理的文件描述符个数是有限的,只有1024个。
下面是poll的使用方式:

#include <poll.h>
#define POLL_SIZE     1024listen(sockfd, 10); struct sockaddr_in clientaddr;socklen_t len = sizeof(struct sockaddr_in);struct pollfd fds[POLL_SIZE] = {0};fds[sockfd].fd = sockfd;fds[sockfd].events = POLLIN;int maxfd = sockfd;int clientfd = 0;while (1) {int nready = poll(fds, maxfd + 1, -1);if (fds[sockfd].revents & POLLIN) {clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept: %d\\n", clientfd);fds[clientfd].fd = clientfd;fds[clientfd].events = POLLIN;if (clientfd > maxfd) maxfd = clientfd;if(--nready == 0) continue;}int i = 0;for (i = 0; i <= maxfd; i++) {if (fds[i].revents & POLLIN) {char buffer[BUFFER_LENGTH] = {0};int ret = recv(i, buffer, BUFFER_LENGTH, 0);if (ret == 0) {fds[i].fd = -1;fds[i].events = 0;close(i);break;}printf("ret: %d, buffer: %s\\n", ret, buffer);send(i, buffer, ret, 0);}}}getchar();

poll相较select,支持的文件描述符数量不受限制,并且每次调用无需重新设置事件,因为内核不会修改,而是通过revent返回。但是他们都有性能瓶颈,他们返回就绪的文件描述符个数,但仍需我们自己去遍历到底是哪个文件描述符上有事件,而epoll解决了这种问题。

文章参考与<零声教育>的C/C++linux服务期高级架构系统教程学习:https://ke.qq.com/course/417774?flowToken=1020253

常用软件下载