<网络> 协议

目录

文章目录

一、认识协议

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序列化,不守护进程化,客户端连接本机

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/58523.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

群控系统服务端开发模式-应用开发-文件上传功能开发

一、文件上传路由 在根目录下route文件夹中app.php文件中&#xff0c;添加文件上传功能路由&#xff0c;代码如下&#xff1a; Route::post(upload/file,common.Upload/file);// 上传文件接口 二、功能代码开发 在根目录下app文件夹下common文件夹中创建上传控制器并命名为Up…

pycharm小游戏贪吃蛇及pygame模块学习()

由于代码量大&#xff0c;会逐渐发布 一.pycharm学习 在PyCharm中使用Pygame插入音乐和图片时&#xff0c;有以下这些注意事项&#xff1a; 插入音乐&#xff1a; - 文件格式支持&#xff1a;Pygame常用的音乐格式如MP3、OGG等&#xff0c;但MP3可能需额外安装库&#xf…

检索增强和知识冲突学习笔记

检索增强生成任务&#xff08;Retrieval-Augmented Generation, RAG&#xff09;是一种自然语言处理技术&#xff0c;它结合了信息检索和生成模型&#xff0c;用于生成高质量的文本输出。具体来说&#xff0c;RAG 模型在生成文本时&#xff0c;会先通过检索模块从外部知识库或文…

从0开始深度学习(25)——多输入多输出通道

之前我们都只研究了一个通道的情况&#xff08;二值图、灰度图&#xff09;&#xff0c;但实际情况中很多是彩色图像&#xff0c;即有标准的RGB三通道图片&#xff0c;本节将更深入地研究具有多输入和多输出通道的卷积核。 1 多输入通道 当输入包含多个通道时&#xff0c;需要…

网管平台(进阶篇):如何正确的管理网络设备?

网络设备作为构建计算机网络的重要基石&#xff0c;扮演着数据传输、连接和管理的关键角色。从交换机、路由器到防火墙、网关&#xff0c;各类网络设备共同协作&#xff0c;形成了高效、稳定的网络系统。本文将详细介绍网络设备的种类&#xff0c;并探讨如何正确管理这些设备&a…

论文 | Teaching Algorithmic Reasoning via In-context Learning

这篇论文《通过上下文学习教授算法推理》探讨了如何通过上下文学习&#xff08;In-context Learning, ICL&#xff09;有效训练大型语言模型&#xff08;LLMs&#xff09;以进行算法推理。以下是从多个角度对这项工作的详细解读&#xff1a; 1. 问题陈述与研究动机 算法推理的…

RK3568平台(基础篇)性能分析工具

