文章目录
- [1. 一些理解](#1. 一些理解)
-
- [1.1 理解序列化与反序列化](#1.1 理解序列化与反序列化)
- [1.2 理解协议](#1.2 理解协议)
- [1.3 重新理解read,write为什么支持全双工](#1.3 重新理解read,write为什么支持全双工)
- [2. 网络版计算器](#2. 网络版计算器)
-
- [2.1 demo v1 (socket封装:引入模板方法模式)](#2.1 demo v1 (socket封装:引入模板方法模式))
-
- [2.1.1 源码](#2.1.1 源码)
- [2.1.2 流程详解](#2.1.2 流程详解)
- [2.1.3 运行结果](#2.1.3 运行结果)
- [2.2 demo v2 (自定义协议)](#2.2 demo v2 (自定义协议))
-
- [2.2.1 源码](#2.2.1 源码)
- [2.2.2 流程详解](#2.2.2 流程详解)
- [2.3 demo v3 (最终版)](#2.3 demo v3 (最终版))
-
- [2.3.1 源码](#2.3.1 源码)
- [2.3.2 流程详解](#2.3.2 流程详解)
1. 一些理解
1.1 理解序列化与反序列化
模拟一个场景:
我们在进行QQ聊天的时候,聊天时的界面不只是显示我们聊天的内容(本质是string),还会显示昵称(本质是string),头像(本质是图片的路径)。这3个数据进行网络传输的时候,不是分开传输的。
而是将三个短的字符串合并成一个长的字符串进行网络传输。该过程称为序列化。
接收方接收到数据后将长字符串拆解成短字符串再使用。该过程称为反序列化。
好处:
- 保证报文的完整性。
- 方便网络传输。
1.2 理解协议

所谓的协议定制 ,本质其实就是在定制双方都能认识的,符合通信和业务需要的结构化数据(结构化数据就是struct,class)。
Q:网络传输数据的时候能不能不转成长字符串,直接把结构体传输过去也就是直接发送二进制对象?
A:可以但是不建议。
理由一:客户端和服务器双方的OS不一定相同,不相同就会导致结构体内存对齐问题不一样。导致无法正确解析传来的二进制。
理由二:客户端和服务器双方使用的语言不一定相同,结构体的定义也就不相同了。
结论:如果要进行网络协议式通信,在应用层,建议使用序列化和反序列化方案,至于直接传输结构体方案,除非特殊场景,否则不建议。
1.3 重新理解read,write为什么支持全双工

Q:write作用是发送数据,write是不是直接把数据发送到网络中?
A:不是的,write没有直接控制硬件的权限,而是将数据拷贝到操作系统的发送缓冲区中 。
所以write本质上其实是一个拷贝函数。至于什么时候发,发多少,出错了怎么办?这些都由操作系统中的TCP传输协议控制,也就是由操作系统内核控制。
操作系统把数据发送网络中,本质上也是拷贝。所以在计算机世界里:通信即拷贝。
read函数也不是从网络中直接读数据,而是等操作系统将数据拷贝到接受缓冲区,然后再去看接收缓冲区有没有数据,如果有数据就将数据再拷贝到用户缓冲区。
结论:
- read/write 本质是进行拷贝。
- 主机间的通信本质:把发送方的发送缓冲区内部的数据拷贝到对端的接收缓冲区。
- TCP通信是全双工的原因是:有两对接收和发送的缓冲区。
2. 网络版计算器
思路:实现网络版计算器,支持客户远端向服务器发送数据,服务器执行计算任务,返回计算结果给客户端。
2.1 demo v1 (socket封装:引入模板方法模式)
2.1.1 源码
socket.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
namespace SocketModule
{
using namespace LogModule;
const static int gbacklog = 16;
// 基类套接字
// 模板方法模式:大部分方法为纯虚方法
class Socket
{
public:
virtual ~Socket() {};
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t port) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual bool Accept() = 0;
public:
void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
// void BuildUdpSocketMethod()
// {
// SocketOrDie();
// BindOrDie();
// }
};
class TcpSocket : public Socket
{
public:
~TcpSocket() {};
void SocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel ::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel ::INFO) << "socket success";
}
void BindOrDie(uint16_t port) override
{
InetAddr localadd(port);
int n = ::bind(_sockfd, localadd.NetAddrPtr(), localadd.NetAddrLen());
if (n < 0)
{
LOG(LogLevel ::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel ::INFO) << "bind success";
}
void ListenOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel ::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel ::INFO) << "listen success";
}
bool Accept() override
{
return true;
}
private:
int _sockfd; // 有可能是listen套接字也有可能是普通的socket
};
// class UdpSocket : public Socket
// {
// public:
// ~UdpSocket() {};
// int SocketOrDie() override
// {
// return 0;
// }
// bool BindOrDie() override
// {
// return true;
// }
// bool ListenOrDie() override
// {
// return true;
// }
// bool Accept() override
// {
// return true;
// }
// private:
// };
};
tcpserver.hpp
cpp
#include "Socket.hpp"
#include <iostream>
#include <memory>
using namespace SocketModule;
using namespace LogModule;
class TcpServer
{
public:
TcpServer(uint16_t port) : _port(port),
_listensockptr(std::make_unique<TcpSocket>())
{
_listensockptr->BuildTcpSocketMethod(_port);
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
sleep(1);
_listensockptr->Accept();
LOG(LogLevel ::DEBUG) << "server is running...";
}
_isrunning = false;
}
~TcpServer()
{
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensockptr;
bool _isrunning;
};
main.cc
cpp
#include "TcpServer.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << "port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]));
tsvr->Start();
return 0;
}
2.1.2 流程详解

从main函数看起。
- 格式化调用方法。
- 创建智能指针对象tsvr,指向服务器对象。传参:端口号。
- 调用服务器方法Start。

先看一眼服务器的成员变量:
- 端口号
- 监听套接字指针
- 运行状态

再看到服务器的构造函数,
main函数创建TcpServer对象时,会先创建一个基类对象(_listensockptr),然后调用TcpServer构造方法的初始化列表时候,再去创建虚基类指向的子类对象(TcpSocket)。
这时候,std::make_unique() 做的是在堆上实例化 TcpSocket 类对象,分配内存、初始化类成员变量 _sockfd,这是C++ 层面的对象构造,不会调用任何系统套接字接口,也不会生成操作系统的文件描述符。
当执行构造方法BuildTcpSocketMethod时:SocketOrDie() 内部执行了系统调用 ::socket(),这是操作系统内核层面创建网络套接字,返回文件描述符 _sockfd,这是真正用于网络通信的内核资源。
这里的socket已经封装,我们先看一下封装的思路。

这里采用的就是模板方法模式。
因为UDP/TCP都需要创建套接字,所以将套接字抽象出来做成模板化,一样的方法直接做成虚方法,放在具体子类中去实现。
二者都需要创建套接字,所以直接放在公共部分实现BuildTcpSocketMethod,里面只需要填各自的逻辑就行。
我们这里只讨论TCP协议。
再回到TcpServer的构造方法,初始化列表完成后,就调用构造方法BuildTcpSocketMethod。完成了创建,绑定,监听。

再回到main函数,创建好TcpServer对象后,调用TcpServer的Start方法。

修改运行状态,从监听队列中取出一个已完成连接的客户端 ,执行相关业务,这里就是打印了一句日志用作测试。
业务完毕后修改服务器状态。
2.1.3 运行结果

2.2 demo v2 (自定义协议)
2.2.1 源码
main.cc
cpp
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << "port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::unique_ptr<Protocol> protocal = std::make_unique<Protocol>();
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),
[&protocal](std::shared_ptr<Socket> &sock, InetAddr &client)
{
protocal->GetRequest(sock, client);
});
tsvr->Start();
return 0;
}
server.hpp
cpp
#include "Socket.hpp"
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>
using namespace SocketModule;
using namespace LogModule;
using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
class TcpServer
{
public:
TcpServer(uint16_t port, ioservice_t service) : _port(port),
_listensockptr(std::make_unique<TcpSocket>()),
_isrunning(false),
_service(service)
{
_listensockptr->BuildTcpSocketMethod(_port);
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
InetAddr client;
auto sock = _listensockptr->Accept(&client);
if (sock == nullptr)
{
continue;
}
LOG(LogLevel ::DEBUG) << "accept success...";
pid_t id = fork();
if (id < 0)
{
LOG(LogLevel::FATAL) << "fock error";
exit(FORK_ERR);
}
else if (id == 0)
{
_listensockptr->Close();
if (fork() > 0)
exit(0);
_service(sock, client);
exit(OK);
}
else
{
sock->Close();
pid_t rid = ::waitpid(id, nullptr, 0);
(void)rid;
}
}
_isrunning = false;
}
~TcpServer()
{
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensockptr;
bool _isrunning;
ioservice_t _service;
};
socket.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
namespace SocketModule
{
using namespace LogModule;
const static int gbacklog = 16;
// 基类套接字
// 模板方法模式:大部分方法为纯虚方法
class Socket
{
public:
virtual ~Socket() {};
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t port) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
virtual void Close() = 0;
public:
void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
};
const static int defaultfd = -1;
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(defaultfd)
{
}
TcpSocket(int fd) : _sockfd(fd)
{
}
~TcpSocket()
{
}
void SocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel ::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel ::INFO) << "socket success";
}
void BindOrDie(uint16_t port) override
{
InetAddr localadd(port);
int n = ::bind(_sockfd, localadd.NetAddrPtr(), localadd.NetAddrLen());
if (n < 0)
{
LOG(LogLevel ::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel ::INFO) << "bind success";
}
void ListenOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel ::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel ::INFO) << "listen success";
}
std::shared_ptr<Socket> Accept(InetAddr *client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = ::accept(_sockfd, CONV(peer), &len);
if (fd < 0)
{
LOG(LogLevel ::WARNING) << "accept warning ...";
return nullptr;
}
client->SetAddr(peer);
return std::make_shared<TcpSocket>(fd);
}
void Close()
{
if (_sockfd >= 0)
::close(_sockfd);
}
private:
int _sockfd; // 有可能是listen套接字也有可能是普通的socket
};
};
protocol.hpp
cpp
#pragma once
#include <string>
#include <iostream>
#include <memory>
#include "Socket.hpp"
using namespace SocketModule;
// 约定好各个字段的含义,本质上就是约定好协议。
// client -> server
class Request
{
public:
Request()
{}
Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
{}
std::string Serialize()
{}
bool Deserialize(std::string &in)
{}
~Request()
{}
private:
int _x;
int _y;
char _oper;
};
// server -> client
class Response
{
public:
Response()
{}
Response(int result, int code) : _result(result), _code(code)
{}
std::string Serialize()
{}
bool Deserialize(std::string &in)
{}
~Response()
{}
private:
int _result;
int _code; // 表示运算结果的退出码
};
// 协议(基于TCP)需要解决的两个问题:
// 1. request和response 必须得有序列化和反序列化功能
// 2. 读取的时候要读取到完整的请求
class Protocol
{
public:
Protocol()
{}
void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
{}
~Protocol()
{}
private:
// Request _req;
// Response _resp;
};
2.2.2 流程详解
这里再捋一遍思路,先从main函数看起

主要是两个智能指针的。
先看到18行,使用智能指针创建了协议对象,这里实现了自定义协议的封装。我们转到protocol



协议中封装了三个类:请求服务类,响应类,和协议类。
协议类作用:对前两个做封装,使面向对象化。
请求类 中的成员变量就是运算数和运算符 ,响应类 中的成员变量是结果和退出码。
重要的是二者要包括序列化和反序列化的成员函数 。
以request类来说:
他的序列化函数的作用 是:将请求中的两个操作数和一个操作符做强转(转成string)后再做字符串的拼接(将三个小的字符串拼接成一个长字符串),再在网络中传输。
反序列化:要将服务器传来的长字符串做分割,将字符串转化为结构化数据。
这里的序列化和反序列化可以自己实现(造轮子)也可以使用开源库Json。具体实现代码我们放在最终版demo中展示。
再回到我们的流程,创建好协议对象后,我们现在要关心的是如何让服务器和协议相勾连起来。
这里的方法是使用lambda表达式 。

这段代码的核心是:
- 通过
std::make_unique创建一个TcpServer类型的std::unique_ptr智能指针(独占所有权); - 初始化
TcpServer时,传入两个参数:- 第一个参数:将命令行参数
argv[1]转为整数(服务器监听的端口号); - 第二个参数:一个 lambda 表达式(作为回调函数 ),用于处理客户端连接事件(当有客户端连入时,调用
protocal->GetRequest处理请求)。
- 第一个参数:将命令行参数
第二个构造参数:lambda 表达式
cpp
[&protocal](std::shared_ptr<Socket> &sock, InetAddr &client)
{
protocal->GetRequest(sock, client);
}
这是代码的核心逻辑,拆解如下:
| 部分 | 含义 |
|---|---|
[&protocal] |
捕获列表:按引用捕获外部变量 protocal ,使 lambda 内可以访问/调用 protocal 的成员函数; ⚠️ 注意:需保证 protocal 在 lambda 执行时仍有效(比如 protocal 是全局/类成员,或生命周期长于 TcpServer)。 |
(std::shared_ptr<Socket> &sock, InetAddr &client) |
lambda 参数列表: - std::shared_ptr<Socket> &sock:指向客户端套接字的共享智能指针(Socket 是自定义套接字类,封装 fd 等); - InetAddr &client:客户端的地址结构体(封装 IP、端口等); 参数用引用(&)避免拷贝,提升效率。 |
{ protocal->GetRequest(sock, client); } |
lambda 函数体:当 TcpServer 检测到新客户端连接时,会调用这个 lambda,进而调用 protocal 的 GetRequest 方法处理客户端请求(比如读取数据、解析协议)。 |
总的来讲就是创建了一个TcpServer类对象,第一个参数是端口号,第二个参数是服务器要执行的业务,也就是参数中的Lambda表达式。Lambda表达式 = _service
这里又使用function包装了一下。

Lambda表达式的函数体是GetRequest

Start函数创建client和sock,传递给Lambda表达式做参数,Lambda表达式又将参数传给GetRequest

当调用Start时,第47行,就会执行调用匿名函数也就是GetRequest,是一个回调的逻辑。

okok,我终于弄通了,现在重新捋一遍:

本质就是创建一个TcpServer对象,第一个参数端口号,第二个参数:匿名函数。
创建TcpServer对象时,调用TcpServer的构造:

走初始化列表:又去创建了一个监听套接字,而这里的监听套接字是一个子类。
主要是第二个参数:匿名函数。
参数sock和client是在24行Start函数创建的(这里也太阴了...)
Q:那不对啊 start函数是在TcpServer对象创建之后才调用的为什么在创建TcpServer的时候就能用start里创建的sock了?
emm...详细解释如下:
创建 TcpServer 时并没有"使用" Start() 里的 sock,只是"约定了如何使用",真正的使用发生在 Start() 调用之后。
创建 TcpServer 时传入的 lambda,本质是"预约一个规则":
- 此时 lambda 只是一个"待执行的逻辑模板",并没有真正执行,也没有访问任何
sock; - 只有当你调用
tsvr->Start()后,Start()循环里创建了sock,才会触发 lambda 执行,这时才真正把sock传给 lambda 使用。
我把 main 函数和 TcpServer 的执行步骤按时间顺序列出来,
| 执行步骤 | 代码行为 | 关键说明 |
|---|---|---|
| 1 | std::unique_ptr<Protocol> protocal = ... |
创建 protocal 对象(准备好业务逻辑) |
| 2 | std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(...) |
执行 TcpServer 构造函数: 1. 保存端口号 _port; 2. 创建监听套接字 _listensockptr 并绑定端口; 3. 把 lambda 保存为成员变量 _service(仅保存,不执行) ; ⚠️ 此时 lambda 里的 protocal->GetRequest(sock, client) 一行代码都没执行! |
| 3 | tsvr->Start() |
调用 Start() 函数,进入循环: 1. 调用 Accept() 创建 sock(客户端连接对象); 2. fork 子进程; 3. 子进程中执行 _service(sock, client) → 触发 lambda 执行 ; 4. 此时才把 Start() 里的 sock 传给 lambda,执行 protocal->GetRequest |
这个过程就像:
- 你去餐厅吃饭,进门时告诉服务员(创建
TcpServer):"等下上菜时,把菜(sock)端给我,我要按自己的方式吃(protocal->GetRequest)"------这是预约规则,此时菜还没做,你只是说了"怎么吃",并没有真的吃; - 等厨师做好菜(
Start()里创建sock),服务员才把菜端给你(_service(sock, client)),你才按之前说的方式吃(执行 lambda)。
创建 TcpServer 时的 lambda 就是"你告诉服务员的吃法",只是一个"约定",不是"执行";Start() 就是"厨师做菜+服务员端菜",是真正触发约定执行的环节。
总结
- 创建
TcpServer时,lambda 仅被保存 为_service,函数体(protocal->GetRequest)完全没执行,因此不需要sock存在; - 只有调用
tsvr->Start()后,Start()里创建了sock,才会调用_service(sock, client),此时 lambda 才执行,sock才有具体指向; - 核心逻辑:lambda 是"待执行的逻辑",不是"立即执行的代码",参数要等 lambda 被调用时才传入。
之前的困惑是误以为"创建 TcpServer 时就执行了 lambda,但实际上 lambda 是"延后执行"的,这也是回调函数的核心特点------定义逻辑,延后触发。
这里的回调太妙了,传一个lambda表达式,相当于把协议给注册到服务器里面!!! 当要执行服务器业务时,再返回去执行回调函数(协议中)。
2.3 demo v3 (最终版)
2.3.1 源码
main.cc
cpp
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include "NetCal.hpp"
#include "Daemon.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << "port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::cout << "服务器已经启动,已经是一个守护进程" << std::endl;
Daemon(0, 0);
Enable_File_Log_Strategy();
// 1. 顶层
std::unique_ptr<Cal> cal = std::make_unique<Cal>();
// 2. 协议层
std::unique_ptr<Protocol> protocal = std::make_unique<Protocol>([&cal](Request &req) -> Response
{ return cal->Execute(req); });
// 3. 服务器层
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),
[&protocal](std::shared_ptr<Socket> &sock, InetAddr &client)
{
protocal->GetRequest(sock, client);
});
tsvr->Start();
return 0;
}
protocol.hpp
cpp
#pragma once
#include <string>
#include <iostream>
#include <memory>
#include <jsoncpp/json/json.h>
#include <functional>
#include "Socket.hpp"
using namespace SocketModule;
// 约定好各个字段的含义,本质上就是约定好协议。
// client -> server
class Request
{
public:
Request()
{
}
Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
{
}
std::string Serialize()
{
std::string s;
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
s = writer.write(root);
return s;
}
bool Deserialize(std::string &in)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in, root);
if (ok)
{
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
}
return ok;
}
~Request()
{
}
int X() { return _x; }
int Y() { return _y; }
char Oper() { return _oper; }
private:
int _x;
int _y;
char _oper;
};
// server -> client
class Response
{
public:
Response()
{
}
Response(int result, int code) : _result(result), _code(code)
{
}
std::string Serialize()
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
return writer.write(root);
}
bool Deserialize(std::string &in)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in, root);
if (ok)
{
_result = root["result"].asInt();
_code = root["code"].asInt();
}
return ok;
}
void SetResult(int res)
{
_result = res;
}
void SetCode(int code)
{
_code = code;
}
void ShowResult()
{
std::cout << "计算结果是:" << _result << "[" << _code << "]" << std::endl;
}
~Response()
{
}
private:
int _result;
int _code; // 表示运算结果的退出码
};
// 协议(基于TCP)需要解决的两个问题:
// 1. request和response 必须得有序列化和反序列化功能
// 2. 读取的时候要读取到完整的请求
const std::string sep = "\r\n";
using func_t = std::function<Response(Request &req)>;
class Protocol
{
public:
Protocol()
{
}
Protocol(func_t func) : _func(func)
{
}
// 应用层封装报文(添加报头)
std::string Encode(const std::string &jsonstr)
{
std::string len = std::to_string(jsonstr.size());
return len + sep + jsonstr + sep;
}
// 1. 判读报文完整性
// 2. 如果包含至少一个完整请求,提取他,并从接收缓冲区中移除
bool DeCode(std::string &buffer, std::string *package)
{
ssize_t pos = buffer.find(sep);
if (pos == std::string::npos)
return false;
std::string package_len_str = buffer.substr(0, pos);
int package_len_int = std::stoi(package_len_str);
int target_len = package_len_str.size() + package_len_int + 2 * sep.size();
if (buffer.size() < target_len)
return false;
*package = buffer.substr(pos + sep.size(), package_len_int);
buffer.erase(0, target_len);
return true;
}
void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
std::string buffer_queue;
while (true)
{
int n = sock->Recv(&buffer_queue);
if (n > 0)
{
std::cout << "----------request_buffer------------" << std::endl;
std::cout << buffer_queue << std::endl;
std::cout << "------------------------------------" << std::endl;
// 1. 拿完整报文
std::string json_package;
// 2. 做反序列化
while (DeCode(buffer_queue, &json_package))
{
std::cout << "-----------request_json-------------" << std::endl;
std::cout << json_package << std::endl;
std::cout << "------------------------------------" << std::endl;
std::cout << "-----------request_buffer-----------" << std::endl;
std::cout << buffer_queue << std::endl;
std::cout << "------------------------------------" << std::endl;
Request req;
bool ok = req.Deserialize(json_package);
if (!ok)
continue;
// 3. 完成计算功能(通过req得到resp)
Response resp = _func(req);
// 4. 序列化
std::string json_str = resp.Serialize();
// 5. 加报头
std::string send_str = Encode(json_str);
// 6. 发送
sock->Send(send_str);
}
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "client:" << client.StringAddr() << "Quit!";
break;
}
else
{
LOG(LogLevel::WARNING) << "client:" << client.StringAddr() << ", recv error";
break;
}
}
sock->Close();
}
bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buff, Response *resp)
{
std::string resp_str;
while (true)
{
int n = client->Recv(&resp_str);
if (n > 0)
{
std::cout << "------------resp_buffer-------------" << std::endl;
std::cout << resp_buff << std::endl;
std::cout << "------------------------------------" << std::endl;
// 1. 拿完整报文
std::string json_package;
while (DeCode(resp_str, &json_package))
{
std::cout << "-----------response json------------" << std::endl;
std::cout << json_package << std::endl;
std::cout << "------------------------------------" << std::endl;
std::cout << "-----------response json------------" << std::endl;
std::cout << resp_buff << std::endl;
std::cout << "------------------------------------" << std::endl;
// 2. 做反序列化
resp->Deserialize(json_package);
}
return true;
}
else if (n == 0)
{
std::cout << "server quit " << std::endl;
return false;
}
else
{
std::cout << "recv error" << std::endl;
return false;
}
}
}
std::string BuildRequestString(int x, int y, char oper)
{
// 1. 构建请求
Request req(x, y, oper);
// 2. 序列化
std::string josn_req = req.Serialize();
// 2.1 debug
std::cout << "----------json_req string-----------" << std::endl;
std::cout << josn_req << std::endl;
std::cout << "------------------------------------" << std::endl;
// 3. 加报头
return Encode(josn_req);
}
~Protocol()
{
}
private:
// Request _req;
// Response _resp;
func_t _func;
};
tcserver.hpp
cpp
#include "Socket.hpp"
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>
using namespace SocketModule;
using namespace LogModule;
using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
class TcpServer
{
public:
TcpServer(uint16_t port, ioservice_t service) : _port(port),
_listensockptr(std::make_unique<TcpSocket>()),
_isrunning(false),
_service(service)
{
_listensockptr->BuildTcpSocketMethod(_port);
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
InetAddr client;
auto sock = _listensockptr->Accept(&client);
if (sock == nullptr)
{
continue;
}
LOG(LogLevel ::DEBUG) << "accept success..." << client.StringAddr();
pid_t id = fork();
if (id < 0)
{
LOG(LogLevel::FATAL) << "fork error";
exit(FORK_ERR);
}
else if (id == 0)
{
_listensockptr->Close();
if (fork() > 0)
exit(0);
_service(sock, client);
exit(OK);
}
else
{
sock->Close();
pid_t rid = ::waitpid(id, nullptr, 0);
(void)rid;
}
}
_isrunning = false;
}
~TcpServer()
{
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensockptr;
bool _isrunning;
ioservice_t _service;
};
tcpclient.cc
cpp
#include "Socket.hpp"
#include "Common.hpp"
#include "Protocol.hpp"
#include <iostream>
#include <string>
#include <memory>
using namespace SocketModule;
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}
void GetDataFromStdin(int *x, int *y, char *oper)
{
std::cout << "Please Enter x: ";
std::cin >> *x;
std::cout << "Please Enter y: ";
std::cin >> *y;
std::cout << "Please Enter oper: ";
std::cin >> *oper;
}
// ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();
client->BuildTcpClientSocketMethod();
if (client->Connect(server_ip, server_port) != 0)
{
std::cerr << "connect errot" << std::endl;
exit(CONNECT_ERR);
}
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();
std::string resp_buffer;
// 成功连接服务器
while (true)
{
int x, y;
char oper;
// 1. 获取数据
GetDataFromStdin(&x, &y, &oper);
// 2. 构建请求字符串
std::string req_str = protocol->BuildRequestString(x, y, oper);
std::cout << "-----------encode string------------" << std::endl;
std::cout << req_str << std::endl;
std::cout << "------------------------------------" << std::endl;
// 3. 发送请求
client->Send(req_str);
// 4. 获取应答
Response resp;
bool res = protocol->GetResponse(client, resp_buffer, &resp);
if (res == false)
break;
// 5. 显示结果
resp.ShowResult();
}
client->Close();
return 0;
}
socket.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
namespace SocketModule
{
using namespace LogModule;
const static int gbacklog = 16;
// 基类套接字
// 模板方法模式:大部分方法为纯虚方法
class Socket
{
public:
virtual ~Socket() {};
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t port) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
virtual void Close() = 0;
virtual int Recv(std::string *out) = 0;
virtual int Send(const std::string &message) = 0;
virtual int Connect(const std::string &server_ip, uint16_t port) = 0;
public:
void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
void BuildTcpClientSocketMethod()
{
SocketOrDie();
}
};
const static int defaultfd = -1;
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(defaultfd)
{
}
TcpSocket(int fd) : _sockfd(fd)
{
}
~TcpSocket()
{
}
void SocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel ::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel ::INFO) << "socket success";
}
void BindOrDie(uint16_t port) override
{
InetAddr localadd(port);
int n = ::bind(_sockfd, localadd.NetAddrPtr(), localadd.NetAddrLen());
if (n < 0)
{
LOG(LogLevel ::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel ::INFO) << "bind success";
}
void ListenOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel ::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel ::INFO) << "listen success";
}
std::shared_ptr<Socket> Accept(InetAddr *client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = ::accept(_sockfd, CONV(peer), &len);
if (fd < 0)
{
LOG(LogLevel ::WARNING) << "accept warning ...";
return nullptr;
}
client->SetAddr(peer);
return std::make_shared<TcpSocket>(fd);
}
int Recv(std::string *out) override
{
char buffer[1024];
ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0); // 文件描述符来源:accept创建的
if (n > 0)
{
buffer[n] = 0;
*out += buffer;
}
return n;
}
int Send(const std::string &message) override
{
return ::send(_sockfd, message.c_str(), message.size(), 0);
}
void Close() override
{
if (_sockfd >= 0)
::close(_sockfd);
}
int Connect(const std::string &server_ip, uint16_t port) override
{
InetAddr server(server_ip, port);
return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
}
private:
int _sockfd; // 有可能是listen套接字也有可能是普通的socket
};
};
2.3.2 流程详解
emm...终于到最后一遍了。
宏观上来看,最终版的demo中已经实现了分层设计。不着急我们先从看一下流程。
服务端:

