当前位置:   article > 正文

计算机网络 -- 序列化与反序列化

计算机网络 -- 序列化与反序列化

一 协议的重要性

  我们都知道,在进行网络通信的过程中,通信的双方可以是不同的设备,不同的平台,不同的平台,比如说,手机用户和电脑用户进行通信,ios系统和安卓系统进行通信。

  自己的数据,如何保证对方端能安全接收到呢,假设linux为服务端,Windows为客户端,如何确保数据能被正确接收呢?

  就像我们中国人用中文进行交流一样,假设我们和某个外国人进行交流,母语的差异导致双方无法正常交流,信息也无法传达,于是我们只能打开手机上的 同声传译功能,可以将信息转换为对方能听懂的语言,最终实现交流。 

  同声传译 这个功能可以看做一种 协议(可以确保对端能理解自己传达的信息),协议 的出现解决了主机间的交流问题。

  也就是说,我们通过协议,规定了网络通信的双方,必须按照某种规则来对传输的内容进行解析或者是打包。

  对于网络来说,协议是双方通信的基石,如果没有协议,那么即使数据传输的再完美也无法使用,比如下面这个就是一个简单的 两正整数运算协议。

  • 协议要求:发送的数据必须由两个操作数(正整数)和一个运算符组成,并且必须遵循 x op y 这样的运算顺序。
  1. int x;
  2. int y;
  3. char op; // 运算符

  主机A在发送消息时需要将 操作数x、操作数y和运算符op 进行传递,只要主机A和主机B都遵循这个 协议,那么主机B在收到消息后一定清楚这是两个操作数和一个运算符

  现在的问题是如何传递?

方案一:将两个操作数和一个运算符拼接在一起直接传递
方案二:将两个操作数和一个运算符打包成一个结构体传递

  1. 方案一:直接拼接 xopy
  2. 方案二:封装成结构体
  3. struct Mssage{
  4. int x;
  5. int y;
  6. char op;
  7. };

  无论是方案一还是方案二都存在问题,前者是对端接收到消息后无法解析,后者则是存在平台兼容问题(不同平台的结构体内存规则可能不同,会导致读取数据出错)

  要想确保双方都能正确理解 协议,还需要进行 序列化与反序列化 处理。

二 .什么是序列化与反序列化?

  序列化是指 将一个或多个需要传递的数据,按照协议的格式,拼接为一条字节流数据,反序列化则是 将收到的数据按照格式解析。 

  可见,反序列化和序列化就是协议的一部分。

  比如主机A想通过 两正整数运算协议 给主机B发送这样的消息:

  1. //1+1
  2. int x = 1;
  3. int y = 1;
  4. char op = '+';

可以根据格式(这里使用 (空格))进行 序列化,序列化后的数据长这样:

  1. // 经过序列化后得到
  2. string msg = "1 + 1";

在经过网络传输后,主机B收到了消息,并根据 (空格)进行 反序列化,成功获取了主机A发送的信息。

  1. string msg = "1 + 1";
  2. // 经过反序列化后得到
  3. int x = 1;
  4. int y = 1;
  5. char op = '+';

   这里可以将需要传递的数据存储在结构体中,传递/接收 时将数据填充至类中,类中提供 序列化与反序列化 的相关接口即可。

  

  1. class Request
  2. {
  3. public:
  4. void Serialization(string* str)
  5. {}
  6. void Deserialization(const sting& str)
  7. {}
  8. public:
  9. int _x;
  10. int _y;
  11. char _op;
  12. };

  以上就是一个简单的 序列化和反序列化 流程,简单来说就是 协议 定制后不能直接使用,需要配合 序列化与反序列化 这样的工具理解,接下来我们就基于 两正整数运算协议 编写一个简易版的网络计算器,重点在于 理解协议、序列化和反序列化。

三 相关程序的实现框架

我们接下来要编写的程序从实现功能来看是十分简单的:

  客户端给出两个正整数和一个运算符,服务器计算出结果后返回

整体框架为:

客户端获取正整数与运算符 -> 将这些数据构建出 Request 对象 -> 序列化 -> 将结果(数据包)传递给服务器 ->

服务器进行反序列化 -> 获取数据 -> 根据数据进行运算 -> 将运算结果构建出 Response 对象(回响对象) -> 序列化 -> 将结果(数据包)传递给客户端 -> 客户端反序列后获取最终结果。

 既然这是一个基于网络的简易版计算器,必然离不开网络相关接口,在编写 服务器 与 客户端 的逻辑之前,需要先将 socket 接口进行封装,方面后续的使用。

四 程序实现

4.1 封装socket相关操作

注:当前实现的程序是基于 TCP 协议的

   简单回顾下,服务器需要 创建套接字、绑定IP地址和端口号、进入监听连接状态、等待客户端连接,至于客户端需要 创建套接字、由操作系统绑定IP地址和端口号、连接服务器,等客户端成功连上服务器后,双方就可以正常进行网络通信了。

    为了让客户端和服务器都能使用同一个头文件,我们可以把客户端和服务器需要的所有操作都进行实现,各自调用即可。

