文章目录
- TCP通信原理
- TCP与UDP的对比
- 应用层
- 应用层协议 --- tcp
- 协议定制
- 直接传递对象
- 自定义协议
- 现在要解决的问题
- 业务处理
- json的使用
- 使用json进行序列化和反序列化操作
- 总结
TCP通信原理
tcp是面向字节流的
同时他也是面向连接的
所以TCP的服务器编写代码如图所示:
客户端的编写代码如图:
tcp在进行connect的时候, 会先进行三次链接, 交换报文, 这一过程叫做TCP的三次握手
这一过程就是客户端的SYN_SENT到服务器的RSTABLISHED过程
而TCP_server在进行, accept的时候, 前提必须是把获取好的连接传输给应用层, 得到新的文件描述符这样才能进行使用
在server的ESTABLISED这一步中, accept会进行返回, 分配新的文件描述符connfd和客户端进行通信
而也在这时表示是握手成功, 需要注意
在connect时只发起一次, 这是一次系统调用, 另外两次由双方的OS自动完成握手的操作
connect与accept可能会发生阻塞, 主要是因为, 他在等待三次握手的链接完成
建立链接, 实际是建立共识, 后续展开~~~~
不完成三次握手, 就不会有后续的整个过程
建立链接完成, 实际上是双方为了维护一条链接, 创建的描述链接的结构体字段
而在udp 的客户端, 没有connect的这一步, 而是直接发送数据
TCP在断开链接的时候, 实际上就是完成了四次挥手.
其中在client的FIN_WAIT_1的字段表示, 客户端首次发起请求断开的链接
CLOSR_WAIT表示服务器做出相应, 同一断开链接的请求
LAST_ACK表示, 服务端也向客户端发送请求断开链接
而他们的挥手都是close()引起的
这一过程是全双工的
整个过程像是离婚, 双方都要进行确认
TCP与UDP的对比
说到底, 他们无非就是
可靠传输VS不可靠传输
有连接VS无连接
字节流VS数据报
应用层
应用层协议 — tcp
协议本身是一种约定, TCP是面向字节流的, 传输需要正确书写, 现在将重点放在读取的位置处, 我们进行网络版本的计算器的完成(client进行数据的请求, 实际的数据的处理工作由server进行处理)
- 在我们的日常通信中, 给对方发送消息包含性别时间, 消息内容等的相关的信息, 但是不能都发送, 而是打包成一体进行发送, 这一过程就会进行序列化的操作
- 到我们发送过去的时候, 会将这个一体的信息进行解析, 这是又会变为初始状态的信息的状态. 从一体的状态变成单个的信息状态, 这个过程就是反序列化
那么协议概念也就如图所示了:
也就是说, 这样的结构化的字段就能被双方进行识别, 双方使用同一种数据类型, 能对指定的字段进行解释, 这是一种约定, 也就是协议
当然, 在应用层不推荐使用结构化字段进行数据的存储和转发到对应的机器, 因为目的机器可能OS不同, 像结构体的内存对齐, 不同类型的指针等的大小都会不同, 所以一般使用序列化处理之后的结果进行转发和处理
那么TCP就是经过序列化处理之后形成的字节流进行转发, 发送过去之后, 进行反序列化得到结构化字段再进行读取数据
序列化
:就是将协议对应的结构化字段, 转化成"字符串"的字节流
反序列化
: 将"字符串"的字节流, 转化为结构化数据
为什么要这样做呢?
为了方便网络发送
为什么不直接使用传struct的方式呢?而却要在应用层, 使用序列化的方式传递字节流?
- 应用层变化快, 因为机器不同导致的问题比较多, 所以更适合使用序列化转化的方式.
- 比较好扩展, 未来如果修改协议字段, 序列化的传送也不影响.
- Linux内核的各种协议字段都是控制好的, 所以该OS内的进程之间通信不需要使用序列化
- 可以跨语言进行使用, 客户端和服务端在未来可能是不同的语言进行写的, 不同的语言使用序列化API之后, 可以响应为对应语言正确的结构化字段
协议定制
直接传递对象
// 封装一个基类, 用于表示一个socket接口类
class Socket {
public:
Socket();
virtual ~Socket(){}
virtual void CreateSocketOrDie() = 0;// 创建或者死亡
virtual void BindSocketOrDie(uint16_t port) = 0;// 绑定或者死亡
virtual void ListenSocketOrDie(int backlog) = 0;// 监听或者死亡 这个参数为listen函数的backlog参数,后续讲
virtual Socket* AcceptSocketOrDie(std::string *peerip, uint16_t *peerport) = 0;// 接受或者死亡 输出型参数, 获得远端ip和端口
virtual void ConnectSocketOrDie(const std::string& serverip, uint16_t serverport) = 0;// 连接或者死亡
virtual void CloseSocketOrDie() = 0;// 关闭或者死亡
virtual int GetSocketFd() const = 0;// 获取socket文件描述符
virtual void SetSocketFd(int sockfd) = 0;// 设置socket文件描述符
public:
void BuildListenSocket(uint16_t port, int backlog)// 创建一个监听socket
{
CreateSocketOrDie();// 创建socket
BindSocketOrDie(port);// 绑定端口
ListenSocketOrDie(backlog);// 监听
}
void BuildConnectSocket(const std::string& serverip, uint16_t serverport)// 创建一个连接socket
{
CreateSocketOrDie();// 创建socket
ConnectSocketOrDie(serverip, serverport);// 连接
}
void BuildNormalSocket(int sockfd)// 创建一个普通socket
{
SetSocketFd(sockfd);
}
};
TcpServerMain.cc
#include "Protocol.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include <memory>
#include <unistd.h>
#include <iostream>
using namespace Net_Work;
void HandlerRequest(Socket *sockp)
{
std::cout << "in handler_request" << std::endl;
}
// ./TcpServer port
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;
return 1;
}
uint16_t localport = std::stoi(argv[1]);
std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerRequest));
svr->Loop();
return 0;
}
TcpServer.hpp
#pragma once
#include "Socket.hpp"
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
#include <functional>
using func_t = std::function<void(Net_Work::Socket* sockp)>;
class TcpServer;
class THreadData
{
public:
THreadData(TcpServer *tcp_this, Net_Work::Socket *sockp) : _this(tcp_this), _sockp(sockp)
{}
public:
TcpServer *_this;
Net_Work::Socket *_sockp;
};
class TcpServer{
public:
TcpServer(uint16_t port, func_t handler_request) : _port(port), _listenSock(new Net_Work::TcpSocket()), _handler_request(handler_request)
{
_listenSock->BuildListenSocketMethod(_port, Net_Work::backlog);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());// 分离线程
THreadData *td = static_cast<THreadData*>(args);
td->_this->_handler_request(td->_sockp);
td->_sockp->CloseSocket();// 关闭套接字
delete td->_sockp;
delete td;
return nullptr;
}
void Loop()// 服务器的主循环
{
while(true)
{
std::string peerip;
uint16_t peerport;
// 获取套接字
Net_Work::Socket* newsock = _listenSock->AcceptSocketOrDie(&peerip, &peerport);// 接受一个连接
if(newsock == nullptr)// 如果接受失败, 则继续循环
{
std::cout << "AcceptSocketOrDie failed" << std::endl;
continue;
}
std::cout << "Get a new connection, sockfd is : " << newsock->GetSocketFd() << ", peer ip: " << peerip << ", peer port: " << peerport << std::endl;
// 使用线程来处理连接
pthread_t tid;
THreadData *td = new THreadData(this, newsock);
pthread_create(&tid, nullptr, ThreadRun, (void*)td);
}
}
~TcpServer()
{
delete _listenSock;
}
private:
int _port;
Net_Work::Socket *_listenSock;// 创建一个监听socket
public:
func_t _handler_request;// 处理连接的函数
};
// 至此, 服务端的代码已经完成, 主要功能在HandlerRequest函数中, 该函数需要用户自己实现
// 服务端只是一个框架, 用户需要自己实现HandlerRequest函数来处理连接
TcpClientMain.cc
#include "Protocol.hpp"
#include "Socket.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
// ./TcpClient Serverip Serverport
int main(int argc, char* argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " <Serverip> <Serverport>" << std::endl;
return 1;
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
Net_Work::Socket* sock = new Net_Work::TcpSocket();
if(!sock->BuildConnectSocketMethod(serverip, serverport))
{
std::cerr << "Connect " << serverip << ":" << serverport << " failed" << std::endl;
return 1;
}
std::cout << "Connect " << serverip << ":" << serverport << " success" << std::endl;
std::string message = "Hello, Server";
write(sock->GetSocketFd(), message.c_str(), message.size());
sock->CloseSocket();
return 0;
}
Socket.hpp
#pragma once
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define Convert(addrptr) ((struct sockaddr*)addrptr) // 这个宏用于将sockaddr_in 转换为struct sockaddr*
namespace Net_Work
{
const static int defaultSockfd = -1;
const static int backlog = 5;
enum{
Socket_Error = 1,
Bind_Error,
Listen_Error
};
// 封装一个基类, 用于表示一个socket接口类
// 设计模式: 模板方法模式, 未来不管是什么socket, 都继承这个类, 实现这些接口, 这样就可以复用这些接口, 这个流程是固定的
class Socket {
public:
virtual ~Socket(){}
virtual void CreateSocketOrDie() = 0;// 创建或者死亡
virtual void BindSocketOrDie(uint16_t port) = 0;// 绑定或者死亡
virtual void ListenSocketOrDie(int backlog) = 0;// 监听或者死亡 这个参数为listen函数的backlog参数,后续讲
virtual Socket* AcceptSocketOrDie(std::string *peerip, uint16_t *peerport) = 0;// 接受或者死亡 输出型参数, 获得远端ip和端口
virtual bool ConnectSocketOrDie(const std::string& serverip, uint16_t serverport) = 0;// 连接或者死亡
virtual int GetSocketFd() const = 0;// 获取socket文件描述符
virtual void SetSocketFd(int sockfd) = 0;// 设置socket文件描述符
virtual void CloseSocket() = 0;// 关闭socket
public:
void BuildListenSocketMethod(uint16_t port, int backlog)// 创建一个监听socket
{
CreateSocketOrDie();// 创建socket
BindSocketOrDie(port);// 绑定端口
ListenSocketOrDie(backlog);// 监听
}
bool BuildConnectSocketMethod(const std::string& serverip, uint16_t serverport)// 创建一个连接socket
{
CreateSocketOrDie();// 创建socket
return ConnectSocketOrDie(serverip, serverport);// 连接
}
void BuildNormalSocketMethod(int sockfd)// 创建一个普通socket
{
SetSocketFd(sockfd);
}
};
// 继承类Socket, 用于表示一个tcp socket
class TcpSocket : public Socket {
public:
TcpSocket(int sockfd = -1)
:_sockfd(sockfd)
{}
~TcpSocket()
{}
void CreateSocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
exit(Socket_Error);
}
}
void BindSocketOrDie(uint16_t port)
{
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;
int n = ::bind(_sockfd, Convert(&local), sizeof(local));
if (n < 0)
{
exit(Bind_Error);
}
}
void ListenSocketOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
{
exit(Listen_Error);
}
}
Socket* AcceptSocketOrDie(std::string *peerip, uint16_t *peerport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newsockfd = ::accept(_sockfd, Convert(&peer), &len);
if (newsockfd < 0)
{
return nullptr;
}
*peerport = ntohs(peer.sin_port);
*peerip = inet_ntoa(peer.sin_addr);
Socket* newSock = new TcpSocket(newsockfd);
return newSock;
}
bool ConnectSocketOrDie(const std::string& serverip, uint16_t serverport) override
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
int n = ::connect(_sockfd, Convert(&server), sizeof(server));
if (n == 0)
{
return true;
}
return false;
}
int GetSocketFd() const override
{
return _sockfd;
}
void SetSocketFd(int sockfd) override
{
_sockfd = sockfd;
}
void CloseSocket() override
{
if (_sockfd > defaultSockfd)
{
::close(_sockfd);
}
}
private:
int _sockfd;
};
}
协议定制文件, Protocol.hpp
#pragma once
#include <iostream>
// 定制协议
class Request {
public:
Request(int x, int y);
~Request();
private:
int _data_x;// 参数x
int _data_y;// 参数y
char _oper;// 操作符 + - * / %
};
class Response {
private:
int _result;// 结果
int _code;// 状态码
};
自定义协议
首先进行举例说明
在这个过程中,
- write/send只是拷贝函数, 将数据从缓冲区拷贝到内核或者是将数据从缓冲区拷贝到内核
- 发送缓冲区数据, 是什么时候发?发多少?出错了怎么办?
都是由内核决定 — TCP控制协议
TCP协议叫做传输控制协议, 传输和控制的就是数据的拷贝内容
- TCP在实际的通信是双方OS之间的通信, 将数据从一台OS拷贝给另一台OS
- 像之前的recv/read也只是与这些功能类似
- 而read/write/send/recv…这些功能, 与当时文件的IO过程一模一样, 在内核到用户发送数据的时候, 用户级缓冲区都是处于阻塞等待状态, 当内核的数据到来时, 才会唤醒用户级缓冲区, 进行内容的写入
- 而这个从一台机器发送到另一台机器的过程就像是生产者消费者模型, 一边进行发送, 一边进行拿取, 这也是最经典的生产者消费者模型
- 一台在发送时, 同时也能完成接收, 这就是全双工协议的TCP, UDP虽然是单工通信, 但是内部与之类似
- 数据的粘包问题
用户输入的字符, 发送到对端时, 可能会变少, 这是因为什么呢?
首先, TCP是面向字节流, 没有边界, 而OS在发送TCP数据时, 会通过缓冲区来进行优化, 例如缓冲区的大小为1024字节. 如果一次请求发送的数据量比较少, 没达到缓冲区大小, TCP则会将多个请求合并为一个请求进行发送, 这就形成了粘包问题, 如果一次请求发送的数据量比较大, 超过了缓冲区的大小, TCp就会将其拆分为多次进行发送, 这就是拆包.
实际用户发送多少字节, 并不一定收到多少字节就是这个原因
但是UDP是数据报, 要么都过不去, 要么全部发过去
- 除了要定制协议, 还要明确报文与报文之间的边界, 目的就是为了防止数据包的粘包问题(后期代码体现)
- UDP是不需要的, 他是面向数据报的协议, 报文与报文之间有明确边界, 而TCp需要自己添加边界, 分为一个一个的数据包, 从用户(上层)看来就是一个一个的字节流, 因此他也叫做面向字节流
现在要解决的问题
- 结构化数据的序列化和反序列化
- 首先这边传入的数据是 x y 和op操作符, 那么传过去的时候, 最合适的方式就是"x op y\n", 但是为了以后数据序列化的通用性, 这边引入数据头, 那么这个数据就会变成"len\nx op y\n", 我们只需要找到第一个\n就可以知道len的长度是多少, len代表一个数据包的长度, 由此将来传过来的数据流的格式可能是"len\nx op y\n"“len\nx op y\n”"len\nx op y\n"但是可以通过上述的寻找方式都找到
- \n都不属于是报文的一部分, 这是一种约定
- 这些很多工作都是在做字符串处理
- 对Response做序列化和反序列化
bool Serialize(std::string &out)
{
out.append(std::to_string(_result));
out.append(ProtSep);
out.append(std::to_string(_code));
return true;
}
bool Deserialize(std::string &in)// 将"_result _code" 反序列化成对象
{
auto pos = in.find(ProtSep);
if(pos == std::string::npos)
{
std::cerr << "Response::Deserialize failed! pos == std::string::npos" << std::endl;
return false;
}
_result = std::stoi(in.substr(0, pos));// 刚好是[)区间
_code = std::stoi(in.substr(pos+ProtSep.size()));
return true;
}
- 对Request做序列化和反序列化
// 序列化 --- 将对象序列化成字符串
bool Serialize(std::string &out)// 序列化都是输出型参数
{
out.append(std::to_string(_data_x));
out.append(ProtSep);
out.append(std::to_string(_data_y));
out.append(ProtSep);
out.append(std::to_string(_oper));
return true;
}
// 反序列化 --- 将字符串反序列化成对象
bool Deserialize(std::string &in)// 反序列化传入的必须是 "x op y" 的格式
{
auto left = in.find(ProtSep);
if(left == std::string::npos)
{
std::cerr << "Request::Deserialize failed! left == std::string::npos" << std::endl;
return false;
}
auto right = in.rfind(ProtSep);
if(right == std::string::npos)
{
std::cerr << "Request::Deserialize failed! right == std::string::npos" << std::endl;
return false;
}
_data_x = std::stoi(in.substr(0, left));
_data_y= std::stoi(in.substr(right+ProtSep.size()));// 从当前位置开始截取到ProtSep.size()个字符, 截到结尾
std::string oper = in.substr(left+ProtSep.size(), right-(left+ProtSep.size()));
if(oper.size() != 1)
{
std::cerr << "Request::Deserialize failed! oper.size() != 1" << std::endl;
return false;
}
_oper = oper[0];
return true;
}
- 上述操作只是完成业务方面的应用, len /n的处理是在网络发送方面先序列化, 换成"x op y"的格式, 然后调用
encode
, 变成"len\nx op y\n"
完成解析函数
std::string Encode(const std::string &message)// 先序列化, 变成"x op y"的格式, 然后调用Encode, 变成"len\nx op y\n"形式
{
std::string len = std::to_string(message.size());
std::string package = len + LineBreakSep + message + LineBreakSep;
return package;
}
// 解析的时候, 无法保证传进来的package一定是一个完整的协议, 所以需要传入一个指针, 指向一个字符串, 将解析出来的协议内容放入这个字符串中
bool Decode(std::string &package, std::string *message)// 先解码, 变成"len\nx op y\n"形式, 然后反序列化, 变成对象
{
auto pos = package.find(LineBreakSep);// 确保拿到的长度是一个完整的协议
if (pos == std::string::npos)
{
std::cerr << "Decode failed! pos == std::string::npos" << std::endl;
return "";
}
std::string lens = package.substr(0, pos);// 拿到协议的长度
int message_len = std::stoi(lens);// 将长度转换为整数
int total_len = message_len + pos + LineBreakSep.size() + 2 * LineBreakSep.size();// 确保长度是完整的协议的长度
if (package.size() < total_len)// 如果长度不够, 返回false
{
std::cerr << "Decode failed! package.size() < message_len + pos + LineBreakSep.size()" << std::endl;
return false;
}
// 走到这里, 说明长度至少是一个完整的协议的长度
*message = package.substr(pos + LineBreakSep.size(), message_len);// 将协议内容赋值给message
package.erase(0, total_len);// 删除已经解析出来的协议, 保证下一次解析的时候, 不会重复解析
return true;
}
业务处理
void HandlerRequest(Socket *sockp)
{
std::string inbufferstream;// 接收请求的缓冲区
// 1.创建一个工厂类, 用于创建请求对象
std::unique_ptr<Factory> factory = std::make_unique<Factory>();
auto req = factory->BuildRequest();
// 接收请求
while(true)
{
// 1.读取报文
if (!sockp->Recv(&inbufferstream, 1024))
{
break;// 接收失败
}
// 2.分析收到的字节流, 是否有一个完整的报文
std::string message;
if(!Decode(inbufferstream, &message))
{
continue;
}
// 3.处理请求, 走到这里, 说明已经有一个完整的报文了, 可以进行反序列化
if(!req->Deserialize(message))
{
break;// 反序列化失败
}
// 4.业务处理, 计算器
auto resp = calculate.Cal(req);
// 5.将响应对象序列化
std::string send_string;
resp->Serialize(&send_string);// 序列化之后数据一定是"result code"
// 6.添加报头(构建完整的字符串级别的响应报文)
send_string = Encode(send_string);
// 上述的操作, 可以封装成一个类, 因为序列化的整体流程就是这样的, 封装成一个类可以直接调用, 然后进行后续的工作, 更整体化, 更清晰
}
}
现在进行修改代码, 实际的输入内容由上层决定, 当前的回调只是将数据进行发送, 转发即可, 后续的操作就不再演示,省略即可
引入成熟的序列化和反序列化的操作
为了方便操作, 先将自己的序列化方式改为条件编译进行限制
json的使用
首先, 我们需要知道现如今常用的序列化方式
json — 常用
protobuf — 网络中常用的序列化和反序列化操作
xml – 比较慢, c++不进行考虑
使用json进行序列化和反序列化操作
java python都是可以直接使用json, 而c++要进行使用, 则必须要使用第三方sdk, 安装别人的库
sudo apt-get install -y jsoncpp-devel //devel表示这个版本是开发板
安装完成序列化的演示实现
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root; // 万能类型, 接受任何数据类型
root["k1"] = 100;
root["k2"] = 200;
root["k3"] = 300;
Json::FastWriter writer;
std::string str = writer.write(root); // 序列化
std::cout << str << std::endl;
return 0;
}
因为jsoncpp是第三方的库, 所以要使用, 必须在编译时进行编译选项的链接
运行结果演示:
同时json支持嵌套键值对的定义实现:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root; // 万能类型, 接受任何数据类型
root["k1"] = 100;
root["k2"] = 200;
root["k3"] = 300;
Json::Value v;
v["hello"] = "world";
v["world"] = "nihao";
root["k4"] = v;
Json::FastWriter writer;
std::string str = writer.write(root); // 序列化
std::cout << str << std::endl;
return 0;
}
运行结果演示:
除了使用FastWrite方式还可以使用StyleWrite方式进行:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root; // 万能类型, 接受任何数据类型
root["k1"] = 100;
root["k2"] = 200;
root["k3"] = 300;
Json::Value v;
v["hello"] = "world";
v["world"] = "nihao";
root["k4"] = v;
Json::StyledWriter writer;
std::string str = writer.write(root); // 序列化
std::cout << str << std::endl;
return 0;
}
可以看到他生成的结果是具有格式演示的序列化, 这样只是可读性强一点, 除此之外没有区别
对上述的业务代码完成序列化和反序列化操作:
序列化:
Json::Value root;
root["x"] = _data_x;
root["y"] = _data_y;
root["op"] = _oper; // 字符的本质就是整数, 所以可以直接赋值
Json::FastWriter writer;
*out = writer.write(root); // 也可以判断是否成功, 这边就不进行了
return true;
反序列化:
Json::Value root;
Json::Reader reader;
bool ret = reader.parse(in, root);
if (!ret) // 解析失败, 返回false, 参数为in, 解析结果为root
{
std::cerr << "Request::Deserialize failed! Json::Reader::parse failed" << std::endl;
return false;
}
_data_x = root["x"].asInt();
_data_y = root["y"].asInt();
_oper = root["op"].asInt(); // 字符的本质就是整数
return ret;
这边代码的encoed和decode和对我们自己的序列化和反序列化变成的"len\nx op y\n"的格式, 在使用json之后有什么区别呢?会有变化吗?
实际上, 不管是自定义序列化还是json序列化, 他们操作之后的结果或者是操作之前的数据格式都是对这两个方法没有影响的, 因为长度信息都在len这个字符当中, 所以未来不管使用哪种序列化的方法, 他都能进行正确的识别
现在已经完成了对自定义序列化和json序列化的条件编译代码, 要想实现对应的序列化操作还需要对他进行编译代码时的选择, 这边选择在makefile中进行修改
在编译时加一个 -static表示静态编译, 这样编译出来的代码内部以静态链接的方式连接着其他的第三方库, 所以内存较大, 但是直接运行的效果较好
可以将这个程序改为守护进程的方式
总结
- 上述用到的协议只有一层, Response Request, 未来如果想引入多个协议以供不同的应用场景, 可以将这些协议写好之后保存在一个vector里面, 再在序列化的时候在序列化字段最前面加上
protocal_code\nlen\n...
, 其中protocal_code就表示使用哪一个协议. - 像是Socket.hpp的整个功能就能表示一个会话层, 用于通信的连接和管理
- 我们写的protocal添加报头, 对象转字符串等的操作都是在表示层
- 而这个应用层就是我们写的calculate.hpp
- 这些我们自己写的三层最终都会汇总到应用层(从OSI模型到TCP/IP模型也是这样)当中, 未来我们写网络服务就一定会用到着三层.