虽然说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;