**Linux 知识补充 -- Socket 编程 - TCP 阻塞方式 - 基本知识** 1652270 **冯舜** 补充知识和预备步骤 =============== ## 在克隆的虚拟机中设置网卡 如图, 克隆作业用虚拟机, 将桥接网卡取消掉. 如//cloned//. !![cloned,克隆后的虚拟机] 在虚拟机的硬件设置中, 为 NAT 网卡生成一个新的随机 MAC 地址, 防止和原虚拟机重复. 如//setmac//. !![setmac,为新网卡生成随机 MAC] 在主虚拟机没有启动的情况下, 启动克隆虚拟机. 接下来配置网卡. 编辑 `/etc/sysconfig/network-scripts/ifcfg-{网卡}` 来配置网卡, 网卡名称一般为 `ens32`. !![vimens1,编辑网卡配置文件] 修改其中的 UUID 行的内容, 使其与原来不同 (注意符合十六进制格式); 修改 IPADDR 的属性, 配置一个新的局域网 IP 地址. !![vimens2,配置新的 UUID 和 IP 地址] 保存退出编辑器. 接下来为方便辨别, 为克隆的虚拟机设置主机名. 用 `hostnamectl set-hostname {需要的主机名}` 来设置主机名. 修改后, 可以用 `hostnamectl` 来查看. 如//sethostname//. !![sethostname,设置主机名] 修改主机名后需要重启生效. 重启后, 可发现主机名和 IP 地址设置都生效了. !![aftersethostname,主机名、IP 地址和 MAC 地址设置生效] ## 一个网卡上多地址 启动主虚拟机, 编辑 `/etc/sysconfig/network-scripts/ifcfg-{网卡}`. 添加一行 IPADDR1 的项, 属性设为第二个 IP 地址, 此处设为不同网段的 `192.168.81.230`. !![alternateip,设置第二个 IP 地址] 现在尝试在两台虚拟机之间互相 ping 测试连通性. !![pingvm1,主虚拟机 ping 克隆虚拟机] !![pingvm2,克隆虚拟机 ping 主虚拟机] 可发现, 在同一个网段 `192.168.80.0/24` 上的两个 IP 地址能相互 ping 通, 但不同网段上的 IP 地址无法 ping 通. ## 在两侧虚拟机处关闭防火墙 ~~~~~~~~~ systemctl disable firewalld systemctl stop firewalld ~~~~~~~~~ !![disablefirewall,在两侧虚拟机处都关闭防火墙] 创建目录 ================= 已创建的目录如//dir//所示. !![dir,创建的目录] TCP Socket 测试程序 ================== 写 TCP Socket 通信程序 `01/tcp_server1.c` `01/tcp_client1.c`. ## 若服务端绑定的端口号已被使用 启用两次 `./tcp_server1 12000`, 试图两次绑定本机地址的端口 12000, 会出现错误: `错误: bind() 失败: Address already in use`, 如//inuse//. !![inuse,两次绑定同一端口, `bind() 失败`] ## 使用 `tcp_client1` 发起连接 在已经启用 `./tcp_server1 12000` 后, 在另一 SecureCRT 会话中运行 `./tcp_client1 localhost 12000` 连接 `localhost:12000`, 如//client//, !![client,客户端连接] ## `tcp_client1` 连接时, IP 地址不正确 ### 当虚拟机上有默认网关时 当虚拟机配置了默认网关, 使用 `./tcp_client1 169.254.33.33 12000` 连接并不存在的 IP 地址, 一段长时间后 `connect()` 函数报错 Connection timed out (超时), 如//notexistip//. !![notexistip,配置默认网关时, 连接超时] ### 当虚拟机上无默认网关时 将虚拟机上所有网卡配置的默认网关注释掉, 再次连接不存在的 IP 地址, 立即退出, `connect()` 函数报错 Network is unreachable (网络不可达), 如//notexistip2//. !![notexistip2,配置默认网关时, 网络不可达] ## `tcp_client1` 连接时, 端口号不正确 当端口号不正确, IP 地址正确时, `connect()` 函数会报错 Connection refused. !![wrongport,端口号错误时报错 Connection refused] ## 连接成功且双方进入 `read/recv` 状态, 中断其中一端 在 `read()` 和 `accept()` 函数的前后分别有提示信息, 可用来确认服务器和客户端连接成功. !![server,连接成功时的服务端阻塞] !![client2,连接成功时的客户端阻塞] ### 用 Ctrl-C 中断 server 中断 server 后, client 显示其接收了 0 字节后退出, 说明侦测到连接中断. !![serverint,中断 server, client 检测到中断] ### 用 Ctrl-C 中断 client 中断 client 后, server 显示其接收了 0 字节后退出, 说明侦测到连接中断. !![clientint,中断 client, server 检测到中断] ### 用 `pkill -9 tcp_server1` 杀死 server 杀死 server 后, client 显示其接收了 0 字节后退出, 说明侦测到连接中断. !![serverkill,杀死 server, client 检测到中断] ### 用 `pkill -9 tcp_client1` 杀死 client 杀死 client 后, server 显示其接收了 0 字节后退出, 说明侦测到连接中断. !![clientkill,杀死 client, server 检测到中断] ## 双方连接成功后, 再使用一个 client 连接 server 首先启动 client 和 server, 使双方互联: !![clientserver,建立 TCP 连接] 在 vm-linux-2 的另一个 SSH 会话中, 启动另一个 client: !![newclient,另一个 client] 发现正常连接, 进入 `read()` 函数. 使用 `netstat -anp -Ainet` 查看 Internet 域的连接, 发现监听的地址有两条 ESTABLISHED 的连接: !![newclientnetstat,两条连接] ## `tcp_server1` 与客户端连接后强制终止, 立即绑定同端口号再次启动 如果没有在 socket 里指定 `REUSEADDR` 选项, 则不能成功, 提示 Address already in use. 这是因为主动断开连接的一方的系统中, 内核会维持原连接在 `TIME_WAIT` 状态一段时间. 在这段时间内, 这个 `地址:端口` 无法被使用. !![restartserver,端口被占用, 短时间内无法重启服务器] 要使得在这段时间内能够再次使用这个地址和端口, 需要在创建 socket 的代码后添加: ~~~~~~~~~~~C if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof(int)) < 0) { errorexit("错误: setsockopt() 失败"); } ~~~~~~~~~~~ !![restartserver2,添加了 `REUSEADDR` 选项后, 短时间内重启服务器也能成功] ## 设置 VMWare Workstation 的 VMNet8 的网段与虚拟机内设置不同, 测试虚拟机间是否能 ping 通 在 "虚拟网络管理器" 中, 把 VMNet8 的网段设置为 `192.168.100.0/24`: !![vmnet8,VMWare 的网络管理器中改网段] 修改完毕后, 所有 SSH 会话断开, 只能通过虚拟机的屏幕来输入命令. !![sshdiscon,所有 SSH 会话断开] vm-linux ping vm-linux-2 的 IP 地址, 能够 ping 通: !![pingvm3,vm-linux ping vm-linux-2] vm-linux-2 ping vm-linux 的 IP 地址, 能够 ping 通: !![pingvm4,vm-linux-2 ping vm-linux] 怀疑能 ping 通是 ARP 缓存未失效的问题, 用 `ip neigh flush dev ens32` 使 ARP 缓存失效, 再试一次: !![pingvm5,刷新缓存后 vm-linux ping vm-linux-2] 仍能 ping 通. 这是因为两机在同一链路上, 且它们的配置 (同一网段) 允许它们之间通信. 至于 VMWare Workstation 中对于 VMNet8 网段的设置, 只影响到宿主机的 IP 地址以及网关的 IP 地址, 对于两虚拟机没有影响. client 固定端口号、server 打印 client 端地址和端口号 ==================== 在 02 子目录中写 server 和 client 的代码 `02/tcp_server2.c` 和 `02/tcp_client2.c` . 其中, server 的改动主要是 `accept()` 函数后将获取的客户端地址打印出来, client 的改动则是在 `connect()` 前先将 socket `bind()` 到 0.0.0.0 的指定端口上. 编译后进行测试: 统一使用 `tcp_server2` 服务端, 先测试 `tcp_client1` 客户端: !![randomport,使用 `tcp_client1` 客户端, 获取到的客户端端口是随机值] 再测试 `tcp_client2`: !![fixedport,使用 `tcp_client2` 客户端, 获取到的客户端端口是设定好的值] 服务端读取所有网卡的所有 IP, 并绑定某个 IP; 客户端代入服务端 IP 发起连接 ================ ## 两台用于测试的 CentOS 7 虚拟机设置多地址 分别在两个虚拟机上编辑 `/etc/sysconfig/network-scripts/ifcfg-ens32`, 设置 `IPADDR1` 项为第二个 IP 地址. !![ens32-1,设置第二个 IP 地址] 移除第二虚拟机的 `ifcfg-ens34` (原桥接网卡) 配置文件. 将两个虚拟机用 `systemctl restart network` 重启网络服务. 重启后, `ip address` 查看 IP 地址结果如下. 发现, 两机的网卡都配置了两个地址. !![afterens32,`ip address` 检查配置] 两机用对方两个地址进行 ping 操作, 正常. !![afterens32-ping,互 ping 检查配置] ## 读所有网卡的所有 IP 地址, 在特定 IP 上进行通信 使用 `getifaddr()` 函数可以获取到一个记录着本机所有网络地址的链表, 遍历输出所有的 IPv4 地址. (如有打印 IPv6 的需要, 去除 `case AF_INET6:` 行下方的 `continue` 并取消下一行的注释) 另外, 只需修改 `serv_addr.sin_addr.s_addr` 的设置方式即可通过参数设置要绑定的 IP 地址. 源码见 `tcp_server3.c`. 运行结果: !![bindtospecificip,绑定到特定 IP, 客户端连接成功] 当客户端尝试连接到服务端没有绑定的 IP 时, 会返回错误 Connection refused. !![bindtospecificip,绑定到特定 IP, 客户端连接到另一个 IP] `read()/write()` 函数和 `recv()/send()` 函数 =============== ## `read()/write()` 函数 写测试程序服务端 `04/tcp_server4-1.c` , 在建立连接后, 一次 `read()` 20 字节. ### 客户端 -- 一次写入超过 20 字节 写测试程序客户端 `04/tcp_client4-1-1`, 建立连接后一次写入超过 20 字节. 运行结果如下: !![5-long20,读 20 字节, 写超过 20 字节] 发现服务端只读入了 20 个字节. ### 客户端 -- 每次写 2 字节, 间隔 1 秒 写测试程序客户端 `tcp_client4-1-2`, 建立连接后间隔 2 秒, 每次 2 字节, 写入超过 20 字节, 另外处理了 SIGPIPE 信号. 代码见 `tcp_client4-1-2.c`. 运行结果如下: !![5-interval20,每次写 2 字节, 间隔 1 秒] 发现服务端一次只读了 2 字节之后退出, 客户端在尝试写几次后, 收到 SIGPIPE 信号而退出. ## `recv()/send()` 函数 在 `tcp_server4-1` `tcp_client-4-1-*` 的基础上写 `tcp_server4-2` `tcp_client-4-2-*` , 将 `read()/write()` 函数换为 `recv()/send()` 函数. `recv()/send()` 函数和 `read()/write()` 函数用法不同, 区别是前者所需的参数多了一个 (`int flags`), 用以接受用户给定的选项标志位. 运行结果如下: !![5-recv,用 `recv()/send()` 读 20 字节, 写超过 20 字节] !![5-recv2,用 `recv()/send()` 每次写 2 字节, 间隔 1 秒] 可以看出, 表现与 `read()/write()` 完全相同. ## 要求 `read()/recv()` 执行后, 不读满 20 字节一直不返回 `recv()` 在标志位参数指定 `MSG_WAITALL` 可以做到这一点, 详见 `tcp_server4-3.c`. 运行结果: !![5-until20,等待指定字节数到达的 `recv()`] `read()` 无法直接做到这一点, 但可以通过在 `read()` 前一直用 `ioctl(new_sockfd, FIONREAD, &readableBytes);` 检查可用字节数到达 20, 再进行 `read()` 以达到目的. !![5-until202,等待指定字节数到达的 `read()`] ## `read()/write()` 和 `recv()/send()` 的使用区别 - `recv()/send()` 只能用于 socket 文件描述符 (第一个参数), 而 `read()/write()` 可以用于任意文件描述符. - `recv()/send()` 比 `read()/write()` 多需要一个参数 `int flags`, 用以指示选项标志位, 其中可以给出如 `MSG_WAITALL` 等等待所有字节完成接收的选项. - `flags` 为 0 的 `recv()/send()` 与 `read()/write()` 行为几乎一致, 仅在接收长度为 0 的数据报时行为有微小不同. Socket 的缓冲及写阻塞 ================== ## 对方不读, 本机持续写 写 `05/tcp_server5-1.c` 服务端, 接收 client 连接后, 使用 `getchar()` 进入等待输入的阻塞状态, 无法读 client; 写 `05/tcp_client5-1.c` 客户端, 连接后持续用 `write()` 写, 并持续输出写了多少个字节, 直到 `write()` 也阻塞. 由于输出已经写的字节时, 每隔几个字节才更新一次, 故在 `write()` 阻塞后, 用 `Ctrl-C` 中断, 让客户端中的信号处理函数给出计数器的最终值. 编译运行后, 结果如下, 精确值为写入 373957 字节后进入阻塞状态, 实际运行多次后数字略有波动. !![6-1,对方不读, 本机最多持续写大约 374000 字节] ## 对方读了多少, 本机解除写阻塞 继续运行服务端和客户端, 但客户端在遇到写阻塞后不退出, 而是在客户端处输入回车解除输入阻塞, 使服务器开始读. 控制服务器读的字节数. 经过测试, 读了约 16000 字节(每次结果有波动, 有时波动较大, 可到 40000), 客户端侧数字开始变化, 证明客户端解除了写阻塞. 解除写阻塞后, 客户端在总计写超过 400000 字节后再次遇到写阻塞. !![6-2,对方读了约 16000 字节, 解除写阻塞] ## 使用 netstat 工具 Netstat 是 Linux 下监测网络连接的有力工具. 用 `man netstat` 查看其用法, 归纳较常用的参数: ~~~~~~~~~~~~~~~~~ --numeric , -n 显示数字形式地址而不是去解析主机、端口或用户名 --protocol=family , -A 指定要显示哪些连接的地址族(也许在底层协议中可以更好地描述) -c, --continuous 将使 netstat 不断地每秒输出所选的信息 -p, --program 显示套接字所属进程的PID和名称 -l, --listening 只显示正在侦听的套接字(这是默认的选项) -a, --all 显示所有正在或不在侦听的套接字。加上 --interfaces 选项将显示没有标记的接口 ~~~~~~~~~~~~~~~~~ 使用 `netstat -anp -Ainet` 显示所有状态的 Internet 域连接(区别于 UNIX 文件 socket), 数字形式显示地址, 显示进程 PID 和名称. 其显示的内容: !![netstat,netstat 的显示及其解释] ## 观察本题过程中, netstat 的显示变化 在服务端启动、客户端未启动时, 服务端处 netstat 显示端口打开: !![test-pre,服务端 netstat 显示 52270 端口打开] 在客户端启动后, 服务端处发现多了一条 TCP 连接, 本地地址是 ens32 网卡的本地 IP 和监听端口, 远端地址是客户端处的 IP 和端口. 客户端处也多了一条 TCP 连接, 本地地址和远端地址恰好对调. 写阻塞后, 服务端的接收队列和客户端的发送队列均显示了一个较大的值. 结束服务端, 终止连接后 (不能用 Ctrl-C 结束客户端, 这会导致写阻塞解除, 加上系统环境变化, 客户端会发出额外的包), 检查客户端累计发送的字节数, 等于前两值的和. !![test-notset,写阻塞时 netstat] !![test-notset-post,终止连接后, 客户端统计的字节数] 若写阻塞时, 服务端开始读, 则随着服务端读的字节数增加, 服务端 netstat 的 Recv-Q 项开始减少, 减少数和服务端读的字节数一致, 客户端 netstat 数据不变. !![6-3,服务端未开始读] !![6-4,服务端开始读] 当客户端解除写阻塞后, 会接着写直到写的字节总计 400000 以上. 这段时间, 服务端的 Recv-Q 和 客户端的 Send-Q 都会增加. !![6-5,解除写阻塞, 两个值都增加] ## 读写角色互换 写 `05/tcp_server5-2.c` 服务端和 `05/tcp_client5-2.c` 客户端, 将它们的读写角色互换. 用 netstat 观察, 得到类似结果, 只不过现客户端的 Recv-Q 变化情况对应原服务端的 Recv-Q, 现服务端的 Send-Q 对应原客户端的 Send-Q. !![6-6,客户端未开始读, 服务端阻塞] !![6-7,客户端开始读, 客户端 Recv-Q 值减少] !![6-8,服务端写阻塞解除, 两值都增加] ## 修改 Socket 收发缓冲区大小 正如上述 netstat 的变化过程所示, 本地和对方的收发缓冲区大小限制共同作用, 造成了 `write()` 阻塞. 现讨论如何获取、改动这个大小. 使用 `getsockopt({sock 文件描述符}, SOL_SOCKET, SO_SNDBUF, &{用来存发送缓冲区大小的变量}, &(socklen_t) {sizeof({用来存发送缓冲区大小的变量})})` 获取发送缓冲区大小. 使用 `setsockopt({sock 文件描述符}, SOL_SOCKET, SO_SNDBUF, &{用来存发送缓冲区大小的变量}, (socklen_t) {sizeof({用来存发送缓冲区大小的变量})})` 设置发送缓冲区大小. 注意, 用户所考虑的缓冲区大小一般都是针对于要传送的有效字节数来定的, 系统考虑为控制字节留出的余量, 会将用户设置的值加倍. 将 `SO_SNDBUF` 换成 `SO_RCVBUF` 可以用来获取/设置接收缓冲区大小. 现在进行一系列改动缓冲区的实验, 看阻塞时的本地发、远程收缓冲区中排队的字节数变化. 设置对照组: 将获取到的缓冲区大小原样设置回去(考虑系统加倍的行为). 但是, 即便看上去并没有改变这个 socket 的缓冲区大小, 阻塞时写的字节数和两个排队字节数相对于没有设置时(//test-notset//)都变小了. 这只能解释为, 获取/设置缓冲区大小时可能改变了 socket 连接的其他设置, 此处不深究. !![test-setdef,【对照组】原样设置缓冲区大小 - 阻塞时各数据] !![test-setdef-post,【对照组】原样设置缓冲区大小 - 终止后客户端发字节数和排队字节数的和一致] 下面是对照组和实验组的实验数据. 相对于对照组, 显著的变化用粗体标示出来. ("Cl/Sv" 分别表示 "客户端/服务端", "Snd/Rcv" 分别表示 "发送/接收缓冲区大小") 编号 | ClSnd | ClRcv | SvSnd | SvRcv | ClSend-Q | SvRecv-Q | 前两列和 | Cl写字节计数 ------------|--------|--------|--------|-------|----------|----------|----------|-------------- 不设置 | ? | ? | ? | ? | 130192 | 244200 | 374392 | 374392 对照组(默认)| 16384 | 87380 | 87040 | 369280| 63624 | 230381 | 294005 | 294005 1 | 16384 | 87380 |**170000**| 369280| 64520 | 231349 | 295869 | 295869 2 | 16384 |**170000**|**170000**| 369280| 65160 | 228398 | 293558 | 293558 3 | 16384 | 87380 | 87040 |**425984**(原设置370000x2, 超过系统限制)| 63880 | **272160** | **336040** | 336040 4 |**32768** | 87380 | 87040 | 369280| 64520 | **242723** | **307243** | 294005 5 |**65536** | 87380 | 87040 | 369280| 64264 | **246064** | **310328** | 310328 6 |**66010**(突变边界)| 87380 | 87040 | 369280|**129296**|**244276**|**373572**| 373572 7 |**240000**(大值)| 87380 | 87040 | 369280|**260512**|**245308**|**505820**| 505820 8 |**32768** | 87380 | 87040 |**425984**| 64392 | **283996** | **348388** | 348388 9 |**65536** | 87380 | 87040 |**425984**| 63752 | **281736** | **345488** | 345488 10 |**66010**(突变边界)| 87380 | 87040 |**425984**|**130192**|**284569**|**414761**| 414761 11 |**240000**(大值) | 87380 | 87040 |**425984**|**260384**|**284610**|**544994**| 544994 分析表中数据, 可以得知: - (结论)客户端写的字节经过了 "本地排队-网络传输-远程排队等待读取" 的顺序队列. - 作为服务端和作为客户端两种情况下, Socket 缓冲区默认设置是存在差异的. - 客户端 Send-Q 和 服务端 Recv-Q 的和, 等于客户端写字节总数, 验证了第一条. - 增加客户端接收缓冲区大小和服务端发送缓冲区大小, 对于本例(客户端发, 服务端收)没有影响. 验证了第一条. - 增加服务端接收缓冲区大小, 可增加服务端接收队列排队字节, 进而增加了客户端可以连续写的字节数. 验证了第一条. - (疑点)略微增加客户端发送缓冲区大小, 只增加了服务端接收队列中的排队字节, 而没有显著增加客户端发送队列中的排队字节. - 客户端发送缓冲区大小到达一定值后(本系统中为 66010), 写阻塞时客户端发送队列中的排队字节数发生翻倍的突变. - 大幅增加客户端发送缓冲区大小, 可小幅增加服务端接收队列排队字节, 大幅增加客户端发送队列排队字节, 进而增加了客户端可以连续写的字节数. 验证了第一条. - 同时增加服务端接收缓冲区大小和客户端发送缓冲区大小, 造成的影响可以叠加. 验证了第一条. 两段不同读写速率时的同步 ========================== ## 两端同时先读后写 当客户端和服务端均先读后写时, 由于两者皆读不出数据, 因此无法继续运行(死锁). !![7-1,无法正常收发数据] 当一端按 Ctrl-C 结束时, 另一端会收发三次之后收到 SIGPIPE 退出, 但收到的字节长度都是 0. !![7-1-1,服务端终止, 客户端收发三次] !![7-1-2,客户端终止, 服务端收发三次] ## 两端同时先写后读 ### 服读/服写/客读/客写 每次 1000/1000/1000/1000 字节 此时, 由于双方读时都有数据, 因此双方可以正常收发数据. 由于速度过快, 在每次发收周期后增加延时. 一端终止后, 另一端 `read()` 返回 0 字节(EOF), 收到 SIGPIPE 退出. 观察 netstat 数据, 双方的两个队列中字节数一直为 0. !![7-2-1,正常收发数据, 客户端终止] !![7-2-2,正常收发数据, 服务端终止] ### 服读/服写/客读/客写 每次 1000/500/500/1000 字节 双方每次读/写的字节数, 与对方写/读的字节数相同, 因此双方可以正常收发数据. 观察 netstat 数据, 双方的两个队列中字节数一直为 0. !![7-2-3,正常收发数据] ### 服读/服写/客读/客写 每次 1000/1000/700/700 字节 服务端每次读/写的字节数和客户端每次写/读的字节数不同, 因此双方虽然可以正常收发数据, 但流量限制在每次 700 字节. 观察 netstat 数据, 客户端的接收队列字节数以 300, 600, 900, 1200 的等差形式递增. !![7-2-4,正常收发数据但队列增长] 这种情况的解释如下: (浅红色表示服务端向客户端发数据以及客户端接收数据的字节数, 深红色代表客户端接收队列中残留的字节数, 蓝色代表客户端向服务端发送、服务端接收的字节数.) !![timeline0,这种情况的解释] ## 服务端先写后读, 客户端先读后写 ### 服读/服写/客读/客写 每次 1000/1000/1000/1000 字节 双方可以正常收发数据. 观察 netstat 数据, 双方的两个队列中字节数一直为 0. !![7-3-1,正常收发数据] ### 服读/服写/客读/客写 每次 1000/500/500/1000 字节 双方可以正常收发数据. 观察 netstat 数据, 双方的两个队列中字节数一直为 0. !![7-3-2,正常收发数据] ### 服读/服写/客读/客写 每次 1000/1000/700/700 字节 服务端每次读/写的字节数和客户端每次写/读的字节数不同, 因此双方虽然可以正常收发数据, 但流量限制在每次 700 字节. 其中的一个例外是客户端第二次接收到的是 300 字节而不是 700 字节. 观察 netstat 数据, 客户端的接收队列字节数开始是 300, 1000, 后以 1000, 1300, 1600, 1900 的等差形式递增. !![7-3-3,正常收发数据但队列增长] 这种情况的解释如下: !![timeline,这种情况的解释] ## 服务端先读后写, 客户端先写后读 ### 服读/服写/客读/客写 每次 1000/1000/1000/1000 字节 双方可以正常收发数据. 观察 netstat 数据, 双方的两个队列中字节数一直为 0. !![7-4-1,正常收发数据] ### 服读/服写/客读/客写 每次 1000/500/500/1000 字节 双方可以正常收发数据. 观察 netstat 数据, 双方的两个队列中字节数一直为 0. !![7-4-2,正常收发数据] ### 服读/服写/客读/客写 每次 1000/1000/700/700 字节 服务端每次读/写的字节数和客户端每次写/读的字节数不同, 因此双方虽然可以正常收发数据, 但流量限制在每次 700 字节. 其中的一个例外是客户端第二次接收到的是 300 字节而不是 700 字节. 观察 netstat 数据, 客户端的接收队列字节数开始是 300, 1000, 后以 1000, 1300, 1600, 1900 的等差形式递增. !![7-4-3,正常收发数据但队列增长] 这种情况的解释与 "服务端先写后读, 客户端先读后写" 的情况类似.