整理一篇学习笔记,把看到的一些要点和自己的理解都记下来。
🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:
文章目录
二. 核心模块源码深度解析- 2.1 网络地址封装:InetAddr.hpp
- 2.2 线程安全基础:Mutex.hpp
- 2.3 工业级日志系统:Logger.hpp
- 2.4 Socket 封装:Socket.hpp(模板方法模式)
- 2.5 TCP 服务器实现:TcpServer.hpp
- 2.6 协议层核心实现:Protocol.hpp
- 2.7 业务层实现:Calculator.hpp
前言:
在上一篇博客《彻底搞懂 TCP 应用层自定义协议与序列化:从底层原理到工业级实战》中,我们深入讲解了应用层协议的核心价值、序列化与反序列化的底层逻辑,以及 TCP 粘包问题的本质与解决方案。理论终究要落地到实践。本文将基于完整的工业级代码实现,手把手带你构建一个三层架构的网络版计算器服务。我们将从底层的 Socket 封装、日志系统,到中间的协议层设计,再到上层的业务逻辑实现,最后搞定服务的守护进程化部署,完整还原企业级网络应用的开发流程。本文所有代码均经过严格测试,不仅能让你彻底掌握自定义协议的实现细节,更能学到模板方法模式、策略模式、RAII 机制等经典设计思想在网络编程中的应用,这些都是后端开发面试中的高频考点。
一. 整体架构设计:三层解耦的工业级架构
在开始编码之前,我们首先要明确整体的架构设计。一个优秀的网络应用,必须做到高内聚、低耦合,便于后续的扩展与维护。我们采用经典的三层架构设计:
┌─────────────────┐
│ 业务层 │ Calculator.hpp:负责具体的计算逻辑
├─────────────────┤
│ 协议层 │ Protocol.hpp:负责封包、解包、序列化、反序列化
├─────────────────┤
│ 网络通信层 │ Socket.hpp + TcpServer.hpp:负责TCP连接与数据收发
└─────────────────┘
1.1 三层架构的核心职责
- 网络通信层:只关心数据的收发,不关心数据的业务含义。提供统一的 Socket 接口,封装 TCP 连接的建立、监听、接受、读写等底层操作。
- 协议层:是连接网络层与业务层的桥梁。负责将网络中收到的字节流解析成结构化的请求报文,将业务层返回的响应报文序列化成字节流,并彻底解决 TCP 粘包问题。
- 业务层:只关心具体的业务逻辑,不关心数据如何在网络中传输。接收协议层传递的结构化请求,处理后返回结构化的响应。
1.2 核心设计思想:回调函数解耦
三层之间通过回调函数进行解耦:
- 网络层收到数据后,不直接处理,而是通过回调函数将数据传递给协议层
- 协议层解析出完整的请求后,通过回调函数将结构化请求传递给业务层
- 业务层处理搞定后,将响应返回给协议层,由协议层负责序列化和发送
二. 核心模块源码深度解析
2.1 网络地址封装:InetAddr.hpp
网络编程中,我们经常需要处理 IP 地址和端口号的转换(主机字节序与网络字节序的转换)。InetAddr 类将这些繁琐的操作封装起来,提供统一的接口。
#ifndef __INETADDR__HPP
#define __INETADDR__HPP
#include
#include
#include
#include
#include
// 宏定义:将具体的 sockaddr_in 指针强制转换为通用的 sockaddr 指针
// 许多套接字系统调用(如 bind, recvfrom)要求传入通用地址结构
#define CONV(addr) (struct sockaddr*)(addr)
class InetAddr
{
public:
// 新增: 无参构造
// 方便今天新增的去使用
InetAddr()
{}
// 网络转本地:主要用于接收消息后,解析对端的 IP 和 端口
InetAddr(struct sockaddr_in &addr): _net_addr(addr)
{
// ntohs: Network to Host Short,将 16 位网络字节序(大端)转为主机字节序(通常是小端)
_port = ntohs(_net_addr.sin_port);
// inet_ntoa: 将 32 位网络数值 IP 转换为点分十进制的字符串形式
_ip = inet_ntoa(_net_addr.sin_addr);
}
// 本地转网络
// 可以给引用
// 这里缺省,我们服务端可以直接传个端口就行,允许任意IP地址, INADDR_ANY
// 客户端的话我们使用的时候需要指定对应的服务端地址 -- ?
InetAddr(uint16_t port, const std::string ip = "0.0.0.0")
: _port(port)
, _ip(ip)
{
// 填充 sockaddr_in 结构体
_net_addr.sin_family = AF_INET; // 设置为 IPv4 协议族
// htons: Host to Network Short,将本地主机字节序转为网络字节序
_net_addr.sin_port = htons(_port);
// inet_addr: 将字符串 IP 转换为 32 位网络字节序的数值
_net_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
}
// 获取主机字节序的端口号
uint16_t Port() const { return _port; }
// 获取点分十进制字符串 IP
std::string Ip() const { return _ip; }
// 获取指向底层 sockaddr 结构的指针,用于 bind/sendto 等系统调用
struct sockaddr *Addr()
{
return CONV(&_net_addr);
}
// 获取底层结构体的大小,用于套接字系统调用时的长度参数
socklen_t AddrLen()
{
return sizeof(_net_addr);
}
// 重载判等运算符:通过 IP 和端口唯一标识一个网络端点
// 常用于在在线用户列表中查找指定客户端
bool operator==(const InetAddr &who) const
{
return (_ip == who._ip) && (_port == who._port);
}
// 新增: 重载赋值运算符: 接收连接时快速填充地址信息
void operator=(const struct sockaddr_in &addr)
{
_net_addr = addr;
_port = ntohs(_net_addr.sin_port);
_ip = inet_ntoa(_net_addr.sin_addr);
}
// 将地址信息转化为易读的字符串格式,如 [127.0.0.1:8080]
// 常用于打印日志信息
std::string StringAddress() const
{
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
~InetAddr()
{}
private:
// 本地主机格式的地址信息
uint16_t _port;
std::string _ip;
// 原始网络格式的地址结构体
struct sockaddr_in _net_addr;
};
#endif
核心设计解读:
- 双构造函数设计:分别支持 “本地转网络” 和 “网络转本地” 两种场景,覆盖了服务端绑定、客户端连接、接收对端消息等所有网络地址处理场景。
- 字节序自动转换:内部自动处理主机字节序与网络字节序的转换,上层调用者无需关心底层细节。
- 便捷的日志支持:提供
StringAddress()方法,将地址转为易读的[IP:Port]格式,极大方便了日志打印和调试。
2.2 线程安全基础:Mutex.hpp
在多线程 / 多进程环境中,我们需要互斥锁来保护临界资源。这里我们封装了 POSIX 互斥锁,并提供 RAII 风格的锁守卫,自动管理锁的生命周期。
#ifndef MUTEX_HPP
#define MUTEX_HPP
#include
#include
// 互斥锁封装类:提供加锁/解锁及获取原始锁的接口
class Mutex
{
public:
// 构造函数:初始化互斥锁
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
// 析构函数:销毁互斥锁
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
// 加锁操作
void Lock()
{
pthread_mutex_lock(&_lock);
}
// 解锁操作
void UnLock()
{
pthread_mutex_unlock(&_lock);
}
// 获取原始互斥锁指针,用于需要原生 pthread_mutex_t 的接口
pthread_mutex_t* Origin()
{
return &_lock;
}
private:
pthread_mutex_t _lock; // POSIX 互斥锁
};
// RAII 风格的锁守卫类:构造时加锁,析构时解锁,自动管理锁的生命周期
class LockGuard
{
public:
// 构造函数:接收一个 Mutex 指针,并立即加锁
LockGuard(Mutex* lockptr) : _lockptr(lockptr)
{
_lockptr->Lock();
}
// 析构函数:自动解锁
~LockGuard()
{
_lockptr->UnLock();
}
private:
Mutex* _lockptr; // 指向被管理的互斥锁
};
#endif
核心设计解读:
- RAII 机制:LockGuard 类利用 C++ 的对象生命周期管理机制,在构造时加锁,析构时解锁。即使函数中途抛出异常,锁也会被正确释放,彻底避免了死锁风险。
- 原生接口支持:提供
Origin()方法获取原始互斥锁指针,方便与需要原生 pthread 接口的第三方库集成。
2.3 工业级日志系统:Logger.hpp
日志是网络服务调试和运维的核心工具。我们实现了一个功能完善的日志系统,支持控制台和文件两种输出策略,采用策略模式设计,易于扩展。
```
ifndef LOGGER_HPP
define LOGGER_HPP
include
include
include
include
include
include
include
include
include
include "Mutex.hpp"
namespace LogModule
{
// 1. 获取时间
std::string GetTimeStamp()
{
time_t currentTime = time(nullptr); // 默认获取当前时区的时间
// 我们希望把这个时间转换成年-月-日 时:分:秒
struct tm dataTime;
// 使用线程安全的版本 localtime_r,防止在多线程并发获取时间时
// 因为共享静态全局变量而导致的时间数据覆盖错乱。
localtime_r(¤tTime, &dataTime);
char dataTimeStr[128];
// 使用 snprintf 保证缓冲区不溢出,%02d 确保时间位宽不足时自动补0(如09秒)
snprintf(dataTimeStr, sizeof(dataTimeStr), "%4d-%02d-%02d %02d:%02d:%02d",
dataTime.tm_year + 1900, // tm_year 是从1900年开始计算的偏移量
dataTime.tm_mon + 1, // tm_mon 范围是 [0, 11],需加1修正
dataTime.tm_mday,
dataTime.tm_hour,
dataTime.tm_min,
dataTime.tm_sec
);
return dataTimeStr;
}
// 2. 日志等级 -- 枚举类型(整数)转换成字符串类型
// 使用 enum class 强类型枚举,避免命名污染,提高类型检查的严谨性
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
/**
@brief 辅助函数:将枚举常量映射为可读字符串
解决强类型枚举无法直接通过 std::cout 打印的问题
*/
std::string LogLevel2String(LogLevel level)
{
switch(level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
// 3. 刷新策略
// 基类: 策略模式
// 设计意图:将“日志消息的生成”与“日志消息的输出去向”解耦,方便后续扩展网络、数据库等输出端
class LogStrategy
{
public:
// 虚析构函数:确保通过基类指针释放子类对象时,子类的资源(如文件句柄)能被正确释放
virtual ~LogStrategy() = default; // 不在这里析构
// 纯虚函数:定义统一的刷新接口规范
virtual void SyncLog(const std::string &message) = 0; // 强制子类对其进行重写
};
// 策略1: 控制台日志策略
// 子类
class ConsoleLogStrategy: public LogStrategy
{
public:
ConsoleLogStrategy(){}
~ConsoleLogStrategy(){}
void SyncLog(const std::string &message) override // 检查重写的错误
{
// 显示器在多线程下是“临界资源”,加锁防止多线程输出字符交织(Interleaving)
LockGuard logGuard(&_mutex);
std::cout
以上就是这次整理的全部内容,希望对你有所启发。如果有不同见解,欢迎在评论区交流讨论。
评论 (0)
暂无评论