82-再议 select 版回射客户端
2017-05-08 13:46
134 查看
第一次,我们写的服务器客户端是停等版本,像下面这样:
后来,我们用 select 改进了它:
writen 函数表示写 n 字节的字符,只要这 n 个字节没有写完,writen 就会一直尝试发送,直到全部写出去。writen 如果要阻塞,唯一的可能就是发送缓冲区满了。但是服务器不是一直都在接收数据吗?总有一个时候发送缓冲区会有空闲出来吧,这样看起来,writen 最多只会临时阻塞。
此时,请思考 5 分钟……
接下来看实验。
while(1) { read(stdin); writen(sockfd); read(sockfd); writen(stdout); }
后来,我们用 select 改进了它:
while(1) { rfds = {stdin, sockfd}; select(rfds); if (stdin in rfds) { read(stdin); // 风险代码,可能产生阻塞 writen(sockfd); } if (sockfd in rfds) { read(sockfd); writen(stdout); } }
1. select 版本分析
看起来似乎完美,不是吗?但是,这个 select 版本仍然存在潜在的风险。有没有可能在writen(sockfd)永远阻塞?
writen 函数表示写 n 字节的字符,只要这 n 个字节没有写完,writen 就会一直尝试发送,直到全部写出去。writen 如果要阻塞,唯一的可能就是发送缓冲区满了。但是服务器不是一直都在接收数据吗?总有一个时候发送缓冲区会有空闲出来吧,这样看起来,writen 最多只会临时阻塞。
此时,请思考 5 分钟……
接下来看实验。
2. 实验
2.1 程序路径
本文使用的程序在 gitos 上可以找到:git clone https://git.oschina.net/ivan_allen/unp.git[/code]
如果你已经 clone 过这个代码了,请使用git pull更新一下。本节程序所使用的程序路径是unp/program/nonblockio/bio.
此文件夹下的 echo 程序同之前的一样,既可以是服务器也可以是客户端。为了精简代码,服务器一次只处理一个连接,处理完就退出。客户端使用 select 进行处理。2.2 实验步骤
在 mars 主机上启动服务器$ ./echo -s
接下来,客户端分别做两次实验,不同点在于客户端一次发送给服务器的数据量大小。2.3.1 writen(sockfd, 4096)
在 sun 主机上启动客户端$ time dd if=/dev/zero bs=1024000 count=1 | ./echo -h mars $2 -l 4096 >/dev/null
上面这条命令表示:利用 dd 命令生成 1024000 字节的数据定向到客户端的标准输入,echo 的-l 4096参数表示一次发送(writen)多少字节数据。
为了方便客户端命令的执行,将这条命令写入到 shell 脚本中,命名为 run_client.sh:// run_client.sh #!/bin/bash length=4096 if [ $1 ]; then length=$1 fi time dd if=/dev/zero bs=1024000 count=1 | ./echo -h mars -l $length >/dev/null
接下来我们就可以这样启动客户端:$ ./run_client.sh 4096
运行结果
图1 write(sockfd, 4096)
图1 左侧是客户端,右侧是服务器。这个程序运行的非常好,客户端发送了 1024000,每次发送 4096 字节,最后收到了服务器回射回来的 1024000 字节,很正常,没毛病。2.3.1 writen(sockfd, 1024000)
这一次实验,一次性将 1024000 个字节全部发送给服务器,看看情况如何。
客户端$ ./run_client 1024000
运行结果
图2 write(sockfd, 1024000)
这一次,结果很不幸,客户端在 ready to send 1024000 字节时,被永久阻塞了。(如果你的机器没出现这个问题,你可以尝试继续增大这个数字,把 dd 的 bs = 1024000 改成 bs = 10240000,然后一次发送 10240000 字节,相信我,总会永久阻塞掉)。
再看看服务器的情况,服务器收到了 326244 字节,一共只发送出去 322148 字节,就再也发不动了。这说明,服务器的发送缓冲区也已经满了。2.3 结果分析
从 2.2 节中的实验可以看到,只要不断的增大 writen 发送的数据量,最终就会导致客户端永久阻塞。这不是服务器的问题,服务器一次接收 4096 字节,然后再将其发送回去,这没什么问题。最终服务器阻塞在了 writen 上,只是因为客户端那边——没有及时的收数据。这导致服务器发送缓冲区也被填满。
我们用图 3 和 图 4 来描述这种情况。
图3 客户端一次 write 1024000 字节,当前尚未阻塞
图 3 表示正在执行语句write(sockfd, buf, 1024000),当前的状态是 TCP 还在工作。
图4 客户端一次 write 1024000 字节,双方的接收和发送缓冲区都被填满
图 4 描述的情况是,双方的接收和发送缓冲区都被填满。不幸的是,客户端的 1024000 个字节还没发完,阻塞在了 writen 上。然而,客户端这个时候并没有机会去 read(sockfd)。因为客户端的代码是下面这样。if (stdin in rfds) { read(stdin); // 风险代码,可能产生阻塞 writen(sockfd); }
客户端没有机会 read,最后客户端的接收缓冲区也被填满,因此客户端会会向服务器发送 0 窗口通告。服务器收到 0 窗口通告后,就不再向客户端发数据,于是服务器的发送缓冲区最终被填满,服务器也阻塞到了 writen 上。
另外,从图 5 和图 6 中也可以看到,客户端和服务器的 Recv-Q 和 Send-Q 的值都很大。特别的,客户端的 Recv-Q 一直没有变化,你可以反复运行图 5 的命令进行验证。
图5 客户端的 Recv-Q 和 Send-Q
图6 服务器的 Recv-Q 和 Send-Q3. 解决方案
我们不能再使用 writen 函数了,因为它有风险。有两种办法:
在客户端的应用层增加发送缓冲区和接收缓冲区。不使用 writen 而改为 write 函数。另一方面,即使是使用 write 函数,谁能保证它就没有阻塞风险呢?最佳的策略是使用非阻塞 IO。
使用多进程或多线程客户端,将 read 和 write 放到两个不同的进程或线程中。
在下一篇,使用第一种方案,看看如何改进这个有风险的客户端。4. 总结
知道 select + 阻塞 IO 回射客户端可能产生的 bug 及其原因
相关文章推荐
- 多路复用I/O之select 实现客户端之间通信【3】
- 客户端select实现DropDownList1_SelectedIndexChangedg事件
- 网页中select控件数据量大时,客户端操作技巧!
- Unix网络编程之select版客户端实现
- socket编程:多路复用I/O服务端客户端之select
- Socket的select制作多客户端传输(Qt)
- python的select服务端的代码和客户端的代码
- Android模拟器上使用NIO编写客户端中,使用select超时,立马返回0的问题
- 【转】jQuery获取客户端控件select
- WSAEventSelect模型客户端代码示例
- select error:不能用DB-library(如isql)或odbc3.7或更早版本将ntext数据或仅使用Unicode排序规则的Unicode数据发送到客户端
- 关联的Select,可以支持客户端动态更新
- 43-使用 select 改进客户端
- 【转】jQuery获取客户端控件select
- 【Linux网络编程】基于TCP协议 I/O多路转接(select) 的高性能回显服务器客户端模型
- nagios监控客户端应用81,82,22
- winsock简单使用(采用select轮询方式,从客户端获取数据)
- select监听多个客户端 -- linux函数
- 当客户端socket关闭时,select模型的server端该如何完美的释放掉连接的socket? [问题点数:100分,结帖人guopo]
- select 函数实现 三种拓扑结构 n个客户端的异步通信 (完全图+线性链表+无环图)