首先我们要知道,在之前的Socket编程学习中,我们通过 socket API 实现了简单的字符串发送和接收,即EchoServer。但在实际的开发场景中,我们需要传输的是"结构化的数据"。
socket API 本质上是面向字节流的,它并不理解什么是"结构体"或"类" 。因此,我们需要在应用层解决如何把业务数据在"结构体"和"网络字节流"之间进行转换的问题,这就是自定义协议 与序列化的由来。
一、什么是"协议"?
所谓的"协议",本质上就是通信双方约定好的结构化数据 。
比如我们要实现一个网络计算器,客户端需要把 1+2 发给服务端。
- 方案一 :直接发送字符串
"1+2"。这就需要约定好格式:数字间用运算符隔开,没有空格等 。 - 方案二:定义一个结构体来表示交互信息 。
无论采用哪种方案,只要保证一端发送的数据,另一端能够按照约定的规则正确解析,这就是应用层协议 。
二、序列化与反序列化
为了在网络上传输结构化的数据,我们需要进行转化:
序列化 (Serialization):发送数据时,将内存中的结构体/对象按照既定规则转换成"字节流"或"字符串"的过程 。
反序列化 (Deserialization):接收数据时,将收到的"字节流"或"字符串"按照相同规则还原回结构体/对象的过程 。
序列化和反序列化的目的
-
在网络传输时,序列化目的是为了方便网络数据的发送和接收,无论是何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络数据传输时看到的统一都是二进制序列。
-
序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制序列的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。

我们可以认为网络通信和业务处理处于不同的层级,在进行网络通信时底层看到的都是二进制序列的数据,而在进行业务处理时看得到则是可被上层识别的数据。如果数据需要在业务处理和网络通信之间进行转换,则需要对数据进行对应的序列化或反序列化操作。
这个过程使得上层业务逻辑不需要关心底层的网络字节流细节,只需处理结构体即可 。
三、重新理解 TCP 通信与 IO 系统调用
在编写协议处理代码前,必须深入理解 read、write、recv、send 这些系统调用的本质。
本质是拷贝
当我们调用 write(sockfd, buffer, ...) 时,并不是直接把数据发到了网络上,而是将数据从用户层缓冲区 拷贝到了内核层的 TCP 发送缓冲区 。
同理,read 也是从内核的接收缓冲区拷贝数据到用户层 。
全双工的物理基础
TCP 支持全双工通信(同时收发),是因为在操作系统内核中,一个 TCP 连接既有发送缓冲区 ,又有接收缓冲区 。

TCP 协议(传输控制协议)负责决定什么时候发数据、发多少、出错重传等细节 。
四、解决"粘包"问题:定制协议报文
TCP 是面向字节流的,它没有"报文"的概念。如果不做处理,接收端可能会一次读到半个请求,或者一次读到两个半请求(即粘包问题)。
我们需要在应用层明确报文的边界。常见的自定义协议格式如下:
协议头 + 有效载荷
有效载荷长度 + \r\n (分隔符) + 有效载荷内容 + \r\ n
例如,要发送 JSON 字符串 {"x":1, "y":2},封装后的报文可能是:
16\r\n{"x":1, "y":2}\r\n
编码与解码实现
在代码实现中,我们需要处理字节流缓冲区:
Encode (打包) : 计算消息长度,拼接字符串:len + "\r\n" + message + "\r\n" 。
Decode (解包): 这是一个循环处理的过程,因为缓冲区里可能包含多条消息或不完整的消息:
- 查找第一个分隔符
\r\n的位置,提取出长度len。 - 计算一条完整报文需要的总长度
total = len.size() + message_len + 2 * Sep.size()。 - 判断缓冲区剩余数据是否足够
total。如果不够,说明报文不完整,返回等待新数据 。 - 如果足够,根据长度截取出一个完整的
message,并从缓冲区中移除已处理的字节 。
五、使用 Jsoncpp 库
在实际开发中,我们很少手写二进制序列化,而是使用成熟的序列化方案,如 JSON。
安装
bash
sudo apt-get install libjsoncpp-dev # Ubuntu
sudo yum install jsoncpp-devel # CentOS
核心操作
Json::Value 这是最核心的类,它可以表示 JSON 中的对象、数组、字符串、数字等。用法类似于 std::map 。
使用示例:
cpp
Json::Value root;
root["datax"] = 10;
root["oper"] = '+'; // 支持自动类型转换
序列化
Json::StyledWriter / toStyledString(): 生成带缩进、格式好看的字符串,适合调试 。
Json::FastWriter: 生成紧凑的字符串(去掉了空格换行),体积小,适合网络传输 。
使用示例:
cpp
Json::FastWriter writer;
std::string s = writer.write(root); // 输出: {"datax":10,"oper":43}
反序列化
使用 Json::Reader 将字符串解析回 Json::Value 对象 。
使用示例:
cpp
Json::Reader reader;
Json::Value root;
if (reader.parse(json_string, root)) {
int x = root["datax"].asInt(); // 提取数据
char op = root["oper"].asInt();
}
六、实战用例
结合上述讲的几点,我们来构建一个网络版本的计算器,加强我们的理解。
服务端代码
首先我们需要对服务器进行初始化:
-
调用socket函数,创建套接字。
-
调用bind函数,为服务端绑定一个端口号。
-
调用listen函数,将套接字设置为监听状态。
初始化完服务器后就可以启动服务器了,服务器启动后要做的就是不断调用accept函数,从监听套接字当中获取新连接,每当获取到一个新连接后就创建一个新线程,让这个新线程为该客户端提供计算服务。
TcpServer.hpp:
cpp
#pragma once
#include <signal.h>
#include <functional>
#include "InetAddr.hpp"
#include "Socket.hpp"
using callback_t = std::function<std::string(std::string &)>;
class Tcpserver
{
public:
Tcpserver(uint16_t port, callback_t cb)
: _port(port), _listensocket(std::make_unique<TcpSocket>()), _cb(cb)
{
_listensocket->BuildListenSocketMethod(_port);
}
void HandlerRequest(std::shared_ptr<Socket> sockfd, InetAddr addr)
{
std::string inbuffer;
while (true)
{
ssize_t n = sockfd->Recv(&inbuffer, 100);
if (n > 0)
{
LOG(LogLevel::INFO) << addr.ToString() << "# " << inbuffer;
// 处理收到的数据
std::string ret_str = _cb(inbuffer); // 检查 序列化 解包
if (ret_str.empty()) // 空串返回
continue;
sockfd->Send(ret_str);
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "client " << addr.ToString() << " quit, close sockfd: " << sockfd->GetSockFd();
break;
}
else
{
LOG(LogLevel::WARNING) << "read client " << addr.ToString() << " error, sockfd: " << sockfd->GetSockFd();
break;
}
}
sockfd->CloseSocket();
}
void Run()
{
signal(SIGCHLD, SIG_IGN);
while (true)
{
// 方法 1
InetAddr clientaddr;
auto sockfd = _listensocket->Accept(&clientaddr);
if (sockfd == nullptr)
{
// Accept 失败(如临时资源不足或信号打断),避免忙循环
sleep(1);
continue;
}
LOG(LogLevel::INFO) << "获取新链接成功, sockfd is : " << sockfd->GetSockFd() << " client addr: " << clientaddr.ToString();
// 方法 2
// std::string *peerip;
// uint16_t *peerport;
// _listensocket->AcceptConnection(peerip, peerport);
if (fork() == 0)
{
// 子进程
_listensocket->CloseSocket();
HandlerRequest(sockfd, clientaddr);
exit(0);
}
sockfd->CloseSocket();
}
}
~Tcpserver()
{
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensocket;
callback_t _cb;
};
上述的Socket.hpp,InetAddr.hpp是对套接字、IP、端口号的封装,具体封装如下:
Socket.hpp:
cpp
#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 <memory>
#include "Logger.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
static const int defaultsockfd = -1;
#define Convert(addrptr) ((struct sockaddr *)addrptr)
static const int gbacklog = 5;
// 封装⼀个基类,Socket接⼝类
// 设计模式:模版⽅法类
class Socket
{
public:
virtual ~Socket() {}
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
virtual void ListenSocketOrDie(int backlog) = 0;
virtual std::unique_ptr<Socket> AcceptConnection(std::string *peerip, uint16_t *peerport) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr *addr) = 0;
virtual bool ConnectServer(std::string &serverip, uint16_t serverport) = 0;
virtual int GetSockFd() = 0;
virtual void SetSockFd(int sockfd) = 0;
virtual void CloseSocket() = 0;
virtual bool Recv(std::string *buffer, int size) = 0;
virtual void Send(const std::string &send_str) = 0;
// TODO
public:
void BuildListenSocketMethod(uint16_t port, int backlog = gbacklog)
{
CreateSocketOrDie();
BindSocketOrDie(port);
ListenSocketOrDie(backlog);
}
bool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport)
{
CreateSocketOrDie();
return ConnectServer(serverip, serverport);
}
void BuildNormalSocketMethod(int sockfd)
{
SetSockFd(sockfd);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd)
{
}
~TcpSocket()
{
}
void CreateSocketOrDie() override
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create tcp socket error";
exit(SOCK_CREATE_ERROR);
}
LOG(LogLevel::INFO) << "create tcp socket success";
}
void BindSocketOrDie(uint16_t port) override
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
// InetAddr lc(port);
// int n = bind(_sockfd, lc.Addr(), lc.Length());
int n = bind(_sockfd, Convert(&local), sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socker error";
exit(SOCK_BIND_ERROR);
}
LOG(LogLevel::INFO) << "bind socker success";
}
void ListenSocketOrDie(int backlog) override
{
int n = listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(SOCK_LISTEN_ERROR);
}
LOG(LogLevel::INFO) << "listen socket success";
}
std::shared_ptr<Socket> Accept(InetAddr *addr)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newsockfd = accept(_sockfd, Convert(&peer), &len);
if (newsockfd < 0)
{
LOG(LogLevel::WARNING) << "accept client error" << strerror(errno);
return nullptr;
}
addr->Init(peer);
std::shared_ptr<Socket> s = std::make_shared<TcpSocket>(newsockfd);
return s;
}
std::unique_ptr<Socket> AcceptConnection(std::string *peerip, uint16_t *peerport) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newsockfd = accept(_sockfd, Convert(&peer), &len);
if (newsockfd < 0)
return nullptr;
*peerport = ntohs(peer.sin_port);
// *peerip = inet_ntoa(peer.sin_addr);
char buffer[64];
inet_ntop(AF_INET, &(peer.sin_addr.s_addr), buffer, sizeof(buffer));
*peerip = buffer;
std::unique_ptr<Socket> s = std::make_unique<TcpSocket>(newsockfd);
return s;
}
bool ConnectServer(std::string &serverip, uint16_t serverport) override
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
// server.sin_addr.s_addr = inet_addr(serverip.c_str());
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr.s_addr));
server.sin_port = htons(serverport);
int n = connect(_sockfd, Convert(&server), sizeof(server));
if (n == 0)
return true;
else
return false;
}
int GetSockFd() override
{
return _sockfd;
}
void SetSockFd(int sockfd) override
{
_sockfd = sockfd;
}
void CloseSocket() override
{
if (_sockfd > defaultsockfd)
close(_sockfd);
}
// 读 序列化及反序列
bool Recv(std::string *buffer, int size) override
{
char inbuffer[size];
ssize_t n = recv(_sockfd, inbuffer, size - 1, 0);
if (n > 0)
{
inbuffer[n] = 0;
*buffer += inbuffer; // 故意拼接的 增加
return true;
}
else if (n == 0)
return false;
else
return false;
}
void Send(const std::string &send_str) override
{
send(_sockfd, send_str.c_str(), send_str.size(), 0);
}
private:
int _sockfd;
};
InetAddr.hpp:
cpp
#pragma once
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <functional>
#include <string>
#include "Logger.hpp"
class InetAddr
{
private:
void Net2Host()
{
_port = ntohs(_addr.sin_port);
// _ip = inet_ntoa(_addr.sin_addr);
char ipbuffer[64]; // 不需要调用函数内部的区域,防止覆盖
inet_ntop(AF_INET, &(_addr.sin_addr.s_addr), ipbuffer, sizeof(ipbuffer));
_ip = ipbuffer;
}
void Host2Net()
{
bzero(&_addr, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
// _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
inet_pton(AF_INET, _ip.c_str(), &(_addr.sin_addr.s_addr));
}
public:
InetAddr()
{}
InetAddr(const struct sockaddr_in &client) : _addr(client)
{
Net2Host();
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0") : _port(port), _ip(ip)
{
Host2Net();
}
void Init(const struct sockaddr_in &client)
{
_addr = client;
Net2Host();
}
uint16_t Port()
{
return _port;
}
std::string Ip()
{
return _ip;
}
struct sockaddr *Addr()
{
return (sockaddr *)&_addr;
}
socklen_t Length()
{
socklen_t len = sizeof(_addr);
return len;
}
std::string ToString()
{
return _ip + "-" + std::to_string(_port);
}
bool operator==(const InetAddr &addr)
{
return _ip == addr._ip && _port == addr._port;
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr; // 网络风格地址
std::string _ip; // 主机风格地址
uint16_t _port;
};
而 Logger.hpp 是一个封装好的日志类,用于输出日志,便于调试。
最后的服务端启动文件:
cpp
#include "Calculator.hpp"// 业务 // 应用层
#include "Parser.hpp" // 报文解析,序列反序列化,打包解包 // 表示层
#include "TcpServer.hpp" // 网络通信开断连接 // 会话层
#include "Daemon.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage : " << proc << " serverport" << std::endl;
}
// ./tcp_client serverport
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t serverport = std::stoi(argv[1]);
Daemon();
// EnableConsoleLogStrategy();
EnableFileLogStrategy();
// 1. 计算机对象
std::unique_ptr<Calculator> cal = std::make_unique<Calculator>();
// 2. 协议解析模块
std::unique_ptr<Parser> parser = std::make_unique<Parser>([&cal](Request &rq) -> Response
{ return cal->Exec(rq); });
// 3. 网络通信模块
std::unique_ptr<Tcpserver> tcpsock = std::make_unique<Tcpserver>(serverport, [&parser](std::string &inbuffer) -> std::string
{ return parser->Parse(inbuffer); });
tcpsock->Run();
while (true)
{
}
return 0;
}
Daemon.hpp:
cpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
void Daemon()
{
// 1. 忽略信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
if (fork() > 0)
exit(0);
setsid();
int fd = open("dev/null", O_RDWR);
if (fd >= 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
}
}
Calculator.hpp:
cpp
#pragma once
#include "Protocol.hpp"
#include <iostream>
#include <string>
class Calculator
{
public:
Calculator()
{
}
Response Exec(Request &rq)
{
Response rp;
switch (rq.Oper())
{
case '+':
rp.SetResult(rq.X() + rq.Y());
break;
case '-':
rp.SetResult(rq.X() - rq.Y());
break;
case '*':
rp.SetResult(rq.X() * rq.Y());
break;
case '/':
{
if (rq.Y() == 0)
{
rp.SetCode(1); // 1 -> /
}
else
{
rp.SetResult(rq.X() / rq.Y());
}
}
break;
case '%':
{
if (rq.Y() == 0)
{
rp.SetCode(2); // 2 -> %
}
else
{
rp.SetResult(rq.X() % rq.Y());
}
}
break;
default:
rp.SetCode(3); // false
break;
}
return rp;
}
~Calculator()
{
}
};
协议定制
要实现一个网络版的计算器,就必须保证通信双方能够遵守某种协议约定,因此我们需要设计一套简单的约定。数据可以分为请求数据和响应数据,因此我们分别需要对请求数据和响应数据进行约定。
采用C++当中的类来实现:
- 请求类中需要包括两个操作数,以及对应需要进行的操作。
- 响应类中需要包括一个计算结果,除此之外,响应类中还需要包括一个状态字段,表示本次计算的状态,因为客户端发来的计算请求可能是无意义的。
规定状态字段对应的含义:
- 状态字段为0,表示计算成功。
- 状态字段为1,表示出现除0错误。
- 状态字段为2,表示出现模0错误。
- 状态字段为3,表示非法计算。
只有当响应结构体当中的状态字段为0时,计算结果才是有意义的,否则计算结果无意义。
Protocol.hpp:
cpp
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
class Request
{
public:
Request()
{
}
// 序列 反序列化对象
bool Serializ(std::string *out)
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::StyledWriter writer;
*out = writer.write(root);
if (out->empty())
return false;
return true;
}
bool Deserialize(std::string &in)
{
Json::Reader reader;
Json::Value droot;
bool ret = reader.parse(in, droot);
if (!ret)
return false;
_x = droot["x"].asInt();
_y = droot["y"].asInt();
_oper = droot["oper"].asInt();
return true;
}
int X()
{
return _x;
}
int Y()
{
return _y;
}
char Oper()
{
return _oper;
}
~Request()
{
}
public:
// 约定
int _x;
int _y;
char _oper;
};
class Response
{
public:
Response() : _result(0), _code(0)
{
}
bool Serializ(std::string *out)
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::StyledWriter writer;
*out = writer.write(root);
if (out->empty())
return false;
return true;
}
bool Deserialize(std::string &in)
{
Json::Reader reader;
Json::Value droot;
bool ret = reader.parse(in, droot);
if (!ret)
return false;
_result = droot["result"].asInt();
_code = droot["code"].asInt();
return true;
}
void SetResult(int r)
{
_result = r;
}
void SetCode(int c)
{
_code = c;
}
void Print()
{
std::cout << _result << "[" << _code << "]" << std::endl;
}
~Response()
{
}
private:
int _result;
int _code;
};
bool DigitSafeCheck(const std::string &str)
{
for (int i = 0; i < str.size(); i++)
{
if (!(str[i] > '0' && str[i] <= '9'))
{
return false;
}
}
return true;
}
static const std::string sep = "\r\n";
class Protocol
{
public:
static std::string Package(const std::string &jsonstr)
{
// jsonstd -> len\r\njsonstr\r\n
if (jsonstr.empty())
{
return std::string();
}
std::string jsonlen = std::to_string(jsonstr.size());
return jsonlen + sep + jsonstr + sep;
}
static int UnPackage(std::string &origin_str, std::string *package) // 输入输出
{
if (package == nullptr)
return -2;
auto pos = origin_str.find(sep);
if (pos == std::string::npos)
{
return 0;
}
std::string len_str = origin_str.substr(0, pos);
if (!DigitSafeCheck(len_str))
{
return -1;
}
int digit_len = std::stoi(len_str);
int target_len = len_str.size() + digit_len + 2 * sep.size();
if (origin_str.size() < target_len)
{
return 0;
}
*package = origin_str.substr(pos + sep.size(), digit_len);
origin_str.erase(0, target_len); // 移除
return package->size();
}
};
在上述源码中,不仅含有请求类、响应类,也有对应的协议类,用于解包封包。
为了更好的解耦,我们将报文解析的过程单独封装成一个Parser类。
Parser.hpp:
cpp
#pragma once
#include "Protocol.hpp"
#include "Calculator.hpp"
#include "Parser.hpp"
#include "Logger.hpp"
#include <iostream>
#include <string>
#include <functional>
using handler_t = std::function<Response(Request &)>;
// 只负责报文解析
class Parser
{
public:
Parser(handler_t handler) : _handler(handler)
{
}
std::string Parse(std::string &inbuffer)
{
std::string package_ptr;
while (true) // 循环处理多个请求
{
// 1. 解包
std::string jsonstr;
int n = Protocol::UnPackage(inbuffer, &jsonstr);
if (n == 0)
{
// return std::string();
break;
}
else if (n < 0)
{
exit(1);
}
// 解包成功
LOG(LogLevel::DEBUG) << jsonstr;
// 2. 反序列化
Request rq;
rq.Deserialize(jsonstr);
// 3. 业务处理
Response rp = _handler(rq);
// 4. 序列化
std::string send_str;
rp.Serializ(&send_str);
// 5. 打包
package_ptr += Protocol::Package(send_str);
}
// 6. 返回
return package_ptr;
}
~Parser()
{
}
private:
handler_t _handler;
};
注意: 协议定制好后必须要被客户端和服务端同时看到,这样它们才能遵守这个约定,那么客户端和服务端都应该包含这个头文件。
客户端代码
客户端首先也需要进行初始化,调用socket函数,创建套接字。
客户端初始化完毕后需要调用connect函数连接服务端,当连接服务端成功后,客户端就可以向服务端发起计算请求了。
用户输入两个数和一个操作符构建一个计算请求,然后将该请求发送给服务端。
当服务端处理完该计算请求后,会对客户端进行响应,因此客户端发送完请求后还需要读取服务端发来的响应数据。
客户端在向服务端发送或接收数据时,可以使用write或read函数进行发送或接收,也可以使用send或recv函数对应进行发送或接收。
send函数
函数原型:
cpp
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
- sockfd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
- buf:需要发送的数据。
- len:需要发送数据的字节个数。
- flags:发送的方式,一般设置为0,表示阻塞式发送。
返回值说明:
- 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
recv函数
函数原型:
cpp
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明:
- sockfd:特定的文件描述符,表示从该文件描述符中读取数据。
- buf:数据的存储位置,表示将读取到的数据存储到该位置。
- len:数据的个数,表示从该文件描述符中读取数据的字节数。
- flags:读取的方式,一般设置为0,表示阻塞式读取。
返回值说明:
- 如果返回值大于0,则表示本次实际读取到的字节个数。
- 如果返回值等于0,则表示对端已经把连接关闭了。
- 如果返回值小于0,则表示读取时遇到了错误。
cpp
#include <iostream>
#include "Socket.hpp"
#include "Protocol.hpp"
#include "Parser.hpp"
void Usage(std::string proc)
{
std::cerr << "Usage : " << proc << " serverip serverport" << std::endl;
}
// ./tcp_client serverip serverport
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]);
std::unique_ptr<Socket> client = std::make_unique<TcpSocket>();
if (client->BuildConnectSocketMethod(serverip, serverport))
{
std::string inbuffer;
while (true)
{
// 1. 构建请求
Request rq;
std::cout << "Please Enter X: ";
std::cin >> rq._x;
std::cout << "Please Enter Y: ";
std::cin >> rq._y;
std::cout << "Please Enter Oper: ";
std::cin >> rq._oper;
// 2. 序列化
std::string jsonstr;
rq.Serializ(&jsonstr);
// 3. 打包
std::string sendstr = Protocol::Package(jsonstr);
// 4. 发送
client->Send(sendstr);
// 5. 接收
client->Recv(&inbuffer, 100);
std::string package;
int n = Protocol::UnPackage(inbuffer, &package);
if (n > 0)
{
Response rp;
bool r = rp.Deserialize(package);
if (r)
{
rp.Print();
}
}
}
}
return 0;
}
上述 demo 就是一个轻量级 TCP 服务示例,使用自定义包协议传输 JSON 请求。父进程监听端口并对每个连接 fork 子进程处理:子进程循环读取、解帧、调用业务(Calculator)并将计算结果序列化返回。