南飓风 发表于 2024-11-12 20:27:24

<网络> 协议



   目录
文章目录
一、认识协议
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),而协议终极都必要通过盘算机语言的方式表示出来。只有通讯盘算机两边都遵守雷同的协议,盘算机之间才能互相通讯交流。协议就像一种通用语言,让差别装备能够相互沟通。

协议本身不是一个结构体,但它可以用结构体来描述。


[*]协议 是一套界说规则,用于规范差别装备之间的通讯。 这些规则包括数据格式、传输方式、错误处置惩罚等等。
[*]结构体 是一种数据结构,用于组织和存储相干数据。 它可以用来描述协议中界说的各种数据格式,比方数据包的结构、字段的寄义等等。

假设我们要界说一个简单的网络协议,用于传输用户信息。 这个协议可以包罗以下信息:


[*]用户名
[*]暗码
[*]邮箱地点
我们可以用一个结构体来描述这个协议的数据格式:
struct UserInformation {
    std::string username;
    std::string password;
    std::string email;
}; 这个结构体界说了协议中数据包的结构,它包罗了用户名、暗码和邮箱地点这三个字段。 
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;
      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;
};https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=data%3Aimage%2Fgif%3Bbase64%2CR0lGODlhAQABAPABAP%2F%2F%2FwAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw%3D%3D&pos_id=qIzIfx5C
相干细节:


[*]对于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;
      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 路径下
https://i-blog.csdnimg.cn/direct/37d5c095741e4d42abbe580f6fc3dca3.png
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();    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 -ljsoncpphttps://i-blog.csdnimg.cn/direct/5081910993384c1ea77631cdb6676389.png
   当然了,我们手写的序列化和反序列化函数肯定没有广为流传的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 MySelf      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;      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()    {}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 MySelf      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;#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; // 运算效果    int code;   // 运算退出码};    编译时可以直接编译宏,所以想使用自己手写的序列化函数时,在编译选项中加上-D选项,添加MySelf=1的宏,想使用Json序列化时就不添加该宏 
       -DMySelf=1     这是gcc、g++自带的一个选项,可以在编译时向源代码中添加界说一个宏,名字是MySelf,值为1
   它是怎样编译进去的
编译时步伐必要预处置惩罚、编译、汇编、链接,那么在预处置惩罚时,编译器就是对源代码进行了删除修改,所以gcc、g++本身就可以对源代码进行修改,那么在预处置惩罚时直接添加一个宏界说这也是情理之中的事变,很公道。
   PHONY:all
all:servercal clientcal

Flag=-DMySelf=1
Lib=-ljsoncpp

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 servercal clientcal3. 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;
                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;
    }

    // 请求报文去报头、反序列化后计算得到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,然后子进程退出
https://i-blog.csdnimg.cn/direct/4a44943196d14e06b21ed8e61526fd3e.png
   #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;
                  size_t n = read(sockfd, buffer, sizeof(buffer));
                  if (n > 0)
                  {
                        buffer = 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;
};https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=data%3Aimage%2Fgif%3Bbase64%2CR0lGODlhAQABAPABAP%2F%2F%2FwAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw%3D%3D&pos_id=SLGhKV0q
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);
      exit(0);
    }
    // 命令行获取port
    uint16_t port = std::stoi(argv);
   
    ServerCal cal;
    TcpServer* tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
    tsvp->InitServer();
    // 守护进程化
    daemon(0, 0);
    // 启动服务
    tsvp->Start();
    return 0;
}https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=data%3Aimage%2Fgif%3Bbase64%2CR0lGODlhAQABAPABAP%2F%2F%2FwAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw%3D%3D&pos_id=cCSQxtzd
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);
      exit(0);
    }

    std::string serverip = argv;
    uint16_t serverport = std::stoi(argv);

    // 客户端创建套接字
    Sock sockfd;
    sockfd.Socket();
    // 连接服务端
    bool r = sockfd.Connect(serverip, serverport);
    if (!r) return 1;

    // 种下随机数种子
    srand(time(nullptr) ^ getpid());
    // 设置循环变量
    int cnt =1;
    // 操作符集,在里面随机获取oper
    const 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;
      // 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;
      size_t n = read(sockfd.Fd(), buffer, sizeof(buffer));
      if (n > 0)
      {
            buffer = 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次
    }

    // 进程结束前关闭sockfd
    sockfd.Close();
    return 0;
}https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=data%3Aimage%2Fgif%3Bbase64%2CR0lGODlhAQABAPABAP%2F%2F%2FwAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw%3D%3D&pos_id=FcJFDCcK
7. 通讯测试

服务器开放8080端口,采用风格的Json序列化,不保卫进程化,客户端连接本机
https://i-blog.csdnimg.cn/direct/9918d0ef79ba494cac0d7126427efe73.png
8. 小结

在写完自界说的盘算器协议后,我们来将代码与OSI七层模子对照
   应用层 :针对 特定应用的协议,比方电子邮件协议、长途登录协议、文件传输协议


[*] 对应ServerCal.hpp 文件内的 CalculatorHelper 成员函数,这是服务器用来盘算数据请求的代码。
   表示层 :装备固有数 据格式和网络标准数据格式的转换,担当差别表现形式的信息,如笔墨流、图像、声音等


[*]对应 Protocol.hpp 内的 Request、Response 类序列化与反序列化 ,由
   会话层 :通讯管理、负责创建和断开通讯连接,管理传输层以下的分层


[*]对应 TcpServer.hpp 内的子进程处置惩罚连接请求代码 
   简单来说,协议本身是属于应用层,而序列化和反序列化步调则跨越了应用层和表示层。
更具体的表明:


[*]协议: 协议界说了数据传输和交互的规则,它属于应用层。比方,HTTP 协议界说了网页请求和相应的格式、方法等,它就是属于应用层。
[*]序列化: 序列化是将应用层的数据结构转换为可传输的格式。它可以被以为是应用层的一部门,因为它处置惩罚了数据结构的转化,以顺应网络传输。
[*]反序列化: 反序列化则是将传输来的数据还原为应用层的数据结构。它也属于应用层,因为它必要明白数据的格式和寄义,才能准确地还原数据。
   为什么序列化和反序列化跨越了应用层和表示层?
因为序列化和反序列化既涉及数据格式转换,也涉及数据结构的明白。 它们必要明白应用层的数据结构,并将它们转换为可传输的格式,同时还必要思量数据格式的转换,这部门与表示层的功能相干。
   OSI模子七层模子与Tcp / IP五层模子对比,Tcp / IP 五层模子将应用层、表示层、会话层合并成 应用层。这是由于以下原因:

[*]功能重叠: 这三个层的功能在一定程度上存在重叠。比方,表示层负责数据格式的转换,但应用层也大概必要进行类似的转换。会话层负责管理通讯会话,但应用层也大概必要进行类似的管理。
[*]实际应用: 在实际应用中,这三个层的功能常常被合并在一起实现。比方,一个 web 欣赏器会直接与 HTTP 服务器进行交互,而不会单独使用表示层或会话层。
[*]简化模子: 将这三个层合并可以简化网络模子,使其更容易明白和学习。
必要注意的是,虽然五层模子将应用层、表示层和会话层合并在一起,但它们的本质功能并没有消失。这些功能依然存在,只是它们在五层模子中被整合到了应用层。 
        应用层、表示层、会话层这三层之所以没有被封装在OS内核而是由用户实现是因为我们根据差别的应用场景会出现差别的协议,根据差别的协议又会添加差别的报头,由于是差别的协议,所以会采用差别的序列化反序列化方法,这些都是无法同一的,OS也不敢肯定自己的序列化反序列化方法是最优的,也不能同意协议同一的报头字段,所以这三层无法被压缩在内核中,应该交给用户,让用户自己实现。
        我们可以制作应用层协议,同样的在互联网发展的几十年中,各种大神也设计了许多应用层协议,比方HTTP、HTTPS、DHCP等广为流传的协议。
   通过协议的组合,我们用能实现出许多的软件,比方
Web 服务器:


[*]Apache HTTP Server: 最流行的开源 Web 服务器,以稳固性、可靠性和模块化设计著称。
[*]Nginx: 高性能 Web 服务器和反向代理服务器,以其速率、效率和机动配置闻名。
[*]Microsoft IIS (Internet Information Services): 微软提供的 Web 服务器,重要用于 Windows 平台。
[*]Lighttpd: 轻量级的 Web 服务器,恰当资源有限的服务器环境。
[*]H2O: 高性能、低内存消耗的 Web 服务器,特殊恰当处置惩罚大量的并发连接。
数据库服务器:


[*]MySQL: 开源关系型数据库管理系统 (RDBMS),广泛应用于各种应用步伐。
[*]PostgreSQL: 开源关系型数据库管理系统,以其数据完备性和安全功能著称。
[*]Oracle Database: 商业关系型数据库管理系统,以其强大的功能和性能闻名。
[*]Microsoft SQL Server: 微软提供的关系型数据库管理系统,重要用于 Windows 平台。
[*]MongoDB: 非关系型数据库 (NoSQL),以其机动的文档模子和高性能著称。
[*]Redis: 内存数据库,以其快速相应速率和高可用性著称。
   
Nginx 是一个高性能的开源 Web 服务器和反向代理服务器,也是一个邮件代理服务器和 HTTP 缓存服务器。它因其高性能、稳固性和机动配置而闻名,被广泛用于各种 Web 应用步伐和服务。Nginx 就可以被看作是封装了各种协议,并基于这些协议实现了一系列功能的软件。它并非发明新的协议,而是利用现有的网络协议,比方 HTTP、TCP、UDP 等,来构建自己的功能。
你可以把它想象成一个乐队,乐队本身不是音乐,而是由演奏差别乐器的乐手构成,他们共同演奏出美好的音乐。Nginx 就如同这个乐队,它本身不是协议,而是使用各种协议(乐器)来实现各种功能(音乐)。
总结一下:


[*]Nginx 本身是一个软件,而不是协议。
[*]Nginx 依靠于各种网络协议来工作。
[*]Nginx 通过组合使用这些协议,并提供额外的功能,如负载均衡、缓存等,成为了一个强大的服务器软件。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: <网络> 协议