通过之前的学习我们已经了解了网络的基本原理以及基于套接字实现UDP或者TCP的网络通信,那么接下来在本篇当中就将继续来学习序列化和反序列化的相关知识,理解自定义协议的概念,在此将基于之前实现的TCP套接字的代码实现一个网络版本的计算器,在此将使用到jsoncpp库来辅助实现该计算器项目当中的自定义协议。在此我们还会试着将之前实现的代码当中的Socket进行封装。最后将整个的服务器代码设计完毕之后分析其整体的结构,相信通过本篇的学习能让你对协议有更深的了解!!!

1.应用层
通过前期学习我们了解到,在网络五层协议栈中,应用层位于最顶层。与协议栈其他层级不同,应用层并非固定写入操作系统内核,而是由开发人员针对具体业务需求自主实现。这一特性使得应用层解决方案能够根据不同场景灵活调整。
在之前的初步认识网络的时候我们认为协议就是进行通信的收发指定的约定,之后再进行学习就了解到协议实际上就是对应的结构体。在之前学习TCP套接字的时候我们发现了每次使用read进行读取的时候实际上是得到一串字符串,在读取的过程当中有时候是会出现将对应的数据多读或者少读,因此在实际的使用当中是需要我们在使用TCP套接字之后,设计对应的应用层的协议的。本质上也就是需要设计对应的结构体。从而实现传输**"格式化"**的数据。
那么在此理解对应的"格式化"之前需要在此先深刻的理解,真正的理解在TCP当中是如何实现对应的全双工通信的。
来看一下的示例图:

通过以上的图就可以看出在对应的协议栈当中在TCP层当中一个主机当中是存在对应的发送缓冲区和接收缓冲区的,这时主机当中的数据就能实现接收和发送互不影响。这也是一个sockfd就能实现对应的接收和发送的原因。并且我们还要知道的是在我们使用write和read等的系统调用的时候实际上并不是真正的将对应的数据直接发送到网络当中,而是从对应的缓冲区当中接收或者发送数据,这也就更能验证之前我们在进程间通信当中了解到了计算机当中的一切数据传输本质上就是拷贝。
因此实际上使用write或者send系统调用的时候,是只会讲对应的数据写入到发送缓冲区当中,之后数据是否真正的发送到网络当中就不是这些系统调用要考虑的了。而使用read或者recv等的系统调用从接收缓冲区当中进行读取的时候读取到的数据是依据对应的从网络当中读取决定的,那么这时就会出现实际上一次性需要读取到的数据无法一次性的存储到缓冲区当中,那么这时就需要传输层以上的应用层来实现对应数据的正确解析。
通过以上我们也就能初步的理解到为什么讲TCP协议称为是面向字节流的,正是以为和UDP传输的是一个个报文不同,TCP传输的是一串的字节序列,简单来理解就是UDP是将一个个的数据进行了封装,而TCP对应的数据是无对应的边界。
那么在此数据在接收的时候如何保证对应的数据能被应用层玩完整的接收到呢,在此为了解决该问题就引入了序列化和反序列化。
2. 序列化和反序列化

