socket编程——深入理解“单进程通过select实现并发”

    在前面的文章《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服务并发的实现就是基于上面的逻辑。这样做的好处是,不给内核增加太多的子进程,也能实现多并发。

你可能感兴趣的:(TCP/IP,Linux)