认识“协议”

文章目录:

  • 什么是协议
  • 结构化的数据传输
  • 序列化和反序列化
  • 网络版本计算器

什么是协议

在计算机网络中,协议是指在网络中进行通信和数据交换时,双方遵循的规则和约定集合。它定义了数据的传输格式、顺序、错误处理、认证和安全性等方面的规范。

协议的设计和实现是计算机网络能够正常运行的基础。它确保了不同设备和系统之间能够相互理解和协作。协议定义了数据的结构和编码方式,规定了数据传输的方式和顺序,以及双方之间进行通信的交互规则。协议的标准化和普遍采用,使得计算机网络得以互联互通,并支持各应用和服务的实现。

为了满足不同的应用场景和需求,已经存在许多成熟的应用层协议。这些协议定义了在特定应用中进行通信和数据交换的规则和格式。(http、https、DNS、ftp、smtp…)。

结构化的数据传输

协议是一种 “约定”,socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些 “结构化的数据” 怎么办呢?

例如,我们需要实现一个网络版本的简易计算器。此时,客户端给服务端发送的数据中,包含一个左操作数、操作符、右操作数。然后由服务端接收处理之后再将结果发送给客户端。

此时,就遇到了一个问题,客户端发送给服务端的不是一个简单的字符串,而是一组结构化的数据。如果客户端将这些结构化的数据单独的通过网络发送给服务端,那么服务端很难将收到的数据进行排列形成正确的数据。因此,客户端最好将这些数据一次性进行发出,此时服务端获取到的就是一个完整的数据请求,客户端常见的方案有以下两种。

约定方案一:将结构化的数据组合成为一个字符串

  • 客户端发送一个形如 “5+7” 的字符串;
  • 这个字符串中有两个操作数,都是整形;
  • 两个数字之间会有一个字符是运算符;
  • 数字和运算符之间没有空格;

这种将结构化的数据组合成一个字符串的方式可以是一个简单的方法,适用于简单的数据结构和通信需求。通过约定字符串的格式和规则,可以将结构化数据转换为字符串进行传输。

约定方案二:将结构数据序列化和反序列化

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体;
  • 这个过程叫做 “序列化” 和 “反序列化”。

通过序列化和反序列化可以更灵活地处理结构化数据,并支持复杂的数据类型和结构。通过定义一个结构体或对象来表示需要交互的信息,可以使用序列化算法将结构体转换为字符串形式进行传输。接收端则可以使用相同的序列化算法进行反序列化,将字符串转化为原始的数据结构。

序列化和反序列化

序列化和反序列化是将数据在不同表示形式之间进行转化的过程。

  • 序列化是指将数据从内存中的对象或数据结构转化为可存储或传输的格式,例如字符串、字节流或二进制数据。
  • 反序列化是将序列化之后的数据重新转化为内存中的对象或数据结构。

在这里插入图片描述

网络版本计算器

接下来我们使用自己制定的协议来写一个网络版本的计算器。

首先,将后续代码需要的日志文件引入,日志 log.hpp 代码如下:

#pragma once
#include <stdio.h>
#include <cstdarg>
#include <ctime>
#include <cassert>
#include <stdlib.h>
#include <cstring>
#include <cerrno>#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};void logMessage(int level,const char *format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);char *name = getenv("USER");char logInfo[1024];va_list ap; // ap -> char*va_start(ap, format);vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);FILE *out = (level == FATAL) ? stderr : stdout;fprintf(out, "%s | %u | %s | %s\n",log_level[level],(unsigned int)time(nullptr),name == nullptr ? "unknow" : name,logInfo);va_end(ap); // ap = NULL
}

协议定制

要实现一个网络版本的计算器,就必须确保通信双方能够遵守某种协议约定。这里我们制定一套简单的协议约定。数据分为请求数据和响应数据。因此我们可以定义请求类和响应类来实现协议约定。

// 请求类
class Request {
public:int x_;int y_;char op_;
};// 响应类
class Response {
public:int exitCode_; // 状态字段int result_;   // 计算结果
};

定义了一个名为 Request 的请求类和一个名为 Response 的响应类。这些类具有公共的成员变量来存储请求和响应的数据。

请求类中包括两个操作数和一个操作符,响应类中包含一个计算结果以及该次计算的状态字段。状态字段用于表示该次的计算是否符合计算要求。

