问题
下面的代码输出什么?为什么?
Receive: ABC
因为 TCP 是流式传输协议,它将应用层的数据以字节流的形式一个一个字节传入自己的缓冲区,然后再发送,接收端 recv 后,把收到的数据放入接收缓冲区,再把接收缓冲区的数据拷贝到 buf 中,所以我们读到的是一连串粘连的数据,这就是 TCP 的粘包现象。
小知识
发送缓冲区
- 数据先进入发送缓冲区,之后由操作系统送往远端主机
接收缓冲区
- 远端的数据被操作系统接收后,放入接收缓冲区
- 之后应用程序从接收缓冲区读取数据
TCP 应用编程中的 "问题"
数据接收端无法知道数据的发送方式!!!
接收端无法知道 "ABC" 是分开3次进行发送的!
网络编程中的期望
每次发送一条完整的消息,每次接收一条完整的消息
即使接收缓冲区有多条消息,也不会出现消息粘连
消息中涵盖了数据类型和数据长度等信息
应用层协议设计
什么是协议?
- 协议是通信双方为数据交换而建立的规则、标准或约定的集合
协议对数据传输的作用
- 通信双方根据协议能够正确收发数据
- 通信双方根据协议能够解释数据的意义
协议设计示例
目标:设计可用于数据传输的协议
完整消息包含
- 数据头:数据类型 (数据区用途,固定长度)
- 数据长度:数据区长度 (固定长度)
- 数据区:字节数据 (变长区域)
因此:
- 消息至少12个字节 (消息头 + 数据长度)
- 通过计算消息的总长度,能够避开数据粘连的问题
柔性数组即数组大小待定的数组;C 语言可以由结构体产生柔性数组;C 语言结构体的最后一个元素可以是大小未知的数组。Message 中的 payload 仅是一个待使用的标识符,不占用存储空间。
把 payload 设置为柔性数组的意义是:首先可以动态分配它的内存空间,其次 Meaage 只需要 malloc 一次即可,如果把 payload 设置为指针的话,除了 Message 需要 malloc,payload 也需要 malloc。
应用层协议设计与实现
message.h
#ifndef MESSAGE_H
#define MESSAGE_H
typedef struct message
{
unsigned short type;
unsigned short cmd;
unsigned short index;
unsigned short total;
unsigned int length;
unsigned char payload[];
} Message;
Message* Message_New(unsigned short type,
unsigned short cmd,
unsigned short index,
unsigned short total,
const char* payload,
unsigned int length);
#endif
message.c
#include "message.h"
#include <malloc.h>
#include <string.h>
Message* Message_New(unsigned short type, unsigned short cmd, unsigned short index, unsigned short total, const char* payload, unsigned int length)
{
Message* ret = (Message*)malloc(sizeof(Message) + length);
if(ret)
{
ret->type = type;
ret->cmd = cmd;
ret->index = index;
ret->total = total;
ret->length = length;
if(payload)
{
memcpy(ret + 1, payload, length);
}
}
return ret;
}
client.c
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include "message.h"
int main()
{
int sock = -1;
struct sockaddr_in addr = {0};
char input[32] = {0};
char buf[128] = {0};
int n = 0;
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("socker error\n");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(8888);
if(connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1)
{
printf("connect error\n");
return -1;
}
printf("connect succeed\n");
Message* pm = Message_New(0, 0, 1, 3, "A", 1);
send(sock, pm, sizeof(Message) + 1, 0);
pm = Message_New(0, 0, 2, 3, "B", 1);
send(sock, pm, sizeof(Message) + 1, 0);
pm = Message_New(0, 0, 3, 3, "C", 1);
send(sock, pm, sizeof(Message) + 1, 0);
close(sock);
return 0;
}
server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
int main()
{
int server = 0;
struct sockaddr_in saddr = {0};
int client = 0;
struct sockaddr_in caddr = {0};
socklen_t csize = 0;
char buf[64] = {0};
int r = 0;
server = socket(AF_INET, SOCK_STREAM, 0);
if(server == -1)
{
printf("server socket error\n");
return -1;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
saddr.sin_port = htons(8888);
if(bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1)
{
printf("server bind error\n");
return -1;
}
if(listen(server, 1) == -1)
{
printf("server listen error\n");
return -1;
}
printf("start to accept\n");
while(1)
{
csize = sizeof(caddr);
client = accept(server, (struct sockaddr*)&caddr, &csize);
if(client == -1)
{
printf("server accept error\n");
return -1;
}
printf("client = %d\n", client);
do
{
r = recv(client, buf, sizeof(buf), 0);
for(int i = 0; i < r; i++)
{
printf("%02X ", buf[i]);
}
printf("\n");
}while(r > 0);
close(client);
}
close(server);
return 0;
}
程序运行结果如下:
服务端成功接收到了客户端发来的 Message。我们以16进制方式,按字节的方式,打印了 Message 中的内容。
注意:printf 函数对于终端这类交互式设备来说是行缓冲的,当出现换行符或者它的缓冲区被填满或者当前程序运行结束,它才会将缓冲区中的数据打印到终端上,否则,就不会打印出来。
思考
如何在代码层面封装协议细节 (仅关系消息本身)?