前言
那么这里博主先安利一些干货满满的专栏了!
首先是博主的高质量博客的汇总,这个专栏里面的博客,都是博主最最用心写的一部分,干货满满,希望对大家有帮助。
高质量干货博客汇总https://blog.csdn.net/yu_cblog/category_12379430.html?spm=1001.2014.3001.5482
该项目GITHUB地址
网络计算器-序列化和反序列化-协议定制https://github.com/Yufccode/BitCode/tree/main/Linux/%E4%BB%A3%E7%A0%81/0226%E7%BD%91%E7%BB%9C%E8%AE%A1%E7%AE%97%E5%99%A8-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%8D%8F%E8%AE%AE%E5%AE%9A%E5%88%B6
什么是字节流和数据报
在计算机网络理论中,什么是字节流,什么是数据报,他们和TCP,UDP的关系是什么,字节流和数据报的关系是什么?
字节流是一种连续的、无边界的数据流。它将数据视为一个连续的字节序列,没有明确的分组或边界。在字节流中,数据按照发送的顺序进行传输,接收方按照相同的顺序接收数据。字节流是一种面向连接的传输方式,它提供可靠的、有序的、基于错误检测和纠正的数据传输。TCP(传输控制协议)是一个使用字节流传输的协议。
数据报是一种离散的、有边界的数据传输方式。它将数据划分为固定大小的数据包,每个数据包都有自己的头部信息(通常包含源地址、目标地址、序列号等),并独立发送。每个数据包在网络中独立传输,可能沿不同的路径到达接收方。数据报传输通常是无连接的,不保证可靠性和有序性。UDP(用户数据报协议)是一个使用数据报传输的协议。
关于它们和TCP、UDP的关系:
- TCP使用字节流传输方式,通过TCP连接来提供可靠的、有序的、面向连接的数据传输。TCP使用序号和确认机制来确保数据的可靠性和有序性。
- UDP使用数据报传输方式,提供了无连接、不可靠的数据传输。UDP适用于对实时性要求较高,但对数据可靠性和顺序性要求不高的应用场景。
字节流和数据报之间没有直接的关系,它们是不同的传输方式。TCP使用字节流传输方式,而UDP使用数据报传输方式。这两种传输方式适用于不同的网络应用场景,具体选择取决于应用的要求和设计。
粘包问题
粘包问题是在网络通信中常见的一个现象,指的是接收方无法准确地将字节流拆分为原始的数据报,导致数据解析错误。为了解决粘包问题,可以采取以下方法:
- 使用固定长度分割,每个数据报长度固定,接收方按照固定长度提取数据。
- 使用特定字符分割,定义一个特殊的字符或字符序列作为数据报之间的分隔符,接收方根据分隔符提取数据报。
- 使用长度字段分割,数据报头部添加表示长度的字段,接收方读取长度字段并提取相应长度的字节作为数据报。
学习计算机网络应用层协议的原理是为了理解网络通信机制和实现自定义协议。定制协议原理的学习能帮助我们根据特殊字符将字节流分割为数据报,并通过反序列化解析出所需报文。这对于开发网络应用、数据交互和错误处理至关重要。
序列化和反序列化
序列化(Serialization)是将对象的状态转换为字节流的过程,以便将其存储在内存、文件或网络中,或者将其传输到其他远程系统。序列化将对象转换为字节序列的形式,使得可以在不同的平台、系统或编程语言之间进行数据交换和持久化存储。
反序列化(Deserialization)是将字节流转换回对象的状态的过程,即从序列化的字节流中恢复对象的属性和数据结构。反序列化将字节流重新转换为原始对象的形式,以便可以使用和操作这些对象。
序列化和反序列化通常在分布式系统、网络通信和持久化存储等场景中使用。通过序列化,可以将对象转换为字节流,在网络传输中发送或存储到磁盘上。然后,通过反序列化,可以将字节流重新还原为原始对象,以便进行处理、操作或者重新恢复对象的状态。
在使用HTTP协议的时候,使用过浏览器的时候,浏览器的后端,服务器的后端,会帮我们做好这些事情,但是今天,博主要带着大家来定制一个自己的协议,带着大家来理解上述的原理。
本项目:实现一个网络版本计算器利用自己定制的协议
Github地址
网络计算器-序列化和反序列化-协议定制https://github.com/Yufccode/BitCode/tree/main/Linux/%E4%BB%A3%E7%A0%81/0226%E7%BD%91%E7%BB%9C%E8%AE%A1%E7%AE%97%E5%99%A8-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%8D%8F%E8%AE%AE%E5%AE%9A%E5%88%B6
底层服务器准备
底层使用TCP连接,在博主实现的代码中,博主简单实用了一下多线程,写了一个多线程的版本,这里博主就不展示底层服务器的代码了,小伙伴可以直接看博主github上的那个就行。
制定序列化反序列化规则和报文规则
规定一个运算包括三个字段:
x,y,op
x代表第一个操作数
y代表第二个操作数
op代表运算符
1+2 即 x=1,y=2,op='+'
规定,客户端输入格式如下所示:
序列化后为:
Request报文格式
length\r\n__x __op __y\r\n
即第一个字段是报文长度,紧接着两个特殊字符\r\n 然后是x,y和op,中间用空格分开,最后再加上一个\r\n。
这个就是我们的报文!
步骤如下:
当用户输入到客户端之后,客户端首先会将三个字段用结构体Request存起来,然后调用序列化的接口,把结构体序列化成一个Request字符串,就是上面我们展示的报文的格式。
当服务端收到这个Request报文之后,再调用反序列化的接口得到三个字段,得到一个结Request构体。
然后通过这个Request结构体的内容,计算得到结果后,形成Respone结构体,对Respone进行序列化,发送给客户端,客户端再把Respone反序列化,得到最后的结果。
Respones报文格式
length\r\n__x __op __y\r\n
Protocol.hpp
#ifndef __Yufc_Protocol_For_Cal
#define __Yufc_Protocol_For_Cal#include <iostream>
#include <string>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"/* 协议的本质 --> 约定! */#define RECV_MAX_SIZE 1024/* 请求协议是必须自带序列化功能的 一个结构体是很难发送的 需要序列化成string */
/* 上次0222定制的协议是不完善的!我们需要定制报文*/// 协议
/* length\r\n__x __op __y\r\n*/
/* length这些就叫做协议报头 */namespace yufc_ns_protocol
{
#define MYSELF true
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)class Request{public:std::string Serialize(){
#if MYSELFstd::string str;str = std::to_string(__x);str += SPACE;str += __op; // BUGstr += SPACE;str += std::to_string(__y);return str;
#else/* 使用别人的序列化方案 */#include <jsoncpp/json/json.h>Json::Value root;root["x"] = __x;root["y"] = __y;root["op"] = __op;Json::FastWriter writer;return writer.write(root);
#endif}bool Deserialize(const std::string &str){
#if MYSELFstd::size_t left = str.find(SPACE);if (left == std::string::npos){return false; // 反序列化失败}std::size_t right = str.rfind(SPACE);if (right == std::string::npos){return false; // 反序列化失败}__x = atoi(str.substr(0, left).c_str()); // 拿到__x的字符串之后直接 atoi 就行__y = atoi(str.substr(right + SPACE_LEN).c_str());if (left + SPACE_LEN > str.size())return false;__op = str[left + SPACE_LEN];return true;
#else#include <jsoncpp/json/json.h>Json::Value root;Json::Reader reader;reader.parse(str, root);__x = root["x"].asInt();__y = root["y"].anInt();__op = root["op"].asInt();return true;
#endif}public:Request() {}Request(int x, int y, char op) : __x(x), __y(y), __op(op) {}~Request() {}public:int __x;int __y;char __op; // '+' ...};class Response{public:/* "code result" */std::string Serialize(){
#if MYSELFstd::string s;s = std::to_string(__code);s += SPACE;s += std::to_string(__result);return s;
#else#include <jsoncpp/json/json.h>Json::Value root;root["code"] = __code;root["result"] = __result;Json::FastWriter writer;return writer.write(root);
#endif}bool Deserialize(const std::string &s){
#if MYSELF// std::cerr << "s: " << s << std::endl;std::size_t pos = s.find(SPACE);if (pos == std::string::npos)return false;__code = atoi(s.substr(0, pos).c_str());__result = atoi(s.substr(pos + SPACE_LEN).c_str());return true;
#else Json::Value root;Json::Reader reader;reader.parse(s, root);__code = root["code"].asInt();__result = root["result"].asInt();return true;
#endif}public:Response() {}Response(int result, int code) : __result(result), __code(code) {}~Response() {}public:int __result; // 计算结果int __code; // 计算结果的状态码/* 0表示计算结果可信任 */};// 临时方案bool Recv(int sock, std::string *out){char buffer[RECV_MAX_SIZE];memset(buffer, 0, sizeof buffer);ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;*out += buffer;}else if (s == 0){logMessage(NORMAL, "client quit");return false;}else{std::cerr << "recv error" << std::endl;return false;}return true;}// 临时方案void Send(int sock, const std::string sendStr){send(sock, sendStr.c_str(), sendStr.size(), 0);}// "length\r\n__x __op __y\r\n"std::string Decode(std::string &buffer){std::size_t pos = buffer.find(SEP);if (pos == std::string::npos) // 没有找到 返回空串return "";int size = atoi(buffer.substr(0, pos).c_str());int surplus = buffer.size() - pos - SEP_LEN - SEP_LEN; // 如果这个surplus大于报文长度// 则说明 这个报文长度是可以保证我们整体解析出一个合法字符串的if (surplus >= size){// 至少具有一个合法报文,可以动手提取了buffer.erase(0, pos + SEP_LEN); // 此时"__x __op __y\r\n"std::string s = buffer.substr(0, size);buffer.erase(0, size + SEP_LEN);// 此时我们就把合法的部分截取出来了 就是sreturn s;}else{return "";}}std::string Encode(std::string &s){// 添加报头std::string length = std::to_string(s.size());std::string new_package = length;new_package += SEP;new_package += s;new_package += SEP;return new_package;}}#endif
设置服务器进程为守护进程
CalServer.cc
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include <memory>
#include <signal.h>
#include "Daemon.hpp"static void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " port\n"<< std::endl;
}void debug(int sock)
{std::cout << "service for debug, sock: " << sock << std::endl;
}static yufc_ns_protocol::Response calHelper(const yufc_ns_protocol::Request &req)
{yufc_ns_protocol::Response resp(0, 0);switch (req.__op){case '+':resp.__result = req.__x + req.__y;break;case '-':resp.__result = req.__x - req.__y;break;case '*':resp.__result = req.__x * req.__y;break;case '/':if (0 == req.__y)resp.__code = 1;elseresp.__result = req.__x / req.__y;break;case '%':if (0 == req.__y)resp.__code = 2;elseresp.__result = req.__x % req.__y;break;default:resp.__code = 3;break;}return resp;
}void Calculator(int sock)
{/* 此处我们就要去定制协议 */std::string inbuffer;while (true){// 1. 读取数据bool res = yufc_ns_protocol::Recv(sock, &inbuffer); // 在这里我们读到了一个请求// 2. 协议解析 -- 需要保证得到一个完整的报文if (!res)break; // 读取失败std::string package = yufc_ns_protocol::Decode(inbuffer); // 这里Decode保证返回的是一个完整的字节流,是正确的,是可以序列化反序列化的!// 如果这个package是空 表示Decode没有给我们返回完整报文if (package.empty())continue;// 3. 保证该报文是一个完整的报文yufc_ns_protocol::Request req;// 4. 反序列化req.Deserialize(package);// 5. 业务逻辑yufc_ns_protocol::Response resp = calHelper(req);// 6. 序列化std::string respString = resp.Serialize(); // 序列化// 7. 在发送之前,添加报头// "length\r\ncode result\r\n"respString = yufc_ns_protocol::Encode(respString);// 8. 发送(暂时这样写,高级IO的时候,我们再来谈发送的逻辑)yufc_ns_protocol::Send(sock, respString); // 发送}
}int main(int argc, char **argv)
{if (argc != 2){Usage(argv[0]);exit(1);}signal(SIGPIPE, SIG_IGN);/*一般经验: server 在编写的时候都要有严谨的判断逻辑一般的服务器,都是要忽略SIGPIPE信号的!防止运行中出现非法写入的问题*/std::unique_ptr<yufc_tcpServer::TcpServer> server(new yufc_tcpServer::TcpServer(atoi(argv[1])));server->BindService(Calculator);MyDaemon();server->Start();return 0;
}