如何做網(wǎng)站本地服務(wù)器嗎廣告投放策略
從零開(kāi)始掌握序列化
- 1 知識(shí)回顧
- 2 序列化與編寫(xiě)協(xié)議
- 2.1 使用Json進(jìn)行序列化
- 2.2 編寫(xiě)協(xié)議
- 3 封裝IOService
- 4 應(yīng)用層 --- 網(wǎng)絡(luò)計(jì)算器
- 5 總結(jié)
1 知識(shí)回顧
上一篇文章我們講解了協(xié)議的本質(zhì)是雙方能夠看到的結(jié)構(gòu)化數(shù)據(jù)。并通過(guò)傳輸層的底層理解了為什么read系列函數(shù)時(shí)全雙工支持同時(shí)讀寫(xiě)的:TCP傳輸層有兩個(gè)緩沖區(qū),分別接收和發(fā)送。最重要的是我們將TCP通信的代碼進(jìn)行的重構(gòu):
- 我們將Socket通信單獨(dú)封裝為一個(gè)類(lèi),負(fù)責(zé)Socket套接字的創(chuàng)建,bind綁定服務(wù)器端口號(hào),進(jìn)入監(jiān)聽(tīng)模式…工作,基類(lèi)Socket并不進(jìn)行定義,只進(jìn)行聲明!具體實(shí)現(xiàn)由派生類(lèi)TcpServer和UdpServer來(lái)進(jìn)行
- TcpServer繼承Socket類(lèi)的所有方法,然后進(jìn)行具體的函數(shù)定義!
- 上層的TcpServer直接底層使用TcpSocket對(duì)象就可以完成Socket系列操作,十分方便!
接下來(lái)我們要實(shí)現(xiàn)是這樣的一個(gè)結(jié)構(gòu):
通信過(guò)程整體分為三層
- 傳輸層TcpServer:負(fù)責(zé)從Socket文件中獲取鏈接,傳輸層不需要進(jìn)行IO,獲取到連接就讓會(huì)話層通過(guò)連接獲取數(shù)據(jù)!
- 會(huì)話層Service:根據(jù)傳輸層給的連接,從Sockfd文件中讀取數(shù)據(jù),解析出報(bào)文結(jié)構(gòu)中的數(shù)據(jù)字符串,然后通過(guò)協(xié)議分離出結(jié)構(gòu)化數(shù)據(jù)。該層只負(fù)責(zé)數(shù)據(jù)的解析,數(shù)據(jù)的處理交給應(yīng)用層進(jìn)行!
- 應(yīng)用層Process:應(yīng)用層是具有的業(yè)務(wù)邏輯,根據(jù)會(huì)話層解析出的數(shù)據(jù),進(jìn)行數(shù)據(jù)處理!
這樣是一個(gè)非常非常優(yōu)雅的封裝操作!!!
2 序列化與編寫(xiě)協(xié)議
2.1 使用Json進(jìn)行序列化
協(xié)議是IO的基礎(chǔ),只有協(xié)議確定下來(lái),才可以進(jìn)行通信。
我們這里想要實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)計(jì)算器的應(yīng)用,所以協(xié)議分為了兩個(gè)類(lèi):Request和Response。分別作為傳入的數(shù)據(jù)和傳出的數(shù)據(jù):
- Request:兩個(gè)數(shù)字和一個(gè)運(yùn)算符
- Response:結(jié)果數(shù)字 , 錯(cuò)誤碼 ,退出信息
他們是作為結(jié)構(gòu)化的數(shù)據(jù)進(jìn)行傳輸,那么想要進(jìn)行傳輸就來(lái)到了最重要的部分序列化與反序列化!序列化與反序列化可以使用第三方庫(kù)也可以自己進(jìn)行編寫(xiě)。這里我們先使用第三方的Json庫(kù)進(jìn)行實(shí)現(xiàn):
Jsoncpp 是一個(gè)用于處理 JSON 數(shù)據(jù)的 C++ 庫(kù)。 它提供了將 JSON 數(shù)據(jù)序列化為字符串以及從字符串反序列化為 C++ 數(shù)據(jù)結(jié)構(gòu)的功能。 Jsoncpp 是開(kāi)源的, 廣泛用于各種需要處理 JSON 數(shù)據(jù)的 C++ 項(xiàng)目中:
- 簡(jiǎn)單易用: Jsoncpp 提供了直觀的 API, 使得處理 JSON 數(shù)據(jù)變得簡(jiǎn)單。
- 高性能: Jsoncpp 的性能經(jīng)過(guò)優(yōu)化, 能夠高效地處理大量 JSON 數(shù)據(jù)。
- 全面支持: 支持 JSON 標(biāo)準(zhǔn)中的所有數(shù)據(jù)類(lèi)型, 包括對(duì)象、 數(shù)組、 字符串、 數(shù)字、 布爾值和 null。
- 錯(cuò)誤處理: 在解析 JSON 數(shù)據(jù)時(shí), Jsoncpp 提供了詳細(xì)的錯(cuò)誤信息和位置, 方便開(kāi)發(fā)者調(diào)試
在Linux中使用需要進(jìn)行安裝對(duì)應(yīng)的JSON庫(kù):
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
安裝之后就可以進(jìn)行使用了:
使用起來(lái)是十分方便的:
- Json::Value是最重要的類(lèi),這是對(duì)Json數(shù)據(jù)結(jié)構(gòu)進(jìn)程操作和表示的關(guān)鍵類(lèi)
- 建立好類(lèi)Json::Value之后就可以通過(guò)
[ ]
操作root["x"] = _x;
,像這樣就可以進(jìn)行賦值- 將Json數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為字符串依靠 Json::FastWriter 或 Json::StreamWriter都可以轉(zhuǎn)換成字符串
Json::StyledWriter writer; std::string s = writer.write(root)
- 通過(guò)Json::Reader可以快速將字符串反序列化得到Json結(jié)構(gòu)!
bool parsingSuccessful = reader.parse(json_string,root); // 訪問(wèn) JSON 數(shù)據(jù) std::string name = root["name"].asString(); int age = root["age"].asInt(); std::string city = root["city"].asString();
通過(guò)這樣就就可以簡(jiǎn)潔的完成序列化與反序列化的工作!
2.2 編寫(xiě)協(xié)議
根據(jù)我們的需求在加入Json操作我們就可以把協(xié)議寫(xiě)出來(lái),代碼雖然很長(zhǎng)但是很好理解:
- Request類(lèi)中需要根據(jù)
int x , int y , char oper
進(jìn)行序列化生成字符串,也要能夠通過(guò)字符串反序列化得到三個(gè)變量 - Response類(lèi)中需要根據(jù)
int res , int code , std::string desc
進(jìn)行序列化生成字符串,也要能夠通過(guò)字符串反序列化得到三個(gè)變量
#pragma once
#include <jsoncpp/json/json.h>
#include <string>
// 協(xié)議就是雙方都認(rèn)識(shí)的結(jié)構(gòu)化數(shù)據(jù)
// "len"\r\n"{json}"\r\n
const std::string sep = "\r\n";struct Request
{
public:Request() {}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}~Request(){}bool Serialize(std::string *out){// 使用現(xiàn)成的 Json 庫(kù)Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;std::string s = writer.write(root);*out = s;return true;}bool Deserialize(std::string &in){Json::Value root; // 創(chuàng)建json對(duì)象Json::Reader reader; // 讀取bool res = reader.parse(in, root);if (res == false)return false;_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}int X() { return _x; }int Y() { return _y; }char Oper() { return _oper; }private:int _x;int _y;char _oper;
};struct Response
{Response() {}Response(int res, int code, std::string desc) : _res(res), _code(code), _desc(desc){}~Response(){}bool Serialize(std::string *out){// 使用現(xiàn)成的 Json 庫(kù)Json::Value root;root["res"] = _res;root["code"] = _code;root["desc"] = _desc;Json::FastWriter writer;std::string s = writer.write(root);*out = s;return true;}bool Deserialize(std::string &in){Json::Value root; // 創(chuàng)建json對(duì)象Json::Reader reader; // 讀取bool res = reader.parse(in, root);if (res == false)return false;_res = root["res"].asInt();_code = root["code"].asInt();_desc = root["desc"].asInt();return true;}int _res;int _code; // 退出碼 0:success 1:div zero 2:非法操作std::string _desc;
};
看一下效果:
完成了基礎(chǔ)的序列化和反序列化之后,我們就可以做到從sockfd流中讀取數(shù)據(jù)了嗎??不可以!因?yàn)椴恢繨son字符串的長(zhǎng)度,就不知道應(yīng)該讀取多少字節(jié)!這樣可就做不到正確的從數(shù)據(jù)中獲取json字符串!
所以我們還有做一步特殊處理:
- 需要對(duì)生成的Json字符串加入報(bào)頭
len
記錄json字符串的長(zhǎng)度,中間以sep
分隔符分割! - 需要對(duì)獲得到的數(shù)據(jù)進(jìn)行解析,去除報(bào)頭得到一個(gè)Json字符串!
// "len"\r\n"{json}"\r\n
const std::string sep = "\r\n";
// 加入報(bào)頭
std::string Encode(const std::string &jsonstr)
{int len = jsonstr.size();std::string lenstr = std::to_string(len);return lenstr + sep + jsonstr + sep;
}std::string Decode(std::string &packagestream)
{auto pos = packagestream.find(sep);if (pos == std::string::npos)return std::string();// 獲取到lenstd::string lenstr = packagestream.substr(0, pos);int len = std::stoi(lenstr);//算上報(bào)頭的完整長(zhǎng)度!int total = lenstr.size() + len + 2 * sep.size();if (total > packagestream.size())return std::string();// 到這里說(shuō)明可以讀取完整數(shù)據(jù)std::string jsonstr = packagestream.substr(pos + sep.size(), len);packagestream.erase(total);return jsonstr;
}
經(jīng)過(guò)這樣的操作,可以保證:
- 上層想要發(fā)送數(shù)據(jù)時(shí),可以將數(shù)據(jù)包裝為json字符串,并加入報(bào)頭形成完整報(bào)文!
- 上層獲取數(shù)據(jù)進(jìn)行反序列化時(shí)可以獲取到完整的json字符串!并成功解析為數(shù)據(jù)
3 封裝IOService
將來(lái)我們的線程會(huì)執(zhí)行將會(huì)執(zhí)行這個(gè)回調(diào)函數(shù)方法,現(xiàn)在我們不再需要TcpServer來(lái)進(jìn)行IO操作,TcpServer只負(fù)責(zé)進(jìn)行獲取鏈接,獲取到連接后通過(guò)ThreadData結(jié)構(gòu)體將數(shù)據(jù)傳到線程中的回調(diào)函數(shù)中:
class ThreadData{public:SockSPtr _sockfd;InetAddr _addr;TcpServer *_this;public:ThreadData(SockSPtr sockfd, InetAddr addr, TcpServer *p) : _sockfd(sockfd),_this(p),_addr(addr){}};
在回調(diào)函數(shù)Execute中:
// 注意設(shè)置為靜態(tài)函數(shù) , 不然參數(shù)默認(rèn)會(huì)有TcpServer* this!!!static void *Execute(void *args){pthread_detach(pthread_self()); // 線程分離!!!// 執(zhí)行Service函數(shù)TcpServer::ThreadData *td = static_cast<TcpServer::ThreadData *>(args);td->_this->_service(td->_sockfd, td->_addr);td->_sockfd->Close();delete td;return nullptr;}
就可以解析出來(lái)套接字文件描述符和客戶(hù)端信息了!解析出信息之后就去執(zhí)行會(huì)話層的回調(diào)函數(shù)進(jìn)行IO操作:
- Service內(nèi)部只有一個(gè)成員變量,就是應(yīng)用層的回調(diào)函數(shù),Service解析出來(lái)數(shù)據(jù)之后就可以傳入到應(yīng)用層中進(jìn)行使用
- IO中主要需要進(jìn)行從sockfd文件中獲取數(shù)據(jù),然后通過(guò)協(xié)議進(jìn)行解析,獲取到真正的數(shù)據(jù)。再調(diào)用回調(diào)函數(shù)對(duì)數(shù)據(jù)進(jìn)行操作!得到結(jié)果之后就可以進(jìn)行序列化,加入報(bào)頭,再發(fā)送給客戶(hù)端!
- 應(yīng)用層的操作邏輯,Service并不關(guān)心,只要回調(diào)函數(shù)可以傳回需要的結(jié)構(gòu)體就可以!
class Service
{
public:Service(process_t process) : _process(process){}void IOExecute(SockSPtr sock, InetAddr &addr){LOG(INFO, "service start!!!\n");std::string message;while (true){// 1. 進(jìn)行讀取ssize_t n = sock->Recv(&message);if (n < 0){LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());break;}// 此時(shí)獲取到客戶(hù)端發(fā)送的數(shù)據(jù)// 但是不能保證是否是完整的報(bào)文// 2.報(bào)文解析std::string str = Decode(message); // 通過(guò)去報(bào)頭獲取報(bào)文if (str.empty())continue; // 說(shuō)明沒(méi)有完整的報(bào)文!// 到這里說(shuō)明有完整的報(bào)文!!!auto req = Factory::BuildRequestDefault();// 3.反序列化初始化Requestreq->Deserialize(str);auto res = Factory::BuildResponseDefault();// 4.業(yè)務(wù)處理res = _process(req);// 5.進(jìn)行序列化處理std::string ret;res->Serialize(&ret);// 6.加入報(bào)頭Encode(ret);// 7.將獲取的數(shù)據(jù)發(fā)送回去sock->Send(ret);}}~Service(){}private:process_t _process;
};
4 應(yīng)用層 — 網(wǎng)絡(luò)計(jì)算器
應(yīng)用層根據(jù)具體需要可以隨時(shí)改變,我這里以網(wǎng)絡(luò)計(jì)算器為例子進(jìn)行書(shū)寫(xiě):
#include "Protocol.hpp"
class NetCal
{
public:NetCal() {}std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req){std::shared_ptr<Response> res = Factory::BuildResponseDefault();switch (req->Oper()){case '+':res->_res = req->X() + req->Y();res->_code = 0;res->_desc = "success";break;case '-':res->_res = req->X() - req->Y();res->_code = 0;res->_desc = "success";break;case '*':res->_res = req->X() * req->Y();res->_code = 0;res->_desc = "success";break;case '/':{if (req->Y() == 0){res->_code = 1;res->_desc = "div zero";}res->_res = req->X() / req->Y();res->_code = 0;res->_desc = "success";}break;case '%':{if (req->Y() == 0){res->_code = 1;res->_desc = "mod zero";}res->_res = req->X() % req->Y();res->_code = 0;res->_desc = "success";}break;default:res->_code = 2;res->_desc = "illegal operations";break;}return res;}~NetCal() {}
};
邏輯很簡(jiǎn)單不在多加贅述!
5 總結(jié)
現(xiàn)在我們的程序分為了三層結(jié)構(gòu):
我們做到了最大程度的解耦!
- 傳輸層只負(fù)責(zé)獲取鏈接,我們應(yīng)用層要進(jìn)行什么工作,只要是進(jìn)行網(wǎng)絡(luò)通信傳輸層的工作就是唯一的!
- 會(huì)話層進(jìn)行IO操作!只要傳輸層提供了鏈接,會(huì)話層就可以獲取數(shù)據(jù),然后根據(jù)具體的協(xié)議進(jìn)行數(shù)據(jù)的解析工作。協(xié)議根據(jù)實(shí)際情況改變,但是會(huì)話層的工作邏輯是不變的!
- 應(yīng)用層只管進(jìn)行數(shù)據(jù)處理即可,什么但不不需要考慮!完成工作后返回給會(huì)話層數(shù)據(jù)即可!
這樣的結(jié)構(gòu)邏輯十分清晰,并且解耦的非常優(yōu)雅,值得反復(fù)品味!!!