在前面的文章《socket编程——服务器并发》中,我们看到服务器的并发可以简单的通过fork 子进程来实现,这种方式比较方便,但是也有些缺点,就是相对开销 比较大,当然了,这里说的也只是相对开销大,毕竟现在的处理器功能相对强大。在实际的工程项目中,还有一种方式,就是结合select机制,也可以实现 单进程下服务器的并发,这个是充分的使用了select函数的强大功能,关于select可以参看之前的文章:《嵌入式Linux编程之select使用总结》、《Linux下 fd_set 结构小结》、《深入理解select》。
我们先说一下通过select实现单进程服务器并发的流程:
1)创建监听套接字listenfd
2)对套接字listenfd进行地址数据赋值并且bind。
3)对套接字listenfd进行 监听listen
4)创建 fd_set 变量 all_set,先对变量all_set 进行赋初值 listenfd,也就是让select先只对套接字listenfd进行监听筛选。
5)进入主循环,通过select进行阻塞,来监听 描述符集 的“可读”性。
8)select返回,然后我们就需要判断select的返回 对象了,注意这里不是说select函数的返回值,而是说去分析select监听的描述符集中哪些 “可读”。
9)我们从 描述符 0 开始轮询判断,一直轮询 当前使用的最大描述符,通过FD_ISSET进行判断。这是程序的核心思路,由于select是监听 “集”,而且第一个参数是 所有 监听“集”的最大值, 只要这个“集合”中有任一描述符“可读”,就返回,所以我们从描述符0开始轮询判断,这肯定不会错,这里可能会有些疑问,难道描述符一定是从0开始,而不是随意的吗?答案是肯定的,描述符的分配就是从0开始,而且 不是 如果是任意的,描述符对于内核来说监听套接,是有限的宝贵资源,毕竟linux下,一切皆文件。
10)如果是 listenfd 可读,这说明是有新的 客户端连接,那么我们就可以通过 accept进行 获取新的客户端套接字信息,包括新的客户端的描述符。然后,特别关键的是,我们需要将这个新的 客户端 的描述符添加到 all_set中,这样select下次就既可以监听 listenfd,又可以同时监听之前已经连接的客户端的 来往数据了。
11)如果不是listenfd可读,那基本上可以 确定是 之前已经连接的客户端 的来往数据了,也就是与客户端之间的 通信数据, 这个时候,我们就可以实现与这个客户端按照约定协议进行通信,比如modbus tcp等等。
12)到这里,可能还有一个疑惑,貌似上面的描述是 针对1个客户端,那如果是又有新的客户端连接,同时已经连接的客户端又有数据通信呢? 答案是步骤9,因为步骤9也是个循环,循环内容包括(10)和(11),步骤9从描述符0开始轮询,反正总能把当前所有可读的 描述符 轮询到,然后针对每个 描述符做相应的处理(是新连接,还是之前已经连接的客户端数据业务)。
上面就是程序流程,理解这个逻辑的前提是要深入理解select,这个强大的功能函数。下面是示例代码:
int
main( void )
{
int listenfd, masterfd;
fd_set refset;
fd_set read_set;
int fdmax;
struct sockaddr_in cliaddr, servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, BACKLOG);
FD_ZERO(&refset);
FD_SET(listenfd, &refset);
fdmax = listenfd;
while( 1 )
{
read_set = refset;
if( select(fdmax + 1, &read_set, NULL,NULL,NULL) == -1){
perror("server select error.\n");
}
for(masterfd = 0; masterfd <= fdmax; masterfd++){
if( !FD_ISSET(masterfd, &read_set)){
/*跳出当前次循环,直接进行下一次的循环*/
continue;
}
if(masterfd == listenfd){
/* a new client connect */
socklen_t addrlen;
struct sockaddr_in clientaddr;
int newfd;
addrlen = sizeof(clientaddr);
memset(&clientaddr, 0, sizeof(clientaddr));
newfd = accept(listenfd, (struct sockaddr *)&clientaddr, &addrlen);
if(newfd == -1)
printf("server error!\n");
else{
FD_SET(newfd, &refset);
if(newfd > fdmax)
fdmax = newfd;
printf("new connect from %s\n", inet_ntoa(clientaddr.sin_addr));
}
}
else{
/*已经连接的客户端的 数据业务,这里进行简单的回显*/
str_echo(masterfd);
}
}
}
}
ps:libmodbus库中的modbus tcp服务并发的实现就是基于上面的逻辑。这样做的好处是,不给内核增加太多的子进程,也能实现多并发。