关于网络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 //创建网络io int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123 servaddr.sin_family = AF_INET; //INADDR_ANY绑定任意网卡,接收任意网卡的数据 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0 servaddr.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); }
值得注意的是htonl
和htons
都是将主机字节序转换为网络字节序
htonl
表示转换四字节的无符号整数,htons
表示转换两字节的无符号整数
htonl, htons, ntohl, ntohs - convert values between host and network byte order
运行这段程序,可以发现程序没有任何效果,直接退出,而当我们在listen
后面加上
getchar();
后,程序阻塞,这时通过命令netstat -anop | grep 9999
查看端口状态:
发现该端口正处于listen
状态,这时通过网络调试助手(充当客户端)连接192.168.209.130:9999发现连接成功。
服务端其实一直都处于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
之后调用recv
和send
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
下面介绍使用select
和poll
的方式:
#include <sys/select.h> #define BUFFER_LENGTH 1024 listen(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 1024 listen(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