本篇总结的是对于协议的定制和序列化反序列化的内容
协议定制
在之前的内容中已经有了这样的概念,协议就是一种约定,说白了就是双方定好的一种结构体对象,然后可以定义一个对象带上协议的报头,但是认知也只是停留在了这里,而现在对于UDP和TCP协议实现网络通信有了一个基本的认知,双方已经可以实现了通信的基本原理,那么该如何进行协议的定制呢?
协议的定制是有很多种的,并不会拘泥于某种特定的协议定制,这里只是提供一种比较基础的协议方案
对于协议的基本认知是说,在进行TCP和UDP的通信的时候,尤其是TCP,把内容发过去后用文件来进行读取,因为TCP是面向字节流的,这是最基础的认知,但是在当时没有谈及的一个问题是,如何做到保证读取的信息是完整的报文呢?在进行网络发送的时候很有可能会出现发送的信息是残缺的或者其他,这些是如何保证的呢?如何能够保证协议解析是完整清晰的呢?其实这些就都是问题,因此本篇主要就是借助协议定制这样的思想,来实现一个基本的协议,那未来也会通过别人已经定义好的协议,例如http协议和https协议来进行对应的学习
TCP是面向字节流的,所以会出现的问题是对于关键字的读取,读取到多少算是结束读取了呢?假设客户端发送了一个你好的信息到服务端,服务端读取到了你好,这算是完成了一次基本的通信,能够完成通信的前提是因为在进行通信的时候数据量并不大,如果数据量到达一定程度,就会出现数据传输异常的情况
对于数据传输异常其实并不陌生,在进行管道通信的内容模块中,就曾经出现过数据传输异常的问题,当向管道中进行读取数据的时候,如果此时管道是满的,就会全部读取出来,那么如何对于读取的这些数据进行区分?如何对于数据进行划分?这些都是将要面临的问题,那么今天的第二个话题,叫做序列化和反序列化,其实就是可以对于这个内容来进行对应的解决
所以为了解决数据通信的问题,就要首先选择一种协议定制的方法,比方说今天定制了一个协议,这个协议的内容是双方使用的是固定大小的通信方式,比如一次性只能传输64个字节,多出来的都不进行传输,那么为了让报文能够维持下来,即使当前读取的数据并不满足64字节,也会想办法写满,这样在进行读取的时候就可以按照64个字节为一个单位进行读取了,这样就能保证读取到的数据是一个完整的数据,这是一种最为初步的设想
序列化和反序列化
那在实际的设计中,一种初步的设计是把要传递的内容放到一个结构体中,然后直接把这个结构体传递给对方,这样对方在接受到这个结构体后,就可以读取结构体中对应的数据,这样的设计是有问题的,一个最明显的问题是结构体的大小不好控制,结构体的大小是受内存对齐的因素影响的,这带来的问题就是,如果存在内存对齐差异的操作系统,那么实际计算出的结构体的大小是不固定的,这势必会带来问题,因此一种可行的方案是,在进行网络通信的时候,把整个结构体当中的数据都做成一个一个的字符串,这个过程就叫做是序列化,那对方在收到了对应的序列化信息后,再把序列化的信息解析出来,换成对应的结构体的数据,这样的过程就叫做是反序列化,这样就能保证在不同的操作系统中也能做到数据通信的原理
那为了方便理解这个过程,这里通过实现一个网络版本的计算器来理解上面的这些原理
首先使用一些提前封装好的代码,这些代码主要是包含有网络套接字的创建,日志的显示,网络通信的创建等等,是隶属于之前的内容:
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level);
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
Log lg;
网路套接字的创建
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
enum
{
SocketErr = 2,
BindErr,
ListenErr,
};
const int backlog = 10;
class Sock
{
public:
Sock()
{
}
~Sock()
{
}
public:
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
}
void Bind(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(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
lg(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
void Close()
{
close(sockfd_);
}
int Fd()
{
return sockfd_;
}
private:
int sockfd_;
};
那下一步就要对于计算器这个内容进行一定的封装,例如我们可以定义下面的这个结构体:
class Request
{
public:
Request(int data1, int data2, char oper) : x(data1), y(data2), op(oper)
{
}
Request()
{}
public:
int x;
int y;
char op;
};
在向服务端发送请求的时候,可以使用上述这样的内容来进行编写请求,这样可以做到的一点是如果正确传递后,服务器可以正确的进行接收信息
那么下一步要设计的一种解决方案是,如何对于上述结构体进行序列化,这里我采用的一种设计方法是这样的:
例如对于x op y这个操作来说,可以将它序列化为这样的一个字符串,最前面表示的是字符串的长度,之后利用\n
来进行充当分割符,这样就充当了是一组数据
"len"\n"x op y"\nXXXXXX
那对于反序列化来说,其主要原理就是要把上述的字符串来解析出来,解析为原来的数据形式
"len"\n"x op y"\nXXXXXX -> x op y
序列化和反序列化是服务端和客户端都需要做的模块,客户端把信息传过去,服务端解析后再传回来,客户端再对信息进行解析,这就是基本的执行逻辑流程
那么下面对于计算的过程进行封装
class Response
{
public:
Response(int res, int c) : result(res), code(c)
{
}
Response()
{
}
public:
int result;
int code;
};
所以现在就要实现一下序列化和反序列化的执行逻辑,其实基本的逻辑就是上述提供的方案,那么这里直接显示出对应代码
#pragma once
#include <iostream>
#include <string>
const std::string blank_space_sep = " ";
const std::string protocol_sep = "\n";
std::string Encode(std::string &content)
{
std::string package = std::to_string(content.size());
package += protocol_sep;
package += content;
package += protocol_sep;
return package;
}
bool Decode(std::string &package, std::string *content)
{
std::size_t pos = package.find(protocol_sep);
if (pos == std::string::npos)
return false;
std::string len_str = package.substr(0, pos);
std::size_t len = std::stoi(len_str);
std::size_t total_len = len_str.size() + len + 2;
if (package.size() < total_len)
return false;
*content = package.substr(pos + 1, len);
package.erase(0, total_len);
return true;
}
class Request
{
public:
Request(int data1, int data2, char oper) : x(data1), y(data2), op(oper)
{
}
Request()
{
}
public:
bool Serialize(std::string *out)
{
std::string s = std::to_string(x);
s += blank_space_sep;
s += op;
s += blank_space_sep;
s += std::to_string(y);
*out = s;
return true;
}
bool Deserialize(const std::string &in)
{
std::size_t left = in.find(blank_space_sep);
if (left == std::string::npos)
return false;
std::string part_x = in.substr(0, left);
std::size_t right = in.rfind(blank_space_sep);
if (right == std::string::npos)
return false;
std::string part_y = in.substr(right + 1);
if (left + 2 != right)
return false;
op = in[left + 1];
x = std::stoi(part_x);
y = std::stoi(part_y);
return true;
}
void DebugPrint()
{
std::cout << "新请求构建完成: " << x << op << y << "=?" << std::endl;
}
public:
int x;
int y;
char op;
};
class Response
{
public:
Response(int res, int c) : result(res), code(c)
{
}
Response()
{
}