- 格式化调用服务端。
- 守护进程化。
- 打开日志(向文件中写)。
- 创建计算模块的对象。
- 创建协议对象。
- 创建服务器。
- 调用服务器。
客户端:

- 格式化调用客户端。
- 创建客户端套接字。
- 建立连接。
- 创建协议对象。
- 创建应答缓冲区。
- 从键盘获取要计算的参数。
- 构建请求字符串。
- 发送请求。
- 创建应答对象。
- 获取应答。
- 显示结果。
- 关闭客户端。
对于服务器来说:
创建计算对象,里面就是一个计算逻辑,没啥可讲的。

创建协议的时候,要绑定匿名函数,指定服务器要执行的逻辑。
捕捉计算对象,传入请求参数,返回计算结果(相应对象)。
对于请求对象:主要实现将请求进行序列化和反序列化。这里就用了Json。

创建服务器。完善了获取请求的逻辑。

首先创建一个请求缓冲区,调用服务器套接字封装的接收请求的方法,将请求放入请求缓冲区中。

recv函数参数意义:从哪获取,获取到哪,获取的大小,接收标志位(默认为0就行)。
序列化与反序列化遇到的问题 因为TCP是面向字节流的,当读取方在读取的时候可能读到一个完整的Json请求,也可能会读到半个。
read本身并不保证读取到的报文的完整性,read只能保证把数据读上来。
由应用层的程序员保证读取数据的完整性。也就是要完善协议(添加报头)。
当我们tcp中读取数据的时候,读取到的报文不完整,或者读多了,导致下一个报文不完整,这样的问题叫做"粘报"问题。
这里获取的请求可能是很多条,或者半条...反正先都放在缓冲区中。通过out带出。回到GetRequest,就是放在了buffer_queue中。
创建一个json_package串,将来放一条完整的报文信息。

先做一个Decode,作用是:获取一条问政的请求,并且去除报文头。通过package带出。也就是json_package。

先对照的看一眼封装报文头的逻辑:

每个报文由,请求长度+标志位+请求体本身+标志位组成。

成功获取到一条报文信息后,做反序列化,再调用回调函数。服务器的协议层已经绑定。
以加法为例:

传入一条请求,实例化一个相应。匹配'+',设置结果和返回码。

然后再对计算好的相应序列化,加报头,最后发送。这里都是封装好的,体现面向对象。
以上就是服务器的执行逻辑。客户端不再赘述。
有任何疑问欢迎给作者留言。: )
完