Sock.hpp 套接字相关接口头文件

  1. #pragma once
  2. #include "Log.hpp"
  3. #include "Err.hpp"
  4. #include <iostream>
  5. #include <string>
  6. #include <cstring>
  7. #include <cstdlib>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <netinet/in.h>
  11. #include <arpa/inet.h>
  12. #include <unistd.h>
  13. class Sock{
  14. const static int default_sock = -1;
  15. const static int default_backlog = 32;
  16. public:
  17. Sock()
  18. :_sock(default_sock)
  19. {}
  20. // 创建套接字
  21. void Socket(){
  22. _sock = socket(AF_INET, SOCK_STREAM, 0);
  23. if(_sock == -1){
  24. logMessage(Fatal, "Creater Socket Fail! [%d]->%s", errno, strerror(errno));
  25. exit(SOCKET_ERR);
  26. }
  27. logMessage(Debug, "Creater Socket Success");
  28. }
  29. // 绑定IP与端口号
  30. void Bind(const uint16_t& port){
  31. struct sockaddr_in local;
  32. memset(&local, 0, sizeof(local));
  33. local.sin_family = AF_INET;
  34. local.sin_port = htons(port);
  35. local.sin_addr.s_addr = INADDR_ANY;
  36. if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) == -1){
  37. logMessage(Fatal, "Bind Socket Fail! [%d]->%s", errno, strerror(errno));
  38. exit(BIND_ERR);
  39. }
  40. logMessage(Debug, "Bind Socket Success");
  41. }
  42. // 进入监听状态
  43. void Listen(){
  44. if(listen(_sock, default_backlog) == -1) {
  45. logMessage(Fatal, "Listen Socket Fail! [%d]->%s", errno, strerror(errno));
  46. exit(LISTEN_ERR);
  47. }
  48. }
  49. // 尝试处理连接请求
  50. int Accept(std::string* ip, uint16_t* port){
  51. struct sockaddr_in client;
  52. socklen_t len = sizeof(client);
  53. int retSock = accept(_sock, (struct sockaddr*)&client, &len)
  54. ;
  55. if(retSock < 0)
  56. logMessage(Warning, "Accept Fail! [%d]->%s", errno, strerror(errno));
  57. else{
  58. *ip = inet_ntoa(client.sin_addr);
  59. *port = ntohs(client.sin_port);
  60. logMessage(Debug, "Accept [%d -> %s:%d] Success", retSock, ip->c_str(), *port);
  61. }
  62. return retSock;
  63. }
  64. // 尝试进行连接
  65. int Connect(const std::string& ip, const uint16_t& port)
  66. {
  67. struct sockaddr_in server;
  68. memset(&server, 0, sizeof(server));
  69. server.sin_family = AF_INET;
  70. server.sin_port = htons(port);
  71. server.sin_addr.s_addr = inet_addr(ip.c_str());
  72. return connect(_sock, (struct sockaddr*)&server, sizeof(server));
  73. }
  74. // 获取sock
  75. int GetSock(){
  76. return _sock;
  77. }
  78. // 关闭sock
  79. void Close(){
  80. if(_sock != default_sock){
  81. close(_sock);
  82. }
  83. logMessage(Debug, "Close Sock Success");
  84. }
  85. ~Sock()
  86. {}
  87. private:
  88. int _sock; // 既可以是监听套接字,也可以是连接成功后返回的套接字
  89. };

Err.hpp 错误码头文件

  1. #pragma once
  2. enum{
  3. USAGE_ERR = 1,
  4. SOCKET_ERR,
  5. BIND_ERR,
  6. LISTEN_ERR,
  7. CONNECT_ERR,
  8. FORK_ERR,
  9. SETSID_ERR,
  10. CHDIR_ERR,
  11. OPEN_ERR,
  12. READ_ERR,
  13. };

Log.hpp 日志输出头文件 

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <vector>
  5. #include <cstdio>
  6. #include <time.h>
  7. #include <sys/types.h>
  8. #include <unistd.h>
  9. #include <stdarg.h>
  10. using namespace std;
  11. enum{
  12. Debug = 0,
  13. Info,
  14. Warning,
  15. Error,
  16. Fatal
  17. };
  18. static const string file_name = "./tcp_log";
  19. string getLevel(int level){
  20. vector<string> vs = {"<Debug>", "<Info>", "<Warning>", "<Error>", "<Fatal>", "<Unknown>"};
  21. //避免非法情况
  22. if(level < 0 || level >= vs.size() - 1){
  23. return vs[vs.size() - 1];
  24. }
  25. return vs[level];
  26. }
  27. string getTime(){
  28. time_t t = time(nullptr); //获取时间戳
  29. struct tm *st = localtime(&t); //获取时间相关的结构体
  30. char buff[128];
  31. 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);
  32. return buff;
  33. }
  34. //处理信息
  35. void logMessage(int level, const char* format, ...)
  36. {
  37. //日志格式:<日志等级> [时间] [PID] {消息体}
  38. string logmsg = getLevel(level); //获取日志等级
  39. logmsg += " " + getTime(); //获取时间
  40. logmsg += " [" + to_string(getpid()) + "]"; //获取进程PID
  41. //截获主体消息
  42. char msgbuff[1024];
  43. va_list p;
  44. va_start(p, format); //将 p 定位至 format 的起始位置
  45. vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
  46. va_end(p);
  47. logmsg += " {" + string(msgbuff) + "}"; //获取主体消息
  48. // 直接输出至屏幕上 方便进行测试
  49. cout << logmsg << endl;
  50. // //持久化。写入文件中
  51. // FILE* fp = fopen(file_name.c_str(), "a"); //以追加的方式写入
  52. // if(fp == nullptr) return; //不太可能出错
  53. // fprintf(fp, "%s\n", logmsg.c_str());
  54. // fflush(fp); //手动刷新一下
  55. // fclose(fp);
  56. // fp = nullptr;
  57. }

有了 Sock.hpp 头文件后,服务器/客户端就可以专注于逻辑编写了.

4.2 服务器

  首先准备好 TcpServer.hpp 头文件,其中实现了服务器初始化、服务器启动、序列化与反序列化等功能。

server.hpp 服务器头文件

  1. #pragma once
  2. #include "Sock.hpp"
  3. #include <iostream>
  4. #include <string>
  5. #include <pthread.h>
  6. namespace My_Server{
  7. class server;
  8. // 线程所需要的信息类
  9. class ThreadDate{
  10. public:
  11. ThreadDate(int& sock, std::string& ip, uint16_t& port, TcpServer* ptsvr)
  12. :_sock(sock)
  13. ,_ip(ip)
  14. ,_port(port)
  15. ,_ptsvr(ptsvr)
  16. {}
  17. ~ThreadDate()
  18. {}
  19. int _sock;
  20. std::string _ip;
  21. uint16_t _port;
  22. TcpServer* _ptsvr; // 回指指针
  23. };
  24. class server{
  25. const static uint16_t default_port = 8888;
  26. private:
  27. // 线程的执行函数
  28. static void* threadRoutine(void* args){
  29. // 线程剥离
  30. pthread_detach(pthread_self());
  31. ThreadDate* td = static_cast<ThreadDate*>(args);
  32. td->_ptsvr->ServiceIO(td->_sock, td->_ip, td->_port);
  33. delete td;
  34. return nullptr;
  35. }
  36. // 进行IO服务的函数
  37. void ServiceIO(const int& sock, const std::string ip, const uint16_t& port){
  38. // TODO
  39. }
  40. public:
  41. server(const uint16_t port = default_port)
  42. :_port(port)
  43. {}
  44. // 初始化服务器
  45. void Init(){
  46. _listen_sock.Socket();
  47. _listen_sock.Bind(_port);
  48. _listen_sock.Listen();
  49. }
  50. // 启动服务器
  51. void Start(){
  52. while(true){
  53. std::string ip;
  54. uint16_t port;
  55. int sock = _listen_sock.Accept(&ip, &port);
  56. if(sock == -1){
  57. continue;
  58. }
  59. // 创建子线程,执行业务处理
  60. pthread_t tid;
  61. ThreadDate* td = new ThreadDate(sock, ip, port, this);
  62. pthread_create(&tid, nullptr, threadRoutine, td);
  63. }
  64. }
  65. ~server(){
  66. _listen_sock.Close();
  67. }
  68. private:
  69. Sock _listen_sock; // 监听套接字
  70. uint16_t _port; // 服务器端口号
  71. };
  72. }

server.cc 简易计算器服务器源文件

  1. #include <iostream>
  2. #include <memory>
  3. #include "server.hpp"
  4. using namespace std;
  5. int main(){
  6. unique_ptr<My_Server::server> tsvr(new My_Server::server());
  7. tsvr->Init();
  8. tsvr->Start();
  9. return 0;
  10. }

Makefile 自动编译脚本

  1. .PHONY:all
  2. all:server
  3. # //client
  4. server:server.cc
  5. g++ -o $@ $^ -std=c++11 -lpthread
  6. # client:client.cc
  7. # g++ -o $@ $^ -std=c++11
  8. .PHONY:clean
  9. clean:
  10. rm -rf server
  11. # client

编译并运行程序,同时查看网络使用情况:
 

netstat -nltp

此时就证明前面写的代码已经没有问题了,接下来是填充 ServiceIO() 函数

4.3 序列化和反序列化

ServiceIO() 函数需要做这几件事

  • 读取数据
  • 反序列化
  • 业务处理
  • 序列化
  • 发送数据

除了 序列化和反序列化 外,其他步骤之前都已经见过了,所以我们先来看看如何实现 序列化与反序列化。

ServiceIO() 函数 — 位于 server.hpp 头文件中的 server 类中

  1. // 进行IO服务的函数
  2. void ServiceIO(const int& sock, const std::string ip, const uint16_t& port){
  3. // 1.读取数据
  4. // 2.反序列化
  5. // 3.业务处理
  6. // 4.序列化
  7. // 5.发送数据
  8. }

  需要明白我们当前的 协议 为 两正整数运算,分隔符为 (空格),客户端传给服务器两个操作数和一个运算符,服务器在计算完成后将结果返回,为了方便数据的读写,可以创建两个类:Request (客户端发送的一串待求的运算字符串)和 Response(服务器发送给客户端的结果),类中的成员需要遵循协议要求,并在其中支持 序列化与反序列化。

  但这两个函数,明显重复率有点高,我们的分隔符同样为一个空格,需要进行提取的,也都是数字,因此我们可以写个工具类,从而方便序列化和反序列化。

