第五章 基于 TCP 的服务器端/客户端(2)
本章将详细讲解 TCP 中必要的理论知识,还将给出第 4 章客户端问题的解决方案。
5.1 回声客户端的完美实现
5.1.1 回声服务端没有问题,只有客户端有问题?
问题不在服务器端,而在客户端,但只看代码也许不太好理解,因为 I/O 中使用了相同的函数。先回顾一下回声服务器端的 I/O 相关代码,下面是第四章 echo_server.c
的部分代码。
while((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
write(clnt_sock, message, str_len);
接着是第四章 echo_client.c
代码
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);
二者都在循环调用 read 或 write 函数。实际上之前的回声客户端将 100% 接收自己传输的数据,只不过接受数据时的单位有些问题。扩展客户端代码回顾范围,下面还是 echo_client.c
的代码:
while (1) {
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
....
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
回声客户端传输的是字符串,而且是通过调用 write 函数一次性发送的。之后还调用一次 read 函数,期待着接受自己传输的字符串,这就是问题所在。(本人认为这里应该指收到的回声不是完整的)
5.1.2 回声客户端问题解决方法
因为可以提前确定接收数据的大小,客户端可以循环调用 read 函数直到大小满足。
书上参考代码一些我的见解
如 read 函数第三个参数设置问题,应该会造成数组越界
逻辑上收到的数据大小应该不会超过发出的大小,用不等式判断没有问题,但可能死循环。书上使用大小判断解决,但是我认为出现这种读超了应该看看超出部分的数据是什么,因为自己代码逻辑并不会超。
代码略,思路并不复杂。
5.1.3 如果问题不在于回声客户端:定义应用层协议
回声客户端可以提前知道接收的数据长度,但这在更多情况下是不可能的。若无法预知接收数据长度时应如何收发数据?这时需要的是应用层协议的定义。
在收发过程中需要定好规则(协议)以表示数据的边界,或提前告知收发数据的大小。服务端/客户端实现过程中逐步定义的这些规则集合就是应用层协议。
请自己实现一个程序来实现计算器功能。
客户端发送需要计算内容,服务端返回计算结果,客户端呈现计算结果。
我自己的实现参考文件 op_client.c
和 op_server.c
和书上方法不同。
我的方法是
mywrite
函数,每次先写两位,表示接下来发送数据长度,在发送数据。
myread
函数,每次先读取两位,表示接下来要接收数据长度,在接收数据。
纯 c 语言写表达式求值太麻烦,就随便实现了一下加减法。
运行结果
# 服务端
wzy@wzypc:~/TCP-IP-NetworkNote/chapter-05$ gcc op_server.c -o opserver.exe
wzy@wzypc:~/TCP-IP-NetworkNote/chapter-05$ ./opserver.exe 9190
Connect client 1
接收到的表达式: 5+2+6-1
返回答案: 12
# 客户端
wzy@wzypc:~/TCP-IP-NetworkNote/chapter-05$ gcc op_client.c -o opclient.exe
wzy@wzypc:~/TCP-IP-NetworkNote/chapter-05$ ./opclient.exe 127.0.0.1 9190
Connected...
输入表达式:
5+2+6-1
result: 12
5.1.4 计算器服务器端/客户端示例
略
5.2 TCP 原理
5.2.1 TCP 套接字中的 I/O 缓冲
TCP 套接字的数据收发无边界。服务器端即使调用 1 次 write 函数传输 40 字节的数据,客户端也有可能通过 4 次 read 函数调用每次读取 10 字节。但此处也有一些疑问,服务器端一次性传输了 40 字节,而客户端居然可以缓慢地分批接收。客户端接受 10 字节后,剩下的 30 字节在何处等候呢?
实际上,write 函数调用后并非立即传输数据,read 函数调用后也并非马上接收数据。write 函数调用瞬间,数据将移至输出缓冲;read 函数调用瞬间,从输入缓冲读取数据。
这些 I/O 缓冲特性可整理如下:
- I/O 缓冲在每个 TCP 套接字中单独存在
- I/O 缓冲在创建套接字时自动生成
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据
- 关闭套接字将丢失输入缓冲中的数据
那么,下面这种情况会会引发什么事情?
客户端输入缓冲为 50 字节,而服务器端传输了 100 字节。
根本不会发生这类问题,因为 TCP 会控制数据流。TCP 中有滑动窗口(Sliding Window)协议,用对话方式呈现如下:
A:你好,最多可以向我传递 50 字节
B:好的
A:我腾出了 20 字节的空间,最多可以收 70 字节
B:好的
数据收发也是如此,因此 TCP 中不会因为缓冲溢出而丢失数据。
write 函数返回在数据移到输出缓冲时,因为 TCP 保证对输出缓冲区数据的传输,所以说 write 函数在数据传输完成时返回。
5.2.2 TCP 内部工作原理 1:与对方套接字的连接
TCP 套接字从创建到消失所经过程分为如下 3 步:
- 与对方套接字建立连接
- 与对方套接字进行数据交换
- 断开与对方套接字的连接
首先讲解与对方套接字建立连接的过程。该过程又被称为 Three-way handshaking(三次握手)。
三次握手过程自行查阅《TCP/IP 详解卷一》。
5.2.2 TCP 内部工作原理 2:与对方主机的数据交换
略
详见《TCP/IP 详解卷一》
5.2.3 TCP 的内部工作原理 3:断开与套接字的连接
四次挥手
详见《TCP/IP 详解卷一》
5.3 基于 Windows 的实现
略
5.4 习题
以下是我的理解
- 请说明 TCP 套接字连接设置的三次握手过程。尤其是 3 次数据交换过程每次收发的数据内容。
- 第一次握手:建立连接时,客户端发送 SYN 包(seq=j)到服务器,并进入 SYN_SENT 状态,等待服务器确认。SYN:同步序列编号
- 第二次握手:服务器收到 SYN 包,回复 SYN+ACK 包,(ack=j+1)(seq=k)此时服务器进入 SYN_RECV 状态
- 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1),完成三次握手。
- TCP 是可靠的数据传输协议,但在通过网络通信的过程中可能丢失数据。请通过 ACK 和 SEQ 说明 TCP 通过何种机制保证丢失数据的可靠传输。
ACK 表示下个需要接收的序列号起始,SEQ 是当前发送数据的起始序号。收到某个 ACK,说明这个 ACK 之前的数据全部正确发送。等待 ACK 超时则说明数据丢失需要重传。
- TCP 套接字中调用 write 和 read 函数时数据如何移动?结合 I/O 缓冲进行说明。
TCP 套接字调用 write 函数时,数据将移至输出缓冲区。由 TCP 协议栈完成传输到对方输入缓冲区。
调用 read 函数从输入缓冲区中读取数据。
- 对方主机的输入缓冲剩余 50 字节空间时,若本主机通过 write 函数请求传输 70 字节,请问 TCP 如何处理这种情况?
因为滑动窗口协议,能发送的只有前 50 字节,多余的 20 字节存储在发送缓冲区直到对方通告窗口空间覆盖到相应字节序号。
-
我的计算器服务器端和客户端收发数据是类似功能,一个固定长度的前缀表示接下来需要收发消息的长度。
-
创建收发文件的服务器端/客户端
略