📚 博主的专栏
上篇文章:TCP(传输控制协议)套接字编程,多线程远程执行命令编程
下篇文章:HTTP协议
目录
重新理解 read、 write、 recv、 send 和 tcp 为什么支持全双工
我们的整个代码一共分成了三层:会话、表示(是我们本文章的重点、协议沟通)、和应用层(业务逻辑)
摘要:本文深入探讨应用层协议的设计与实践,以构建一个网络版计算器为例,详细解析如何通过自定义协议实现客户端与服务端的高效通信。文章重点讲解协议的核心作用,通过序列化(JSONcpp库)与反序列化技术将结构化数据转化为字符串传输,并设计报文头(如“len\r\n{json}\r\n”)确保数据完整性。结合Socket编程,演示TCP全双工通信的实现,以及如何通过多线程处理并发请求。代码层面分层设计会话层(TcpServer)、表示层(协议解析)和应用层(业务逻辑),最终实现支持加减乘除运算的网络服务。本文通过完整项目实践,帮助读者理解协议定制、数据传输控制及跨平台通信的关键技术。
应用层
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层
再谈 "协议"
协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些 "结构化的数据" 怎么办呢?
其实, 协议就是双方约定好的结构化的数据
也就是在之后,我们将实现一个复杂的网络版计算器:实现这样的效果,这两个结构体,就是协议
但是我们非常不推荐直接以结构体的方式传给客户端或者服务端,以结构体的方式来进行双方通信,因为不具有跨平台性, 可扩展性非常不好。
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。
约定方案一:
• 客户端发送一个形如"1+1"的字符串;
• 这个字符串中有两个操作数, 都是整形;
• 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
• 数字和运算符之间没有空格;
• ...
约定方案二:
• 定义结构体来表示我们需要交互的信息;
• 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
• 这个过程叫做 "序列化" 和 "反序列化"
序列化 和 反序列化