Util.hpp 工具类

  1. #pragma once
  2. #include <string>
  3. #include <vector>
  4. class Util{
  5. public:
  6. //数字转化为字符串
  7. static std::string IntToStr(int val){
  8. // 特殊处理
  9. if(val == 0)
  10. return "0";
  11. std::string str;
  12. while(val){
  13. str += (val % 10) + '0';
  14. val /= 10;
  15. }
  16. int left = 0;
  17. int right = str.size() - 1;
  18. while(left < right){
  19. std::swap(str[left++], str[right--]);
  20. }
  21. return str;
  22. }
  23. //字符串转化为数字
  24. static int StrToInt(const std::string& str) {
  25. int ret = 0;
  26. for(auto e : str){
  27. ret = (ret * 10) + (e - '0');
  28. }
  29. return ret;
  30. }
  31. // 将给定的字符串用分隔符进行分割
  32. static void StringSplit(const std::string& str, const std::string& sep, std::vector<std::string>* result){
  33. size_t left = 0;
  34. size_t right = 0;
  35. while(right < str.size()){
  36. // 每次right都查找到下一个分隔符
  37. right = str.find(sep, left);
  38. if(right == std::string::npos){
  39. break;
  40. }
  41. //left到right之间即为要提取的数字
  42. result->push_back(str.substr(left, right - left));
  43. //left指向分割符下一个数字
  44. left = right + sep.size();
  45. }
  46. //会漏掉最后一个数字
  47. if(left < str.size()){
  48. result->push_back(str.substr(left));
  49. }
  50. }
  51. };

  Protocol.hpp 协议处理相关头文件。

  1. #pragma once
  2. #include <string>
  3. #include"Util.hpp"
  4. #include<iostream>
  5. namespace My_protocol{
  6. // 协议的分隔符 这里我们自己设定为" "
  7. const char* SEP= " ";
  8. //分隔符长度
  9. const int SEP_LEN = strlen(SEP);
  10. //对运算进行序列和反序列化
  11. class Request{
  12. public:
  13. Request(int x = 0, int y = 0, char op = '+')
  14. : _x(x), _y(y), _op(op)
  15. {}
  16. // 序列化
  17. bool Serialization(std::string *outStr){
  18. *outStr = ""; // 清空
  19. std::string left = Util::IntToStr(_x);
  20. std::string right = Util::IntToStr(_y);
  21. *outStr = left + SEP + _op + SEP + right;
  22. return true;
  23. }
  24. // 反序列化
  25. bool Deserialization(const std::string &inStr){
  26. std::vector<std::string> result;
  27. Util::StringSplit(inStr, SEP, &result);
  28. // 协议规定:只允许存在两个操作数和一个运算符
  29. if(result.size() != 3){
  30. return false;
  31. }
  32. // 规定:运算符只能为一个字符
  33. if(result[1].size() != 1){
  34. return false;
  35. }
  36. _x = Util::StrToInt(result[0]);
  37. _y = Util::StrToInt(result[2]);
  38. _op = result[1][0];
  39. return true;
  40. }
  41. ~Request()
  42. {}
  43. public:
  44. int _x;
  45. int _y;
  46. char _op;
  47. };
  48. //业务处理函数
  49. class Response
  50. {
  51. public:
  52. Response(int result = 0, int code = 0)
  53. :_result(result), _code(code)
  54. {}
  55. // 序列化
  56. bool Serialization(std::string *outStr) {
  57. *outStr = ""; // 清空
  58. std::string left = Util::IntToStr(_result);
  59. std::string right = Util::IntToStr(_code);
  60. *outStr = left + SEP + right;
  61. return true;
  62. }
  63. // 提取结果和错误码
  64. bool Deserialization(const std::string &inStr){
  65. std::vector<std::string> result;
  66. Util::StringSplit(inStr, SEP, &result);
  67. if(result.size() != 2)
  68. return false;
  69. _result = Util::StrToInt(result[0]);
  70. _code = Util::StrToInt(result[1]);
  71. return true;
  72. }
  73. ~Response()
  74. {}
  75. public:
  76. int _result; // 结果
  77. int _code; // 错误码
  78. };
  79. }

4.4 业务的实际处理

  server 中的业务处理函数由 CalcServer.cc 传递,规定业务处理函数的类型为 void(Request&, Response*)

Calculate() 函数 — 位于server.cc

  1. #include "server.hpp"
  2. #include "Protocol.hpp"
  3. #include <iostream>
  4. #include <memory>
  5. #include <functional>
  6. #include <unordered_map>
  7. using namespace std;
  8. void Calculate(My_protocol::Request& req, My_protocol::Response* resp){
  9. // 这里只是简单的计算而已
  10. int x = req._x;
  11. int y = req._y;
  12. char op = req._op;
  13. unordered_map<char, function<int()>> hash =
  14. {
  15. {'+', [&](){ return x + y; }},
  16. {'-', [&](){ return x - y; }},
  17. {'*', [&](){ return x * y; }},
  18. {'/', [&]()
  19. {
  20. if(y == 0)
  21. {
  22. resp->_code = 1;
  23. return 0;
  24. }
  25. return x / y;
  26. }
  27. },
  28. {'%', [&]()
  29. {
  30. if(y == 0)
  31. {
  32. resp->_code = 2;
  33. return 0;
  34. }
  35. return x % y;
  36. }
  37. }
  38. };
  39. if(hash.count(op) == 0)
  40. resp->_code = 3;
  41. else
  42. resp->_result = hash[op]();
  43. }
  44. int main(){
  45. unique_ptr<My_Server::server> tsvr(new My_Server::server(Calculate));
  46. tsvr->Init();
  47. tsvr->Start();
  48. return 0;
  49. }

既然 CalcServer 中传入了 Calculate() 函数对象,server 类中就得接收并使用,也就是业务处理.

