Linux 自定义协议实现网络计算器
链路层中有驱动程序,应用层属于上层业务逻辑,之前我们所写的代码都是基于操作系统给我们提供的系统调用完成的。我们进行数据的发送时,都是一个个字符串,在不同场景下有不同的涵义。

我们写的TCP数据读取的代码:读取字符串,实际上是有bug的。
char buffer[1024];while (true){// 1. 先读取数据// a. n>0: 读取成功// b. n<0: 读取失败// c. n==0: 对端把链接关闭了,读到了文件的结尾 --- pipessize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
}
...一.自定义协议
1.重谈应用层
我们程序员写的一个个解决实际问题,满足日常需求的网络程序,都在应用层。协议就是双方约定好的结构化数据,一个简单的例子:在网络中通信,比如发送消息时,包含的信息有:

将三个字符串打包形成一个长字符串,将这个长字符串发送到网络中。而接收方读取了这个长字符串,按某种协议将长字符串再进行拆分。我们形成一个长字符串的原因:保证报文完整性,方便网络传输。再合并这些短字符串之前,它们实际上是一个结构体。

上面的字符串转换,就叫做序列化和反序列化。
2.序列化与反序列化
现在给出一个需求:实现网络版的计算器。
实现方案有两种:
约定⽅案⼀:
• 客⼾端发送⼀个形如"1+2"的字符串;
• 这个字符串中有两个操作数, 都是整形;
• 两个数字之间会有⼀个字符是运算符, 运算符只能是 + ;
• 数字和运算符之间没有空格; • ... 约定⽅案⼆:
• 定义结构体来表⽰我们需要交互的信息;
• 发送数据时将这个结构体按照⼀个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;• 这个过程叫做 "序列化" 和 "反序列化"
序列化与反序列化在网络程序中的应用:将一系列结构化数据转为字符串,然后再将字符串还原成这种结构化数据,序列化的目的在于数据的完整性传输。

现在有问题:
能直接把结构体按二进制发送吗?
可以,但是不建议!不论是从平台方面(结构体内存对齐)还是语言迁移(C++,Java,python)上都会有问题。
那为什么,序列化和反序列化是一种可行的方案?
因为这种方法对软件进行了解耦,不关心发送的底层是什么,因为通过序列化转化成了字符串,接收方也同理。
如果我们进行网络协议式的通信,在应用层建议使用序列和反序列方案。而传送结构体的方案,除非场景特殊否则不建议
3.从网络角度理解Socket接口
当我们之前写Tcp和Udp的服务端与客户端时,有没有考虑过一个问题:
我们调用接口read/recvfrom和write/sendto时,是立刻把数据从网络中读/写的吗?