一.Linux 性能优化工具简介 Linux 系统性能指标无非就是这几个方面,CPU、内存、磁盘 I/O、文件系统、网络等相关指标。不同的性能指标都有对应的具体命令工具进行查看与监控,接下来我们将介绍一些常见的 Linux 系统性能指标及其对应的命令工具(通过命令工具找出 Linux 系统性…

2024阿里云CTF Web writeup

《Java代码审计》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484219&idx1&sn73564e316a4c9794019f15dd6b3ba9f6&chksmc0e47a67f793f371e9f6a4fbc06e7929cb1480b7320fae34c32563307df3a28aca49d1a4addd&scene21#wechat_redirect 前言 又是周末…

Bartender 5 for Mac 菜单栏管理软件 安装教程【保姆级教程,操作简单小白轻松上手使用】

Mac分享吧 文章目录 Bartender 5 for Mac 菜单栏管理软件 安装完成&#xff0c;软件打开效果一、Bartender 5 菜单栏管理软件 Mac电脑版——v5.2.3⚠️注意事项&#xff1a;1️⃣&#xff1a;下载软件2️⃣&#xff1a;安装软件3️⃣&#xff1a;打开软件&#xff0c;根据自己…

职场逆袭!学会管理上司,你也能成为职场赢家

书友们&#xff0c;不要错过了&#xff01;我挖到了一本真正让我彻夜难眠的小说&#xff0c;情节跌宕起伏&#xff0c;角色鲜活得就像从书里跳出来陪你聊天。每一页都是新的惊喜&#xff0c;绝对让你欲罢不能。要是你也在寻找那种让人上瘾的阅读体验&#xff0c;这本书就是你的…

Actor-Critic方法【A2C,A3C,Policy Gradient】

强化学习笔记系列目录 第一章 强化学习基本概念 第二章 贝尔曼方程 第三章 贝尔曼最优方程 第四章 值迭代和策略迭代 第五章 强化学习实例分析:GridWorld 第六章 蒙特卡洛方法 第七章 Robbins-Monro算法 第八章 多臂老虎机 第九章 强化学习实例分析:CartPole 第十章 时序差分法…

若依管理系统使用已有 Nacos 部署流程整理

背景 玩了一下开源项目 RuoYi 管理系统Cloud 版&#xff0c;卡住的地方是&#xff1a;它用到了 nacos 配置管理&#xff0c;如果用的 nacos 环境是单机且是内置数据库的话&#xff0c;该怎么配置呢&#xff1f; 本文整理本机启动 RuoYi Cloud 应用本地部署的过程&#xff0c;…

数字信号处理-FPGA插入不同误码率的模拟源

module data_error_injector (input clk, // 时钟信号&#xff0c;50MHzinput reset, // 复位信号&#xff0c;高有效input DIN_EN, // 数据输入使能&#xff0c;高有效input [7:0] ERROR_LEVEL, // 错误等级…

对称二叉树(力扣101)

题目如下: 思路 对于这道题, 我会采用递归的解法. 看着对称的二叉树, 写下判断对称的条件, 再进入递归即可. 值得注意的是, 代码中会有两个函数, 第一个是isSymmetric,第二个是judge. 因为这里会考虑到一种特殊情况, 那就是 二叉树的根结点(最上面的那个),它会单独用…

山东布谷科技:关于直播源码|语音源码|一对一直播源码提交App Store的流程及重构建议

自从YY、六间房开启国内聊天室和秀场等网红盛行的网络红利时代以来&#xff0c;紧随其后国内各大音视频平台相应出现&#xff0c;先有映客花椒等直播平台的风头正劲&#xff0c;后有功能板块更丰富的头条抖音Tiktok等&#xff0c;盈利功能点不仅仅有直播PK连麦等礼物打赏功能&a…

k8s图形化显示(KRM)

在master节点 kubectl get po -n kube-system 这个命令会列出 kube-system 命名空间中的所有 Pod 的状态和相关信息&#xff0c;比如名称、状态、重启次数等。 systemctl status kubelet #查看kubelet状态 yum install git #下载git命令 git clone https://gitee.com/duk…

ArcGIS地理空间平台 manager 任意文件读取漏洞复现

0x01 产品描述&#xff1a; ‌ ArcGIS‌是一个综合的地理空间平台&#xff0c;由Esri开发&#xff0c;旨在为专业人士和组织提供全面的地理信息系统&#xff08;GIS&#xff09;功能。ArcGIS通过集成和连接地理环境中的数据&#xff0c;支持创建、管理、分析、映射和共享…

【Rust实现命令模式】

Rust实现命令模式 什么是命令模式命令模式的应用场景命令模式的在Rust中的关系图Rust中的命令模式代码示例运行结果总结 什么是命令模式 命令模式,即通过统一接口,如C#interface,亦或C中的抽象类的0方法,通过定义统一的接口,在定义不同的对象,为之接口实现具体的方法逻辑,再通…

【学术论文投稿】React全攻略:构建高性能前端应用的黄金法则

第六届国际科技创新学术交流大会暨管理科学信息化与经济创新发展学术会议&#xff08;MSIEID 2024&#xff09;_艾思科蓝_学术一站式服务平台 更多学术会议论文投稿请看&#xff1a;https://ais.cn/u/nuyAF3 目录 引言 1. React简介 2. React的三大核心概念 2.1 JSX 2.2…

鸿蒙应用App测试-专项测试(DevEco Testing)

注意&#xff1a;大家记得先学通用测试在学专项测试 鸿蒙应用App测试-通用测试-CSDN博客 注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注&am…