
💡Yupureki:个人主页
✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》《MySQL数据库》
《个人在线OJ平台》《Linux网络编程》《CMake自动化构建工具》
🌸Yupureki🌸的简介:

目录
[1. 应用层](#1. 应用层)
[1.1 再谈协议](#1.1 再谈协议)
[1.2 网络版计算器](#1.2 网络版计算器)
[1.3 序列化与反序列化](#1.3 序列化与反序列化)
[2. 网络计算器实现](#2. 网络计算器实现)
[2.1 Socket封装](#2.1 Socket封装)
[2.2 定制协议](#2.2 定制协议)
[2.3 序列化和反序列化的开源库](#2.3 序列化和反序列化的开源库)
[2.3.1 安装](#2.3.1 安装)
[2.3.2 序列化](#2.3.2 序列化)
[2.3.3 反序列化](#2.3.3 反序列化)
[2.3.4 Request和Response实现](#2.3.4 Request和Response实现)
[2.4 计算器设计](#2.4 计算器设计)
[2.5 服务器设计](#2.5 服务器设计)
[2.6 上层使用](#2.6 上层使用)
[2.7 总结](#2.7 总结)
1. 应用层
1.1 再谈协议
在Linux网络(以及所有计算机网络)中,协议 可以通俗地理解为一种事先约定好的规则和标准。
TCP/IP模型协议分4层:
| 层级 | 作用 | Linux中的常见协议 | 生活类比 |
|---|---|---|---|
| 应用层 | 为应用程序提供特定网络服务 | HTTP (网页)、SSH (远程登录)、DNS (域名解析)、NFS (文件共享) | 信的内容(用中文写的信) |
| 传输层 | 负责端到端的连接和数据可靠性 | TCP (可靠、有连接,如下载文件)、UDP (快速、无连接,如视频通话) | 怎么把信安全送到(挂号信 vs 平信) |
| 网络层 | 负责寻址和路由,找到目标设备 | IP (IPv4/IPv6,定义源和目标IP地址)、ICMP (ping命令用的协议) | 信封上的地址(国家-城市-街道) |
| 网络接口层 | 处理物理硬件(网卡、网线) | ARP (通过IP地址找MAC地址)、以太网协议 | 实际送信的交通工具(货车、飞机) |
我们之前谈到的TCP/UDP是传输层的协议,负责传输快递,但快递里面的内容是什么,需要我们自定义,这属于应用层协议
1.2 网络版计算器
我们期望做一个网络版的计算器:
- 客户端发送计算式,如1+1,发送给服务器
- 服务器接受到计算式,在后台进行计算,然后发送给客户端
那我们怎么处理发送的计算式,客户端直接把"1+1"这个字符串发送给服务器吗?
但是互联网上的信息可能传输不完全,我假设要发送"1+1+1"这个式子给服务器。但由于传输的问题,没有传输完整,只发送了1+1,那么服务器拿到后就会直接开始计算,根本不知道没有拿完数据。
因此我们需要对数据进行一层封装(如同快递的包装盒),再交给服务端(拆解快递盒),如果发现封装后的数据不完整(快递盒子只有一半),就不进行处理,继续接受直到数据完整,这就是序列化与反序列化
1.3 序列化与反序列化
在网络传输中,我们在传输前会对数据以一种特定的格式进行封装,也就是序列化 ;当对端接收到数据后,会再次按照该格式进行拆分,也就是反序列化

如上述的struct message结构体,其中的变量:year,month,day和name我们依次排列 ,再在外面加上花括号,这就是序列化
对端接受的数据可能是以下情况:
- {year,month,day,name} (完整)
- {year,month,da (不完整)
- {year,month,day,name}{year,mon (多出了不完整的部分)
如何判断是否完整?->外面的花括号是否完整
如果只有前花括号,那么就不完整;如果前后花括号都有,那么这中间的数据就是完整的,直接拿走,然后继续处理下一个数据
2. 网络计算器实现
2.1 Socket封装
为了方便,我们专门把socket相关函数用C++封装成类
- Socket虚基类:保留socket基本接口,如bind,listen,recv等,设计InitTcpServer和InitTcpClient接口,后续派生类可调用这两个接口自由创建服务器端和客户端
- TcpSocket派生类:根据虚基类实现具体的TCP的接口
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "com.hpp"
#include <memory>
class Socket
{
public:
Socket()
{}
~Socket()
{}
virtual void create_socket() = 0;
virtual void Bind(uint16_t) = 0;
virtual void Listen(int) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr&) = 0;
virtual bool Connect(const std::string&,const uint16_t&) = 0;
virtual int Recv(std::string&) = 0;
virtual void Send(const std::string&) = 0;
virtual int get_sockfd() = 0;
void InitTcpServer(uint16_t port = DEFAULT_PORT,int backlog = DEFAULT_BACKLOG)
{
create_socket();
Bind(port);
Listen(backlog);
}
void InitTcpClient(std::string ip = DEFAULT_IP,uint16_t port = DEFAULT_PORT)
{
create_socket();
Connect(ip,port);
}
};
class TcpSocket : public Socket
{
private:
using func_t = std::function<void()>;
public:
TcpSocket(int sockfd = DEFAULT_SOCKFD)
:_sockfd(sockfd)
{}
void create_socket() override
{
_sockfd = socket(AF_INET,SOCK_STREAM,0);
if(_sockfd < 0)
exit(ExitCode::SOCKET);
}
void Bind(uint16_t port)override
{
InetAddr addr(port);
int n = bind(_sockfd,CONV(addr.get_addr()),sizeof(addr.get_addr()));
if(n < 0)
exit(ExitCode::BIND);
}
void Listen(int backlog)override
{
int n = listen(_sockfd,backlog);
if(n < 0)
exit(ExitCode::LISTEN);
}
std::shared_ptr<Socket> Accept(InetAddr& addr)override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_sockfd,CONV(peer),&len);
if(sockfd < 0)
return nullptr;
InetAddr tmp(peer);
addr = tmp;
std::shared_ptr<Socket> p = std::make_shared<TcpSocket>(sockfd);
return p;
}
bool Connect(const std::string& ip,const uint16_t& port)override
{
InetAddr addr(ip,port);
int n = connect(_sockfd,CONV(addr.get_addr()),sizeof(addr.get_addr()));
if(n < 0)
return false;
return true;
}
int Recv(std::string& out)override
{
char buffer[MAXNUM];
ssize_t n = recv(_sockfd,buffer,sizeof(buffer) - 1,0);
if(n > 0)
{
buffer[n] = '\0';
out += buffer;
return n;
}
else if(n == 0)
return 0;
else
return -1;
}
void Send(const std::string& out)override
{
send(_sockfd,out.c_str(),sizeof(out),0);
}
int get_sockfd()override
{
return _sockfd;
}
private:
int _sockfd;
};
2.2 定制协议
一个完整的协议至少需要这两个类:
- Request:发送端发送的请求数据
- Response:接收端根据请求,进行处理后返回给发送端的数据
具体流程:
发送端->request序列化,发送给接收端->接收端接受request,反序列化->对request处理,制作response->response序列化,返回给发送端->发送端接受response,反序列化得到结果
代码构造:
cpp
class Request
{
public:
Request(int x = 0,char oper = ' ',int y = 0)
:_x(x),_oper(oper),_y(y)
{}
void Serialize(std::string& out)//序列化:成员变量序列化成字符串
{
}
bool Dserialize(std::string& out)//反序列化:字符串反序列化,提取成类中的成员变量
}
int get_x(){return _x;}
char get_oper(){return _oper;}
int get_y(){return _y;}
std::string get_request_string()
{
return std::to_string(_x) + " " + _oper + " " + std::to_string(_y);
}
private:
int _x;
char _oper;
int _y;
};
cpp
class Reponse
{
public:
Reponse(double ret = 0,int code = 0)
:_result(ret),_code(code)
{}
Reponse(Reponse& rep)
{}
Reponse(Reponse&& rep)
{}
Reponse& operator=(Reponse&& rep)
{}
Reponse& operator=(Reponse& rep)
{}
void Serialize(std::string& out)//序列化
{}
bool Dserialize(std::string& out)//反序列化
{}
double get_ret()
{
return _result;
}
int get_code()
{
return _code;
}
std::string get_result_string()
{
return std::to_string(_result) + "[" +std::to_string(_code) + "]";
}
private:
double _result;//计算结果
int _code;//计算状态码
};
问题:
TCP 是一个面向字节流 的协议,它本身不维护消息边界。当发送方连续发送多个小数据包,或接收方一次读取到多个包的数据时,就可能出现"粘包"------即多个应用层消息粘在一起,无法区分哪里是结束、哪里是开始。解决粘包问题的核心是在应用层定义消息边界。
如:
- request对1+1序列化得:{1 + 1}
- 后续再对{1+1}定义消息边界,如7\r\n{1 + 1}\r\n
- \r\n表示每个消息的开头和结尾
- 7表示正文"{1 + 1}"(包括空格和花括号)的长度,相当于字节个数
- 接受端接收到7\r\n{1 + 1}\r\n,会先:
- 找到第一个"\r\n",如果没找到直接返回
- 找到数据长度7,计算完整报文的长度:1('7'的长度) + 2*2(两个"\r\n"的长度) + 7(正文的长度)
- 如果实际接受到的数据长度小于计算的长度,说明不完全,直接返回;如果大于或等于,则截断计算的长度的字符串,进行处理后再处理下一个报文
这就是打包和解包的过程
我们再构建Protocol类,这个类用来:
- 对数据的打包和解包
- 接发送数据
- 处理数据(具体业务,需要上层指定)
cpp
class Protocol
{
private:
const std::string proto_sep = " ";
const std::string line_sep = "\r\n";//分隔符
using func_t = std::function<Reponse(Request&)>;//业务处理函数,上层指定
public:
Protocol()
{}
Protocol(func_t func)
:_func(func)
{}
void Encode(std::string& message)//打包
{
message = std::to_string(message.size()) + line_sep + message + line_sep;
}
bool Decode(std::string& package,std::string& message)//解包
{
int pos = package.find(line_sep);
if(pos == std::string::npos)
return false;
std::string num = package.substr(0,pos);
int total = std::stoi(num) + num.size() + 2 * line_sep.size();
if(package.size() < total)
return false;
message = package.substr(pos + line_sep.size(),std::stoi(num));
package.erase(0,total);
return true;
}
bool get_request(std::shared_ptr<Socket>& sock,Request& req)//接受request,进行1.解包2.反序列化
{
std::string package;
while(1)
{
int n = sock->Recv(package);
if(n > 0)
{
std::string message;
if(!Decode(package,message))
continue;
req.Dserialize(message);
return true;
}
else if(n == 0)
exit(ExitCode::NORMAL);
else
return false;
}
}
bool get_reponse(std::shared_ptr<Socket>& sock,Reponse& rep)//接受response,进行1.解包2.反序列化
{
std::string package;
while(true)
{
int n = sock->Recv(package);
if(n > 0)
{
std::string message;
if(!Decode(package,message))
continue;
rep.Dserialize(message);
return true;
}
else if(n == 0)
return false;
else
return false;
}
}
Reponse handle(Request& req)//数据处理(接受端的任务)
{
return _func(req);
}
private:
func_t _func;
};
2.3 序列化和反序列化的开源库
Jsoncpp是一个用于处理JSON数据的C++库 。它提供了将JSON数据序列化 为字符串以及从字符串
反序列化为C++数据结构的功能。Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。
2.3.1 安装
ubuntu:sudo apt-get install libjsoncpp-dev
Centos:sudo yum installjsoncpp-devel
2.3.2 序列化
使用Json::Value 和Json::FastWriter快速上手
cpp
#include <jsoncpp/json/json.h>
#include <iostream>
#include <jsoncpp/json/value.h>
int main()
{
Json::Value json;
json["x"] = 1;
json["oper"] = '+';
json["y"] = 2;
Json::FastWriter w;
std::string out = w.write(json);
std::cout<<out;
return 0;
}

2.3.3 反序列化
使用Json::Value 和Json::Reader快速上手
cpp
#include <jsoncpp/json/json.h>
#include <iostream>
#include <jsoncpp/json/reader.h>
#include <jsoncpp/json/value.h>
int main()
{
Json::Value json;
json["x"] = 1;
json["oper"] = '+';
json["y"] = 2;
Json::FastWriter w;
std::string out = w.write(json);
std::cout<<out;
Json::Reader r;
bool res = r.parse(out,json);
if(res)
{
int x = json["x"].asInt();
char oper = json["oper"].asInt();
int y = json["y"].asInt();
std::cout<<x<<" "<<oper<<" "<<y<<std::endl;
}
return 0;
}

2.3.4 Request和Response实现
我们使用jsoncpp来进行request和response的序列化和反序列化
cpp
class Request
{
public:
Request(int x = 0,char oper = ' ',int y = 0)
:_x(x),_oper(oper),_y(y)
{}
void Serialize(std::string& out)
{
Json::Value json;
json["x"] = _x;
json["oper"] = _oper;
json["y"] = _y;
Json::FastWriter w;
out = w.write(json);
}
bool Dserialize(std::string& out)
{
Json::Value json;
Json::Reader r;
bool res = r.parse(out,json);
if(res)
{
_x = json["x"].asInt();
_oper = json["oper"].asInt();
_y = json["y"].asInt();
}
return res;
}
int get_x(){return _x;}
char get_oper(){return _oper;}
int get_y(){return _y;}
std::string get_request_string()
{
return std::to_string(_x) + " " + _oper + " " + std::to_string(_y);
}
private:
int _x;
char _oper;
int _y;
};
class Reponse
{
public:
Reponse(double ret = 0,int code = 0)
:_result(ret),_code(code)
{}
Reponse(Reponse& rep)
{
_result = rep.get_ret();
_code = rep.get_code();
}
Reponse(Reponse&& rep)
{
_result = rep.get_ret();
_code = rep.get_code();
}
Reponse& operator=(Reponse&& rep)
{
_result = rep.get_ret();
_code = rep.get_code();
return *this;
}
Reponse& operator=(Reponse& rep)
{
_result = rep.get_ret();
_code = rep.get_code();
return *this;
}
void Serialize(std::string& out)
{
Json::Value json;
json["result"] = _result;
json["code"] = _code;
Json::FastWriter w;
out = w.write(json);
}
bool Dserialize(std::string& out)
{
Json::Value json;
Json::Reader r;
bool res = r.parse(out,json);
if(res)
{
_result = json["result"].asDouble();
_code = json["code"].asInt();
}
return res;
}
double get_ret()
{
return _result;
}
int get_code()
{
return _code;
}
std::string get_result_string()
{
return std::to_string(_result) + "[" +std::to_string(_code) + "]";
}
private:
double _result;
int _code;
};
2.4 计算器设计
自定义协议能够让我们成功接收到"{1 + 1}"这个字符串,并对这个字符串反序列化成了Request,但这个Request怎么处理?我们需要上层自定义。而在这里,我们要对这个Request中的数据进行普通的加减乘除的运算,因此我们需要设计计算器
这个计算器需要:
- 接受Request参数
- 对Request中的数据进行处理
- 对处理后数据做成Response
cpp
class cal
{
public:
Reponse func(Request &req)
{
auto x = req.get_x();
auto y = req.get_y();
char oper = req.get_oper();
switch (oper)
{
case '+':
return Reponse(x + y, 0);
case '-':
return Reponse(x - y, 0);
case '*':
return Reponse(x * y, 0);
case '/':
if (y == 0)
return Reponse(0, 1);
else
return Reponse(x / y, 0);
case '%':
return Reponse(x % y, 0);
default:
return Reponse(0, 2);
}
}
};
2.5 服务器设计
我们作为开发者,期望设计一个TcpServer,这个TcpServer内部包含TcpSocket和业务处理函数;TcpSocket由我们自己提供,而业务处理函数需要上层进行提供
cpp
#pragma once
#include "socket.hpp"
class TcpServer : public nocopy
{
private:
using func_t = std::function<void(std::shared_ptr<Socket>&,InetAddr&)>;//自定义处理函数
public:
TcpServer(uint16_t port,func_t func)
:_port(port),_ioserver(func),_listensock(std::make_shared<TcpSocket>())
{}
void init(int backlog = DEFAULT_BACKLOG)
{
_listensock->InitTcpServer(_port,backlog);
_listensockfd = _listensock->get_sockfd();
}
void run()
{
_isrunning = true;
while(_isrunning)
{
InetAddr addr;
std::shared_ptr<Socket> sock = _listensock->Accept(addr);
if(sock == nullptr)
continue;
pid_t pid = fork();
if(pid > 0)
{
close(sock->get_sockfd());
waitpid(pid,nullptr,0);
}
else if(pid == 0)
{
if(fork() > 0)
exit(ExitCode::NORMAL);
close(_listensock->get_sockfd());
_ioserver(sock,addr);//把数据全盘交给用户的业务处理函数
}
else
exit(ExitCode::FORK);
}
}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning = false;
std::shared_ptr<Socket> _listensock;
func_t _ioserver;
};
2.6 上层使用
- 底层:提供TcpServer,用来接受数据。但这个数据接受后,我该如何处理?看上层的选择
- 上层:提供自定义协议,在TcpServer接受到数据后进行个性化处理
cpp
#include "NetCal.hpp"
#include "protocol.hpp"
#include "TcpServer.hpp"
#include "com.hpp"
int main(int argv, char *argc[])
{
if (argv != 2)
exit(ExitCode::FORMAT);
std::shared_ptr<cal> netcal = std::make_shared<cal>();
std::shared_ptr<Protocol> prot = std::make_shared<Protocol>([&netcal](Request &req) -> Reponse
{ return netcal->func(req); });
TcpServer server(atoi(argc[1]), [&prot](std::shared_ptr<Socket> &server, InetAddr &addr)
{//上层自定义处理函数
while(1)
{
Request req;
prot->get_request(server,req);
std::cout<<"request : "<<req.get_request_string()<<std::endl;
Reponse rep = prot->handle(req);
std::cout<<"reponse : "<<rep.get_result_string()<<std::endl;
std::string message;
rep.Serialize(message);
prot->Encode(message);
server->Send(message);
}
}
);
server.init();
server.run();
}
2.7 总结
一图理清流程:
