poll实现的网络聊天室
引言本篇文章实现一个简单的网络聊天室代码参考《Linux高性能服务编程》里面涉及到了一些比较好的思想给我们之后的学习提供了比较好的一些思路比如一些零拷贝的思想。。。完整的代码已经放在了github上面fengyue05/Linux-: 本仓库里面会有关于《Linux高性能服务器编程》的一些主要代码实现其实在之前我们也写过一个网络聊天室但是思路和这个完全不一样相当于这个是第二遍写网络聊天室这一次自己的体会也是不一样的。一下是我关于第一次写网络聊天室的博客写文章-CSDN创作中心写文章-CSDN创作中心客户端我们这里用poll注册了两个文件描述符第一个就是连接着终端的标准输入第二个就是我们连接服务器的文件描述符我们也就是通过这个文件描述符和服务器进行沟通。我们要把终端的内容传输到服务器的地方需要进行数据的传输所以如果我们使用传统的方式我们必须要开辟一个内存然后readwrite这样子不仅仅要拷贝而且还占用内存十分耗费性能所以我们这里创建了一个管道方便我们用splice进行零拷贝的操作。在while循环里面我们需要判断两个文件描述符因为一个客户端既要发送消息又要接受消息如果是接受消息就是fds[1]那么我们就是接受消息也就是利用recv如果是发送消息就是连接终端的fds[0]然后通过管道和splice的方式先把终端的数据拷贝到管道里面然后从管道里面读到socket里面。int main(int argc, char const *argv[]) { if (argc 2) { return -1; } const char* ip argv[1]; int port atoi(argv[2]); sockaddr_in server_address; bzero(server_address, sizeof(server_address)); server_address.sin_family AF_INET; server_address.sin_port htons(port); int sockfd socket(AF_INET, SOCK_STREAM, 0); assert(sockfd 0); if (connect(sockfd, (sockaddr*) server_address, sizeof(server_address)) 0) { close(sockfd); return -1; } pollfd fds[2]; // 注入文件描述符0标准输入和文件描述符sockfd上的可读事件 fds[0].fd 0; fds[0].events POLLIN; fds[0].revents 0; fds[1].fd sockfd; fds[1].events POLLIN | POLLRDHUP; // 只可以读不可以写 fds[1].revents 0; char read_buf[BUFFER_SIZE]; int pipefd[2]; int ret pipe(pipefd); assert(ret ! -1); while(1) { ret poll(fds, 2, -1); if (ret 0) { break; } else if (fds[1].revents POLLIN) { memset(read_buf, \0, BUFFER_SIZE); recv(fds[1].fd, read_buf, BUFFER_SIZE - 1, 0); std::cout read_buf std::endl; } else if (fds[1].revents POLLRDHUP) { std::cout server close the connection std::endl; break; } if (fds[0].revents POLLIN) { ret splice(0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE); ret splice(pipefd[0], NULL, fds[1].fd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE); } } close(sockfd); return 0; }服务器服务器需要接受客户的数据那么我们需要记录客户的地址还有用读缓冲区来接受客户的数据如果客户要写一些东西还需要一个写的缓冲区。struct client_data { sockaddr_in address; char* write_buf; char buf[BUFFER_SIZE]; };然后我们这里一般都是用的非阻塞这个就是对一个文件描述符设置非阻塞的方式但是我们最后返回的是原本的文件描述符是因为方便我们之后可能会还原文件描述符int setNoBlocking (int fd) { int old_option fcntl(fd, F_GETFL); int new_option old_option | O_NONBLOCK; fcntl(fd, F_SETFL, new_option); return old_option; }一下是一些常规的操作int main(int argc, char const *argv[]) { if (argc 3) { return -1; } const char* ip argv[1]; int port atoi(argv[2]); sockaddr_in address; bzero(address, sizeof(address)); address.sin_family AF_INET; address.sin_port htons(port); inet_pton(AF_INET, ip, address.sin_addr); int sockfd ::socket(AF_INET, SOCK_STREAM, 0); assert(sockfd 0); int ret bind(sockfd, (sockaddr*)address, sizeof(address)); assert(ret ! -1); ret listen(sockfd, 128); assert(ret ! -1);我们为了保证服务器的高效性一次不可以操作过多的的用户所以必须有一个用户的限制但是对于连接用户来说我们的限制可以比较大可以存储65536个文件描述符。所以现在情况分成了两种一种是来消息的用户一种是仅仅连接但是没有事情发生的文件描述符。fds就是存储来消息的用户而users就是存储所有连接的文件描述符这样子当有连接到来的时候我们只需要从users取出对应的文件描述符就可以知道其events和revents。#define USER_LIMIT 5 // 最大用户的限制 #define BUFFER_SIZE 64 #define FD_LIMIT 65536 // 文件描述符的限制 /* 创建users数组分配FD_LIMIT个client_data对象。可以预期每个可能的socket连接都可以获得这么一个对象 并且socket的值可以直接用来索引作为数组的下标socket连接对应的client_data对象这是将socket和客户数据关联的简单而高效的方式 */ client_data* users new client_data[FD_LIMIT]; // 尽管我们分配了足够多的client_data但是为了提高效率我们还是要限制用户的数量 pollfd fds[USER_LIMIT 1]; int user_counter 0; // 一般把0留给监听的socket for (int i 1; i USER_LIMIT; i) { fds[i].fd -1; fds[i].events 0; } fds[0].fd sockfd; fds[0].events POLLIN | POLLERR; fds[0].revents 0;首先最先到来的事件一定是连接所以一开始我们的user_counter设置为0其实也是默认了监听连接的socket的文件描述符是0而poll里面的user_counter 1就是因为我们设置的文件描述符是从0开始的包含监听的socket然后我们就开始循环遍历发生的事情那么我们就要对事情分类第一个是监听的socket对于这个socket就是有客户端连接到来里面的操作就是建立与客户端的连接然后判断一下目前发生事件的数量是不是超过了我们的限制如果超过了就发送一个消息给客户端并断开连接如果没有超过连接的数量我们就更新存储的fdsusers然后就是已经建立连接的socket如果是连接断开了那么服务器也应该断开并且在users里面删除对应的数据还有user_counter要-1因为说明可以再来一个用户了。不过这里重要的就是删除数据因为我们的poll是告诉内核前 user_counter 1 个元素都是有效的但是如果中间有一个数据是空的就会出错所以传统的做法就是往前移动一格因为删除了一个就要有新的一个连接进来所以最后一个必须空出来所以必须移动而这样子时间复杂度就是On。所以我们采取的做法就是把最后一个文件描述符放在删除的地方然后i--因为我们现在i的位置是一个新的文件描述符。这样子就完美的解决了移动的问题啦~~~如果接受到了数据我们聊天服务器的作用就是通过广播的方式发送给其他的客户端所以我们还需要一个for来遍历发生事件的文件描述符同时把读事件关闭防止下一次poll到来的又是读事件把写事件打开只要缓冲区有写的空间就不停的发出信号LT那写的内容就是把读的缓冲区数据拷贝到写的缓冲区。最后就是写的事件也就是我们要把这些写缓冲区里面的数据全部send到对应的客户端里当发送完了之后我们把读事件打开把写事件关闭否则会一直提醒poll缓冲区里面有数据。这个在muduo网络库里面也有涉及写完一定要把写事件关闭特别是在LT模式下。while (1) { ret poll(fds, user_counter 1, -1); if (ret 0) { break; } for (int i 0; i user_counter 1; i) { if ((fds[i].fd sockfd) (fds[i].revents EPOLLIN)) { sockaddr_in client_address; bzero(client_address, sizeof(client_address)); socklen_t client_len sizeof(client_address); int connfd accept(sockfd, (sockaddr*) client_address, client_len); if (connfd 0) { continue; } // 如果请求过多那么就关闭新到的连接 if (user_counter USER_LIMIT) { const char* info too many users\n; std::cout info; send(connfd, info, strlen(info), 0); close(connfd); continue; } // 对于新的连接同时修改fds和users数组。前文已经提到了users[connfd]对应线连接的客户数据 user_counter; users[connfd].address client_address; setNoBlocking(connfd); fds[user_counter].fd connfd; fds[user_counter].events POLLIN | POLLRDHUP | POLLERR; fds[user_counter].revents 0; } else if (fds[i].revents POLLERR) { char errors[100]; memset(errors, \0, sizeof(errors)); socklen_t length sizeof(length); if (getsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, errors, length) 0) { std::cout get socket option failed std::endl; } continue; } else if (fds[i].revents POLLRDHUP) { // 则客户端关闭连接则服务器也关闭对应的连接并将其用户数量-1 users[fds[i].fd] users[fds[user_counter].fd]; // 把最后一个文件描述符的数据复制到断开连接的地方 close(fds[i].fd); fds[i] fds[user_counter]; // 同理也是把最后一个放到我们删除的地方来 i--; // 这个是因为我们现在的i是一个新的文件描述符所以我们需要重新遍历 user_counter--; std::cout a client left std::endl; } else if (fds[i].revents POLLIN) { int connfd fds[i].fd; memset(users[connfd].buf, \0, BUFFER_SIZE); ret recv (connfd, users[connfd].buf, BUFFER_SIZE - 1, 0); std::cout users[connfd].buf std::endl; if (ret 0) { // 如果读的操作出现了错误则关闭连接 if (errno ! EAGAIN) { close(connfd); users[fds[i].fd] users[fds[user_counter].fd]; fds[i] fds[user_counter]; i--; user_counter--; } } else if (ret 0) { } else { // 如果接收到客户的数据则通知其他的socket连接准备写数据因为我们这里实现的是广播聊天 for (int j 1; j user_counter; j) { if (fds[j].fd connfd) { // 这个就是poll的缺点要遍历才知道我们具体操作的是哪一个fd continue; } fds[j].events | ~POLLIN; fds[j].events | POLLOUT; users[fds[j].fd].write_buf users[connfd].buf; } } } else if (fds[i].revents POLLOUT) { int connfd fds[i].fd; if (!users[connfd].write_buf) { continue; } ret send (connfd, users[connfd].write_buf, strlen(users[connfd].write_buf), 0); users[connfd].write_buf NULL; // 写完数据之后就要重新注册fds[i]上的可读事件 fds[i].events | POLLIN; fds[i].events | ~POLLOUT; } } }总结本篇文章到这里就结束了希望可以帮助大家理解~~~