server.hpp 头文件

  1. #pragma once
  2. #include "Sock.hpp"
  3. #include <iostream>
  4. #include <string>
  5. #include <pthread.h>
  6. #include "Protocol.hpp"
  7. #include<functional>
  8. namespace My_Server
  9. {
  10. class server;
  11. // 线程所需要的信息类
  12. class ThreadDate
  13. {
  14. public:
  15. ThreadDate(int &sock, std::string &ip, uint16_t &port, server *ptsvr)
  16. : _sock(sock), _ip(ip), _port(port), _ptsvr(ptsvr)
  17. {
  18. }
  19. ~ThreadDate()
  20. {
  21. }
  22. int _sock;
  23. std::string _ip;
  24. uint16_t _port;
  25. server *_ptsvr; // 回指指针
  26. };
  27. using func_t = std::function<void(My_protocol::Request&, My_protocol::Response*)>;
  28. class server
  29. {
  30. const static uint16_t default_port = 8088;
  31. private:
  32. // 线程的执行函数
  33. static void *threadRoutine(void *args)
  34. {
  35. // 线程剥离
  36. pthread_detach(pthread_self());
  37. ThreadDate *td = static_cast<ThreadDate *>(args);
  38. td->_ptsvr->ServiceIO(td->_sock, td->_ip, td->_port);
  39. delete td;
  40. return nullptr;
  41. }
  42. // 进行IO服务的函数
  43. void ServiceIO(const int &sock, const std::string ip, const uint16_t &port){
  44. while (true){
  45. // 1.读取数据
  46. std::string package; // 假设这是已经读取到的数据包,格式为 "1 + 1"
  47. // 2.反序列化
  48. My_protocol::Request req;
  49. if (req.Deserialization(package) == false){
  50. logMessage(Warning, "Deserialization fail!");
  51. continue;
  52. }
  53. // 3.业务处理
  54. // TODO
  55. My_protocol::Response resp; // 业务处理完成后得到的响应对象
  56. _func(req, &resp);
  57. // 4.序列化
  58. std::string sendMsg;
  59. resp.Serialization(&sendMsg);
  60. std::cout<<sendMsg<<std::endl;
  61. // 5.发送数据
  62. }
  63. }
  64. public:
  65. server(func_t fun,const uint16_t port = default_port)
  66. : _port(port)
  67. ,_func(fun)
  68. {}
  69. // 初始化服务器
  70. void Init(){
  71. _listen_sock.Socket();
  72. _listen_sock.Bind(_port);
  73. _listen_sock.Listen();
  74. }
  75. // 启动服务器
  76. void Start(){
  77. while (true){
  78. std::string ip;
  79. uint16_t port;
  80. int sock = _listen_sock.Accept(&ip, &port);
  81. if (sock == -1)
  82. {
  83. continue;
  84. }
  85. // 创建子线程,执行业务处理
  86. pthread_t tid;
  87. ThreadDate *td = new ThreadDate(sock, ip, port, this);
  88. pthread_create(&tid, nullptr, threadRoutine, td);
  89. }
  90. }
  91. ~server(){
  92. _listen_sock.Close();
  93. }
  94. private:
  95. Sock _listen_sock; // 监听套接字
  96. uint16_t _port; // 服务器端口号
  97. func_t _func; // 上层传入的业务处理函数
  98. };
  99. }

  这就做好业务处理了,ServiceIO() 函数已经完成了 50% 的工作,接下来的重点是如何读取和发送数据?

  TCP 协议是面向字节流的,这也就意味着数据在传输过程中可能会因为网络问题,分为多次传输,这也就意味着我们可能无法将其一次性读取完毕,需要制定一个策略,来确保数据全部递达.

4.5 报头处理

如何确认自己已经读取完了所以数据?答案是提前知道目标数据的长度,边读取边判断

数据在发送时,是需要在前面添加 长度 这个信息的,通常将其称为 报头,而待读取的数据称为 有效载荷,报头 和 有效载荷 的关系类似于快递单与包裹的关系,前者是后者成功递达的保障

最简单的 报头 内容就是 有效载荷 的长度

问题来了,如何区分 报头 与 有效载荷 呢?

  • 当前可以确定的是,我们的报头中只包含了长度这个信息
  • 可以通过添加特殊字符,如 \r\n 的方式进行区分
  • 后续无论有效载荷变成什么内容,都不影响我们通过报头进行读取

报头处理属于协议的一部分

所以在正式读写数据前,需要解决 报头 的问题(收到数据后移除报头,发送数据前添加报头)

ReadPackage() 读取函数 — 位于 Protocol.hpp 头文件

在 Protocol.hpp 中完成报头的添加和移除

  1. #define HEAD_SEP "\r\n"
  2. #define HEAD_SEP_LEN strlen(HEAD_SEP)
  3. // 添加报头
  4. void AddHeader(std::string& str){
  5. // 先计算出长度
  6. size_t len = str.size();
  7. std::string strLen = Util::IntToStr(len);
  8. // 再进行拼接
  9. str = strLen + HEAD_SEP + str;
  10. }
  11. // 移除报头
  12. void RemoveHeader(std::string& str, size_t len){
  13. // len 表示有效载荷的长度
  14. str = str.substr(str.size() - len);
  15. }

报头+有效载荷需要通过 read() 或者 recv() 函数从网络中读取,并且需要边读取边判断。

