目录
文章目录
一、认识协议
1. 协议概念
2. 结构化数据传输
3. 序列化和反序列化
二、网络计算器
1. 封装socket类
2. 协议定制
request类的序列化和反序列化
response类的序列化和反序列化
报头的添加与去除
Json序列化工具
Jsoncpp 的主要特点:
Jsoncpp 的使用方法:
3. ServerCal.hpp
4. TcpServer.hpp
5. Server.cc
6. ClientCal.cc
7. 通信测试
一、认识协议
1. 协议概念
在计算机网络中,协议是至关重要的,它就像一套共同遵守的规则和约定,使得不同类型的设备能够相互理解和通信。想象一下,如果你和一位来自不同国家的人想交流,但你们说不同的语言,就会造成很大的障碍。再例如,如果我们和家长规定一个打电话协议,电话铃响一次表示我们一切安好,不需要接通电话,如果电话铃响两次则表示这个月没钱了,需要家长打饭钱,不需要接通电话,如果电话铃响三次则表示有突发情况,需要接通电话
为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。协议就像一种通用语言,让不同设备能够相互沟通。
2. 结构化数据传输
通信双方在进行网络通信时:
- 如果需要传输的数据是一个字符串,那么直接将这一个字符串发送到网络当中,此时对端也能从网络当中获取到这个字符串。
- 但如果需要传输的是一些结构化的数据,此时就不能将这些数据一个个发送到网络当中。
比如现在要实现一个网络版的计算器,那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应的操作符,此时客户端要发送的就不是一个简单的字符串,而是用结构体封装的一组结构化的数据。
如果客户端将这些结构化的数据单独一个个的发送到网络当中,那么服务端从网络当中获取这些数据时也只能一个个获取,但是服务端还需要纠结如何将接收到的数据进行组合。因此客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据,可以更方便的进行获取数据。
客户端常见的“打包”方式有以下两种。
将结构化的数据组合成一个字符串
约定方案一:
- 客户端发送一个形如“1 + 1”的字符串。
- 这个字符串中有两个操作数,都是整型。
- 两个数字之间会有一个字符是运算符。
- 数字和运算符之间有空格。
客户端可以按某种方式将这些结构化的数据组合成一个字符串,然后将这个字符串发送到网络当中,此时服务端每次从网络当中获取到的就是这样一个字符串,然后服务端再以相同的方式对这个字符串进行解析,此时服务端就能够从这个字符串当中提取出这些结构化的数据。
定制结构体+序列化和反序列化
约定方案二:
- 定制结构体来表示需要交互的信息。
- 发送数据时将这个结构体按照一个一致的规则转换成网络标准数据格式,接收数据时再按照相同的规则把接收到的数据转化为结构体。
- 这个过程叫做“序列化”和“反序列化”。
客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后再对其进行反序列化,此时服务端就能得到客户端发送过来的结构体,进而从该结构体当中提取出对应的信息。
3. 序列化和反序列化
序列化和反序列化:
- 序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程。
- 反序列化是把字节序列恢复为对象的过程。
OSI七层模型中表示层的作用就是,实现设备固有数据格式和网络标准数据格式的转换。其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式。
序列化和反序列化的目的
- 在网络传输时,序列化目的是为了方便网络数据的发送和接收,无论是何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络数据传输时看到的统一都是二进制序列。
- 序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制序列的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。
我们可以认为网络通信和业务处理处于不同的层级,在进行网络通信时底层看到的都是二进制序列的数据,而在进行业务处理时看得到则是可被上层识别的数据。如果数据需要在业务处理和网络通信之间进行转换,则需要对数据进行对应的序列化或反序列化操作。
不同的编译器规定不同,导致同一个结构体的大小可能不同,因为存在结构体内存对齐,所以这就可能导致结构体大小不同,而导致收发数据的双方数据不能对齐
- 协议本身是一种约定
二、网络计算器
1. 封装socket类
Socket.hpp
由于我们在Udp、Tcp网络编程时经常使用socket的各种接口,所以我们就将这些接口封装起来,便于后续使用
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"const int backlog = 10;enum{SockErr=2,BindErr,ListenErr
};class Sock
{
public:Sock(){}~Sock(){}
public:void Socket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){_log(Fatal, "socket create error, strerror: %s, errno: %d", strerror(errno), errno);exit(SockErr);}}void Bind(uint16_t port){struct sockaddr_in local;memset(&local, 9, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_sockfd, (const struct sockaddr*)&local, sizeof(local)) < 0){_log(Fatal, "bind create error, strerror: %s, errno: %d", strerror(errno), errno);exit(BindErr);}}void Listen(){if (listen(_sockfd, backlog) < 0){_log(Fatal, "bind create error, strerror: %s, errno: %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, strerror: %s, errno: %d", strerror(errno), errno);return -1;}char ipstr[64];inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(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);peer.sin_addr.s_addr = inet_addr(ip.c_str());int n = connect(_sockfd, (const struct sockaddr*)&peer, sizeof(peer));if (n == -1){std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;return false;}return true;}void Close(){if (_sockfd >= 0)close(_sockfd);}int Fd(){return _sockfd;}
private:int _sockfd;
};
相关细节:
- 对于socket函数,服务端和客户端都需要创建套接字打开网络文件。对于Tcp服务器来说,这里的 socket 实则为 listensocket,因为Tcp服务器需要监听网络中是否有连接请求,accept返回的 fd 才是服务器要服务的连接请求。
- 对于 bind 函数,只有服务器需要手动 bind ,客户端在向 sockfd 发送数据前会自动进行bind(随机的port、固定的ip),在服务器 bind 时,只需要传入 port 端口号即可,需要注意不能bind特定的IP地址,因为服务器一般有多张网卡,一旦bind特定的网卡ip后,就不能接收到来自其他网卡的信息。
- 对于 listen 函数,只有Tcp服务器需要 listen,监听网络中是否有对该服务器的连接请求
- 对于 accept 函数,只有Tcp服务器需要accept,accept成功后会返回newfd,对于本次的连接,服务器会向该 fd 文件发送和接受数据信息。该函数会记录客户端的 ip 和 port,并向外输出主机字节序的port,和点分十进制的IP地址(输出型参数)
- 对于 connect 函数,只有Tcp客户端需要 connect,该函数需要传入参数服务器的 ip 和 port ,从而在互联网中确定唯一的主机中唯一的进程
- 对于close 函数,在本次服务完成后,可手动进行close网络文件,避免服务器进程耗尽文件描述符而崩溃
- 对于 Fd 函数,方便对外返回 sockfd 成员变量
2. 协议定制
protocol.hpp
对于计算器功能,服务器与客户之间需要定制一个公认的协议:
- 客户端要发送 “1 + 1”格式的数据请求(左操作数 操作符 右操作数)
- 服务端要发送“2 0”格式的数据响应(表示result和code)
- 为了防止因网络原因导致的数据丢失,报文不完整,我们需要在数据序列化之后在字符串之前加上报头,len \n content \n,len表示本次发送的内容的长度,便于服务器分割出请求数据的内容。\n 表示每个报文之前的分隔符,便于服务器识别单个数据请求
request类的序列化和反序列化
const std::string blank_sapce_sep = " "; // 内容分隔符class Request
{
public:Request(int data1, int data2, char oper) : x(data1), y(data2), op(oper){}Request(){}
public:// 序列化bool Serialize(std::string* out){// 构建序列化字符串, struct -> string, "x op y"std::string s = std::to_string(x) + blank_sapce_sep + op + blank_sapce_sep + std::to_string(y);// 输出序列化后的字符串*out = s;return true;}// 反序列化bool Deserialize(const std::string& in){ssize_t left = in.find(blank_sapce_sep);if (left == std::string::npos) return false;std::string part_x = in.substr(0, left);size_t right = in.rfind(blank_sapce_sep);if (right == std::string::npos) return false;std::string part_y = in.substr(right + 1);if (left + 2 != right) return false;op = in[left + 1];x = std::stoi(part_x);y = std::stoi(part_y);return true;}void DebugPrint(){std::cout << "新请求构建完成: " << x << op << y << "=?" << std::endl;}
public:int x; // 左操作数int y; // 右操作数char op; // 操作符
};
- 序列化函数:将类的成员变量x、y、op 转为 “x op y” 的字符串格式,并输出(out是输出型参数),将结构化数据转为字符串数据
- 反序列化函数:将 “x op y” 的字符串格式转为类内的 x、y、op 成员变量,即字符串数据转为结构化数据
- DebugPrint函数:输出类内的三个成员变量,便于debug观察
response类的序列化和反序列化
- 服务器接收到客户发来的计算请求后,就会进行计算,但是客户可能会发来有问题的算术式,例如除零、模零、错误操作符这类情况,所以我们在返回计算结果的同时,也需要告诉用户本次计算结果是否可信,这里的可信表示的是计算是否合法,结果是否正确
- 计算正确则code为0,不正确则code为制定值
- 计算函数我们单独封装
const std::string blank_sapce_sep = " "; // 内容分隔符class Response
{
public:Response(int res, int c) : result(res), code(c){}Response(){}
public:// 序列化bool Serialize(std::string* out){// 构建序列化字符串std::string s = std::to_string(result);s += blank_sapce_sep + std::to_string(code);// 输出序列化字符串*out = s;return true;}// 反序列化bool Deserialize(const std::string& in){ssize_t pos = in.find(blank_sapce_sep);if (pos == std::string::npos) return false;std::string part_left = in.substr(0, pos);std::string part_right = in.substr(pos + 1);result = std::stoi(part_left);code = std::stoi(part_right);return true;}void DebugPrint(){std::cout << "结果响应完成, result: " << result << ", code: " << code << std::endl;}
public:int result; // 运算结果int code; // 运算退出码
};
报头的添加与去除
Encode 添加报头,并封装为完整的报文 “len”\ncontent\n
- 函数需要获取序列化之后的content,并返回添加报头后的整个序列化字符串
- 添加报头是为了双方更好的辨别本次的数据报长度是否正确,有没有丢失、异常增多情况,也可以更好的分割出数据内容。
- 报头与数据内容之间需要加上\n,进行分隔
- 在内容后也需要加上\n,进行数据报之间的分隔
const std::string protocol_sep = "\n"; // 报文分隔符// 封装头部报文, "len"\n content \n
std::string Encode(std::string& content)
{std::string package = std::to_string(content.size());package += protocol_sep + content + protocol_sep;return package;
}
Decode去除报头,并检测报文是否合法正常
- 函数需要传入获取到的带有报头的序列化字符串,并向 content 返回去除报头后的数据内容
- 先find '\n',看是否是一个报文
- 然后获取头部的len字符串,转为int类型
- 计算理论上的报文长度,len + 2 + len.size(),进行总报文长度比对,如果总报文长度比理论值要下,那么表明这次的报文有部分丢失,然后丢弃
特别注意:我们每一次处理一个报文后就会在传入的package字符串中删除该报文
// 去除头部报文, 获取报文内容,并检查报文是否丢失数据(用报文头部的len来判断)
bool Decode(std::string& package, std::string* content)
{// 获取头部的len字符串并转为int类型size_t pos = package.find(protocol_sep);// 没有\n 表示没有一个完整报文,出错返回if (pos == std::string::npos) return false;std::string len_str = package.substr(0, pos);size_t len = std::stoi(len_str);// 计算理论总长size_t total_len = len_str.size() + 2 + len;// 判断报文长度是否正确if (package.size() < total_len) return false;// 输出报文内容的字符串*content = package.substr(pos + 1, len);// 获取内容后删除报文package.erase(0, total_len);return true;
}
Json序列化工具
JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式,它以人类可读的文本格式表示数据对象。JSON 在网络应用程序中广泛使用,用于数据交换和 API 通信。实际上就是序列化反序列化的工具。
Jsoncpp 是一个 C++ 库,用于处理 JSON 数据。它是一个功能强大、易于使用的库,可用于解析、生成和操作 JSON 数据。
Java、Python、Php等语言都支持 Json,同样的 C++ 也要支持 Json,所以我们直接安装第三方库
sudo yum install -y jsoncpp-devel
我们知道,安装第三方库实际上就是将该库的头文件放在我们的 /usr/include 路径下,将 .o 文件放在 /lib64 路径下
Jsoncpp 的主要特点:
- 跨平台: Jsoncpp 支持多个平台,包括 Windows、Linux、Mac OS X 等。
- 易于使用: Jsoncpp 提供了一个简洁易懂的 API,使开发者能够轻松地解析、生成和操作 JSON 数据。
- 高效性: Jsoncpp 经过优化,以提供高效的性能,并支持大规模 JSON 数据处理。
- 功能丰富: Jsoncpp 提供了丰富的功能,包括:
- 解析 JSON 字符串
- 生成 JSON 字符串
- 访问 JSON 对象的属性和元素
- 遍历 JSON 对象
- 对 JSON 数据进行格式化和压缩
- 错误处理机制
Jsoncpp 的使用方法:
包含头文件:
#include "json/json.h"
创建 Json::Value 对象:
Json::Value root;
添加数据:
root["name"] = "John Doe";
root["age"] = 30;
root["city"] = "New York";
序列化 (生成 JSON 字符串):
Json::StyledWriter writer;
std::string json_string = writer.write(root);
std::cout << json_string << std::endl;
反序列化 (解析 JSON 字符串):
Json::Reader reader;
Json::Value value;
reader.parse(json_string, value);
访问数据:
std::string name = value["name"].asString();
int age = value["age"].asInt();
std::string city = value["city"].asString();
举例:
- 使用改库时,要加上头文件 <jsoncpp/json/json.h>
- 在编译时需要加上 -ljsoncpp 选项
#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
int main()
{// 类似结构体,可以嵌套Json::Value part1;part1["man"] = "man";part1["haha"] = "haha";// json序列化Json::Value root;root["x"] = 100;root["y"] = 200;root["op"] = "+"; root["desc"] = "this oper is +";root["test"] = part1;// Json::FastWriter w;Json::StyledWriter w;std::string s = w.write(root);std::cout << s << std::endl;// json反序列化Json::Value v;Json::Reader r;r.parse(s, v);int x = v["x"].asInt();int y = v["y"].asInt();char op = v["op"].asString()[0];std::string desc = v["desc"].asString();Json::Value temp = v["test"];std::cout << x << std::endl;std::cout << y << std::endl;std::cout << op << std::endl;std::cout << desc << std::endl;return 0;
}
g++ test -ljsoncpp
当然了,我们手写的序列化和反序列化函数肯定没有广为流传的Json工具更加方便、高效,所以我们可以保留我们手写的序列化函数,使用条件编译#ifdef,在编译时来选择使用哪一种序列化和反序列化方式
#pragma once
#include <string>
#include <iostream>
#include <jsoncpp/json/json.h>const std::string blank_sapce_sep = " "; // 内容分隔符
const std::string protocol_sep = "\n"; // 报文分隔符// 封装头部报文, "len"\n content \n
std::string Encode(std::string& content)
{std::string package = std::to_string(content.size());package += protocol_sep + content + protocol_sep;return package;
}// 去除头部报文, 获取报文内容,并检查报文是否丢失数据(用报文头部的len来判断)
bool Decode(std::string& package, std::string* content)
{// 获取头部的len字符串并转为int类型size_t pos = package.find(protocol_sep);// 没有\n 表示没有一个完整报文,出错返回if (pos == std::string::npos) return false;std::string len_str = package.substr(0, pos);size_t len = std::stoi(len_str);// 计算理论总长size_t total_len = len_str.size() + 2 + len;// 判断报文长度是否正确if (package.size() < total_len) return false;// 输出报文内容的字符串*content = package.substr(pos + 1, len);// 获取内容后删除报文package.erase(0, total_len);return true;
}class Request
{
public:Request(int data1, int data2, char oper) : x(data1), y(data2), op(oper){}Request(){}
public:// 序列化bool Serialize(std::string* out){
#ifdef MySelf// 构建序列化字符串, struct -> string, "x op y"std::string s = std::to_string(x) + blank_sapce_sep + op + blank_sapce_sep + std::to_string(y);// 输出序列化后的字符串*out = s;return true;
#else// json序列化Json::Value root;root["x"] = x;root["y"] = y;root["op"] = op;// 序列化结果输出// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}// 反序列化bool Deserialize(const std::string& in){
#ifdef MySelfssize_t left = in.find(blank_sapce_sep);if (left == std::string::npos) return false;std::string part_x = in.substr(0, left);size_t right = in.rfind(blank_sapce_sep);if (right == std::string::npos) return false;std::string part_y = in.substr(right + 1);if (left + 2 != right) return false;op = in[left + 1];x = std::stoi(part_x);y = std::stoi(part_y);return true;
#elseJson::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(){}
public:// 序列化bool Serialize(std::string* out){
#ifdef MySelf// 构建序列化字符串std::string s = std::to_string(result);s += blank_sapce_sep + std::to_string(code);// 输出序列化字符串*out = s;return true;
#else// json序列化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 MySelfssize_t pos = in.find(blank_sapce_sep);if (pos == std::string::npos) return false;std::string part_left = in.substr(0, pos);std::string part_right = in.substr(pos + 1);result = std::stoi(part_left);code = std::stoi(part_right);return true;
#elseJson::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; // 运算结果int code; // 运算退出码
};
编译时可以直接编译宏,所以想使用自己手写的序列化函数时,在编译选项中加上-D选项,添加MySelf=1的宏,想使用Json序列化时就不添加该宏
-DMySelf=1
这是gcc、g++自带的一个选项,可以在编译时向源代码中添加定义一个宏,名字是MySelf,值为1
它是如何编译进去的
编译时程序需要预处理、编译、汇编、链接,那么在预处理时,编译器就是对源代码进行了删除修改,所以gcc、g++本身就可以对源代码进行修改,那么在预处理时直接添加一个宏定义这也是情理之中的事情,很合理。
PHONY:all
all:servercal clientcalFlag=-DMySelf=1
Lib=-ljsoncppservercal:ServerCal.ccg++ -o $@ $^ -std=c++11 $(Lib) $(Flag)
clientcal:ClientCal.ccg++ -o $@ $^ -std=c++11 -g $(Lib) $(Flag).PHONY:clean
clean:rm -f servercal clientcal
3. ServerCal.hpp
ServerCal.hpp文件中,我们封装计算器的计算功能,并实现服务器接收到客户发来的请求报文,进行去报头、反序列化、获取结果、序列化、加报头然后return返回处理后的字符串的函数。
- 该文件最终会被引用在服务器的主函数中。
- 该类内的成员函数Calculator,最终会被回调的方式绑定在TcpServer类中
#pragma once
#include <iostream>
#include "Protocol.hpp"// 计算错误
enum
{Div_Zero = 1, // 除零Mod_Zero, // 模零Other_Oper // 运算符错误,只支持 + - * /
};class ServerCal
{
public:ServerCal(){}~ServerCal(){}// 将计算结果放入response并返回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;elseresp.result = req.x / req.y;}break;case '%':{if (req.y == 0)resp.code = Mod_Zero;elseresp.result = req.x % req.y;}break;default:resp.code = Other_Oper;break;}return resp;}// 请求报文去报头、反序列化后计算得到result、code// 再将运算结果序列化、加报头后的字符串返回std::string Calculator(std::string& package){// 去报头std::string content;bool r = Decode(package, &content);if (!r) return "";// 反序列化Request req;r = req.Deserialize(content);if (!r) return "";// 计算content = "";Response resp = CalculatorHelper(req);// 序列化resp.Serialize(&content);// 加报头content = Encode(content);return content;}
};
4. TcpServer.hpp
我们前面的Sock类封装各种sock接口函数,所以在 TcpServer.hpp 这个文件中,我们引用Sock头文件,封装一个负责通信的TcpServer的模块,来管理服务器端的通信事务
- 成员变量使用封装的Sock类对象,因为我们是Tcp服务端,所以第一个sockfd实际上名为listensockfd
- 构造函数初始化列表初始化成员变量,port(服务器端口号),callback(Calculator回调函数)
- InitServer 函数负责服务器的sock创建、bind、listen。
- Start 函数负责服务器的运行。
- 服务器接收到用户连接后,就创建子进程,让子进程去执行通信服务,然后父进程关闭该sockfd,然后再去连接新的客户服务请求(因为创建子进程比较方便,我们也可以复用之前的线程池),父进程会提前捕捉SIGCHLD信号,不用wait子进程。
- 这里的关键点在子进程提供服务的逻辑,首先我们采用服务器一直运行原则(while死循环),然后创建一个 inbuffer_stream 字符串,子进程每次从本次连接的 sockfd 中 read 到数据后就追加在 inbuffer_stream 中,原因在于服务器一次 read 可能包含多个客户的计算请求报文,如果服务器 read 一次就只计算一次,那么后面的客户计算请求就会被忽视,这是不合理的,所以我们将每次 read 的数据追加在 inbuffer_stream 中,然后循环处理inbuffer_stream,调用回调函数 Calculator,将 inbuffer_stream 作为参数传进回调函数,回调函数内部会进行去报头、反序列化、获取结果、序列化、加报头后返回处理后的要发送给客户端的带有报头的序列化字符串(在去报头 Decode 函数中,会自动删除已经处理掉报头的一整个报文,从而达到处理一个计算请求报文,就删除一个计算请求报文的行为),如果 inbuffer_stream 内已经没有一个完整的计算请求报文,或已经为空时,在回调函数 Calculator 中就会返回一个空串,如果返回的是空串,那么就 break 掉本次 inbuffer_stream 字符串的循环处理,否则就将回调函数Calculator处理后的要发送给客户端的带有报头的序列化字符串 write 给客户。
- read读到0表示写端关闭,所以break跳出子进程的循环read,然后子进程退出
#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(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();_log(Info, "Init server ... done");return true;}void Start(){// 捕捉CHLD信号,父进程不需要等待子进程// 捕捉PIPE管道信号,防止因为客户端退出而导致服务端被OS杀死signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);while (true){std::string clientip;uint16_t clientport;int sockfd = _listensock.Accept(&clientip, &clientport);if (sockfd < 0) continue;_log(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);// 提供服务if (fork() == 0){// 子进程关闭不需要的listensocketfd,防止误写_listensock.Close();std::string inbuffer_stream;// 数据计算while (true){char buffer[2048];size_t n = read(sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;inbuffer_stream += buffer;_log(Debug, "server receive a package\n%s", inbuffer_stream.c_str());// 1次read的报文内可能有多个请求,所以循环处理while (true){std::string info = _callback(inbuffer_stream);if (info.empty()) break;_log(Debug, "server response a package:\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 _listensock;func_t _callback;
};
5. Server.cc
服务端的各种模块我们都已经封装完毕,所以在Server,cc中我们直接包含这些手写的头文件即可组合实现服务端逻辑代码
- TcpServer.hpp 依赖 Socket.hpp
- ServerCal.hpp 依赖 protocol.hpp
- Server.cc 依赖 TcpServer.hpp 和 ServerCal.hpp
- 采用命令行参数获取服务器的port端口号
- 然后 new 出 TcpServer 对象 tsvp,将 ServerCal 类对象的成员函数 Calculator 绑定到 tsvp 对象的 callback 成员变量中,然后执行服务器的 Init 初始化操作,再启动服务器
- 根据情况可以选择将服务器进程守护进程化
#include "TcpServer.hpp"
#include "ServerCal.hpp"
#include <unistd.h>static void Usage(const std::string& proc)
{std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}int main(int argc, char* argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}// 命令行获取portuint16_t port = std::stoi(argv[1]);ServerCal cal;TcpServer* tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));tsvp->InitServer();// 守护进程化daemon(0, 0);// 启动服务tsvp->Start();return 0;
}
6. ClientCal.cc
客户端的主函数,该文件负责客户端的计算请求构建、发送、接受服务端的计算结果响应
- 先采用命令函参数获取服务器的IP地址和port端口号
- 使用我们封装的Socket类,定义对象sockfd,然后 sockfd.Socket() 创建套接字、sockfd.Connect() 连接服务器
- 采用随机数获取操作数和操作符,将他们放在结构化的Request类对象中,然后序列化、加报头,write 发送给服务器
- read接收服务器响应的计算结果,去报头、反序列化,输出处理后的数据
#include "Protocol.hpp"
#include "Socket.hpp"
#include <iostream>
#include <string>
#include <ctime>
#include <assert.h>
#include <unistd.h>// Usage用法提示
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]);// 客户端创建套接字Sock sockfd;sockfd.Socket();// 连接服务端bool r = sockfd.Connect(serverip, serverport);if (!r) return 1;// 种下随机数种子srand(time(nullptr) ^ getpid());// 设置循环变量int cnt =1;// 操作符集,在里面随机获取operconst std::string opers = "+-*/%~!^";// 报文缓冲区std::string inbuffer_stream;while (cnt <= 10){std::cout << "===============第" << cnt << "次测试==============" << std::endl;// 随机生成运算int x = rand() % 100 + 1;int y = rand() % 100;char op = opers[rand() % opers.size()];// 1. 放到协议结构体内Request req(x, y, op);req.DebugPrint(); // 打印出来看一下// 2. 序列化std::string package;req.Serialize(&package);// 3. 加报头package = Encode(package);// 4. 发送write(sockfd.Fd(), package.c_str(), package.size());// 接收服务器处理后返回的带有报头的序列化字符串char buffer[128];size_t n = read(sockfd.Fd(), buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;inbuffer_stream += buffer;// 打印观察接收到的加有头文件的序列化字符串std::cout << "client receive a package: " << std::endl << inbuffer_stream << std::endl;// 1. 去报头std::string content;bool r = Decode(inbuffer_stream, &content);assert(r);// 2. 反序列化Response resp;r = resp.Deserialize(content);assert(r);// 3. 输出处理后数据resp.DebugPrint();}// std::cout << "================================" << std::endl;sleep(1);cnt++; // 只发送10次}// 进程结束前关闭sockfdsockfd.Close();return 0;
}
7. 通信测试
服务器开放8080端口,采用风格的Json序列化,不守护进程化,客户端连接本机