【Linux网络】Linux 网络编程:应用层自定义协议与序列化(3):网络计算器实现和守护进程

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:


文章目录

  • [1 ~> 应用层协议与序列化基础](#1 ~> 应用层协议与序列化基础)
    • [1.1 核心概念](#1.1 核心概念)
    • [1.2 请求 / 应答报文设计](#1.2 请求 / 应答报文设计)
      • [1.2.1 文字描述](#1.2.1 文字描述)
      • [1.2.2 Protocol.hpp](#1.2.2 Protocol.hpp)
  • [2 ~> TCP 粘包问题与自定义协议](#2 ~> TCP 粘包问题与自定义协议)
    • [2.1 粘包问题](#2.1 粘包问题)
    • [2.2 自定义协议格式](#2.2 自定义协议格式)
    • [2.3 封包 / 解包实现](#2.3 封包 / 解包实现)
      • [2.3.1 Protocol.hpp](#2.3.1 Protocol.hpp)
  • [3 ~> 模板方法模式封装 Socket](#3 ~> 模板方法模式封装 Socket)
    • [3.1 模板方法模式思想](#3.1 模板方法模式思想)
    • [3.2 Socket 基类设计](#3.2 Socket 基类设计)
      • [3.2.1 Socket.hpp](#3.2.1 Socket.hpp)
    • [3.3 TcpSocket 实现](#3.3 TcpSocket 实现)
      • [3.3.1 笔记整理](#3.3.1 笔记整理)
      • [3.3.2 Socket.hpp](#3.3.2 Socket.hpp)
    • [3.4 InetAddr 地址封装](#3.4 InetAddr 地址封装)
      • [3.4.1 笔记整理](#3.4.1 笔记整理)
      • [3.4.2 InetAddr.hpp](#3.4.2 InetAddr.hpp)
  • [4 ~> TcpServer 服务端实现](#4 ~> TcpServer 服务端实现)
    • [4.1 多进程 + 回调架构](#4.1 多进程 + 回调架构)
    • [4.2 TcpServer.hpp](#4.2 TcpServer.hpp)
  • [5 ~> 三层架构:协议层 + 业务层 + 网络通信层](#5 ~> 三层架构:协议层 + 业务层 + 网络通信层)
    • [5.1 三层架构设计](#5.1 三层架构设计)
    • [5.2 协议层处理入口](#5.2 协议层处理入口)
      • [5.2.1 笔记整理](#5.2.1 笔记整理)
      • [5.2.2 Protocol.hpp](#5.2.2 Protocol.hpp)
    • [5.3 业务层:计算器实现](#5.3 业务层:计算器实现)
      • [5.3.1 笔记整理](#5.3.1 笔记整理)
      • [5.3.2 Calculator.hpp](#5.3.2 Calculator.hpp)
  • [6 ~> 理解应用层(OSI七层结构中的上三层)](#6 ~> 理解应用层(OSI七层结构中的上三层))
    • [6.1 为什么OSI的上三层都叫"应用层"?](#6.1 为什么OSI的上三层都叫“应用层”?)
    • [6.2 为什么操作系统只负责到传输层?](#6.2 为什么操作系统只负责到传输层?)
    • [6.3 面对"十几个请求同时到达"该如何处理?](#6.3 面对“十几个请求同时到达”该如何处理?)
      • [6.3.1 报文维度的处理:TCP粘包与解析(表示层)](#6.3.1 报文维度的处理:TCP粘包与解析(表示层))
      • [6.3.2 并发维度的处理:I/O多路复用与线程池(会话层 / 架构层)](#6.3.2 并发维度的处理:I/O多路复用与线程池(会话层 / 架构层))
  • [7 ~> 客户端(OnlineCalClient.cc)的读 / 写新接口](#7 ~> 客户端(OnlineCalClient.cc)的读 / 写新接口)
    • [7.1 读](#7.1 读)
    • [7.2 写](#7.2 写)
    • [7.3 OnlineCalClient.cc中的发送和接受应答](#7.3 OnlineCalClient.cc中的发送和接受应答)
  • [7 ~> 工具类:日志 + 互斥锁](#7 ~> 工具类:日志 + 互斥锁)
    • [7.1 互斥锁(Mutex.hpp)](#7.1 互斥锁(Mutex.hpp))
      • [7.1.1 笔记整理](#7.1.1 笔记整理)
      • [7.1.2 源码](#7.1.2 源码)
    • [7.2 日志系统(Logger.hpp)](#7.2 日志系统(Logger.hpp))
      • [7.2.1 笔记整理](#7.2.1 笔记整理)
      • [7.2.2 源码](#7.2.2 源码)
  • [8 ~> 守护进程](#8 ~> 守护进程)
    • [8.1 守护进程整理](#8.1 守护进程整理)
    • [8.2 回顾:前台作业 VS 后台作业](#8.2 回顾:前台作业 VS 后台作业)
    • [8.3 认识守护进程的新系统调用:setsid()](#8.3 认识守护进程的新系统调用:setsid())
    • [8.4 Daemon.hpp](#8.4 Daemon.hpp)
    • [8.5 守护进程的细节问题](#8.5 守护进程的细节问题)
    • [8.6 父进程(一般进程)和子进程(守护进程)](#8.6 父进程(一般进程)和子进程(守护进程))
    • [8.7 补充](#8.7 补充)
      • [8.7.1:测试 && 项目部署过程](#8.7.1:测试 && 项目部署过程)
      • [8.7.2 补充2:部署是什么?](#8.7.2 补充2:部署是什么?)
      • [8.7.3 补充3:是否要更改工作路径 && 是否要进行重定向](#8.7.3 补充3:是否要更改工作路径 && 是否要进行重定向)
    • [8.8 我是不是一定要手搓一个守护进程代码?](#8.8 我是不是一定要手搓一个守护进程代码?)
    • [8.9 守护进程总结](#8.9 守护进程总结)
  • [9 ~> 拓展:了解nohup命令](#9 ~> 拓展:了解nohup命令)
    • [9.1 飞书笔记链接](#9.1 飞书笔记链接)
  • 结尾


1 ~> 应用层协议与序列化基础

1.1 核心概念

  • 应用层 :程序员编写的、解决实际需求的网络程序都运行在应用层,应用层的核心是自定义协议
  • 协议 :通信双方提前约定好的结构化数据格式,是数据传输的 "共同语言"。
  • 序列化 :把内存中的结构化数据 转换成可在网络中传输的字符串 / 字节流 ,即多变一
  • 反序列化 :把网络中收到的字节流 / 字符串 还原成内存中的结构化数据 ,即一变多
  • 技术选型 :艾莉丝这里的网络计算器 是使用jsoncpp库实现序列化与反序列化,可读性强、跨语言、开发效率高。
  • 通信本质 :网络通信只关心字节流,不关心数据具体含义,协议负责定义数据含义。

1.2 请求 / 应答报文设计

1.2.1 文字描述

  • 报文分为请求报文(Client→Server)和应答报文(Server→Client)
  • 请求报文包含:两个操作数_x/_y、操作符_oper(支持+ - * / %)。
  • 应答报文包含:计算结果_result、状态码_exitcode
  • 状态码约定:0=计算成功1=除0错误2=模0错误3=操作码非法
  • 报文必须实现Serialize (序列化)和Deserialize(反序列化)接口。

1.2.2 Protocol.hpp

cpp 复制代码
// 请求报文: client -> server
class Request
{
public:
    Request(){}
    Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper) {}
    // 序列化:结构化对象 → JSON字符串(多变一)
    void Serialize(std::string *outstr)
    {
        Json::Value root;
        root["datax"] = _x;
        root["datay"] = _y;
        root["oper"] = _oper;
        Json::FastWriter writer; // 紧凑格式,适合网络传输
        *outstr = writer.write(root);
    }
    // 反序列化:JSON字符串 → 结构化对象(一变多)
    void Deserialize(std::string &instr)
    {
        Json::Value root;
        Json::Reader reader;
        if(reader.parse(instr, root))
        {
            _x = root["datax"].asInt();
            _y = root["datay"].asInt();
            _oper = root["oper"].asInt();
        }
        else std::cout << "bug!!! 反序列化失败" << std::endl;
    }
public:
    int _x;      // 左操作数
    int _y;      // 右操作数
    char _oper;  // 操作符
};

// 应答报文: server->client
class Response
{
public:
    Response() : _result(0), _exitcode(0) {}
    // 应答序列化
    void Serialize(std::string *outstr)
    {
        Json::Value root;
        root["result"] = _result;
        root["exitcode"] = _exitcode;
        Json::FastWriter writer;
        *outstr = writer.write(root);
    }
    // 应答反序列化
    void Deserialize(std::string &instr)
    {
        Json::Value root;
        Json::Reader reader;
        if(reader.parse(instr, root))
        {
            _result = root["result"].asInt();
            _exitcode = root["exitcode"].asInt();
        }
        else std::cout << "bug!!! 反序列化失败" << std::endl;
    }
public:
    int _result;    // 计算结果
    int _exitcode;  // 状态码:0可信,1除0,2模0,3操作码错误
};

2 ~> TCP 粘包问题与自定义协议

2.1 粘包问题

  • TCP 本质面向字节流 的传输协议,没有报文边界
  • 粘包原因 :客户端write次数 ≠ 服务端read次数,多个报文会被粘连截断
  • 危害:直接解析 JSON 会失败、程序崩溃、数据错乱。
  • 解决方案应用层自定义协议 ,手动定义报文边界,保证报文完整性。

2.2 自定义协议格式

协议格式:报文长度 + \r\n + JSON 串 + \r\n

设计思路:先读长度字段 ,知道完整报文长度,再读取对应长度的数据,确保报文完整。

分隔符:使用\r\n,符合 HTTP 等通用协议,不易与 JSON 数据冲突。

2.3 封包 / 解包实现

  • 封包Package :给序列化后的 JSON 串添加长度 + 分隔符,组装成符合协议的报文。
  • 解包UnPackage :从字节流中提取完整 JSON 串,判断报文是否完整。
  • 解包返回值:>0= 提取到完整报文、0= 报文不完整、<0= 协议解析错误。
  • 安全校验:长度字段必须是纯数字,防止非法数据导致stoi崩溃。

2.3.1 Protocol.hpp

cpp 复制代码
const std::string gsep = "\r\n"; // 协议分隔符

// 封包:JSON字符串 → 完整协议报文
std::string Package(const std::string jsonstr)
{
    uint32_t len = jsonstr.size();
    // 拼接格式:长度\r\nJSON串\r\n
    return std::to_string(len) + gsep + jsonstr + gsep;
}

// 解包:字节流 → 提取完整JSON串
int UnPackage(std::string &streamstr, std::string *jsonstr)
{
    // 1. 找第一个分隔符,获取长度字段
    auto pos = streamstr.find(gsep);
    if(pos == std::string::npos) return 0; // 未找到分隔符,报文不完整

    // 2. 提取长度字符串,校验纯数字
    std::string packlenstr = streamstr.substr(0, pos);
    for(auto &c : packlenstr)
        if(c < '0' || c > '9') return -1; // 非法长度,协议错误

    // 3. 计算完整报文总长度
    uint32_t packlenint = std::stoi(packlenstr);
    uint32_t targetlen = packlenstr.size() + packlenint + 2 * gsep.size();

    // 4. 缓冲区数据不足,报文不完整
    if(streamstr.size() < targetlen) return 0;

    // 5. 提取JSON串,移除已处理报文(出队列)
    *jsonstr = streamstr.substr(pos + gsep.size(), packlenint);
    streamstr.erase(0, targetlen);

    return targetlen;
}

3 ~> 模板方法模式封装 Socket

3.1 模板方法模式思想

  • 设计模式模板方法模式,定义算法骨架,具体步骤延迟到子类实现。
  • 设计目的 :统一 TCP/UDP/Unix 域套接字的创建流程,代码复用、解耦、易扩展
  • 核心 :基类Socket定义纯虚接口 ,模板方法组合接口流程;子类TcpSocket重写接口。
  • 优势 :新增套接字类型只需继承基类,无需修改原有代码,符合开闭原则

3.2 Socket 基类设计

  • 基类定义套接字必备操作:创建、绑定、监听、接收、连接、收发数据、关闭。
  • 模板方法:
    • BuildSocketMethod:服务端套接字流程(创建→绑定→监听)。
    • BuildClientSocketMethod:客户端套接字流程(创建→连接)。
  • 析构函数设为虚函数,保证子类析构被正确调用。

3.2.1 Socket.hpp

cpp 复制代码
class Socket
{
public:
    virtual ~Socket(){} // 虚析构,防止内存泄漏
    // 纯虚接口,子类必须实现
    virtual void CreateSocketOrDie() = 0;
    virtual void BindSocketOrDie(uint16_t port) = 0;
    virtual void ListenSocketOrDie() = 0;
    virtual std::shared_ptr<Socket> Accepter(InetAddr *clientaddr) = 0;
    virtual void ConnectOrDie(const std::string &serverip, uint16_t serverport) = 0;
    virtual int Socketfd() = 0;
    virtual void Close() = 0;
    virtual int Recv(std::string *outstr) = 0;
    virtual int Send(const std::string &outstr) = 0;

public:
    // 模板方法:服务端套接字创建流程(骨架固定)
    void BuildSocketMethod(uint16_t port)
    {
        CreateSocketOrDie();
        BindSocketOrDie(port);
        ListenSocketOrDie();
    }
    // 模板方法:客户端套接字创建流程
    void BuildClientSocketMethod(const std::string &serverip, uint16_t serverport)
    {
        CreateSocketOrDie();
        ConnectOrDie(serverip, serverport);
    }
};

3.3 TcpSocket 实现

3.3.1 笔记整理

  • 继承Socket基类,重写所有纯虚接口,实现 TCP 套接字具体操作。
  • CreateSocketOrDie:创建 TCP 套接字,失败直接退出。
  • BindSocketOrDie:绑定端口号,失败直接退出。
  • ListenSocketOrDie:启动监听,队列长度gbacklog=16。
  • Accepter:接收客户端连接,返回新套接字的智能指针。
  • Recv/Send:封装recv/send系统调用,Recv自动拼接缓冲区数据。
  • Close:关闭文件描述符,防止资源泄漏。

3.3.2 Socket.hpp

cpp 复制代码
static const int gbacklog = 16; // 监听队列长度
enum { SOCKET_ERR = 1, BIND_ERR, LISTEN_ERR }; // 错误码

class TcpSocket : public Socket
{
public:
    TcpSocket() : _sockfd(-1) {}
    TcpSocket(int sockfd) : _sockfd(sockfd) {}

    // 创建TCP套接字
    void CreateSocketOrDie() override
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_sockfd < 0) { LOG(LogLevel::FATAL) << "create socket error"; exit(SOCKET_ERR); }
        LOG(LogLevel::INFO) << "create socket success";
    }
    // 绑定端口
    void BindSocketOrDie(uint16_t port) override
    {
        InetAddr local(port);
        int n = bind(_sockfd, local.Addr(), local.AddrLen());
        if(n < 0) { LOG(LogLevel::FATAL) << "bind socket error"; exit(BIND_ERR); }
        LOG(LogLevel::INFO) << "bind socket success";
    }
    // 监听
    void ListenSocketOrDie() override
    {
        int n = listen(_sockfd, gbacklog);
        if(n < 0) { LOG(LogLevel::FATAL) << "listen socket error"; exit(LISTEN_ERR); }
        LOG(LogLevel::FATAL) << "listen socket success";
    }
    // 接收连接
    std::shared_ptr<Socket> Accepter(InetAddr *clientaddr) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sockfd = accept(_sockfd, CONV(&peer), &len);
        if(sockfd < 0) return nullptr;
        *clientaddr = peer; // 赋值客户端地址
        return std::make_unique<TcpSocket>(sockfd);
    }
    // 接收数据:自动拼接到缓冲区
    int Recv(std::string *outstr) override
    {
        char buffer[1024];
        ssize_t n = recv(_sockfd, buffer, sizeof(buffer)-1, 0);
        if(n > 0) { buffer[n] = 0; *outstr += buffer; return n; }
        else if(n == 0) return 0; // 对端关闭
        else return -1; // 读取失败
    }
    // 发送数据
    int Send(const std::string &outstr) override
    {
        return send(_sockfd, outstr.c_str(), outstr.size(), 0);
    }
    // 连接服务器
    void ConnectOrDie(const std::string &serverip, uint16_t serverport) override
    {
        InetAddr serveraddr(serverport, serverip);
        int n = connect(_sockfd, serveraddr.Addr(), serveraddr.AddrLen());
        if(n != 0) { LOG(LogLevel::FATAL) << "connect failed"; return; }
        LOG(LogLevel::INFO) << "connect success";
    }
    // 关闭套接字
    void Close() override { if(_sockfd >=0) close(_sockfd); _sockfd = -1; }
    // 获取文件描述符
    int Socketfd() override { return _sockfd; }

private:
    int _sockfd; // TCP套接字文件描述符
};

3.4 InetAddr 地址封装

3.4.1 笔记整理

  • 封装sockaddr_in结构体,统一处理网络字节序↔主机字节序转换。
  • 提供Addr()/AddrLen()方法,直接适配bind/connect/accept系统调用。
  • 重载赋值运算符,支持accept时直接赋值客户端地址。
  • StringAddress():将地址转为[IP:端口]格式,用于日志打印。

3.4.2 InetAddr.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define CONV(addr) ((struct sockaddr *)(addr)) // 类型转换宏

class InetAddr
{
public:
    InetAddr(){}
    // 从sockaddr_in构造
    InetAddr(struct sockaddr_in &addr) : _net_addr(addr)
    {
        _port = ntohs(_net_addr.sin_port); // 网络序转主机序
        _ip = inet_ntoa(_net_addr.sin_addr);
    }
    // 从端口+IP构造
    InetAddr(uint16_t port, std::string ip = "0.0.0.0"): _port(port), _ip(ip)
    {
        _net_addr.sin_family = AF_INET;
        _net_addr.sin_port = htons(_port); // 主机序转网络序
        _net_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    struct sockaddr *Addr() { return CONV(&_net_addr); } // 用于系统调用
    socklen_t AddrLen() { return sizeof(_net_addr); }
    // 赋值重载:accept时直接赋值
    void operator=(const struct sockaddr_in &addr)
    {
        _net_addr = addr;
        _port = ntohs(_net_addr.sin_port);
        _ip = inet_ntoa(_net_addr.sin_addr);
    }
    // 转为字符串格式,日志打印
    std::string StringAddress() { return "[" + _ip + ":" + std::to_string(_port) + "]"; }

private:
    uint16_t _port;
    std::string _ip;
    struct sockaddr_in _net_addr; // 网络地址结构体
};

4 ~> TcpServer 服务端实现

4.1 多进程 + 回调架构

  • 并发模型 :多进程,父进程负责accept接收连接,子进程负责 IO 处理。
  • 优势:子进程崩溃不影响父进程,服务稳定性高。
  • 信号处理 :忽略SIGCHLD信号,避免产生僵尸进程。
  • 解耦设计 :网络层只负责 IO,业务逻辑通过回调函数(_cb) 交给上层处理。
  • 长连接:客户端连接后可持续发送请求,不主动断开。

4.2 TcpServer.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <unistd.h>
#include <signal.h>
#include <functional>
#include "Socket.hpp"

// 回调类型:字节流→应答报文
using handler_t = std::function<std::string(std::string &)>;

class TcpServer
{
public:
    TcpServer(uint16_t port, handler_t handler)
        : _port(port),_listensockfd(std::make_unique<TcpSocket>()),_handlerstream(handler)
    {
        // 调用模板方法创建监听套接字
        _listensockfd->BuildSocketMethod(_port);
    }
    // 服务启动
    void Run()
    {
        _isrunning = true;
        signal(SIGCHLD, SIG_IGN); // 忽略子进程退出信号,防止僵尸进程
        while(_isrunning)
        {
            InetAddr clientaddr;
            // 接收客户端连接
            auto sockfd = _listensockfd->Accepter(&clientaddr);
            if(!sockfd) { LOG(LogLevel::WARNING) << "accepter error"; continue; }
            LOG(LogLevel::DEBUG) << "client addr:" << clientaddr.StringAddress();

            // 多进程处理:父进程继续监听,子进程处理IO
            if(fork() == 0)
            {
                _listensockfd->Close(); // 子进程关闭监听套接字
                HanderIO(sockfd, clientaddr);
                exit(0);
            }
            sockfd->Close(); // 父进程关闭客户端套接字
        }
    }

private:
    // 处理客户端IO
    void HanderIO(std::shared_ptr<Socket> sockfd, InetAddr clientaddr)
    {
        std::string inbuffer; // 字节流缓冲区,自动拼接
        while(true)
        {
            // 读取数据
            int n = sockfd->Recv(&inbuffer);
            if(n < 0) { LOG(LogLevel::WARNING) << "recv error"; break; }
            if(n == 0) { LOG(LogLevel::INFO) << "client quit"; break; } // 客户端退出

            // 回调协议层处理业务
            std::string outbuffer;
            if(_handlerstream) outbuffer = _handlerstream(inbuffer);
            // 发送应答
            if(!outbuffer.empty()) sockfd->Send(outbuffer);
        }
        sockfd->Close();
    }

private:
    int _port;
    std::unique_ptr<Socket> _listensockfd; // 监听套接字
    bool _isrunning;
    handler_t _handlerstream; // 业务回调
};

5 ~> 三层架构:协议层 + 业务层 + 网络通信层

5.1 三层架构设计

  • 网络通信层TcpServer/Socket,负责连接管理、IO 收发。
  • 协议层Protocol,负责封包 / 解包、序列化 / 反序列化、粘包解决。
  • 业务层Calculator,负责具体的业务逻辑(计算器运算)。
  • 解耦方式:回调函数注入,层与层之间互不感知内部实现。

5.2 协议层处理入口

5.2.1 笔记整理

  • HandlerRequest:协议层入口,循环解包,支持一次处理多个报文。
  • 流程:解包→反序列化→回调业务→序列化→封包
  • 循环处理:直到报文不完整或解析错误才退出。

5.2.2 Protocol.hpp

cpp 复制代码
// 业务回调类型:Request→Response
using callback_t = std::function<Response(const Request &)>;

class Protocol
{
public:
    Protocol(callback_t cb):_cb(cb){}
    // 协议处理入口:字节流→应答报文
    std::string HandlerRequest(std::string &streamstr)
    {
        std::string resp_package;
        // 循环解包,处理所有完整报文
        while(true)
        {
            std::string jsonstring;
            int n = UnPackage(streamstr, &jsonstring);
            if(n == 0) return resp_package; // 报文不完整,返回已处理应答
            if(n == -1) { LOG(LogLevel::DEBUG) << "协议解析失败"; exit(1); }

            // 反序列化→业务处理→序列化→封包
            Request req; req.Deserialize(jsonstring);
            Response resp = _cb(req); // 回调业务层
            std::string respjsonstr; resp.Serialize(&respjsonstr);
            resp_package += Package(respjsonstr); // 拼接应答
        }
    }

private:
    callback_t _cb; // 业务回调
};

5.3 业务层:计算器实现

5.3.1 笔记整理

  • 业务层只负责运算逻辑,与网络、协议完全解耦。
  • 支持+ - * / %五种运算。
  • 错误处理:除 0、模 0、非法操作码,返回对应状态码。

5.3.2 Calculator.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include "Protocol.hpp"

// 前向声明
class Request;
class Response;

class Calculator
{
public:
    Calculator() {}
    ~Calculator() {}
    // 业务核心:根据请求生成应答
    Response Exec(const Request &req)
    {
        Response resp;
        switch (req._oper)
        {
        case '+': resp._result = req._x + req._y; break;
        case '-': resp._result = req._x - req._y; break;
        case '*': resp._result = req._x * req._y; break;
        case '/':
            if (req._y == 0) resp._exitcode = 1; // 除0错误
            else resp._result = req._x / req._y;
            break;
        case '%':
            if (req._y == 0) resp._exitcode = 2; // 模0错误
            else resp._result = req._x % req._y;
            break;
        default: resp._exitcode = 3; break; // 非法操作码
        }
        return resp;
    }
};

6 ~> 理解应用层(OSI七层结构中的上三层)

6.1 为什么OSI的上三层都叫"应用层"?

6.2 为什么操作系统只负责到传输层?

6.3 面对"十几个请求同时到达"该如何处理?

6.3.1 报文维度的处理:TCP粘包与解析(表示层)

6.3.2 并发维度的处理:I/O多路复用与线程池(会话层 / 架构层)


7 ~> 客户端(OnlineCalClient.cc)的读 / 写新接口

读取的时候自然可以使用read,从指定文件描述符里读取指定缓冲区指定大小的数据,今天我不用,补充使用一组新的接口:

7.1 读

7.2 写

7.3 OnlineCalClient.cc中的发送和接受应答


7 ~> 工具类:日志 + 互斥锁

7.1 互斥锁(Mutex.hpp)

7.1.1 笔记整理

  • 封装pthread_mutex,实现线程安全。
  • LockGuard:RAII 风格,自动加锁 / 解锁,防止死锁。

7.1.2 源码

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

// 互斥锁封装
class Mutex
{
public:
    Mutex() { pthread_mutex_init(&_lock, nullptr); }
    void Lock() { pthread_mutex_lock(&_lock); }
    void Unlock() { pthread_mutex_unlock(&_lock); }
    ~Mutex() { pthread_mutex_destroy(&_lock); }
private:
    pthread_mutex_t _lock;
};

// RAII自动锁
class LockGuard
{
public:
    LockGuard(Mutex *lockp): _lockp(lockp) { _lockp->Lock(); }
    ~LockGuard() { _lockp->Unlock(); }
private:
    Mutex *_lockp;
};

7.2 日志系统(Logger.hpp)

7.2.1 笔记整理

  • 策略模式:支持控制台 / 文件日志输出。
  • RAII 日志:析构时自动刷新日志,简化使用。
  • 日志格式:时间 + 等级 + 文件名 + 行号 + 信息。
  • 宏封装:LOG(level)简化调用。

7.2.2 源码

源码前面已经展示过了,这里不写了,太冗长啦。


8 ~> 守护进程

8.1 守护进程整理

  • 守护进程 :脱离终端、会话、用户登录状态,后台长期运行的服务进程。
  • 作用:关闭终端不影响服务运行,是网络服务的标准运行方式。
  • 实现步骤
    • 忽略SIGPIPE/SIGCHLD信号。
    • fork后父进程退出,确保子进程非进程组组长。
    • setsid创建新会话,脱离原终端。
    • 重定向标准 IO 到/dev/null(黑洞文件)。

8.2 回顾:前台作业 VS 后台作业

8.3 认识守护进程的新系统调用:setsid()

8.4 Daemon.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>

// 进程守护化
void Daemon()
{
    // 1. 忽略信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);
    // 2. 父进程退出,子进程非组长
    if(fork() > 0) exit(0);
    // 3. 创建新会话,成为守护进程
    setsid();
    // 4. 重定向标准输入输出到黑洞
    int fd = open("/dev/null", O_RDWR);
    if(fd >= 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

8.5 守护进程的细节问题

8.6 父进程(一般进程)和子进程(守护进程)

我已经知道:守护进程也是一种孤儿进程------父进程退出了!

8.7 补充

8.7.1:测试 && 项目部署过程



比如网易云音乐:

dll就是动态库。

8.7.2 补充2:部署是什么?

在Linux当中安装软件就叫做部署------所谓的部署是什么?可不可以把这样部署:



8.7.3 补充3:是否要更改工作路径 && 是否要进行重定向

8.8 我是不是一定要手搓一个守护进程代码?

系统说你别做了,我帮你做了:

系统调用接口:

可以自己手搓守护进程也可以不做,不做就调用系统的daemon系统调用接口。

这个逻辑和刚才蛋哥写的是反的。

测试一下:

这里(0,0)是更改工作路径、进行重定向。

路径更改到根目录、重定向到/dev/null

8.9 守护进程总结

守护进程也叫做精灵进程,本质也是孤儿进程。本质也是后台进程,一般的后台,一定隶属于一个Shell对应的session会话,而守护进程自成进程组、自成会话。


9 ~> 拓展:了解nohup命令

如果你运行了 nohup 却发现当前目录没生成 nohup.out,可能是因为你没有写权限。此时 nohup 会尝试把日志写到 $HOME/nohup.out 中。

9.1 飞书笔记链接

应用层自定义协议与序列化:模版方法模式


结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!" "技术之路难免有困惑,但同行的人会让前进更有方向。" |

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【Linux网络】Linux 网络编程:应用层自定义协议与序列化(2)序列化与反序列化

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
相思难忘成疾1 小时前
RHCE 综合实验:基于 Nginx 实现 openlab 多站点部署、用户访问控制与 HTTPS 加密访问
linux·运维·nginx·http·https·rhel
Be reborn1 小时前
CSV + YAML 怎么描述测试:H5 SDK 自动化框架的数据模型设计
运维·自动化·pytest
千寻girling1 小时前
周日那天参加的力扣周赛... —— 10号
java·javascript·c++·python·算法·leetcode·职场和发展
TechWayfarer1 小时前
订单未到、运力先行:IP精确地理位置在物流调度中的实战应用
服务器·网络·python·tcp/ip·交通物流
xhbh6661 小时前
Linux端口转发到外网完全教程:iptables DNAT+SNAT实现内网服务暴露
linux·运维·服务器·网络·ip·流量转发·端口流量转发
Q_4582838681 小时前
基于 JTT1078MediaServer 的集群方案实践(Nginx + 溯源模式)轻量级车联网音视频集群
运维·服务器·nginx·架构·音视频·交通物流
吠品1 小时前
Node.js谜团:fs.Stats废弃警告的侦探之旅与破局之道
linux·服务器·数据库
小此方1 小时前
Re:Linux系统篇(十)工具篇 · 二:Vim 编辑器深度解析:从基础模式到高效配置
linux·编辑器·vim
@encryption1 小时前
计算机网络 --- RSTP,MSTP
服务器·网络·计算机网络