【計算機網絡_應用層】協議定制序列化反序列化

文章目錄

  • 1. TCP協議的通信流程
  • 2. 應用層協議定制
  • 3. 通過“網絡計算器”的實現來實現應用層協議定制和序列化
    • 3.1 protocol
    • 3.2 序列化和反序列化
      • 3.2.1 手寫序列化和反序列化
      • 3.2.2 使用Json庫
    • 3.3 數據包讀取
    • 3.4 服務端設計
    • 3.5 最后的源代碼和運行結果

1. TCP協議的通信流程

在之前的代碼中,相信大家對TCP的通信過程的代碼已經有了一定了了解。在很早之前就了解到過一些網絡通信的相關描述,比如TCP的三次握手和四次揮手。那么什么是三次握手和四次揮手呢?

在介紹之前我們首先看一個圖,通過這個圖來了解,接下來我們講解這張圖:

ca04d7ca00e56d5855fd5d0bc694bc6d

在最開始的時候客戶端和服務器都是處于關閉狀態的。

1. 開始前的準備

  1. 服務端和客戶端在任意時刻在應用層調用socket函數分配一個文件描述符
  2. 服務端顯示bind指定端口和任意IP地址
  3. 服務端調用listen使對應的文件描述符成為一個監聽描述符
  4. 服務端調用accept阻塞等待客戶端的連接(至此,服務端在通信錢的準備已經完成

2. 三次握手

  1. 客戶端調用connect函數向服務器發起連接請求,然后阻塞自己等待完成

  2. 服務端收到客戶端的連接請求之后由OS完成連接然后accept調用完成

    這里connect是三次握手的開始,accept調用完成時三次握手一定已經結束了,三次握手是OS內部自己完成的在TCP層我們感知不到

3. 四次揮手

四次揮手的工作都是由雙方的OS完成,而我們決定什么時候揮手,一旦調用系統調用close,應用層就不用管了

2. 應用層協議定制

我們在第一次談到協議的時候就說協議其實就是一種約定。在此之前,我們也寫過一些UDP和TCP的通信代碼,使用過一些socket API,我們可以發現socket API在發送數據的時候都是按照“字符串”的形式來發送和接收的,那如果我們要傳輸一些結構化的數據該怎么辦呢?

比如在發送一條QQ消息的時候,需要帶上發消息的人的昵稱、QQ號、消息本身等等,這些消息必須要一次性綁定的發送,那么我們在發送的時候就需要把這些內容打包成一個“字符串”來發送

為什么不直接發送一個結構體對象?

網絡通信涉及到不同的機器,可能出現大小段問題和內存對齊問題等等,所以不能直接發送結構體

這個打包成一個字符串的過程就是序列化,將收到的一個字符串轉化為多個信息的過程就是反序列化

那么最終我們發送的消息就可以看作是一個完整的Content,但是TCP通信是面向字節流的,所以在通信的過程中,我們也沒有辦法知道一次發送過來的數據里面有幾個完整的Content,這就需要在應用層定制一些“協議”來保證能區分每個數據包,一般來說我們有以下幾種方法

1. 確保每個數據包是定長的; 2. 用特殊符號來表示結尾; 3. 自描述

注意:這里序列化反序列化和協議定制是兩碼事。序列化反序列化的作用是將要發送的信息變成一整條消息;協議定制的作用是保證每次讀取一整個數據包,這個數據包里面會包含包頭和有效載荷,這個有效載荷就是我們所說的“一整條消息”

3. 通過“網絡計算器”的實現來實現應用層協議定制和序列化

3.1 protocol

設計思想:實現兩個類:request用于存儲對應的運算請求,存放算式,包括兩個操作數和一個操作符。response表示對應請求的響應,也就是運算的結果狀態和運算結果。最終經過系列化和反序列化之后形成一個字符串形式的有效載荷,我們在這個有效載荷前面加上報頭信息,這里我們**約定:報頭的內容是一個字符串格式的數據,存放的是有效載荷的長度,有效載荷和報頭之間存在一個分隔符**

這里的約定就是我們的協議

既然有了應用層的通信協議,那么我們就要實現對應的為有效載荷添加報頭和去除報頭

std::string enLength(const std::string &text) // 在text上加報頭
{// "content_len"\r\t"text"\r\tstd::string send_string = std::to_string(text.size());send_string += LINE_SEP;send_string += text;send_string += LINE_SEP;return send_string;
}
bool deLength(const std::string &package, std::string *text) // 從package上去報頭
{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;
}

3.2 序列化和反序列化

3.2.1 手寫序列化和反序列化

按照我們的約定,我們希望發送的結構化的數據就是Request和Response,里面有一些特定的字段

enum // 協議定義的相關錯誤枚舉
{OK = 0,DIV_ZERO,MOD_ZERO,OP_ERROR
};
class Request // 客戶端請求數據
{
public:int x;int y;char op;
};
class Response // 服務器響應數據
{
public:int exitcode;int result;
};

那么對于結構化的數據,我們要首先將其序列化,才能夠作為有效載荷去添加報頭,然后發送。接收到發送的數據去除報頭之后的有效載荷,同樣需要進行反序列化才能拿到結構化的數據,進行操作

#define SEP " "                       // 分隔符
#define SEP_LEN strlen(SEP)           // 分隔符長度
#define LINE_SEP "\r\n"               // 行分隔符(分隔報頭和有效載荷)
#define LINE_SEP_LEN strlen(LINE_SEP) // 行分隔符長度
// class Request // 客戶端請求數據
bool serialize(std::string *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 y"
bool deserialize(std::string &in) // 反序列化
{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 - SEP_LEN - left != 1)return false; // op的長度不為1std::string left_str = in.substr(0, left);std::string right_str = in.substr(right + SEP_LEN);if (left_str.empty() || right_str.empty())return false;x = std::stoi(left_str);y = std::stoi(right_str);op = in[left + SEP_LEN];return true;
}
// class Response // 服務器響應數據
bool serialize(std::string *out) // 序列化
{// "exitcode result"*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(std::string &in) // 反序列化 "exitcode result"
{auto pos = in.find(SEP);if (pos == std::string::npos)return false;std::string ec_string = in.substr(0, pos);std::string res_string = in.substr(pos + SEP_LEN);if (ec_string.empty() || res_string.empty())return false;exitcode = std::stoi(ec_string);result = std::stoi(res_string);return true;
}

3.2.2 使用Json庫

我們會發現手寫序列化好麻煩 ,那么實際上有人已經幫我們做過這件事情了,提供了一些可以使用的組件,我們只需要按照規則使用即可。常用的序列化和反序列化工具有1. Json; 2. protobuf; 3. xml。這里我們為了使用的方便,采用Json來寫。(protobuf在之后的博文會更新使用方式)

// class Request // 客戶端請求數據
bool serialize(std::string *out) // 序列化
{Json::Value root; // Json::Value 是一個KV結構。首先定義出這個結構root["first"] = x; // 按照KV結構的模式,為每個字段添加一個Key,給這個字段賦值root["second"] = y;root["oper"] = op;Json::FastWriter writer; // FastWriter是一個序列化的類,里面提供了write方法,這個方法可以將Value的對象轉成std::string*out = writer.write(root); // 轉換后的字符串就是序列化后的結果return true;
}
bool deserialize(std::string &in) // 反序列化
{Json::Value root; // 序列化后的結果需要被存放Json::Reader reader; // Reader類是用作讀取的,里面提供了parse(解析)方法,可以將對應的序列化結果string轉化成Value對象reader.parse(in, root);x = root["first"].asInt();// 按照KV結構的模式將存放的內容提取出來,提取出來的結果的類型是Json內部的,要使用的時候需要指定類型y = root["second"].asInt();op = root["oper"].asInt();return true;
}// class Response // 服務器響應數據
bool serialize(std::string *out) // 序列化
{Json::Value root;root["first"] = exitcode;root["second"] = result;Json::FastWriter writer;*out = writer.write(root);return true;
}
bool deserialize(std::string &in) // 反序列化 "exitcode result"
{Json::Value root;Json::Reader reader;reader.parse(in, root);exitcode = root["first"].asInt();result = root["second"].asInt();return true;
}

Json庫不是標準庫的內容,所以在使用之前需要安裝,在cent OS下的安裝命令

sudo yum install -y jsoncpp-devel # 安裝json

安裝之后編譯我們的代碼會報錯么?當然會!因為我們沒有鏈接

cc=g++.PHONY:all
all:Server ClientServer:calServer.cc$(cc) -o $@ $^ -lpthread -ljsoncpp -std=c++11 # 這里加上-ljsoncppClient:calClient.cc$(cc) -o $@ $^ -ljsoncpp -std=c++11 # 這里加上-ljsoncpp.PHONY:clean
clean:rm -f Server Client

3.3 數據包讀取

首先明確一點:TCP協議是面向字節流的,不能確定是否當前收到的就是一個完整的報文,所以需要進行判斷與讀取

這里我們采用的方法是:如果讀取到一個完整的報文就進行后續處理,如果沒有讀取到一個完整的報文,那就繼續讀取,直到遇到完整報文再處理

/*** sock:讀取對應套接字的報文* inbuffer:接收緩沖區,這里存放接收到的所有數據* req_text:輸出型參數,如果讀到完整報文就將報文內容存放到req_text中* 返回值:讀取成功返回true,失敗返回false
*/
bool recvPackage(int sock, std::string &inbuffer, std::string *req_text)
{char buffer[1024];while (true){ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 接收數據if (n > 0){buffer[n] = 0;      // 當前本次接收的數據inbuffer += buffer; // 放在inbuffer后面,處理整個inbufferauto 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 + 2 * LINE_SEP_LEN + text_len_string.size(); // 報文總長度if (inbuffer.size() < total_len){// 收到的信息不是一個完整的報文continue;}// 到這里就拿到了一個完整的報文*req_text = inbuffer.substr(0, total_len);inbuffer.erase(0, total_len); // 在緩沖區中刪除拿到的報文return true;}elsereturn false;}
}

3.4 服務端設計

按照我們在上一篇博文的多進程版本設計,這里服務端將會讓一個孫子進程來執行相關的操作,其中孫子進程需要執行的任務分為5個步驟:

1. 讀取報文,讀取到一個完整報文之后去掉報頭; 2. 將有效載荷反序列化; 3. 進行業務處理(回調); 4. 將響應序列化; 5. 將徐姐話的響應數據構建成一個符合協議的報文發送回去

void handleEntery(int sock, func_t func) // 服務端調用
{std::string inbuffer;// 接收緩沖區while(true){// 1. 讀取數據std::string req_text, req_str;// 1.1 讀到一個完整的請求(帶報頭)req_text = "content_len"\r\t"x op y"\r\tif(!recvPackage(sock, inbuffer, &req_text)) return;// 1.2 將req_text解析成req_str(不帶報頭)"x op y"if(!deLength(req_text, &req_str)) return;// 2. 數據反序列化Request req;if(!req.deserialize(req_str)) return;// 3. 業務處理Response resp;func(req, resp);// 4. 數據序列化std::string send_str;if(!resp.serialize(&send_str)) return;// 5. 發送響應數據// 5.1 構建一個完整的報文std::string resp_str = enLength(send_str);// 5.2 發送send(sock, resp_str.c_str(), resp_str.size(), 0);}
}

對應需要執行的內容我們就在業務邏輯層來處理

bool cal(const Request &req, Response &resp)
{// 此時結構化的數據就在req中,可以直接使用resp.exitcode = OK;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.exitcode = DIV_ZERO;elseresp.result = req.x / req.y;}break;case '%':{if (req.y == 0)resp.exitcode = MOD_ZERO;elseresp.result = req.x % req.y;}break;default:resp.exitcode = OP_ERROR;break;}
}

3.5 最后的源代碼和運行結果

/*calServer.hpp*/
#pragma once#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>#include <string>
#include <functional>#include "log.hpp"
#include "protocol.hpp"namespace Server
{enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;static const int gbacklog = 5;typedef std::function<bool(const Request &req, Response &resp)> func_t;void handleEntery(int sock, func_t func) // 服務端調用{std::string inbuffer;// 接收緩沖區while(true){// 1. 讀取數據std::string req_text, req_str;// 1.1 讀到一個完整的請求(帶報頭)req_text = "content_len"\r\t"x op y"\r\tif(!recvPackage(sock, inbuffer, &req_text)) return;// 1.2 將req_text解析成req_str(不帶報頭)"x op y"if(!deLength(req_text, &req_str)) return;// 2. 數據反序列化Request req;if(!req.deserialize(req_str)) return;// 3. 業務處理Response resp;func(req, resp);// 4. 數據序列化std::string send_str;if(!resp.serialize(&send_str)) return;// 5. 發送響應數據// 5.1 構建一個完整的報文std::string resp_str = enLength(send_str);// 5.2 發送send(sock, resp_str.c_str(), resp_str.size(), 0);}}class tcpServer;class ThreadData // 封裝線程數據,用于傳遞給父進程{public:ThreadData(tcpServer *self, int sock) : _self(self), _sock(sock) {}public:tcpServer *_self;int _sock;};class tcpServer{public:tcpServer(uint16_t &port) : _port(port){}void initServer(){// 1. 創建socket文件套接字對象_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock == -1){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success:%d", _listensock);// 2.bind自己的網絡信息sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;int n = bind(_listensock, (struct sockaddr *)&local, sizeof local);if (n == -1){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");// 3. 設置socket為監聽狀態if (listen(_listensock, gbacklog) != 0) // listen 函數{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}void start(func_t func){while (true){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;}// version 2:多進程版本pid_t id = fork();if (id == 0){close(_listensock); // 子進程不會使用監聽socket,但是創建子進程的時候寫時拷貝會拷貝,這里先關掉// 子進程再創建子進程if (fork() > 0)exit(0); // 父進程退出// 走到當前位置的就是子進程handleEntery(sock, func); // 使用close(sock);     // 關閉對應的通信socket(這里也可以不關閉,因為此進程在下個語句就會退出)exit(0);         // 孫子進程退出}// 走到這里的是監聽進程(爺爺進程)pid_t n = waitpid(id, nullptr, 0);if (n > 0){logMessage(NORMAL, "wait success pid:%d", n);}close(sock);}}~tcpServer() {}private:uint16_t _port;int _listensock;};} // namespace Server
/*calServer.cc*/
#include <iostream>
#include <memory>#include "calServer.hpp"
#include "protocol.hpp"using namespace Server;static void Usage(const char *proc)
{std::cout << "\n\tUsage:" << proc << " local_port\n";
}bool cal(const Request &req, Response &resp)
{// 此時結構化的數據就在req中,可以直接使用resp.exitcode = OK;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.exitcode = DIV_ZERO;elseresp.result = req.x / req.y;}break;case '%':{if (req.y == 0)resp.exitcode = MOD_ZERO;elseresp.result = req.x % req.y;}break;default:resp.exitcode = OP_ERROR;break;}
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<tcpServer> tsvr(new tcpServer(port));tsvr->initServer();tsvr->start(cal);return 0;
}
/*protocol.hpp*/
#pragma once#include <cstring>
#include <string>
#include <jsoncpp/json/json.h>#define SEP " "                       // 分隔符
#define SEP_LEN strlen(SEP)           // 分隔符長度
#define LINE_SEP "\r\n"               // 行分隔符(分隔報頭和有效載荷)
#define LINE_SEP_LEN strlen(LINE_SEP) // 行分隔符長度enum // 協議定義的相關錯誤枚舉
{OK = 0,DIV_ZERO,MOD_ZERO,OP_ERROR
};std::string enLength(const std::string &text) // 在text上加報頭
{// "content_len"\r\t"text"\r\tstd::string send_string = std::to_string(text.size());send_string += LINE_SEP;send_string += text;send_string += LINE_SEP;return send_string;
}
bool deLength(const std::string &package, std::string *text) // 從package上去報頭
{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;
}class Request // 客戶端請求數據
{
public:Request() {}Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_) {}bool serialize(std::string *out) // 序列化 -> "x op y"{
#ifdef MYSELFstd::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;
#elseJson::Value root; // Json::Value 是一個KV結構。首先定義出這個結構root["first"] = x; // 按照KV結構的模式,為每個字段添加一個Key,給這個字段賦值root["second"] = y;root["oper"] = op;Json::FastWriter writer; // FastWriter是一個序列化的類,里面提供了write方法,這個方法可以將Value的對象轉成std::string*out = writer.write(root); // 轉換后的字符串就是序列化后的結果
#endifreturn true;}// "x op y"bool deserialize(std::string &in) // 反序列化{
#ifdef MYSELFauto 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 - SEP_LEN - left != 1)return false; // op的長度不為1std::string left_str = in.substr(0, left);std::string right_str = in.substr(right + SEP_LEN);if (left_str.empty() || right_str.empty())return false;x = std::stoi(left_str);y = std::stoi(right_str);op = in[left + SEP_LEN];
#elseJson::Value root; // 序列化后的結果需要被存放Json::Reader reader; // Reader類是用作讀取的,里面提供了parse(解析)方法,可以將對應的序列化結果string轉化成Value對象reader.parse(in, root);x = root["first"].asInt();// 按照KV結構的模式將存放的內容提取出來,提取出來的結果的類型是Json內部的,要使用的時候需要指定類型y = root["second"].asInt();op = root["oper"].asInt();
#endifreturn true;}public:int x;int y;char op;
};class Response // 服務器響應數據
{
public:bool serialize(std::string *out) // 序列化{
#ifdef MYSELF// "exitcode result"*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;
#elseJson::Value root;root["first"] = exitcode;root["second"] = result;Json::FastWriter writer;*out = writer.write(root);
#endifreturn true;}bool deserialize(std::string &in) // 反序列化 "exitcode result"{
#ifdef MYSELFauto pos = in.find(SEP);if (pos == std::string::npos)return false;std::string ec_string = in.substr(0, pos);std::string res_string = in.substr(pos + SEP_LEN);if (ec_string.empty() || res_string.empty())return false;exitcode = std::stoi(ec_string);result = std::stoi(res_string);
#elseJson::Value root;Json::Reader reader;reader.parse(in, root);exitcode = root["first"].asInt();result = root["second"].asInt();
#endifreturn true;}public:int exitcode;int result;
};/*** sock:讀取對應套接字的報文* inbuffer:接收緩沖區,這里存放接收到的所有數據* req_text:輸出型參數,如果讀到完整報文就將報文內容存放到req_text中* 返回值:讀取成功返回true,失敗返回false
*/
bool recvPackage(int sock, std::string &inbuffer, std::string *req_text)
{char buffer[1024];while (true){ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 接收數據if (n > 0){buffer[n] = 0;      // 當前本次接收的數據inbuffer += buffer; // 放在inbuffer后面,處理整個inbufferauto 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 + 2 * LINE_SEP_LEN + text_len_string.size(); // 報文總長度if (inbuffer.size() < total_len){// 收到的信息不是一個完整的報文continue;}// 到這里就拿到了一個完整的報文*req_text = inbuffer.substr(0, total_len);inbuffer.erase(0, total_len); // 在緩沖區中刪除拿到的報文return true;}elsereturn false;}
}
/*calClient.hpp*/
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#include <string>#include "log.hpp"
#include "protocol.hpp"namespace Client
{class tcpClient{public:tcpClient(uint16_t &port, std::string &IP) : _serverPort(port), _serverIP(IP), _sockfd(-1) {}void initClient(){// 1. 創建socket_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd == -1){std::cerr << "create socket error" << std::endl;exit(2);}}void run(){struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(_serverPort);server.sin_addr.s_addr = inet_addr(_serverIP.c_str());if (connect(_sockfd, (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);Request req = ParseLine(line);std::string content;req.serialize(&content); // 序列化結果存放的content中std::string send_string = enLength(content); // 添加報頭send(_sockfd, send_string.c_str(), send_string.size(), 0);std::string package, text;if (!recvPackage(_sockfd, inbuffer, &package))continue;if (!deLength(package, &text))continue;// text中的結果就是 "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){int status = 0; // 0 操作符之前 1 操作符 2 操作符之后int i = 0, size = line.size();char op;std::string left, right;while (i < size){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;}}return Request(std::stoi(left), std::stoi(right), op);}~tcpClient(){if (_sockfd >= 0)close(_sockfd); // 使用完關閉,防止文件描述符泄露(當然這里也可以不寫,當進程結束之后一切資源都將被回收)}private:uint16_t _serverPort;std::string _serverIP;int _sockfd;};} // namespace Client
/*calClient.cc*/
#include <memory>
#include <string>#include "calClient.hpp"
using namespace Client;static void Usage(const char *proc)
{std::cout << "\n\tUsage:" << proc << " server_ip server_port\n";
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}std::string IP = argv[1];uint16_t port = atoi(argv[2]);std::unique_ptr<tcpClient> tclt(new tcpClient(port, IP));tclt->initClient();tclt->run();return 0;
}
/*log.hpp*/
#include <unistd.h>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>// 這里是日志等級對應的宏
#define DEBUG (1 << 0)
#define NORMAL (1 << 1)
#define WARNING (1 << 2)
#define ERROR (1 << 3)
#define FATAL (1 << 4)#define NUM 1024 // 日志行緩沖區大小
#define LOG_NORMAL "log.normal" // 日志存放的文件名
#define LOG_ERR    "log.error"const char *logLevel(int level) // 把日志等級轉變為對應的字符串
{switch (level){case DEBUG:return "DEBUG";case NORMAL:return "NORMAL";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOW";}
}
//[日志等級][時間][pid]日志內容
void logMessage(int level, const char *format, ...) // 核心調用
{char logprefix[NUM]; // 存放日志相關信息time_t now_ = time(nullptr);struct tm *now = localtime(&now_);snprintf(logprefix, sizeof(logprefix), "[%s][%d年%d月%d日%d時%d分%d秒][pid:%d]",logLevel(level), now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min, now->tm_sec, getpid());char logcontent[NUM];va_list arg; // 聲明一個變量arg指向可變參數列表的對象va_start(arg, format); // 使用va_start宏來初始化arg,將它指向可變參數列表的起始位置。// format是可變參數列表中的最后一個固定參數,用于確定可變參數列表從何處開始vsnprintf(logcontent, sizeof(logcontent), format, arg); // 將可變參數列表中的數據格式化為字符串,并將結果存儲到logcontent中FILE *log =  fopen(LOG_NORMAL, "a");FILE *err = fopen(LOG_ERR, "a");if(log != nullptr && err != nullptr){FILE *curr = nullptr;if(level == DEBUG || level == NORMAL || level == WARNING) curr = log;if(level == ERROR || level == FATAL) curr = err;if(curr) fprintf(curr, "%s%s\n", logprefix, logcontent);fclose(log);fclose(err);}
}
cc=g++.PHONY:all
all:Server ClientServer:calServer.cc$(cc) -o $@ $^ -lpthread -ljsoncpp -std=c++11Client:calClient.cc$(cc) -o $@ $^ -ljsoncpp -std=c++11.PHONY:clean
clean:rm -f Server Client.PHONY:cleanlog
cleanlog:rm -f log.error log.normal

image-20240227195945695


本節完…

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/716341.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/716341.shtml
英文地址,請注明出處:http://en.pswp.cn/news/716341.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

深入分析Android運行時環境ART:原理、特點與優化策略

摘要 隨著移動互聯網的快速發展&#xff0c;智能手機的性能和功能日益強大&#xff0c;其中Android操作系統因其開放性和靈活性而占據主導地位。Android運行時環境&#xff08;ART&#xff09;作為執行應用程序代碼的關鍵組件&#xff0c;在系統性能和用戶體驗方面起著至關重要…

Vue+SpringBoot打造高校學生管理系統

目錄 一、摘要1.1 項目介紹1.2 項目錄屏 二、功能模塊2.1 學生管理模塊2.2 學院課程模塊2.3 學生選課模塊2.4 成績管理模塊 三、系統設計3.1 用例設計3.2 數據庫設計3.2.1 學生表3.2.2 學院課程表3.2.3 學生選課表3.2.4 學生成績表 四、系統展示五、核心代碼5.1 查詢課程5.2 新…

DFS剪枝

剪枝 將搜索過程中一些不必要的部分剔除掉&#xff0c;因為搜索過程構成了一棵樹&#xff0c;剔除不必要的部分&#xff0c;就像是在樹上將樹枝剪掉&#xff0c;故名剪枝。 剪枝是回溯法中的一種重要優化手段&#xff0c;方法往往先寫一個暴力搜索&#xff0c;然后找到某些特…

超詳細紅黑樹的模擬實現

前言 有人說設計出AVL樹的的人是個大牛&#xff0c;那寫紅黑樹&#xff08;RBTree&#xff09;的人就是天才&#xff01; 上一篇文章&#xff0c;我們已經學習了AVL樹&#xff0c;牛牛個人認為AVL樹已經夠優秀了&#xff0c;那讓我們一起探究一下&#xff0c;為什么紅黑樹比AV…

鏈表類型題目

文章目錄 簡介鏈表的常用技巧兩數相加原理代碼代碼|| 兩兩交換鏈表中的節點代碼原理 重排鏈表(重要)原理代碼 合并 K 個升序鏈表代碼遞歸代碼 K 個一組翻轉鏈表原理代碼 簡介 大家好,這里是jiantaoyab,這篇文章給大家帶來的是鏈表相關的題目練習和解析,希望大家能相互討論進步 …

[線代]自用大綱

部分內容整理自張宇和網絡 序 題型分布&#xff1a; 題型單題分值題目數量總分值選擇題5315填空題515解答題12112 *一道大題可能用到六部分所有知識 矩陣 性質 k k k倍和乘積行列式 ∣ k A ∣ k n ∣ A ∣ |kA|k^n|A| ∣kA∣kn∣A∣ ∣ A B ∣ ≠ ∣ A ∣ ∣ B ∣ |AB|≠…

DDE圖像增強

DDE&#xff08;Detail and Darkness Enhancement&#xff0c;細節和暗部增強&#xff09;是一種用于增強圖像細節和暗部區域的方法。其原理可以簡要概括如下&#xff1a; 細節增強&#xff1a;在圖像中突出顯示細節信息&#xff0c;使得圖像更加清晰和具有視覺沖擊力。這可以通…

藍橋杯刷題--python-15-二分(進階)

503. 借教室 - AcWing題庫 n,mmap(int,input().split()) class_list(map(int,input().split())) class_[0]class_ d[0] s[0] t[0] for _ in range(m): dj,sj,tjmap(int,input().split()) d.append(dj) s.append(sj) t.append(tj) def check(k): b[0]*(n2) …

如何解決微服務的數據一致性分發問題?

介紹 系統架構微服務化以后,根據微服務獨立數據源的思想,每個微服務一般具有各自獨立的數據源,但是不同微服務之間難免需要通過數據分發來共享一些數據,這個就是微服務的數據分發問題。Netflix/Airbnb等一線互聯網公司的實踐[參考附錄1/2/3]表明,數據一致性分發能力,是構…

在嵌入式設備中用多項式快速計算三角函數和方根

慣性傳感器的傾角計算要用到三角函數. 在 MCS-51, Cortex M0, M3 之類的芯片上編程時, 能使用的資源是非常有限, 通常只有兩位數KB的Flash, 個位數KB的RAM. 如果要使用三角函數和開方就要引入 math.h, 會消耗掉10KB以上的Flash空間. 在很多情況下受硬件資源限制無法使用 math.…

【 10X summary report】怎么看?詳細解讀筆記

報告內容 在開始正式的分析之前&#xff0c;需要查看在對齊和計數過程中生成的任何總結統計信息。下圖是由Cell Ranger工具創建的10X總結報告&#xff0c;在從10X scRNA-seq實驗生成計數矩陣時會生成。 The left half of the report describes sequencing and mapping statist…

賣wordpress網站模板的網站

WP模板牛 http://www.wpniu.com 上面有很多免費wordpress模板資源的網站&#xff0c;除了免費模板&#xff0c;還有付費模板。 My模板(我的模板) http://www.mymoban.com 老牌網站模板資源站&#xff0c;上面有wordpress模板、帝國CMS模板、WooCommerce模板可以直接免費下載…

Linux whois命令教程:查詢域名所有者信息(附案例詳解和注意事項)

Linux whois命令介紹 whois命令是一個用于查詢域名所有者信息的工具。它可以直接從命令行進行查詢&#xff0c;這對于沒有圖形用戶界面的系統或者需要在shell腳本中進行查詢的情況非常有用。 Linux whois命令適用的Linux版本 whois命令在大多數Linux發行版中都可以使用&…

C++之stack

1、stack簡介 stack是實現的一個先進后出&#xff0c;后進先出的容器。它只有一個出口&#xff0c;只能操作最頂端元素。 2、stack庫函數 &#xff08;1&#xff09;push() //向棧壓入一個元素 &#xff08;2&#xff09;pop() //移除棧頂元素 &#xff08;3…

基于springboot+vue的中國陜西民俗網

博主主頁&#xff1a;貓頭鷹源碼 博主簡介&#xff1a;Java領域優質創作者、CSDN博客專家、阿里云專家博主、公司架構師、全網粉絲5萬、專注Java技術領域和畢業設計項目實戰&#xff0c;歡迎高校老師\講師\同行交流合作 ?主要內容&#xff1a;畢業設計(Javaweb項目|小程序|Pyt…

在 Angular 中使用 Renderer2

Renderer2 類 Renderer2 類是 Angular 提供的一個抽象服務&#xff0c;允許在不直接操作 DOM 的情況下操縱應用程序的元素。這是推薦的方法&#xff0c;因為它使得更容易開發可以在沒有 DOM 訪問權限的環境中渲染的應用程序&#xff0c;比如在服務器上、在 Web Worker 中或在原…

Java如何剪切視頻

背景&#xff1a;如何使用Java批量切割視頻 FFmpeg 是一個強大的開源多媒體處理工具&#xff0c;被廣泛應用于音視頻的錄制、轉碼、編輯等方面。它支持幾乎所有主流的音視頻格式&#xff0c;能夠在各種操作系統平臺上運行&#xff0c;包括 Windows、macOS 和 Linux。FFmpeg 提…

nginx,php-fpm

一&#xff0c;Nginx是異步非阻塞多進程&#xff0c;io多路復用 1、master進程&#xff1a;管理進程 master進程主要用來管理worker進程&#xff0c;具體包括如下4個主要功能&#xff1a; &#xff08;1&#xff09;接收來自外界的信號。 &#xff08;2&#xff09;向各worker進…

SAP PP學習筆記04 - BOM2 -通過Serial來做簡單的BOM變式配置,副明細,BOM狀態,BOM明細狀態,項目種類,遞歸BOM

本章繼續講BOM。 本章講通過Serial來做簡單的BOM變式配置。還講了BOM的相關概念&#xff1a;副明細&#xff0c;BOM狀態&#xff0c;BOM明細狀態&#xff0c;項目種類&#xff0c;遞歸BOM 等。 1&#xff0c;通過Serial&#xff08;序列號&#xff09;來做簡單的 VC&#xff0…

spring自定義注解之-ElementType.METHOD方法級注解聲明

自定義注解類型和常用場景 可以參考之前的文章 &#xff1a; ElementType.FIELD字段級注解聲明 如果在項目中&#xff0c;多處地方都需調用到同一個方法進行邏輯處理&#xff0c;且與方法的業務邏輯無關&#xff0c;比如監控&#xff0c;日志等&#xff0c;則可用自定義的方法…