例如以上的示例当使用对应的TCP进行网络之间的聊天功能的时候,那么这时候就会出现用户将几条的消息发出的时候TCP协议是不会将这几条的消息内容进行拆分,而是会将这些消息的数据作为一条字符串一样发送到网络当中,那么这时在接收端就会存在得到的是一整个字符串,无法明确的解析出对应的字符串各个的形式是什么样的。
那么在此在接收端的主机当中就需要在传输层当中使用TCP得到对应的数据之后接下来再通过协议栈将数据在应用层当中进行解析,在此我们就将该过程称为反序列化。 实际上进行反序列化的方式大致是可以分为两种的,一种就得到的数据就是一个大字符串,那么这时就只需要对大的字符串进行解析即可。例如以上的示例就只需要将大的字符串进行分割,将不同的消息得到。那么对于这种的方式下进行序列化的操作就是将所有的消息合并成为一个大的字符串。
但实际上更适合的是在在发送的主机和接收的主机当中都定义一个结构体来进行数据的接收和发送,在此序列化就是将对应的信息填入到结构体当中,反序列化就是将结构体当中的信息读取出来。
3.网络版本计算器
以上我们就初步了了解了应用层当中是需要对TCP当中的字节流进行对应的序列化和反序列化,那么接下来我们就基于之前实现的TCP基于套接字实现一个网络版本的计算器,实现的要求是用户子在客户端当中传出要进行计算的数值只会会将得到的数据进行序列化的操作,接下来在通过TCP套接字在网络的基础下传输给服务器,服务器在接收到到对应的数据之后再进行反序列化的操作,之后再服务器当中进行对应的计算,最终再将计算的结果返回到客户端当中进行显示。
那么在此我们要实现对应的网络版本的计算器就分为以下的几步,首先是将Socket当中的接口进行面向对象式的封装,接下来再实现对应的自定义协议也就是数据解析当中的结构体。最后再将整个服务器进行结构化的设计。
3.1 socket封装
在之前使用对应的套接字的接口我们都是通过直接调用来使用,整个的过程还是会较为繁琐,那么接下来就试着将socket、bind、listen等系统调用进行面向对象的封装。
注:在此实现socket类我使用的是模板方法模式,以下模板方法模式的简单介绍:
模板方法模式(Template Method Pattern) 是一种行为设计模式,定义了一个操作中的算法骨架,而将一些步骤延迟到子类中实现。这样,子类可以在不改变算法结构的情况下,重新定义算法的某些特定步骤。模板方法模式通过将共同的部分抽象到父类中,减少了代码重复,并允许子类灵活地定制算法的特定步骤。
实现的代码如下所示:
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
//将对应的日志、ip协议格式转换
#include "log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
namespace SocketModule
{
using namespace LogModule;
const int gbacklog = 16;
//socket父类
class Socket
{
public:
//定义抽象的接口
virtual ~Socket() {}
//创建套接字函数
virtual void SocketOrDie() = 0;
//进行bind绑定
virtual void BindOrDie(uint16_t port) = 0;
//进行listen监听
virtual void ListenOrDie(int backlog) = 0;
//accept获取连接
virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
//connect发起连接
virtual void Connect(const std::string &server_ip, uint16_t port) = 0;
//关闭socket套接字
virtual void Close() = 0;
//recv获取消息
virtual int Recv(std::string *out) = 0;
//send发送消息
virtual int Send(std::string &message) = 0;
public:
//创建tcp服务器套接字方法
void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
//创建tcp客户端套接字方法
void BuildTcpClientMethod()
{
SocketOrDie();
}
};
const int defaultsockfd = -1;
//tcp套接字子类
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(defaultsockfd)
{
}
TcpSocket(int sockfd) : _sockfd(sockfd)
{
}
~TcpSocket()
{
}
//实现父类的虚函数接口
virtual void SocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(ExitCode::SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
}
virtual void BindOrDie(uint16_t port) override
{
InetAddr localaddr(port);
int n = ::bind(_sockfd, localaddr.GetSockaddr(), localaddr.Getlen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(ExitCode::BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
}
virtual void ListenOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(ExitCode::LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success";
}
virtual std::shared_ptr<Socket> Accept(InetAddr *clinet) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = ::accept(_sockfd, COW(peer), &len);
if(n<0)
{
LOG(LogLevel::WARNING)<<"accept waring";
return nullptr;
}
//设置客户端的地址信息
clinet->SetAddr(peer);
//返回新的套接字对象
return std::make_shared<TcpSocket>(n);
}
//进行关闭socket套接字
virtual void Close() override
{
if (_sockfd >= 0)
{
::close(_sockfd);
}
}
virtual void Connect(const std::string &server_ip,uint16_t port)override
{
InetAddr addr(server_ip,port);
int n=connect(_sockfd,addr.GetSockaddr(),addr.Getlen());
if(n<0)
{
LOG(LogLevel::FATAL)<<"connect error";
exit(ExitCode::CONNECT_ERR);
}
LOG(LogLevel::INFO)<<"connect success";
}
virtual int Send(std::string &message)override
{
return ::send(_sockfd,message.c_str(),message.size(),0);
}
virtual int Recv(std::string *out)override
{
char buffer[1024];
ssize_t n=::recv(_sockfd,buffer,sizeof(buffer)-1,0);
if(n>0)
{
buffer[n]=0;
*out+=buffer;
}
return n;
}
private:
//sockfd套接字描述符
int _sockfd;
};
}
以上就是实现的Socket.hpp文件,以上首先创建了一个Socket的分类,在该类当中实现的抽象的接口,之后这些接口的实现由具体的子类当中实现,并且在父类当中实现了不同端口当中对应的套接字具体的创建,也就是通过不同的调用组合来实现不同端口需要的服务。以上实现成这样的形式就是为了让使用Socket子类当中定义不同协议套接字的通信,例如以上实现了TCP套接字的通信,那么如果要实现UDP套接字的实现,这时就只需要再创建一个UdpSocket类,这样就能在实现UdpSocket的过程当中不需要改变原来的TcpSocket而是只需要在Socket父类当中添加UDP独特的调用方法即可,那么这样实现就可以让Socket封装当中不同部分代码的耦合度。
3.2 自定义协议
以上我们在讲对应的Socket接口进行面向对象式的封装之后接下来就需要来实现对应的对数据进行处理的自定义协议,在此我们不再采用讲对应对应的数据直接形成一条长字符串的形式,而是使用结构体的方式来实现序列化和反序列化。但是在此我们不再自己来设计对应的结构体,而是使用成熟的数据库当中提供的方式来实现。
3.2.1 认识Jsoncpp
在此使用到的是------Jsoncpp,以下是Jsoncpp的简介:
JsonCpp 是一个开源的 C++ 库,用于解析和生成 JSON(JavaScript Object Notation) 数据。它提供了一个简单易用的接口来处理 JSON 数据格式,广泛用于 C++ 项目中,特别是需要处理 JSON 格式的配置文件、Web API 数据等场景。
以上在了解了JsonCpp是什么之后接下来就来了解JsonCpp当中进行序列化和反序列化的接口是什么样的,在此在使用JsonCpp时需要先在系统当中安装。
安装的指令如下所示:
bash
ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
序列化
cpp
#include<iostream>
#include<jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"]="zhz";
root["sex"]="man";
std::string s= root.toStyledString();
std::cout<<s<<std::endl;
return 0;
}
以上代码当我们就使用到了Jsoncpp库,那么这时就需要咋头文件的引入当中加上对应的路径,以上的代码当中就是创建了应该Json库当中的Value对象root,,接下来再将对应的键值对保存到root对象当中,接下来再使用Value当中提供的toStyedString函数来讲对应的添加到Value对象当中的键值对序列化为应一个字符串。
以上的代码使用g++进行编译的时候除了需要使用-l 选项来表明编译编译代码的时候需要链接的文件,除此之外还需要使用-L 选项指明对应的文件在系统当中的路径。
具体的makefile内容如下所示:
bash
Main:test.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp -L jsoncpp/json/json.h
.PHONY:clean
clean:
rm -f Main
使用make编译代码之后之后执行对应的代码结果如下所示:

通过以上的输出结果就可以看出Json串的形式是将对应的键值对封装到大括号里面。
实际上除了使用以上的Value当中的toStyledString来实现序列化之外还可以使用StreamWrite来实现,具体的示例代码如下所示:
cpp
#include<iostream>
#include<jsoncpp/json/json.h>
#include<memory>
#include<sstream>
int main()
{
Json::Value root;
root["name"]="zhz";
root["sex"]="man";
Json::StreamWriterBuilder wbuilder;
std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
std::stringstream ss;
writer->write(root,&ss);
std::cout<<ss.str()<<std::endl;
return 0;
}
以上的代码当中首先创建出一个Value对象之后将对应的将键值对写入到Value对象当中,接下来再创建出一个一个StreamWriteBuilder对象也就是创建JSON写入器的对象,接下来再使用智能指针来创建一个StreamWrite的对象,使用StreamWriteBuilder当中的newStreamWriter进行初始化的工作,最后使用StreamWriter当中的writer方法来将序列化的数据写入到stringstream对象当中。
编译以上的代码输出的结果运行如下所示:

相比原来的直接使用使用toStyledString的方式以上通过使用创建出StreamWriter的方式提供了更多的定制选项, 如缩进、 换行符等。
除了以上对的两种方式之外实际上还可以通过FasterWriter当中的write来实现对应序列化的功能实现的示例代码如下所示:
cpp
#include <iostream>
#include <jsoncpp/json/json.h>
#include <memory>
#include <sstream>
int main()
{
Json::Value root;
root["name"] = "zhz";
root["sex"] = "man";
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
以上代码就先通过创建出对应的Value对象之后接下来再创建出对应的FastWriter对象,接下来就通过该对象当中的writer函数实现序列化的效果,最后只需要将序列化的数据写入到string对象当中即可。
以上代码编译之后执行的内容如下所示:

反序列化
以上我们已经了解到了如何使用Jsoncpp提供的方法来来实现对应序列化的工作,那么接下来就来继续来了解反序列化是操作的。
在Jsoncpp当中提供了以下的方法来实现反序列化的操作。
1.使用Json::Reader
例如以下对的示例:
cpp
#include <iostream>
#include <jsoncpp/json/json.h>
#include <memory>
#include <sstream>
int main()
{
std::string json_string = "{\"name\":\"张三\",\"age\":30,\"city\":\"北京\"}";
std::cout<<json_string<<std::endl;
Json::Reader reader;
Json::Value root;
bool p = reader.parse(json_string, root);
if (!p)
{
std::cout << "Failed to parse JSON" << std::endl;
return 1;
}
std::string name = root["name"].asString();
int age = root["age"].asInt();
std::string City = root["city"].asString();
std::cout << "Name:" << name << std::endl;
std::cout << "Age:" << age << std::endl;
std::cout << "City:" << City << std::endl;
return 0;
}
在以上代码当中先创建出Reader对象之后再使用对象当中的parse方法将json_string序列化字符串当中的反序列化的写入到Value对象当中,接着再通过对应的键得到对应的值,最后将结果输出。
编译以上的代码执输出的结果如下所示:

2.使用Json::CharReader
以上我们使用了对应的Json::Reader实现了反序列化,在此不推荐使用Json::charReader进行反序列化的操作,在此就不再对该方法进行讲解。
3.2.2自定义协议编写
在以上我们了解了Jsoncpp的使用那么接下来就可以来试着实现网络版本计算器当中应用层协议的编写,具体要实现的就是对于数据的序列化和反序列化。
我们知道在协议当中最最重要就是实现对于的结构体并且实现结构体的写入以及读取,那么接下来机先来实现计算请求以及应答结构体的代码,实现代码如下所示:
cpp
//请求类
class Requst
{
public:
Requst(int x, int y, char oper)
: _x(x), _y(y), _oper(oper)
{
}
Requst()
{
}
~Requst()
{
}
// 使用Json进行序列化处理
std::string Serialize()
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
// 使用Json进行反序列化处理
bool Deserialize(std::string &in)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in, root);
if (ok)
{
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
}
return ok;
}
int X() { return _x; }
int Y() { return _y; }
char Oper() { return _oper; }
private:
//操作数
int _x;
int _y;
//操作符
char _oper;
};
//应答类
class Response
{
public:
Response()
{
}
Response(int result, int code)
: _result(result), _code(code)
{
}
~Response()
{
}
//使用JSON进行序列化的操作
std::string Serialize()
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
return writer.write(root);
}
//使用JSON进行反序列化的操作
bool Deserialize(std::string &in)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in, root);
if (ok)
{
_result = root["result"].asInt();
_code = root["code"].asInt();
}
return ok;
}
//设置操作符
void setResult(int res)
{
_result = res;
}
//设置错误码
void SetCode(int code)
{
_code = code;
}
//展示计算的结果
void ShowResult()
{
std::cout << "计算的结果是:" << _result << "[" << _code << "]" << std::endl;
}
private:
//计算结果
int _result;
//错误码
int _code;
};
以上就是对应的协议当中的结构体,那么接下来就需要继续的实现将数据序列化的写入到Requst结构体当中的执行函数,以及将计算之后的结果写入到Respond结构体的执行函数。
但在设计对应的执行函数之前我们先要来思考一下我们知道在TCP是面向字节流的,那么这时就会存在问题就是,一个一个的报文的边界在网络层当中时无法确定的,那么这时机需要上层的应用层来确定缓冲区当中一个报文的起始和结束是具体到哪里的。在此我们就在定一个协议就是在将报文进行序列化之后在对应的报头当中添加当前报文的长度,那么在反序列化当中就可以先提取到对应报文的长度信息,接下来再从很缓冲区中读取。

