自定義網絡協議與序列化/反序列化
如果你做過網絡編程,可能會遇到這樣的問題:用TCP發了一個“1+1”的計算請求,對方卻只收到了“1”;或者一次收到了“1+12+3”這種拼接的內容——這時候怎麼判斷哪個是完整的請求?又怎麼把這些字符串解析成能計算的操作數和運算符?這篇文章就從TCP的“天生缺陷”説起,帶你搞懂自定義協議的必要性、序列化與反序列化的核心邏輯,最後通過一個網絡計算器實例,把這些知識點落地成可運行的代碼。
一、TCP通信的字節流邊界問題
先問一個問題:TCP是“面向字節流”的協議,這意味着什麼?
你可能知道,發送端調用write,只是把數據拷貝到操作系統的內核緩衝區,什麼時候發、發多少,全由TCP協議自己決定(比如根據網絡帶寬、接收方緩衝區大小調整);接收端調用read,可能只讀到部分數據(比如對方只發了一半),也可能讀到多個拼接的數據(比如對方連續發了兩個請求,被TCP打包成一次發送)。
舉個例子:你發了兩次請求“1+1”和“2+3”,接收端read時可能拿到“1+12+3”——這時候你怎麼區分這是兩個請求,而不是一個“1+12+3”的請求?又怎麼保證每次處理的都是一個完整的請求?
這就是TCP字節流的“邊界問題”——它沒法保證應用層能讀到“完整的報文”。要解決這個問題,光靠TCP協議不夠,必須在應用層自定義協議。
二、應用層協議定製的必要性
協議本質是“通信雙方的約定”——比如約定“數據格式是什麼”“每個字段代表什麼意思”“怎麼判斷一個報文結束”。沒有約定,接收端根本不知道怎麼解析數據。
以“網絡計算器”為例,我們需要客户端給服務器發“計算請求”,服務器返回“計算結果”。這時候協議該怎麼定?
至少要明確兩件事:
- 請求格式(Request):需要包含兩個操作數(比如
x和y)、一個運算符(比如op,支持+-*/%)。 - 響應格式(Response):需要包含計算結果(
result)、狀態碼(code,比如0表示成功,1表示除0錯誤,2表示非法運算符)。
如果沒有這個約定,客户端發個“1+1”,服務器可能以為是字符串“1+1”,而不是“x=1,op=+,y=1”——根本沒法計算。
所以,應用層協議的核心作用是:給字節流“貼標籤”,讓接收端能正確識別數據結構和字段含義。
三、關鍵技術:序列化與反序列化
協議定好了Request和Response的結構,接下來的問題是:怎麼把這些結構化數據(比如C++的類/結構體)通過網絡發送?
直接髮結構體行不行?比如客户端定義一個struct Request { int x; int y; char op; },然後把結構體直接write到socket——這在同平台(比如都是Linux)可能暫時能用,但跨平台(比如客户端Windows,服務器Linux)一定會出問題。
為什麼?因為結構體的內存對齊。不同編譯器(比如VS和GCC)對結構體的內存對齊規則可能不同:比如一個struct { char a; int b; },VS可能按4字節對齊(總大小8字節),GCC可能按自然對齊(總大小5字節)。直接傳結構體,接收端解析時字段會錯位(比如把a的內存當成b的一部分),結果完全錯誤。
這時候就需要“序列化”和“反序列化”:
- 序列化(Serialization):把結構化數據(比如Request類對象)轉換成通用的字符串或字節流(比如“10 + 20”),消除平台差異。
- 反序列化(Deserialization):把接收到的字符串/字節流還原成結構化數據(比如把“10 + 20”還原成
x=10,op=+,y=20)。
舉個生活中的例子:你在QQ發消息,消息裏包含“暱稱、時間、內容”——這其實是一個結構化數據。QQ會把這三個字段打包成一個字符串(比如“小明|2024-05-20 10:00|你好”),這個過程就是序列化;接收方拿到字符串後,按“|”分割,還原出暱稱、時間、內容,這就是反序列化。
四、實戰:網絡計算器的協議設計與實現
下面我們從0實現一個網絡計算器,把“協議定製”“序列化/反序列化”落地。整個過程分三步:設計協議結構、實現序列化/反序列化、保障報文完整性。
4.1 第一步:定義協議結構(Request/Response)
首先,我們用C++類定義請求和響應的結構(代替結構體,方便封裝方法):
// protocol.hpp
#pragma once
#include <string>
#include <iostream>
// 計算請求:x op y(比如10 + 20)
class Request {
public:
int x; // 左操作數
int y; // 右操作數
char op; // 運算符:+ - * / %
std::string sep = " "; // 字段分隔符(空格)
// 構造函數
Request(int x_ = 0, int y_ = 0, char op_ = 0)
: x(x_), y(y_), op(op_) {}
// 序列化:把Request對象轉成字符串(比如"10 + 20")
bool Serialize(std::string& out_str);
// 反序列化:把字符串轉成Request對象
bool Deserialize(const std::string& in_str);
};
// 計算響應:result(結果) + code(狀態碼)
class Response {
public:
int result; // 計算結果(僅當code=0時有效)
int code; // 狀態碼:0=成功,1=除0錯誤,2=模0錯誤,3=非法運算符
std::string sep = " "; // 字段分隔符(空格)
// 構造函數
Response(int res_ = 0, int code_ = 0)
: result(res_), code(code_) {}
// 序列化:把Response對象轉成字符串(比如"30 0")
bool Serialize(std::string& out_str);
// 反序列化:把字符串轉成Response對象
bool Deserialize(const std::string& in_str);
};
4.2 第二步:實現序列化與反序列化
接下來實現Serialize和Deserialize方法,核心是“按約定格式處理字符串”。
4.2.1 Request的序列化與反序列化
- 序列化:把
x、op、y用空格拼接成字符串(比如x=10,op='+',y=20→ “10 + 20”)。 - 反序列化:把字符串按空格分割,提取
x、op、y(比如“10 + 20” →x=10,op='+',y=20)。
// Request序列化
bool Request::Serialize(std::string& out_str) {
// 拼接格式:x + " " + op + " " + y
out_str = std::to_string(x) + sep + op + sep + std::to_string(y);
return true; // 簡單場景,暫不處理異常
}
// Request反序列化
bool Request::Deserialize(const std::string& in_str) {
// 1. 找第一個分隔符(空格),提取x
size_t first_sep = in_str.find(sep);
if (first_sep == std::string::npos) {
return false; // 沒找到分隔符,格式錯誤
}
std::string x_str = in_str.substr(0, first_sep);
x = std::stoi(x_str); // 字符串轉整數
// 2. 找第二個分隔符,提取op
size_t second_sep = in_str.find(sep, first_sep + 1);
if (second_sep == std::string::npos) {
return false;
}
op = in_str[first_sep + 1]; // 分隔符後第一個字符是op
// 3. 提取y
std::string y_str = in_str.substr(second_sep + 1);
y = std::stoi(y_str);
return true;
}
4.2.2 Response的序列化與反序列化
邏輯類似,只是字段變成result和code:
// Response序列化
bool Response::Serialize(std::string& out_str) {
// 拼接格式:result + " " + code
out_str = std::to_string(result) + sep + std::to_string(code);
return true;
}
// Response反序列化
bool Response::Deserialize(const std::string& in_str) {
// 找分隔符,分割result和code
size_t sep_pos = in_str.find(sep);
if (sep_pos == std::string::npos) {
return false;
}
std::string res_str = in_str.substr(0, sep_pos);
std::string code_str = in_str.substr(sep_pos + 1);
result = std::stoi(res_str);
code = std::stoi(code_str);
return true;
}
4.3 第三步:保障報文完整性——加“長度頭”和“分隔符”
現在我們能把Request/Response轉成字符串了,但還有個問題:接收端怎麼知道一個報文的結束?比如客户端發了“10 + 20”,接收端可能只讀到“10 + ”(半包),或者讀到“10 + 2020 - 5”(粘包)。
解決方案:給每個報文加一個“長度頭”,格式約定為“長度\n有效載荷”——比如“5\n10+20”(長度5表示後面的有效載荷是5個字符)。接收端先讀“長度”,再按長度讀“有效載荷”,就能保證拿到完整的報文。
我們寫兩個通用函數Encode(加長度頭)和Decode(解長度頭):
// protocol.hpp 中添加
#include <cstring>
// 協議分隔符:用於分割長度頭和有效載荷(換行符\n)
const std::string PROTOCOL_SEP = "\n";
// Encode:給有效載荷加長度頭(格式:長度\n有效載荷)
std::string Encode(const std::string& payload) {
int len = payload.size();
return std::to_string(len) + PROTOCOL_SEP + payload;
}
// Decode:從報文中提取有效載荷(輸入是接收緩衝區,輸出是有效載荷)
// 返回值:true=解析成功,false=報文不完整
bool Decode(std::string& in_buf, std::string& out_payload) {
// 1. 找長度頭的分隔符\n
size_t sep_pos = in_buf.find(PROTOCOL_SEP);
if (sep_pos == std::string::npos) {
return false; // 沒找到分隔符,報文不完整
}
// 2. 提取長度頭,轉成整數
std::string len_str = in_buf.substr(0, sep_pos);
int payload_len = std::stoi(len_str);
// 3. 檢查緩衝區是否包含完整的有效載荷
int total_len = len_str.size() + PROTOCOL_SEP.size() + payload_len;
if (in_buf.size() < total_len) {
return false; // 有效載荷不完整
}
// 4. 提取有效載荷,並從緩衝區中移除已處理的報文
out_payload = in_buf.substr(sep_pos + 1, payload_len);
in_buf.erase(0, total_len); // 移除已處理的部分(長度頭+有效載荷)
return true;
}
五、工程化優化:套接字封裝與服務端框架
現在協議和序列化都搞定了,接下來要實現網絡通信。為了避免重複寫socket代碼,我們先封裝一個Socket類;再寫一個TCP服務器,用多進程處理併發連接。
5.1 套接字封裝(Socket類)
把socket的創建、綁定、監聽、accept、connect等操作封裝成類,方便客户端和服務器複用:
// socket.hpp
#pragma once
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <iostream>
class Socket {
private:
int sock_fd_; // 套接字文件描述符
public:
Socket() : sock_fd_(-1) {}
~Socket() { if (sock_fd_ != -1) close(sock_fd_); }
// 1. 創建流式套接字(TCP)
bool Create() {
sock_fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd_ < 0) {
std::cerr << "Socket create failed: " << strerror(errno) << std::endl;
return false;
}
return true;
}
// 2. 綁定端口(服務器用,IP默認INADDR_ANY)
bool Bind(uint16_t port) {
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port); // 主機字節序轉網絡字節序
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 綁定所有網卡IP
if (bind(sock_fd_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
std::cerr << "Socket bind failed: " << strerror(errno) << std::endl;
return false;
}
return true;
}
// 3. 監聽(服務器用)
bool Listen(int backlog = 10) {
if (listen(sock_fd_, backlog) < 0) {
std::cerr << "Socket listen failed: " << strerror(errno) << std::endl;
return false;
}
return true;
}
// 4. 接受連接(服務器用,返回新的通信套接字)
int Accept(std::string& client_ip, uint16_t& client_port) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int conn_fd = accept(sock_fd_, (struct sockaddr*)&client_addr, &client_addr_len);
if (conn_fd < 0) {
std::cerr << "Socket accept failed: " << strerror(errno) << std::endl;
return -1;
}
// 提取客户端IP和端口
client_ip = inet_ntoa(client_addr.sin_addr); // 網絡字節序轉IP字符串
client_port = ntohs(client_addr.sin_port); // 網絡字節序轉主機字節序
return conn_fd;
}
// 5. 連接服務器(客户端用)
bool Connect(const std::string& server_ip, uint16_t server_port) {
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port);
// IP字符串轉網絡字節序
if (inet_pton(AF_INET, server_ip.c_str(), &server_addr.sin_addr) <= 0) {
std::cerr << "Invalid server IP: " << server_ip << std::endl;
return false;
}
if (connect(sock_fd_, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
std::cerr << "Socket connect failed: " << strerror(errno) << std::endl;
return false;
}
return true;
}
// 獲取套接字文件描述符
int GetFd() const { return sock_fd_; }
};
5.2 多進程TCP服務器實現
服務器的邏輯:主進程監聽端口,接受新連接;每收到一個連接,fork一個子進程處理業務(計算請求),主進程繼續監聽。同時忽略SIGCHLD信號,避免殭屍進程。
// server.cpp
#include "socket.hpp"
#include "protocol.hpp"
#include <signal.h>
#include <sys/wait.h>
#include <functional>
// 忽略SIGCHLD信號,避免殭屍進程
void IgnoreSigchld() {
signal(SIGCHLD, SIG_IGN);
}
// 處理客户端請求:讀取請求→計算→返回響應
void HandleClient(int conn_fd) {
std::string in_buf; // 接收緩衝區(累積數據,處理粘包/半包)
char buf[1024] = {0};
while (true) {
// 1. 讀取客户端數據
ssize_t n = read(conn_fd, buf, sizeof(buf) - 1);
if (n < 0) {
std::cerr << "Read failed: " << strerror(errno) << std::endl;
break;
} else if (n == 0) {
std::cout << "Client closed connection" << std::endl;
break;
}
// 2. 把讀取到的字節流追加到緩衝區
buf[n] = '\0'; // 確保字符串結束
in_buf += buf;
memset(buf, 0, sizeof(buf));
// 3. 解析緩衝區中的完整報文(可能有多個)
std::string payload;
while (Decode(in_buf, payload)) { // 循環解析,直到沒有完整報文
// 4. 反序列化:payload→Request對象
Request req;
if (!req.Deserialize(payload)) {
std::cerr << "Invalid request format: " << payload << std::endl;
continue;
}
// 5. 執行計算,生成Response
Response resp;
switch (req.op) {
case '+':
resp.result = req.x + req.y;
resp.code = 0;
break;
case '-':
resp.result = req.x - req.y;
resp.code = 0;
break;
case '*':
resp.result = req.x * req.y;
resp.code = 0;
break;
case '/':
if (req.y == 0) {
resp.code = 1; // 除0錯誤
} else {
resp.result = req.x / req.y;
resp.code = 0;
}
break;
case '%':
if (req.y == 0) {
resp.code = 2; // 模0錯誤
} else {
resp.result = req.x % req.y;
resp.code = 0;
}
break;
default:
resp.code = 3; // 非法運算符
break;
}
// 6. 序列化Response→字符串,加長度頭
std::string resp_str;
resp.Serialize(resp_str);
std::string encoded_resp = Encode(resp_str);
// 7. 發送響應給客户端
write(conn_fd, encoded_resp.c_str(), encoded_resp.size());
std::cout << "Handled request: " << payload
<< " → Response: " << resp_str << std::endl;
}
}
close(conn_fd); // 關閉通信套接字
}
// TCP服務器類
class TcpServer {
private:
Socket listen_sock_;
uint16_t port_;
public:
TcpServer(uint16_t port) : port_(port) {}
// 初始化服務器:創建→綁定→監聽
bool Init() {
if (!listen_sock_.Create()) return false;
if (!listen_sock_.Bind(port_)) return false;
if (!listen_sock_.Listen()) return false;
std::cout << "Server init success, listening on port " << port_ << std::endl;
return true;
}
// 啓動服務器:接受連接→fork子進程處理
void Start() {
IgnoreSigchld(); // 忽略SIGCHLD,避免殭屍進程
while (true) {
std::string client_ip;
uint16_t client_port;
int conn_fd = listen_sock_.Accept(client_ip, client_port);
if (conn_fd < 0) continue;
// fork子進程處理客户端請求
pid_t pid = fork();
if (pid < 0) {
std::cerr << "Fork failed: " << strerror(errno) << std::endl;
close(conn_fd);
continue;
} else if (pid == 0) {
// 子進程:處理請求(關閉監聽套接字,只保留通信套接字)
listen_sock_.~Socket(); // 子進程不需要監聽套接字,手動析構
HandleClient(conn_fd);
exit(0); // 處理完請求,子進程退出
} else {
// 父進程:關閉通信套接字(子進程會複製一份)
close(conn_fd);
}
}
}
};
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
TcpServer server(port);
if (!server.Init()) return 1;
server.Start();
return 0;
}
5.3 客户端實現
客户端邏輯:創建套接字→連接服務器→構造請求→發送→接收響應→解析。
// client.cpp
#include "socket.hpp"
#include "protocol.hpp"
#include <random>
#include <unistd.h>
// 隨機生成計算請求(模擬用户輸入)
Request GenerateRandomRequest() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> num_dist(1, 100); // 1~100的隨機數
std::uniform_int_distribution<> op_dist(0, 4); // 0~4對應5種運算符
int x = num_dist(gen);
int y = num_dist(gen);
char ops[] = "+-*/%";
char op = ops[op_dist(gen)];
// 避免除0/模0(簡化測試)
if ((op == '/' || op == '%') && y == 0) {
y = 1;
}
return Request(x, y, op);
}
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " <server_ip> <server_port>" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 創建套接字並連接服務器
Socket sock;
if (!sock.Create()) return 1;
if (!sock.Connect(server_ip, server_port)) return 1;
std::cout << "Connected to server: " << server_ip << ":" << server_port << std::endl;
// 2. 發送10個隨機請求
for (int i = 0; i < 10; ++i) {
// 生成請求
Request req = GenerateRandomRequest();
std::string req_str;
req.Serialize(req_str);
std::string encoded_req = Encode(req_str);
// 發送請求
write(sock.GetFd(), encoded_req.c_str(), encoded_req.size());
std::cout << "Sent request: " << req_str << std::endl;
// 接收響應
char buf[1024] = {0};
ssize_t n = read(sock.GetFd(), buf, sizeof(buf) - 1);
if (n < 0) {
std::cerr << "Read response failed: " << strerror(errno) << std::endl;
break;
}
buf[n] = '\0';
std::string in_buf = buf;
// 解析響應
std::string payload;
if (Decode(in_buf, payload)) {
Response resp;
if (resp.Deserialize(payload)) {
if (resp.code == 0) {
std::cout << "Received response: " << req_str << " = " << resp.result << std::endl;
} else {
std::string err_msg;
switch (resp.code) {
case 1: err_msg = "division by zero"; break;
case 2: err_msg = "mod by zero"; break;
case 3: err_msg = "invalid operator"; break;
}
std::cout << "Received error: " << req_str << " → " << err_msg << std::endl;
}
}
}
sleep(1); // 每隔1秒發一次,避免請求太密集
}
// 3. 關閉套接字
close(sock.GetFd());
std::cout << "Disconnected from server" << std::endl;
return 0;
}
六、從手寫到底層到通用方案——引入JSON
手寫序列化/反序列化雖然能幫我們理解原理,但實際項目中很繁瑣(比如字段多了要寫大量字符串分割代碼)。工業界常用JSON作為通用序列化方案——它是跨語言的文本格式,支持各種數據類型(整數、字符串、數組、對象),還有成熟的庫(比如C++的jsoncpp)。
6.1 為什麼用JSON?
- 跨語言:Python、Java、Go都支持JSON,不同語言的服務能輕鬆通信。
- 易讀易調試:JSON格式直觀(比如
{"x":10,"y":20,"op":"+"}),方便打印日誌和調試。 - 無需手寫代碼:庫會自動處理序列化/反序列化,減少重複工作。
6.2 用jsoncpp替換手寫序列化
首先安裝jsoncpp庫(Ubuntu為例):
sudo apt-get install libjsoncpp-dev
然後修改protocol.hpp,用JSON實現序列化/反序列化(保留原手寫代碼,用條件編譯切換):
// protocol.hpp 中添加
#include <json/json.h> // jsoncpp頭文件
// 條件編譯:定義USE_JSON則用JSON,否則用手寫
#ifdef USE_JSON
// JSON版Request序列化
bool Request::Serialize(std::string& out_str) {
Json::Value root;
root["x"] = x;
root["y"] = y;
root["op"] = op; // jsoncpp會自動把char轉成字符串
Json::FastWriter writer; // 快速序列化(無格式)
out_str = writer.write(root);
// 去掉JSON字符串末尾的換行符(FastWriter會加)
if (!out_str.empty() && out_str.back() == '\n') {
out_str.pop_back();
}
return true;
}
// JSON版Request反序列化
bool Request::Deserialize(const std::string& in_str) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(in_str, root)) {
std::cerr << "JSON parse failed: " << in_str << std::endl;
return false;
}
// 檢查字段是否存在
if (!root.isMember("x") || !root["x"].isInt()) return false;
if (!root.isMember("y") || !root["y"].isInt()) return false;
if (!root.isMember("op") || !root["op"].isString()) return false;
x = root["x"].asInt();
y = root["y"].asInt();
op = root["op"].asString()[0]; // 字符串轉char
return true;
}
// JSON版Response序列化
bool Response::Serialize(std::string& out_str) {
Json::Value root;
root["result"] = result;
root["code"] = code;
Json::FastWriter writer;
out_str = writer.write(root);
if (!out_str.empty() && out_str.back() == '\n') {
out_str.pop_back();
}
return true;
}
// JSON版Response反序列化
bool Response::Deserialize(const std::string& in_str) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(in_str, root)) {
std::cerr << "JSON parse failed: " << in_str << std::endl;
return false;
}
if (!root.isMember("result") || !root["result"].isInt()) return false;
if (!root.isMember("code") || !root["code"].isInt()) return false;
result = root["result"].asInt();
code = root["code"].asInt();
return true;
}
#else
// 原手寫序列化/反序列化代碼(略)
#endif
6.3 編譯與測試
用-DUSE_JSON啓用JSON模式,鏈接jsoncpp庫:
# 編譯服務器
g++ server.cpp -o server -DUSE_JSON -ljsoncpp
# 編譯客户端
g++ client.cpp -o client -DUSE_JSON -ljsoncpp
啓動服務器和客户端,會看到請求格式變成JSON:
# 服務器輸出
Handled request: {"op":"+","x":10,"y":20} → Response: {"code":0,"result":30}
七、總結
這篇文章從TCP的邊界問題出發,講了自定義協議、序列化/反序列化的核心邏輯,最後用實戰代碼落地。複習時可以重點關注這幾個點:
- TCP邊界問題:面向字節流導致讀不完整/粘包,必須在應用層解決。
- 協議定製:約定數據格式(Request/Response)和報文結構(長度頭+有效載荷)。
- 序列化/反序列化:
- 作用:消除跨平台差異,把結構化數據轉通用格式。
- 手寫實現:按約定分隔符處理字符串,加長度頭保障完整性。
- 通用方案:用JSON等成熟格式,減少重複代碼。
- 工程化:套接字封裝複用代碼,多進程處理併發,忽略SIGCHLD避免殭屍進程。