Linux 学习笔记(四十三):网络编程进阶——自定义协议解决 TCP 粘包与守护进程

📌 核心结论(TL;DR):

  1. TCP 粘包本质 :TCP 是字节流,没有边界,必须依靠应用层自定义协议来划分边界。

  2. 协议设计 :通常采用 [长度]\r\n[数据体]\r\n 的格式,接收方先读长度,再按长度读数据。

  3. 序列化:网络不能直接传结构体,需要把数据转成 JSON 字符串(序列化),接收后再转回结构体(反序列化)。

  4. 守护进程(Daemon) :Linux 后台服务程序。核心步骤:fork 退出父进程、setsid 创建新会话、重定向标准 I/O 到 /dev/null。

一、 为什么 TCP 会"粘包"?

UDP 是面向数据报的,一次发一个包,有边界;而 TCP 是面向字节流的,它是一堆连续的字节。如果发送方连续发了两个数据包,接收方底层 read 时,可能一次性把两个包都读到了(粘包),也可能只读到了半个包(半包)。

二、 应用层自定义协议

解决粘包的最常见做法,就是双方约定一个格式。我们约定报文格式为:有效载荷长度\r\n有效载荷内容\r\n。

三、 代码实战:协议的编解码

(插入 Encode 和 Decode 代码)

cpp 复制代码
// 编码:len\r\nx op y\r\n
std::string Encode(const std::string &message) {
    std::string len = std::to_string(message.size());
    return len + "\r\n" + message + "\r\n";
}

// 解码:判断数据完整性,并截取
bool Decode(std::string &package, std::string *message) {
    auto pos = package.find("\r\n");
    if (pos == std::string::npos) return false;
    int messageLen = std::stoi(package.substr(0, pos));
    int total = pos + 2 + messageLen + 2;
    if (package.size() < total) return false; // 半包
    *message = package.substr(pos + 2, messageLen);
    package.erase(0, total);
    return true;
}

四、 序列化与反序列化

我们引入了 jsoncpp 库,Request 结构体通过 Json::FastWriter 转成 JSON 字符串发出,接收方通过 Json::Reader 解析成对象,非常方便。

五、 守护进程:让程序在后台默默运行

(插入 Daemon 函数代码)

我们写的服务器程序如果直接跑,一旦关掉终端它就退出了。要变成真正的服务器,必须守护进程化:

  1. fork 后父进程退出(脱离终端)。

  2. setsid() 创建新会话。

  3. 将 0, 1, 2 文件描述符重定向到 /dev/null,防止输出报错影响主进程。

自我实战操作

手敲一个极简协议 和守护进程。

bash 复制代码
cd ~/linux_study/network_test
mkdir protocol_test
cd protocol_test
实战 1:手写协议的编解码(解决粘包)
bash 复制代码
vim protocol.h

(直接复制下面的代码,体会 \r\n 作为分隔符的妙用)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>

// 协议格式: "长度\r\n数据\r\n"
const std::string LineBreakSep = "\r\n";

// 编码
std::string Encode(const std::string &message) {
    std::string len = std::to_string(message.size());
    return len + LineBreakSep + message + LineBreakSep;
}

// 解码
bool Decode(std::string &package, std::string *message) {
    // 1. 找第一个 \r\n,截取长度
    auto pos = package.find(LineBreakSep);
    if (pos == std::string::npos) return false; // 连长度都没收全

    std::string lens = package.substr(0, pos);
    int messageLen = std::stoi(lens);
    
    // 2. 计算总长度:长度字段长度 + 数据长度 + 两个 \r\n 的长度
    int total = lens.size() + messageLen + 2 * LineBreakSep.size();
    if (package.size() < total) {
        return false; // 数据还没收全,半包
    }

    // 3. 截取有效数据,并删除已处理的部分
    *message = package.substr(pos + LineBreakSep.size(), messageLen);
    package.erase(0, total); 
    return true;
}
实战 2:守护进程
bash 复制代码
vim daemon.c

(注意核心的 3 步)

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
    // 1. 忽略子进程退出信号和管道破裂信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);

    // 2. fork并退出父进程 (脱离控制终端)
    if (fork() > 0) exit(0);

    // 3. 创建新会话 (摆脱终端的控制)
    setsid();

    // 4. 改变工作目录到根目录
    chdir("/");

    // 5. 重定向标准输入、输出、错误到 /dev/null
    int fd = open("/dev/null", O_RDWR);
    if (fd > 0) {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }

    // 守护进程主体逻辑 (死循环)
    while (1) {
        // 后台默默干活,比如写日志、监听网络等
        sleep(5);
    }
    return 0;
}

编译运行 gcc daemon.c -o daemon && ./daemon 。

你会发现程序运行后没有任何输出,直接回到了终端。敲 ps aux | grep daemon 就能看到它在后台一直跑着。

相关推荐
蒸蒸yyyyzwd3 小时前
学习笔记 八股手撕
笔记·学习
Android系统攻城狮4 小时前
Linux FFmpeg深度解析之avcodec_flush_buffers调用流程与实战(十九)
linux·运维·服务器·ffmpeg·车载音视频·ffmpeg音视频进阶
揣一尾游鱼yu4 小时前
Spring上(IOC)笔记
java·笔记·spring·ioc
樱花树下的数学喵4 小时前
手写简易 STL string|C++ 字符串底层实现
开发语言·c++
QXWZ_IA4 小时前
边坡监测预算有限时的建设顺序:虚拟基准站与成本账
网络·数据库
jimy15 小时前
C++ 报错的 qualifiers 的意思
开发语言·c++
水饺编程5 小时前
第2章:Linux 基础,解压缩【.tar.bz2】压缩包文件
linux·c语言·汇编
Zevalin爱灰灰5 小时前
Linux入门第二部分(ARM裸机篇) 第二章 Cortex-A7 MPCore架构与ARM汇编基础
linux·汇编·arm开发·嵌入式
倔强的石头1065 小时前
【Linux指南】进程通信系列(七):System V 信号量 —— 同步互斥的内核原语
linux·运维·服务器
GeW5 小时前
从"补短板"到"强优势"Linux与数据库认证成为产业跃迁人才支点
linux·数据库