不是!只是写到了TCP对应的fd对应的文件缓冲区!这些函数的本质都是拷贝函数!
那么,数据什么时候发,发多少,出错了怎么办,由发送方操作系统和TCP协议自主决定。所以TCP叫做传输控制协议。
操作系统把数据发送到网络中,本质也是拷贝!
read读数据,其实在监测接收缓冲区是否有数据,如果没有就会阻塞;如果有数据,就会拷贝到内核中。
主机间通信的本质,时把发送方的发送缓冲区内的数据,拷贝到接收方的接收缓冲区中。
问题:在TCP中,一个完整的序列不一定会一次发送,假设接收方接收到的不是一个完整的序列,就不会进行反序列化。这个问题叫做TCP的粘包问题。
TCP面向字节流,发送方发送了多少不一定和接收方对等!
而对于UDP就不会出现这个问题,UDP是面向数据报的,一定是整个序列发送的。
所以不难想象,之前写的TCP服务器的接收read是存在bug的。读取时我们期望读sizeof的长度,实际上不一定会读到。
tips:
为什么TCP通信是全双工?因为有两对接收和发送缓冲区。
操作系统往接收缓冲区写,用户调用read读,这就是一个内核和用户之间的生产者消费者模型。read阻塞的原因,就是同步机制。
为了解决上面TCP的粘包问题,就需要定义协议来提取完整的数据报。
4.自定义协议的要求
我们要实现网络计算器,就需要有协议,对协议的要求如下:
1.有结构化的字段,提供好序列化和反序列化方案
2.解决因为字节流问题导致读取报文不完整的问题
实现网络计算器:封装套接字socket(模板方法模式),定制协议,守护进程化
二.实现网络计算器
1.socket封装
这次我们对socket封装,使用了模板方法模式。
基类socket中不仅定义了虚函数,而且定义了调用这些函数的方法。
#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"namespace SocketModule
{using namespace LogModule;const static int gbacklog = 16;// 首先编写基类Socketclass Socket{public:~Socket() {}// 基类主要提供虚方法,用于子类继承// 下面是所有Socket类都需要实现的虚方法virtual void SocketOrDie() = 0;virtual void BindOrDie(uint16_t port) = 0;virtual void ListenOrDie() = 0;// Server需要的接口:acceptvirtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;// Client需要的接口:connectvirtual int Connect(const std::string &server_ip, uint16_t port) = 0;// 发送数据接口virtual int Send(const std::string &message) = 0;virtual int Recv(std::string *out) = 0;// 关闭连接接口virtual void Close() = 0;public:// 为了跟方便创建Server和Client对象,创建两个函数调用各自的接口void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog){// Server的初始化工作SocketOrDie();BindOrDie(port);ListenOrDie();}void BuileTcpSocketClientMethod(){// Client的初始化工作SocketOrDie();}};}当我们创建子类TcpSocket时需要继承socket,在创建tcpsocket对象时调用这个方法即可。
int defaultfd = -1;class TcpSocket : public Socket{public:TcpSocket() : _sockfd(defaultfd){}TcpSocket(int fd) : _sockfd(fd){}~TcpSocket() {}void SocketOrDie() override{// 创建socket_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO) << "create socket success";}void BindOrDie(uint16_t port) override{InetAddr localaddr(port);int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void ListenOrDie() override{int n = ::listen(_sockfd, gbacklog);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);// peer是一个输出型参数,用来接收客户端的地址信息int fd = ::accept(_sockfd, CONV(peer), &len);if (fd < 0){LOG(LogLevel::WARNING) << "accept warning ...";return nullptr; // TODO}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);// 当接收数据成功,我们把数据追加到缓冲区bufferif (n > 0){buffer[n] = 0;*out += buffer;}return n;}int Send(const std::string &message) override{return send(_sockfd, message.c_str(), message.length(), 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;};在main.cc中创建Tcp服务器的流程如下,是不是变得非常简单呢?
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),[&protocol](std::shared_ptr<Socket> &sock, InetAddr &client){protocol->GetRequest(sock, client);
});
tsvr->Start();相对的,如果要创建Udp的socket,只要在基类中编写一个调用Udp相关接口的函数即可。
2.TCP服务器
我们再创建一个文件TcpServer.cc,包含上面的socket头文件,编写TCP的start方法。主要就是循环accept客户端的连接,并通过创建子孙进程执行service方法。
#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;// 服务器应该是一个长期运行的进程// 上面完成了对server的初始化,下面开始具体的连接和业务回调while (_isrunning){InetAddr client;// 获取客户端连接,拿到用于处理业务的socketauto sock = _listensockptr->Accept(&client);if (sock == nullptr){continue;}LOG(LogLevel::DEBUG) << "accept success ...";// 创建子进程进行业务回调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(OK);// 孙子进程执行任务,已经是孤儿_service(sock, client);// 孙子进程执行完直接释放资源sock->Close();exit(OK);}else{// 父进程等待子进程,并关闭套接字sock->Close();pid_t rid = ::waitpid(id, nullptr, 0);(void)rid;}}_isrunning = false;}~TcpServer() {}private:uint16_t _port;std::unique_ptr<TcpSocket> _listensockptr;bool _isrunning;ioservice_t _service;
};
而具体的service方法,需要结合之前提到的自定义协议进行收发数据的序列与反序化,执行网络服务器方法。
3.自定义协议
目标:实现自定义协议的网络计算器

1.json库实现序列化与反序列化
在这里我们使用json库来辅助完成序列化与反序列化
Json库的特性:
1. 简单易⽤:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。 2. ⾼性能:Jsoncpp 的性能经过优化,能够⾼效地处理⼤量 JSON 数据。 3. 全⾯⽀持:⽀持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。4. 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,⽅便开发者调试。
序列化使用实例:将对象转化为Json格式的字符串
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";std::string s = root.toStyledString();std::cout << s << std::endl;return 0;
}$./test.exe{"name" : "joe","sex" : "男"}反序列化使用实例:反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{// JSON 字符串std::string json_string = "{\"name\":\"张三\", \"age\":30,
\"city\":\"北京\"}";// 解析 JSON 字符串Json::Reader reader;Json::Value root;// 从字符串中读取 JSON 数据bool parsingSuccessful = reader.parse(json_string, root);if (!parsingSuccessful){// 解析失败,输出错误信息std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return 1;} // 访问 JSON 数据std::string name = root["name"].asString();int age = root["age"].asInt();std::string city = root["city"].asString();// 输出结果std::cout << "Name: " << name << std::endl;std::cout << "Age: " << age << std::endl;std::cout << "City: " << city << std::endl;return 0;
}$
./test.exe
Name: 张三
Age: 30
City: 北京接着我们开始写序列化与反序列化的完整代码。
首先我们要知道,当server接收数据,或者client发送数据时,它们都应该:

