【Linux】基于自定义TCP协议的日期计算器

文章目录


前言

今天小编要来实现一个关于Linux网络编程入门级别的一个小项目:自定义TCP协议的网络日期计算器(主要实现日期-天数 日期+天数 日期-日期这三个业务处理函数) 。代码大概也就500对一点点的样子。希望能够给大家一点启示和小小的帮助。


一、项目简介

本项目基于 Linux C++ 实现了一个自定义TCP协议的网络日期计算器(主要实现日期-天数 日期+天数 日期-日期这三个业务处理函数)。服务端采用多进程模型实现高并发,自主设计「长度头+报文体」通信协议,彻底解决TCP粘包、半包问题,客户端可输入日期表达式,服务端解析并完成日期运算,返回计算结果。

项目整合了 网络编程、TCP协议原理、面向对象编程、运算符重载、字符串解析、并发编程 等核心知识点,可以当做Linux网络编程入门实战项目。

二、核心问题:TCP粘包问题

  • 粘包原因
    TCP是流式协议,无边界、无消息结构,操作系统会优化合并发送数据:

多次发送的小包会被内核合并为一次接收

一次大数据可能被拆分多次接收

直接读写会导致:消息错乱、解析失败、数据粘连。

  • 解决方案 :自定义协议
    本项目采用行业通用方案:固定4字节长度头 + 可变长正文
    协议格式:【4字节网络序长度】 + 【正文数据】
  • 发送端:先发送正文长度(网络字节序),再发送正文
  • 接收端:先读取4字节长度,再循环读满对应长度正文

三、协议核心代码实现

  1. 协议发送函数 protoSend
    将正文长度转为网络字节序,先发长度头,再发正文:
cpp 复制代码
bool protoSend(int fd, const std::string &data)
{
    int len = data.size();
    int netLen = htonl(len);          // 把消息长度 → 变成网络能识别的数字,
    if (send(fd, &netLen, 4, 0) != 4) // 这里我们约定:先发 4 字节长度头用来储存数字,再发真实数据,注意发送的是正文的长度!发出去的是正文长度的具体数字
        return false;
    if (send(fd, data.c_str(), len, 0) != len) // 发送正文
        return false;
    return true;
}
  1. 协议接收函数 protoRecv
    先读4字节长度、网络序转本地序,循环读满数据,解决半包问题:
    hasRead 变量核心作用:记录读取进度,防止数据覆盖,保证完整接收整包数据。
cpp 复制代码
// 接收  解决 TCP 粘包问题
bool protoRecv(int fd, std::string &out)
{
    out.clear(); // 对接收缓冲区清零
    int netLen = 0;
    if (recv(fd, &netLen, 4, 0) != 4) // 先用4字节来接收正文的长度,把长度写入netLen中
        return false;
    int len = ntohl(netLen); // 网络-->本地
    if (len <= 0 || len > 1024)
        return false;

    char buf[1024] = {0};
    int hasRead = 0;      // 已经读到的字节数,因为 TCP 是流协议,数据是一段一段来的,不能保证一次读完!
    while (hasRead < len) // 如果小于len就一直读取
    {
        int s = recv(fd, buf + hasRead, len - hasRead, 0);
        if (s <= 0)
            return false;
        hasRead += s;
    }
    buf[len] = 0;
    out = buf;
    return true;
}

四、多进程并发TCP服务端

  1. 服务端架构
    采用经典二次fork多进程并发模型,彻底解决僵尸进程问题,分工明确、服务稳定,核心运行逻辑如下:
  • 父进程:只负责循环监听端口、accept接收新客户端连接,不处理任何业务
  • 一级子进程:不干活、不阻塞,仅创建孙子进程后直接退出,避免产生僵尸进程
  • 孙子进程(孤儿进程):真正负责和客户端通信、处理日期计算业务,提供服务
  • 通过二次fork,让孙子进程成为孤儿进程,由init进程领养,结束后自动回收资源,彻底避免僵尸进程
  1. 长连接业务处理
    单个客户端连接后可连续发送多条指令,无需重连:
cpp 复制代码
void Service(int sockfd, InetAddr &peer) // 服务函数
    {
        while (true)
        {
            std::string buffer;
            // ========== 协议接收(解决粘包) ==========
            if (!protoRecv(sockfd, buffer))
            {
                std::cout << "客户端退出: " << peer.StringAddr() << std::endl;
                break;
            }
            std::cout << peer.StringAddr() << " say: " << buffer << std::endl;
            // 调用业务
            std::string echo = _func(buffer, peer);

            // ========== 协议发送 ==========
            protoSend(sockfd, echo);
        }
        close(sockfd);
    }