约定状态字段对应的含义如下:

  • 0 - 表示计算成功;
  • -1 - 表示除0错误;
  • -2 - 表示模0错误;
  • -3 - 表示非法操作符;

注意,计算结果只有在状态码为0的时候才有意义,否则计算的结果是没有意义的。

序列化和反序列化

下面代码实现了一个简单的请求类和响应类,并提供了序列化和反序列化的方法来将结构化的数据转换为字符串,并将字符串转换会结构化的数据。根据定义的宏 MY_SELF 的是否定义,可以使用自定义的序列化方法或者使用 JSON 库进行序列化。

#pragma once
#include <iostream>
#include <string>
#include <cassert>
#include <jsoncpp/json/json.h>
#include "util.hpp"#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define OPS "+-*/%"
#define BUFFER_SIZE 1024
// #define MY_SELF 1// encode,整个序列化之后的字符串进行添加长度
std::string encode(const std::string &in, uint32_t len)
{std::string encodein = std::to_string(len);encodein += CRLF;encodein += in;encodein += CRLF;return encodein;
}// decode,这个序列化之后的字符串进行提取长度
// 1.必须具有完整的长度   2.必须具有和len相符合的有效载荷
// 3.具备上述两个长度才返回有效载荷和len,否则,就是一个检测函数
std::string decode(std::string &in, uint32_t *len)
{assert(len);// 1.确认是否是一个包含len的有效字符串*len = 0;std::size_t pos = in.find(CRLF);if (pos == std::string::npos)return "";// 2.提取长度std::string Len = in.substr(0, pos);int intLen = atoi(Len.c_str());// 3.确认有效载荷也是符合要求的int surplus = in.size() - 2 * CRLF_LEN - pos;if (surplus < intLen)return "";// 4.确认有完整的报文结构std::string package = in.substr(pos + CRLF_LEN, intLen);*len = intLen;// 5.将当前的报文完整的从in中移除掉int removeLen = Len.size() + package.size() + 2 * CRLF_LEN;in.erase(0, removeLen);// 6.正常返回return package;
}class Request
{
public:Request() {}~Request() {}// 序列化 - 结构化的数据 -> 字符串void serialize(std::string *out){
#ifdef MY_SELFstd::string xStr = std::to_string(x_);std::string yStr = std::to_string(y_);*out = xStr;*out += SPACE;*out += op_;*out += SPACE;*out += yStr;
#else// 1.Value对象,json基于KV,json是有两套操作方法的Json::Value root;root["x"] = x_;root["y"] = y_;root["op"] = op_;Json::FastWriter fw;*out = fw.write(root);
#endif}// 反序列化 - 字符串 -> 结构化的数据bool deserialize(std::string &in){
#ifdef MY_SELF// 100 + 200std::size_t spaceOne = in.find(SPACE);if (std::string::npos == spaceOne)return false;std::size_t spaceTwo = in.rfind(SPACE);if (std::string::npos == spaceTwo)return false;std::string dataOne = in.substr(0, spaceOne);std::string dataTwo = in.substr(spaceTwo + SPACE_LEN);std::string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));if (oper.size() != 1)return false;x_ = atoi(dataOne.c_str());y_ = atoi(dataTwo.c_str());op_ = oper[0];return true;
#elseJson::Value root;Json::Reader rd;rd.parse(in, root);x_ = root["x"].asInt();y_ = root["y"].asInt();op_ = root["op"].asInt();return true;
#endif}void debug(){std::cout << "debug---------------------" << std::endl;std::cout << x_ << " " << op_ << " " << y_ << std::endl;std::cout << "debug---------------------" << std::endl;}public:int x_;int y_;char op_;
};class Response
{
public:Response() {}~Response() {}// 序列化void serialize(std::string *out){
#ifdef MY_SELF// "exitCode_ result_"std::string ec = std::to_string(exitCode_);std::string res = std::to_string(result_);*out = ec;*out += SPACE;*out += res;
#elseJson::Value root;root["exitcode"] = exitCode_;root["result"] = result_;Json::FastWriter fw;*out = fw.write(root);
#endif}// 反序列化 - 不仅仅是在网路中应用,本地也是可以直接使用的bool deserialize(std::string &in){
#ifdef MY_SELF// "0 300"std::size_t pos = in.find(SPACE);if (std::string::npos == pos)return false;std::string codeStr = in.substr(0, pos);std::string restStr = in.substr(pos + SPACE_LEN);exitCode_ = atoi(codeStr.c_str());result_ = atoi(restStr.c_str());return true;
#elseJson::Value root;Json::Reader rd;rd.parse(in, root);exitCode_ = root["exitcode"].asInt();result_ = root["result"].asInt();return true;
#endif}void debug(){std::cout << "debug---------------------" << std::endl;std::cout << "exitCode = " << exitCode_ << " result = " << result_ << std::endl;std::cout << "debug---------------------" << std::endl;}public:// 退出状态,0表示运算结果合法,非0表示表示运行结果是非法的,!0是几就表示是因为什么原因错了。int exitCode_;int result_;
};bool makeRequest(std::string &str, Request *req)
{// 1+1char strcmp[BUFFER_SIZE];snprintf(strcmp, sizeof strcmp, "%s", str.c_str());char *left = strtok(strcmp, OPS);if (!left)return false;char *right = strtok(nullptr, OPS);if (!right)return false;char mid = str[strlen(left)];req->x_ = atoi(left);req->y_ = atoi(right);req->op_ = mid;return true;
}
  • debug 方法用于调试,打印对象的成员变量值。
  • makeRequese 函数用于解析字符串并生成请求对象。
  • 此外,代码还定义了一些变量和辅助函数来处理字符串的拼接和分割。
  • 若要使用第三方库 jsoncpp 来处理 JSON 数据的序列化和反序列化。需要确保项目中包含了这个库,并在编译时链接到正确的库文件。

服务端代码

TCP 服务器我们使用多线程版本的,当服务器初始化完成并启动之后。当有一个客户端来请求服务器时,服务器就为其创建一个新的线程用于服务该客户端。这里,我们为客户端提供的是简单的计算器功能,服务端完成客户端给出的计算并给客户端返回结果。

class ServerTcp; // 声明一下static Response calculator(const Request &req)
{Response resp;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.exitCode_ = -1; // -1,除0elseresp.result_ = req.x_ / req.y_;}break;case '%':{if (req.y_ == 0)resp.exitCode_ = -2; // -2,模0elseresp.result_ = req.x_ % req.y_;}break;default:resp.exitCode_ = -3; // -3,非法操作符break;}return resp;
}void netCalc(int sock, const std::string &clientIp, uint16_t clientPort)
{assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);// 9\r\n100 + 200\r\n    9\r\n100 + 200\r\nstd::string inbuffer;while (true){// 定义一个请求对象Request req;char buffer[128];ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s == 0){logMessage(NOTICE, "client[%s:%d] close sock,service done.", clientIp.c_str(), clientPort);break;}else if (s < 0){logMessage(WARINING, "read client[%s:%d] error,errorCode: %d,errorMessage: %s", clientIp.c_str(), clientPort, errno, strerror(errno));break;}// read successbuffer[s] = 0;inbuffer += buffer;// 1.检查inbuffer是否已经具有了一个strPackageuint32_t packgeLen = 0;std::string package = decode(inbuffer, &packgeLen);if (packgeLen == 0)continue; // 无法提取一个完整的报文,继续提取// 2.已经获取了一个完整的packageif (req.deserialize(package)){// 3.处理逻辑,输入的是一个req,得到一个respResponse resp = calculator(req); // resp是一个结构化的数据// 4.对resp进行序列化std::string respPackage;resp.serialize(&respPackage);// 5.对报文进行encoderespPackage = encode(respPackage, respPackage.size());// 6.简单进行发送write(sock, respPackage.c_str(), respPackage.size());}}
}class ThreadData
{
public:uint16_t clientPort_;std::string clientIp_;int sock_;ServerTcp *this_;ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts): clientPort_(port), clientIp_(ip), sock_(sock), this_(ts) {}
};class ServerTcp
{
public:ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1) {}~ServerTcp() {}public:void init(){// 1. 创建socketlistenSock_ = socket(PF_INET, SOCK_STREAM, 0);if (listenSock_ < 0){logMessage(FATAL, "socket:%s", strerror(errno));exit(SOCKET_ERR);}logMessage(DEBUG, "socket:%s,%d", strerror(errno), listenSock_);// 2. bind// 2.1 填充服务器信息struct sockaddr_in local; // 用户栈memset(&local, 0, sizeof local);local.sin_family = PF_INET;local.sin_port = htons(port_);ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));// 2.2 本地socket信息,写入sock_对应的内核区域if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0){logMessage(FATAL, "bind:%s", strerror(errno));exit(BIND_ERR);}logMessage(DEBUG, "bind:%s,%d", strerror(errno), listenSock_);// 3. 监听socket,为何要监听呢?tcp是面向连接的!if (listen(listenSock_, 5) < 0){logMessage(FATAL, "bind:%s", strerror(errno));exit(LISTEN_ERR);}logMessage(DEBUG, "listen:%s,%d", strerror(errno), listenSock_);}void loop(){while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);// 4. 获取连接,accept的返回值是一个新的socket fdint serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);if (serviceSock < 0){// 获取连接失败,继续获取logMessage(WARINING, "accept:%s[%d]", strerror(errno), serviceSock);continue;}// 4.1 获取客户端基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMessage(DEBUG, "accept:%s | %s[%d], socket fd:%d", strerror(errno), peerIp.c_str(), peerPort, serviceSock);// 5. 提供服务,echho -> 小写 -> 大写// v2版本 - 多线程// 多线程不需要关闭文件描述符,因为多线程会共享文件描述符表!ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)td);}}static void *threadRoutine(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);netCalc(td->sock_, td->clientIp_, td->clientPort_);delete td;}
private:int listenSock_;uint16_t port_;std::string ip_;
};

