浅析tcp中read阻塞

       最近学习route组件,了解了些关于tcp通信中I/O复用的知识。比如:select,poll,epoll。目前系统主要是用select。本来以为select是个好东西,解决了单进程单线程的server可以连接多个客户端的问题。后来,同事跟我说read函数是阻塞的,那么连接建立后,server会阻塞在read处,其他连接就没法正常工作了。然后这个问题就一直困扰着。想起了之前在知乎上有个问题是:怎么设计tcp连接?有个点赞很多的是建多线程,一个线程一个连接。但是,也有不少人批判这个设计,说这样太费资源,指出I/O多路复用中使用类似于select在一个线程中可以实现连接多个客户端。当时也是没想明白,而且公司route组件(老版本)的设计是一个线程一个连接,apache也是一个进程一个连接,坏处就是连接数量很少,毕竟进程切换是耗cpu的。后来就查阅资料,然后通过代码测试,read的阻塞可以不会干扰其他连接的,一个server连N客户端跟连一个客户端一个麻溜溜的。测试代码如下: 

 

 1     for (;;) {
 2         memset(szBuf, 0, sizeof(szBuf));
 3         FD_ZERO(&fset);
 4         FD_SET(fd, &fset);
 5         tv.tv_sec = 5;
 6         tv.tv_usec = 0;
 7 
 8         for (int i = 0; i < BACKLOG; i++) {
 9             if (fd_A[i] != 0)
10                 FD_SET(fd_A[i], &fset);
11         }
12 
13         ret = select(maxfd+1, &fset, NULL, NULL, &tv);
14 
15         if (ret < 0) {
16             printf("select调用发生错误\n");
17             break;
18         }
19         else if (ret == 0) {
20             printf("select timeout\n");
21             continue;
22         }
23         else {
24             printf("select normal\n");
25         }
26 
27         for (int i = 0; i < BACKLOG; i++) {
28             if (fd_A[i] && FD_ISSET(fd_A[i], &fset)) {
29                 printf("recv before\n");
30                 if ((ret = recv(fd_A[i], szBuf, sizeof(szBuf), 0)) == 0) {
31                     close(fd_A[i]);
32                     FD_CLR(fd_A[i], &fset);
33                     fd_A[i] = 0;
34                     conn_amount--;
35                 }
36                 else {
37                     printf("fd_A[%d]:%s", i, szBuf);
38                 }
39             }
40         }
41 
42         if (FD_ISSET(fd, &fset)) {
43             newfd = accept(fd, (struct sockaddr *)&cli_addr, &cli_len);
44             if (newfd <= 0) {
45                 printf("accept出错\n");
46                 continue;
47             }
48             else
49                 printf("accept normal\n");
50 。。。

 

 

 

           当客户端connect连接上的时候,会输出"select normal"  "accept normal",没有走到read/recv这块;如果5秒内客户端没其他操作,server就会在select处超时(select的超时时间设置的是5秒)。接着客户端调用send,然后代码会走到select->read这块,并没有进去accept。因为在调用accept,recv/read之前我用了FD_ISSET来判断。

          fd_set是一组文件描述符(fd)的集合,它用一位来表示一个fd。至于fd有多大,操作系统定义了常量FD_SETSIZE。在很久 以前是32,现在一般是1024。select函数用于检查fd_set集合中是否有可读的,同时也会更新fd_set集合。FD_ISSET用于测试指定的文件描述符是否在该集合中。假设现在客户端1是成功连接的,如果客户端2发起连接,那么select后客户端1对应fd使用FD_ISSET后返回值是false的,那么就不去调用recv/read函数。如果客户端1发送数据过来,select检测到后,使用FD_ISSET判断连接1返回true,可以用recv/read不会阻塞;使用FD_ISSET判断连接2的返回是false的,不去调用recv/read函数。

同时,客户端在send的时候一次发2k数据,在server接收一次1k的,第一次没取完,select会再次检测到该fd可读,再收一次,正好2k,select才不会检测到该fd可读。这个例子是一个简单的非阻塞(NIO)的例子,难点就是对于半包问题要处理好。很多时候我们接收到的数据要完整了才行进行decode。

 

posted @ 2017-09-08 14:56  超龄码农  阅读(6352)  评论(0编辑  收藏  举报