ReadPackage() 读取函数 — 位于 Protocol.hpp 头文件

  1. #define BUFF_SIZE 1024
  2. // 读取数据
  3. int ReadPackage(int sock, std::string& inBuff, std::string* package){
  4. // 也可以使用 read 函数
  5. char buff[BUFF_SIZE];
  6. int n = recv(sock, buff, sizeof(buff) - 1, 0);
  7. if(n < 0)
  8. return -1; // 表示读取失败
  9. else if(n == 0)
  10. return 0; // 需要继续读取
  11. buff[n] = '\0';
  12. inBuff += buff;
  13. // 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
  14. int pos = inBuff.find(HEAD_SEP);
  15. if(pos == std::string::npos)
  16. return -1;
  17. std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
  18. int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
  19. if(inBuff.size() < packLen)
  20. return -1;
  21. *package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
  22. inBuff.erase(0, packLen); // 从缓冲区中取走字符串
  23. return Util::StrToInt(strLen);
  24. }

完整代码:
 

  1. #pragma once
  2. #include <string>
  3. #include"Util.hpp"
  4. #include<iostream>
  5. namespace My_protocol{
  6. #define HEAD_SEP "\r\n"
  7. #define HEAD_SEP_LEN strlen(HEAD_SEP)
  8. // 添加报头
  9. void AddHeader(std::string& str){
  10. // 先计算出长度
  11. size_t len = str.size();
  12. std::string strLen = Util::IntToStr(len);
  13. // 再进行拼接
  14. str = strLen + HEAD_SEP + str;
  15. }
  16. // 移除报头
  17. void RemoveHeader(std::string& str, size_t len){
  18. // len 表示有效载荷的长度
  19. str = str.substr(str.size() - len);
  20. }
  21. #define BUFF_SIZE 1024
  22. // 读取数据
  23. int ReadPackage(int sock, std::string& inBuff, std::string* package){
  24. // 也可以使用 read 函数
  25. char buff[BUFF_SIZE];
  26. int n = recv(sock, buff, sizeof(buff) - 1, 0);
  27. if(n < 0)
  28. return -1; // 表示读取失败
  29. else if(n == 0)
  30. return 0; // 需要继续读取
  31. buff[n] = '\0';
  32. inBuff += buff;
  33. // 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
  34. int pos = inBuff.find(HEAD_SEP);
  35. if(pos == std::string::npos)
  36. return -1;
  37. std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
  38. int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
  39. if(inBuff.size() < packLen)
  40. return -1;
  41. *package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
  42. inBuff.erase(0, packLen); // 从缓冲区中取走字符串
  43. return Util::StrToInt(strLen);
  44. }
  45. // 协议的分隔符 这里我们自己设定为" "
  46. const char* SEP= " ";
  47. //分隔符长度
  48. const int SEP_LEN = strlen(SEP);
  49. //对运算进行序列和反序列化
  50. class Request{
  51. public:
  52. Request(int x = 0, int y = 0, char op = '+')
  53. : _x(x), _y(y), _op(op)
  54. {}
  55. // 序列化
  56. bool Serialization(std::string *outStr){
  57. *outStr = ""; // 清空
  58. std::string left = Util::IntToStr(_x);
  59. std::string right = Util::IntToStr(_y);
  60. *outStr = left + SEP + _op + SEP + right;
  61. return true;
  62. }
  63. // 反序列化
  64. bool Deserialization(const std::string &inStr){
  65. std::vector<std::string> result;
  66. Util::StringSplit(inStr, SEP, &result);
  67. // 协议规定:只允许存在两个操作数和一个运算符
  68. if(result.size() != 3){
  69. return false;
  70. }
  71. // 规定:运算符只能为一个字符
  72. if(result[1].size() != 1){
  73. return false;
  74. }
  75. _x = Util::StrToInt(result[0]);
  76. _y = Util::StrToInt(result[2]);
  77. _op = result[1][0];
  78. return true;
  79. }
  80. ~Request()
  81. {}
  82. public:
  83. int _x;
  84. int _y;
  85. char _op;
  86. };
  87. //业务处理函数
  88. class Response
  89. {
  90. public:
  91. Response(int result = 0, int code = 0)
  92. :_result(result), _code(code)
  93. {}
  94. // 序列化
  95. bool Serialization(std::string *outStr) {
  96. *outStr = ""; // 清空
  97. std::string left = Util::IntToStr(_result);
  98. std::string right = Util::IntToStr(_code);
  99. *outStr = left + SEP + right;
  100. return true;
  101. }
  102. // 提取结果和错误码
  103. bool Deserialization(const std::string &inStr){
  104. std::vector<std::string> result;
  105. Util::StringSplit(inStr, SEP, &result);
  106. if(result.size() != 2)
  107. return false;
  108. _result = Util::StrToInt(result[0]);
  109. _code = Util::StrToInt(result[1]);
  110. return true;
  111. }
  112. ~Response()
  113. {}
  114. public:
  115. int _result; // 结果
  116. int _code; // 错误码
  117. };
  118. }

此时对于 ServiceIO() 函数来说,核心函数都已经准备好了,只差拼装了。

ServiceIO() 函数 — 位于 server.hpp 头文件中的server 类中

  1. // 进行IO服务的函数
  2. void ServiceIO(const int &sock, const std::string ip, const uint16_t &port)
  3. {
  4. std::string inBuff;
  5. while (true)
  6. {
  7. // 1.读取数据
  8. std::string package; // 假设这是已经读取到的数据包,格式为 "5\r\n1 + 1"
  9. int len = My_protocol::ReadPackage(sock, inBuff, &package);
  10. if (len < 0)
  11. break;
  12. else if (len == 0)
  13. continue;
  14. // 2.移除报头
  15. My_protocol::RemoveHeader(package, len);
  16. // 3.反序列化
  17. My_protocol::Request req;
  18. if (req.Deserialization(package) == false)
  19. {
  20. logMessage(Warning, "Deserialization fail!");
  21. continue;
  22. }
  23. // 4.业务处理
  24. My_protocol::Response resp; // 业务处理完成后得到的响应对象
  25. _func(req, &resp);
  26. // 5.序列化
  27. std::string sendMsg;
  28. resp.Serialization(&sendMsg);
  29. cout << sendMsg << endl;
  30. // 6.添加报头
  31. My_protocol::AddHeader(sendMsg);
  32. // 7.发送数据
  33. send(sock, sendMsg.c_str(), sendMsg.size(), 0);
  34. }
  35. }

