🔭个人主页: 北 海
🛜所属专栏: Linux学习之旅、神奇的网络世界
💻操作环境: CentOS 7.6 阿里云远程服务器
文章目录
- 🌤️前言
- 🌦️正文
- 1.协议的重要性
- 2.什么是序列化与反序列化?
- 3.实现相关程序
- 4.封装socket相关操作
- 5.服务器
- 6.序列化与反序列
- 7.工具类
- 8.业务处理
- 9.报头处理
- 10.客户端
- 11.测试
- 12.使用库
- 🌨️总结
🌤️前言
本文将介绍如何使用C++实现简单的服务器和客户端通信,并重点讲解序列化与反序列化的概念和实现。这篇文章将深入探究数据在网络传输中的转换过程,以及如何在C++中应用这些技术
🌦️正文
1.协议的重要性
假设张三在路上遇到了一位外国人 Jack
,这位外国朋友急于寻找厕所,对张三进行了一波 英语
输出,这可难到了张三,因为他 英语
可谓是十分差劲,母语的差异导致双方无法正常交流,信息也无法传达,张三急中生智,打开了手机上的 同声传译功能,可以将信息转换为对方能听懂的语言,在工具的帮助之下外国友人最终知晓了厕所的位置
在这个故事中,张三和外国人 Jack
就是两台主机,同声传译 这个功能可以看做一种 协议(可以确保对端能理解自己传达的信息),协议 的出现解决了主机间的交流问题
对于网络来说,协议是双方通信的基石,如果没有协议,那么即使数据传输的再完美也无法使用,比如下面这个就是一个简单的 两正整数运算协议
- 协议要求:发送的数据必须由两个操作数(正整数)和一个运算符组成,并且必须遵循
x op y
这样的运算顺序
int x;
int y;
char op; // 运算符
主机A在发送消息时需要将 操作数x
、操作数y
和运算符op
进行传递,只要主机A和主机B都遵循这个 协议,那么主机B在收到消息后一定清楚这是两个操作数和一个运算符
现在的问题是如何传递?
- 方案一:将两个操作数和一个运算符拼接在一起直接传递
- 方案二:将两个操作数和一个运算符打包成一个结构体传递
方案一:直接拼接 xopy
方案二:封装成结构体
struct Mssage
{
int x;
int y;
char op;
};
无论是方案一还是方案二都存在问题,前者是对端接收到消息后无法解析,后者则是存在平台兼容问题(不同平台的结构体内存规则可能不同,会导致读取数据出错)
要想确保双方都能正确理解 协议,还需要进行 序列化与反序列化 处理
2.什么是序列化与反序列化?
序列化是指 将一个或多个需要传递的数据,按照一定的格式,拼接为一条数据,反序列化则是 将收到的数据按照格式解析
比如主机A想通过 两正整数运算协议 给主机B发送这样的消息
//1+1
int x = 1;
int y = 1;
char op = '+';
可以根据格式(这里使用
(空格))进行 序列化,序列化后的数据长这样
// 经过序列化后得到
string msg = "1 + 1";
在经过网络传输后,主机B收到了消息,并根据
(空格)进行 反序列化,成功获取了主机A发送的信息
string msg = "1 + 1";
// 经过反序列化后得到
int x = 1;
int y = 1;
char op = '+';
这里可以将需要传递的数据存储在结构体中,传递/接收 时将数据填充至类中,类中提供 序列化与反序列化 的相关接口即可
class Request
{
public:
void Serialization(string* str)
{}
void Deserialization(const sting& str)
{}
public:
int _x;
int _y;
char _op;
};
以上就是一个简单的 序列化和反序列化 流程,简单来说就是 协议 定制后不能直接使用,需要配合 序列化与反序列化 这样的工具理解,接下来我们就基于 两正整数运算协议 编写一个简易版的网络计算器,重点在于 理解协议、序列化和反序列化
3.实现相关程序
我们接下来要编写的程序从实现功能来看是十分简单的:客户端给出两个正整数和一个运算符,服务器计算出结果后返回
整体框架为:客户端获取正整数与运算符 -> 将这些数据构建出 Request
对象 -> 序列化 -> 将结果(数据包)传递给服务器 -> 服务器进行反序列化 -> 获取数据 -> 根据数据进行运算 -> 将运算结果构建出 Response
对象 -> 序列化 -> 将结果(数据包)传递给客户端 -> 客户端反序列后获取最终结果
既然这是一个基于网络的简易版计算器,必然离不开网络相关接口,在编写 服务器 与 客户端 的逻辑之前,需要先将 socket
接口进行封装,方面后续的使用
4.封装socket相关操作
关于 socket
的相关操作可以看看这两篇博客《网络编程『socket套接字 ‖ 简易UDP网络程序』》、《网络编程『简易TCP网络程序』》
注:当前实现的程序是基于
TCP
协议的
简单回顾下,服务器需要 创建套接字、绑定IP地址和端口号、进入监听连接状态、等待客户端连接,至于客户端需要 创建套接字、由操作系统绑定IP地址和端口号、连接服务器,等客户端成功连上服务器后,双方就可以正常进行网络通信了
为了让客户端和服务器都能使用同一个头文件,我们可以把客户端和服务器需要的所有操作都进行实现,各自调用即可
Sock.hpp
套接字相关接口头文件
#pragma once
#include "Log.hpp"
#include "Err.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
class Sock
{
const static int default_sock = -1;
const static int default_backlog = 32;
public:
Sock()
:sock(default_sock)
{}
// 创建套接字
void Socket()
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
logMessage(Fatal, "Creater Socket Fail! [%d]->%s", errno, strerror(errno));
exit(SOCKET_ERR);
}
logMessage(Debug, "Creater Socket Success");
}
// 绑定IP与端口号
void Bind(const 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;
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) == -1)
{
logMessage(Fatal, "Bind Socket Fail! [%d]->%s", errno, strerror(errno));
exit(BIND_ERR);
}
logMessage(Debug, "Bind Socket Success");
}
// 进入监听状态
void Listen()
{
if(listen(sock, default_backlog) == -1)
{
logMessage(Fatal, "Listen Socket Fail! [%d]->%s", errno, strerror(errno));
exit(LISTEN_ERR);
}
}
// 尝试处理连接请求
int Accept(std::string* ip, uint16_t* port)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int retSock = accept(sock, (struct sockaddr*)&client, &len)
;
if(retSock < 0)
logMessage(Warning, "Accept Fail! [%d]->%s", errno, strerror(errno));
else
{
*ip = inet_ntoa(client.sin_addr);
*port = ntohs(client.sin_port);
logMessage(Debug, "Accept [%d -> %s:%d] Success", retSock, ip->c_str(), *port);
}
return retSock;
}
// 尝试进行连接
int Connect(const std::string& ip, const uint16_t& port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
return connect(sock, (struct sockaddr*)&server, sizeof(server));
}
// 获取sock
int GetSock()
{
return sock;
}
// 关闭sock
void Close()
{
if(sock != default_sock)
close(sock);
logMessage(Debug, "Close Sock Success");
}
~Sock()
{}
private:
int sock; // 既可以是监听套接字,也可以是连接成功后返回的套接字
};
这里还需要用到之前编写的错误码和日志输出
Err.hpp
错误码头文件
#pragma once
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
FORK_ERR,
SETSID_ERR,
CHDIR_ERR,
OPEN_ERR,
READ_ERR,
};
Log.hpp
日志输出头文件
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>
using namespace std;
enum
{
Debug = 0,
Info,
Warning,
Error,
Fatal
};
static const string file_name = "log/TcpServer.log";
string getLevel(int level)
{
vector<string> vs = {"<Debug>", "<Info>", "<Warning>", "<Error>", "<Fatal>", "<Unknown>"};
//避免非法情况
if(level < 0 || level >= vs.size() - 1)
return vs[vs.size() - 1];
return vs[level];
}
string getTime()
{
time_t t = time(nullptr); //获取时间戳
struct tm *st = localtime(&t); //获取时间相关的结构体
char buff[128];
snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);
return buff;
}
//处理信息
void logMessage(int level, const char* format, ...)
{
//日志格式:<日志等级> [时间] [PID] {消息体}
string logmsg = getLevel(level); //获取日志等级
logmsg += " " + getTime(); //获取时间
logmsg += " [" + to_string(getpid()) + "]"; //获取进程PID
//截获主体消息
char msgbuff[1024];
va_list p;
va_start(p, format); //将 p 定位至 format 的起始位置
vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
va_end(p);
logmsg += " {" + string(msgbuff) + "}"; //获取主体消息
// 直接输出至屏幕上
cout << logmsg << endl;
// //持久化。写入文件中
// FILE* fp = fopen(file_name.c_str(), "a"); //以追加的方式写入
// if(fp == nullptr) return; //不太可能出错
// fprintf(fp, "%s\n", logmsg.c_str());
// fflush(fp); //手动刷新一下
// fclose(fp);
// fp = nullptr;
}
有了 Sock.hpp
头文件后,服务器/客户端就可以专注于逻辑编写了
5.服务器
首先准备好 TcpServer.hpp
头文件,其中实现了服务器初始化、服务器启动、序列化与反序列化等功能
TcpServer.hpp
服务器头文件
#pragma once
#include "Sock.hpp"
#include <iostream>
#include <string>
#include <pthread.h>
namespace CalcServer
{
class TcpServer;
// 线程所需要的信息类
class ThreadDate
{
public:
ThreadDate(int& sock, std::string& ip, uint16_t& port, TcpServer* ptsvr)
:_sock(sock), _ip(ip), _port(port), _ptsvr(ptsvr)
{}
~ThreadDate()
{}
int _sock;
std::string _ip;
uint16_t _port;
TcpServer* _ptsvr; // 回指指针
};
class TcpServer
{
const static uint16_t default_port = 8888;
private:
// 线程的执行函数
static void* threadRoutine(void* args)
{
// 线程剥离
pthread_detach(pthread_self());
ThreadDate* td = static_cast<ThreadDate*>(args);
td->_ptsvr->ServiceIO(td->_sock, td->_ip, td->_port);
delete td;
return nullptr;
}
// 进行IO服务的函数
void ServiceIO(const int& sock, const std::string ip, const uint16_t& port)
{
// TODO
}
public:
TcpServer(const uint16_t port = default_port)
:_port(port)
{}
// 初始化服务器
void Init()
{
_listen_sock.Socket();
_listen_sock.Bind(_port);
_listen_sock.Listen();
}
// 启动服务器
void Start()
{
while(true)
{
std::string ip;
uint16_t port;
int sock = _listen_sock.Accept(&ip, &port);
if(sock == -1)
continue;
// 创建子线程,执行业务处理
pthread_t tid;
ThreadDate* td = new ThreadDate(sock, ip, port, this);
pthread_create(&tid, nullptr, threadRoutine, td);
}
}
~TcpServer()
{
_listen_sock.Close();
}
private:
Sock _listen_sock; // 监听套接字
uint16_t _port; // 服务器端口号
};
}
上面这份代码我们之前在 《网络编程『简易TCP网络程序』》 中已经写过了,本文的重点在于实现 ServiceIO()
函数,现在可以先尝试编译并运行程序,看看代码是否有问题
CalcServer.cc
简易计算器服务器源文件
#include <iostream>
#include <memory>
#include "TcpServer.hpp"
using namespace std;
int main()
{
unique_ptr<CalcServer::TcpServer> tsvr(new CalcServer::TcpServer());
tsvr->Init();
tsvr->Start();
return 0;
}
Makefile
自动编译脚本
.PHONY:all
all:CalcServer CalcClient
CalcServer:CalcServer.cc
g++ -o $@ $^ -std=c++11 -lpthread
CalcClient:CalcClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf CalcServer CalcClient
编译并运行程序,同时查看网络使用情况
netstat -nltp
此时就证明前面写的代码已经没有问题了,接下来是填充 ServiceIO()
函数
6.序列化与反序列
ServiceIO()
函数需要做这几件事
- 读取数据
- 反序列化
- 业务处理
- 序列化
- 发送数据
除了 序列化和反序列化 外,其他步骤之前都已经见过了,所以我们先来看看如何实现 序列化与反序列化
ServiceIO()
函数 — 位于TcpServer.hpp
头文件中的TcpServer
类中
// 进行IO服务的函数
void ServiceIO(const int& sock, const std::string ip, const uint16_t& port)
{
// 1.读取数据
// 2.反序列化
// 3.业务处理
// 4.序列化
// 5.发送数据
}
需要明白我们当前的 协议 为 两正整数运算,分隔符为
(空格),客户端传给服务器两个操作数和一个运算符,服务器在计算完成后将结果返回,为了方便数据的读写,可以创建两个类:Request
和 Response
,类中的成员需要遵循协议要求,并在其中支持 序列化与反序列化
Protocol.hpp
协议处理相关头文件
#pragma once
#include <string>
namespace my_protocol
{
// 协议的分隔符
#define SEP " "
#define SEP_LEN strlen(SEP)
class Request
{
public:
Request(int x = 0, int y = 0, char op = '+')
: _x(x), _y(y), _op(op)
{}
// 序列化
bool Serialization(std::string *outStr)
{
}
// 反序列化
bool Deserialization(const std::string &inStr)
{}
~Request()
{}
public:
int _x;
int _y;
char _op;
};
class Response
{
public:
Response(int result = 0, int code = 0)
:_result(result), _code(code)
{}
// 序列化
bool Serialization(std::string *outStr)
{}
// 反序列化
bool Deserialization(const std::string &inStr)
{}
~Response()
{}
public:
int _result; // 结果
int _code; // 错误码
};
}
接下来就是实现 Serialization()
和 Deserialization()
这两个接口
Serialization()
:将类中的成员根据协议要求,拼接成一个字符串Deserialization()
:将字符串根据格式进行拆解
Request
类 — 位于Protocol.hpp
协议相关头文件中
class Request
{
public:
Request(int x = 0, int y = 0, char op = '+')
: _x(x), _y(y), _op(op)
{}
// 序列化
bool Serialization(std::string *outStr)
{
*outStr = ""; // 清空
std::string left = Util::IntToStr(_x);
std::string right = Util::IntToStr(_y);
*outStr = left + SEP + _op + SEP + right;
return true;
}
// 反序列化
bool Deserialization(const std::string &inStr)
{
std::vector<std::string> result;
Util::StringSplit(inStr, SEP, result);
// 协议规定:只允许存在两个操作数和一个运算符
if(result.size() != 3)
return false;
// 规定:运算符只能为一个字符
if(result[1].size() != 1)
return false;
_x = Util::StrToInt(result[0]);
_y = Util::StrToInt(result[2]);
_op = result[1][0];
return true;
}
~Request()
{}
public:
int _x;
int _y;
char _op;
};
其中涉及 IntToStr()
、StringSplit()
、StrToInt()
等接口,等后面实现 Response
类时也需要使用,所以我们可以直接将其放入 Util
工具类中
7.工具类
工具类中包含了常用的工具函数
Util.hpp
工具类
#pragma once
#include <string>
#include <vector>
class Util
{
public:
static std::string IntToStr(int val)
{
// 特殊处理
if(val == 0)
return "0";
std::string str;
while(val)
{
str += (val % 10) + '0';
val /= 10;
}
int left = 0;
int right = str.size() - 1;
while(left < right)
std::swap(str[left++], str[right--]);
return str;
}
static int StrToInt(const std::string& str)
{
int ret = 0;
for(auto e : str)
ret = (ret * 10) + (e - '0');
return ret;
}
static void StringSplit(const std::string& str, const std::string& sep, std::vector<std::string>* result)
{
size_t left = 0;
size_t right = 0;
while(right < str.size())
{
right = str.find(sep, left);
if(right == std::string::npos)
break;
result->push_back(str.substr(left, right - left));
left = right + sep.size();
}
if(left < str.size())
result->push_back(str.substr(left));
}
};
接下来就可以顺便把 Response
中的 Serialization()
和 Deserialization()
这两个接口给实现了,逻辑和 Request
类中的差不多(当然结果也要符合 协议 的规定,使用
(空格)进行分隔)
Response
类 — 位于Protocol.hpp
协议相关头文件中
class Response
{
public:
Response(int result = 0, int code = 0)
:_result(result), _code(code)
{}
// 序列化
bool Serialization(std::string *outStr)
{
*outStr = ""; // 清空
std::string left = Util::IntToStr(_result);
std::string right = Util::IntToStr(_code);
*outStr = left + SEP + right;
return true;
}
// 反序列化
bool Deserialization(const std::string &inStr)
{
std::vector<std::string> result;
Util::StringSplit(inStr, SEP, &result);
if(result.size() != 2)
return false;
_result = Util::StrToInt(result[0]);
_code = Util::StrToInt(result[1]);
return true;
}
~Response()
{}
public:
int _result; // 结果
int _code; // 错误码
};
现在 ServiceIO()
中可以进行 序列化和反序列化 了
ServiceIO()
函数 — 位于TcpServer.hpp
头文件中的TcpServer
类中
// 进行IO服务的函数
void ServiceIO(const int& sock, const std::string ip, const uint16_t& port)
{
while(true)
{
// 1.读取数据
std::string package; // 假设这是已经读取到的数据包,格式为 "1 + 1"
// 2.反序列化
my_protocol::Request req;
if(req.Deserialization(package) == false)
{
logMessage(Warning, "Deserialization fail!");
continue;
}
// 3.业务处理
// TODO
my_protocol::Response resp; // 业务处理完成后得到的响应对象
// 4.序列化
std::string sendMsg;
resp.Serialization(&sendMsg);
// 5.发送数据
}
}
至于业务处理函数如何实现,交给上层决定,也就是 CalcServer.cc
8.业务处理
TcpServer
中的业务处理函数由 CalcServer.cc
传递,规定业务处理函数的类型为 void(Request&, Response*)
Calculate()
函数 — 位于CalcServer.cc
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <iostream>
#include <memory>
#include <functional>
#include <unordered_map>
using namespace std;
void Calculate(my_protocol::Request& req, my_protocol::Response* resp)
{
// 这里只是简单的计算而已
int x = req._x;
int y = req._y;
char op = req._op;
unordered_map<char, function<int()>> hash =
{
{'+', [&](){ return x + y; }},
{'-', [&](){ return x - y; }},
{'*', [&](){ return x * y; }},
{'/', [&]()
{
if(y == 0)
{
resp->_code = 1;
return 0;
}
return x / y;
}
},
{'%', [&]()
{
if(y == 0)
{
resp->_code = 2;
return 0;
}
return x % y;
}
}
};
if(hash.count(op) == 0)
resp->_code = 3;
else
resp->_result = hash[op]();
}
int main()
{
unique_ptr<CalcServer::TcpServer> tsvr(new CalcServer::TcpServer(Calculate));
tsvr->Init();
tsvr->Start();
return 0;
}
既然 CalcServer
中传入了 Calculate()
函数对象,TcpServer
类中就得接收并使用,也就是业务处理
TcpServer.hpp
头文件
#pragma once
#include "Sock.hpp"
#include "Protocol.hpp"
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
namespace CalcServer
{
using func_t = std::function<void(my_protocol::Request&, my_protocol::Response*)>;
class TcpServer
{
const static uint16_t default_port = 8888;
private:
// ...
// 进行IO服务的函数
void ServiceIO(const int& sock, const std::string ip, const uint16_t& port)
{
while(true)
{
// 1.读取数据
std::string package; // 假设这是已经读取到的数据包,格式为 "1 + 1"
// 2.反序列化
my_protocol::Request req;
if(req.Deserialization(package) == false)
{
logMessage(Warning, "Deserialization fail!");
continue;
}
// 3.业务处理
my_protocol::Response resp; // 业务处理完成后得到的响应对象
_func(req, &resp);
// 4.序列化
std::string sendMsg;
resp.Serialization(&sendMsg);
cout << sendMsg << endl;
// 5.发送数据
}
}
public:
// ...
private:
// ...
func_t _func; // 上层传入的业务处理函数
};
}
这就做好业务处理了,ServiceIO()
函数已经完成了 50%
的工作,接下来的重点是如何读取和发送数据?
TCP
协议是面向字节流的,这也就意味着数据在传输过程中可能会因为网络问题,分为多次传输,这也就意味着我们可能无法将其一次性读取完毕,需要制定一个策略,来确保数据全部递达
9.报头处理
如何确认自己已经读取完了所以数据?答案是提前知道目标数据的长度,边读取边判断
数据在发送时,是需要在前面添加 长度 这个信息的,通常将其称为 报头,而待读取的数据称为 有效载荷,报头 和 有效载荷 的关系类似于快递单与包裹的关系,前者是后者成功递达的保障
最简单的 报头 内容就是 有效载荷 的长度
问题来了,如何区分 报头 与 有效载荷 呢?
- 当前可以确定的是,我们的报头中只包含了长度这个信息
- 可以通过添加特殊字符,如
\r\n
的方式进行区分 - 后续无论有效载荷变成什么内容,都不影响我们通过报头进行读取
报头处理属于协议的一部分
所以在正式读写数据前,需要解决 报头 的问题(收到数据后移除报头,发送数据前添加报头)
ServiceIO()
函数 — 位于TcpServer.hpp
头文件中的TcpServer
类中
// 进行IO服务的函数
void ServiceIO(const int& sock, const std::string ip, const uint16_t& port)
{
while(true)
{
// 1.读取数据
std::string package; // 假设这是已经读取到的数据包,格式为 "5\r\n1 + 1"
// 2.移除报头
// 3.反序列化
my_protocol::Request req;
if(req.Deserialization(package) == false)
{
logMessage(Warning, "Deserialization fail!");
continue;
}
// 4.业务处理
my_protocol::Response resp; // 业务处理完成后得到的响应对象
_func(req, &resp);
// 5.序列化
std::string sendMsg;
resp.Serialization(&sendMsg);
cout << sendMsg << endl;
// 6.添加报头
// 7.发送数据
}
}
在 Protocol.hpp
中完成报头的添加和移除
Protocol.hpp
协议相关头文件
#define HEAD_SEP "\r\n"
#define HEAD_SEP_LEN strlen(HEAD_SEP)
// 添加报头
void AddHeader(std::string& str)
{
// 先计算出长度
size_t len = str.size();
std::string strLen = Util::IntToStr(len);
// 再进行拼接
str = strLen + HEAD_SEP + str;
}
// 移除报头
void RemoveHeader(std::string& str, size_t len)
{
// len 表示有效载荷的长度
str = str.substr(str.size() - len);
}
报头+有效载荷需要通过 read()
或者 recv()
函数从网络中读取,并且需要边读取边判断
ReadPackage()
读取函数 — 位于Protocol.hpp
头文件
#define BUFF_SIZE 1024
// 读取数据
int ReadPackage(int sock, std::string& inBuff, std::string* package)
{
// 也可以使用 read 函数
char buff[BUFF_SIZE];
int n = recv(sock, buff, sizeof(buff) - 1, 0);
if(n < 0)
return -1; // 表示读取失败
else if(n == 0)
return 0; // 需要继续读取
buff[n] = '\0';
inBuff += buff;
// 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
int pos = inBuff.find(HEAD_SEP);
if(pos == std::string::npos)
return -1;
std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
if(inBuff.size() < packLen)
return -1;
*package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
inBuff.erase(0, packLen); // 从缓冲区中取走字符串
return Util::StrToInt(strLen);
}
此时对于 ServiceIO()
函数来说,核心函数都已经准备好了,只差拼装了
ServiceIO()
函数 — 位于TcpServer.hpp
头文件中的TcpServer
类中
// 进行IO服务的函数
void ServiceIO(const int& sock, const std::string ip, const uint16_t& port)
{
std::string inBuff;
while(true)
{
// 1.读取数据
std::string package; // 假设这是已经读取到的数据包,格式为 "5\r\n1 + 1"
int len = my_protocol::ReadPackage(sock, inBuff, &package);
if(len < 0)
break;
else if(len == 0)
continue;
// 2.移除报头
my_protocol::RemoveHeader(package, len);
// 3.反序列化
my_protocol::Request req;
if(req.Deserialization(package) == false)
{
logMessage(Warning, "Deserialization fail!");
continue;
}
// 4.业务处理
my_protocol::Response resp; // 业务处理完成后得到的响应对象
_func(req, &resp);
// 5.序列化
std::string sendMsg;
resp.Serialization(&sendMsg);
cout << sendMsg << endl;
// 6.添加报头
my_protocol::AddHeader(sendMsg);
// 7.发送数据
send(sock, sendMsg.c_str(), sendMsg.size(), 0);
}
}
至此服务器编写完毕,接下来就是进行客户端的编写了
10.客户端
编写客户端的 TcpCilent.hpp
头文件
TcpClient.hpp
客户端头文件
#pragma once
#include "Sock.hpp"
#include "Protocol.hpp"
#include "Log.hpp"
#include "Err.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
namespace CalcClient
{
class TcpClient
{
public:
TcpClient(const std::string& ip, const uint16_t& port)
:_server_ip(ip), _server_port(port)
{}
void Init()
{
_sock.Socket();
}
void Start()
{
int i = 5;
while(i > 0)
{
if(_sock.Connect(_server_ip, _server_port) != -1)
break;
logMessage(Warning, "Connect Server Fail! %d", i--);
sleep(1);
}
if(i == 0)
{
logMessage(Fatal, "Connect Server Fail!");
exit(CONNECT_ERR);
}
// 执行读写函数
ServiceIO();
}
void ServiceIO()
{
while(true)
{
std::string str;
std::cout << "Please Enter:> ";
std::getline(std::cin, str);
// 1.判断是否需要退出
if(str == "quit")
break;
// 2.分割输入的字符串
my_protocol::Request req;
[&]()
{
std::string ops = "+-*/%";
int pos = 0;
for(auto e : ops)
{
pos = str.find(e);
if(pos != std::string::npos)
break;
}
req._x = Util::StrToInt(str.substr(0, pos));
req._y = Util::StrToInt(str.substr(pos + 1));
req._op = str[pos];
}();
// 3.序列化
std::string sendMsg;
req.Serialization(&sendMsg);
// 4.添加报头
my_protocol::AddHeader(sendMsg);
// 5.发送数据
send(_sock.GetSock(), sendMsg.c_str(), sendMsg.size(), 0);
// 6.获取数据
std::string inBuff;
std::string package;
int len = 0;
while(true)
{
len = my_protocol::ReadPackage(_sock.GetSock(), inBuff, &package);
if(len < 0)
exit(READ_ERR);
else if(len > 0)
break;
}
// 7.移除报头
my_protocol::RemoveHeader(package, len);
// 8.反序列化
my_protocol::Response resp;
if(resp.Deserialization(package) == false)
{
logMessage(Warning, "Deserialization fail!");
continue;
}
// 9.获取结果
std::cout << "The Result: " << resp._result << " " << resp._code << endl;
}
}
~TcpClient()
{
_sock.Close();
}
private:
Sock _sock;
std::string _server_ip;
uint16_t _server_port;
};
}
注意: 客户端也需要边读取边判断,确保读取内容的完整性
下面是 CalcClient.cc
的代码
CalcClient.cc
客户端源文件
#include "TcpClient.hpp"
#include <iostream>
#include <memory>
using namespace std;
int main()
{
unique_ptr<CalcClient::TcpClient> tclt(new CalcClient::TcpClient("127.0.0.1", 8888));
tclt->Init();
tclt->Start();
return 0;
}
11.测试
编译并运行代码
可以在代码中添加一定的输出语句,感受 序列化和反序列化 的过程
12.使用库
事实上,序列化与反序列化 这种工作轮不到我们来做,因为有更好更强的库,比如 Json
、XML
、Protobuf
等
比如我们就可以使用 Json
来修改程序
首先需要安装 json-cpp
库,如果是 CentOS7
操作系统的可以直接使用下面这条命令安装
yum install -y jsoncpp-devel
安装完成后,可以引入头文件 <jsoncpp/json/json.h>
然后就可以在 Protocol.hpp
头文件中进行修改了,如果想保留原来自己实现的 序列化与反序列化 代码,可以利用 条件编译 进行区分
Protocol.hpp
协议相关头文件
#pragma once
#include "Util.hpp"
#include <jsoncpp/json/json.h>
#include <string>
#include <vector>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
namespace my_protocol
{
// 协议的分隔符
#define SEP " "
#define SEP_LEN strlen(SEP)
#define HEAD_SEP "\r\n"
#define HEAD_SEP_LEN strlen(HEAD_SEP)
#define BUFF_SIZE 1024
// #define USER 1
// 添加报头
void AddHeader(std::string& str)
{
// 先计算出长度
size_t len = str.size();
std::string strLen = Util::IntToStr(len);
// 再进行拼接
str = strLen + HEAD_SEP + str;
}
// 移除报头
void RemoveHeader(std::string& str, size_t len)
{
// len 表示有效载荷的长度
str = str.substr(str.size() - len);
}
// 读取数据
int ReadPackage(int sock, std::string& inBuff, std::string* package)
{
// 也可以使用 read 函数
char buff[BUFF_SIZE];
int n = recv(sock, buff, sizeof(buff) - 1, 0);
if(n < 0)
return -1; // 表示什么都没有读到
else if(n == 0)
return 0; // 需要继续读取
buff[n] = 0;
inBuff += buff;
// 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
int pos = inBuff.find(HEAD_SEP);
if(pos == std::string::npos)
return 0;
std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
if(inBuff.size() < packLen)
return 0;
*package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
inBuff.erase(0, packLen); // 从缓冲区中取走字符串
return Util::StrToInt(strLen);
}
class Request
{
public:
Request(int x = 0, int y = 0, char op = '+')
: _x(x), _y(y), _op(op)
{}
// 序列化
bool Serialization(std::string *outStr)
{
*outStr = ""; // 清空
#ifdef USER
std::string left = Util::IntToStr(_x);
std::string right = Util::IntToStr(_y);
*outStr = left + SEP + _op + SEP + right;
#else
// 使用 Json
Json::Value root;
root["x"] = _x;
root["op"] = _op;
root["y"] = _y;
Json::FastWriter writer;
*outStr = writer.write(root);
#endif
std::cout << "序列化完成: " << *outStr << std::endl << std::endl;
return true;
}
// 反序列化
bool Deserialization(const std::string &inStr)
{
#ifdef USER
std::vector<std::string> result;
Util::StringSplit(inStr, SEP, &result);
// 协议规定:只允许存在两个操作数和一个运算符
if(result.size() != 3)
return false;
// 规定:运算符只能为一个字符
if(result[1].size() != 1)
return false;
_x = Util::StrToInt(result[0]);
_y = Util::StrToInt(result[2]);
_op = result[1][0];
#else
// 使用Json
Json::Value root;
Json::Reader reader;
reader.parse(inStr, root);
_x = root["x"].asInt();
_op = root["op"].asInt();
_y = root["y"].asInt();
#endif
return true;
}
~Request()
{}
public:
int _x;
int _y;
char _op;
};
class Response
{
public:
Response(int result = 0, int code = 0)
:_result(result), _code(code)
{}
// 序列化
bool Serialization(std::string *outStr)
{
*outStr = ""; // 清空
#ifdef USER
std::string left = Util::IntToStr(_result);
std::string right = Util::IntToStr(_code);
*outStr = left + SEP + right;
#else
// 使用 Json
Json::Value root;
root["_result"] = _result;
root["_code"] = _code;
Json::FastWriter writer;
*outStr = writer.write(root);
#endif
std::cout << "序列化完成: " << *outStr << std::endl << std::endl;
return true;
}
// 反序列化
bool Deserialization(const std::string &inStr)
{
#ifdef USER
std::vector<std::string> result;
Util::StringSplit(inStr, SEP, &result);
if(result.size() != 2)
return false;
_result = Util::StrToInt(result[0]);
_code = Util::StrToInt(result[1]);
#else
// 使用Json
Json::Value root;
Json::Reader reader;
reader.parse(inStr, root);
_result = root["_result"].asInt();
_code = root["_code"].asInt();
#endif
return true;
}
~Response()
{}
public:
int _result; // 结果
int _code; // 错误码
};
}
注意: 因为现在使用了 Json
库,所以编译代码时需要指明其动态库
Makefile
自动编译脚本
.PHONY:all
all:CalcServer CalcClient
CalcServer:CalcServer.cc
g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
CalcClient:CalcClient.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp
.PHONY:clean
clean:
rm -rf CalcServer CalcClient
使用了 Json
库之后,序列化 后的数据会更加直观,当然也更易于使用
🌨️总结
编写网络服务需要注意以下几点
- 确定协议
- 如何进行序列化和反序列化
- 业务处理
相关文章推荐
网络编程『简易TCP网络程序』
网络编程『socket套接字 ‖ 简易UDP网络程序』
网络基础『发展 ‖ 协议 ‖ 传输 ‖ 地址』