【C语言系统编程】【第三部分:网络编程】3.2 数据传输和协议

news/2024/12/21 22:17:55/
3.2 数据传输和协议

这一部分将探索网络传输中数据的组织和操纵方式,包括数据封包和拆包、数据完整性校验以及数据序列化与反序列化的方法。这些知识对确保数据可靠和高效传输至关重要。

3.2.1 数据传输
3.2.1.1 数据封包与拆包
  • 定义:数据封包是指将数据按照一定的协议格式进行组织,将其封装成包以便于在网络上传输。数据拆包则是指在接收端将封装的数据包还原回原始数据的过程。

  • 作用:封包将分散的数据组合成固定格式的数据包,以确保数据传输的可靠性和可解析性;拆包则确保接收到的数据包能够还原回具有实际意义的原始数据。

  • 示例代码解析

#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
在上述代码中,`pack_data`函数将字符串`input`封装到`Packet`结构中,而`unpack_data`函数将`Packet`结构中的数据解封回原始字符串。
3.2.1.2 数据完整性与校验(Checksum)
  • 定义:校验和是一种用于检测数据传输错误的技术,通过对数据进行一定的数学运算生成校验值,并在数据传输时附加到数据末尾,接收端通过相同运算验证数据的完整性。

  • 作用:确保数据在传输过程中没有受到损坏或篡改。

  • 示例代码解析

#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() 输出原始消息及其对应的校验和值。
在上述代码中,`checksum`函数计算字符串`message`的校验和,结果用于检测数据传输中的错误。
3.2.1.3 数据序列化与反序列化(JSON, XML, Protocol Buffers, etc.)
  • 定义
    • 数据序列化:将复杂的数据结构转换为便于存储和传输的格式,如JSON、XML、Protocol Buffers等。
    • 数据反序列化:将序列化格式的数据转换回原来的数据结构。
  • 作用:增强不同系统之间的数据交换能力,将数据在不同语言和平台之间传输和解释。
  • 示例(使用JSON):
#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 对象。
在上述代码中,使用`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请求示例

GET /index.html HTTP/1.1
Host: www.example.comHTTP/1.1 200 OK
Content-Type: text/html<html>...</html>
3.2.2.2 FTP 协议基础

FTP(File Transfer Protocol) 用于在客户端和服务器之间传输文件,允许文件的上传、下载、删除等操作。

  • 作用:进行文件传输。
  • 特点
    • 双通道通信:控制通道和数据通道分开(21端口用于控制,20端口用于数据)。
    • 身份验证:支持匿名登录和基于用户名、密码的登录。

典型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命令
      HELO domain.com
      MAIL FROM:<sender@domain.com>
      RCPT TO:<recipient@domain.com>
      DATA
      
  • POP3
    • 作用:邮件接收。
    • 特点
      • 下载邮件到本地并从服务器删除(通常)。
      • 简单且高效,适合于低带宽环境。
    • 典型POP3命令
      USER username
      PASS password
      LIST
      RETR 1
      
3.2.2.4 DNS 协议基础

DNS(Domain Name System):用于将域名解析为IP地址,以便客户端可以找到并连接到服务器。

  • 作用:域名解析。
  • 特点
    • 分层结构:分为根域、顶级域、二级域等。
    • 缓存机制:为了提高查询效率,DNS采用多级缓存。
    • 递归查询与迭代查询:通过多级DNS服务器进行查询。

DNS查询示例

$ nslookup www.example.com
Server:  dns.example.com
Address:  192.0.2.1Name:    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  |
------------------------------

解析数据帧示例代码

  • 示例代码解析
#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)技术,在发送数据时添加冗余信息,接收端可以通过冗余信息纠正错误。

超时重传示例代码

  • 示例代码解析
#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) 让程序在每秒后检查一次是否收到应答,以实现逐秒检查,直到超时。

以上代码展示了如何设计自定义协议,解析数据帧,以及实施常见的错误处理机制。通过自定义协议,开发者可以灵活控制数据传输的各个方面,以满足特定应用的需求。


http://www.ppmy.cn/news/1537036.html

相关文章

PHP如何更改要上传的文件大小的最大值

在PHP中&#xff0c;要更改要上传的文件大小的最大值&#xff0c;需要调整一些配置文件和参数。这些参数决定了PHP脚本可以处理的最大文件大小、上传文件的最大大小以及脚本可以使用的最大内存量等。以下是一些详细的步骤和参数解释&#xff0c;帮助你配置PHP以允许更大的文件上…

chatGPT模型接口分享

前言: 仅供学习和交流&#xff0c;请合理使用。 API&#xff1a;https://api.gptnet.org key&#xff1a;sk-x9Rmq3HeHh5z9EIi8wFaXCl02OfxRSk5UAFodYm1o4zo5X3i 支持模型&#xff1a;gpt-3.5-turbo、gpt-3.5-turbo-16k、gpt-4o-mini、llama-3.1-405b 暂时支持以上四个模型…

44 C 语言输入输出流、scanf 与 printf 函数详解、清除输入缓冲区

目录 1 文件基本介绍 1.1 文件的主要功能 1.2 输入输出流 2 C 语言中的输入与输出 2.1 输入 2.2 输出 2.3 标准文件与文件指针 3 scanf() 函数详解 3.1 功能描述 3.2 函数原型 3.3 常用格式说明符 3.4 返回值 3.5 注意事项 3.5.1 处理空白字符 3.5.2 防止缓冲区…

Object.defineProperty()总结

概述&#xff1a;Object.defineProperty() 方法用于在对象上定义或修改一个属性 语法&#xff1a; //obj&#xff1a;要在其上定义或修改属性的对象。 //prop&#xff1a;要定义或修改的属性的名称或 Symbol。 //descriptor&#xff1a;定义或修改属性的属性描述符。 Object.…

打不死的超强生命力

水熊虫是你可能听说过的小生物&#xff0c;它们能够在极端环境中生存&#xff0c;堪称地球上的“超强幸存者”。数十年来&#xff0c;科学家们试图通过各种极端实验杀死它们&#xff0c;但无论是把它们以900米/秒的速度发射&#xff0c;还是将它们暴露在宇宙辐射下&#xff0c;…

MR30系列IO——工业自动化的智慧纽带

一、引言 在工业自动化技术的广阔天地中&#xff0c;MR30系列IO模块、数字量模块以及模拟量模块构成了控制系统的核心基石。它们被广泛应用于可编程逻辑控制器&#xff08;PLC&#xff09;、分布式控制系统&#xff08;DCS&#xff09;等多种自动化系统中&#xff0c;为工业生…

深入了解 【ObjectMapper】:Java 中的 JSON 解析利器

深入了解 ObjectMapper&#xff1a;Java 中的 JSON 解析利器 在现代开发中&#xff0c;处理 JSON 已成为构建应用程序的重要组成部分。对于 Java 开发者来说&#xff0c;Jackson 是一个强大的库&#xff0c;它能方便、高效地在 Java 对象和 JSON 数据之间进行转换&#xff0c;…

力扣之1322.广告效果

题目&#xff1a; sql建表语句&#xff1a; Create table If Not Exists Ads (ad_id int,user_id int,action ENUM (Clicked, Viewed, Ignored) ); Truncate table Ads; insert into Ads (ad_id, user_id, action) values (1, 1, Clicked); insert into Ads (ad_id, use…