2.保证数据完整
问题:因为TCP面向字节流,所以当读取方在读取的时候,可能读到一个完整的json请求,也可能读到不完整的。
read不保证读取完整性,需要用户自己完成。
在应用层添加一个长度报头,让接收方读取对应长度即可。因此为了让读取方读取到一个完整长度,我们需要设计一个Encode方法:把json串加上长度和分隔符——这不就是协议报头吗!
std::string Encode(const std::string &jsonstr) {std::string len = std::to_string(jsonstr.size()); // 计算JSON长度return len + sep + jsonstr + sep; // 格式: "长度\r\nJSON数据\r\n"
}同样地,我们也需要设计一个Decode方法:在写Recv方法时,我们故意把接收到的数据全追加写在一个缓冲区buffer中,因此我们需要从buffer中分割出一个完整的json,然后将这个json对象从buffer中删去。
bool Decode(std::string &buffer, std::string *package) {// 1. 查找第一个分隔符,获取长度字段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);// 2. 计算完整报文长度并检查是否完整int target_len = package_len_str.size() + package_len_int + 2 * sep.size();if (buffer.size() < target_len) return false;// 3. 提取JSON数据并清理缓冲区*package = buffer.substr(pos + sep.size(), package_len_int);buffer.erase(0, target_len); // 移除已处理的数据return true;
}3.序列化与反序列化
Request 类 - 客户端请求
class Request {int _x; // 操作数1int _y; // 操作数2 char _oper; // 运算符 (+, -, *, /, %)
};序列化 (Serialize):
std::string Serialize() {Json::Value root;root["x"] = _x; // 整数直接存储root["y"] = _y;root["oper"] = _oper; // char 被转换为整数存储Json::FastWriter writer;return writer.write(root); // 返回JSON字符串
}反序列化 (Deserialize):
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(); // 注意:这里从整数还原为char}return ok;
}Response 类 - 服务端响应
class Response {int _result; // 计算结果int _code; // 状态码 (0:成功, 1-4:错误码)
};序列化/反序列化逻辑与Request类似,使用JSON格式传输结果和状态码。
协议工作流程
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::string json_package;// 步骤1: 解析协议,提取完整JSON请求if (!Decode(buffer_queue, &json_package)) continue;// 步骤2: 反序列化JSON到Request对象Request req;if (!req.Deserialize(json_package)) continue;// 步骤3: 执行业务逻辑计算Response resp = _func(req); // 调用计算函数// 步骤4-6: 序列化响应并发送std::string json_str = resp.Serialize();std::string send_str = Encode(json_str);sock->Send(send_str);}else if (n == 0) {// 客户端断开连接break;}else {// 接收错误break;}}
}关键组件交互可见下图:

对于OSI七层模型而言,为什么上面的应用层,表示层,会话层三层,无法被设计到内核中?什么时候获取连接,接收请求内核是无法预知的,也就是说主要是收到用户需求的影响。
4.网络计算器NetCal
完整逻辑如下,很简单,不再赘述
#pragma once#include "Protocol.hpp"
#include <iostream>class Cal
{
public:Response Execute(Request &req){Response resp(0, 0); // code: 0表示成功switch (req.Oper()){case '+':resp.SetResult(req.X() + req.Y());break;case '-':resp.SetResult(req.X() - req.Y());break;case '*':resp.SetResult(req.X() * req.Y());break;case '/':{if (req.Y() == 0){resp.SetCode(1); // 1除零错误}else{resp.SetResult(req.X() / req.Y());}}break;case '%':{if (req.Y() == 0){resp.SetCode(2); // 2 mod 0 错误}else{resp.SetResult(req.X() % req.Y());}}break;default:resp.SetCode(3); // 非法操作break;}return resp;}
};5.main.cc执行方法
编写一个完整的Tcp服务,需要用户做三层事情
最底层就是由我们手动实现socket的初始化以及服务端和客户端的连接
中间层就是我们需要实现网络通信的协议,比如说这里的自定义协议,我们需要实现自定义协议的解析和序列化
最上层就是我们需要实现一个完整的业务逻辑,比如说这里的网络计算器NetCal等等
#include "NetCal.hpp"
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include <memory>void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}// 我的代码为什么要这样写???
// ./tcpserver 8080
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERR);}// 1. 顶层std::unique_ptr<Cal> cal = std::make_unique<Cal>();// 2. 协议层std::unique_ptr<Protocol> protocol = 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]),[&protocol](std::shared_ptr<Socket> &sock, InetAddr &client){protocol->GetRequest(sock, client);});tsvr->Start();return 0;
}完整的从客户端读取到数据,传输给服务端然后返回结果的流程:

