第16.1节 写一个服务端代码
-
服务端代码
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> int main() { //open 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; 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); getchar(); }
-
运行该段代码,可以使用netstat -anop | grep 9999查看某一个端口(如:9999)进程的命令。结果如下图。可以发现代码执行到此处的时候,程序已经开始监听了。
-
可以使用第三方的网络助手工具尝试连接该端程序。可以发现可以正常发送成功,但是却没有没有反馈。
-
出现上述问题主要原因见下图。通过listen这是监听了,并没有真正的建立连接。建立连接是通过accept来实现的,并且每个客户端都有一个服务对应处理。
-
故而添加accept代码如下
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> int main() { //open 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; 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); //sleep(10); #if 0 printf("sleep\n"); int flags = fcntl(sockfd, F_GETFL, 0); flags |= O_NONBLOCK; fcntl(sockfd, F_SETFL, flags); #endif struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); // getchar(); }
-
上述代码运行效果如下
可以发现代码阻塞在accept函数处,此时通过工具建立连接,就会继续运行。而通过代码中的#if...#endif处的代码可以将阻塞io转换为非阻塞io。
-
思考:若在listen之后还未到accept的时候建立连接会成功吗?修改代码验证该思考。
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> int main() { //open 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; 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); sleep(10); printf("sleep\n"); #if 0 printf("sleep\n"); int flags = fcntl(sockfd, F_GETFL, 0); flags |= O_NONBLOCK; fcntl(sockfd, F_SETFL, flags); #endif struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd); // getchar(); }
使用工具在未打印sleep之前点击连接,过一会儿运行结果如下图,可以发现依然可以建立连接。
所以该思考的结果是:accept和是否能建立连接没有关系。如在listen之后sleep(10),还没有到accept的时候建立连接也是可以的。
-
思考:将代码改成非阻塞的,若在accept到来之前还未点击连接会如何?修改案例代码如下:
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> int main() { //open 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; 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); sleep(10); #if 1 printf("sleep\n"); int flags = fcntl(sockfd, F_GETFL, 0); flags |= O_NONBLOCK; fcntl(sockfd, F_SETFL, flags); #endif struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd); // getchar(); }
代码运行结果:
可以发现失败了,linux下正数表示成功,-1表示失败。而如果在运行到accept之前点击了连接,就会连接成功。此处从3开始,是因为0是标准输入,1是标准输出,2是错误。
-
上面只实现了网络io的连接,那么接下来考虑服务端如何接收数据。使用recv,recv返回0表示断开连接
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> #define BUFFER_LENGTH 1024 int main() { //open 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; 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); struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd); 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); // getchar(); }
使用第三方的网络助手工具尝试连接该端程序,代码运行结果如下:
通过上图可以发现recv也是阻塞的,只会阻塞一次,send 也是一次**。**
-
思考:那么放入到while中是否可以发送多次数据呢?代码如下:
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> #define BUFFER_LENGTH 1024 int main() { //open 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; 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); struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd); while (1) { //slave char buffer[BUFFER_LENGTH] = {0}; int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0); if (ret == 0) { close(clientfd); break; } printf("ret: %d, buffer: %s\n", ret, buffer); send(clientfd, buffer, ret, 0); } // getchar(); }
代码运行结果如下:
可以发现支持数据的多次接收。那么这段代码是否可以接收多个客户端的请求呢。实例如下:
可以发现无法处理第二个客户端的请求,思考后,这是因为accept只有一次,那么将accept放入到while中是否可以呢?
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> #define BUFFER_LENGTH 1024 int main() { //open 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; 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); struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); while (1) { //slave int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd); char buffer[BUFFER_LENGTH] = {0}; int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0); if (ret == 0) { close(clientfd); break; } printf("ret: %d, buffer: %s\n", ret, buffer); send(clientfd, buffer, ret, 0); } // getchar(); }
代码运行结果:
可以发现只能发送一次,这是因为第二次发送的时候调用了accept,此时没有连接,会被阻塞住。那么为了在while中即调用accept,又可以调用recv,所以考虑使用多线程实现。代码如下所示。该段代码编译需要使用gcc -o xxx xxx.c -lpthread.
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> #include <pthread.h> #define BUFFER_LENGTH 1024 void *client_thread(void *arg) { int clientfd = *(int*)arg; while (1) { //slave char buffer[BUFFER_LENGTH] = {0}; int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0); if (ret == 0) { close(clientfd); break; } printf("ret: %d, buffer: %s\n", ret, buffer); send(clientfd, buffer, ret, 0); } } int main() { //open 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; 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); struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); while (1) { //slave int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd); pthread_t threadid; pthread_create(&threadid, NULL, client_thread, &clientfd); } // getchar(); }
代码运行结果:
从上图结果可以发现可以达到我们的预期目标。但是在客户端多的时候,比如有10000个客户端,无法创建10000个线程。那么如何处理这个问题。此时就需要用到本章的重点知识,网络io多路复用技术,对多个服务进行管理。如select, pool, epool,kqueue(mac)等。
第16.2节 select
16.2.1 介绍
网络IO复用是指在单线程或少数线程的情况下,通过一种机制同时监控多个IO流的状态,当某个IO流有数据到达时,就通知相应的线程进行处理。其中,select是一种比较常用的IO多路复用技术,它可以同时监控多个文件描述符,当某个文件描述符就绪(一般是读就绪或写就绪)时,就会通知应用程序进行相应的操作。
16.2.2 代码案例
-
代码
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> #include <sys/select.h> #define BUFFER_LENGTH 1024 int main() { //open 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; 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); struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); fd_set rfds, rset; FD_ZERO(&rfds); FD_SET(sockfd, &rfds); int maxfd = sockfd; int clientfd = 0; while (1) { rset = rfds; 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(maxfd, &rfds, &wfds, efds, timeout)
maxfd - 表示的是所有的accept连接中返回值最大的id,
rfds - 可读的集合,记录了可以读的io集合
wfds - 可写的集合,记录了可以写的io集合
efds - 出错的集合,记录了上次出错的io集合
timeout - 表示多久轮询上面三个集合一次
返回值 - 表示当前有多少个io连接
-
fd_set rfds: fd_set内部是按照bit位来的
-
FD_ZERO(&rfds): 是设置fd_set中的每一个bit位为0
-
FD_SET(x, &rfds):将rfds中的dix位置为1
-
FD_ISSET(socked, &rfds):表示查询rfds的第socked位是否为1
-
-
关于send是否可以写的问题,是应用将需要发送的数据放入到内核的sendbuffer中。通常而言都是可以send成功的,只有在循环send或sendbuffer()非常小的时候才会失败,这个配置可以在sysconfig文件中修改,所以send是否可以写是需要判断的。
16.2.3 缺陷
- 由于select的fd_set需要通过select先传入到内核,再从内核传出来,所以会很消耗性能。
- 在select中,一个fd_set是128个bit位,如果io的数量超过128个后,就会出现资源不够。
- 在内核中会循环遍历,这也会比较耗时。
第16.3节 poll
16.3.1 介绍
poll是一种常见的IO多路复用技术,它可以同时监视多个文件描述符,当其中任意一个文件描述符就绪时,就会通知应用程序进行相应的操作。
16.3.2 代码案例
-
代码
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> #include <sys/poll.h> #define BUFFER_LENGTH 1024 #define POLL_SIZE 1024 int main() { //open 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; 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); 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; // 需要将fd置为无效才行。 fds[i].events = 0; close(i); break; } printf("ret: %d, buffer: %s\n", ret, buffer); send(i, buffer, ret, 0); } } } // getchar(); }
-
代码说明
结构中的events是我们传入到内核中的可读的项,而revents是从内核中反馈出来的。
cppstruct poolfd { int fd; short events; short revent; }
16.3.3 相对select的优缺点
- 与select相比,poll没有最大文件描述符数量的限制,因此可以处理更多的并发连接。poll的使用方法与select类似,但是poll的效率比select更高,因为它不需要遍历整个文件描述符集合,而是只需要遍历就绪的文件描述符集合。
- pool只有一个数组
- 接口简单只有一个
第16.4节 epoll
16.4.1 介绍
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。epoll可以同时处理大量的文件描述符,是基于事件驱动的IO操作方式,可以取代select和poll函数。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。另外,epoll使用红黑树存储管理事件,每次插入和删除事件的效率都是O(logn)的,其中n是红黑树中节点的个数。
16.4.2 代码案例
-
代码
cpp#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> #include <sys/epoll.h> #define BUFFER_LENGTH 1024 int main() { //open 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; 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); struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int epfd = epoll_create(1);//1000 //list struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = sockfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // struct epoll_event events[1024] = {0}; while (1) { // mainloop int nready = epoll_wait(epfd, events, 1024, -1); //-1, 0, if (nready < 0) continue; int i = 0; for (i = 0;i < nready;i ++) { int connfd = events[i].data.fd; if (sockfd == connfd) { // accept int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); if (clientfd <= 0) { continue; } printf(" clientfd: %d\n", clientfd); ev.events = EPOLLIN | EPOLLET; ev.data.fd = clientfd; epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev); } else if (events[i].events & EPOLLIN) { char buffer[10] = {0}; short len = 0; recv(connfd, &len, 2, 0); len = ntohs(len); int n = recv(connfd, buffer, 10, 0); if (n > 0) { printf("recv : %s\n", buffer); send(connfd, buffer, n, 0); } else if (n == 0) { printf("close\n"); epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL); close(connfd); } } } } // getchar(); }
运行结果
16.4.3 相对select,pool的优点
- 可以处理大量请求
- 底层使用红黑树实现,效率更高
补充:
- io的数量意味着什么?意味着并发