3.2 数据传输和协议
这一部分将探索网络传输中数据的组织和操纵方式,包括数据封包和拆包、数据完整性校验以及数据序列化与反序列化的方法。这些知识对确保数据可靠和高效传输至关重要。
3.2.1 数据传输
3.2.1.1 数据封包与拆包
-
定义:数据封包是指将数据按照一定的协议格式进行组织,将其封装成包以便于在网络上传输。数据拆包则是指在接收端将封装的数据包还原回原始数据的过程。
-
作用:封包将分散的数据组合成固定格式的数据包,以确保数据传输的可靠性和可解析性;拆包则确保接收到的数据包能够还原回具有实际意义的原始数据。
-
示例代码解析:
c
#include <stdio.h>
#include <string.h>
// 定义结构体 Packet
struct Packet {
unsigned int length; // 数据包长度 [1]
char data[256]; // 数据内容 [2]
};
// 函数 pack_data:打包数据
void pack_data(const char *input, struct Packet *packet) {
packet->length = strlen(input); // 设置数据包长度 [3]
strcpy(packet->data, input); // 拷贝数据到包中 [4]
}
// 函数 unpack_data:解包数据
void unpack_data(const struct Packet *packet, char *output) {
strncpy(output, packet->data, packet->length); // 拷贝数据 [5]
output[packet->length] = '\0'; // 追加字符串结束符 [6]
}
int main() {
char message[] = "Hello, world!";
struct Packet packet;
char unpacked_message[256];
pack_data(message, &packet); // 打包数据 [7]
unpack_data(&packet, unpacked_message); // 解包数据 [8]
printf("Original message: %s\n", message);
printf("Packed message length: %d\n", packet.length);
printf("Unpacked message: %s\n", unpacked_message);
return 0;
}
- [1] 数据包长度 :
unsigned int length
用于存储数据包的长度,以字节为单位。 - [2] 数据内容 :
char data[256]
定义了一个最大存储256字节的字符数组,用于承载数据内容。 - [3] 设置数据包长度 :通过
strlen()
函数计算input
的长度,以标记实际承载数据的长度。 - [4] 拷贝数据到包中 :
strcpy()
函数用于将字符串从input
拷贝到结构体的data
字段中。 - [5] 拷贝数据 :
strncpy()
从数据包的data
字段拷贝出length
字节的数据放入输出缓冲区output
。 - [6] 追加字符串结束符 :对
output
在最后一位追加空字符结束符,以形成正确的字符串。 - [7] 打包数据 :调用
pack_data()
将message
中的数据打包到packet
。 - [8] 解包数据 :通过
unpack_data()
从packet
解包出数据到unpacked_message
。
markdown
在上述代码中,`pack_data`函数将字符串`input`封装到`Packet`结构中,而`unpack_data`函数将`Packet`结构中的数据解封回原始字符串。
3.2.1.2 数据完整性与校验(Checksum)
-
定义:校验和是一种用于检测数据传输错误的技术,通过对数据进行一定的数学运算生成校验值,并在数据传输时附加到数据末尾,接收端通过相同运算验证数据的完整性。
-
作用:确保数据在传输过程中没有受到损坏或篡改。
-
示例代码解析:
c
#include <stdio.h>
// 函数 checksum:计算字符串数据的校验和
unsigned int checksum(const char *data) {
unsigned int sum = 0; // 初始化校验和值为 0 [1]
while (*data) { // 遍历字符串直到末尾 [2]
sum += *data++; // 将每个字符的 ASCII 值累加到 sum 中 [3]
}
return ~sum; // 返回 sum 的按位取反值 [4]
}
int main() {
char message[] = "Hello, world!";
unsigned int cs = checksum(message); // 计算校验和 [5]
printf("Message: %s\n", message);
printf("Checksum: %u\n", cs); // 打印校验和 [6]
return 0;
}
- [1] 初始化校验和值 :
unsigned int sum = 0
初始化了一个无符号整数用于累加字符串中每个字符的 ASCII 值。 - [2] 遍历字符串 :通过
while (*data)
循环遍历字符串各个字符,直到遇到空字符\0
(字符串结束)。 - [3] 字符累加 :
sum += *data++
累加每个字符的 ASCII 值到sum
中,并将指针移向下一个字符。 - [4] 按位取反 :
~sum
返回累加结果的按位取反值,常用于生成更复杂的校验和以用于校验机制。 - [5] 计算校验和 :在
main
函数中调用checksum()
函数对message
进行校验和计算。 - [6] 打印校验和 :通过
printf()
输出原始消息及其对应的校验和值。
markdown
在上述代码中,`checksum`函数计算字符串`message`的校验和,结果用于检测数据传输中的错误。
3.2.1.3 数据序列化与反序列化(JSON, XML, Protocol Buffers, etc.)
- 定义 :
- 数据序列化:将复杂的数据结构转换为便于存储和传输的格式,如JSON、XML、Protocol Buffers等。
- 数据反序列化:将序列化格式的数据转换回原来的数据结构。
- 作用:增强不同系统之间的数据交换能力,将数据在不同语言和平台之间传输和解释。
- 示例(使用JSON):
c
#include <stdio.h>
#include <jansson.h> // 用于 JSON 库的包含 [1]
// 定义结构体 Person
typedef struct {
char name[50];
int age;
} Person;
// 函数 serialize:序列化 Person 对象
void serialize(const Person *p, char *out) {
json_t *root = json_object(); // 创建 JSON 对象 [2]
json_object_set_new(root, "name", json_string(p->name)); // 设置名称字段 [3]
json_object_set_new(root, "age", json_integer(p->age)); // 设置年龄字段 [4]
strcpy(out, json_dumps(root, 0)); // 将 JSON 对象序列化为字符串 [5]
json_decref(root); // 渐减对象引用计数以释放资源 [6]
}
// 函数 deserialize:反序列化 JSON 字符串
void deserialize(const char *in, Person *p) {
json_t *root;
json_error_t error;
root = json_loads(in, 0, &error); // 从字符串加载 JSON 对象 [7]
if (!root) {
fprintf(stderr, "error: on line %d: %s\n", error.line, error.text); // 错误处理
return;
}
json_t *name = json_object_get(root, "name"); // 获取名称字段 [8]
json_t *age = json_object_get(root, "age"); // 获取年龄字段 [9]
if (json_is_string(name)) {
strcpy(p->name, json_string_value(name)); // 复制字符串值 [10]
}
if (json_is_integer(age)) {
p->age = json_integer_value(age); // 获取整数值 [11]
}
json_decref(root); // 释放 JSON 对象 [12]
}
int main() {
Person p1 = {"John Doe", 30};
char json_data[256];
serialize(&p1, json_data); // 序列化 Person 到 JSON 字符串 [13]
printf("Serialized JSON: %s\n", json_data);
Person p2;
deserialize(json_data, &p2); // 从 JSON 字符串反序列化到 Person [14]
printf("Deserialized Person: Name = %s, Age = %d\n", p2.name, p2.age);
return 0;
}
- [1] 使用 Jansson 库 :
jansson.h
是一个用于处理 JSON 数据的 C 库。 - [2] 创建 JSON 对象 :
json_t *root = json_object();
创建一个新的 JSON 对象。 - [3] 设置名称字段 :使用
json_object_set_new()
将name
字段添加到 JSON 对象中,值为字符串。 - [4] 设置年龄字段 :使用
json_object_set_new()
将age
字段添加到 JSON 对象中,值为整数。 - [5] 序列化为字符串 :
json_dumps(root, 0)
将 JSON 对象转换为 JSON 格式的字符串,并复制到输出缓冲区out
。 - [6] 释放资源 :
json_decref(root);
減少root
的引用计数,并在需要时释放内存。 - [7] 从字符串加载 JSON 对象:将 JSON 格式的字符串转换回 JSON 对象。
- [8] 获取名称字段 :从 JSON 对象中获取
name
字段。 - [9] 获取年龄字段 :从 JSON 对象中获取
age
字段。 - [10] 复制字符串值 :如果是字符串值,使用
strcpy
复制到Person
结构体中的name
字段。 - [11] 获取整数值 :如果是整数值,使用
json_integer_value
获取并赋值给Person
结构体中的age
。 - [12] 释放 JSON 对象 :释放
root
对象的内存。 - [13] 序列化 Person 到 JSON 字符串 :通过
serialize()
函数将Person
对象转换为 JSON 格式字符串。 - [14] 从 JSON 字符串反序列化到 Person :使用
deserialize()
函数将 JSON 字符串转换回Person
对象。
markdown
在上述代码中,使用`jansson`库函数将名为`Person`的结构体序列化为JSON格式字符串,并将其反序列化回来。
3.2.2 常见协议
在网络编程中,理解和掌握一些常见的应用层协议非常重要。以下是一些广泛使用的协议及其基础概述。
3.2.2.1 HTTP/HTTPS 协议基础
HTTP(HyperText Transfer Protocol) :是用于万维网上信息传输的基础协议。HTTPS(HTTP Secure) 则是在HTTP的基础上加入了SSL/TLS层,用于加密通信。
- 作用:用于传输网页文档、表单数据、图像等资源。
- 特点 :
- 请求 - 响应模型:客户端发出请求,服务器进行响应。
- 无状态:每次请求均独立,服务器不会保留之前请求的状态。
- 安全性:HTTPS通过SSL/TLS来加密数据,保证数据的机密性和完整性。
典型HTTP/HTTPS请求示例:
http
GET /index.html HTTP/1.1
Host: www.example.com
HTTP/1.1 200 OK
Content-Type: text/html
<html>...</html>
3.2.2.2 FTP 协议基础
FTP(File Transfer Protocol) 用于在客户端和服务器之间传输文件,允许文件的上传、下载、删除等操作。
- 作用:进行文件传输。
- 特点 :
- 双通道通信:控制通道和数据通道分开(21端口用于控制,20端口用于数据)。
- 身份验证:支持匿名登录和基于用户名、密码的登录。
典型FTP命令示例:
ftp
USER username
PASS password
LIST
RETR filename
3.2.2.3 SMTP 和 POP3 协议基础
SMTP(Simple Mail Transfer Protocol) 用于邮件发送,POP3(Post Office Protocol 3) 用于邮件接收。
-
SMTP :
-
作用:邮件发送。
-
特点 :
- 服务器间邮件传递。
- 用户名密码验证常用于发送邮件。
-
典型SMTP命令 :
smtpHELO domain.com MAIL FROM:<sender@domain.com> RCPT TO:<recipient@domain.com> DATA
-
-
POP3 :
-
作用:邮件接收。
-
特点 :
- 下载邮件到本地并从服务器删除(通常)。
- 简单且高效,适合于低带宽环境。
-
典型POP3命令 :
pop3USER username PASS password LIST RETR 1
-
3.2.2.4 DNS 协议基础
DNS(Domain Name System):用于将域名解析为IP地址,以便客户端可以找到并连接到服务器。
- 作用:域名解析。
- 特点 :
- 分层结构:分为根域、顶级域、二级域等。
- 缓存机制:为了提高查询效率,DNS采用多级缓存。
- 递归查询与迭代查询:通过多级DNS服务器进行查询。
DNS查询示例:
dns
$ nslookup www.example.com
Server: dns.example.com
Address: 192.0.2.1
Name: www.example.com
Address: 93.184.216.34
这些协议是网络编程中非常基础和重要的部分。理解这些协议的工作原理和使用方式,对于开发可靠的网络应用程序至关重要。随时深入理解和正确实现这些协议,可以有效避免在实际项目中出现的一些常见错误。
3.2.3 自定义协议
在进行网络编程时,有时需要设计并实现自定义协议,以满足特定应用的需求。自定义协议设计需要考虑到数据的传输、安全、效率等多方面因素。
3.2.3.1 自定义协议设计原则
在设计自定义协议时,有几个基本原则需要遵循:
- 清晰性:协议应具有清晰的语法和语义,使开发者能快速理解和实现。
- 扩展性:协议应允许未来的功能扩展,而不破坏现有功能。
- 安全性:数据传输过程中应考虑加密和校验,以保证数据不被篡改。
- 高效性:应尽可能减少网络带宽占用,提升数据传输效率。
- 容错性:协议应能处理各种网络异常情况,如数据丢失、重复和延迟。
3.2.3.2 数据帧格式与解析
设计数据帧格式时需要考虑:
- 头部信息:包括协议版本、数据类型、序列号等。
- 长度信息:数据帧的总长度,以便接收方知道何时完成接收。
- 实际数据:包含业务相关的数据。
- 校验信息:如校验和,用于校验数据完整性。
示例:自定义协议的数据帧格式:
------------------------------
| Version | Type | Length | Payload | Checksum |
------------------------------
| 1 Byte | 1 Byte | 2 Bytes | Variable | 2 Bytes |
------------------------------
解析数据帧示例代码:
- 示例代码解析:
c
#include <stdio.h>
#include <stdint.h>
#include <string.h>
// 数据帧结构
#pragma pack(1) // 指定内存对齐为1字节 [1]
typedef struct {
uint8_t version; // 版本号 [2]
uint8_t type; // 类型 [3]
uint16_t length; // 数据长度 [4]
uint8_t payload[256]; // 载荷数据 [5]
uint16_t checksum; // 校验和 [6]
} DataFrame;
// 校验和计算函数
uint16_t calculate_checksum(DataFrame* frame) {
uint16_t checksum = 0;
// 累加所有字节
uint8_t* data = (uint8_t*)frame;
for (size_t i = 0; i < frame->length + 4; i++) { // 计算除校验和外的所有字节 [7]
checksum += data[i];
}
return checksum;
}
// 数据帧解析函数
int parse_data_frame(uint8_t* data, size_t data_len, DataFrame* frame) {
if (data_len < 6) { // 基本帧长验证 [8]
return -1; // 数据不足
}
memcpy(frame, data, data_len); // 拷贝数据到帧 [9]
uint16_t received_checksum = frame->checksum; // 提取收到的校验和 [10]
frame->checksum = 0; // 清除校验和字段以便计算 [11]
if (calculate_checksum(frame) != received_checksum) { // 校验和比较 [12]
return -2; // 校验和错误
}
return 0; // 解析成功
}
int main() {
// 示例数据
uint8_t data[] = {1, 2, 0, 4, 'h', 'e', 'l', 'l', 0}; // 版本 1, 类型 2, 长度 4, 载荷 "hell"
DataFrame frame;
int ret = parse_data_frame(data, sizeof(data), &frame); // 解析数据帧 [13]
if (ret == 0) {
printf("解析成功: 版本=%d 类型=%d 长度=%d 数据=%s\n",
frame.version, frame.type, frame.length, frame.payload);
} else {
printf("解析失败: 错误码=%d\n", ret); // 打印错误信息 [14]
}
return 0;
}
- [1] 内存对齐 :
#pragma pack(1)
指令用于设置结构体成员对齐为1字节,以确保数据布局一致,适合于协议或文件读写。 - [2] 版本号 :
uint8_t version
存储数据帧的版本信息,通常用于区分不同的协议版本。 - [3] 类型 :
uint8_t type
表示数据帧的类型,可以根据应用进行定义和解读。 - [4] 数据长度 :
uint16_t length
指示实际有效载荷的字节数。 - [5] 载荷数据 :
uint8_t payload[256]
用于存储实际传输的数据。 - [6] 校验和 :
uint16_t checksum
用于数据完整性的验证。 - [7] 校验和计算 :在
calculate_checksum
函数中,以字节累加方式计算校验和,范围涵盖长度+4(version、type、length加实际载荷)。 - [8] 基本帧长验证 :在
parse_data_frame
调用前, 检查数据长度是否足以存储基本帧信息。 - [9] 拷贝数据到帧 :使用
memcpy
函数将源数据复制到数据帧结构中。 - [10] 提取收到的校验和:存储收到数据的校验和以便后续比较。
- [11] 清除校验和字段以便计算:将帧中的校验和设为0便于重新计算。
- [12] 校验和比较:计算当前帧的校验和并与收到的校验和比较以验证数据完整性。
- [13] 解析数据帧 :
parse_data_frame
函数利用示例数据解析成DataFrame
结构。 - [14] 打印错误信息 :根据
parse_data_frame
的返回值打印结果或错误信息。
3.2.3.3 常见错误处理机制(超时重传,错误校正)
- 超时重传:设置一个超时时间,如果在规定时间内未收到应答,则重传数据。
- 错误校正:如使用前向错误纠正(Forward Error Correction, FEC)技术,在发送数据时添加冗余信息,接收端可以通过冗余信息纠正错误。
超时重传示例代码:
- 示例代码解析:
c
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h> // 引入 sleep 函数
#define TIMEOUT 5 // 超时时间(秒) [1]
// 模拟数据发送函数
bool send_data(uint8_t* data, size_t len) {
return true; // 假设发送成功
}
// 模拟应答接收函数
bool receive_ack() {
sleep(3); // 假设3秒后收到应答 [2]
return true; // 假设成功接收到应答
}
int main() {
uint8_t data[] = "Hello, world!";
bool success = false;
for (int attempt = 0; attempt < 3; attempt++) { // 尝试发送3次 [3]
printf("发送数据,尝试 %d...\n", attempt + 1);
if (send_data(data, sizeof(data))) {
printf("等待应答...\n");
for (int t = 0; t < TIMEOUT; t++) { // 等待时间循环 [4]
if (receive_ack()) {
success = true; // 收到应答 [5]
break;
}
sleep(1); // 等待一秒后重试 [6]
}
}
if (success) {
printf("数据发送成功并收到应答。\n");
break;
} else {
printf("超时未收到应答,重试...\n");
}
}
if (!success) {
printf("最终数据发送失败。\n");
}
return 0;
}
-
[1] 超时时间 :
#define TIMEOUT 5
定义了等待应答的最大时间为 5 秒。在这段时间内,如果没有收到应答则认为超时。 -
[2] 模拟应答延迟 :在
receive_ack()
函数中,通过sleep(3)
模拟延迟 3 秒后收到应答。此处假设在给定时间后,系统能接收到应答。 -
[3] 发送重试机制 :
for (int attempt = 0; attempt < 3; attempt++)
定义了最多重试 3 次发送数据以确保数据成功发送并接收到应答。 -
[4] 等待时间循环 :内部的
for (int t = 0; t < TIMEOUT; t++)
用于处理超时,在不超过 timeout 的时间内等待应答。 -
[5] 收到应答 :如果
receive_ack()
返回true
,则表示成功接收到应答,设置success = true
并跳出等待循环。 -
[6] 一秒重试 :使用
sleep(1)
让程序在每秒后检查一次是否收到应答,以实现逐秒检查,直到超时。
以上代码展示了如何设计自定义协议,解析数据帧,以及实施常见的错误处理机制。通过自定义协议,开发者可以灵活控制数据传输的各个方面,以满足特定应用的需求。