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

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.com

HTTP/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.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  |
------------------------------

解析数据帧示例代码

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值