文章目录
- 前言
- 一、项目简介
- 二、核心问题:TCP粘包问题
- 三、协议核心代码实现
- 四、多进程并发TCP服务端
- 五、日期业务模块设计
- 六、项目运行效果
- 七、项目总结与收获
- 八、项目优势
- 九、整体的代码展示(包含服务端和客户端)
-
- Server.hpp(服务器和客户端的公用文件)
- [Server.cc (服务端)](#Server.cc (服务端))
- Client.cc(客户端)
- Makfeile(自动编译文件)
- 总结
前言
今天小编要来实现一个关于Linux网络编程入门级别的一个小项目:自定义TCP协议的网络日期计算器(主要实现日期-天数 日期+天数 日期-日期这三个业务处理函数) 。代码大概也就500对一点点的样子。希望能够给大家一点启示和小小的帮助。

一、项目简介
本项目基于 Linux C++ 实现了一个自定义TCP协议的网络日期计算器(主要实现日期-天数 日期+天数 日期-日期这三个业务处理函数)。服务端采用多进程模型实现高并发,自主设计「长度头+报文体」通信协议,彻底解决TCP粘包、半包问题,客户端可输入日期表达式,服务端解析并完成日期运算,返回计算结果。
项目整合了 网络编程、TCP协议原理、面向对象编程、运算符重载、字符串解析、并发编程 等核心知识点,可以当做Linux网络编程入门实战项目。
二、核心问题:TCP粘包问题
- 粘包原因
TCP是流式协议,无边界、无消息结构,操作系统会优化合并发送数据:
多次发送的小包会被内核合并为一次接收
一次大数据可能被拆分多次接收
直接读写会导致:消息错乱、解析失败、数据粘连。
- 解决方案 :自定义协议
本项目采用行业通用方案:固定4字节长度头 + 可变长正文
协议格式:【4字节网络序长度】 + 【正文数据】 - 发送端:先发送正文长度(网络字节序),再发送正文
- 接收端:先读取4字节长度,再循环读满对应长度正文
三、协议核心代码实现
- 协议发送函数 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;
}
- 协议接收函数 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服务端
- 服务端架构
采用经典二次fork多进程并发模型,彻底解决僵尸进程问题,分工明确、服务稳定,核心运行逻辑如下:
- 父进程:只负责循环监听端口、accept接收新客户端连接,不处理任何业务
- 一级子进程:不干活、不阻塞,仅创建孙子进程后直接退出,避免产生僵尸进程
- 孙子进程(孤儿进程):真正负责和客户端通信、处理日期计算业务,提供服务
- 通过二次fork,让孙子进程成为孤儿进程,由init进程领养,结束后自动回收资源,彻底避免僵尸进程
- 长连接业务处理
单个客户端连接后可连续发送多条指令,无需重连:
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);
}
五、日期业务模块设计
- 字符串日期解析
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();
}
- 多场景日期计算逻辑
支持三种核心运算:
- 日期 + 天数:如2026-01-01 + 10
- 日期 - 天数:如2026-01-01 - 5
- 日期 - 日期:如计算两日期间隔天数
通过isdigit() 判断后续参数是数字还是日期,区分运算场景。
- 运算符重载
完整重载日期加减、自增、比较运算符,实现日期对象直接运算,代码简洁优雅。
六、项目运行效果
- 启动服务端:指定端口监听
- 启动客户端,连接服务端IP+端口
- 输入指令测试:
- 输入:2026-01-01 + 20 → 返回计算后日期
- 输入:2026-05-01 - 2026-04-01 → 返回相差天数

全程无粘包、无乱码、支持多客户端并发。
七、项目总结与收获
- 理解TCP流式特性:彻底搞懂粘包、半包产生原因与工业级解决方案
- 掌握自定义通信协议:学会长度头协议设计,网络数据封包、解包核心思想
- 熟练Linux并发编程:掌握多进程TCP服务器、孤儿进程、僵尸进程处理
- 巩固C++面向对象:类封装、运算符重载、回调解耦业务与网络层
- 掌握数据解析能力:字符串格式化解析、数据合法性校验
八、项目优势
- 摒弃原生裸读写,自主实现可靠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
总结
今天的分享就到这里了吧,下期我们再见!!