基于以上的要求我们就可以实现一下的代码:
cpp
const std::string sep = "\r\n";
using func_t = std::function<Response(Requst &req)>;
// 进行格式化处理类
class Protocol
{
public:
Protocol()
{
}
Protocol(func_t func)
: _func(func)
{
}
~Protocol()
{
}
// 封装报头
std::string Ecode(const std::string &jsonstr)
{
std::string len = std::to_string(jsonstr.size());
return len + sep + jsonstr + sep;
}
// 将报文当中的报头去除,并将一条序列化数据提取出来
bool Decode(std::string &buffer, std::string *package)
{
ssize_t pos = buffer.find(sep);
if (pos == std::string::npos)
{
return false;
}
//将报文当中的长度信息提取出来
std::string package_len_str = buffer.substr(0, pos);
int package_len_int = std::stoi(package_len_str);
//报文的总长度
int target_len = package_len_str.size() + package_len_int + 2 * sep.size();
if (target_len > buffer.size())
{
return false;
}
//将对应的数据写入到输出型参数当中
*package = buffer.substr(pos + sep.size(), package_len_int);
//提取出数据之后将报文当中对应的数据移除
buffer.erase(0, target_len);
return true;
}
//获取请求报文
void GetRequst(std::shared_ptr<Socket> &sock, InetAddr &client)
{
std::string buffer_queue;
while (1)
{
int n = sock->Recv(&buffer_queue);
if (n > 0)
{
std::cout << "---------requst_buffer------------" << std::endl;
std::cout << buffer_queue << std::endl;
std::cout << "----------------------------------" << std::endl;
std::string json_package;
// 给数据去除报头
while (Decode(buffer_queue, &json_package))
{
std::cout << "---------requst_json------------" << std::endl;
std::cout << json_package << std::endl;
std::cout << "----------------------------------" << std::endl;
std::cout << "---------requst_buffer------------" << std::endl;
std::cout << buffer_queue << std::endl;
std::cout << "----------------------------------" << std::endl;
LOG(LogLevel::DEBUG) << client.StringAddr() << "请求:" << json_package;
Requst req;
// 对报文进行反序列化处理
bool ok = req.Deserialize(json_package);
if (!ok)
{
continue;
}
// 进行计算
Response resp = _func(req);
// 将计算的结果序列化
std::string json_str = resp.Serialize();
// 将结果添加报头
std::string send_str = Ecode(json_str);
// 进行发送
sock->Send(send_str);
}
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "client:" << client.StringAddr() << "Quit";
break;
}
else
{
LOG(LogLevel::WARNING) << "client:" << client.StringAddr() << " recv error";
break;
}
}
}
//获取应答报文
bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buff, Response *resp)
{
while (1)
{
int n = client->Recv(&resp_buff);
if (n > 0)
{
std::string json_package;
bool ret = Decode(resp_buff, &json_package);
if (!ret)
continue;
std::cout << "---------requst_json------------" << std::endl;
std::cout << json_package << std::endl;
std::cout << "----------------------------------" << std::endl;
std::cout << "---------requst_buffer------------" << std::endl;
std::cout << resp_buff << std::endl;
std::cout << "----------------------------------" << std::endl;
resp->Deserialize(json_package);
return true;
}
else if (n == 0)
{
std::cout << "Server quit" << std::endl;
return false;
}
else
{
std::cout << "recv erroe" << std::endl;
return false;
}
}
}
//构建请求字符串
std::string BuildRequeString(int x, int y, char oper)
{
Requst req(x, y, oper);
std::string json_req = req.Serialize();
std::cout << "---------requst_json------------" << std::endl;
std::cout << json_req << std::endl;
std::cout << "----------------------------------" << std::endl;
return Ecode(json_req);
}
private:
//执行计算的回调函数
func_t _func;
};
以上我们就实现了对应进行报文的提取和分离的函数,那么接下来就继续来实现网络版本计算器当中进行计算功能的部分。我们在此将该部分独立到NetCal.hpp文件当中实现,实现的代码如下所示:
cpp
#pragma once
#include "Protool.hpp"
#include <iostream>
class Cal
{
public:
//执行计算功能
Response Execute(Requst &req)
{
Response resp(0, 0);
switch (req.Oper())
{
case '+':
resp.setResult(req.X() + req.Y());
break;
case '-':
resp.setResult(req.X() - req.Y());
break;
case '*':
resp.setResult(req.X() * req.Y());
break;
case '/':
{
if (req.Y() == 0)
{
resp.SetCode(1);
}
else
{
resp.setResult(req.X() / req.Y());
}
}
break;
case '%':
{
if (req.Y() == 0)
{
resp.SetCode(2);
}
else
{
resp.setResult(req.X() % req.Y());
}
}
break;
default:
resp.SetCode(3);
break;
}
return resp;
}
};
以上就实现出了计算的功能,具体的实现内容就是将对应的Requst当中的x和y操作数以及oper操作符得到得到对应的计算结果。将计算结果写入到Resopnd当中。
3.2.3 服务器完整编写
服务器端
以上就将对应的自定义协议部分编写完成,那么接下来就可以将服务器进行具体的实现。在此依旧需要实现的是客服端和服务器两大部分,其本质上和之前我们使用的Tcp套接字实现通信是一样的,只不过在此使用Tcp套接字的时候是使用我们封装之后的Socket类,而不是使用原生的接口。那么接下来就先来实现Server.hpp当中的代码。
cpp
#include "Socket.hpp"
#include <functional>
#include <memory>
#include <sys/wait.h>
using namespace SocketModule;
//执行回调函数
using ioserver_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
class TcpServer
{
public:
TcpServer(uint16_t port, ioserver_t service)
: _port(port),
_listensockptr(std::make_unique<TcpSocket>()),
_isrunning(false),
_service(service)
{
_listensockptr->BuildTcpSocketMethod(_port);
}
void Start()
{
_isrunning=true;
while(_isrunning)
{
InetAddr client;
auto sock=_listensockptr->Accept(&client);
if(sock==nullptr)
{
continue;
}
pid_t pid=fork();
if(pid==0)
{
_listensockptr->Close();
if(fork()>0)
{
exit(OK);
}
_service(sock,client);
sock->Close();
exit(OK);
}
else if(pid<0)
{
LOG(LogLevel::FATAL)<<"fork error";
exit(ExitCode::FORK_ERR);
}
else{
sock->Close();
int n=::waitpid(pid,nullptr,0);
(void)n;
}
}
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensockptr;
bool _isrunning;
ioserver_t _service;
};
以上我们实现的TcpServer.hpp的代码就只是进行创建TCP连接的工作,有了之前实现的Socketd类,那么这时就需要在TcpServer类的构造函数当中创建出一个Socket的对象,之后再再而具体的数据处理的工作就通过回调函数的方式来实现。
实现了TcpServer.hpp的代码之后接下来继续来实现TcpServer.cc的代码。
实现代码如下所示:
cpp
#include "log.hpp"
#include "Protool.hpp"
#include "TcpServer.hpp"
#include <memory>
#include "NetCal.hpp"
#include"Dameon.hpp"
void Usage(std::string proc)
{
std::cerr << "Usage:" << proc << " port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
Enable_File_log_Strategy();
std::unique_ptr<Cal> cal = std::make_unique<Cal>();
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Requst &req) -> Response
{ return cal->Execute(req); });
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]), [&protocol](std::shared_ptr<Socket> &sock, InetAddr &client)
{ protocol->GetRequst(sock, client); });
tsvr->Start();
return 0;
}
以上的服务器端的代码就首先通过创建出一个进行计算的Cal对象,之后将该对象当中的Execute函数作为创建出的Protocol对象当中的回调函数。最后创建出一个TcpServer的对象再将Protocol对象当中的GetRequst作为其的回调函数。
客户端
以上我们实现了服务器端的代码之后接下来就继续来实现客户端的代码,实现的代码如下所示:
cpp
#include "Socket.hpp"
#include "Protool.hpp"
#include "Common.hpp"
#include <memory>
#include <iostream>
#include <string>
using namespace SocketModule;
void Usage(std::string proc)
{
std::cerr << "Usage:" << proc << " port" << std::endl;
}
void GetDate(int &x, int &y, char &oper)
{
std::cout << "please input x:";
std::cin >> x;
std::cout << "please input y:";
std::cin >> y;
std::cout << "please input oper:";
std::cin >> oper;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[1]);
exit(USAGE_ERR);
}
std::string server_ip = argv[1];
uint16_t port = std::stoi(argv[2]);
// 创建套接字
std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();
client->BuildTcpClientMethod();
// 进行服务器的连接
client->Connect(server_ip, port);
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();
std::string resp_buffer;
while (1)
{
int x, y;
char oper;
GetDate(x, y, oper);
// 构建序列化数据
std::string req_str = protocol->BuildRequeString(x, y, oper);
// 向服务器当中发送数据
client->Send(req_str);
// 从服务器当中获取对应的报文
Response ret;
bool res = protocol->GetResponse(client, resp_buffer, &ret);
if (res == false)
break;
// 显示结果
ret.ShowResult();
}
return 0;
}
以上就实现了Client.cc的代码,实现的原理就是通过创建出Socket的对象之后通过调用对象的内的函数来实现套接字创建以及服务器连接的工作。接下来再创建Protocol对象,接下来通过该对象当中的BuildRequeString函数来实现序列化字符串的构建,之后再向服务器当中发送数据,最后通过Getsponse函数来得到服务器计算之后的结果。最后将得到的结果显示再客户端上。
makefile编写
以上我们在将客户端和服务器的代码编写完成之后接下来就可以将对应的makeifle文件编写完成,再将整个的程序运行看是否满足我们的要求。
bash
.PHONY:all
all:Server Client
Server:TcpServer.cc
g++ -o $@ $^ -std=c++17 -ljsoncpp -lpthread
Client:TcpClient.cc
g++ -o $@ $^ -std=c++17 -ljsoncpp -lpthread
.PHONY:clean
clean:
rm -f Server Client
使用make指令编译程序之后,在客户端和服务端分别的运行,运行结果如下所示:


通过以上的输出结果就可以发现当用户输入的数据传输到服务器端之后就会发现缓冲区对应的数据被提取出来的,而在服务器端则会添加出对应的数据。
并且还能根据用户的输入操作数和操作符来实现对应的计算,例如以下示例:


通过以上的输出结果就可以看出我们实现的网络版本计算器的实现是满足要求的。
项目整体解析
在之前学习网络当中的协议栈的时候我们就了解到OSI模型是分为7层的,相比原来的5层模型当中将应用层再具体的划分为会话层、表示层和应用层。但是在之前的学习中还是无法理解为什么要将应用层具体的划分为三层,但是通过以上编写的网络网络版本计算器就可以看出实现我们在编写的过程当中就是将项目中的应用层划分为具体的三层。

通过以上就发现OSI模型当中的设计实际上是非常的合理的,基于该模型实现的代码能具备高内聚低耦合的特性,之前我们无法理解只是实现的代码还较为的简单,还无法具体的体现出该模型的优势。
除此之外系统当中未将该三层和之前的网络层等一样写入到内核当中的原因是应用层不同与其他的层,在应用层主要是受到用户的影响,那么如果这时再将其写入到内核当中就会使得用户所有的操作都是一样的。
完整代码
完整代码如下所示:
Tcp_Calculator · 追梦卓/Linux - 码云 - 开源中国
以上就是本篇的所有内容了,接下来我们将继续了解守护进程等,未完待续......