五、日期业务模块设计

  1. 字符串日期解析
    StringToDate 函数:解析客户端字符串、校验日期合法性:
cpp 复制代码
bool StringToDate(const std::string &s, Date &out)
{
    int y, m, d;
    // 按格式解析年月日
    if (sscanf(s.c_str(), "%d-%d-%d", &y, &m, &d) != 3)
        return false;
    out = Date(y, m, d);
    // 校验日期合法性
    return out.CheckDate();
}
  1. 多场景日期计算逻辑
    支持三种核心运算
  • 日期 + 天数:如2026-01-01 + 10
  • 日期 - 天数:如2026-01-01 - 5
  • 日期 - 日期:如计算两日期间隔天数

通过isdigit() 判断后续参数是数字还是日期,区分运算场景。

  1. 运算符重载
    完整重载日期加减、自增、比较运算符,实现日期对象直接运算,代码简洁优雅。

六、项目运行效果

  1. 启动服务端:指定端口监听
  2. 启动客户端,连接服务端IP+端口
  3. 输入指令测试:
  • 输入:2026-01-01 + 20 → 返回计算后日期
  • 输入:2026-05-01 - 2026-04-01 → 返回相差天数

    全程无粘包、无乱码、支持多客户端并发

七、项目总结与收获

  1. 理解TCP流式特性:彻底搞懂粘包、半包产生原因与工业级解决方案
  2. 掌握自定义通信协议:学会长度头协议设计,网络数据封包、解包核心思想
  3. 熟练Linux并发编程:掌握多进程TCP服务器、孤儿进程、僵尸进程处理
  4. 巩固C++面向对象:类封装、运算符重载、回调解耦业务与网络层
  5. 掌握数据解析能力:字符串格式化解析、数据合法性校验

八、项目优势

  • 摒弃原生裸读写,自主实现可靠TCP协议,解决工程级问题
  • 网络层与业务层解耦,架构清晰,可扩展性强
  • 支持长连接、多并发、异常处理完善
  • 代码规范、封装完整,具备小型项目工程思维

九、整体的代码展示(包含服务端和客户端)

这个整体的代码量呢大概也就在500多一点点的这个样子。这里呢小编方便大家的观看体验,所以就把原本是很多板块合成一个大的文件Server.hpp,所以一个就四个文件分别是:Server.hpp(服务器和客户端的公用文件) Server.cc (服务端),Client.cc(客户端), Makfeile(自动编译文件)

Server.hpp(服务器和客户端的公用文件)

cpp 复制代码
#pragma once
#include <iostream>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <string>
#include <sstream>
#include <cstdio>
#include <cstring>
// 错误码定义
enum
{
    OK = 0,
    SOCK_ERR = 1,
    BIND_ERR = 2,
    LISTEN_ERR = 3,
    FORK_ERR = 4,
    USAGE_ERR = 5
};

// 禁止拷贝基类
class NoCopy
{
public:
    NoCopy() = default;
    NoCopy(const NoCopy &) = delete;
    NoCopy &operator=(const NoCopy &) = delete;
};

// IP 地址封装类
class InetAddr
{
public:
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
    {
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(port);
        _addr.sin_addr.s_addr = inet_addr(ip.c_str());
    }

    InetAddr(const struct sockaddr_in &addr) : _addr(addr) {}

    struct sockaddr *NetAddrPtr()
    {
        return (struct sockaddr *)&_addr;
    }

    socklen_t InetAddrLen()
    {
        return sizeof(_addr);
    }

    std::string StringAddr() const
    {
        std::string ip = inet_ntoa(_addr.sin_addr);
        uint16_t port = ntohs(_addr.sin_port);
        return ip + ":" + std::to_string(port);
    }

private:
    struct sockaddr_in _addr;
};

#define CONV(a) (struct sockaddr *)&a

