应用层协议设计与实现

问题

下面的代码输出什么?为什么?

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 函数对于终端这类交互式设备来说是行缓冲的,当出现换行符或者它的缓冲区被填满或者当前程序运行结束,它才会将缓冲区中的数据打印到终端上,否则,就不会打印出来。 

思考

如何在代码层面封装协议细节 (仅关系消息本身)?

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值