在上篇文章中已经介绍过I/O多路复用和其他IO模型的区别:网络编程中的I/O多路复用 - I/O Models
实现I/O多路复用的方式主要有select
和poll
两种方式
Select()
下面是使用select
实现的I/O多路复用,Server开启了一个线程用来对所有的socket描述符进行select,下面是一个使用selec()
实现I/O多路复用的例子:
对应的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| static int g_svfd; std::vector<int> g_fds; pthread_t select_thread;
void select_servermsg(void) { pthread_attr_t attr; (void)pthread_attr_init(&attr); pthread_create(&select_thread, &attr, server_select, NULL); pthread_attr_destroy(&attr); }
int accept_client_conn(void) { int cs = accept(g_svfd, NULL, NULL); if (cs < 0) { perror("accept"); return -1; } g_fds.push_back(cs); printf("Server Accept acc:%d\n", cs); return cs; }
void read_data_from_socket(int sockfd) { char recv_data[1024] = {0}; long rc = recv(sockfd, recv_data, sizeof(recv_data), 0); if (rc <= 0) { printf("Server Read Failed cs:%ld\n", rc); return; } printf("Server Socket acc:%d revc:%s \n", sockfd, recv_data); }
void mg_add_to_set(int sock, fd_set *set, int *max_fd) { if (sock != INVALID_SOCKET && sock < (int) FD_SETSIZE) { FD_SET(sock, set); if (*max_fd == INVALID_SOCKET || sock > *max_fd) { *max_fd = sock; } } }
void *server_select(void *udata) { g_selecting = true; while (g_selecting) { int milli = 1500; struct timeval tv; tv.tv_sec = milli / 1000; tv.tv_usec = (milli % 1000) * 1000; fd_set read_set, err_set; FD_ZERO(&read_set); FD_ZERO(&err_set); int max_fd = INVALID_SOCKET; for (auto sock = g_fds.begin(); sock != g_fds.end(); sock++) { mg_add_to_set(*sock, &read_set, &max_fd); mg_add_to_set(*sock, &err_set, &max_fd); } int num_selected = select((int) max_fd + 1, &read_set, NULL, &err_set, &tv); if (num_selected <= 0) { continue; } unsigned long size = g_fds.size(); for (int i = 0; i < size; i++) { int sock = g_fds[i]; if (FD_ISSET(sock, &read_set)) { if (sock == g_svfd) { accept_client_conn(); } else { read_data_from_socket(sock); } continue; } if (FD_ISSET(sock, &err_set)) { printf("select:%d error\n", sock); continue; } } } return NULL; }
|
上面select
使用的FD_SET
和FD_ISSET
定义如下:
1 2 3 4 5 6 7
| #ifndef FD_ISSET #define FD_ISSET(n, p) __DARWIN_FD_ISSET(n, p) #endif
#ifndef FD_SET #define FD_SET(n, p) __DARWIN_FD_SET(n, p) #endif
|
在iOS系统中,上面的__DARWIN_FD_ISSET
和__DARWIN_FD_SET
定义在_fd_def.h中,文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #define __DARWIN_FD_SETSIZE 1024 #define __DARWIN_NBBY 8 #define __DARWIN_NFDBITS (sizeof(__int32_t) * __DARWIN_NBBY) #define __DARWIN_howmany(x, y) ((((x) % (y)) == 0) ? ((x) / (y)) : (((x) / (y)) + 1))
typedef struct fd_set { __int32_t fds_bits[__DARWIN_howmany(__DARWIN_FD_SETSIZE, __DARWIN_NFDBITS)]; } fd_set;
int __darwin_fd_isset(int _fd, const struct fd_set *_p) { return _p->fds_bits[(unsigned long)_fd / __DARWIN_NFDBITS] & ((__int32_t)(((unsigned long)1) << ((unsigned long)_fd % __DARWIN_NFDBITS))); }
void __darwin_fd_set(int _fd, struct fd_set *const _p){ (_p->fds_bits[(unsigned long)_fd / __DARWIN_NFDBITS] |= ((__int32_t)(((unsigned long)1) << ((unsigned long)_fd % __DARWIN_NFDBITS)))); }
void __darwin_fd_clr(int _fd, struct fd_set *const _p) { (_p->fds_bits[(unsigned long)_fd / __DARWIN_NFDBITS] &= ~((__int32_t)(((unsigned long)1) << ((unsigned long)_fd % __DARWIN_NFDBITS)))); }
#define __DARWIN_FD_SET(n, p) __darwin_fd_set((n), (p)) #define __DARWIN_FD_CLR(n, p) __darwin_fd_clr((n), (p)) #define __DARWIN_FD_ISSET(n, p) __darwin_fd_isset((n), (p))
|
select采用的是位掩码的模型,其使用的fds_bits
是一个有32个int32值的数组,一共有32 * 32 = 1024 bit位,数组中的每一位代表一个文件句柄的掩码,这个fds_bits
只能支持fd < 1024的情况,当socket fd > 1024,就会出现无法处理的情况。
poll()
虽然select
和poll
都可以实现I/O的多路复用,但是大家更推崇poll(),因为poll是通过传入自定义数组pollfd的形式,对socket描述符并没有1024的限制。下面是使用poll的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| void *server_poll(void *udata) { g_polling = true; while (g_polling) { int32_t maxsock = 0; unsigned long fd_size = g_fds.size() * sizeof(pollfd); struct pollfd *pollfds = (struct pollfd *)malloc(fd_size); for (auto sock = g_fds.begin(); sock != g_fds.end(); sock++) { pollfds[maxsock].fd = *sock; pollfds[maxsock].events = POLLIN; ++maxsock; } int rc = poll(pollfds, maxsock, 10); if (rc < 0) { continue;; } for (int j = 0; j < maxsock; j++) { if (pollfds[j].revents & POLLIN) { int socket = pollfds[j] .fd; if (socket == g_svfd) { accept_client_conn(); } else { read_data_from_socket(socket); } } } } return NULL; }
void poll_servermsg(void) { pthread_attr_t attr; (void)pthread_attr_init(&attr); pthread_create(&poll_thread, &attr, server_poll, NULL); pthread_attr_destroy(&attr); }
|
the difference of select and poll
这里总结一下select和poll的不同:
1、select使用的是定长数组,而poll是通过用户自定义数组长度的形式(pollfd[]);
2、select只支持最大fd < 1024,如果单个进程的文件句柄数超过1024,select就不能用了。poll在接口上无限制,考虑到每次都要拷贝到内核,一般文件句柄多的情况下建议用epoll;
3、select由于使用的是位运算,所以select需要分别设置read/write/error fds的掩码。而poll是通过设置数据结构中fd和event参数来实现read/write,比如读为POLLIN,写为POLLOUT,出错为POLLERR;
4、select中fd_set是被内核和用户共同修改的,所以要么每次FD_CLR再FD_SET,要么备份一份memcpy进去。而poll中用户修改的是events,系统修改的是revents。所以参考muduo的代码,都不需要自己去清除revents,从而使得代码更加简洁;
5、select的timeout为NULL时表示无限等待,否则是指定的超时目标时间;poll的timeout为-1表示无限等待。所以有用select来实现usleep的;
源码:https://github.com/zhangdongxuan/CodeSlice/tree/master/network/MultiplexingIO/ServerTest