5.1 回声客户端的完美实现
2025/10/5大约 4 分钟
5.1 回声客户端的完美实现
回声客户端的问题原因是使用TCP传输的无数据边界特性。没办法控制TCP,只能在应用层解决数据边界问题。
根据输入确定数据边界
边界包含两个性质:
- 数据起点
- 数据长度
记录下输入字符串的长度,接收到输入长度的数据就是完整的服务端的回复
实现:
int recv_len = 0, recv_cnt = 0;
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
str_len = write(sock, message, strlen(message));
recv_len = 0;
while(recv_len < str_len)
{
recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
if(recv_cnt == -1)
unix_error("read() error");
recv_len += recv_cnt;
}
message[str_len] = 0;
printf("Message from server: %s\n", message);
}
close(sock);
return 0;| 行号 | 功能 | 说明 |
|---|---|---|
| 8 | 存储发送消息的长度 | |
| 9 | 重置接收消息的长度 | |
| 10 | 判断是否已经收到完整消息 | 通过比较已接收消息的长度和发送消息的长度 |
| 12 | 读取当前接收消息 | |
| 15 | 计算发送开始到目前接收的所有消息的长度 |
定义应用层协议
在应用层 收发数据的过程中定义好规则(协议)以表示数据边界,或提前告知收发数据的大小。
计算器应用协议
功能:客户端提供要计算的两个数据,以及计算方式,服务端返回计算结果。
数据类型固定为float,满足 IEEE-754 标准 ,网络字节序收发
客户端发送的报文格式:
| 字节序号 | 值 | 说明 | 长度 |
|---|---|---|---|
| 0 | 0x66 | 起始标识 | 1 |
| 1~4 | 第一个操作数 | 4 | |
| 5~8 | 第二个操作数 | 4 | |
| 9 | “+”、“-”、“*“、”/“ | 用于计算的四则运算字符 | 1 |
固定长度为10个字节
服务端回复的报文格式:
| 字节序号 | 值 | 说明 | 长度 |
|---|---|---|---|
| 0 | 0x66 | 起始标识 | 1 |
| 1~4 | 计算结果 |
关于数据的字节序
float的格式:
| bit | 31 | 30~24 | 23~0 |
|---|---|---|---|
| 含义 | 符号位 | 指数 | 小数 |
符号位在最高有效字节内。
将值存储在字节数组中,
如果系统是大端字节序,最高有效字节本身就存储在低地址,可以把float数据按字节拷贝到发送数组中。
如果系统是小端字节序,最高有效字节存储在高位地址,需要调换
如浮点数 3.14f
按照标准其十六进制值为:0x4048F5C3,按网络字节序发送:0x40, 0x48, 0xF5, 0xC3
目前使用的平台是小端,需要转换为大端
客户端
实现:
// connect() ...
float operand0, operand1;
char *p = nullptr;
message[0] = 0x66;
printf("Operand0:");
scanf("%f", &operand0);
printf("\n");
p = (char*)&operand0;
message[1] = *(p + 3);
message[2] = *(p + 2);
message[3] = *(p + 1);
message[4] = *(p + 0);
printf("Operand1:");
scanf("%f", &operand1);
printf("\n");
p = (char*)&operand0;
message[5] = *(p + 3);
message[6] = *(p + 2);
message[7] = *(p + 1);
message[8] = *(p + 0);
printf("Operator:");
scanf(" %c", &message[9]);
printf("\n");
write(sock, message, 10);
char recvMsg[BUF_SIZE];
int recv_len = 0, recv_cnt = 0;
float result = 0.0;
bool recvResult = false;
while(recv_len < 5)
{
recv_cnt = read(sock, &recvMsg[recv_len], sizeof(recvMsg) - recv_len);
recv_len += recv_cnt;
if(recv_len >= 5)
{
for(int i=0; i<recv_len; i++)
{
if(recvMsg[i] == 0x66)
{
char *pr = (char*)&result;
pr[3] = recvMsg[i + 1];
pr[2] = recvMsg[i + 2];
pr[1] = recvMsg[i + 3];
pr[0] = recvMsg[i + 4];
printf("result:%f\n", result);
recvResult = true;
}
}
}
}
if(!recvResult)
printf("Not found\n");
close(sock);| 行号 | 功能 | 说明 |
|---|---|---|
| 6-13 | 获取第一个操作数并转换为网络字节序(高位字节存储在低位地址) | 系统使用的是小端字节序,所以需要转换 |
| 24-26 | 获取操作符 | 在输入第二个操作数后按回车将数据保存到operand1中,此时Enter 还保存在输入缓冲中,直接使用scanf("%c") 获取到的是'\n' - 0x0a,而不是实际输入的字符。使用scanf(" %c") 跳过所有空白字符(包括回车、空格和tab) |
| 33 | 长度足够后只进行一次解析 | 无法解析直接退出 |
| 37 | 在满足最小有效数据长度时进行解析 | |
| 35、36 | 接收到多条消息时进行拼接 | |
| 41 | 找起始标识 | |
| 43-47 | 获取结果,转换为小端字节序 |
效果:
计算3.14 / 3.14,服务端接收:
66 40 48 F5 C3 40 48 F5 C3 2F回复:
66 3F 80 00 00客户端打印:
Connected ...
Operand0:3.14
Operand1:3.14
Operator:/
result:1.000000其他测试情况:
+
客户端发送:
66 40 48 F5 C3 40 48 F5 C3 2B服务端回复:
66 40 C8 F5 C3客户端打印:
Connected ...
Operand0:3.14
Operand1:3.14
Operator:+
result:6.280000*
客户端发送:
66 40 48 F5 C3 40 48 F5 C3 2A服务端回复:
66 41 1D C0 EC客户端打印:
Connected ...
Operand0:3.14
Operand1:3.14
Operator:*
result:9.859600有多余数据
回复消息包含多余消息
客户端发送:
66 40 48 F5 C3 40 48 F5 C3 2D //3.14 - 2.68服务端回复:
0A 0B 66 3E EB 85 1F客户端打印:
Connected ...
Operand0:3.14
Operand1:2.68
Operator:-
result:0.460000分两次回复结果
客户端发送:
66 3F 8C CC CD 3F 8C CC CD 2B //1.1 + 2.2第一次回复:
66 40 53第二次回复:
33 33客户端打印:
Connected ...
Operand0:1.1
Operand1:2.2
Operator:+
result:3.300000回复报文不带标识
65 40 53 33 33结果:
Not found