Linux网络的应用层自定义协议
目录
1、应用层协议
2、NetCal
2.1 大致思路
2.2 Socket.hpp
2.3 Protocol.hpp
2.3.1 Jsoncpp
2.3.2 代码
2.4 Tcpserver.hpp
2.5 TcpServer.cc
2.6 TcpClient.cc
2.7 守护进程化
2.7.1 前台进程与后台进程
2.7.2 进程组&&会话&&终端
2.7.3 守护进程
2.8 示例及完整代码
1、应用层协议
- 前面,我们写了一些Socket编程,都是按"字符串"进行传输。但是,当我们想传输一些"结构化的数据"怎么办?
- 可以将结构化数据 序列化为字节序列进行传输,后面再反序列化。如:
- 文本格式(如Json)序列化后,是人类可读的字符串,再转成字节(每个字符对应 ASCII/UTF-8 编码的字节)。
- 二进制格式(如Protobuf)序列化后,是人类不可读但更紧凑的字节序列。
- 之前,我们说,协议是一种约定,方便快速形成共识,减少通信成本。
- 现在,一方对一个结构序列化并发送和另一方收到并反序列化为同一个结构,这种约定,就是应用层协议。
注意:
- 不序列化,直接传结构化数据的内存字节不行吗?可以,但是不推荐,因为不兼容。
- 为什么要说字节呢?不是二进制吗?因为字节是计算机硬件和网络协议能直接处理的 “最小实际单元”。
2、NetCal
2.1 大致思路
- 实现一个NetCal(网络计算器),TcpClient给TcpServer发结构化的数据(操作数+运算符),TcpServer给TcpClient回结构化的数据(执行结果)。
- 以TCP为例,TCP比UDP复杂一点,UDP类似。
- write/send,将数据拷贝到TCP的发送缓存区,由TCP决定怎么发送;对于网络中的数据,由TCP决定怎么接收,再read/recv,拷贝TCP的接收缓冲区的数据。
- 主机之间的通信,本质是:把发送方的发送缓冲区的数据拷贝到对端的接收缓冲区。
- 对于缓冲区,则是为内核和用户之间的生产者和消费者模型。
- 因为TCP有独立的发送和接收缓冲区,所以是全双工(双方能同时接收和发送消息),tcpsockfd可以边读边写。
2.2 Socket.hpp
- 对socket相关的接口进行封装。
- 多态:统一接口(父类),屏蔽实现差异(子类);便于扩展新协议(子类继承父类,进行具体实现。“开闭原则”:对扩展开放(能加 UdpSocket),对修改关闭(不用改已有代码))。
- 像TcpScoket,这样的代码,这个类可以用于listen_sockfd,也可以是sockfd,但是,对于其中一个,也用不全,里面的函数,会不会冗余?函数不占空间? 逻辑上存在 “用不上的函数”,但物理上(内存层面)几乎没有额外开销,工程中 “代码复用优先于极致精简” 。
#pragma once#include "Common.hpp"
#include "Log.hpp"
#include <memory>using namespace LogModule;const static int default_sockfd = -1;
const static int default_backlog = 16;class Socket
{
public:virtual void SocketOrDie() = 0; // = 0, 不需要实现 virtual void BindOrDie(uint16_t port) = 0;virtual void ListenOrDie(int backlog) = 0;virtual void ConnectOrDie(std::string &server_ip, uint16_t server_port) = 0;virtual std::shared_ptr<Socket> Accept(InetAddr* client) = 0;virtual int Recv(std::string* out) = 0;virtual int Send(const std::string& in) = 0;virtual void Close() = 0;
public:void BuildTcpServer(uint16_t port){SocketOrDie();BindOrDie(port);ListenOrDie(default_backlog);}void BuildTcpClient(std::string &server_ip, uint16_t server_port){SocketOrDie();ConnectOrDie(server_ip,server_port);}
};class TcpSocket : public Socket
{
public:TcpSocket(int sockfd = default_sockfd): _sockfd(sockfd){}virtual void Close() override{if (_sockfd != default_sockfd)::close(_sockfd); // ::表示调用 全局作用域 中的 close 函数}virtual void SocketOrDie() override{_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket error!";exit(SOCKET_ERROR);}LOG(LogLevel::INFO) << "socket success, socket: " << _sockfd;}virtual void BindOrDie(uint16_t port) override{InetAddr local(port);int n = ::bind(_sockfd, CONST_CONV(local.Addr()), local.AddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind error!";exit(BIND_ERROR);}LOG(LogLevel::INFO) << "bind success, socket: " << _sockfd;}virtual void ListenOrDie(int backlog) override{int n = ::listen(_sockfd, default_backlog);if (n < 0){LOG(LogLevel::FATAL) << "listen error!";exit(LISTEN_ERROR);}LOG(LogLevel::INFO) << "listen success, sockfd: " << _sockfd;}virtual void ConnectOrDie(std::string &server_ip, uint16_t server_port) override{InetAddr server(server_ip, server_port);int n = ::connect(_sockfd, CONST_CONV(server.Addr()), server.AddrLen());if (n < 0){LOG(LogLevel::FATAL) << "connect error!";exit(CONNECT_ERROR);}LOG(LogLevel::INFO) << "connect success, sockfd: " << _sockfd;}virtual std::shared_ptr<Socket> Accept(InetAddr* client) override{struct sockaddr_in addr;socklen_t len = sizeof(addr);int fd = ::accept(_sockfd, CONV(addr), &len);if (fd < 0){LOG(LogLevel::WARNING) << "accept failed";return nullptr;}client->SetAddr(addr);LOG(LogLevel::INFO) << "accept success, client: " << client->StringAddr();return std::make_shared<TcpSocket>(fd); // 这个server的sockfd就可以调用Recv和Send方法。}virtual int Recv(std::string* out) override{char buf[1024];ssize_t n = ::recv(_sockfd,buf,sizeof(buf)-1,0);if(n > 0){buf[n] = 0;*out += buf; // += 可能要不断的读 }return n;}virtual int Send(const std::string& in) override{return ::send(_sockfd,in.c_str(),in.size(),0);}private:int _sockfd; // 既可以是listen_sockfd,也可以是sockfd,复用代码。
};
2.3 Protocol.hpp
2.3.1 Jsoncpp
- Jsoncpp是一个用于处理JSON数据的C++库。它提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能。Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。
- 特性:简单;高性能;支持JSON标准中的所有数据类型,包括对象、数组、字符串、数 字、布尔值和null;方便错误处理(日志清晰)。
- 安装: ubuntu: sudo apt install -y libjsoncpp-dev。
- 使用实例:
- <jsoncpp/json/json.h> 的路径设计是 Jsoncpp 为了避免头文件命名冲突、保持内部结构清晰、遵循行业惯例而采用的方案。它通过 “目录前缀” 的方式,让编译器和开发者都能明确区分这是 Jsoncpp 库的核心头文件,同时兼容库的安装和引用逻辑。
#include <iostream>
#include <json/json.h>
#include <string>int main() {// -------------------------- 序列化(生成字符串) --------------------------Json::Value root;root["name"] = "David";root["score"] = 95.5;root["passed"] = true;Json::StreamWriterBuilder writer_builder;std::string json_str = Json::writeString(writer_builder, root); // 简洁写法std::cout << "序列化结果:\n" << json_str << std::endl;// json_str
//{
// "name": "David",
// "passed": true,
// "score": 95.5
//}// -------------------------- 反序列化(解析字符串) --------------------------Json::Value parsed_root;Json::CharReaderBuilder reader_builder;std::string err_msg; // 用于存储解析错误信息// 创建CharReader并解析字符串std::unique_ptr<Json::CharReader> reader(reader_builder.newCharReader());bool success = reader->parse(json_str.c_str(), // 字符串起始地址json_str.c_str() + json_str.length(), // 字符串结束地址&parsed_root, // 解析结果存储到parsed_root&err_msg // 错误信息存储到err_msg);if (success) {std::cout << "\n反序列化结果:" << std::endl;std::cout << "姓名:" << parsed_root["name"].asString() << std::endl;std::cout << "分数:" << parsed_root["score"].asDouble() << std::endl;} else {std::cerr << "\n解析失败!错误信息:" << err_msg << std::endl;}return 0;
}
2.3.2 代码
- TCP是面向字节流的,双方的收发次数可能不一致,要收到完整的报文,需要对json串加一些标记(最主要的是在前面加上json串的长度)。
- UDP是面向数据报的,双方收发次数一致,收到的报文是完整的。
- 这里先处理读取方面,先弱化写的方面,后面再讲解。
#pragma once#include "Socket.hpp"
#include "Common.hpp"
#include <jsoncpp/json/json.h>
#include <functional>
#include <memory>class Request
{
public:Request(){}Request(int x, int y, char oper): _x(x), _y(y), _oper(oper){}std::string Serialize(){// -------------------------- 序列化(生成字符串) --------------------------Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::StreamWriterBuilder writer_builder;std::string json_str = Json::writeString(writer_builder, root); // 简洁写法return json_str;}bool Deserialize(std::string &json_str){// -------------------------- 反序列化(解析字符串) --------------------------Json::Value parsed_root;Json::CharReaderBuilder reader_builder;std::string err_msg; // 用于存储解析错误信息// 创建CharReader并解析字符串std::unique_ptr<Json::CharReader> reader(reader_builder.newCharReader());bool success = reader->parse(json_str.c_str(), // 字符串起始地址json_str.c_str() + json_str.length(), // 字符串结束地址&parsed_root, // 解析结果存储到parsed_root&err_msg // 错误信息存储到err_msg);if (success){_x = parsed_root["x"].asInt();_y = parsed_root["y"].asInt();_oper = parsed_root["oper"].asInt(); // char通过ASCII映射为整数}else{LOG(LogLevel::ERROR) << "\n解析失败! 错误信息: " << err_msg;}return success;}int GetX(){return _x;}int GetY(){return _y;}char GetOper(){return _oper;}private:int _x;int _y;char _oper;
};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::StreamWriterBuilder writer_builder;std::string json_str = Json::writeString(writer_builder, root); // 简洁写法return json_str;}bool Deserialize(std::string &json_str){// -------------------------- 反序列化(解析字符串) --------------------------Json::Value parsed_root;Json::CharReaderBuilder reader_builder;std::string err_msg; // 用于存储解析错误信息// 创建CharReader并解析字符串std::unique_ptr<Json::CharReader> reader(reader_builder.newCharReader());bool success = reader->parse(json_str.c_str(), // 字符串起始地址json_str.c_str() + json_str.length(), // 字符串结束地址&parsed_root, // 解析结果存储到parsed_root&err_msg // 错误信息存储到err_msg);if (success){_result = parsed_root["result"].asInt();_code = parsed_root["code"].asInt();}else{LOG(LogLevel::ERROR) << "\n解析失败! 错误信息: " << err_msg;}return success;}void ShowResult(){std::cout << "result is: " << _result << '[' << _code << ']' << std::endl;}void SetResult(int result){_result = result;}void SetCode(int code){_code = code;}private:int _result;int _code; // 0为正确,非0为各种错误
};const static 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 &json_str){// TCP是面向字节流的,要读到一个完整的json串,需要做些标记// 这里使用格式:json串长度+\r\n+json串+\r\n,即"json串长度\r\njson串\r\n"int len = json_str.size();return std::to_string(len) + sep + json_str + sep;}bool Decode(std::string &buffer, std::string *package){int pos = buffer.find("\r\n");if (pos == std::string::npos)return false;int str_len = std::stoi(buffer.substr(0, pos));int json_str_len = pos + sep.size() + str_len + sep.size();if (buffer.size() < json_str_len)return false;*package = buffer.substr(pos + sep.size(), str_len);buffer.erase(0, json_str_len);return true;}void GetRequest(std::shared_ptr<Socket> &sockfd, const InetAddr &client){std::string buffer;while (true){int n = sockfd->Recv(&buffer);if (n > 0){std::string json_package;// 1. 解析报文,不完整,继续读(所以在Recv里面是+=)while (Decode(buffer, &json_package)){// 2. 反序列化Request req;bool OK = req.Deserialize(json_package);if (!OK)continue; // 跳过当前无效的报文,继续处理后续可能有效的报文,防止buffer累积// 3. 执行函数Response resp = _func(req);// 4. 序列化std::string json_str = resp.Serialize();// 5. Encode,加报头std::string send_str = Encode(json_str);// 6. 发送sockfd->Send(send_str);}}else if (n == 0){LOG(LogLevel::INFO) << client.StringAddr() << " 退出了!";sockfd->Close();break;}else // n < 0{LOG(LogLevel::WARNING) << client.StringAddr() << " 异常";sockfd->Close();break;}}}bool GetResponse(std::shared_ptr<Socket> &client, Response *resp){std::string buffer;while (true){int n = client->Recv(&buffer);if (n > 0){std::string json_package;// 1. 解析报文,不完整,继续读(所以在Recv里面是+=)if (Decode(buffer, &json_package)){// 2. 反序列化return resp->Deserialize(json_package); // 只读取一次完整的报文}}else if (n == 0){std::cout << "server quit" << std::endl;return false;}else // n < 0{std::cout << "server error" << std::endl;return false;}}}std::string BuildRequestString(int x, int y, char oper){Request req(x, y, oper);// 1. 序列化std::string json_str = req.Serialize();// 2. Encode.加报头return Encode(json_str);}private:func_t _func;
};
2.4 Tcpserver.hpp
#pragma once#include "Socket.hpp"
#include <functional>
#include <sys/wait.h>using ioservice_t = std::function<void(std::shared_ptr<Socket>&, const InetAddr& )>;class TcpServer : public NoCopy
{
public:TcpServer(uint16_t port,ioservice_t service): _listen_socket(std::make_unique<TcpSocket>()), _running(false),_service(service){ // 1. 创建套接字// 2. bind套接字// 3. 设置监听套接字_listen_socket->BuildTcpServer(port);}// version-多进程void Start(){_running = true;while (_running){// 4. 创建已连接套接字InetAddr client;std::shared_ptr<Socket> sockfd = _listen_socket->Accept(&client);if(!sockfd)continue;pid_t pid = fork();if(pid < 0){LOG(LogLevel::WARNING) << "fork failed";continue;}else if(pid == 0){_listen_socket->Close(); // 关闭listen_sockfd// 子进程if(fork() > 0)exit(OK);// 孙子进程_service(sockfd,client);exit(OK);}else{sockfd->Close(); // 关闭sockfd// 父进程waitpid(pid,nullptr,0);}}}private:std::unique_ptr<Socket> _listen_socket;bool _running;ioservice_t _service;
};
2.5 TcpServer.cc
- 在 TCP/IP 五层协议中,OSI 七层协议的应用层、表示层、会话层,全部由应用程序自行实现,内核(操作系统)不介入 —— 因为用户需求(如数据格式、会话逻辑)千变万化,内核无法预先定义通用的应用层,表示层、会话层逻辑。
- 但是OSI 七层协议 非常好,因为在应用层设计的时候,这样分层,逻辑清晰,便于理解和模块化设。
#include "NetCal.hpp"
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include "Daemon.hpp"
#include <memory>// ./tcpserver server_port
int main(int argc, char* argv[])
{if(argc != 2){std::cerr << "Usage: " << argv[0] << " server_port" << std::endl;exit(USAGE_ERROR);}std::cout << "服务器已经启动,已经是一个守护进程了" << std::endl;Daemon(0,0); // daemon(0,0);// Enable_Console_Log_Strategy();Enable_File_Log_Strategy(); // 向文件里打印日志uint16_t server_port = std::stoi(argv[1]);// 应用层 具体功能的执行std::unique_ptr<NetCal> net_cal = std::make_unique<NetCal>();// 表示层 数据格式的转化std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&net_cal](Request& req){return net_cal->Execute(req);});// 会话层 通信连接的管理std::unique_ptr<TcpServer> tcp_server = std::make_unique<TcpServer>(server_port,[&protocol](std::shared_ptr<Socket>& sockfd, const InetAddr &client){protocol->GetRequest(sockfd,client);});tcp_server->Start();return 0;
}
2.6 TcpClient.cc
#include "Socket.hpp"
#include "Common.hpp"
#include "Protocol.hpp"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){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;exit(USAGE_ERROR);}Enable_Console_Log_Strategy();std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);// 1. 创建套接字// 2. 设置连接套接字std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();client->BuildTcpClient(server_ip,server_port);auto protocol = std::make_unique<Protocol>();while (true){int x, y;char oper;GetDataFromStdin(&x, &y, &oper);std::string request_string = protocol->BuildRequestString(x,y,oper);client->Send(request_string);Response resp;bool OK = protocol->GetResponse(client,&resp);if(!OK)break;resp.ShowResult();}client->Close();return 0;
}
2.7 守护进程化
2.7.1 前台进程与后台进程
- 路径/可执行程序 是 前台进程,从标准输入(键盘)中获取内容。只能有一个,因为标准输入的内容要给到一个确定的进程。
- 路径/可执行程序 & 是 后台进程,无法从标准输入(键盘)中获取内容,可有多个。
- 前台进程和后台进程,都可以向标准输出(显示器)打印内容。
- 一个现象,运行前台程序,父进程创建子进程,Ctrl+C父进程终止,父进程退出,子进程被一号进程“领养”,变成后台进程,Ctrl+C无法杀死子进程。
- jobs。查看所有后台进程。
- fg 任务号。指定进程,提到前台。
- Ctrl+Z。暂停前台进程,提到后台。
- bg 任务号。让后台进程恢复运行。
2.7.2 进程组&&会话&&终端
- 前面的任务,也称为作业。
- PGID,即进程组id,为组长的pid,即使组长退了,也不变。作业以进程组(一个或多个进程)的形式运行。
- SID,即session(会话) id。进程组属于某一个会话。
- TTY,即终端。终端是会话的 “载体” 和 “控制界面”。
2.7.3 守护进程
- 守护进程也称精灵进程。
- 当关闭终端时,在该会话里的进程可能会受影响(如:还可能向该终端打印等),守护进程就是让进程完全脱离终端、在后台稳定运行。
#pragma once#include "Log.hpp"
#include "Common.hpp"
#include <string>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>using namespace LogModule;const static std::string dev = "/dev/null";void Daemon(int nochdir, int noclose)
{// 1. 设置信号signal(SIGPIPE, SIG_IGN);signal(SIGCLD, SIG_IGN);// 2. 孤儿进程if (fork())exit(0);// 3. 创建新会话setsid();// 4. 更改目录if (nochdir == 0)chdir("/");// 5. 重定向标准输入,标准输出,标准错误if (noclose == 0){int fd = open(dev.c_str(), O_RDWR); // 以读写的方式打开if (fd < 0){LOG(LogLevel::FATAL) << "open " << dev << " error";exit(OPEN_ERROR);}else{dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}}
}
- SIGPIPE:当一个进程向已经关闭的管道(pipe)或网络连接(如 TCP socket)写入数据时,默认处理方式是终止进程。守护进程通常作为服务端长期运行,可能会遇到 “客户端意外断开连接后,服务端仍尝试向其写数据” 的情况(比如客户端崩溃、网络中断)。此时若不处理 SIGPIPE,守护进程会被直接杀死,导致服务中断。signal(SIGPIPE, SIG_IGN) 确保服务端不会因 “向已关闭连接写数据” 而被终止。
- SIGCHLD 信号的触发场景:当子进程退出时,操作系统会向其父进程发送 SIGCHLD 信号,通知父进程 “子进程已终止”,默认处理方式是忽略信号(什么都不做)。守护进程可能会创建子进程处理任务,如果不处理 SIGCHLD,大量子进程退出后会积累僵尸进程,耗尽系统的进程 ID 资源(PID 是有限的),最终导致无法创建新进程。signal(SIGCHLD, SIG_IGN),自动回收退出的子进程。
- 父进程创建子进程(子进程天生非进程组领头,满足setsid()条件)→ 父进程退出(子进程成孤儿被 init 收养,脱离原父进程控制,就变成后台进程了)→ 子进程调用setsid()(成功创建新会话,脱离原终端)→ 最终成为独立的守护进程。
- 通常将工作目录切换到根目录(/),避免守护进程占用某个挂载的文件系统(如 U 盘),导致该文件系统无法卸载。
- 守护进程不需要与终端交互,因此关闭 标准输入,标准输出,标准错误。/dev/null就是一个”黑洞“,读不到数据,写入数据也被直接丢弃。重定向后所,确保所有标准流操作都合法。守护进程可以通过向文件写日志(注意守护进程的路径已经改变了)。
- 设置守护进程也有对应的接口:
#include <unistd.h>int daemon(int nochdir, int noclose);If nochdir is zero, daemon() changes the process's current working directory to the root directory ("/");otherwise, the current working directory is left unchanged.If noclose is zero, daemon() redirects standard input, standard output and standard error to /dev/null; otherwise, no changes are made to these file descriptors.RETURN VALUE(This function forks, and if the fork(2) succeeds, the parent calls _exit(2), so that further errors are seen by the child only.)Onsuccess daemon() returns zero.If an error occurs, daemon() returns -1and sets errno to any of the errors specified for the fork(2) and setsid(2).
2.8 示例及完整代码
- 示例:
- 需要sudo,才能在/var/log/创建my.log文件,并且需要sudo kill server_netcal。
- 完整代码:NetCal。