// 发送 解决 TCP 粘包问题,协议约定报文:正文长度+正文
bool protoSend(int fd, const std::string &data)
{
    int len = data.size();
    int netLen = htonl(len);          // 把消息长度 → 变成网络能识别的数字,
    if (send(fd, &netLen, 4, 0) != 4) // 这里我们约定:先发 4 字节长度头用来储存数字,再发真实数据,注意发送的是正文的长度!发出去的是正文长度的具体数字
        return false;
    if (send(fd, data.c_str(), len, 0) != len) // 发送正文
        return false;
    return true;
}
// 接收  解决 TCP 粘包问题
bool protoRecv(int fd, std::string &out)
{
    out.clear(); // 对接收清零
    int netLen = 0;
    if (recv(fd, &netLen, 4, 0) != 4) // 先用4字节来接收正文的长度,把长度写入netLen中
        return false;
    int len = ntohl(netLen); // 网络-->本地
    if (len <= 0 || len > 1024)
        return false;

    char buf[1024] = {0};
    int hasRead = 0;      // 已经读到的字节数,因为 TCP 是流协议,数据是一段一段来的,不能保证一次读完!
    while (hasRead < len) // 如果小于len就一直读取
    {
        int s = recv(fd, buf + hasRead, len - hasRead, 0);
        if (s <= 0)
            return false;
        hasRead += s;
    }
    buf[len] = 0;
    out = buf;
    return true;
}
// ====================== TCP服务器类======================
const static int defaultsockfd = -1;
const static int backlog = 253;

using func_t = std::function<std::string(const std::string &, InetAddr &)>;

class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port, func_t func) : _port(port), _listensockfd(defaultsockfd), isrunning(false), _func(func)
    {
    }
    void Init()
    {
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建listen套接字
        if (_listensockfd < 0)
        {
            std::cout << "socket error" << std::endl;
            exit(SOCK_ERR);
        }
        std::cout << "sockfd success:" << _listensockfd << std::endl;

        InetAddr peer(_port);
        int n = bind(_listensockfd, peer.NetAddrPtr(), peer.InetAddrLen()); // 绑定listen套接字
        if (n < 0)
        {
            std::cout << "bind error" << std::endl;
            exit(BIND_ERR);
        }
        std::cout << "bind success:" << n << std::endl;

        int l = listen(_listensockfd, backlog); // 监听套接字
        if (l < 0)
        {
            std::cout << "listen error" << std::endl;
            exit(LISTEN_ERR);
        }
        std::cout << "listen success:" << l << std::endl;
    }
    void Service(int sockfd, InetAddr &peer) // 服务函数
    {
        while (true)
        {
            std::string buffer;
            // ========== 协议接收(解决粘包) ==========
            if (!protoRecv(sockfd, buffer))
            {
                std::cout << "客户端退出: " << peer.StringAddr() << std::endl;
                break;
            }
            std::cout << peer.StringAddr() << " say: " << buffer << std::endl;
            // 调用业务
            std::string echo = _func(buffer, peer);

            // ========== 协议发送 ==========
            protoSend(sockfd, echo);
        }
        close(sockfd);
    }
    void Run()
    {
        isrunning = true;
        while (isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            int _sockfd = accept(_listensockfd, CONV(peer), &len); // 获取连接
            if (_sockfd < 0)
            {
                std::cout << "accept error" << std::endl;
                continue;
            }
            InetAddr addr(peer);
            std::cout << "accept success: " << addr.StringAddr() << std::endl;

            // 多进程来提供服务
            pid_t id = fork();
            if (id < 0)
            {
                std::cout << "fork error" << std::endl;
                exit(FORK_ERR);
            }
            else if (id == 0)
            {
                close(_listensockfd);
                if (fork() > 0)
                    exit(OK);
                Service(_sockfd, addr); // 让孙子进程来提供服务
                exit(OK);
            }
            else
            {
                close(_sockfd);
                waitpid(id, nullptr, 0); // 等待子进程退出
            }
        }
        isrunning = false;
    }
    ~TcpServer()
    {
        if (_listensockfd != defaultsockfd)
        {
            close(_listensockfd);
            _listensockfd = defaultsockfd;
        }
    }

private:
    uint16_t _port;
    int _listensockfd;
    bool isrunning;
    func_t _func;
};

// ====================== Date日期类======================
class Date
{
    friend std::ostream &operator<<(std::ostream &out, const Date &d);
    friend std::istream &operator>>(std::istream &in, Date &d);

public:
    Date(int year = 2026, int month = 5, int day = 22);
    void Print();
    int GetMonthDay(int year, int month)
    {
        static int days[13] = {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
            return 29;
        return days[month];
    }

    Date &operator-=(int day); // 日期-=天数
    Date &operator+=(int day); // 日期+=天数
    Date operator+(int day);   // 日期+天数
    Date operator-(int day);   // 日期-天数

    // 日期比较
    bool operator<(const Date &d);
    bool operator==(const Date &d);
    bool operator<=(const Date &d);
    bool operator>(const Date &d);
    bool operator>=(const Date &d);
    bool operator!=(const Date &d);

    Date operator++(int); // 后置++
    Date &operator++();   // 前置++

    int operator-(const Date &d);                                   // 两个日期相减返回相差的天数
    bool CheckDate();                                               // 判断日期是否合法
    bool StringToDate(const std::string &s, Date &out);             // 把客户端发来的字符串转换成 Date 对象,并检查日期是否合法!
    std::string DateCalc(const std::string &msg, InetAddr &client); // 对日期进行业务处理,如日期-天数 日期+天数 日期-日期

private:
    int _year;  // 年
    int _month; // 月
    int _day;   // 日
};

Date::Date(int year, int month, int day)
{
    _year = year;
    _month = month;
    _day = day;
}

void Date::Print()
{
    std::cout << _year << "_" << _month << "_" << _day << std::endl;
}

Date &Date::operator+=(int day)
{
    if (day < 0)
        return *this -= (-day);
    _day += day;
    while (_day > GetMonthDay(_year, _month))
    {
        _day -= GetMonthDay(_year, _month);
        _month++;
        if (_month == 13)
        {
            _year++;
            _month = 1;
        }
    }
    return *this;
}

Date Date::operator+(int day)
{
    Date tmp = *this;
    tmp += day;
    return tmp;
}

Date &Date::operator-=(int day)
{
    if (day < 0)
        return *this += (-day);
    _day -= day;
    while (_day <= 0)
    {
        _month--;
        if (_month == 0)
        {
            _month = 12;
            _year--;
        }
        _day += GetMonthDay(_year, _month);
    }
    return *this;
}

Date Date::operator-(int day)
{
    Date tmp = *this;
    tmp -= day;
    return tmp;
}

bool Date::operator<(const Date &d)
{
    if (_year != d._year)
        return _year < d._year;
    if (_month != d._month)
        return _month < d._month;
    return _day < d._day;
}

bool Date::operator==(const Date &d)
{
    return _year == d._year && _month == d._month && _day == d._day;
}

bool Date::operator<=(const Date &d)
{
    return *this < d || *this == d;
}

bool Date::operator>(const Date &d)
{
    return !(*this <= d);
}

bool Date::operator>=(const Date &d)
{
    return !(*this < d);
}

bool Date::operator!=(const Date &d)
{
    return !(*this == d);
}

Date Date::operator++(int)
{
    Date tmp = *this;
    *this += 1;
    return tmp;
}

Date &Date::operator++()
{
    *this += 1;
    return *this;
}

int Date::operator-(const Date &d)
{
    int flag = 1;
    Date max = *this;
    Date min = d;
    if (*this < d)
    {
        max = d;
        min = *this;
        flag = -1;
    }
    int n = 0;
    while (min != max)
    {
        min++;
        n++;
    }
    return n * flag;
}

std::ostream &operator<<(std::ostream &out, const Date &d)
{
    out << d._year << "-" << d._month << "-" << d._day;
    return out;
}

std::istream &operator>>(std::istream &in, Date &d)
{
    in >> d._year >> d._month >> d._day;
    return in;
}

bool Date::CheckDate()
{
    if (_month < 1 || _month > 12)
        return false;
    if (_day < 1 || _day > GetMonthDay(_year, _month))
        return false;
    return true;
}

// ====================== 日期计算业务函数======================
bool Date::StringToDate(const std::string &s, Date &out)
{
    int y, m, d;
    if (sscanf(s.c_str(), "%d-%d-%d", &y, &m, &d) != 3)
        return false;
    out = Date(y, m, d);
    return out.CheckDate();
}

std::string Date::DateCalc(const std::string &msg, InetAddr &client)
{
    std::stringstream ss(msg);
    std::string a, op, b;
    ss >> a >> op >> b;

    Date d1, d2;
    if (!StringToDate(a, d1))
    {
        return "错误:日期格式错误:示例:2025-12-25 + 10";
    }

    // 1. 日期1 - 日期2 //这个顺序必须在日期-天数之前,不然会出错的,如果在之后,那么当输入一个日期时,
    // 代码会认为你是在执行日期-天数这个函数,会把日期转换成一个数字进行计算,但是这样是错误的。所以要放在前面
    if (op == "-" && StringToDate(b, d2))
    {
        int diff = d1 - d2;
        return "相差天数:" + std::to_string(abs(diff)); // abs-->绝对值
    }

    // 2. 日期 + 天数
    if (op == "+" && isdigit(b[0])) // isdigit (字符) → 判断这个字符是不是0-9 的数字。是数字 → 返回 非 0 值(真)不是数字 → 返回 0(假),
                                    // 用来判断用户输入的是天数,而不是日期
    {
        int days = stoi(b);
        Date res = d1 + days;
        std::stringstream ans;
        ans << "计算结果:" << res;
        return ans.str();
    }

    // 3. 日期 - 天数
    if (op == "-" && isdigit(b[0]))
    {
        int days = stoi(b);
        Date res = d1 - days;
        std::stringstream ans; // 因为C++ 不允许 字符串 + 自定义对象 直接拼接。stringstream它就像一个 "临时拼接字符串的缓冲区"你可以往里面塞任何东西:
        ans << "计算结果:" << res;
        return ans.str();
    }
    return "格式错误!支持:2025-1-1 + 10 | 2025-1-1 - 5 | 2025-1-1 - 2024-1-1";
}

Server.cc (服务端)

cpp 复制代码
#include "DateServer.hpp"
#include <iostream>
#include <memory>
void Usage(std::string s)
{
    std::cout << s << " port" << std::endl;
    exit(USAGE_ERR);
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
    }
    uint16_t port = std::stoi(argv[1]);

