目录
前言
1.理解协议
2.网络版本计算器
2.1设计思路
2.2接口设计
2.3代码实现:
2.4编译测试
总结
前言
在之前的文章中,我们说TCP是面向字节流的,但是可能对于面向字节流这个概念,其实并不理解的,今天我们要介绍的是如何理解TCP是面向字节流的,通过编码的方式,自己定制协议,实现序列化和反序列化,相信看完这篇文章之后,关于TCP面向字节流的这个概念,你将会有一个清晰的认识,下面我们就一起来看看。
1.理解协议
前面,我们通俗的介绍过协议,在网络中协议是属于一种约定,今天要说的是,数据在网络中传输的时候,协议又是如何体现的。
根据我们之前写的TCP服务器实现数据通信知道socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
什么是结构化的数据呢?
举个简单的例子,比如在微信上发送信息的时候,除了有发送的信息之外还包含有昵称,时间,头像这些信息,这些合起来就称为是结构化的数据。
所以我们将结构化的数据打包形成一个字符串的过程就称为是序列化,将打包形成的一个字符串转化为结构化数据的过程就称为是反序列化
如图所示:
TCP发送和接受数据的流程
如图所示:
作为程序员,在应用层定义一个缓冲区,然后send接口将数据发送,在这里发送不是将数据直接发送到网络里了,而是调用send接口将数据拷贝到传输层操作系统维护的缓冲区中,而read是将传输层的数据拷贝到应用层,当数据拷贝到传输层之后,剩下数据如何继续发送是由操作系统进行维护的,所以将TCP协议称为是传输控制协议,又因为TCP协议既可以是客户端向服务端发送信息,也可以是服务端向客户端发送信息,所以TCP是全双工的。
了解了TCP协议发送和接受数据的流程之后,因为TCP是面向字节流的,思考当数据由客户端发送给服务端的时候,有没有可能服务端的接受缓冲区中不足一个报文,有没有可能上层来不及处理导致服务端传输层接受缓冲区中有多个报文的情况,此时如何正确的拿到一个完整的报文呢?
因为这些问题的存在,所以我们要定制协议,明确一个完整报文大小,明确一个报文和一个报文的边界,所以我们要采取定制协议的方案获取到一个正确的报文。
一般采取的策略有三种:
1.定长
2.特殊符号
3.自描述的方式
下面我们按照上述的三种方式实现编码上的协议定制。
2.网络版本计算器
说明:为了演示协议定制和序列化以及反序列化在编码上如何实现,以及如何在编码上体现TCP面向字节流的特性,我们通过实现一个网络版本计算器为大家进行介绍
实现网络版本计算机约定:
客户端发送一个形如"1+1"的字符串;
这个字符串中有两个操作数, 都是整形;
两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格;
2.1设计思路
客户端将想要计算的请求按照序列化的方式打包成一个字符串,然后发送给服务端,服务端按照定制协议的方式准确收到客户端的请求,然后服务端进行反序列化获取到结构化的数据,然后进行处理业务逻辑计算结果,结果计算完成之后,服务端将计算结果序列化打包形成一个字符串发送给客户端,然后客户端按照定制协议的方式准确获取到服务端发送过来的一个完整报文,至此就基于TCP协议实现了一个网络版本的计算器
2.2接口设计
要向完成上述的要求,就必须要包含几个接口:
a.请求的序列化和反序列化
b.响应的序列化和反序列化
c.协议定制
d.计算业务逻辑
e.准确获取一个报文
f.客户端和服务端编写
2.3代码实现:
1.请求的序列化和反序列化
class Request
{
public:Request():x(0),y(0),op(char()){}Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_){}bool serialize(std::string *out){*out = "";// 结构化 -> "x op y";std::string x_string = std::to_string(x);std::string y_string = std::to_string(y);*out = x_string;*out += SEP;*out += op;*out += SEP;*out += y_string;return true;}// "x op yyyy";bool deserialize(const std::string &in){// "x op y" -> 结构化auto left = in.find(SEP);auto right = in.rfind(SEP);if (left == std::string::npos || right == std::string::npos)return false;if (left == right)return false;if (right - (left + SEP_LEN) != 1)return false;std::string x_string = in.substr(0, left); // [0, 2) [start, end) , start, end - startstd::string y_string = in.substr(right + SEP_LEN);if (x_string.empty())return false;if (y_string.empty())return false;x = stoi(x_string);y = stoi(y_string);op = in[left + SEP_LEN];return true;}public:int x;int y;char op;
};
序列化结果:将x,y,op - > 转化为 "x y op\r\n"
反序列化结果:"x y op\r\n" - > 转化为 x,y,op
2.响应的序列化和反序列化
#define SEP " "
#define SEP_LEN strlen(SEP) // 不敢使用sizeof()
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()
class Response
{
public:Response():exitcode(0),result(0) {}Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_){}bool serialize(std::string *out){*out = "";std::string ec_string = std::to_string(exitcode);std::string res_string = std::to_string(result);*out = ec_string;*out += SEP;*out += res_string;return true;}bool deserialize(const std::string &in){// "exitcode result"auto mid = in.find(SEP);if (mid == std::string::npos)return false;std::string ec_string = in.substr(0, mid);std::string res_string = in.substr(mid + SEP_LEN);if (ec_string.empty() || res_string.empty())return false;exitcode = std::stoi(ec_string);result = std::stoi(res_string);return true;}
public:int exitcode;int result;
};
序列化结果:将exitcode,result - > 转化为 "exitcode result\r\n"
反序列化结果: "exitcode result\r\n" - > 转化为 exitcode,result
3.协议定制
说明:采用自描述的方式+特殊符号,给一个报文头部加上报文的长度,特殊符号"\r\n"用来区分报文长度和报文数据
#define SEP " "
#define SEP_LEN strlen(SEP) // 不敢使用sizeof()
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()
//enLength 和 deLength:打包和解包,解决服务端和客户端准确拿到数据
// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
std::string enLength(const std::string &text)
{std::string send_string = std::to_string(text.size());send_string += LINE_SEP;send_string += text;send_string += LINE_SEP;return send_string;
}// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{auto pos = package.find(LINE_SEP);if (pos == std::string::npos)return false;std::string text_len_string = package.substr(0, pos);int text_len = std::stoi(text_len_string);*text = package.substr(pos + LINE_SEP_LEN, text_len);return true;
}
4.计算业务逻辑
//req是反序列化后的结果,根据res业务处理填充req即可
bool cal(const Request& req,Response& res)
{//req是结构化的数据,可以直接使用// req已经有结构化完成的数据啦,你可以直接使用res.exitcode = OK;res.result = OK;switch (req.op){case '+':res.result = req.x + req.y;break;case '-':res.result = req.x - req.y;break;case '*':res.result = req.x * req.y;break;case '/':{if (req.y == 0)res.exitcode = DIV_ZERO;elseres.result = req.x / req.y;}break;case '%':{if (req.y == 0)res.exitcode = MOD_ZERO;elseres.result = req.x % req.y;}break;default:res.exitcode = OP_ERROR;break;}return true;
}
5.准确获取一个报文
//从sock中读取数据保存到text中
//continue是因为tcp协议是面向字节流的,传输数据的时候可能不完整
bool recvPackage(int sock,string &inbuffer,string *text)
{char buffer[1024];while (true){ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;inbuffer += buffer;// 分析处理auto pos = inbuffer.find(LINE_SEP);if (pos == std::string::npos)continue;std::string text_len_string = inbuffer.substr(0, pos);int text_len = std::stoi(text_len_string);int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;// text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size();std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;if (inbuffer.size() < total_len){std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;continue;}// 至少有一个完整的报文*text = inbuffer.substr(0, total_len);inbuffer.erase(0, total_len);std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;break;}elsereturn false;}return true;
}
注:看到这里我们就可以理解了TCP是面向字节流的概念了。
6.客户端和服务端实现:
calServer.hpp:
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "log.hpp"
#include "protocol.hpp" //按照协议约定读取请求
using namespace std;
namespace server
{enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;static const int gbacklog = 5;typedef function<bool(const Request& req,Response& res)> func_t;//读取请求,保证解耦void handlerEnter(int sock,func_t fun){string inbuffer;while(true){//1. 读取:"content_len"\r\n"x op y"\r\n// 1.1 你怎么保证你读到的消息是 【一个】完整的请求string req_text, req_str;if (!recvPackage(sock,inbuffer,&req_text))return;std::cout << "带报头的请求:\n" << req_text << std::endl;//req_str:获取报文if (!deLength(req_text, &req_str))return;std::cout << "去掉报头的正文:\n" << req_str << std::endl;// 2. 对请求Request,反序列化// 2.1 得到一个结构化的请求对象Request req;if(!req.deserialize(req_str))return;// 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑// 3.1 得到一个结构化的响应Response res;fun(req,res);//req处理的结果放到res中,采用回调的方式保证上层业务逻辑和服务器的解耦// 4.对响应Response,进行序列化// 4.1 得到了一个"字符串"string resp_str;if(!res.serialize(&resp_str))return;std::cout << "计算完成, 序列化响应: " << resp_str << std::endl;// 5. 然后我们在发送响应// 5.1 构建成为一个完整的报文std::string send_string = enLength(resp_str);std::cout << "构建完成完整的响应\n" << send_string << std::endl;send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说}}class CalServer{public:CalServer(const uint16_t &port = gport) : _listensock(-1), _port(port){}void initServer(){// 1. 创建socket文件套接字对象_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success: %d", _listensock);// 2. bind绑定自己的网络信息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(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");// 3. 设置socket 为监听状态if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}void start(func_t fun){for (;;){// 4. server 获取新链接// sock, 和client进行通信的fdstruct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){logMessage(ERROR, "accept error, next");continue;}logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?// version 2 多进程版(2)pid_t id = fork();if (id == 0) // child{close(_listensock);handlerEnter(sock,fun);close(sock);exit(0);}close(sock);// fatherpid_t ret = waitpid(id, nullptr, 0);if (ret > 0){logMessage(NORMAL, "wait child success"); // ?}}}~CalServer() {}private:int _listensock; // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!uint16_t _port;};} // namespace server
calClient.hpp:
#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 "protocol.hpp"#define NUM 1024class CalClient
{
public:CalClient(const std::string &serverip, const uint16_t &serverport): _sock(-1), _serverip(serverip), _serverport(serverport){}void initClient(){// 1. 创建socket_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){std::cerr << "socket create error" << std::endl;exit(2);}}void start(){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_serverport);server.sin_addr.s_addr = inet_addr(_serverip.c_str());if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0){std::cerr << "socket connect error" << std::endl;}else{std::string line;std::string inbuffer;while (true){std::cout << "mycal>>> ";std::getline(std::cin, line); // 1+1Request req = ParseLine(line); // "1+1"std::string content;req.serialize(&content);std::string send_string = enLength(content);send(_sock, send_string.c_str(), send_string.size(), 0); // bug?? 不管std::string package, text;// "content_len"\r\n"exitcode result"\r\nif (!recvPackage(_sock, inbuffer, &package))continue;if (!deLength(package, &text))continue;// "exitcode result"Response resp;resp.deserialize(text);std::cout << "exitCode: " << resp.exitcode << std::endl;std::cout << "result: " << resp.result << std::endl;}}}Request ParseLine(const std::string &line){// 建议版本的状态机!//"1+1" "123*456" "12/0"int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后int i = 0;int cnt = line.size();std::string left, right;char op;while (i < cnt){switch (status){case 0:{if(!isdigit(line[i])){op = line[i];status = 1;}else left.push_back(line[i++]);}break;case 1:i++;status = 2;break;case 2:right.push_back(line[i++]);break;}}std::cout << std::stoi(left)<<" " << std::stoi(right) << " " << op << std::endl;return Request(std::stoi(left), std::stoi(right), op);}~CalClient(){if (_sock >= 0)close(_sock);}private:int _sock;std::string _serverip;uint16_t _serverport;
};
2.4编译测试
如图所示:我们准确的实现了网络版本计算器
总结
通过上面代码的编写,包含定制协议,序列化和反序列代码的实现,我们就能够理解协议在网络传输的重要性了,以及理解了TCP是面向字节流的概念。感谢大家的观看,希望能够帮助到大家,我们下次再见。