至此服务器编写完毕,接下来就是进行客户端的编写了.

4.5 客户端

Client.hpp 客户端头文件

  1. #pragma once
  2. #include "Sock.hpp"
  3. #include "Protocol.hpp"
  4. #include "Log.hpp"
  5. #include "Err.hpp"
  6. #include <iostream>
  7. #include <string>
  8. #include <unistd.h>
  9. namespace My_Client
  10. {
  11. class client
  12. {
  13. public:
  14. client(const std::string& ip, const uint16_t& port)
  15. :_server_ip(ip)
  16. ,_server_port(port)
  17. {}
  18. void Init()
  19. {
  20. _sock.Socket();
  21. }
  22. void Start()
  23. {
  24. int i = 5;
  25. while(i > 0)
  26. {
  27. if(_sock.Connect(_server_ip, _server_port) != -1)
  28. break;
  29. logMessage(Warning, "Connect Server Fail! %d", i--);
  30. sleep(1);
  31. }
  32. if(i == 0)
  33. {
  34. logMessage(Fatal, "Connect Server Fail!");
  35. exit(CONNECT_ERR);
  36. }
  37. // 执行读写函数
  38. ServiceIO();
  39. }
  40. void ServiceIO()
  41. {
  42. while(true)
  43. {
  44. std::string str;
  45. std::cout << "Please Enter:> ";
  46. std::getline(std::cin, str);
  47. // 1.判断是否需要退出
  48. if(str == "quit")
  49. break;
  50. // 2.分割输入的字符串
  51. My_protocol::Request req;
  52. [&]()
  53. {
  54. std::string ops = "+-*/%";
  55. int pos = 0;
  56. for(auto e : ops)
  57. {
  58. pos = str.find(e);
  59. if(pos != std::string::npos)
  60. break;
  61. }
  62. req._x = Util::StrToInt(str.substr(0, pos));
  63. req._y = Util::StrToInt(str.substr(pos + 1));
  64. req._op = str[pos];
  65. }();
  66. // 3.序列化
  67. std::string sendMsg;
  68. req.Serialization(&sendMsg);
  69. // 4.添加报头
  70. My_protocol::AddHeader(sendMsg);
  71. // 5.发送数据
  72. send(_sock.GetSock(), sendMsg.c_str(), sendMsg.size(), 0);
  73. // 6.获取数据
  74. std::string inBuff;
  75. std::string package;
  76. int len = 0;
  77. while(true)
  78. {
  79. len = My_protocol::ReadPackage(_sock.GetSock(), inBuff, &package);
  80. if(len < 0)
  81. exit(READ_ERR);
  82. else if(len > 0)
  83. break;
  84. }
  85. // 7.移除报头
  86. My_protocol::RemoveHeader(package, len);
  87. // 8.反序列化
  88. My_protocol::Response resp;
  89. if(resp.Deserialization(package) == false)
  90. {
  91. logMessage(Warning, "Deserialization fail!");
  92. continue;
  93. }
  94. // 9.获取结果
  95. std::cout << "The Result: " << resp._result << " " << resp._code << endl;
  96. }
  97. }
  98. ~client()
  99. {
  100. _sock.Close();
  101. }
  102. private:
  103. Sock _sock;
  104. std::string _server_ip;
  105. uint16_t _server_port;
  106. };
  107. }

client.cc 客户端源文件

  1. #include "client.hpp"
  2. #include <iostream>
  3. #include <memory>
  4. using namespace std;
  5. int main()
  6. {
  7. unique_ptr<My_Client::client> tclt(new My_Client::client("127.0.0.1", 8088));
  8. tclt->Init();
  9. tclt->Start();
  10. return 0;
  11. }

五 测试

六 使用库

事实上,序列化与反序列化 这种工作轮不到我们来做,因为有更好更强的库,比如 JsonXMLProtobuf 等

比如我们就可以使用 Json 来修改程序

首先需要安装 json-cpp 库,如果是 CentOS7 操作系统的可以直接使用下面这条命令安装

yum install -y jsoncpp-devel

安装完成后,可以引入头文件 <jsoncpp/json/json.h>

然后就可以在 Protocol.hpp 头文件中进行修改了,如果想保留原来自己实现的 序列化与反序列化 代码,可以利用 条件编译 进行区分

