本文共 4276 字,大约阅读时间需要 14 分钟。
在学习网络编程时,select机制是一个非常重要的工具。它允许多个客户端同时与服务器通信,而不像传统的accept机制那样只能处理一个连接。这一机制通过IO复用(select、poll、epoll)与内核合作,显著提升了服务器的性能和处理能力。
select函数的作用是监控多个文件描述符(套接字)的读写事件。它通过设置超时返回,避免阻塞,能够高效地处理大量客户端连接。以下是select的关键点:
我们编写一个简单的select服务器示例,功能是接收客户端连接并转换大写字母。以下是代码的主要部分:
#include#include #include #include #include #include #include #include #include #define SERV_PORT 6666 void perr_exit(const char *s) { perror(s); exit(-1); } int accept(int fd, struct sockaddr *sa, socklen_t *salen) { int n; again: if ((n = accept(fd, sa, salen)) < 0) { if ((errno == ECONNABORTED) || (errno == EINTR)) goto again; else perr_exit("accept error"); } return n; } int main() { int nready, i, j, n; int maxfd, lfd, cfd; socklen_t clie_addr_len; char buf[BUFSIZ]; fd_set rset, allset; lfd = socket(AF_INET, SOCK_STREAM, 0); if (lfd == -1) { perr_exit("socket failed."); } struct sockaddr_in serv_addr; bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_PORT); if (bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) { perr_exit("bind failed."); } if (listen(lfd, 128) == -1) { perr_exit("listen failed."); } FD_ZERO(&allset); FD_SET(lfd, &allset); maxfd = lfd; while (1) { rset = allset; nready = select(maxfd + 1, &rset, NULL, NULL, NULL); if (nready < 0) { perr_exit("select error."); } if (FD_ISSET(lfd, &rset)) { cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len); FD_SET(cfd, &allset); if (cfd > maxfd) { maxfd = cfd; } } if (nready == 1) { continue; } for (i = lfd + 1; i <= maxfd; i++) { if (FD_ISSET(i, &rset)) { if ((n = read(i, buf, sizeof(buf))) == 0) { close(i); FD_CLR(i, &allset); } else if (n > 0) { for (j = 0; j < n; j++) { buf[j] = toupper(buf[j]); } write(i, buf, n); write(STDOUT_FILENO, buf, n); } } } } close(lfd); return 0; }
select的优点:跨平台支持,兼容性好。缺点:监听文件描述符的数量受限(最大1024),处理效率较低,代码复杂度较高。
poll的优点:支持更多文件描述符,事件分离更灵活。缺点:仅在Linux/Unix系统上支持,开发门槛较高。
epoll的优点:效率更高,代码简洁。缺点:仅在Linux/Unix系统上支持,跨平台性差。
虽然select本身效率不错,但传统的实现方式存在一定局限性。通过引入文件描述符数组,可以优化事件处理逻辑,但实际效果有限。以下是优化后的代码示例:
#include#include #include #include #include #include #include #include #include #define SERV_PORT 6666 void perr_exit(const char *s) { perror(s); exit(-1); } int main() { int nready, i, j, n; int maxfd, lfd, cfd, tmpfd; int maxi; socklen_t clie_addr_len; char buf[BUFSIZ]; char *client = malloc(1024); FD_ZERO(&allset); FD_SET(lfd, &allset); maxfd = lfd; int opt = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in serv_addr; bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_PORT); if (bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) { perr_exit("bind failed."); } if (listen(lfd, 128) == -1) { perr_exit("listen failed."); } while (1) { rset = allset; nready = select(maxfd + 1, &rset, NULL, NULL, NULL); if (nready < 0) { perr_exit("select error."); } if (FD_ISSET(lfd, &rset)) { clie_addr_len = sizeof(clie_addr); cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len); FD_SET(cfd, &allset); if (cfd > maxfd) { maxfd = cfd; } for (i = 0; i < 1024; i++) { if (client[i] == -1) { client[i] = cfd; break; } } maxi = i - 1; if (nready == 1) { continue; } } if (nready == 1) { continue; } for (i = 0; i <= maxi; i++) { if (client[i] == -1) { continue; } if (FD_ISSET(client[i], &rset)) { if ((n = read(client[i], buf, sizeof(buf))) == 0) { close(client[i]); FD_CLR(client[i], &allset); client[i] = -1; } else if (n > 0) { for (j = 0; j < n; j++) { buf[j] = toupper(buf[j]); } write(client[i], buf, n); write(STDOUT_FILENO, buf, n); } } if (--nready == 0) { break; } } } close(lfd); return 0; }
通过测试可以发现,当使用select实现服务器时,能够支持多个客户端同时连接并通信。例如,使用nc模拟多个客户端连接到服务器,服务器可以正确接收和转换客户端发送的数据。这种实现方式有效地解决了传统accept机制的效率问题,展示了select机制的强大之处。
转载地址:http://hpfv.baihongyu.com/