浅析tcp中select使用(思路源于工作)
虽然说poll和epoll要比select好很多,但是还是有很多地方select稳健运行着,而且select相关的资料也多。接下来就简单阐述下工作中route模块对这些的使用。
在服务端使用tcp的一般流程是:socket->bind->listen->select->accept->read/write
listen函数的第二个参数backlog值是表示期待连接的描述符的个数,假设传5,实际放列队的个数会稍微大于5,不同操作系统有不同的实现。放到三次握手流程里说,就是client发了个SYN过来和client发SYN+ACK,对应的server端状态是SYN_RECV和ESTABLISED。如果队列满了,client端调用connect请求就会返回-1。如果backlog不知道用什么值,可以用系统函数getenv("LISTENQ")取环境变量中设置的值。这个函数没有超时设置,不是阻塞,所以监控是否有连接请求是内核做的事情,存放描述符到队列也是内核做的,这个函数也就是触发内核要做的事情。
接下来用select函数来获得可用的描述符。在收到connect的请求时,驱动通知内核,内核完成3次握手后把描述符放入数组中,select遍历数组就能拿到该描述符了(linux系统环境变量一般设置这个数组的大小是1024)。然后调用accept接收一个套接字中已建立的连接。这个连接是双全工管道,既可以读也可以写。但是,工作中有些模块不是这么用的。以工作中的Route模块为例,它是先accept,再select,总觉得哪里不对,为啥?因为跟教科书中不符。其实,仔细分析下,也是对的。理由如下:
1.在listen之后,内核就在收集客户端连接请求。accept去接收一个已建立的连接,如果连接建立成功后,即长连接了,就创建一个新线程用于收发数据。每次收数据之前都会使用select来校验下描述符是否可用,如果是断开状态,就不收数据。
2.如果select->accept后建一个线程去处理收发数据,那么recv数据时候就不知道描述符是否还是可用的了。如果接着再调用selec也是可以的。
这块业务看似是一个线程一个链接,其实是一个线程多个tcp连接,还是用到了I/O复用。主线程接收N个客户端的连接,然后每个连接建立一个线程去处理收发数据。
缺点是这个Route模块是单进程的,linux下最多可以接受1024个连接。如果修改环境变量把值变大,在连接数很大的时候,select会比较慢,影响效率。现在的方案都是在环境用中起好几个route程序,并做级联对应。而且处理收发数据的线程相当于是阻塞的。还有个缺点是一个系统所能创建的线程数量有限,而且线程在cpu中频繁切换也是耗费资源,达到某个阀值后性能就大大降低了。所以后来又做了个升级版。
Route模块大致结构图如下:
主要就是解析数据然后转发。
# select的一个简单用法 #include <sys/socket.h> #include <sys/types.h> #include <sys/select.h> #include <unistd.h> #include<netinet/in.h> #include <iostream> using namespace std; const int PORT = 9000; #define BACKLOG 5 // how many pending connections queue will hold #define BUF_SIZE 1024 int Listen(int fd, int backlog) { char *ptr; /*4can override 2nd argument with environment variable */ if ( (ptr = getenv("LISTENQ")) != NULL) backlog = atoi(ptr); return listen(fd, backlog); } int main(int argc, char *argv[]) { // socket bind listen select accept read struct sockaddr_in addr; struct sockaddr_in cli_addr; int fd, newfd; int conn_amount = 0; char szBuf[BUF_SIZE]; //protocol=0会自动选择协议 if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf("socket调用发生错误\n"); return -1; } int yes = 1; if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1) { printf("setsockopt错误\n"); exit(1); } memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(PORT); if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { printf("bind调用发生错误\n"); return -1; } if (Listen(fd, BACKLOG) == -1) { printf("listen调用发生错误\n"); return -1; } fd_set fset; int ret = 0, maxfd = fd; struct timeval tv; int fd_A[BACKLOG] = {0}; socklen_t cli_len = sizeof(cli_addr); printf("fd[%d]\n", fd); for (;;) { memset(szBuf, 0, sizeof(szBuf)); FD_ZERO(&fset); FD_SET(fd, &fset); tv.tv_sec = 5; tv.tv_usec = 0; for (int i = 0; i < BACKLOG; i++) { if (fd_A[i] != 0) FD_SET(fd_A[i], &fset); } ret = select(maxfd+1, &fset, NULL, NULL, &tv); if (ret < 0) { printf("select调用发生错误\n"); break; } else if (ret == 0) { printf("select timeout\n"); continue; } else { printf("select normal\n"); } for (int i = 0; i < BACKLOG; i++) { if (fd_A[i] && FD_ISSET(fd_A[i], &fset)) { printf("recv before\n"); if ((ret = recv(fd_A[i], szBuf, sizeof(szBuf), 0)) == 0) { close(fd_A[i]); FD_CLR(fd_A[i], &fset); fd_A[i] = 0; conn_amount--; } else { printf("fd_A[%d]:%s", i, szBuf); } } } if (FD_ISSET(fd, &fset)) { newfd = accept(fd, (struct sockaddr *)&cli_addr, &cli_len); if (newfd <= 0) { printf("accept出错\n"); continue; } else printf("accept normal\n"); if (conn_amount < BACKLOG - 1) { for (int i = 0; i <BACKLOG; i++) { if (fd_A[i] == 0) { fd_A[i] = newfd; conn_amount++; break; } } if (newfd > maxfd) { maxfd = newfd; } } else { send(newfd, "test", 5, 0); //close(newfd); continue; } } } //close all connections for (int i = 0; i < BACKLOG; i++) { if (fd_A[i] != 0) { close(fd_A[i]); } } return 0; }