2.1.1 网络io与io多路复用select/poll/epoll1. 搭建一个简单的服务端#include stdio.h #include stdlib.h #include sys/socket.h #include netinet/in.h #include string.h #include errno.h int main() { // 1. create socket int listenFd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serverAddr; serverAddr.sin_family AF_INET; serverAddr.sin_addr.s_addr htonl(INADDR_ANY); serverAddr.sin_port htons(2000); // 2. bind socket int flag bind(listenFd, (struct sockaddr *)serverAddr, sizeof(serverAddr)); if (-1 flag) { printf(bind error: %s\n, strerror(errno)); return -1; } // 3. listen listen(listenFd, 10); struct sockaddr_in clientAddr; int len sizeof(clientAddr); int clientFd; char buff[1024]; while(1) { memset(buff, 0, sizeof(buff)); // 4. accept client connection clientFd accept(listenFd,(struct sockaddr*)clientAddr,len); // 5. recv data from client recv(clientFd, buff, sizeof(buff), 0); printf(recv data: %s\n, buff); // 6. send data to client memset(buff, 0, sizeof(buff)); sprintf(buff, hello client, I have received your data); send(clientFd, buff, strlen(buff), 0); close(clientFd); } close(listenFd); return 0; }上述代码可以循环的接收客户端连接并且每一个客户端拥有一次和服务端收发数据的过程。但是有一个致命的确定比如client1 client2 client3依此连接到服务端先连接不发数据此时会出现什么情况呢如果我client3 client2 client1依此给server发消息但只有当client1发出消息之后client3和client2才会收到消息。分析原因client1 client2 client3依此连接到服务端时程序阻塞在recv上那么其他的client只有三次握手成功并且阻塞在TCP队列上并不能建立tcp连接。2. 改用多线程的方式#include stdio.h #include stdlib.h #include sys/socket.h #include netinet/in.h #include string.h #include errno.h #include bits/pthreadtypes.h void handle_task(void* arg) { int clientFd *(int*)arg; char buff[1024]; memset(buff, 0, sizeof(buff)); while(1) { memset(buff, 0, sizeof(buff)); int count recv(clientFd, buff, sizeof(buff), 0); printf(recv data: %s\n, buff); if(0 count) { printf(client closed connection\n); break; } memset(buff, 0, sizeof(buff)); sprintf(buff, hello client, I have received your data); send(clientFd, buff, strlen(buff), 0); } close(clientFd); } int main() { // 1. create socket int listenFd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serverAddr; serverAddr.sin_family AF_INET; serverAddr.sin_addr.s_addr htonl(INADDR_ANY); serverAddr.sin_port htons(2000); // 2. bind socket int flag bind(listenFd, (struct sockaddr *)serverAddr, sizeof(serverAddr)); if (-1 flag) { printf(bind error: %s\n, strerror(errno)); return -1; } // 3. listen listen(listenFd, 10); struct sockaddr_in clientAddr; int len sizeof(clientAddr); int clientFd; char buff[1024]; static int count 0; #if 0 while(1) { memset(buff, 0, sizeof(buff)); // 4. accept client connection clientFd accept(listenFd,(struct sockaddr*)clientAddr,len); printf(accept client connection, count: %d\n, count); // 5. recv data from client recv(clientFd, buff, sizeof(buff), 0); printf(recv data: %s\n, buff); // 6. send data to client memset(buff, 0, sizeof(buff)); sprintf(buff, hello client, I have received your data); send(clientFd, buff, strlen(buff), 0); close(clientFd); } #elif 1 // with multi thread while(1) { // 4. accept client connection clientFd accept(listenFd,(struct sockaddr*)clientAddr,len); pthread_t thid; pthread_create(thid, NULL, (void *)handle_task, (void *)clientFd); } #endif close(listenFd); return 0; }上述代码在不断开tcp连接时候可以一直进行通信。弊端1.如果tcp连接“占着茅坑不拉屎”那么是对server资源的一种浪费。这里要明确一个概念网络IO和Tcp连接。① 只要完成3次握手即可建立tcp连接但是只有通过accept获得fd才能进行IO通信 ② accept有一个建立连接的队列accept是从队列中获取fd的如果fd在队列里面那么说明三次握手成功 建立tcp连接如果队列满了那么无法三次握手成功只能等着。3. select的方式I/O多路复用多路复用的意思就是在一个线程中既能够处理监听事件又能处理i/o事件#include stdio.h #include stdlib.h #include sys/socket.h #include netinet/in.h #include string.h #include errno.h #include bits/pthreadtypes.h #include unistd.h #include sys/select.h #define PORT 2001 int main() { // 1. create socket int listenFd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serverAddr; serverAddr.sin_family AF_INET; serverAddr.sin_addr.s_addr htonl(INADDR_ANY); serverAddr.sin_port htons(PORT); // 2. bind socket int flag bind(listenFd, (struct sockaddr *)serverAddr, sizeof(serverAddr)); if (-1 flag) { printf(bind error: %s\n, strerror(errno)); return -1; } // 3. listen listen(listenFd, 10); struct sockaddr_in clientAddr; int len sizeof(clientAddr); char buff[1024]; // 定义两个文件描述符集合rfds是总的rset是临时的 fd_set rfds, rset; FD_ZERO(rfds); FD_SET(listenFd, rfds); // 将listenFd放到文件描述符集合中 int maxFd listenFd; while (1) { rset rfds; /* param1: select要检查的范围由于从0开始所以 1 param2: 监听read event param3监听write event param4监听exception event param4是否设置超时时间 */ int nready select(maxFd 1, rset, NULL, NULL, NULL); // 检查是否是listenFd是可读状态如果可读则说明有新的连接进来了 if(FD_ISSET(listenFd, rset)) { int clientFd accept(listenFd, (struct sockaddr*)clientAddr, len); printf(accept finshed: %d\n, clientFd); // 将新连接加到fd集合中 FD_SET(clientFd, rfds); if(clientFd maxFd) maxFd clientFd; } // 遍历除了监听集合外的fd是否是可读状态 for (size_t i listenFd 1; i maxFd 1; i) { if(FD_ISSET(i, rset)) { printf(%ld:\n,i); char buffer[1024] {0}; int count recv(i, buffer, 1024, 0); printf(buffer:%s\n,buffer); if (count 0) // disconnect { printf(client disconnect: %ld\n, i); close(i); FD_CLR(i, rfds); continue; } } } } close(listenFd); return 0; }注① FD_SET(fd,rfds)fd_set是一个bit位的数组比如bit arr[1024]就是每一个格子存一个bit位fd是几久存到对应格子的第几位比如fd是45则在arr[45]② 为啥select返回之后会有一个if和一个for循环呢如果select返回了有可能是listenFd被激活了也有可能是被监听的连接active了那么select返回了则就存在两种情况也可能两种情况同时存在。所以第一个if是判断是否是listenFd被激活被激活说明有新的连接要将新连接放到rfds中等待被监听然后for循环就是遍历剩下被监听的clientFd是否有数据来了。③ select中的第一个参数必须是maxFd 1不然监听不到linstenFd因为fd是从0开始的。4. poll的方式I/O多路复用#include stdio.h #include stdlib.h #include sys/socket.h #include netinet/in.h #include string.h #include errno.h #include bits/pthreadtypes.h #include unistd.h #include sys/select.h #include poll.h #define PORT 2001 int main() { // 1. create socket int listenFd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serverAddr; serverAddr.sin_family AF_INET; serverAddr.sin_addr.s_addr htonl(INADDR_ANY); serverAddr.sin_port htons(PORT); // 2. bind socket int flag bind(listenFd, (struct sockaddr *)serverAddr, sizeof(serverAddr)); if (-1 flag) { printf(bind error: %s\n, strerror(errno)); return -1; } // 3. listen listen(listenFd, 10); struct sockaddr_in clientAddr; int len sizeof(clientAddr); char buff[1024]; struct pollfd fds[1024] { 0 }; fds[listenFd].fd listenFd; fds[listenFd].events POLLIN; int maxFd listenFd; while ( 1 ) { int nReady poll(fds, maxFd 1, -1); if(fds[listenFd].revents POLLIN) { // listenFd可读将新的fd加入到监听中 printf(have new connect...\n); int clientFd accept(listenFd,(struct sockaddr*)clientAddr,len); fds[clientFd].fd clientFd; fds[clientFd].events POLLIN; if(clientFd maxFd) maxFd clientFd; } for (size_t i listenFd 1; i maxFd 1; i) { if(fds[i].revents POLLIN) { // clientFd可读 char buf[1024] { 0 }; int count recv(i, buf, sizeof(buf), 0); if(count 0) { // 关闭clientFd并移出监听 printf(client disconnect: %ld\n, i); close(i); fds[i].fd -1; fds[i].events 0; continue; } else { printf(收到的消息:%s\n,buf); } } } } close(listenFd); return 0; }注① 这里的event有多种状态POLLIN表示监听读事件这里为啥要用fds[clientFd].revents和POLLIN做运算呢因为revents有多种状态有可读可写异常等等那么POLLIN是0X0001如果revents的最后一位是被触发状态那么说明该event是可读状态。总结select和poll① select和poll都是循环遍历的做法在select或者poll的时候会将整个数组从用户空间拷贝到内核空间进行遍历如果被触发则从内核空间拷贝出来② 适合io较少的时候使用③ select在linux2.4之前因为没有poll和epoll所以有poll用poll没有poll用select5. epoll的方式I/O多路复用#include stdio.h #include stdlib.h #include sys/socket.h #include netinet/in.h #include string.h #include errno.h #include bits/pthreadtypes.h #include unistd.h #include sys/select.h #include poll.h #include sys/epoll.h #define PORT 2001 int main() { // 1. create socket int listenFd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serverAddr; serverAddr.sin_family AF_INET; serverAddr.sin_addr.s_addr htonl(INADDR_ANY); serverAddr.sin_port htons(PORT); // 2. bind socket int flag bind(listenFd, (struct sockaddr *)serverAddr, sizeof(serverAddr)); if (-1 flag) { printf(bind error: %s\n, strerror(errno)); return -1; } // 3. listen listen(listenFd, 10); struct sockaddr_in clientAddr; int len sizeof(clientAddr); char buff[1024]; // 1是参数,兼容老版本系统遗留问题,但参数必须大于0 int epfd epoll_create(1); struct epoll_event ev; ev.data.fd listenFd; ev.events EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, listenFd, ev); while ( 1 ) { /* code */ struct epoll_event events[1024] { 0 }; int nReady epoll_wait(epfd, events, 1024, -1); for (size_t i 0; i nReady; i) { /* code */ if(events[i].data.fd listenFd) { int clientFd accept(listenFd,(struct sockaddr*)clientAddr,len); struct epoll_event clientEv; clientEv.data.fd clientFd; clientEv.events EPOLLIN; printf(accept finshed: %d\n, clientFd); epoll_ctl(epfd, EPOLL_CTL_ADD, clientFd, clientEv); continue; } else if(events[i].events EPOLLIN) { char buffer[1024] {0}; int count recv(events[i].data.fd, buffer, 1024, 0); if(count 0) { printf(client disconnect: %d\n, events[i].data.fd); epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL); close(events[i].data.fd); continue; } printf(recv: %s\n, buffer); count send(events[i].data.fd, buffer, count, 0); printf(send: %d\n, count); } } } close(listenFd); return 0; }注① epoll底层是通过红黑树 双向链表构成的首先红黑树监听事件如果有事件被激活双向链表指向被激活的节点上去然后将链表中数据拷贝到用户空间中epoll_wait返回值nReady表示拷贝的数量。总结select、poll、epollselect底层用的fd_set位图数组来管理是否有事件处于被激活状态通过FD_ISSET来查看是否被激活通过FD_SET来增加监听的fd通过FD_CLR来删除被监听的事件。位图大小是系统固定一般是1024大小意思是被监听的数组大小最多只有1024是有限的。poll用户自己创建struct pollfd fds[] 数组来做的通过fds[i].revent POLLIN来判断是否是被触发可读状态通过fds[i].data.fd clientFdfds[i].eventEPOLLIN来设置监听的事件通过fds[i].data.fd -1fds[i].event0来取消。被监听的数量按理论是和硬件相关的没有个数限制。epollepoll底层是通过红黑树 双向链表构成的首先红黑树监听事件如果有事件被激活双向链表指向被激活的节点上去然后将链表中数据拷贝到用户空间中。首先创建一个红黑树epoll_create(unsiged int i)返回值是一个int epfdepfd相当于通过这个文件描述符去操控红黑树增加一个监听事件epoll_ctl(epfd, EPOLL_CTL_ADD, clientFd, clientEv);删除一个监听事件epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);6. reactor百万并发