引言
本篇文章实现一个简单的网络聊天室,代码参考《Linux高性能服务编程》,里面涉及到了一些比较好的思想,给我们之后的学习提供了比较好的一些思路,比如一些零拷贝的思想。。。
完整的代码已经放在了github上面:
fengyue05/Linux-: 本仓库里面会有关于《Linux高性能服务器编程》的一些主要代码实现
其实在之前我们也写过一个网络聊天室,但是思路和这个完全不一样,相当于这个是第二遍写网络聊天室,这一次自己的体会也是不一样的。一下是我关于第一次写网络聊天室的博客:
写文章-CSDN创作中心
写文章-CSDN创作中心
客户端
我们这里用poll注册了两个文件描述符,第一个就是连接着终端的标准输入,第二个就是我们连接服务器的文件描述符,我们也就是通过这个文件描述符和服务器进行沟通。
我们要把终端的内容传输到服务器的地方需要进行数据的传输,所以如果我们使用传统的方式我们必须要开辟一个内存,然后read(),write(),这样子不仅仅要拷贝,而且还占用内存,十分耗费性能,所以我们这里创建了一个管道,方便我们用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,就是有客户端连接到来,里面的操作就是建立与客户端的连接,然后判断一下,目前发生事件的数量是不是超过了我们的限制,如果超过了就发送一个消息给客户端并断开连接;如果没有超过连接的数量,我们就更新存储的fds,users
然后就是已经建立连接的socket,如果是连接断开了,那么服务器也应该断开,并且在users里面删除对应的数据,还有user_counter要-1,因为说明可以再来一个用户了。不过这里重要的就是删除数据,因为我们的poll是告诉内核前 user_counter + 1 个元素都是有效的,但是如果中间有一个数据是空的就会出错,所以传统的做法就是往前移动一格(因为删除了一个就要有新的一个连接进来,所以最后一个必须空出来,所以必须移动),而这样子时间复杂度就是O(n)。所以我们采取的做法就是把最后一个文件描述符放在删除的地方,然后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; } } }总结
本篇文章到这里就结束了!!!!希望可以帮助大家理解~~~