代码解释:

  • 定义了一个 calculator 函数,用于根据请求对象进行计算并返回响应对象。根据请求的操作符,执行相应的计算,并将结果储存在响应对象中。
  • netCalc 函数用于吃力与客户端的网路通信。在循环中,读取客户端发送的数据,并解析出完整的请求报文。然后,调用 calculator 函数进行计算,并将计算结果序列化为响应报文发送给客户端。
  • 其余的 TCP 服务器相关的代码在 TCP网络程序 中有详细的解释。

客户端代码

接下来实现一个简单的客户端程序,可以向服务端发送请求并接收响应。其主要作用是与服务器进行通信,实现请求和响应的交互。

客户端代码如下:

volatile bool quit = false;static void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " prot ip" << std::endl;std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8080\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t serverPort = atoi(argv[2]);std::string serverIp = argv[1];// 1. 创建socket SOCK_STREAMint sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 2. connect,向服务器发起连接请求// 2.1 先填充需要连接的远端主机的基本信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverPort);inet_aton(serverIp.c_str(), &server.sin_addr);// 2.2 发送请求,connect 会自动帮我们进行bindif (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0){std::cerr << "connect: " << strerror(errno) << std::endl;exit(CONN_ERR);}std::string message;while (!quit){message.clear();std::cout << "Place Enter# ";std::getline(std::cin, message);if (strcasecmp(message.c_str(), "quit") == 0){quit = true;continue;}// message=trimStr(message);Request req;if (!makeRequest(message, &req))continue;std::string package;req.serialize(&package);std::cout << "debug->serialize-> " << package << std::endl;package = encode(package, package.size());std::cout << "debug->encode-> \n" << package << std::endl;ssize_t s = write(sock, package.c_str(), package.size());if (s > 0){char buffer[1024];ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0)buffer[s] = 0;std::string echoPackage = buffer;Response resp;uint32_t len = 0;std::string tmp = decode(echoPackage, &len);if (len > 0){echoPackage = tmp;resp.deserialize(echoPackage);printf("[exitcode: %d] %d\n", resp.exitCode_, resp.result_);}}else if (s <= 0){break;}}return 0;
}

运行测试:

编写 makefile 构建程序,如下所示,需要定义 MY_SELF 时将 Method=-DMY_SELF 写上,不需要定义时就不需要写。

.PHONY:all
all:clientTcp serverTcpd
Method=-DMY_SELFclientTcp:clientTcp.ccg++ -o $@ $^ $(Method) -std=c++11 -ljsoncpp
serverTcpd:serverTcp.ccg++ -o $@ $^ $(Method) -std=c++11 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f serverTcpd clientTcp

使用我们写的序列化与反序列化代码进行测试。

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

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

相关文章

2018年五一杯数学建模C题江苏省本科教育质量综合评价解题全过程文档及程序

2019年五一杯数学建模 C题 江苏省本科教育质量综合评价 原题再现 随着中国的改革开放&#xff0c;国家的综合实力不断增强&#xff0c;中国高等教育发展整体已进入世界中上水平。作为一个教育大省&#xff0c;江苏省的本科教育发展在全国名列前茅&#xff0c;而江苏省13个地级…

openldap-sasl身份认证镜像

