计算机网络(五) —— 自定义协议简单网络程序
目录
一,关于“协议”
1.1 结构化数据
1.2 序列化和反序列化
二,网络版计算器实现准备
2.1 套用旧头文件
2.2 封装sock API
三,自定义协议
3.1 关于自定义协议
3.2 实现序列化和反序列化
3.3 测试
三,服务器实现
3.1 逻辑梳理
3.2 各头文件实现
四,客户端实现
一,关于“协议”
1.1 结构化数据
两个主机通过网络和协议进行通信时,发送的数据有两种形式:
- 如果传输的数据直接就是一个字符串,那么把这个字符串发出去,对方也能得到这个字符串
- 如果需要传输的是一个struct结构体,那么不能将结构体数据一个个发送到网络中
比如我要实现一个网络版的计算器,那么客户端给服务器发送的数据,就要包含左操作数,运算符和右操作数,那么这就不仅仅是一个字符串了,而是一组数据
所以客户端不能把这些数据一个个发送过去,需要把这些数据“打个包”,统一发到网络中,此时服务器就能获取到一个完整的数据请求,“打包”方式有两种:
方案一:将结构化的数据结合成一个大的字符串
- 比如我要发送“1+1”,用户输入的是“整型”,“字符”,“整型”
- 我们先用to_string函数把整型转为字符串,然后用strcat或者C++/string的 "+="运算符重载将这三个字符拼接成一个长字符串,然后就可以直接发送
- 最后服务器收到了长字符串,再以相同的方式进行拆分,用stoi函数将字符串转整型,就可以提取这些结构化的数据
方案二:定制结构化数据,实现序列化和反序列化
- 客户端可以定制一个结构体,将需要交互的信息放到结构体种
- 客户端发送前,将结构体的数据进行序列化,服务器收到数据后进行反序列化,此时服务器就能得到客户端发送过来的结构体,下面我们来详细讲讲序列化和反序列化
1.2 序列化和反序列化
- 序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程
- 反序列化就是把序列化的字节序列恢复为对象的过程
OSI七层模型中表示层的作用,就是“实现数据格式和网络标准数据格式的转换”。前者数据格式就是指数据再应用层上的格式,后者就是指序列化之后可以进行网络传输的数据格式
- 序列化的目的,是为了方便网络数据的发送和接收,序列化后数据就全变成了二进制数据,此时底层在进行数据传输时看到的统一都是二进制序列
- 我发的是二进制数据,所以对方收到的也是二进制数据,所以需要进行反序列化,将二进制数据转化为上层能够识别的比如字符串,整型数据
二,网络版计算器实现准备
前置博客:计算机网络(三) —— 简单Udp网络程序-CSDN博客
计算机网络(四) —— 简单Tcp网络程序-CSDN博客
下面我们来全程手搓一个网络版计算器服务,并且我们自己实现一个自定义协议,主要是为了感受一下协议的实现,后面我们就不会再自定义协议了,直接用现成的
2.1 套用旧头文件
源代码下载:计算机网络/自定义协议——网络版计算器 · 小堃学编程/Linux学习 - 码云 - 开源中国 (gitee.com)
网络版计算器我们要用到的头文件有以下几个:
我们先把前面写的头文件套用一下:
makefile:
.PHONY:all
all:servercal clientcal
Flag=#-DMySelf=1
Lib=-ljsoncpp #这个是后面使用json头文件时要用的
servercal:ServerCal.cc
g++ -o $@ $^ -std=c++11 $(Lib) $(Flag)
clientcal:ClientCal.cc
g++ -o $@ $^ -std=c++11 -g $(Lib) $(Flag)
.PHONY:clean
clean:
rm -f clientcal servercal
Log.hpp:
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
Log log;
Deamon.hpp:
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string nullfile = "/dev/null";
void Daemon(const std::string &cwd = "")
{
// 1. 忽略其他异常信号
signal(SIGCLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
// 2. 将自己变成独立的会话
if (fork() > 0)
exit(0);
setsid();
// 3. 更改当前调用进程的工作目录
if (!cwd.empty())
chdir(cwd.c_str());
// 4. 标准输入,标准输出,标准错误重定向至/dev/null
int fd = open(nullfile.c_str(), O_RDWR);
if (fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
2.2 封装sock API
在Udp和Tcp服务器编写时,可以发现在使用sock API以及填装sockaddr结构体时,步骤都非常相似,所以我们可以把这些相似的步骤都封装起来,下面是Socket.hpp的代码:
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#include <cstring>
enum
{
SocketErr = 2,
BindErr,
ListenErr,
};
const int backlog = 10;
class Sock
{
public:
Sock()
{
}
~Sock()
{
}
public:
void Socket() // 创建套接字
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
log(Fatal, "socket error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
}
void Bind(uint16_t port) // 绑定套接字
{
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;
if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) // 如果小于0就绑定失败
{
log(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen() // 监听套接字
{
if (listen(_sockfd, backlog) < 0) // 如果小于0就代表监听失败
{
log(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string *clientip, uint16_t *clientport) // 获取连接,参数做输出型参数
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(_sockfd, (struct sockaddr *)(&peer), &len);
if (newfd < 0) // 获取失败
{
log(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr)); // 把网络字节序列转化为字符串保存在ipstr数组里供用户读取
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string &ip, const uint16_t port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer));
if (n == -1)
{
std::cerr << "connect to " << ip << ":" << port << "error" << std::endl;
return false;
}
return true;
}
void Close()
{
close(_sockfd);
}
int Fd()
{
return _sockfd;
}
private:
int _sockfd;
};
三,自定义协议
3.1 关于自定义协议
在之前的文章中介绍过,任何的网络协议,都要提供两种功能,下面是博客的截图:计算机网络(一) —— 网络基础入门_计算机网络基础教程-CSDN博客
网络版计算器,用户会在命令行输入三个字符:"1","+","1",然后我们可以拼接成一个长字符串:"1 + 1",数字与运算符通过一个空格隔开,
但是,如果客户端连续发了两个字符串,那么最终服务器收到的报文就是“1 + 12 + 1”,可以发现,两个字符串粘在了一起,所以我们的自定义协议,不仅仅要提供将报文和有效载荷分离的能力,也要提供将报文与报文分开的能力,有下面两种方法:
- 方案一,用特殊字符隔开报文与报文 --> "1 + 1" \n "2 + 2"
- 方案二,在报文前面加上报文的长度,也就是报头 --> "9"\n"100 + 200"\n,这样就为一个完整的报文(其实只要有长度就可以了,这里增加\n是为了可读性,也是为了方便后面打印)
所以下面来梳理一下我们自定义协议的序列化和反序列化全流程:
3.2 实现序列化和反序列化
这个部分就是具体实现Protocol.hpp头文件了,这个文件具体包含下面几个内容:
- "100","+","200" --> "100 + 200"
- "100 + 200" --> "9"\n"100 + 200"
- "9"\n"100 + 200" --> "100 + 200"
- "100 + 200" --> "100","+","200"
该文件包含两个类,一个类是请求类,是客户端发给服务器用到的类;另一个类是响应类,是服务器处理完后,返回给客户端的类;此外还包括两个方法,分别是封装报头和将报头和有效载荷分离
Request类:
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#define MySelf 0 // 去掉注释就是用我们自己的序列化和反序列化,加上注释就是用json库提供的
const std::string blank_space = " "; // 分隔符
const std::string protocol_sep = "\n";
class Request // 计算的请求
{
public:
Request(int data1, int data2, char oper)
: x(data1), y(data2), op(oper)
{
}
Request()
{
}
~Request()
{
}
public:
bool Serialize(std::string *out) // 序列化
{
#ifdef MySelf
// 1,构建报文的有效载荷
// 需要把结构化的数据转化为字符串 struct --> string, "x op y"
std::string s = std::to_string(x);
s += blank_space;
s += op;
s += blank_space;
s += std::to_string(y);
// 走到这里的时候就是字符串 "x op y"
// 但是在传输的时候可能发过来的不是完整的一个报文:"10 + 20",而是只有半个报文:"10 + "
// 解决方案一:用特殊字符隔开报文与报文 --> "10 + 20" \n "20 + 40"
// 解决方案二:再在报文前面加一个字符串的长度也就是报头,例如s.size()
// 结合起来就是"9"\n"100 + 200"\n,为一个完整的报文,其实只要有长度就可以了,这里增加\n是为了可读性,也是为了方便后面
// 2,封装报头
*out = s;
return true;
#else
Json::Value root;
root["x"] = x;
root["y"] = y;
root["op"] = op;
Json::FastWriter w;
*out = w.write(root);
return true;
#endif
}
bool DeSerialize(const std::string &in) // 反序列化 "9"\n"10 + 20"
{
#ifdef MySelf
std::size_t left = in.find(blank_space); // 找空格的左边,"10 + 20",也就是找10的右边位置
if (left == std::string::npos) // 没找到空格,说明当前解析错误
{
return false;
}
std::string part_x = in.substr(0, left); // 截取第一个数字,也就是10
std::size_t right = in.rfind(blank_space); // 逆向再次找空格,"10 + 20",找20左边的位置
if (right == std::string::npos) // 没找到空格,说明当前解析错误
{
return false;
}
std::string part_y = in.substr(right + 1); // 截取后面的数字,也就是20,+1是因为找到的是空格的右边,+1跳过空格才是数字
if (left + 2 != right)
return false; // 数字中间还有运算符,所以left+2就应该是right的左边那个空格的左边位置,如果不是那么就是解析错误
op = in[left + 1]; // 拿到运算符
// op = in[right - 1]; //一样的
x = std::stoi(part_x); // 拿到数字
y = std::stoi(part_y);
return true;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root);
x = root["x"].asInt();
y = root["y"].asInt();
op = root["op"].asInt();
return true;
#endif
}
void DebugPrint()
{
std::cout << "新请求构建完成: " << x << " " << op << " " << y << "=?" << std::endl;
}
public:
int x;
int y;
char op; // 运算符
};
class Response // 计算的应答
{
public:
Response(int res, int c)
: result(res), code(c)
{
}
Response()
{
}
~Response()
{
}
public:
bool Serialize(std::string *out) // 序列化
{
#ifdef MySelf
// 1,构建报文的有效载荷
//"len"\n"result code"
std::string s = std::to_string(result);
s += blank_space;
s += std::to_string(code);
*out = s;
return true;
#else
Json::Value root;
root["result"] = result;
root["code"] = code;
// Json::FastWriter w;
Json::StyledWriter w;
*out = w.write(root);
return true;
#endif
}
bool DeSerialize(const std::string &in) // 反序列化
{
#ifdef MySelf
// 对服务器发过来的结果报文做解析: "result code"
std::size_t pos = in.find(blank_space); // 找空格的左边
if (pos == std::string::npos) // 没找到空格,说明当前解析错误
{
return false;
}
std::string part_left = in.substr(0, pos); // 截取第一个数字,也就是result
std::string part_right = in.substr(pos + 1); // 截取后面第二个数字,也就是code
result = std::stoi(part_left);
code = std::stoi(part_right);
return true;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root);
result = root["result"].asInt();
code = root["code"].asInt();
return true;
#endif
}
void DebugPrint()
{
std::cout << "结果响应完成, result: " << result << ", code: " << code << std::endl;
}
public:
int result; // x op y
int code; // 错误码,为0时结果正确,为其它数时对应的数表示对应的原因
};
Response类:
class Response // 计算的应答
{
public:
Response(int res, int c)
: result(res), code(c)
{
}
Response()
{
}
~Response()
{
}
public:
bool Serialize(std::string *out) // 序列化
{
#ifdef MySelf
// 1,构建报文的有效载荷
//"len"\n"result code"
std::string s = std::to_string(result);
s += blank_space;
s += std::to_string(code);
*out = s;
return true;
#else
Json::Value root;
root["result"] = result;
root["code"] = code;
// Json::FastWriter w;
Json::StyledWriter w;
*out = w.write(root);
return true;
#endif
}
bool DeSerialize(const std::string &in) // 反序列化
{
#ifdef MySelf
// 对服务器发过来的结果报文做解析: "result code"
std::size_t pos = in.find(blank_space); // 找空格的左边
if (pos == std::string::npos) // 没找到空格,说明当前解析错误
{
return false;
}
std::string part_left = in.substr(0, pos); // 截取第一个数字,也就是result
std::string part_right = in.substr(pos + 1); // 截取后面第二个数字,也就是code
result = std::stoi(part_left);
code = std::stoi(part_right);
return true;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root);
result = root["result"].asInt();
code = root["code"].asInt();
return true;
#endif
}
void DebugPrint()
{
std::cout << "结果响应完成, result: " << result << ", code: " << code << std::endl;
}
public:
int result; // x op y
int code; // 错误码,为0时结果正确,为其它数时对应的数表示对应的原因
};
添加和去掉报头函数:
std::string Encode(const std::string &content) // 添加报头
{
std::string packge = std::to_string(content.size()); // 加报头
packge += protocol_sep; // 加\n
packge += content; // 加正文
packge += protocol_sep; // 再加\n
return packge;
}
bool Decode(std::string &package, std::string *content) // 解析并去掉报头 "9"\n"10 + 20"\n -->"10 + 20" 俗称解包,但是只是去掉了报头,没有做报文的具体解析
{
std::size_t pos = package.find(protocol_sep); // 找到\n的左边
if (pos == std::string::npos)
return false; // 解析失败
std::string len_str = package.substr(0, pos); // 从开始截到我找到的\n处,把前面的9给截出来
std::size_t len = std::stoi(len_str); // 把截出来的报头转化为size_t,也就是把字符串9转化成数字9
// packge的长度 = 报头长度len_str + 有效载荷长度content_str + 两个\n 2
std::size_t total_len = len_str.size() + len + 2;
// ①找到了第一个\n说明一定有长度,如果没找到\n就说明连报头都没有
// ②有了长度报头,你也还得保证后面的内容也是完整的,如果不完整也就是长度不一样,那我也就不玩了
if (package.size() < total_len)
return false;
// 走到这一步说明我们能保证报文是完整的,开始拿有效载荷
*content = package.substr(pos + 1, len); // pos现在是第一个\n左边的位置,+1后面的就是正文内容,正文内容长度为len
// 移除一个报文,该功能需要和网络相结合
package.erase(0, total_len);
return true;
}
3.3 测试
我们可以在ServerCal.cc文件里测试上面我们的序列化和反序列化操作
先测试Request:
ServerCal.cc:
#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"
int main()
{
// Request测试--------------------
Request req(10, 20, '+');
std::string s;
req.Serialize(&s);
std::cout << "有效载荷为: " << s << std::endl;
s = Encode(s);
std::cout << "报文为:" << s;
std::string content;
bool r = Decode(s, &content); //分离报头和有效载荷
std::cout << "分离报头后的有效载荷为: "<< content << std::endl;
Request temp;
temp.DeSerialize(content); //解析有效载荷
std::cout<< "有效载荷分离后, x为: " << temp.x << " 运算符为:\"" << temp.op << "\" y为: " << temp.y << std::endl;
return 0;
}
然后是Response的测试:
ServerCal.cc:
#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"
int main()
{
// Response测试--------------------
Response resp(10, 20);
std::string s;
resp.Serialize(&s);
std::cout << "有效载荷为: " << s << std::endl;
std::string package = Encode(s); //分离报头和有效载荷
std::cout << "报文为: " << package;
s = "";
bool r = Decode(package, &s);
std::cout << "分离报头后的有效载荷为: " << s << std::endl;
Response temp;
temp.DeSerialize(s); // 解析有效载荷
std::cout << "解析有效载荷: " << std::endl;
std::cout << "结果为: " << temp.result << std::endl;
std::cout << "错误码为: " << temp.code << std::endl;
return 0;
}
三,服务器实现
3.1 逻辑梳理
服务器涉及两个个头文件和一个源文件,有点绕,下面先梳理一下:
有三个文件:
- 首先,TcpServer.hpp是服务器主函数,ServerCal.cc包含服务器初始化和启动的main函数,ServerCal.hpp是进行计算器运算的头文件
- 首先构建服务器对象,并在构造函数里将ServerCal.cc里面的运算函数带进去,然后是初始化服务器,执行创建套接字等操作,然后启动服务器
- 当服务器收到客户端发来的报文后,直接将报文传给运算函数,由运算函数做去掉报头,解析有效载荷等过程,并执行运算,最后把运算结果再次构建成响应报文,以返回值形式返回给服务器运行函数
- 然后服务器再把响应报文发给客户端,完成一次计算请求处理
3.2 各头文件实现
Server.hpp实现:
#pragma once
#include <iostream>
#include <string>
#include "Protocol.hpp"
enum
{
Div_Zero = 1,
Mod_Zero,
Other_Oper
};
class ServerCal
{
public:
ServerCal()
{
}
~ServerCal()
{
}
Response CalculatorHelper(const Request &req)
{
Response resp(0, 0);
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.code = Div_Zero;
}
else
{
resp.result = req.x / req.y;
}
}
break;
case '%':
{
if (req.y == 0)
{
resp.code = Mod_Zero;
}
else
{
resp.result = req.x % req.y;
}
}
break;
default:
resp.code = Other_Oper;
break;
}
return resp;
}
std::string Calculator(std::string &package)
{
std::string content;
if (!Decode(package, &content)) // 分离报头和有效载荷:"len"\n"10 + 20"\n
return "";
// 走到这里就是完整的报文
Request req;
if (!req.DeSerialize(content)) // 反序列化,解析有效载荷 "10 + 20" --> x=10 op="+" y=20
return "";
content = "";
Response resp = CalculatorHelper(req); // 执行计算逻辑
resp.Serialize(&content); // 序列化计算结果的有效载荷 result=10, code=0
content = Encode(content); // 将有效载荷和报头封装成响应报文 "len"\n"30 0"
return content;
}
};
TcpServer.hpp实现:
#pragma once
#include "Log.hpp"
#include "Socket.hpp"
#include <signal.h>
#include <string>
#include <functional>
using func_t = std::function<std::string(std::string &package)>;
class TcpServer
{
public:
TcpServer(uint16_t port, func_t callback)
: _port(port), _callback(callback)
{
}
bool InitServer()
{
// 创建,绑定,监听套接字
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
log(Info, "Init server... done");
return true;
}
void Start()
{
signal(SIGCHLD, SIG_IGN); // 忽略
signal(SIGPIPE, SIG_IGN);
while (true)
{
std::string clientip;
uint16_t clientport;
int sockfd = _listensockfd.Accept(&clientip, &clientport);
if (sockfd < 0)
continue;
log(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
// 走到了这里就是成功获取发起连接方IP与port,后面就是开始提供服务
if (fork() == 0)
{
_listensockfd.Close();
// 进行数据运算服务
std::string inbuffer_stream;
while (true)
{
char buffer[1280];
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
inbuffer_stream += buffer; // 这里用+=
log(Debug, "debug:\n%s", inbuffer_stream.c_str());
while (true)
{
std::string info = _callback(inbuffer_stream);
// if (info.size() == 0) //ServerCal.hpp,解析报文失败的话会返回空串
if (info.empty()) // 空的话代表inbuffstream解析时出问题,表示不遵守协议,发不合法的报文给我,我直接丢掉不玩了
break; // 不能用continue
log(Debug, "debug, response:\n%s", info.c_str());
log(Debug, "debug:\n%s", inbuffer_stream.c_str());
write(sockfd, info.c_str(), info.size());
}
}
else if (n == 0) // 读取出错
break;
else // 读取出错
break;
}
exit(0);
}
close(sockfd);
}
}
~TcpServer()
{
}
private:
uint16_t _port;
Sock _listensockfd;
func_t _callback;
};
ServerCal.cc实现:
#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"
static void Usage(const std::string &proc)
{
std::cout << "\nUsage: " << proc << "port\n\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
ServerCal cal;
TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
tsvp->InitServer();
//Daemon();
//daemon(0, 0);
tsvp->Start();
// Request测试--------------------
// Request req(10, 20, '+');
// std::string s;
// req.Serialize(&s);
// std::cout << "有效载荷为: " << s << std::endl;
// s = Encode(s);
// std::cout << "报文为:" << s;
// std::string content;
// bool r = Decode(s, &content); //分离报头和有效载荷
// std::cout << "分离报头后的有效载荷为: "<< content << std::endl;
// Request temp;
// temp.DeSerialize(content); //解析有效载荷
// std::cout<< "有效载荷分离后, x为: " << temp.x << " 运算符为:\"" << temp.op << "\" y为: " << temp.y << std::endl;
// Response测试--------------------
// Response resp(10, 20);
// std::string s;
// resp.Serialize(&s);
// std::cout << "有效载荷为: " << s << std::endl;
// std::string package = Encode(s); //分离报头和有效载荷
// std::cout << "报文为: " << package;
// s = "";
// bool r = Decode(package, &s);
// std::cout << "分离报头后的有效载荷为: " << s << std::endl;
// Response temp;
// temp.DeSerialize(s); // 解析有效载荷
// std::cout << "解析有效载荷: " << std::endl;
// std::cout << "结果为: " << temp.result << std::endl;
// std::cout << "错误码为: " << temp.code << std::endl;
return 0;
}
四,客户端实现
客户端的话,为了方便发送计算请求,会采用随机数的方式获取运算数和运算符,如下代码:
#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"
static void Usage(const std::string &proc)
{
std::cout << "\nUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
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]); //获取IP和端口
Sock sockfd;
sockfd.Socket();
if (!sockfd.Connect(serverip, serverport))
return 1;
srand(time(nullptr) ^ getpid()); // 种随机数种子
int cnt = 1;
const std::string opers = "+-*/%-=&^";
std::string inbuffer_stream;
while (cnt <= 5)
{
std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;
int x = rand() % 100 + 1;
usleep(1234);
int y = rand() % 100;
usleep(4321);
char oper = opers[rand() % opers.size()];
Request req(x, y, oper);
req.DebugPrint();
// 下面是根据协议发送给对方
std::string package;
req.Serialize(&package); // 序列化
package = Encode(package); // 形成报文
int fd = sockfd.Fd(); // 获取套接字
write(fd, package.c_str(), package.size()); // 将请求从客户端往服务端写过去
// 下面是读取服务器发来的结果并解析
char buffer[128];
ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer)); // 读取服务器发回来的结果,但是这里也无法保证能读取到一个完整的报文
if (n > 0) // 读成功了
{
buffer[n] = 0;
inbuffer_stream += buffer; // "len"\n"result code"\n
std::cout << inbuffer_stream << std::endl;
std::string content;
bool r = Decode(inbuffer_stream, &content); // 去掉报头"result code"\n
assert(r); // r为真说明报头成功去掉
Response resp;
r = resp.DeSerialize(content); // 对有效荷载进行反序列化
assert(r);
resp.DebugPrint(); // 打印结果
}
std::cout << "=================================================" << std::endl;
sleep(1);
cnt++;
}
sockfd.Close();
return 0;
}
效果演示: