网络版本计算器
目录
前言
代码部分
复用代码部分:
网络地址封装类-InetAddr.hpp
锁的封装类-LockGuard.hpp
日志类-Log.hpp
协议头文件-Protocol.hpp
计算机业务类-NetCal.hpp
核心功能
关键逻辑说明
作用与设计意义
Tcp套接字封装类-Socket.hpp
1. 类结构与设计模式
2. 核心功能(TcpSocket)
3. 设计优势
4. 典型应用场景
1. CreateSocketOrDie()
2. CreateBindOrDie(uint16_t port)
3. CreateListenOrDie(int backlog)
4. Accepter(InetAddr *cliaddr)
5. Conntecor(const std::string &peerip, uint16_t peerport)
6. Sockfd()
7. Close()
8. Recv(std::string *out)
9. Send(const std::string &in)
总结
服务类-Service.hpp
1. 核心组件与初始化
2. IOExcute 方法:完整的通信处理流程
3. 设计意义与优势
服务端源文件-ServerMain.cc
1. 程序启动与参数校验
2. 三层架构绑定(核心逻辑)
3. 服务启动与运行
4. 设计优势
服务端头文件-TcpServer.hpp
1. 核心组件与初始化
2. 核心方法:Loop() 服务端主循环
3. 多线程处理:Execute() 静态线程函数
4. 设计优势
5. 注意事项
客户端源文件-ClientMain.cc
1. 程序启动与参数校验
2. 连接服务端
3. 核心流程:循环发送请求与接收响应
4. 设计特点
5. 注意事项
前言
我们的网络版本计算器项目就是为了将我们前面谈到的序列化和反序列化带到服务里进行实现。
代码部分
复用代码部分:
网络地址封装类-InetAddr.hpp
#pragma once#include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>// 封装网络地址类 class InetAddr { private:void ToHost(const struct sockaddr_in &addr){_port = ntohs(addr.sin_port);//_ip = inet_ntoa(addr.sin_addr);char ip_buf[32];::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));_ip = ip_buf;}public:InetAddr(const struct sockaddr_in &addr): _addr(addr){ToHost(addr); // 将addr进行转换}std::string AddrStr(){return _ip + ":" + std::to_string(_port);}InetAddr(){}bool operator==(const InetAddr &addr){return (this->_ip == addr._ip && this->_port == addr._port);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr_in Addr(){return _addr;}~InetAddr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr; };锁的封装类-LockGuard.hpp
#pragma once#include <pthread.h>class LockGuard { public:LockGuard(pthread_mutex_t *mutex) : _mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}private:pthread_mutex_t *_mutex; };日志类-Log.hpp
#pragma once#include <iostream> #include <string> #include <unistd.h> #include <sys/types.h> #include <ctime> #include <stdarg.h> #include <fstream> #include <string.h> #include <pthread.h>namespace log_ns {enum{DEBUG = 1,INFO,WARNING,ERROR,FATAL};std::string LevelToString(int level){switch (level){case DEBUG:return "DEBUG";case INFO:return "INFO";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOW";}}std::string GetCurrTime(){time_t now = time(nullptr);struct tm *curr_time = localtime(&now);char buffer[128];snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",curr_time->tm_year + 1900,curr_time->tm_mon + 1,curr_time->tm_mday,curr_time->tm_hour,curr_time->tm_min,curr_time->tm_sec);return buffer;}class logmessage{public:std::string _level;pid_t _id;std::string _filename;int _filenumber;std::string _curr_time;std::string _message_info;};#define SCREEN_TYPE 1#define FILE_TYPE 2const std::string glogfile = "./log.txt";pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;class Log{public:Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE){}void Enable(int type){_type = type;}void FlushLogToScreen(const logmessage &lg){printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());}void FlushLogToFile(const logmessage &lg){std::ofstream out(_logfile, std::ios::app);if (!out.is_open())return;char logtxt[2048];snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());out.write(logtxt, strlen(logtxt));out.close();}void FlushLog(const logmessage &lg){pthread_mutex_lock(&glock);switch (_type){case SCREEN_TYPE:FlushLogToScreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}pthread_mutex_unlock(&glock);}void logMessage(std::string filename, int filenumber, int level, const char *format, ...){logmessage lg;lg._level = LevelToString(level);lg._id = getpid();lg._filename = filename;lg._filenumber = filenumber;lg._curr_time = GetCurrTime();va_list ap;va_start(ap, format);char log_info[1024];vsnprintf(log_info, sizeof(log_info), format, ap);va_end(ap);lg._message_info = log_info;// 打印出日志FlushLog(lg);}~Log(){}private:int _type;std::string _logfile;};Log lg;#define LOG(level, Format, ...) do {lg.logMessage(__FILE__, __LINE__, level, Format, ##__VA_ARGS__); }while (0)#define EnableScreen() do {lg.Enable(SCREEN_TYPE);}while(0)#define EnableFile() do {lg.Enable(FILE_TYPE);}while(0) }以上三个封装类都是我们的老朋友了,我们接下来就来了解了解我们的新朋友,首先就是我们本次的核心内容-协议(即我们上篇文章提到过的序列化与反序列化)。
协议头文件-Protocol.hpp
#pragma once#include <iostream> #include <memory> #include <string> #include <jsoncpp/json/json.h>static const std::string sep = "\r\n";// 设计一下协议的报头和报文的完整格式 // "len"\r\n"json"\r\n --- 完整的报文, len有效载荷的长度! //\r\n:区分len和json串 //\r\n暂时没有其他用,打印方便,debug // 添加报头 std::string Encode(const std::string &jsonstr) {int len = jsonstr.size();std::string lenstr = std::to_string(len);return lenstr + sep + jsonstr + sep; } // 不能带const //"le //"len" //"len"\r\n //"len"\r\n"js //"len"\r\n"json" //"len"\r\n"json"\r\n // "len"\r\n"json"\r\n"len"\r\n"json"\r\n"len"\r\n"json"\r\n std::string Decode(std::string &packagestream) {// 分析auto pos = packagestream.find(sep);if (pos == std::string::npos)return std::string();std::string lenstr = packagestream.substr(0, pos);int len = std::stoi(lenstr);// 计算一个完整的报文应该是多长??int total = lenstr.size() + len + 2 * sep.size();if (packagestream.size() < total)return std::string();// 提取std::string jsonstr = packagestream.substr(pos + sep.size(), len);packagestream.erase(0, total);return jsonstr; } class Request { public:Request(){}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}bool Serialize(std::string *out) // 序列化{// 1.使用现成的库, xml,json(jsoncpp),protobufJson::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;// Json::StytledWriter writer;std::string s = writer.write(root);*out = s;return true;}bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}void Print(){std::cout << _x << std::endl;std::cout << _y << std::endl;std::cout << _oper << std::endl;}~Request(){}int X(){return _x;}int Y(){return _y;}char Oper(){return _oper;}void SetValue(int x, int y, char oper){_x = x;_y = y;_oper = oper;}private:int _x;int _y;char _oper; // + - * / % };// struct request resp={30,0}; class Response { public:Response() : _result(0), _code(0), _desc("success"){}bool Serialize(std::string *out){// 1.使用现成的库, xml,json(jsoncpp),protobufJson::Value root;root["result"] = _result;root["code"] = _code;root["desc"] = _desc;Json::FastWriter writer;// Json::StytledWriter writer;std::string s = writer.write(root);*out = s;return true;}bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);if (!res)return false;_result = root["result"].asInt();_code = root["code"].asInt();_desc = root["desc"].asString();return true;}void PrintResult(){std::cout << "result: " << _result << std::endl;}~Response(){}public:int _result;int _code; // 0:success,1:div zero 2.非法操作std::string _desc; };class Factory { public:static std::shared_ptr<Request> BuildRequestDefault(){return std::make_shared<Request>();}static std::shared_ptr<Response> BuildResponseDefault(){return std::make_shared<Response>();} };根据我们上一篇文章提到的内容我们大致理解了协议是一个什么东西,协议就是两端之间互相通信的一种约定,有了协议我们就可以将一份非基本数据类型的数据传达给两方并让两方清楚数据表达的信息。
在我们这,我们就理解的简单一点,我们在此处的协议就是表达我们的序列化与反序列化(实际上应该是说序列化与反序列化是我们协议的一种)。
既然此处我们要实现协议的序列化与反序列化内容,根据我们先前的铺垫我们知道他们实际上是两种过程导向,比如说序列化,为何要序列化,就是因为我要发送数据给对方,为了让对方能够准确的获取我们的数据我们才需要将我们的数据给进行序列化嘛,反序列化也是一个道理,我们要将对方发过来的数据进行解析就需要我们反序列化的过程。
而且大家观察我所写的代码不难发现怎么会有两个类呢?一个好像是请求的意思,一个好像是响应的意思,里面的内容好像都大差不差,这是为什么呢?实际上这是为了我们代码的规范和解耦,我们将两种模式-请求和响应分别做处理,这样我们上层调用代码的时候就更加清晰,我们让请求类做请求相关的处理,让响应类做响应相关的处理。
class Request { public:Request(){}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}bool Serialize(std::string *out) // 序列化{// 1.使用现成的库, xml,json(jsoncpp),protobufJson::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;// Json::StytledWriter writer;std::string s = writer.write(root);*out = s;return true;}bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}void Print(){std::cout << _x << std::endl;std::cout << _y << std::endl;std::cout << _oper << std::endl;}~Request(){}int X(){return _x;}int Y(){return _y;}char Oper(){return _oper;}void SetValue(int x, int y, char oper){_x = x;_y = y;_oper = oper;}private:int _x;int _y;char _oper; // + - * / % };我们一步一步来,我们先来看看请求部分的代码,我们既然是一个网络版本计算器,那么我们的核心数据我们就要约定好,我们这里只实现简单的加减乘除,因此我们就三个数据类型,分别是两个操作数和一个运算符,这也是我们双方关心的数据。
上层创建Request对象的时候是我们接受请求进行处理,我们提供了两种构造函数,带参和不带参的进行选择。
序列化过程就是调用我们上篇文章提到过的Jsoncpp类,我们直接使用线程的类就能满足我们的需求了,用法在上一篇文章中我也都做了详细的说明,这里我就大致说一说就可以了。我们先创建一个Json::Value的对象,然后将我们的三个变量和对应的值给填写进去,再通过我们的Json::FastWriter对象将我们的数据转换成字符串类型,让out带出我们的数据就可以了。
反序列化就是将我们的接收到的字符串进行解析,我们要创建Json::Reader类型的对象将我们的字符串内容解析成Json::Value类型的数据,解析成功后,我们就可以将三个参数对应的值从Json::Value对象中获取到了。
这几个方法见名知意我就不做过多讲解了。
class Response { public:Response() : _result(0), _code(0), _desc("success"){}bool Serialize(std::string *out){// 1.使用现成的库, xml,json(jsoncpp),protobufJson::Value root;root["result"] = _result;root["code"] = _code;root["desc"] = _desc;Json::FastWriter writer;// Json::StytledWriter writer;std::string s = writer.write(root);*out = s;return true;}bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);if (!res)return false;_result = root["result"].asInt();_code = root["code"].asInt();_desc = root["desc"].asString();return true;}void PrintResult(){std::cout << "result: " << _result << std::endl;}~Response(){}public:int _result;int _code; // 0:success,1:div zero 2.非法操作std::string _desc; };响应类部分的内容要处理的信息是不一样的,但是操作跟我们上面的请求类是雷同的,其核心功能同样也是序列化和反序列化。
我们来看看我们三个成员变量,既然是响应,那么我们就需要result返回我们的计算结果了,同时,我们还可能面临我们的数据是存在问题的,所以还需要我们的code来表示我们运算结果的正确性,0表示成功,1表示出现了除零错误,2表示数据操作是非法的,比如运算符没给对等等。desc就是对我们本次处理结果的描述。
构造函数方面我们给我们就无参构造就可以了,我们给三个变量先附上默认初始值。
序列化还是一样的操作,将我们上层计算好的结果通过同样的操作转换成字符串带出。
我们的反序列化就是将字符串的内容解析到本地。
如果想要查查结果是否正确可以调用此函数。
我们采用工厂模式,也就是通过智能指针的方式让上层获取请求与响应的对象指针。采用工厂模式的好处如下:
1. 封装对象创建逻辑
将
Request和Response对象的创建逻辑封装在工厂类的静态方法中,外界无需关心对象具体是如何创建的,只需调用工厂方法即可获取对象实例。这样可以隐藏对象创建的细节,降低代码的耦合度。2. 集中管理对象创建
如果后续需要修改
Request或Response的创建方式(比如修改构造参数、更换实现类等),只需要修改工厂类中的对应方法即可,无需在所有使用这些对象的地方进行修改,便于维护和扩展。3. 利用智能指针管理内存
使用
std::shared_ptr(通过std::make_shared创建)来管理对象的生命周期,能够自动处理内存的分配和释放,避免内存泄漏问题。std::make_shared还能优化内存分配(将控制块和对象本身的内存一次性分配),提升性能。4. 遵循设计模式思想
这是简单工厂模式的一种体现,通过工厂类来统一创建相关对象,符合面向对象设计中 “单一职责” 和 “依赖倒置” 的原则,使代码结构更清晰、更具可维护性。
我们再回到这个类最开头的位置,不知道大家有没有发现我们还有两个函数没有讲,就是添加报头和解析报头,这个我前面是没有提到过的,那么这个函数的作用是是什么呢?答案就是以下四点:
1. 解决通信中的 “粘包 / 拆包” 问题
在网络通信(如 TCP)中,数据是以流的形式传输的,可能出现多个报文 “粘” 在一起,或一个报文被拆分成多个部分的情况。通过在报文中明确加入长度字段和分隔符,接收方可以准确识别每个报文的边界,从而正确拆分出完整的报文,避免解析错误。
2. 定义统一的通信规则
不同的系统或模块之间通信时,需要一套双方都认可的 “语言规则”。这段代码定义了 **“长度 + JSON 数据”** 的报文格式,让发送方和接收方对 “如何组织数据” 形成共识,确保数据能被正确理解和处理。
3. 适配业务数据格式
业务数据通常以 JSON 格式存储和传递(如接口请求、数据上报等),将 JSON 字符串封装到自定义报文中,既利用了 JSON 的通用性和可读性,又通过自定义协议的结构保障了传输的可靠性。
4. 便于调试与维护
以
\r\n作为分隔符,在日志打印、调试过程中能直观地区分报文的不同部分(长度段、数据段),降低了开发和排障的成本;同时,集中化的编码逻辑(Encode函数)也让后续格式调整(如修改分隔符、扩展报文字段)更加便捷。更加通俗的讲就是说我们发送过去的数据有可能是不完整的
- 因为网络传输是 “流式” 的,多个数据报文可能被合并成一个流(粘包);
- 或者一个大的报文被拆成多个小片段传输(拆包)。
我们再回过头看我们的添加报头函数,
1. 协议格式设计
- 报文结构:
长度字符串\r\nJSON字符串\r\n
- 以
\r\n(定义为静态常量sep)作为分隔符,区分 “有效载荷长度” 和 “JSON 格式的有效载荷”,同时也作为报文的结束标识。- 长度字段(
lenstr)用于标识后续 JSON 字符串的字节数,便于接收方解析时先读取长度,再准确提取有效载荷。2. 编码函数
Encode的作用接收一个 JSON 格式的字符串,生成符合上述协议的完整报文:
- 步骤 1:计算 JSON 字符串的长度
len;- 步骤 2:将长度转换为字符串
lenstr;- 步骤 3:按照
长度字符串 + 分隔符 + JSON字符串 + 分隔符的格式拼接,返回完整报文。3. 设计优势
- 解析明确性:通过长度字段和固定分隔符,接收方可以清晰地拆分报文结构,避免因 JSON 内容本身的复杂性(如包含特殊字符)导致解析错误。
- 调试便利性:
\r\n的分隔方式在打印和调试时直观易读,便于开发阶段排查问题。- 扩展性:若后续需新增报文字段,可基于现有分隔符逻辑进行扩展,无需大幅修改整体结构。
1. 解码流程
- 步骤 1:查找分隔符通过
find(sep)查找报文里的分隔符\r\n,若找不到则返回空字符串,说明报文不完整或格式错误。- 步骤 2:提取长度字段从报文开头到分隔符的位置截取长度字符串
lenstr,并转换为整数len(有效载荷的长度)。- 步骤 3:判断报文完整性计算一个完整报文的总长度(
长度字符串长度 + 有效载荷长度 + 2倍分隔符长度),若当前报文长度不足,则返回空字符串,等待后续数据。- 步骤 4:提取有效载荷并清理从分隔符后截取长度为
len的 JSON 字符串作为有效载荷,同时从报文字符串中删除已解析的完整报文,以便处理后续数据。2. 设计目的
与之前的
Encode函数配合,实现自定义协议的双向解析:
- 解决网络通信中的 “粘包 / 拆包” 问题,确保接收方能准确拆分并提取有效业务数据(JSON 字符串)。
- 通过严格的格式校验(分隔符存在性、长度匹配性),保证报文解析的可靠性。
计算机业务类-NetCal.hpp
#pragma once#include "Protocol.hpp" #include <memory>class NetCal { public:NetCal(){}~NetCal(){}std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req){auto resp = Factory::BuildResponseDefault();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->_code = 1;resp->_desc = "div zero";}else{resp->_result = req->X() / req->Y();}}break;case '%':{if (req->Y() == 0){resp->_code = 2;resp->_desc = "mod zero";}else{resp->_result = req->X() % req->Y();}}break;default:{resp->_code = 3;resp->_desc = "illegal operation";}break;}return resp;} };这段代码定义了一个名为
NetCal的类,核心功能是处理网络请求中的计算逻辑,具体作用和特点如下:核心功能
NetCal类通过Calculator成员函数,接收一个封装了计算请求的Request对象(智能指针),执行对应的算术运算(加、减、乘、除、取模),并返回封装了计算结果的Response对象(智能指针)。关键逻辑说明
参数与返回值
- 输入:
std::shared_ptr<Request> req(包含待计算的两个数X()、Y()和运算符Oper())。- 输出:
std::shared_ptr<Response>(包含计算结果_result、状态码_code、描述信息_desc)。运算处理
- 根据
req->Oper()判断运算符,执行对应的计算(+、-、*、/、%)。- 针对异常情况(如除数为 0、取模除数为 0、非法运算符),设置不同的状态码和描述信息(例如:
div zero表示除零错误)。对象管理
- 使用
std::shared_ptr管理Request和Response对象的生命周期,自动处理内存释放,避免泄漏。- 通过
Factory::BuildResponseDefault()创建默认的Response对象。作用与设计意义
- 职责单一:专注于算术运算逻辑,与网络传输、协议解析等功能解耦(通过
Request/Response对象交互)。- 异常处理:清晰区分正常计算结果和错误状态,便于调用方(如网络服务)根据
Response中的状态码处理后续逻辑。- 兼容性:结合智能指针和工厂模式,符合面向对象设计规范,便于扩展更多运算类型或修改错误码规则。
Tcp套接字封装类-Socket.hpp
#pragma once#include <iostream> #include <cstring> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <pthread.h> #include <memory>#include "Log.hpp" #include "InetAddr.hpp"namespace socket_ns {using namespace log_ns;class Socket;using SockSPtr = std::shared_ptr<Socket>;enum{SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR};const static int gbacklog = 8;// 模板方法模式class Socket{public:virtual void CreateSocketOrDie() = 0;virtual void CreateBindOrDie(uint16_t port) = 0;virtual void CreateListenOrDie(int backlog = gbacklog) = 0;virtual SockSPtr Accepter(InetAddr *cliaddr) = 0;virtual bool Conntecor(const std::string &peerip, uint16_t peerport) = 0;virtual int Sockfd() = 0;virtual void Close() = 0;virtual ssize_t Recv(std::string *out) = 0;virtual ssize_t Send(const std::string &in) = 0;public:void BuildListenSocket(uint16_t port){CreateSocketOrDie();CreateBindOrDie(port);CreateListenOrDie();}bool BuildClientSocket(const std::string &peerip, uint16_t peerport){CreateSocketOrDie();return Conntecor(peerip, peerport);}};class TcpSocket : public Socket{public:TcpSocket(){}TcpSocket(int sockfd) : _sockfd(sockfd){}~TcpSocket(){}void CreateSocketOrDie() override{// 1.创建socket_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(FATAL, "sockfd create error\n");exit(SOCKET_ERROR);}LOG(INFO, "listensockfd create success, fd: %d\n", _sockfd);}void CreateBindOrDie(uint16_t port) override{struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;// 2.bind _listensockfd 和 Socket addrif (::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0){LOG(FATAL, "bind error\n");exit(BIND_ERROR);}LOG(INFO, "bind success, sockfd: %d\n", _sockfd);}void CreateListenOrDie(int backlog) override{// 3.因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接if (::listen(_sockfd, gbacklog) < 0){LOG(FATAL, "listen error\n");exit(LISTEN_ERROR);}LOG(INFO, "listen success\n");}SockSPtr Accepter(InetAddr *cliaddr) override{struct sockaddr_in client;socklen_t len = sizeof(client);// 4. 获取连接int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);if (sockfd < 0){LOG(WARNING, "accept error\n");return nullptr;}*cliaddr = InetAddr(client);LOG(INFO, "get a new link, client info: %s, sockfd is : %d\n", cliaddr->AddrStr().c_str(), sockfd);return std::make_shared<TcpSocket>(sockfd); // C++14}bool Conntecor(const std::string &peerip, uint16_t peerport) override{struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(peerport);::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){return false;}return true;}int Sockfd(){return _sockfd;}void Close(){if (_sockfd > 0){::close(_sockfd);}}ssize_t Recv(std::string *out) override{char inbuffer[4096];ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if (n > 0){inbuffer[n] = 0;*out += inbuffer;}return n;}ssize_t Send(const std::string &in) override{return ::send(_sockfd, in.c_str(), in.size(), 0);}private:int _sockfd; // 可以是listensock, 也可以是普通socketfd};}这是一个基于 C++ 实现的TCP 套接字封装类库,属于网络编程模块,核心设计与功能如下:
1. 类结构与设计模式
- 抽象基类
Socket:定义了网络套接字的通用接口(创建、绑定、监听、接收连接、收发数据等),采用模板方法模式,将具体实现延迟到子类。- 子类
TcpSocket:继承并实现Socket接口,专注于 TCP 协议的套接字操作。2. 核心功能(
TcpSocket)
功能 说明 创建套接字 通过 socket(AF_INET, SOCK_STREAM, 0)创建 TCP 套接字,失败则记录日志并退出。绑定与监听 绑定本地 IP 和端口( bind),并设置监听队列(listen),用于服务端建立监听套接字。接收连接 通过 accept获取客户端连接,返回新的套接字对象(std::shared_ptr<TcpSocket>),用于后续通信。发起连接 客户端通过 connect连接服务端 IP 和端口,实现端到端通信。收发数据 - Recv:从套接字读取数据到字符串,支持流式接收;-Send:将字符串数据发送到对端。资源管理 封装 close操作,确保套接字资源正确释放。3. 设计优势
- 解耦与扩展性:抽象基类隔离了接口与实现,若需扩展 UDP 或其他协议的套接字,只需新增子类实现接口即可。
- 内存安全:通过
std::shared_ptr管理套接字对象生命周期,避免内存泄漏。- 日志集成:结合自定义日志模块(
Log.hpp),在关键操作(如创建、绑定失败)时输出分级日志(FATAL、INFO、WARNING),便于调试和运维。4. 典型应用场景
- 服务端:通过
BuildListenSocket快速创建监听套接字,循环Accepter处理客户端连接,实现多客户端并发通信。- 客户端:通过
BuildClientSocket连接服务端,利用Send/Recv与服务端交互数据。简言之,这段代码是一个轻量级的 TCP 套接字工具类,封装了底层系统调用,简化了 C++ 网络编程中 TCP 套接字的创建、连接和数据收发流程,同时保证了代码的可维护性和扩展性。
Socket基类中定义的虚拟函数是网络套接字操作的核心接口,TcpSocket子类对这些接口的实现,完整覆盖了 TCP 协议从创建到通信的全流程。以下是各虚拟函数的实现细节及作用:1.
CreateSocketOrDie()实现:调用系统函数
socket(AF_INET, SOCK_STREAM, 0)创建 TCP 套接字(AF_INET表示 IPv4,SOCK_STREAM表示 TCP 流式协议),将返回的文件描述符保存到_sockfd。若创建失败(_sockfd < 0),通过日志模块输出错误信息并退出程序(错误码SOCKET_ERROR)。作用:负责初始化 TCP 套接字的底层资源,是所有网络操作的基础。
OrDie后缀表示 “失败则终止程序”,确保关键资源创建失败时及时报错。2.
CreateBindOrDie(uint16_t port)实现:构造本地地址结构
sockaddr_in(指定 IPv4 协议、端口号port、绑定所有本地 IPINADDR_ANY),调用bind(_sockfd, (struct sockaddr*)&local, sizeof(local))将套接字与本地地址绑定。若绑定失败,输出日志并退出(错误码BIND_ERROR)。作用:为服务端套接字 “绑定” 一个固定的端口号,让客户端能通过该端口找到服务端。
OrDie确保绑定失败(如端口被占用)时程序终止,避免后续无效操作。3.
CreateListenOrDie(int backlog)实现:调用
listen(_sockfd, backlog)将套接字设置为 “监听状态”(仅服务端需要),backlog指定未完成连接队列的最大长度(默认gbacklog=8)。若监听失败,输出日志并退出(错误码LISTEN_ERROR)。作用:使 TCP 套接字进入 “被动模式”,准备接收客户端的连接请求。
backlog控制并发连接的临时队列大小,防止连接请求溢出。4.
Accepter(InetAddr *cliaddr)实现:调用
accept(_sockfd, (struct sockaddr*)&client, &len)从监听队列中获取一个已完成的客户端连接,返回新的套接字描述符sockfd(用于与该客户端单独通信)。同时将客户端的地址信息(IP 和端口)存入cliaddr,并通过std::make_shared<TcpSocket>(sockfd)封装为智能指针返回。若失败(如被信号中断),输出警告日志并返回nullptr。作用:服务端接收客户端连接的核心接口,返回的新套接字用于后续与该客户端的收发数据,原监听套接字继续等待其他连接。
cliaddr用于获取客户端的网络地址信息(如打印日志)。5.
Conntecor(const std::string &peerip, uint16_t peerport)实现:构造服务端地址结构
sockaddr_in(指定服务端 IPpeerip、端口peerport),调用connect(_sockfd, (struct sockaddr*)&server, sizeof(server))向服务端发起连接。返回true表示连接成功,false表示失败。作用:客户端主动连接服务端的接口,建立 TCP 三次握手,是客户端与服务端通信的前提。
6.
Sockfd()实现:直接返回成员变量
_sockfd(套接字描述符)。作用:提供获取底层套接字描述符的接口,便于在类外进行一些扩展操作(如设置套接字选项
setsockopt)。7.
Close()实现:若
_sockfd有效(>0),调用close(_sockfd)关闭套接字,释放文件描述符资源。8.
Recv(std::string *out)实现:用固定大小缓冲区(
inbuffer[4096])调用recv(_sockfd, inbuffer, sizeof(inbuffer)-1, 0)从套接字读取数据,读取的字节数为n。若读取成功(n>0),将数据追加到out字符串中(并添加终止符\0),返回实际读取的字节数;若连接关闭(n=0)或错误(n<0),直接返回n。作用:从套接字接收数据(TCP 流式数据),将字节流转换为字符串,方便上层业务处理(如协议解析)。
9.
Send(const std::string &in)实现:调用
send(_sockfd, in.c_str(), in.size(), 0)将字符串in的数据发送到对端,返回实际发送的字节数。作用:向套接字发送数据(字符串形式),封装了底层字节流发送的细节,简化上层数据发送逻辑。
总结
这些虚拟函数通过
TcpSocket的实现,完整封装了 TCP 协议的核心操作:从服务端的 “创建 - 绑定 - 监听 - 接连接”,到客户端的 “创建 - 连服务端”,再到双向的 “收发数据” 和 “资源释放”。抽象接口与具体实现分离的设计,既保证了 TCP 操作的规范性,又为扩展其他协议(如 UDP)预留了接口。服务类-Service.hpp
#pragma once #include <iostream> #include <functional> #include "InetAddr.hpp" #include "Socket.hpp" #include "Log.hpp" #include "Protocol.hpp"using namespace socket_ns; using namespace log_ns;using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;class IOService { public:IOService(process_t process) : _process(process){}void IOExcute(SockSPtr sock, InetAddr &addr){std::string packagestreamqueue;while (true){// 1.负责读取ssize_t n = sock->Recv(&packagestreamqueue);if (n <= 0){LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());break;}std::cout << "--------------------------------" << std::endl;std::cout << "packagestreamqueue: \n"<< packagestreamqueue << std::endl;// 我们能保证我们读到的是一个完整的报文吗?不能!// 2.报文解析,提取报头和有效载荷std::string package = Decode(packagestreamqueue);if (package.empty())continue;// 我们能保证我们读到的是一个完整的报文吗?能!!auto req = Factory::BuildRequestDefault();std::cout << "package: \n"<< package << std::endl;// 3.反序列化req->Deserialize(package);// 4.业务处理auto resp = _process(req); // 通过请求,得到应答// 5.序列化应答std::string respjson;resp->Serialize(&respjson);std::cout << "respjson: \n"<< respjson << std::endl;// 6.添加len长度报头respjson = Encode(respjson);std::cout << "respjson add header done: \n"<< respjson << std::endl;// 7.发送回去sock->Send(respjson);}}~IOService(){}private:process_t _process; };这是一个网络服务中的IO 处理服务类
IOService,负责网络数据的收发、协议解析、业务处理和响应回发,核心逻辑与作用如下:1. 核心组件与初始化
process_t类型:是一个函数对象(std::function),定义为std::shared_ptr<Response>(std::shared_ptr<Request>),用于封装业务处理逻辑(如之前的NetCal::Calculator)。- 构造函数:接收一个
process_t类型的业务处理函数,将其保存到成员变量_process中,实现 “IO 处理” 与 “业务逻辑” 的解耦。2.
IOExcute方法:完整的通信处理流程该方法是类的核心,接收一个已建立连接的套接字
sock和客户端地址addr,循环处理该客户端的通信请求,流程如下:
步骤 操作 说明 1. 数据接收 sock->Recv(&packagestreamqueue)从套接字读取数据,存入 packagestreamqueue(流式接收,可能包含多个报文或不完整报文)。2. 报文解析 Decode(packagestreamqueue)调用解码函数(如之前的 Decode),从流式数据中提取完整的业务报文。若报文不完整则跳过,等待后续数据。3. 请求反序列化 req->Deserialize(package)将解析出的报文(JSON 格式)反序列化为 Request对象,提取计算请求的参数(如X、Y、运算符)。4. 业务处理 _process(req)调用封装的业务处理函数(如计算器逻辑),传入 Request对象,得到Response响应对象。5. 响应序列化 resp->Serialize(&respjson)将 Response对象序列化为 JSON 字符串。6. 报文编码 Encode(respjson)为 JSON 响应添加长度报头(如之前的 Encode),生成符合自定义协议的完整报文。7. 数据发送 sock->Send(respjson)将编码后的响应报文发送回客户端。 3. 设计意义与优势
- 职责分离:
IOService专注于 “网络 IO + 协议解析”,业务逻辑通过process_t注入,符合 “单一职责” 和 “依赖倒置” 原则,便于更换或扩展业务(如从 “计算器” 改为 “字符串处理”)。- 可靠性保障:通过
Decode确保只处理完整报文,避免因数据不完整导致的解析错误;网络操作失败时(如客户端断开)会退出循环,释放资源。- 可观测性:结合日志模块(
Log.hpp)和控制台打印,在关键步骤输出信息(如客户端地址、报文内容),便于调试和运维。简言之,
IOService是网络服务的 “IO 处理中枢”,它串联了 “数据接收 - 协议解析 - 业务处理 - 响应回发” 的全流程,同时通过函数对象解耦业务逻辑,让网络层与业务层的代码更加清晰、可维护。服务端源文件-ServerMain.cc
#include "TcpServer.hpp" #include "Service.hpp" #include "NetCal.hpp"// ./tcpserver 8888 int main(int argc, char *argv[]) {if (argc != 2){std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);// 我们的软件代码,我们手动的划分了三层NetCal cal;IOService service(std::bind(&NetCal::Calculator, &cal, std::placeholders::_1));std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::bind(&IOService::IOExcute, &service, std::placeholders::_1, std::placeholders::_2), port);tsvr->Loop();return 0; }这是整个TCP 计算器服务端的入口程序,通过三层架构(业务层、IO 处理层、网络层)的绑定,实现了 “接收客户端计算请求→处理计算→返回结果” 的完整服务,核心逻辑与架构如下:
1. 程序启动与参数校验
- 启动方式:需在命令行传入本地端口号(如
./tcpserver 8888),程序通过argc校验参数数量,若参数错误则输出使用提示并退出。- 端口转换:将命令行传入的字符串端口号(
argv[1])转换为无符号整数port,作为服务端监听端口。2. 三层架构绑定(核心逻辑)
通过
std::bind实现各层之间的解耦与调用链绑定,三层职责清晰:
层级 核心组件 作用 业务层 NetCal cal封装计算逻辑(加、减、乘、除、取模),提供 Calculator方法处理Request并返回Response。IO 处理层 IOService service封装网络 IO 与协议解析逻辑,通过 std::bind(&NetCal::Calculator, &cal, _1)将业务逻辑注入,实现 “IO + 协议 + 业务” 的串联。网络层 std::unique_ptr<TcpServer> tsvr封装 TCP 服务端的监听、连接接收逻辑,通过 std::bind(&IOService::IOExcute, &service, _1, _2)将 IO 处理逻辑注入,负责接收客户端连接并交给IOService处理。3. 服务启动与运行
- 调用
tsvr->Loop()启动服务端主循环:服务端会在指定端口监听客户端连接,一旦有客户端接入,就会创建新的通信链路,并调用绑定的IOService::IOExcute方法处理该客户端的后续通信(数据收发、协议解析、业务计算)。- 主循环会持续运行,直到程序被主动终止(如 Ctrl+C),实现服务的持续可用。
4. 设计优势
- 架构清晰:三层分离(网络层负责连接、IO 层负责协议、业务层负责计算),各模块职责单一,便于维护和扩展。
- 解耦灵活:通过
std::bind注入依赖,若需更换业务逻辑(如从 “计算器” 改为 “数据查询”),只需修改业务层对象,无需改动 IO 层和网络层代码。- 资源安全:使用
std::unique_ptr管理TcpServer对象,自动释放资源,避免内存泄漏。简言之,这行代码完成了整个 TCP 计算器服务端的 “组装” 与启动,将网络通信、协议解析、业务计算三个核心能力串联起来,最终实现一个可对外提供计算服务的 TCP 服务器。
服务端头文件-TcpServer.hpp
#pragma once #include <functional> #include "Socket.hpp" #include "Log.hpp" #include "InetAddr.hpp"using namespace socket_ns;static const int gport = 8888;using service_io_t = std::function<void(SockSPtr, InetAddr &)>;class TcpServer { public:TcpServer(service_io_t service, uint16_t port = gport): _port(port), _listensock(std::make_shared<TcpSocket>()), _isrunning(false), _service(service){_listensock->BuildListenSocket(_port);}class ThreadData{public:SockSPtr _sockfd;TcpServer *_self;InetAddr _addr;public:ThreadData(SockSPtr sockfd, TcpServer *self, const InetAddr &addr): _sockfd(sockfd), _self(self), _addr(addr){}};void Loop(){_isrunning = true;while (_isrunning){InetAddr client;SockSPtr newsock = _listensock->Accepter(&client);if (newsock == nullptr)continue;LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", client.AddrStr().c_str(), newsock->Sockfd());// version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了pthread_t tid;ThreadData *td = new ThreadData(newsock, this, client);pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离}_isrunning = false;}static void *Execute(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->_self->_service(td->_sockfd, td->_addr);td->_sockfd->Close();delete td;return nullptr;}~TcpServer(){}private:uint16_t _port;SockSPtr _listensock;bool _isrunning;service_io_t _service; };这是多线程 TCP 服务端核心类
TcpServer,负责监听客户端连接并通过多线程并发处理每个客户端的 IO 请求,核心设计与功能如下:1. 核心组件与初始化
service_io_t类型:函数对象(std::function<void(SockSPtr, InetAddr &)>),用于接收 IO 处理逻辑(如IOService::IOExcute),实现网络层与 IO 处理层的解耦。- 构造函数:
- 接收 IO 处理函数
_service和监听端口_port(默认gport=8888)。- 通过
_listensock->BuildListenSocket(_port)快速创建 TCP 监听套接字(封装了 “创建 - 绑定 - 监听” 三步操作)。- 成员
_listensock是TcpSocket的智能指针,负责管理监听套接字资源。2. 核心方法:
Loop()服务端主循环作为服务端的 “入口”,持续监听并接收客户端连接,流程如下:
- 设
_isrunning=true,进入无限循环(服务运行状态)。- 调用
_listensock->Accepter(&client)阻塞等待客户端连接,成功后得到与该客户端通信的新套接字newsock和客户端地址client。- 若连接接收失败(
newsock==nullptr),跳过此次循环,继续等待下一个连接。- 为新连接创建独立线程:通过
pthread_create启动新线程,传入封装了newsock、当前TcpServer实例、客户端地址的ThreadData对象,由新线程处理该客户端的后续通信。3. 多线程处理:
Execute()静态线程函数
- 线程启动后,首先调用
pthread_detach(pthread_self())让线程分离,无需主线程等待回收,避免资源泄漏。- 将传入的
args转换为ThreadData指针,调用td->_self->_service(td->_sockfd, td->_addr),即执行注入的 IO 处理逻辑(如IOService::IOExcute),处理该客户端的收发数据、协议解析、业务计算。- 客户端通信结束后(如断开连接),调用
td->_sockfd->Close()关闭套接字,释放ThreadData对象,线程退出。4. 设计优势
- 并发处理:通过多线程实现 “一个客户端一个线程”,支持同时处理多个客户端请求,提升服务并发能力。
- 解耦灵活:IO 处理逻辑通过
service_io_t注入,TcpServer仅负责连接管理和线程创建,无需关心具体业务,可适配不同 IO 处理场景。- 资源安全:监听套接字通过智能指针管理,客户端套接字在线程结束时主动关闭,
ThreadData动态内存手动释放,避免资源泄漏。- 简单可靠:基于 POSIX 线程库(
pthread)实现,逻辑简洁,适配 Linux 等类 Unix 系统,满足基础并发服务需求。5. 注意事项
- 该实现为 “简单多线程” 模型,若客户端数量过多(如千级以上),会因线程数量过多导致系统资源耗尽,适合中小规模客户端场景。
- 线程分离后,主线程无法控制子线程生命周期,若需更精细的线程管理(如线程池、优雅退出),需额外扩展。
客户端源文件-ClientMain.cc
#include <iostream> #include <ctime> #include <unistd.h> #include "Socket.hpp" #include "Protocol.hpp"using namespace socket_ns;int main(int argc, char *argv[]) {if (argc != 3){std::cerr << "Usage: " << argv[0] << " service-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);SockSPtr sock = std::make_shared<TcpSocket>();if (!sock->BuildClientSocket(serverip, serverport)){std::cerr << "connect error" << std::endl;exit(1);}srand(time(nullptr) ^ getpid());const std::string opers = "+-*/%&^!";int cnt = 3;std::string packagestreamqueue;while (true){// 构建数据int x = rand() % 10;usleep(x * 1000);int y = rand() % 10;usleep(x * y * 100);char oper = opers[y % opers.size()];// 构建请求auto req = Factory::BuildRequestDefault();req->SetValue(x, y, oper);// 1.序列化std::string reqstr;req->Serialize(&reqstr);// 2.添加长度报头字段reqstr = Encode(reqstr);std::cout << "###################################" << std::endl;std::cout << "requst string: \n"<< reqstr << std::endl;// 3.发送数据sock->Send(reqstr);while (true){// 4.读取应答,responsessize_t n = sock->Recv(&packagestreamqueue);if (n <= 0){break;}// 我们能保证我们读到的是一个完整的报文吗?不能!// 5. 报文解析,提取报头和有效载荷std::string package = Decode(packagestreamqueue);if (package.empty())continue;std::cout << "package: \n"<< package << std::endl;// 6.反序列化auto resp = Factory::BuildResponseDefault();resp->Deserialize(package);// 7.打印结果resp->PrintResult();break;}sleep(1);}sock->Close();return 0; }这是TCP 计算器客户端程序,用于主动连接服务端、随机生成计算请求并接收响应,核心逻辑与功能如下:
1. 程序启动与参数校验
- 启动方式:需在命令行传入服务端 IP 和端口(如
./tcpclient 127.0.0.1 8888)。- 参数校验:若参数数量不为 3,输出使用提示并退出,确保连接信息完整。
2. 连接服务端
- 创建 TCP 客户端套接字(
SockSPtr sock = std::make_shared<TcpSocket>()),通过智能指针管理资源。- 调用
BuildClientSocket(serverip, serverport)向服务端发起连接,失败则输出错误并退出程序。3. 核心流程:循环发送请求与接收响应
程序会持续生成随机计算请求并与服务端交互,单轮流程如下:
步骤 操作 说明 1. 生成随机请求数据 随机生成两个整数 x(0-9)、y(0-9),从运算符集合"+-*/%&^!"中随机选一个运算符oper。用 rand()结合进程 ID 和时间戳初始化随机种子,确保每次运行生成的请求不同。2. 构建并序列化请求 通过 Factory::BuildRequestDefault()创建Request对象,调用SetValue(x, y, oper)赋值,再通过Serialize转换为 JSON 字符串。统一请求格式,便于服务端解析。 3. 协议编码 调用 Encode为 JSON 字符串添加长度报头,生成符合自定义协议的完整报文。适配服务端的解码逻辑,解决粘包 / 拆包问题。 4. 发送请求 通过 sock->Send(reqstr)将编码后的报文发送给服务端。完成客户端到服务端的请求传输。 5. 接收响应 调用 sock->Recv(&packagestreamqueue)读取服务端返回的数据,存入流式缓冲区。流式接收,可能包含不完整报文,需后续解析。 6. 解析响应报文 调用 Decode从缓冲区提取完整报文,若报文不完整则继续等待接收。确保只处理完整的响应数据,避免解析错误。 7. 反序列化与结果打印 将完整报文反序列化为 Response对象,调用PrintResult()打印计算结果(或错误信息,如除零错误)。直观呈现服务端的处理结果。 4. 设计特点
- 自动化请求生成:无需手动输入,随机生成计算用例,适合测试服务端的并发处理和异常处理能力(如非法运算符、除零等)。
- 协议适配:严格遵循 “序列化→编码→发送” 和 “接收→解码→反序列化” 的流程,与服务端的协议逻辑完全匹配。
- 资源安全:通过智能指针管理套接字,避免内存泄漏;通信结束后主动调用
Close()释放套接字资源。- 循环运行:发送一次请求后休眠 1 秒,持续与服务端交互,直到程序被主动终止。
5. 注意事项
- 运算符集合中包含
&^!,而服务端仅支持+-*/%,这些非法运算符会触发服务端返回 “illegal operation” 错误,可用于测试服务端的异常处理。- 客户端为单线程单连接模式,一次只处理一个请求 - 响应周期,适合简单的服务端功能测试。