背景 在这篇文章中&#xff0c;AD域信息同步至openLDAP我们使用了SASL将身份验证从OpenLDAP委托给AD”这种方案&#xff0c;本文主要来构建此方案的docker镜像。 sasl官网&#xff1a;Cyrus SASL bitnami/openldap镜像地址&#xff1a;containers/Dockerfile bitnami/openl…

修改服务器端Apache默认根目录

目标&#xff1a;修改默认Apache网站根目录 /var/www/html 一、找到 DocumentRoot “/var/www/html” 这一段 apache的根目录&#xff0c;把/var/www/html 这个目录改 #DocumentRoot "/var/www/html" DocumentRoot "/home/cloud/tuya_mini_h5/build" 二、…

图论| 827. 最大人工岛 127. 单词接龙

827. 最大人工岛 题目&#xff1a;给你一个大小为 n x n 二进制矩阵 grid 。最多 只能将一格 0 变成 1 。返回执行此操作后&#xff0c;grid 中最大的岛屿面积是多少&#xff1f; 岛屿 由一组上、下、左、右四个方向相连的 1 形成。 题目链接&#xff1a;[827. 最大人工岛](ht…

obsidian和bookmaster

1 手动安装插件 插件地址&#xff1a;https://forum-zh.obsidian.md/t/topic/12333 安装file服务器 地址&#xff1a;http://www.rejetto.com/hfs/ hfs.exe可以改个端口 改成8866&#xff0c;ip地址也可以改成 127.0.0.1 # 因为安装到本地 如果要创建账户的话&#xff0c;就…

wpf devexpress在未束缚模式中生成Tree

TreeListControl 可以在未束缚模式中没有数据源时操作&#xff0c;这个教程示范如何在没有数据源时创建tree 在XAML生成tree 创建ProjectObject类实现数据对象显示在TreeListControl: public class ProjectObject {public string Name { get; set; }public string Executor {…

Kubernetes基础知识了解

一、Kubernetes简介 Kubernetes是一个轻便的和可扩展的开源平台&#xff0c;用于管理容器化应用和服务。通过Kubernetes能够进行应用的自动化部署和扩缩容。在Kubernetes中&#xff0c;会将组成应用的容器组合成一个逻辑单元以更易管理和发现。Kubernetes积累了作为Google生产…

微服务架构之路7,Nacos配置中心的Pull原理,附源码

目录 一、本地配置二、配置中心1、以Nacos为例&#xff1a;2、Pull模式3、也可以通过Nacos实现注册中心 三、配置中心提供了哪些功能四、如何操作配置中心1、配置注册2、配置反注册3、配置查看4、配置变更订阅 五、主流的微服务注册中心有哪些&#xff0c;如何选择&#xff1f;…

Echarts 实现两两柱图重叠(背景和实际值柱图)

Echarts实现两两重叠柱状图_echarts 重叠柱状图_Web_阿凯的博客-CSDN博客 引用启发的博客 先来效果&#xff1a; option {backgroundColor: #03213D,animation: true, // 控制动画是否开启animationDuration: 1000, // 动画的时长, 它是以毫秒为单位animationDuration: func…

(免费领源码)基于Vue+Node.js的宠物领养网站的设计与开发83352-计算机毕业设计项目选题推荐

摘 要 随着互联网大趋势的到来&#xff0c;社会的方方面面&#xff0c;各行各业都在考虑利用互联网作为媒介将自己的信息更及时有效地推广出去&#xff0c;而其中最好的方式就是建立网络管理系统&#xff0c;并对其进行信息管理。由于现在网络的发达&#xff0c;宠物领养网站的…

滚雪球学Java(09-2):Java中的关系运算符,你真的掌握了吗?

咦咦咦&#xff0c;各位小可爱&#xff0c;我是你们的好伙伴——bug菌&#xff0c;今天又来给大家普及Java SE相关知识点了&#xff0c;别躲起来啊&#xff0c;听我讲干货还不快点赞&#xff0c;赞多了我就有动力讲得更嗨啦&#xff01;所以呀&#xff0c;养成先点赞后阅读的好…

Minio - 多节点多驱动器安装部署

先决条件 网络互通 MinIO集群中的节点的网络需要互相双向互通。 MinIO API默认端口9000 MinIO console默认端口9001 MinIO强烈建议使用负载均衡器来管理与集群的连接。负载均衡器策略使用“最小连接数”逻辑&#xff0c;因为在部署中任何 MinIO 节点都可以接收、路由或处理…