手機網(wǎng)站模板免費模板南昌seo推廣公司
目錄
一,關(guān)于“協(xié)議”
1.1 結(jié)構(gòu)化數(shù)據(jù)
1.2 序列化和反序列化
二,網(wǎng)絡(luò)版計算器實現(xiàn)準(zhǔn)備
2.1 套用舊頭文件
2.2 封裝sock API
三,自定義協(xié)議
3.1 關(guān)于自定義協(xié)議
3.2??實現(xiàn)序列化和反序列化
3.3 測試
三,服務(wù)器實現(xiàn)
3.1 邏輯梳理
3.2 各頭文件實現(xiàn)
四,客戶端實現(xiàn)
一,關(guān)于“協(xié)議”
1.1 結(jié)構(gòu)化數(shù)據(jù)
兩個主機通過網(wǎng)絡(luò)和協(xié)議進(jìn)行通信時,發(fā)送的數(shù)據(jù)有兩種形式:
- 如果傳輸?shù)臄?shù)據(jù)直接就是一個字符串,那么把這個字符串發(fā)出去,對方也能得到這個字符串
- 如果需要傳輸?shù)氖且粋€struct結(jié)構(gòu)體,那么不能將結(jié)構(gòu)體數(shù)據(jù)一個個發(fā)送到網(wǎng)絡(luò)中
比如我要實現(xiàn)一個網(wǎng)絡(luò)版的計算器,那么客戶端給服務(wù)器發(fā)送的數(shù)據(jù),就要包含左操作數(shù),運算符和右操作數(shù),那么這就不僅僅是一個字符串了,而是一組數(shù)據(jù)
所以客戶端不能把這些數(shù)據(jù)一個個發(fā)送過去,需要把這些數(shù)據(jù)“打個包”,統(tǒng)一發(fā)到網(wǎng)絡(luò)中,此時服務(wù)器就能獲取到一個完整的數(shù)據(jù)請求,“打包”方式有兩種:
方案一:將結(jié)構(gòu)化的數(shù)據(jù)結(jié)合成一個大的字符串
- 比如我要發(fā)送“1+1”,用戶輸入的是“整型”,“字符”,“整型”
- 我們先用to_string函數(shù)把整型轉(zhuǎn)為字符串,然后用strcat或者C++/string的 "+="運算符重載將這三個字符拼接成一個長字符串,然后就可以直接發(fā)送
- 最后服務(wù)器收到了長字符串,再以相同的方式進(jìn)行拆分,用stoi函數(shù)將字符串轉(zhuǎn)整型,就可以提取這些結(jié)構(gòu)化的數(shù)據(jù)
方案二:定制結(jié)構(gòu)化數(shù)據(jù),實現(xiàn)序列化和反序列化?
- 客戶端可以定制一個結(jié)構(gòu)體,將需要交互的信息放到結(jié)構(gòu)體種
- 客戶端發(fā)送前,將結(jié)構(gòu)體的數(shù)據(jù)進(jìn)行序列化,服務(wù)器收到數(shù)據(jù)后進(jìn)行反序列化,此時服務(wù)器就能得到客戶端發(fā)送過來的結(jié)構(gòu)體,下面我們來詳細(xì)講講序列化和反序列化
1.2 序列化和反序列化
- ?序列化是將對象的狀態(tài)信息轉(zhuǎn)換為可以存儲或傳輸?shù)男问?#xff08;字節(jié)序列)的過程
- 反序列化就是把序列化的字節(jié)序列恢復(fù)為對象的過程
OSI七層模型中表示層的作用,就是“實現(xiàn)數(shù)據(jù)格式和網(wǎng)絡(luò)標(biāo)準(zhǔn)數(shù)據(jù)格式的轉(zhuǎn)換”。前者數(shù)據(jù)格式就是指數(shù)據(jù)再應(yīng)用層上的格式,后者就是指序列化之后可以進(jìn)行網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)格式?
- ?序列化的目的,是為了方便網(wǎng)絡(luò)數(shù)據(jù)的發(fā)送和接收,序列化后數(shù)據(jù)就全變成了二進(jìn)制數(shù)據(jù),此時底層在進(jìn)行數(shù)據(jù)傳輸時看到的統(tǒng)一都是二進(jìn)制序列
- 我發(fā)的是二進(jìn)制數(shù)據(jù),所以對方收到的也是二進(jìn)制數(shù)據(jù),所以需要進(jìn)行反序列化,將二進(jìn)制數(shù)據(jù)轉(zhuǎn)化為上層能夠識別的比如字符串,整型數(shù)據(jù)
二,網(wǎng)絡(luò)版計算器實現(xiàn)準(zhǔn)備
前置博客:計算機網(wǎng)絡(luò)(三) —— 簡單Udp網(wǎng)絡(luò)程序-CSDN博客
計算機網(wǎng)絡(luò)(四) —— 簡單Tcp網(wǎng)絡(luò)程序-CSDN博客
下面我們來全程手搓一個網(wǎng)絡(luò)版計算器服務(wù),并且我們自己實現(xiàn)一個自定義協(xié)議,主要是為了感受一下協(xié)議的實現(xiàn),后面我們就不會再自定義協(xié)議了,直接用現(xiàn)成的
2.1 套用舊頭文件
源代碼下載:計算機網(wǎng)絡(luò)/自定義協(xié)議——網(wǎng)絡(luò)版計算器 · 小堃學(xué)編程/Linux學(xué)習(xí) - 碼云 - 開源中國 (gitee.com)
網(wǎng)絡(luò)版計算器我們要用到的頭文件有以下幾個:
?
?我們先把前面寫的頭文件套用一下:
makefile:
.PHONY:all
all:servercal clientcalFlag=#-DMySelf=1
Lib=-ljsoncpp #這個是后面使用json頭文件時要用的servercal:ServerCal.ccg++ -o $@ $^ -std=c++11 $(Lib) $(Flag)
clientcal:ClientCal.ccg++ -o $@ $^ -std=c++11 -g $(Lib) $(Flag).PHONY:clean
clean:rm -f clientcal servercal
Log.hpp:
#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); // "log.txt"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); // "log.txt.Debug/Warning/Fatal"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);// 格式:默認(rèn)部分+自定義部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// printf("%s", logtxt); // 暫時打印printLog(level, logtxt);}private:int printMethod;std::string path;
};Log log;
Deamon.hpp:
#pragma once#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";void Daemon(const std::string &cwd = "")
{// 1. 忽略其他異常信號signal(SIGCLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);signal(SIGSTOP, SIG_IGN);// 2. 將自己變成獨立的會話if (fork() > 0)exit(0);setsid();// 3. 更改當(dāng)前調(diào)用進(jìn)程的工作目錄if (!cwd.empty())chdir(cwd.c_str());// 4. 標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出,標(biāo)準(zhǔn)錯誤重定向至/dev/nullint fd = open(nullfile.c_str(), O_RDWR);if (fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}
}
2.2 封裝sock API
在Udp和Tcp服務(wù)器編寫時,可以發(fā)現(xiàn)在使用sock API以及填裝sockaddr結(jié)構(gòu)體時,步驟都非常相似,所以我們可以把這些相似的步驟都封裝起來,下面是Socket.hpp的代碼:
#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"#include <cstring>enum{SocketErr = 2,BindErr,ListenErr,
};const int backlog = 10;class Sock
{
public:Sock(){}~Sock(){}public:void Socket() // 創(chuàng)建套接字{_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){log(Fatal, "socket 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) // 如果小于0就綁定失敗{log(Fatal, "bind error, %s: %d", strerror(errno), errno);exit(BindErr);}}void Listen() // 監(jiān)聽套接字{if (listen(_sockfd, backlog) < 0) // 如果小于0就代表監(jiān)聽失敗{log(Fatal, "listen error, %s: %d", strerror(errno), errno);exit(ListenErr);}}int Accept(std::string *clientip, uint16_t *clientport) // 獲取連接,參數(shù)做輸出型參數(shù){struct sockaddr_in peer;socklen_t len = sizeof(peer);int newfd = accept(_sockfd, (struct sockaddr *)(&peer), &len);if (newfd < 0) // 獲取失敗{log(Warning, "accept error, %s: %d", strerror(errno), errno);return -1;}char ipstr[64];inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr)); // 把網(wǎng)絡(luò)字節(jié)序列轉(zhuǎn)化為字符串保存在ipstr數(shù)組里供用戶讀取*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;
};
三,自定義協(xié)議
3.1 關(guān)于自定義協(xié)議
在之前的文章中介紹過,任何的網(wǎng)絡(luò)協(xié)議,都要提供兩種功能,下面是博客的截圖:計算機網(wǎng)絡(luò)(一) —— 網(wǎng)絡(luò)基礎(chǔ)入門_計算機網(wǎng)絡(luò)基礎(chǔ)教程-CSDN博客
網(wǎng)絡(luò)版計算器,用戶會在命令行輸入三個字符:"1","+","1",然后我們可以拼接成一個長字符串:"1 + 1",數(shù)字與運算符通過一個空格隔開,
但是,如果客戶端連續(xù)發(fā)了兩個字符串,那么最終服務(wù)器收到的報文就是“1 + 12 + 1”,可以發(fā)現(xiàn),兩個字符串粘在了一起,所以我們的自定義協(xié)議,不僅僅要提供將報文和有效載荷分離的能力,也要提供將報文與報文分開的能力,有下面兩種方法:
- 方案一,用特殊字符隔開報文與報文 --> "1 + 1" \n "2 + 2"
- 方案二,在報文前面加上報文的長度,也就是報頭 --> "9"\n"100 + 200"\n,這樣就為一個完整的報文(其實只要有長度就可以了,這里增加\n是為了可讀性,也是為了方便后面打印)
所以下面來梳理一下我們自定義協(xié)議的序列化和反序列化全流程:
3.2??實現(xiàn)序列化和反序列化
這個部分就是具體實現(xiàn)Protocol.hpp頭文件了,這個文件具體包含下面幾個內(nèi)容:
- "100","+","200" --> "100 + 200"
- "100 + 200" --> "9"\n"100 + 200"
- ?"9"\n"100 + 200" --> "100 + 200"
- "100 + 200" --> "100","+","200"
該文件包含兩個類,一個類是請求類,是客戶端發(fā)給服務(wù)器用到的類;另一個類是響應(yīng)類,是服務(wù)器處理完后,返回給客戶端的類;此外還包括兩個方法,分別是封裝報頭和將報頭和有效載荷分離
Request類:
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>#define MySelf 0 // 去掉注釋就是用我們自己的序列化和反序列化,加上注釋就是用json庫提供的const std::string blank_space = " "; // 分隔符
const std::string protocol_sep = "\n";class Request // 計算的請求
{
public:Request(int data1, int data2, char oper): x(data1), y(data2), op(oper){}Request(){}~Request(){}public:bool Serialize(std::string *out) // 序列化{
#ifdef MySelf// 1,構(gòu)建報文的有效載荷// 需要把結(jié)構(gòu)化的數(shù)據(jù)轉(zhuǎn)化為字符串 struct --> string, "x op y"std::string s = std::to_string(x);s += blank_space;s += op;s += blank_space;s += std::to_string(y);// 走到這里的時候就是字符串 "x op y"// 但是在傳輸?shù)臅r候可能發(fā)過來的不是完整的一個報文:"10 + 20",而是只有半個報文:"10 + "// 解決方案一:用特殊字符隔開報文與報文 --> "10 + 20" \n "20 + 40"// 解決方案二:再在報文前面加一個字符串的長度也就是報頭,例如s.size()// 結(jié)合起來就是"9"\n"100 + 200"\n,為一個完整的報文,其實只要有長度就可以了,這里增加\n是為了可讀性,也是為了方便后面// 2,封裝報頭*out = s;return true;
#elseJson::Value root;root["x"] = x;root["y"] = y;root["op"] = op;Json::FastWriter w;*out = w.write(root);return true;#endif}bool DeSerialize(const std::string &in) // 反序列化 "9"\n"10 + 20"{
#ifdef MySelfstd::size_t left = in.find(blank_space); // 找空格的左邊,"10 + 20",也就是找10的右邊位置if (left == std::string::npos) // 沒找到空格,說明當(dāng)前解析錯誤{return false;}std::string part_x = in.substr(0, left); // 截取第一個數(shù)字,也就是10std::size_t right = in.rfind(blank_space); // 逆向再次找空格,"10 + 20",找20左邊的位置if (right == std::string::npos) // 沒找到空格,說明當(dāng)前解析錯誤{return false;}std::string part_y = in.substr(right + 1); // 截取后面的數(shù)字,也就是20,+1是因為找到的是空格的右邊,+1跳過空格才是數(shù)字if (left + 2 != right)return false; // 數(shù)字中間還有運算符,所以left+2就應(yīng)該是right的左邊那個空格的左邊位置,如果不是那么就是解析錯誤op = in[left + 1]; // 拿到運算符// op = in[right - 1]; //一樣的x = std::stoi(part_x); // 拿到數(shù)字y = std::stoi(part_y);return true;
#elseJson::Value root;Json::Reader r;r.parse(in, root);x = root["x"].asInt();y = root["y"].asInt();op = root["op"].asInt();return true;
#endif}void DebugPrint(){std::cout << "新請求構(gòu)建完成: " << x << " " << op << " " << y << "=?" << std::endl;}public:int x;int y;char op; // 運算符
};class Response // 計算的應(yīng)答
{
public:Response(int res, int c): result(res), code(c){}Response(){}~Response(){}public:bool Serialize(std::string *out) // 序列化{
#ifdef MySelf// 1,構(gòu)建報文的有效載荷//"len"\n"result code"std::string s = std::to_string(result);s += blank_space;s += std::to_string(code);*out = s;return true;
#elseJson::Value root;root["result"] = result;root["code"] = code;// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}bool DeSerialize(const std::string &in) // 反序列化{
#ifdef MySelf// 對服務(wù)器發(fā)過來的結(jié)果報文做解析: "result code"std::size_t pos = in.find(blank_space); // 找空格的左邊if (pos == std::string::npos) // 沒找到空格,說明當(dāng)前解析錯誤{return false;}std::string part_left = in.substr(0, pos); // 截取第一個數(shù)字,也就是resultstd::string part_right = in.substr(pos + 1); // 截取后面第二個數(shù)字,也就是coderesult = std::stoi(part_left);code = std::stoi(part_right);return true;
#elseJson::Value root;Json::Reader r;r.parse(in, root);result = root["result"].asInt();code = root["code"].asInt();return true;
#endif}void DebugPrint(){std::cout << "結(jié)果響應(yīng)完成, result: " << result << ", code: " << code << std::endl;}public:int result; // x op yint code; // 錯誤碼,為0時結(jié)果正確,為其它數(shù)時對應(yīng)的數(shù)表示對應(yīng)的原因
};
?Response類:
class Response // 計算的應(yīng)答
{
public:Response(int res, int c): result(res), code(c){}Response(){}~Response(){}public:bool Serialize(std::string *out) // 序列化{
#ifdef MySelf// 1,構(gòu)建報文的有效載荷//"len"\n"result code"std::string s = std::to_string(result);s += blank_space;s += std::to_string(code);*out = s;return true;
#elseJson::Value root;root["result"] = result;root["code"] = code;// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}bool DeSerialize(const std::string &in) // 反序列化{
#ifdef MySelf// 對服務(wù)器發(fā)過來的結(jié)果報文做解析: "result code"std::size_t pos = in.find(blank_space); // 找空格的左邊if (pos == std::string::npos) // 沒找到空格,說明當(dāng)前解析錯誤{return false;}std::string part_left = in.substr(0, pos); // 截取第一個數(shù)字,也就是resultstd::string part_right = in.substr(pos + 1); // 截取后面第二個數(shù)字,也就是coderesult = std::stoi(part_left);code = std::stoi(part_right);return true;
#elseJson::Value root;Json::Reader r;r.parse(in, root);result = root["result"].asInt();code = root["code"].asInt();return true;
#endif}void DebugPrint(){std::cout << "結(jié)果響應(yīng)完成, result: " << result << ", code: " << code << std::endl;}public:int result; // x op yint code; // 錯誤碼,為0時結(jié)果正確,為其它數(shù)時對應(yīng)的數(shù)表示對應(yīng)的原因
};
?添加和去掉報頭函數(shù):
std::string Encode(const std::string &content) // 添加報頭
{std::string packge = std::to_string(content.size()); // 加報頭packge += protocol_sep; // 加\npackge += content; // 加正文packge += protocol_sep; // 再加\nreturn packge;
}bool Decode(std::string &package, std::string *content) // 解析并去掉報頭 "9"\n"10 + 20"\n -->"10 + 20" 俗稱解包,但是只是去掉了報頭,沒有做報文的具體解析
{std::size_t pos = package.find(protocol_sep); // 找到\n的左邊if (pos == std::string::npos)return false; // 解析失敗std::string len_str = package.substr(0, pos); // 從開始截到我找到的\n處,把前面的9給截出來std::size_t len = std::stoi(len_str); // 把截出來的報頭轉(zhuǎn)化為size_t,也就是把字符串9轉(zhuǎn)化成數(shù)字9// packge的長度 = 報頭長度len_str + 有效載荷長度content_str + 兩個\n 2std::size_t total_len = len_str.size() + len + 2;// ①找到了第一個\n說明一定有長度,如果沒找到\n就說明連報頭都沒有// ②有了長度報頭,你也還得保證后面的內(nèi)容也是完整的,如果不完整也就是長度不一樣,那我也就不玩了if (package.size() < total_len)return false;// 走到這一步說明我們能保證報文是完整的,開始拿有效載荷*content = package.substr(pos + 1, len); // pos現(xiàn)在是第一個\n左邊的位置,+1后面的就是正文內(nèi)容,正文內(nèi)容長度為len// 移除一個報文,該功能需要和網(wǎng)絡(luò)相結(jié)合package.erase(0, total_len);return true;
}
3.3 測試
我們可以在ServerCal.cc文件里測試上面我們的序列化和反序列化操作
先測試Request:
ServerCal.cc:
#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"int main()
{// Request測試--------------------Request req(10, 20, '+');std::string s;req.Serialize(&s);std::cout << "有效載荷為: " << s << std::endl;s = Encode(s);std::cout << "報文為:" << s;std::string content;bool r = Decode(s, &content); //分離報頭和有效載荷std::cout << "分離報頭后的有效載荷為: "<< content << std::endl;Request temp;temp.DeSerialize(content); //解析有效載荷std::cout<< "有效載荷分離后, x為: " << temp.x << " 運算符為:\"" << temp.op << "\" y為: " << temp.y << std::endl;return 0;
}
然后是Response的測試:?
ServerCal.cc:
#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"int main()
{// Response測試--------------------Response resp(10, 20);std::string s;resp.Serialize(&s);std::cout << "有效載荷為: " << s << std::endl;std::string package = Encode(s); //分離報頭和有效載荷std::cout << "報文為: " << package;s = "";bool r = Decode(package, &s);std::cout << "分離報頭后的有效載荷為: " << s << std::endl;Response temp;temp.DeSerialize(s); // 解析有效載荷std::cout << "解析有效載荷: " << std::endl;std::cout << "結(jié)果為: " << temp.result << std::endl;std::cout << "錯誤碼為: " << temp.code << std::endl;return 0;
}
三,服務(wù)器實現(xiàn)
3.1 邏輯梳理
服務(wù)器涉及兩個個頭文件和一個源文件,有點繞,下面先梳理一下:
有三個文件:
- 首先,TcpServer.hpp是服務(wù)器主函數(shù),ServerCal.cc包含服務(wù)器初始化和啟動的main函數(shù),ServerCal.hpp是進(jìn)行計算器運算的頭文件
- 首先構(gòu)建服務(wù)器對象,并在構(gòu)造函數(shù)里將ServerCal.cc里面的運算函數(shù)帶進(jìn)去,然后是初始化服務(wù)器,執(zhí)行創(chuàng)建套接字等操作,然后啟動服務(wù)器
- 當(dāng)服務(wù)器收到客戶端發(fā)來的報文后,直接將報文傳給運算函數(shù),由運算函數(shù)做去掉報頭,解析有效載荷等過程,并執(zhí)行運算,最后把運算結(jié)果再次構(gòu)建成響應(yīng)報文,以返回值形式返回給服務(wù)器運行函數(shù)
- 然后服務(wù)器再把響應(yīng)報文發(fā)給客戶端,完成一次計算請求處理
3.2 各頭文件實現(xiàn)
Server.hpp實現(xiàn):
#pragma once
#include <iostream>
#include <string>
#include "Protocol.hpp"enum
{Div_Zero = 1,Mod_Zero,Other_Oper
};class ServerCal
{
public:ServerCal(){}~ServerCal(){}Response CalculatorHelper(const Request &req){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 (req.y == 0){resp.code = Div_Zero;}else{resp.result = req.x / req.y;}}break;case '%':{if (req.y == 0){resp.code = Mod_Zero;}else{resp.result = req.x % req.y;}}break;default:resp.code = Other_Oper;break;}return resp;}std::string Calculator(std::string &package){std::string content;if (!Decode(package, &content)) // 分離報頭和有效載荷:"len"\n"10 + 20"\nreturn "";// 走到這里就是完整的報文Request req;if (!req.DeSerialize(content)) // 反序列化,解析有效載荷 "10 + 20" --> x=10 op="+" y=20return "";content = "";Response resp = CalculatorHelper(req); // 執(zhí)行計算邏輯resp.Serialize(&content); // 序列化計算結(jié)果的有效載荷 result=10, code=0content = Encode(content); // 將有效載荷和報頭封裝成響應(yīng)報文 "len"\n"30 0"return content;}
};
TcpServer.hpp實現(xiàn):
#pragma once
#include "Log.hpp"
#include "Socket.hpp"
#include <signal.h>
#include <string>
#include <functional>using func_t = std::function<std::string(std::string &package)>;class TcpServer
{
public:TcpServer(uint16_t port, func_t callback): _port(port), _callback(callback){}bool InitServer(){// 創(chuàng)建,綁定,監(jiān)聽套接字_listensockfd.Socket();_listensockfd.Bind(_port);_listensockfd.Listen();log(Info, "Init server... done");return true;}void Start(){signal(SIGCHLD, SIG_IGN); // 忽略signal(SIGPIPE, SIG_IGN);while (true){std::string clientip;uint16_t clientport;int sockfd = _listensockfd.Accept(&clientip, &clientport);if (sockfd < 0)continue;log(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);// 走到了這里就是成功獲取發(fā)起連接方IP與port,后面就是開始提供服務(wù)if (fork() == 0){_listensockfd.Close();// 進(jìn)行數(shù)據(jù)運算服務(wù)std::string inbuffer_stream;while (true){char buffer[1280];ssize_t n = read(sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;inbuffer_stream += buffer; // 這里用+=log(Debug, "debug:\n%s", inbuffer_stream.c_str());while (true){std::string info = _callback(inbuffer_stream);// if (info.size() == 0) //ServerCal.hpp,解析報文失敗的話會返回空串if (info.empty()) // 空的話代表inbuffstream解析時出問題,表示不遵守協(xié)議,發(fā)不合法的報文給我,我直接丟掉不玩了break; // 不能用continuelog(Debug, "debug, response:\n%s", info.c_str());log(Debug, "debug:\n%s", inbuffer_stream.c_str());write(sockfd, info.c_str(), info.size());}}else if (n == 0) // 讀取出錯break;else // 讀取出錯break;}exit(0);}close(sockfd);}}~TcpServer(){}private:uint16_t _port;Sock _listensockfd;func_t _callback;
};
ServerCal.cc實現(xiàn):
#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"static void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << "port\n\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);ServerCal cal;TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));tsvp->InitServer();//Daemon();//daemon(0, 0);tsvp->Start();// Request測試--------------------// Request req(10, 20, '+');// std::string s;// req.Serialize(&s);// std::cout << "有效載荷為: " << s << std::endl;// s = Encode(s);// std::cout << "報文為:" << s;// std::string content;// bool r = Decode(s, &content); //分離報頭和有效載荷// std::cout << "分離報頭后的有效載荷為: "<< content << std::endl;// Request temp;// temp.DeSerialize(content); //解析有效載荷// std::cout<< "有效載荷分離后, x為: " << temp.x << " 運算符為:\"" << temp.op << "\" y為: " << temp.y << std::endl;// Response測試--------------------// Response resp(10, 20);// std::string s;// resp.Serialize(&s);// std::cout << "有效載荷為: " << s << std::endl;// std::string package = Encode(s); //分離報頭和有效載荷// std::cout << "報文為: " << package;// s = "";// bool r = Decode(package, &s);// std::cout << "分離報頭后的有效載荷為: " << s << std::endl;// Response temp;// temp.DeSerialize(s); // 解析有效載荷// std::cout << "解析有效載荷: " << std::endl;// std::cout << "結(jié)果為: " << temp.result << std::endl;// std::cout << "錯誤碼為: " << temp.code << std::endl;return 0;
}
四,客戶端實現(xiàn)
客戶端的話,為了方便發(fā)送計算請求,會采用隨機數(shù)的方式獲取運算數(shù)和運算符,如下代碼:
#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"static void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]); //獲取IP和端口Sock sockfd;sockfd.Socket();if (!sockfd.Connect(serverip, serverport))return 1;srand(time(nullptr) ^ getpid()); // 種隨機數(shù)種子int cnt = 1;const std::string opers = "+-*/%-=&^";std::string inbuffer_stream;while (cnt <= 5){std::cout << "===============第" << cnt << "次測試....., " << "===============" << std::endl;int x = rand() % 100 + 1;usleep(1234);int y = rand() % 100;usleep(4321);char oper = opers[rand() % opers.size()];Request req(x, y, oper);req.DebugPrint();// 下面是根據(jù)協(xié)議發(fā)送給對方std::string package;req.Serialize(&package); // 序列化package = Encode(package); // 形成報文int fd = sockfd.Fd(); // 獲取套接字write(fd, package.c_str(), package.size()); // 將請求從客戶端往服務(wù)端寫過去// 下面是讀取服務(wù)器發(fā)來的結(jié)果并解析char buffer[128];ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer)); // 讀取服務(wù)器發(fā)回來的結(jié)果,但是這里也無法保證能讀取到一個完整的報文if (n > 0) // 讀成功了{(lán)buffer[n] = 0;inbuffer_stream += buffer; // "len"\n"result code"\nstd::cout << inbuffer_stream << std::endl;std::string content;bool r = Decode(inbuffer_stream, &content); // 去掉報頭"result code"\nassert(r); // r為真說明報頭成功去掉Response resp;r = resp.DeSerialize(content); // 對有效荷載進(jìn)行反序列化assert(r);resp.DebugPrint(); // 打印結(jié)果}std::cout << "=================================================" << std::endl;sleep(1);cnt++;}sockfd.Close();return 0;
}
?效果演示: