目錄
前言
1.理解協(xié)議
2.網(wǎng)絡(luò)版本計(jì)算器
2.1設(shè)計(jì)思路
2.2接口設(shè)計(jì)
2.3代碼實(shí)現(xiàn):
2.4編譯測試
總結(jié)
前言
? ? ? ? 在之前的文章中,我們說TCP是面向字節(jié)流的,但是可能對于面向字節(jié)流這個概念,其實(shí)并不理解的,今天我們要介紹的是如何理解TCP是面向字節(jié)流的,通過編碼的方式,自己定制協(xié)議,實(shí)現(xiàn)序列化和反序列化,相信看完這篇文章之后,關(guān)于TCP面向字節(jié)流的這個概念,你將會有一個清晰的認(rèn)識,下面我們就一起來看看。
1.理解協(xié)議
? ? ? ? 前面,我們通俗的介紹過協(xié)議,在網(wǎng)絡(luò)中協(xié)議是屬于一種約定,今天要說的是,數(shù)據(jù)在網(wǎng)絡(luò)中傳輸?shù)臅r候,協(xié)議又是如何體現(xiàn)的。
根據(jù)我們之前寫的TCP服務(wù)器實(shí)現(xiàn)數(shù)據(jù)通信知道socket api的接口, 在讀寫數(shù)據(jù)時, 都是按 "字符串" 的方式來發(fā)送接收的. 如果我們要傳輸一些"結(jié)構(gòu)化的數(shù)據(jù)" 怎么辦呢?
什么是結(jié)構(gòu)化的數(shù)據(jù)呢?
舉個簡單的例子,比如在微信上發(fā)送信息的時候,除了有發(fā)送的信息之外還包含有昵稱,時間,頭像這些信息,這些合起來就稱為是結(jié)構(gòu)化的數(shù)據(jù)。
所以我們將結(jié)構(gòu)化的數(shù)據(jù)打包形成一個字符串的過程就稱為是序列化,將打包形成的一個字符串轉(zhuǎn)化為結(jié)構(gòu)化數(shù)據(jù)的過程就稱為是反序列化
如圖所示:
TCP發(fā)送和接受數(shù)據(jù)的流程
如圖所示:
作為程序員,在應(yīng)用層定義一個緩沖區(qū),然后send接口將數(shù)據(jù)發(fā)送,在這里發(fā)送不是將數(shù)據(jù)直接發(fā)送到網(wǎng)絡(luò)里了,而是調(diào)用send接口將數(shù)據(jù)拷貝到傳輸層操作系統(tǒng)維護(hù)的緩沖區(qū)中,而read是將傳輸層的數(shù)據(jù)拷貝到應(yīng)用層,當(dāng)數(shù)據(jù)拷貝到傳輸層之后,剩下數(shù)據(jù)如何繼續(xù)發(fā)送是由操作系統(tǒng)進(jìn)行維護(hù)的,所以將TCP協(xié)議稱為是傳輸控制協(xié)議,又因?yàn)門CP協(xié)議既可以是客戶端向服務(wù)端發(fā)送信息,也可以是服務(wù)端向客戶端發(fā)送信息,所以TCP是全雙工的。
了解了TCP協(xié)議發(fā)送和接受數(shù)據(jù)的流程之后,因?yàn)門CP是面向字節(jié)流的,思考當(dāng)數(shù)據(jù)由客戶端發(fā)送給服務(wù)端的時候,有沒有可能服務(wù)端的接受緩沖區(qū)中不足一個報(bào)文,有沒有可能上層來不及處理導(dǎo)致服務(wù)端傳輸層接受緩沖區(qū)中有多個報(bào)文的情況,此時如何正確的拿到一個完整的報(bào)文呢?
因?yàn)檫@些問題的存在,所以我們要定制協(xié)議,明確一個完整報(bào)文大小,明確一個報(bào)文和一個報(bào)文的邊界,所以我們要采取定制協(xié)議的方案獲取到一個正確的報(bào)文。
一般采取的策略有三種:
1.定長
2.特殊符號
3.自描述的方式
下面我們按照上述的三種方式實(shí)現(xiàn)編碼上的協(xié)議定制。
2.網(wǎng)絡(luò)版本計(jì)算器
說明:為了演示協(xié)議定制和序列化以及反序列化在編碼上如何實(shí)現(xiàn),以及如何在編碼上體現(xiàn)TCP面向字節(jié)流的特性,我們通過實(shí)現(xiàn)一個網(wǎng)絡(luò)版本計(jì)算器為大家進(jìn)行介紹
實(shí)現(xiàn)網(wǎng)絡(luò)版本計(jì)算機(jī)約定:
客戶端發(fā)送一個形如"1+1"的字符串;
這個字符串中有兩個操作數(shù), 都是整形;
兩個數(shù)字之間會有一個字符是運(yùn)算符, 運(yùn)算符只能是 + ;
數(shù)字和運(yùn)算符之間沒有空格;
2.1設(shè)計(jì)思路
? ? ? ? 客戶端將想要計(jì)算的請求按照序列化的方式打包成一個字符串,然后發(fā)送給服務(wù)端,服務(wù)端按照定制協(xié)議的方式準(zhǔn)確收到客戶端的請求,然后服務(wù)端進(jìn)行反序列化獲取到結(jié)構(gòu)化的數(shù)據(jù),然后進(jìn)行處理業(yè)務(wù)邏輯計(jì)算結(jié)果,結(jié)果計(jì)算完成之后,服務(wù)端將計(jì)算結(jié)果序列化打包形成一個字符串發(fā)送給客戶端,然后客戶端按照定制協(xié)議的方式準(zhǔn)確獲取到服務(wù)端發(fā)送過來的一個完整報(bào)文,至此就基于TCP協(xié)議實(shí)現(xiàn)了一個網(wǎng)絡(luò)版本的計(jì)算器
2.2接口設(shè)計(jì)
要向完成上述的要求,就必須要包含幾個接口:
a.請求的序列化和反序列化
b.響應(yīng)的序列化和反序列化
c.協(xié)議定制
d.計(jì)算業(yè)務(wù)邏輯
e.準(zhǔn)確獲取一個報(bào)文
f.客戶端和服務(wù)端編寫
2.3代碼實(shí)現(xiàn):
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 = "";
// 結(jié)構(gòu)化 -> "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" -> 結(jié)構(gòu)化
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 - start
std::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;
};
序列化結(jié)果:將x,y,op - > 轉(zhuǎn)化為?"x y op\r\n"
反序列化結(jié)果:"x y op\r\n"?- > 轉(zhuǎn)化為?x,y,op
2.響應(yīng)的序列化和反序列化
#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;
};
序列化結(jié)果:將exitcode,result?- > 轉(zhuǎn)化為?"exitcode result\r\n"
反序列化結(jié)果:?"exitcode result\r\n"?- > 轉(zhuǎn)化為?exitcode,result
3.協(xié)議定制
說明:采用自描述的方式+特殊符號,給一個報(bào)文頭部加上報(bào)文的長度,特殊符號"\r\n"用來區(qū)分報(bào)文長度和報(bào)文數(shù)據(jù)
#define SEP " "
#define SEP_LEN strlen(SEP) // 不敢使用sizeof()
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()
//enLength 和 deLength:打包和解包,解決服務(wù)端和客戶端準(zhǔn)確拿到數(shù)據(jù)
// "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.計(jì)算業(yè)務(wù)邏輯
//req是反序列化后的結(jié)果,根據(jù)res業(yè)務(wù)處理填充req即可
bool cal(const Request& req,Response& res)
{
//req是結(jié)構(gòu)化的數(shù)據(jù),可以直接使用
// req已經(jīng)有結(jié)構(gòu)化完成的數(shù)據(jù)啦,你可以直接使用
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;
else
res.result = req.x / req.y;
}
break;
case '%':
{
if (req.y == 0)
res.exitcode = MOD_ZERO;
else
res.result = req.x % req.y;
}
break;
default:
res.exitcode = OP_ERROR;
break;
}
return true;
}
5.準(zhǔn)確獲取一個報(bào)文
//從sock中讀取數(shù)據(jù)保存到text中
//continue是因?yàn)閠cp協(xié)議是面向字節(jié)流的,傳輸數(shù)據(jù)的時候可能不完整
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 << "你輸入的消息,沒有嚴(yán)格遵守我們的協(xié)議,正在等待后續(xù)的內(nèi)容, continue" << std::endl;
continue;
}
// 至少有一個完整的報(bào)文
*text = inbuffer.substr(0, total_len);
inbuffer.erase(0, total_len);
std::cout << "處理后#inbuffer:\n " << inbuffer << std::endl;
break;
}
else
return false;
}
return true;
}
注:看到這里我們就可以理解了TCP是面向字節(jié)流的概念了。
6.客戶端和服務(wù)端實(shí)現(xiàn):
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" //按照協(xié)議約定讀取請求
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 << "帶報(bào)頭的請求:\n" << req_text << std::endl;
//req_str:獲取報(bào)文
if (!deLength(req_text, &req_str))
return;
std::cout << "去掉報(bào)頭的正文:\n" << req_str << std::endl;
// 2. 對請求Request,反序列化
// 2.1 得到一個結(jié)構(gòu)化的請求對象
Request req;
if(!req.deserialize(req_str))
return;
// 3. 計(jì)算機(jī)處理,req.x, req.op, req.y --- 業(yè)務(wù)邏輯
// 3.1 得到一個結(jié)構(gòu)化的響應(yīng)
Response res;
fun(req,res);//req處理的結(jié)果放到res中,采用回調(diào)的方式保證上層業(yè)務(wù)邏輯和服務(wù)器的解耦
// 4.對響應(yīng)Response,進(jìn)行序列化
// 4.1 得到了一個"字符串"
string resp_str;
if(!res.serialize(&resp_str))
return;
std::cout << "計(jì)算完成, 序列化響應(yīng): " << resp_str << std::endl;
// 5. 然后我們在發(fā)送響應(yīng)
// 5.1 構(gòu)建成為一個完整的報(bào)文
std::string send_string = enLength(resp_str);
std::cout << "構(gòu)建完成完整的響應(yīng)\n" << send_string << std::endl;
send(sock, send_string.c_str(), send_string.size(), 0); // 其實(shí)這里的發(fā)送也是有問題的,不過后面再說
}
}
class CalServer
{
public:
CalServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{}
void initServer()
{
// 1. 創(chuàng)建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綁定自己的網(wǎng)絡(luò)信息
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. 設(shè)置socket 為監(jiān)聽狀態(tài)
if (listen(_listensock, gbacklog) < 0) // 第二個參數(shù)backlog后面在填這個坑
{
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
void start(func_t fun)
{
for (;;)
{
// 4. server 獲取新鏈接
// sock, 和client進(jìn)行通信的fd
struct 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 多進(jìn)程版(2)
pid_t id = fork();
if (id == 0) // child
{
close(_listensock);
handlerEnter(sock,fun);
close(sock);
exit(0);
}
close(sock);
// father
pid_t ret = waitpid(id, nullptr, 0);
if (ret > 0)
{
logMessage(NORMAL, "wait child success"); // ?
}
}
}
~CalServer() {}
private:
int _listensock; // 不是用來進(jìn)行數(shù)據(jù)通信的,它是用來監(jiān)聽鏈接到來,獲取新鏈接的!
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 1024
class CalClient
{
public:
CalClient(const std::string &serverip, const uint16_t &serverport)
: _sock(-1), _serverip(serverip), _serverport(serverport)
{}
void initClient()
{
// 1. 創(chuàng)建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+1
Request 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\n
if (!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)
{
// 建議版本的狀態(tài)機(jī)!
//"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編譯測試
如圖所示:我們準(zhǔn)確的實(shí)現(xiàn)了網(wǎng)絡(luò)版本計(jì)算器文章來源:http://www.zghlxwxcb.cn/news/detail-663218.html
總結(jié)
? ? ? ? 通過上面代碼的編寫,包含定制協(xié)議,序列化和反序列代碼的實(shí)現(xiàn),我們就能夠理解協(xié)議在網(wǎng)絡(luò)傳輸?shù)闹匾粤?,以及理解了TCP是面向字節(jié)流的概念。感謝大家的觀看,希望能夠幫助到大家,我們下次再見。文章來源地址http://www.zghlxwxcb.cn/news/detail-663218.html
到了這里,關(guān)于TCP定制協(xié)議,序列化和反序列化的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!