Protocol.hpp 协议相关头文件

  1. #pragma once
  2. #include "Util.hpp"
  3. #include <jsoncpp/json/json.h>
  4. #include <string>
  5. #include <vector>
  6. #include <cstring>
  7. #include <sys/types.h>
  8. #include <sys/socket.h>
  9. namespace My_protocol
  10. {
  11. // 协议的分隔符
  12. #define SEP " "
  13. #define SEP_LEN strlen(SEP)
  14. #define HEAD_SEP "\r\n"
  15. #define HEAD_SEP_LEN strlen(HEAD_SEP)
  16. #define BUFF_SIZE 1024
  17. // #define USER 1
  18. // 添加报头
  19. void AddHeader(std::string& str)
  20. {
  21. // 先计算出长度
  22. size_t len = str.size();
  23. std::string strLen = Util::IntToStr(len);
  24. // 再进行拼接
  25. str = strLen + HEAD_SEP + str;
  26. }
  27. // 移除报头
  28. void RemoveHeader(std::string& str, size_t len)
  29. {
  30. // len 表示有效载荷的长度
  31. str = str.substr(str.size() - len);
  32. }
  33. // 读取数据
  34. int ReadPackage(int sock, std::string& inBuff, std::string* package)
  35. {
  36. // 也可以使用 read 函数
  37. char buff[BUFF_SIZE];
  38. int n = recv(sock, buff, sizeof(buff) - 1, 0);
  39. if(n < 0)
  40. return -1; // 表示什么都没有读到
  41. else if(n == 0)
  42. return 0; // 需要继续读取
  43. buff[n] = 0;
  44. inBuff += buff;
  45. // 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
  46. int pos = inBuff.find(HEAD_SEP);
  47. if(pos == std::string::npos)
  48. return 0;
  49. std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
  50. int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
  51. if(inBuff.size() < packLen)
  52. return 0;
  53. *package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
  54. inBuff.erase(0, packLen); // 从缓冲区中取走字符串
  55. return Util::StrToInt(strLen);
  56. }
  57. class Request
  58. {
  59. public:
  60. Request(int x = 0, int y = 0, char op = '+')
  61. : _x(x), _y(y), _op(op)
  62. {}
  63. // 序列化
  64. bool Serialization(std::string *outStr)
  65. {
  66. *outStr = ""; // 清空
  67. #ifdef USER
  68. std::string left = Util::IntToStr(_x);
  69. std::string right = Util::IntToStr(_y);
  70. *outStr = left + SEP + _op + SEP + right;
  71. #else
  72. // 使用 Json
  73. Json::Value root;
  74. root["x"] = _x;
  75. root["op"] = _op;
  76. root["y"] = _y;
  77. Json::FastWriter writer;
  78. *outStr = writer.write(root);
  79. #endif
  80. std::cout << "序列化完成: " << *outStr << std::endl << std::endl;
  81. return true;
  82. }
  83. // 反序列化
  84. bool Deserialization(const std::string &inStr)
  85. {
  86. #ifdef USER
  87. std::vector<std::string> result;
  88. Util::StringSplit(inStr, SEP, &result);
  89. // 协议规定:只允许存在两个操作数和一个运算符
  90. if(result.size() != 3)
  91. return false;
  92. // 规定:运算符只能为一个字符
  93. if(result[1].size() != 1)
  94. return false;
  95. _x = Util::StrToInt(result[0]);
  96. _y = Util::StrToInt(result[2]);
  97. _op = result[1][0];
  98. #else
  99. // 使用Json
  100. Json::Value root;
  101. Json::Reader reader;
  102. reader.parse(inStr, root);
  103. _x = root["x"].asInt();
  104. _op = root["op"].asInt();
  105. _y = root["y"].asInt();
  106. #endif
  107. return true;
  108. }
  109. ~Request()
  110. {}
  111. public:
  112. int _x;
  113. int _y;
  114. char _op;
  115. };
  116. class Response
  117. {
  118. public:
  119. Response(int result = 0, int code = 0)
  120. :_result(result), _code(code)
  121. {}
  122. // 序列化
  123. bool Serialization(std::string *outStr)
  124. {
  125. *outStr = ""; // 清空
  126. #ifdef USER
  127. std::string left = Util::IntToStr(_result);
  128. std::string right = Util::IntToStr(_code);
  129. *outStr = left + SEP + right;
  130. #else
  131. // 使用 Json
  132. Json::Value root;
  133. root["_result"] = _result;
  134. root["_code"] = _code;
  135. Json::FastWriter writer;
  136. *outStr = writer.write(root);
  137. #endif
  138. std::cout << "序列化完成: " << *outStr << std::endl << std::endl;
  139. return true;
  140. }
  141. // 反序列化
  142. bool Deserialization(const std::string &inStr)
  143. {
  144. #ifdef USER
  145. std::vector<std::string> result;
  146. Util::StringSplit(inStr, SEP, &result);
  147. if(result.size() != 2)
  148. return false;
  149. _result = Util::StrToInt(result[0]);
  150. _code = Util::StrToInt(result[1]);
  151. #else
  152. // 使用Json
  153. Json::Value root;
  154. Json::Reader reader;
  155. reader.parse(inStr, root);
  156. _result = root["_result"].asInt();
  157. _code = root["_code"].asInt();
  158. #endif
  159. return true;
  160. }
  161. ~Response()
  162. {}
  163. public:
  164. int _result; // 结果
  165. int _code; // 错误码
  166. };
  167. }

注意: 因为现在使用了 Json 库,所以编译代码时需要指明其动态库

  1. .PHONY:all
  2. all:server client
  3. server:server.cc
  4. g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
  5. client:client.cc
  6. g++ -o $@ $^ -std=c++11 -ljsoncpp
  7. .PHONY:clean
  8. clean:
  9. rm -rf server client

使用了 Json 库之后,序列化 后的数据会更加直观,当然也更易于使用

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/594776
推荐阅读
相关标签
  

闽ICP备14008679号