82-再议 select 版回射客户端

第一次,我们写的服务器客户端是停等版本,像下面这样:

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

如果你已经 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-Q

3. 解决方案

我们不能再使用 writen 函数了,因为它有风险。有两种办法:

  • 在客户端的应用层增加发送缓冲区和接收缓冲区。不使用 writen 而改为 write 函数。另一方面,即使是使用 write 函数,谁能保证它就没有阻塞风险呢?最佳的策略是使用非阻塞 IO。
  • 使用多进程或多线程客户端,将 read 和 write 放到两个不同的进程或线程中。

在下一篇,使用第一种方案,看看如何改进这个有风险的客户端。

4. 总结

  • 知道 select + 阻塞 IO 回射客户端可能产生的 bug 及其原因
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值