    Date d;
    std::unique_ptr<TcpServer> strv = std::make_unique<TcpServer>(port, [&d](const std::string &buffer, InetAddr &client) -> std::string
                                                                  { return d.DateCalc(buffer, client); });
    strv->Init();
    strv->Run();
    // TcpServer server(port, DateCalc); // 传入日期计算函数
    // server.Init();
    // server.Run();
    return 0;
}

Client.cc(客户端)

cpp 复制代码
#include "DateServer.hpp"
#include <memory>
void Usage(std::string s)
{
    std::cout << s << "ip port" << std::endl;
    exit(USAGE_ERR);
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
    }

    uint16_t port = std::stoi(argv[2]);
    std::string ip = argv[1];

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cout << "socket error" << std::endl;
        exit(SOCK_ERR);
    }
    std::cout << "sockfd success:" << sockfd << std::endl;
    // struct sockaddr_in addr;
    //  memset(&addr, 0, sizeof(addr));
    //  addr.sin_family = AF_INET;
    //  addr.sin_port = htons(port);
    //  addr.sin_addr.s_addr = inet_addr(ip.c_str());
    //  socklen_t len = sizeof(addr);
    InetAddr peer(port, ip);
    int n = connect(sockfd, peer.NetAddrPtr(), peer.InetAddrLen());
    if (n < 0)
    {
        std::cout << "connect error" << std::endl;
        close(sockfd);
        return 1;
    }
    std::cout << "connect success!" << std::endl;
    while (true)
    {
        // 发消息
        std::string line;
        std::cout << "Please Enter#";
        std::getline(std::cin, line);

        // ========== 发送:使用协议函数 ==========
        protoSend(sockfd, line);
        // ========== 接收:使用协议函数 ==========
        std::string recvBuf;

        if (protoRecv(sockfd, recvBuf))
        {
            std::cout << "server reply# " << recvBuf << std::endl;
        }
        else
        {
            break;
        }
    }
    close(sockfd);
}

Makfeile(自动编译文件)

bash 复制代码
.PHONY:all
all:client server
client:Client.cc
	g++ -o $@ $^ -std=c++17
server:Server.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -f client server

总结

今天的分享就到这里了吧,下期我们再见!!

相关推荐
YsyaaabB2 小时前
ACM 模式通用代码模板
java·c++·python·算法
我命由我123452 小时前
C++ - 面向对象 - 析构函数
android·c语言·开发语言·c++·visualstudio·visual studio·android runtime
2501_920047032 小时前
iptables防火墙
linux·运维·网络安全
中国lanwp3 小时前
GitLab 按访问IP动态切换项目下载/克隆地址原理与配置说明
网络协议·tcp/ip·gitlab
带土13 小时前
7. 线程编程(线程概念和创建)
linux
华清远见IT开放实验室3 小时前
硬核根基,智能载体:华清远见嵌入式“硬件+仿真+课程+师资”产教融合与实践教学方案
linux·人工智能·stm32·物联网·嵌入式·虚拟仿真
Anthony_2313 小时前
Linux 防火墙完全指南:从 iptables 到 firewalld
linux·运维·服务器
月走乂山3 小时前
Linux 服务器安装 CC Switch GUI 工具 + VNC 远程桌面完整教程
linux·运维·服务器
手可摘星辰的少年3 小时前
二级指针到底在改什么?——从C语言基础到Linux内核文件系统注册机制
linux