无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据,
在另一端能够正确的进行解析, 就是 ok 的. 这种约定, 就是 应用层协议
但是, 为了让我们深刻理解协议, 我们打算自定义实现一下协议的过程。
• 我们采用方案 2, 我们也要体现协议定制的细节
• 我们要引入序列化和反序列化, 只不过我们直接采用现成的方案 -- jsoncpp库
• 我们要对 socket 进行字节流的读取处理
重新理解 read、 write、 recv、 send 和 tcp 为什么支持全双工
一个fd代表一个连接、一个连接,有两个缓冲区。
结论:
1. read、 write、 recv、 send本质都是拷贝函数
2. 发数据的本质,是从发送方的发送缓冲区把数据通过协议栈和网络拷贝给接受方的缓冲区
3. tcp支持全双工通信的原因:接口不同。
注意:(传输层很网络层属于OS)、数据什么时候发、数据发多少,出错怎么办,OS管理(定期刷新)由TCP决定。
4. tcp叫做传输控制的原因(OS)
5. 生产者消费者模型
6. 为什么IO函数会阻塞:本质就是在维护同步关系
所以:
• 在任何一台主机上, TCP 连接既有发送缓冲区, 又有接受缓冲区, 所以, 在内核中,可以在发消息的同时, 也可以收消息, 即全双工
• 这就是为什么一个 tcp sockfd 读写都是它的原因
• 实际数据什么时候发, 发多少, 出错了怎么办, 由 TCP 控制, 所以 TCP 叫做传输控制协议
面向字节流:
客户端发的,不一定全部是服务端收的
上篇博客所遗留的问题:
recv为什么是不对的:
如果我给服务器发送的字符串是ls -a -l,但os只发了一个 ls -。(偶发性)
因此如何保证操作系统发的是一个完整的请求?
使用"序列化" 和 "反序列化"制定协议,并且分割完整的报文
开始实现
代码结构
Makefile
Protocol.hpp ---->协议
Socket.hpp --->套接字
Calculate.hpp --->
TcpServer.hpp ---->服务端
Daemon.hpp
TcpClientMain.cc
TcpServerMain.cc --->服务端调用
简单起见, 可以直接采用自定义线程
直接 client<<->>server 通信, 这样可以省去编写没有干货的代码
我先对上一篇博客中最后完成的代码Tcp Echo Server 进行重构
Socket.hpp
封装了套接字相关方法:套接字创建、绑定、监听、连接
#pragma once
#include <iostream>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <memory>
// #include "Command.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
namespace sock_ns
{
class Socket;
using namespace log_ns;
using SockSPtr = std::shared_ptr<Socket>;
const static int gblcklog = 8;
enum
{
SOCKET_ERROR = 1,
BIND_ERROR,
LISTEN_ERROR
};
// 模版方法类模式:
class Socket
{
public:
virtual void CreateSocketOrDie() = 0;
virtual void CreateBindOrDie(uint16_t port) = 0;
virtual void CreateListenOrDie(int backlog = gblcklog) = 0;
virtual SockSPtr Accepter(InetAddr *clientaddr) = 0;
virtual bool Connector(std::string peerip, uint16_t peerport) = 0;
virtual int Sockfd() = 0;
virtual void Close() = 0;
virtual ssize_t Recv(std::string *out) = 0;
virtual ssize_t Send(const std::string &in) = 0;
public:
// 创建监听套接字
void BuildListenSocket(uint16_t port)
{
CreateSocketOrDie();
CreateBindOrDie(port);
CreateListenOrDie();
}
// 创建客户端套接字
void BuildClientSocket(std::string peerip, uint16_t peerport)
{
CreateSocketOrDie();
Connector(peerip, peerport);
}
// void BuildUdpSocket()
// {}
};
class TcpSocket : public Socket
{
public:
TcpSocket()
{
}
TcpSocket(int sockfd) : _sockfd(sockfd)
{
}
// 创建套接字
void CreateSocketOrDie() override
{
// 1.创建socket
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(FATAL, "socker create error\n");
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success, sockfd: %d\n", _sockfd);
}
// 绑定、本地socket信息、端口和IP
void CreateBindOrDie(uint16_t port) override
{
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;
// 2.绑定sockfd 和 socket addr
if (::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
LOG(FATAL, "bind error \n");
exit(BIND_ERROR);
}
LOG(INFO, "bind success \n");
}
// 监听
void CreateListenOrDie(int backlog) override
{
// 让套接字设置为listen状态
if (::listen(_sockfd, gblcklog))
{
LOG(FATAL, "listen error \n");
exit(LISTEN_ERROR);
}
LOG(INFO, "listen success \n");
}
SockSPtr Accepter(InetAddr *clientaddr) override
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 4.获取新链接
// 从监听套接字获取新的套接字、获取客户端信息
int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);
// 获取连接失败、继续获取
if (sockfd < 0)
{
LOG(WARNING, "accept error\n");
return nullptr;
}
*clientaddr = InetAddr(client);
// 获客成功,提供服务
return std::make_shared<TcpSocket>(sockfd);
}
// 链接客户端服务器
// peerip远端ip、peerport远端port
bool Connector(std::string peerip, uint16_t peerport) override
{ // 2.不需要显示的bind,但是一定要有自己的IP和port,所以需要隐式的bind,OS会自动bind sockfd,用自己的IP和随机算口号
// 什么时候进行自动bind,if the connection or binding succeeds
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(peerport);
// server.sin_addr.s_addr =
// 进程序列转为网络序列
::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);
int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
// 连接失败
std::cerr << "connect socket error" << std::endl;
return false;
}
return true;
}
int Sockfd()
{
return _sockfd;
}
void Close()
{
if (_sockfd > 0)
{
::close(_sockfd);
}
}
// 还需调整
ssize_t Recv(std::string *out) override
{
// 缺点:inbuffer不是动态的,每次只能读取这么多,或者说,明明只发了5个字节,但是却还是要读取1023个字节
char inbuffer[4096];
// n是实际读取的字节数 // 当做字符串
ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0); //(留一个\n的位置)
if (n > 0)
{
inbuffer[n] = 0;
*out = inbuffer;
}
return n;
}
ssize_t Send(const std::string &in) override
{
return ::send(_sockfd, in.c_str(), in.size(), 0);
}
~TcpSocket()
{
}
private:
int _sockfd; // 可以是listen套接字、也可以是普通套接字
};
}
TcpServer.hpp
#pragma once
#include"Socket.hpp"
#include "InetAddr.hpp"
#include <functional>
//设置函数对象
using namespace sock_ns;
static const int gport = 8888;
using service_io_t = std::function<void(SockSPtr, const InetAddr &)>;
class TcpServer
{
public:
TcpServer(service_io_t service, uint16_t port = gport)
: _port(port)
, _listensock(std::make_shared<TcpSocket>())
, _isrunning(false)
, _service(service)
{
_listensock->BuildListenSocket(_port);
}
class ThreadData
{
public:
SockSPtr _sockfd;
TcpServer *_self;
InetAddr _addr;//给InetAddr再添加一个无参构造
public:
ThreadData(SockSPtr sockfd, TcpServer *self, const InetAddr &addr) : _sockfd(sockfd), _self(self), _addr(addr)
{
}
};
void Loop()
{
// signal(SIGCHLD, SIG_IGN);
_isrunning = true;
while (_isrunning)
{
InetAddr client;
SockSPtr newsock = _listensock->Accepter(&client);
if(newsock == nullptr)
{
continue;
}
LOG(INFO, "get a new link, client info : %s, sockfd is: %d\n", client.AddrStr().c_str(), newsock->Sockfd());
// version2-----多线程版本 --- 不能关闭fd,也不需要
pthread_t tid;
ThreadData* td = new ThreadData(newsock, this, client);
pthread_create(&tid, nullptr, Execute, td);
}
_isrunning = false;
}
static void *Execute(void *args)
{
pthread_detach(pthread_self());
ThreadData* td = static_cast<ThreadData *>(args);
//直接回调我们定义的函数对象
td->_self->_service(td->_sockfd, td->_addr);
//处理完了,直接关闭
::close(td->_sockfd->Close());
delete td;
return nullptr;
}
~TcpServer()
{
}
private:
uint16_t _port;
// int _listensockfd;
// 定义套接字对象
SockSPtr _listensock;
bool _isrunning;
service_io_t _service;
};
Service.hpp:
将来服务器要求的是这种类型:
using service_io_t = std::function<void(SockSPtr, const InetAddr &)>;
#pragma once
#include <iostream>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Log.hpp"
using namespace log_ns;
using namespace sock_ns;
// 主要是进行io服务
// using service_io_t = std::function<void(SockSPtr, const InetAddr &)>;
class IOService
{
public:
IOService()
{
}
void IOExecute(SockSPtr sock, InetAddr &addr)
{
// 长服务
while (true)
{
std::string message;
ssize_t n = sock->Recv(&message);
if (n > 0)
{
LOG(INFO, "get message from client %s, message: %s\n", addr.AddrStr().c_str(), message.c_str());
std::string hello = "hello";
sock->Send(hello);
}
else if (n == 0)
{
LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
break;
}
else
{
LOG(ERROR, "recv error: %s\n", addr.AddrStr().c_str());
break;
}
}
}
~IOService()
{
}
};
现在我们可以开始制定协议;
Protocol.hpp
对于将字符串转成结构化字段、和将结构化字段转成字符串,序列化和反序列化:
1.我们可以自己写:
规定:“x opr y”;做一个传的字符串的格式,根据分隔符来提取。
2.使用现成的库来完成
xml 、JSON、protobuf
我们选择JSON(C++、JAVA、Python都使用的是JSON)
而在C++中就必须使用JSON库(jsoncpp)
简单讲解Jsoncpp
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。 它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。 Jsoncpp 是开源的, 广泛用于各种需要处理 JSON 数据的 C++ 项目中。
特性:
1. 简单易用: Jsoncpp 提供了直观的 API, 使得处理 JSON 数据变得简单。
2. 高性能: Jsoncpp 的性能经过优化, 能够高效地处理大量 JSON 数据。
3. 全面支持: 支持 JSON 标准中的所有数据类型, 包括对象、 数组、 字符串、 数字、 布尔值和 null。
4. 错误处理: 在解析 JSON 数据时, Jsoncpp 提供了详细的错误信息和位置, 方便开发者调试。当使用 Jsoncpp 库进行 JSON 的序列化和反序列化时, 确实存在不同的做法和工具类可供选择。 以下是对 Jsoncpp 中序列化和反序列化操作的详细介绍:
安装
ubuntu:
sudo apt-get install libjsoncpp-dev
centos:
sudo yum install jsoncpp-devel
序列化
序列化指的是将数据结构或对象转换为一种格式, 以便在网络上传输或存储到文件中。 Jsoncpp 提供了多种方式进行序列化:
1. 使用 Json::FastWriter:
优点 :比 StyledWriter 更快 ,因为它不添加额外的空格和换行符。
示例:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
// 结构化转字符串、字符串转结构化---->序列化和反序列化
// 请求要有序列化的接口、客户端要发请求给服务器, 也要有反序列化,因为服务器也需要读到请求,然后我们做反序列化
class Request
{
public:
Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
{
}
// 客户端用序列----->目的是把结构化字段转化成一个字符串
bool Serialize(std::string *out)
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
// FastWriter是一个类
Json::FastWriter writer;
std::string s = writer.write(root);
// std::cout << s << std::endl;
*out = s;
return true;
}
// 服务端用反序列,别人会给我传进来一个字符串,我需要将这个字符串转成结构化字段(内部属性的值
void Deserialize(const std::string &in)
{
}
~Request()
{
}
private:
int _x;
int _y;
char _oper; //+ 、— 、/、% //x oper y
};
int main()
{
Request req(111, 222, '-');
std::string s;
req.Serialize(&s);
std::cout << s << std::endl;
return 0;
}
运行结果:
注意在执行命令时,要添加上第三方库jsoncpp:
g++ -o test test.cc -ljsoncpp ./test {"oper":45,"x":111,"y":222}
最后转换成了这种格式的字符串,就叫做JSON串,序列化的结果是json自定义的,但是我们也可以修改一些属性自定义。
StyleWriter
Json::StyledWriter writer;
运行结果:
{ "oper" : 45, "x" : 111, "y" : 222 }
允许我们做嵌套:
bool Serialize(std::string *out)
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::Value sub;
sub["name"] = "张三";
sub["age"] = 12;
//Json::value中可以套对象,最后是可以呈树状呈现
root["sub"] = sub;
// FastWriter是一个类
// Json::FastWriter writer;
Json::StyledWriter writer;
std::string s = writer.write(root);
// std::cout << s << std::endl;
*out = s;
return true;
}
{
"oper" : 45,
"sub" : {
"age" : 12,
"name" : "\u5f20\u4e09"
},
"x" : 111,
"y" : 222
}
数组追加:
bool Serialize(std::string *out) { Json::Value root; root["x"] = _x; root["y"] = _y; root["oper"] = _oper; Json::Value sub; sub["name"] = "张三"; sub["age"] = 12; //Json::value中可以套对象,最后是可以呈树状呈现 root["sub"] = sub; Json::Value array(Json::arrayValue); array.append(1); array.append(2); array.append(5); root["array"] = array; // FastWriter是一个类 // Json::FastWriter writer; Json::StyledWriter writer; std::string s = writer.write(root); // std::cout << s << std::endl; *out = s; return true; }
运行结果:
{ "array" : [ 1, 2, 5 ], "oper" : 45, "sub" : { "age" : 12, "name" : "\u5f20\u4e09" }, "x" : 111, "y" : 222 }
反序列化:
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。 Jsoncpp 提供了以下方法进行反序列化:
1. 使用 Json ::Reader:
优点 :提供详细的错误信息和位置,方便调试。
示例:
#pragma once
#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
#include<memory>
// 设计一下协议的报头,和报文的完整格式
// "len"\r\n"{json}"\r\n ---完整的报文 len代表有效载荷的长度
// 结构化转字符串、字符串转结构化---->序列化和反序列化
// 请求要有序列化的接口、客户端要发请求给服务器, 也要有反序列化,因为服务器也需要读到请求,然后我们做反序列化
static const std::string sep = "\r\n";
//添加报头
std::string Encode(const std::string &jsonstr)
{
//将jsonstr编程我需要的这个:"len"\r\n"{json}"\r\n
int len = jsonstr.size();
std::string lenstr = std::to_string(len);
return lenstr+sep+jsonstr+sep;
}
// 不能带const、因为后面还要修改erase、为了处理下一次的字符串
std::string Decode(std::string &packagestream)
{
auto pos = packagestream.find(sep);
//无法提取到一个完整的报文,返回空串
if(pos == std::string::npos) return std::string();
//前闭后开:
std::string lenstr = packagestream.substr(0, pos);
int len = std::stoi(lenstr);
// len代表有效载荷的长度
int total = lenstr.size() + len + 2*sep.size();
if(packagestream.size() < total) return std::string();
//提取json字符串
std::string jsonstr = packagestream.substr(pos+sep.size(), len);
//下一次处理下一个字符串
packagestream.erase(total);
return jsonstr;
}
class Request
{
public:
Request()
{}
Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
{
}
// 客户端用序列----->目的是把结构化字段转化成一个字符串
bool Serialize(std::string *out)
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
// Json::StyledWriter writer;
Json::FastWriter writer;
std::string s = writer.write(root);
// std::cout << s << std::endl;
*out = s;
return true;
}
// 服务端用反序列,别人会给我传进来一个字符串,我需要将这个字符串转成结构化字段(内部属性的值
bool Deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
// 传进来的字符串转换成发序列化成一个json对象
bool res = reader.parse(in, root);
// std::cout <
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return true;
}
void Print()
{
std::cout << _x << std::endl;
std::cout << _y << std::endl;
std::cout << _oper << std::endl;
}
int X()
{
return _x;
}
int Y()
{
return _y;
}
char Oper()
{
return _oper;
}
~Request()
{
}
private:
int _x;
int _y;
char _oper; //+ 、— 、/、% //x oper y
};
// 最终相当于是服务器给客户端响应,序列化成字符串发过去,而服务器还会收需求,就可以反序列化成结构化字段
class Response
{
public:
Response():_result(0), _code(0), _desc("success")
{
}
// 客户端用序列----->目的是把结构化字段转化成一个字符串,发出去
bool Serialize(std::string *out)
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
root["desc"] = _desc;
// Json::StyledWriter writer;
Json::FastWriter writer;
std::string s = writer.write(root);
// std::cout << s << std::endl;
*out = s;
return true;
}
// 服务端用反序列,别人会给我传进来一个字符串,我需要将这个字符串转成结构化字段(内部属性的值
bool Deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
// 传进来的字符串转换成发序列化成一个json对象
bool res = reader.parse(in, root);
if(!res) return false;
_result = root["result"].asInt();
_code = root["code"].asInt();
_desc = root["desc"].asString();
return true;
}
void Print()
{
std::cout << _result << std::endl;
std::cout << _code << std::endl;
std::cout << _desc << std::endl;
}
int Result()
{
return _result;
}
int Code()
{
return _code;
}
std::string Desc()
{
return _desc;
}
~Response()
{
}
public:
int _result;
//状态码
int _code; // 0:success
//描述
std::string _desc;
};
//工厂模式:能够直接帮我们去进行对象的创建
class Factory
{
public:
static std::shared_ptr<Request> BuildRequestDefault()
{
return std::make_shared<Request>();
}
static std::shared_ptr<Response> BuildResponseDefault()
{
return std::make_shared<Response>();
}
};
运行结果:
111
222
-
如何保证服务器读到的是一个完整的请求
继续定制协议:Protocol.hpp
设计一下协议的报头,和报文的完整格式
"len"\r\n"{json}"\r\n ---完整的报文 len代表有效载荷的长度
\r\n:是为了区分len 和 json 串
\r\n:暂时没有其他作用,便于打印,debug
我们收到的字符串有可能是:
可能出现异常情况
"le
"len"
"len"\r\n"
"len"\r\n"{j
"len"\r\n"{json}
"len"\r\n"{json}"\r\n----->正常情况
"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n
"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r
字符串不完整,就不要,要读到一个完整的,有多的就不要
协议处理类:
#pragma once
#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
// 设计一下协议的报头,和报文的完整格式
// "len"\r\n"{json}"\r\n ---完整的报文 len代表有效载荷的长度
// 结构化转字符串、字符串转结构化---->序列化和反序列化
// 请求要有序列化的接口、客户端要发请求给服务器, 也要有反序列化,因为服务器也需要读到请求,然后我们做反序列化
static const std::string sep = "\r\n";
//添加报头
std::string Encode(const std::string &jsonstr)
{
//将jsonstr编程我需要的这个:"len"\r\n"{json}"\r\n
int len = jsonstr.size();
std::string lenstr = std::to_string(len);
return lenstr+sep+jsonstr+sep;
}
// 不能带const,最后要删除掉我们读取到的报文
std::string Decode(std::string &packagestream)
{
auto pos = packagestream.find(sep);
//无法提取到一个完整的报文,返回空串
if(pos == std::string::npos) return std::string();
//前闭后开:
std::string lenstr = packagestream.substr(0, pos);
int len = std::stoi(lenstr);
// len代表有效载荷的长度
int total = lenstr.size() + len + 2*sep.size();
if(packagestream.size() < total) return std::string();
//提取json字符串
std::string jsonstr = packagestream.substr(pos+sep.size(), len);
//下一次处理下一个字符串
packagestream.erase(0, total);
return jsonstr;
}
class Request
{
public:
Request()
{}
Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
{
}
// 客户端用序列----->目的是把结构化字段转化成一个字符串
bool Serialize(std::string *out)
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
// Json::StyledWriter writer;
Json::FastWriter writer;
std::string s = writer.write(root);
// std::cout << s << std::endl;
*out = s;
return true;
}
// 服务端用反序列,别人会给我传进来一个字符串,我需要将这个字符串转成结构化字段(内部属性的值
bool Deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
// 传进来的字符串转换成发序列化成一个json对象
bool res = reader.parse(in, root);
// std::cout <
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return true;
}
void Print()
{
std::cout << _x << std::endl;
std::cout << _y << std::endl;
std::cout << _oper << std::endl;
}
~Request()
{
}
private:
int _x;
int _y;
char _oper; //+ 、— 、/、% //x oper y
};
// 最终相当于是服务器给客户端响应,序列化成字符串发过去,而服务器还会收需求,就可以反序列化成结构化字段
class Response
{
public:
Response():_result(0), _code(0), _desc("success")
{}
// 客户端用序列----->目的是把结构化字段转化成一个字符串,发出去
bool Serialize(std::string *out)
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
root["desc"] = _desc;
// Json::StyledWriter writer;
Json::FastWriter writer;
std::string s = writer.write(root);
// std::cout << s << std::endl;
*out = s;
return true;
}
// 服务端用反序列,别人会给我传进来一个字符串,我需要将这个字符串转成结构化字段(内部属性的值
bool Deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
// 传进来的字符串转换成发序列化成一个json对象
bool res = reader.parse(in, root);
if(!res) return false;
_result = root["result"].asInt();
_code = root["code"].asInt();
_desc = root["desc"].asString();
return true;
}
void Print()
{
std::cout << _result << std::endl;
std::cout << _code << std::endl;
std::cout << _desc << std::endl;
}
int X()
{
return _x;
}
int Y()
{
return _y;
}
char Oper()
{
return _oper;
}
~Response()
{
}
public:
int _result;
//状态码
int _code; // 0:success
//描述
std::string _desc;
};
有了协议处理类,我们再返回更新IOService类:
能保证我们读取到的是一个完整的报文吗?不能!
原来我们的读取Recv,是将读取的数据每一次进行覆盖,但是我们这里想要保留每一次读取到的内容,因此改动:*out += inbuffer;将每一次读取到的内容都拼接到原内容后:
ssize_t Recv(std::string *out) override
{
// 缺点:inbuffer不是动态的,每次只能读取这么多,或者说,明明只发了5个字节,但是却还是要读取1023个字节
char inbuffer[4096];
// n是实际读取的字节数 // 当做字符串
ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0); //(留一个\n的位置)
if (n > 0)
{
inbuffer[n] = 0;
*out += inbuffer;
}
return n;
}
Service.hpp中IOExecute修改:
void IOExecute(SockSPtr sock, InetAddr &addr)
{
std::string packagestreamqueue;
// 长服务
while (true)
{
ssize_t n = sock->Recv(&packagestreamqueue);
if (n <= 0)
{
LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());
break;
}
//能保证我们读取到的是一个完整的报文吗?不能!
std::string package = Decode(packagestreamqueue);
if(package.empty()) continue;
//现在能保证我们读取到的是一个完整的jsonstr
//反序列化做处理
}
}
为了反序列做处理:
我们再在协议Protocol.hpp中添加一个外部类:
//工厂模式:能够直接帮我们去进行对象的创建
class Factory
{
public:
static std::shared_ptr<Request> BuildRequestDefault()
{
return std::make_shared<Request>();
}
};
我们再在Service.hpp中定义一个函数对象
作用:
定义一个函数对象:返回值是std::shared_ptr<Response>、参数是:std::shared_ptr<Request>
专门做任务处理:将一个结构化的请求,变成一个结构化的响应using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;
因此我们也需要在IOServer类当中添加相应的私有成员自定义函数对象变量:
private:
process_t _process;
并且在构造函数当中初始化:
IOService(process_t process): _process(process)
{
}
最后我们一共经过:7个步骤
1.读取、2.报文解析、3.反序列化处理、4.业务处理、5.序列化应答、6.添加len长度报头、7.相应回去
void IOExecute(SockSPtr sock, InetAddr &addr)
{
std::string packagestreamqueue;
// 长服务
while (true)
{
// 1.负责读取
ssize_t n = sock->Recv(&packagestreamqueue);
if (n <= 0)
{
LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());
break;
}
//能保证我们读取到的是一个完整的报文吗?不能!
// 2.报文解析,提取报文和有效载荷
std::string package = Decode(packagestreamqueue);
if(package.empty()) continue;
//现在能保证我们读取到的是一个完整的jsonstr
//反序列化做处理
auto req = Factory::BuildRequestDefault();
// 3.做反序列化
req->Deserialize(package);
// 4.业务处理
//做业务处理,我想通过一个请求得到应答
auto resp = _process(req);
// 5.构建序列化应答
//给别人响应回去
std::string respjson;
//先序列化
resp->Serialize(&respjson);
// 6.添加len长度报头
//携带报文的应答集成串
respjson = Encode(respjson);
//7.响应回去
//再发出去
sock->Send(respjson);
}
}
NetCal.hpp
因为我们要做的是一个网络版本的计算器:
因此还需要处理收到的报文:
因此我们还需要构建Reponce对象:因此在工厂Factory类中再更新:
//工厂模式:能够直接帮我们去进行对象的创建
class Factory
{
public:
static std::shared_ptr<Request> BuildRequestDefault()
{
return std::make_shared<Request>();
}
static std::shared_ptr<Response> BuildResponseDefault()
{
return std::make_shared<Response>();
}
};
NetCal.hpp
#pragma once
#include"Protocol.hpp"
#include<memory>
class NetCal
{
public:
NetCal()
{}
~NetCal()
{}
//将外部穿过来的请求,处理结果后,转回响应
std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req)
{
auto resp = Factory::BuildResponseDefault();
switch (req->Oper())
{
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 = 1;
resp->_desc = "div zero";
}
else
{
resp->_result = req->X() / req->Y();
}
break;
case '%':
if(req->Y() == 0)
{
resp->_code = 2;
resp->_desc = "mod zero";
}
else
{
resp->_result = req->X() % req->Y();
}
break;
default:
{
resp->_code = 3;
resp->_desc = "illegal operation";
}
break;
}
return resp;
}
};
调用函数cpp:ServerMain.cc
#include "TcpServer.hpp"
#include "Service.hpp"
#include "NetCal.hpp"
// ./tcpserver 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
// 我们的软件代码,我们手动划分成了三层
NetCal cal;
IOService service(std::bind(&NetCal::Calculator, &cal, std::placeholders::_1));
//构建TCP服务器
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
std::bind(&IOService::IOExecute,
&service, std::placeholders::_1, std::placeholders::_2),
port);
tsvr->Loop();
return 0;
}
验证:
pupu@pupu-ubuntu:~/computer-network/class_50/3.cal_server$ ./calserver 8888
[INFO][4267][Socket.hpp][83][2025-04-28 22:52:56] socket create success, sockfd: 3
[INFO][4267][Socket.hpp][101][2025-04-28 22:52:56] bind success
[INFO][4267][Socket.hpp][112][2025-04-28 22:52:56] listen success
客户端:ClientMain.cc
#include <iostream>
#include <ctime>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"
using namespace socket_ns;
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
SockSPtr sock = std::make_shared<TcpSocket>();
if (!sock->BuildClientSocket(serverip, serverport))
{
std::cerr << "connect error" << std::endl;
exit(1);
}
//此时已经有套接字了
srand(time(nullptr) ^ getpid());
const std::string opers = "+-*/%&^!";
int cnt = 3;
std::string packagestreamqueue;
while (true)
{
// 构建数据
int x = rand() % 10;
usleep(x * 1000);
int y = rand() % 10;
usleep(x * y * 100);
char oper = opers[y % opers.size()];//获取操作符
// 构建请求
auto req = Factory::BuildRequestDefault();
req->SetValue(x, y, oper);
// 1. 序列化
std::string reqstr;
req->Serialize(&reqstr);
// 2. 添加长度报头字段
reqstr = Encode(reqstr);
std::cout << "####################################" << std::endl;
std::cout << "request string: \n" << reqstr << std::endl;
// 3. 发送数据
sock->Send(reqstr);
//需要保证我们获得的报文是完整的
while (true)
{
// 4. 读取应答,response
ssize_t n = sock->Recv(&packagestreamqueue);
if (n <= 0)
{
break;
}
// 我们能保证我们读到的是一个完整的报文吗?不能!
// 5. 报文解析,提取报头和有效载荷
std::string package = Decode(packagestreamqueue);
if (package.empty())
continue;
std::cout << "package: \n" << package << std::endl;
// 6. 反序列化
auto resp = Factory::BuildResponseDefault();
//反序列化---->得到一个结构化数据
resp->Deserialize(package);
// 7. 打印结果
resp->Print();
break;
}
sleep(1);
// break;
}
sock->Close();
return 0;
}
运行结果:
我们的整个代码一共分成了三层:会话、表示(是我们本文章的重点、协议沟通)、和应用层(业务逻辑)
#include "TcpServer.hpp" //会话层
#include "Service.hpp" //表示层
#include "NetCal.hpp" //应用层
我们之前讲过TCP/IP五层(或四层)模型:我们一般做应用层、会话层和表示层
会话层:TcpServer做的就是会话层
表示层:设备通信的两个端,以固定的数据格式把请求反序列化、应答序列化发回去,以固定的格式,在本主机内上下层转化以及客户端服务器之间通信。
应用层:NetCal就是争对特定应用层,所定的协议Request、Require。
结语:
随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。
你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容。