Linux-序列化与自定义协议
1. 场景
- 进程间通信:
- 管道、消息队列、共享内存、网络套接字等,它们传递的都是原始的字节流。如果你想在两个进程间传递一个
struct student { int id; char name[20]; },你必须先将这个结构体序列化成字节流发送,接收方再反序列化回结构体。
- 管道、消息队列、共享内存、网络套接字等,它们传递的都是原始的字节流。如果你想在两个进程间传递一个
- 数据持久化:
- 将一些数据保存到文件中。关机后内存会丢失,但文件不会。下次启动时,从文件中读取字节流并反序列化,就能恢复到上次的状态。
- 网络通信(最常用):
- 这是最典型的应用。客户端和服务器程序可能运行在不同的机器、不同的操作系统甚至由不同的编程语言编写。它们之间通信的唯一"通用语言"就是字节流。一个定义好的序列化协议确保了双方都能正确理解数据。
2. 序列化与反序列化
2.1 概念
你可以把它们想象成 "打包" 和 "拆包" 的过程。
- 序列化:
- 过程:将一个存在于内存中的、结构复杂的对象(例如,一个 C 语言的结构体
struct,或一个 C++ 的类对象)转换成一个平坦的、连续的字节序列(字节流)。 - 目的:这个字节序列可以被轻松地存储到文件中,或者通过网络发送到另一台机器。
- 过程:将一个存在于内存中的、结构复杂的对象(例如,一个 C 语言的结构体
- 反序列化:
- 过程:将接收到的或从文件中读取的字节序列,重新构造成一个与原始对象一模一样的内存对象。
- 目的:恢复数据的状态,使得程序可以像使用原始对象一样使用它。
2.2 字节流 VS 数据报
2.2.1 什么是字节流?
字节流是一个连续的、有序的字节序列,数据像流水一样,一个字节接一个字节的进行读取或写入。
重要特性:
- 无边界性
- 字节流本身没有消息边界的概念。
- 发送方写入10次的数据,接收方可能1次就全部读完。
- 发送方写入1次的数据,接收方可能分10次读完。
- 有序性
- 先发送的字节一定先到达,顺序不会错乱。
- 面向连接
- 典型的字节流服务(如TCP)需要先建立连接。
2.2.2 什么是数据报?
数据报是一种在网络中传输的独立的数据单元。
重要特性:
- 有边界性
- 数据报保留完整的消息边界。
- 发送方写入N次的数据,接收方一定通过N次读取读完。
- 每个数据报都是独立的,不会与其他数据报混合。
- 无序可能性
- 先发送的数据报可能后到达,顺序无法保证。
- 无连接
- 典型的数据报服务(如UDP)不需要先建立连接。
数据报传输的是一个个独立的包,而字节流传输的是一个连续的、没有固定分界的数据流。
2.3 字节流的发送方式
2.3.1 字符串方式
特点:
- 数据以可读的文本形式组织。
- 通常使用编码格式(如UTF-8、GBK)。
1.纯文本格式
c
// 发送方
const char* message = "Hello,World,123\n";
send(sockfd, message, strlen(message), 0);
// 接收方
char buffer[1024];
recv(sockfd, buffer, sizeof(buffer), 0);
// 接收到的可能是: "Hello,World,123\n"
2.结构化文本格式(JSON、XML等)
c
// 发送JSON格式
const char* json_str = "{\"name\":\"Alice\",\"age\":25,\"score\":95.5}\n";
send(sockfd, json_str, strlen(json_str), 0);
// 接收方需要解析JSON
// {"name":"Alice","age":25,"score":95.5}
2.3.2 二进制方式
特点:
- 数据以原始的二进制格式组织。
- 直接操作内存字节,效率高。
c
#include <arpa/inet.h>
struct Data {
uint32_t id;
uint16_t age;
};
// 发送二进制数据
void send_binary(int sockfd) {
struct Data data;
data.id = htonl(1001); // 转换字节序
data.age = htons(20); // 转换字节序
send(sockfd, &data, sizeof(data), 0); // 直接发送结构体
}
2.3.3 总结
字符串方式(文本协议)通常是更推荐的选择。
二进制方式会有如下问题:
- 结构体内存对齐问题。
- 语言不同问题。
2.4 TCP粘包问题
当TCP读取数据的时候,读取到的报文不完整或者多读,导致下一个报文不完整,这个问题被称为"粘包"问题。
TCP是字节流,本身没有"包"的概念,这本质是应用层的表述。
客户端发送了多个结构逻辑数据,在接收时被合并成了一个TCP字节流片段。 本质的原因是因为TCP没有消息边界。因此,我们需要自己在应用层维护。
常见的解决方案:长度前缀法 + 分隔符法 。
为序列化后的数据添加报头,报头中存在数据长度字段,报头与数据中可以通过分隔符间隔。
2.5 JSON
在C++中使用JSON,需要采用JsonCpp开源的第三方库,JSON主要被用于序列化和反序列化。
2.5.1 安装(ubuntu)
bash
sudo apt-get install libjsoncpp-dev
注意:
- 编译时需携带 -ljsoncpp
- 头文件引入 #include <jsoncpp/json/json.h>
2.5.1 JSON序列化
1.使用Json::Value的toStyledString方法:
- 优点:将
Json::Value对象直接转换为格式化的JSON字符串。
cpp
#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;
}
// 结果:
{
"name" : "joe",
"sex" : "男"
}
2.使用Json::StreamWriter:
- 优点:提供了更多的定制选项,如缩进、换行符等。
cpp
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::StreamWriterBuilder wbuilder; // StreamWriter的工厂
std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
std::stringstream ss;
writer->write(root, &ss);
std::cout << ss.str() << std::endl;
return 0;
}
3.使用 Json::FastWriter 或 Json::StyledWriter。
cpp
#include <iostream>
#include <string>
#include <sstream>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
// 结果:
{"name":"joe","sex":"男"}
Json::StyledWriter writer;
// 结果:
{
"name" : "joe",
"sex" : "男"
}
2.5.2 JSON反序列化
使用 Json::Reader:
cpp
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main() {
// JSON 字符串
std::string json_string = "{\"name\":\"张三\",\"age\":30,\"city\":\"北京\"}";
Json::Reader reader;
Json::Value root;
// 从字符串中读取 JSON 数据
bool parsingSuccessful = reader.parse(json_string, root);
if(!parsingSuccessful) {
std::cout << "解析失败" << 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;
}
3. 自定义协议
3.1 概念理解
自定义协议是指在网络通信中,应用程序自己定义的数据格式和通信规则,而不是使用现有的标准协议(如 HTTP、FTP、SMTP 等)。
应用程序为了特定需求而设计的数据格式、消息结构和通信规则的集合。
自定义协议:通信双方约定的完整通信规则和数据格式。
只有序列化(缺少协议):
- 这是什么类型的数据?
- 数据有多长?
- 如果处理这个数据?
序列化实际是自定义协议的一部分。
3.2 基于自定义协议的TCP网络版本计算器实现
传输的数据格式(协议):
text
[Json字符串的长度]\r\n[Json字符串]\r\n
10\r\n{"x":10, "y":20, "oper":"+"}\r\n
3.2.1 Socket.hpp
Linux中原生Socket的封装。
cpp
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
namespace SocketMoudle{
const static int default_backlog = 16;
// 设计为虚基类,TCP和UDP的模板类
class Socket{
public:
virtual ~Socket() {}
virtual void socket() = 0;
virtual void bind(uint16_t server_port) = 0;
virtual void listen(int backlog) = 0;
virtual std::shared_ptr<Socket> accept(InetAddr* client_addr) = 0;
virtual void close() = 0;
virtual int recv(std::string* res) = 0;
virtual int send(const std::string& message) = 0;
virtual void connect(const InetAddr& server_addr) = 0;
public:
void buildTcpListenSocket(uint16_t server_port , int backlog = default_backlog) {
socket();
bind(server_port);
listen(default_backlog);
}
void buildTcpClientListendSocket() {
socket();
}
};
const static int default_sockfd = -1;
class TcpSocket : public Socket{
public:
TcpSocket() :_sockfd(default_sockfd) {} // 初始化sockfd为listenfd
TcpSocket(int acceptfd) :_sockfd(acceptfd) {} // 初始化sockfd为accpetfd
virtual void socket() override {
_sockfd = ::socket(AF_INET , SOCK_STREAM , 0);
if(_sockfd < 0) {
LogModule::LOG(LogModule::LogLevel::FATAL) << "socket create error";
exit(SOCKET_ERROR);
}
LogModule::LOG(LogModule::LogLevel::INFO) << "socket create success - sockfd: " << _sockfd;
}
virtual void bind(uint16_t server_port) override {
InetAddr server_addr(server_port);
int n = ::bind(_sockfd , server_addr.getSockaddr() , server_addr.getSockaddrLen());
if(n < 0) {
LogModule::LOG(LogModule::LogLevel::FATAL) << "bind error";
exit(BIND_ERROR);
}
LogModule::LOG(LogModule::LogLevel::INFO) << "bind success - sockfd: " << _sockfd;
}
virtual void listen(int backlog) override {
int n = ::listen(_sockfd , backlog);
if(n < 0) {
LogModule::LOG(LogModule::LogLevel::FATAL) << "listen error";
exit(LISTEN_ERROR);
}
LogModule::LOG(LogModule::LogLevel::INFO) << "listen success - sockfd: " << _sockfd;
}
virtual std::shared_ptr<Socket> accept(InetAddr* client_addr) override {
struct sockaddr_in client;
socklen_t addrlen = sizeof(client);
int acceptfd = ::accept(_sockfd , (struct sockaddr*)&client , &addrlen);
if(acceptfd < 0) {
LogModule::LOG(LogModule::LogLevel::WARNING) << "accpet warning";
return nullptr;
}
client_addr->setSockaddrIn(client); // 输出型参数
return std::make_shared<TcpSocket>(acceptfd); // 多个服务器可以共享一个客户端的链接
}
virtual void close() override {
if(_sockfd > 0)
::close(_sockfd); // close(listenfd) 或 close(accpetfd)
}
// recv返回值和::recv返回值保持一致
virtual int recv(std::string* res) override {
char buffer[1024];
ssize_t n = ::recv(_sockfd , buffer , sizeof(buffer) , 0);
*res += buffer; // 读取可能会不完整
return n;
}
virtual int send(const std::string& message) override {
ssize_t n = ::send(_sockfd , message.c_str() , message.size() , 0);
return n;
}
virtual void connect(const InetAddr& server_addr) {
int n = ::connect(_sockfd , server_addr.getSockaddr() , server_addr.getSockaddrLen());
if(n < 0) {
LogModule::LOG(LogModule::LogLevel::FATAL) << "connect server error";
exit(CONNECT_ERROR);
}
}
private:
int _sockfd; // listenfd 或 accpetfd
};
}
3.2.2 Common.hpp
存放公共代码和头文件的头文件。
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <memory>
#include <unistd.h>
#include <functional>
enum ExitCode{
OK = 0,
SOCKET_ERROR,
BIND_ERROR,
LISTEN_ERROR,
USAGE_ERROR,
CONNECT_ERROR,
FORK_ERRO
};
class NoCopy{
public:
NoCopy() = default;
~NoCopy() = default;
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
};
3.2.3 InetAddr.hpp
网络序列与主机序列相互转换的头文件。
cpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>
const std::string any_ip = "0.0.0.0";
// 解析IP地址和端口号的类
class InetAddr{
public:
InetAddr() = default;
// 这个构造函数用来将 struct sockaddr_in 结构体转换为
// - 1.本地序列的字符串风格的点分十进制的IP
// - 2.本地序列的整数端口
// 网络转主机
InetAddr(const struct sockaddr_in& addr)
:_addr(addr)
{
_port = ntohs(addr.sin_port);
char ip_buffer[64];
inet_ntop(AF_INET , &addr.sin_addr , ip_buffer, sizeof(ip_buffer));
_ip = ip_buffer;
}
void setSockaddrIn(const struct sockaddr_in& addr) {
_addr = addr;
_port = ntohs(addr.sin_port);
char ip_buffer[64];
inet_ntop(AF_INET , &addr.sin_addr , ip_buffer, sizeof(ip_buffer));
_ip = ip_buffer;
}
// 主机转网络
// #define INADDR_ANY 0
InetAddr(const std::string ip , u_int16_t port)
:_ip(ip)
,_port(port)
{
memset(&_addr , 0 , sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
inet_pton(AF_INET , _ip.c_str() , &_addr.sin_addr);
}
InetAddr(u_int16_t port)
:_port(port)
,_ip(any_ip)
{
memset(&_addr , 0 , sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
_addr.sin_addr.s_addr = INADDR_ANY;
// inet_pton(AF_INET , _ip.c_str() , &_addr.sin_addr);
}
const std::string& getIP() const { return _ip; }
u_int16_t getPort() const { return _port; }
const struct sockaddr_in& getSockaddrin() const { return _addr; }
const struct sockaddr* getSockaddr() const { return (const struct sockaddr*)&_addr; }
struct sockaddr* getSockaddr() { return (struct sockaddr*)&_addr; }
socklen_t getSockaddrLen() const { return sizeof(_addr); }
// 格式化显示IP + Port
std::string showIpPort() const {
return "[" + _ip + " : " + std::to_string(_port) + "]";
}
bool operator==(const InetAddr& addr) const {
return _ip == addr.getIP() && _port == addr.getPort();
}
private:
struct sockaddr_in _addr;
std::string _ip;
u_int16_t _port;
};
3.2.4 TcpServer.hpp
通过多进程实现的Tcp服务器。
cpp
#pragma once
#include "Socket.hpp"
#include <sys/types.h>
#include <sys/wait.h>
using namespace SocketMoudle;
using callback_t = std::function<void(std::shared_ptr<Socket>& , const InetAddr&)>;
class TcpServer{
public:
TcpServer(u_int16_t port , callback_t callback)
:_tcp_listensocket(std::make_unique<TcpSocket>(port))
,_port(port)
,_is_running(false)
,_callback(callback)
{
// 多态调用socket/bind/listen
_tcp_listensocket->buildTcpListenSocket(_port);
}
void start() {
_is_running = true;
while(_is_running) {
// 获取客户端的连接
InetAddr client_addr;
std::shared_ptr<Socket> tcp_acceptsocket = _tcp_listensocket->accept(&client_addr);
if(tcp_acceptsocket == nullptr) {
continue;
}
// 多进程/多线程处理客户端连接
pid_t pid = fork();
if(pid < 0) {
LogModule::LOG(LogModule::LogLevel::FATAL) << "server fork error";
exit(FORK_ERRO);
} else if(pid == 0) {
// child process
// 子进程关闭不需要的文件描述符
_tcp_listensocket->close(); // close(accpetfd), 这里子进程可能会写时拷贝
if(fork() > 0)
exit(OK); // 子进程正常退出,父进程回收
// 孙子进程被系统领养,自动回收资源
// 回调函数处理服务器与客户端的连接
_callback(tcp_acceptsocket , client_addr);
tcp_acceptsocket->close();
exit(OK);
}
// parent process
// 父进程关闭不需要的文件描述符
tcp_acceptsocket->close(); // close(listenfd)
// 回收子进程,循环继续执行获取与客户端的连接
waitpid(pid , nullptr , 0);
}
_is_running = false;
}
private:
std::unique_ptr<Socket> _tcp_listensocket;
u_int16_t _port;
bool _is_running;
callback_t _callback;
};
3.2.5 Protocol.hpp
这是一个协议类,主要功能有提供结构化数据的序列化和反序列化,以及添加报头和分离报头的功能。接收(客户端/服务器)发送的数据,并将处理的结果进行返回。
cpp
#pragma once
#include <iostream>
#include <string>
#include "Socket.hpp"
#include <jsoncpp/json/json.h> // 引入jsoncpp第三方库
using namespace SocketMoudle;
// 基于网络版本的计算器
class Request{
public:
Request() = default;
Request(int x , int y , char op)
:left(x)
,right(y)
,oper(op)
{}
std::string serialization() {
Json::Value root;
root["x"] = left;
root["y"] = right;
root["oper"] = oper;
Json::StyledWriter writer;
return writer.write(root);
}
bool deserialization(const std::string& data) {
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(data , root);
if(!ok) {
LogModule::LOG(LogModule::LogLevel::WARNING) << "request deserialization error";
return false;
}
left = root["x"].asInt();
right = root["y"].asInt();
oper = root["oper"].asInt();
return true;
}
int get_x() const { return left; }
int get_y() const { return right; }
char get_oper() const { return oper; }
private:
int left;
int right;
char oper;
};
class Response{
public:
Response(int res = 0 , bool _valid = false)
:result(res)
,valid(_valid)
{}
std::string serialization() {
Json::Value root;
root["result"] = result;
root["valid"] = valid;
Json::StyledWriter writer;
return writer.write(root);
}
bool deserialization(const std::string& data) {
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(data , root);
if(!ok) {
LogModule::LOG(LogModule::LogLevel::WARNING) << "response deserialization error";
return false;
}
result = root["result"].asInt();
valid = root["valid"].asBool();
return true;
}
void showResult() {
std::cout << "result: " << result << " [valid:" << valid << "]" << std::endl;
}
private:
int result;
bool valid;
};
using calculate_t = std::function<Response(const Request&)>;
// 协议类,需要解决两个问题
// 1. 需要有序列化和反序列化功能
// 2. 对于Tcp必须保证读到完整的报文
const static std::string sep = "\r\n";
// format: 10\r\n{"x":10, "y":20, "oper":"+"}\r\n
class Protocol{
public:
Protocol() = default;
Protocol(calculate_t cal_handler) :_cal_handler(cal_handler) {}
// 添加报头
std::string encode(const std::string& jsonstr) {
std::string json_len = std::to_string(jsonstr.size());
return json_len + sep + jsonstr + sep;
}
// 分离报头
bool decode(std::string& buffer_queue , std::string& res) {
size_t pos = buffer_queue.find(sep);
if(pos == std::string::npos) {
return false;
}
std::string json_len = buffer_queue.substr(0 , pos); // 有效载荷总长度
int packet_len = json_len.size() + std::stoi(json_len) + 2 * sep.size();
if(packet_len > buffer_queue.size()) {
return false; //说明当前读取的数据不足一个完整的报文,读取失败,应该继续读取
}
// 来到这里,当前已经有一个完整的报文或者多个完整的报文,或者一个半报文
res = buffer_queue.substr(json_len.size() + sep.size() , std::stoi(json_len)); //将有效载荷带回上层
// 将整个报文从buffer_queue分离
buffer_queue.erase(0 , packet_len);
return true;
}
void getClientAccept(std::shared_ptr<Socket>& client_accpet , const InetAddr& client_addr) {
LogModule::LOG(LogModule::LogLevel::INFO) << "accept success client: " << client_addr.showIpPort();
std::string buffer;
while(true) {
int n = client_accpet->recv(&buffer);
if(n > 0) {
// 1.可能读到不是一个完整的报文 decode为false内层循环不进去,执行外层循环继续读取
// 2.也可能读取到多个完整的报文 decode为true内层循环持续处理多个完整报文
std::string jsonstr;
while(decode(buffer , jsonstr)) {
// 3.服务端接收客户端的计算任务,需要反序列化
Request req;
req.deserialization(jsonstr);
// 4.将反序列化的结果交给上层计算模块处理
Response res = _cal_handler(req);
// 5.将计算后的结果序列化
std::string resp_json = res.serialization();
// 6.将序列化后的json字符串添加报头
std::string packet = encode(resp_json);
// 7.发送给客户端
client_accpet->send(packet);
}
} else if(n == 0) {
// client quit
LogModule::LOG(LogModule::LogLevel::INFO) << "client: " << client_addr.showIpPort() << " quit";
break;
} else {
LogModule::LOG(LogModule::LogLevel::ERROR) << "server recv error";
break;
}
}
}
std::string buildClientRequest(int x, int y , char oper) {
// 1.构建客户端请求
Request req(x , y , oper);
// 2.序列化
std::string json_req = req.serialization();
// 3.添加报头
return encode(json_req);
}
bool getServerResponse(std::shared_ptr<Socket>& client ,std::string& buffer_queue, Response* resq) {
while(true) {
// 可能读取到不是一个完整的报文
int n = client->recv(&buffer_queue);
if(n > 0) {
std::string json_response;
bool ok = decode(buffer_queue , json_response);
if(!ok)
continue; //不是一个完整的报文,继续读取
while(ok) {
// 保证了肯定有一个完整的报文,但是可能会有多个,所以需要连续处理
// 4.反序列化
resq->deserialization(json_response);
// 5.显示结果
resq->showResult();
// sleep(100); debug
ok = decode(buffer_queue , json_response);
}
return true;
} else if(n == 0) {
// server quit
LogModule::LOG(LogModule::LogLevel::INFO) << "server quit";
return false;
} else {
LogModule::LOG(LogModule::LogLevel::INFO) << "recv error";
return false;
}
}
}
private:
calculate_t _cal_handler;
};
3.2.6 Calculate.hpp
主要负责处理数据的类。
cpp
#pragma once
#include "Protocol.hpp"
class Calculate{
public:
Response execute(const Request& req) {
switch(req.get_oper()) {
case '+':
return Response(req.get_x() + req.get_y() , true);
case '-':
return Response(req.get_x() - req.get_y() , true);
case '*':
return Response(req.get_x() * req.get_y() , true);
case '/':
{
if(req.get_y() == 0)
return Response(0, false);
else
return Response(req.get_x() / req.get_y() , true);
}
default:
LogModule::LOG(LogModule::LogLevel::WARNING) << "未知的操作符";
}
return Response(0 , false);
}
};
3.2.7 TcpServer.cc
cpp
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Calculate.hpp"
// tcpserver port
int main(int argc , char* argv[]) {
if(argc != 2) {
LogModule::LOG(LogModule::LogLevel::FATAL) << argv[0] << " port";
exit(USAGE_ERROR);
}
uint16_t server_port = std::stoi(argv[1]);
// 1.应用层处理上层业务
Calculate cal;
// 2.表示层负责收发数据并进行序列化和反序列化
Protocol protocol([&cal](const Request& req)->Response{
return cal.execute(req);
});
// 3.会话层负责建立与客户端的链接
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(server_port ,
[&protocol](std::shared_ptr<Socket>& client_accpet , const InetAddr& client_addr){
protocol.getClientAccept(client_accpet,client_addr);
});
tsvr->start(); // 启动服务器
return 0;
}
3.2.8 TcpClient.cc
cpp
#include "Socket.hpp"
#include "Protocol.hpp"
int main(int argc , char* argv[]) {
if(argc != 3) {
std::cout << argv[0] << " server_ip server port" << std::endl;
exit(USAGE_ERROR);
}
std::string server_ip = argv[1];
u_int16_t server_port = std::stoi(argv[2]);
InetAddr server_addr(server_ip , server_port);
std::shared_ptr<SocketMoudle::Socket> sptr = std::make_shared<SocketMoudle::TcpSocket>();
sptr->buildTcpClientListendSocket();
sptr->connect(server_addr); // 与服务器建立连接
Protocol protocol;
std::string buffer_queue;
while(true) {
int x , y;
char oper;
std::cout << "Please input x: ";
std::cin >> x;
std::cout << "Please input y: ";
std::cin >> y;
std::cout << "Please input operator: ";
std::cin >> oper;
// debug
// std::cout << "x: " << x << " y: " << y << " oper: " << oper << std::endl;
// 1.构建客户端请求
std::string packet = protocol.buildClientRequest(x , y , oper);
// 2.向服务器发送
sptr->send(packet);
// 3.接收服务器返回结果
Response resq;
bool ok = protocol.getServerResponse(sptr ,buffer_queue, &resq);
if(!ok) {
break;
}
}
sptr->close();
return 0;
}
应用层、表示层、会话层一般来说都是用户自己需要实现的部分,这三层经常经常被合并为一层就是应用层。代码中的体现:
- Calculate.hpp 代表的是应用层处理业务的逻辑。
- Protocol.hpp 代表表示层负责收发数据并进行序列化和反序列化。
- TcpServer.hpp 代表会话层负责建立与客户端的连接。
4. 进程间关系与守护进程
4.1 进程组
每一个进程除了有一个进程唯一ID之外,还属于一个进程组。进程组是一个或者多个进程的集合,一个进程组可以包含多个进程。 每一个进程组也有一个唯一的ID,被称为PGID。
每一个进程组都有一个组长进程。组长进程的ID等于其进程组ID。
- 进程组组长的作用:进程组组长可以创建一个进程组或者创建该组中的进程。
- 进程组的生命周期:,从进程组创建开始到其中最后一个进程离开为止。注意: 主要某个进程组中,有一个进程存在,则该进程组就存在,这与其组长进程是否已经终止无关。
- 通常通过管道将几个进程编成一个进程组。
- 向一个前台进程组发送信号,该信号会被传递给组内的所有进程。
4.2 会话
4.2.1 什么是会话?
会话可以看成是一个或多个进程组的集合,一个会话可以包含多个进程组 。每一个会话也会有一个会话ID(SID)。会话ID是会话中首进程的进程ID。
4.2.2 如何创建会话?
通过 setsid 函数来创建一个会话,前提是调用进程不能是一个进程组的组长。
c
#include <unistd.h>
/*
功能:创建会话
返回值:创建成功返回SID, 失败返回-1
*/
pid_t setsid(void);
该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程。 此时,新会话中只有唯一的一个进程。
- 调用进程会变成进程组组长。 新进程组ID就是当前调用进程ID
- 该进程没有控制终端。 如果在调用setsid之前该进程存在控制终端,则调用之后会切断联系。
需要注意的是: 这个接口如果调用进程是进程组组长,则会报错,,为了避免这种情况,通常使用方法是先调用fork创建子进程,父进程终止,子进程继续执行,因为子进程会继承父进程的进程组ID,而进程ID则是新分配的,就不会出现错误的情况。
- 一个会话通常对应一个终端(如
tty1,pts/0)。 - 一个会话中,有且只有一个前台进程组,可以有多个后台进程组。
- 只有前台进程组中的进程才能从终端读取输入并向终端输出。
当你通过SSH登陆Linux时,系统会为你创建一个新的会话,并启动bash命令行解释器(其实就是首进程),让bash进程成为前台任务并关联终端
4.3 守护进程
守护进程是Linux中一种特殊的后台进程 ,它独立于控制终端 并且长期运行 ,通常在操作系统启动时开启,操作系统关闭时终止。
Daemon.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string dev = "/dev/null";
// 将服务进行守护进程化的服务
void Daemon(int nochdir, int noclose)
{
// 1. 忽略IO,子进程退出等相关的信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2. 父进程直接结束
if (fork() > 0)
exit(0);
// 3. 只能是子进程,孤儿了,父进程就是1
setsid(); // 成为一个独立的会话
if(nochdir == 0)
chdir("/");
// 守护进程,不从键盘输入,也不需要向显示器打印
// 打开/dev/null, 重定向标准输入,标准输出,标准错误到/dev/null
if (noclose == 0)
{
int fd = ::open(dev.c_str(), O_RDWR);
if (fd < 0)
{
exit(OPEN_ERR);
}
else
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
}
- 为什么忽略
SIGPIPE?- 当一个进程向一个已经关闭的管道、socket等写入会触发终止进程的行为,守护进程通常作为网络服务运行,可能会遇到客户端意外断开连接的情况,如果不忽略,可能会网络写入失败导致守护进程被意外终止。
- 为什么忽略
SIGCHLD?- 守护进程通常不会创建子进程,或者创建了也不关系子进程的退出状态。设置为
SIG_IGN可以让内核自动回收子进程,避免僵尸进程。
- 守护进程通常不会创建子进程,或者创建了也不关系子进程的退出状态。设置为
- 为什么要将工作目录改为
/?- 根目录总是存在的,确保守护进程中使用到的路径都是以根目录为相对路径开始的。
daemon函数(现成将当前进程变成守护进程的函数)
c
#include <unistd.h>
int daemon(int nochdir, int noclose);
函数:
nochdir:是否改变工作目录到根目录。- 0:改变工作目录到 /
- 非0:保持当前工作目录
noclose:是否重定向标准输入、输出、错误到 /dev/null。- 0:重定向到 /dev/null
- 非0:保持原来的文件描述符
返回值:
- 成功:返回0。
- 失败:返回-1。