[计算机网络]认识“协议”

认识“协议”

文章目录

  • 认识“协议”
    • 序列化和反序列化
    • 网络计算器
      • 引入Sock类
      • 设计协议
      • 编写服务端类
      • 启动服务端
      • 编写客户端类
      • 启动客户端
      • 程序测试

序列化和反序列化

在网络体系结构中,应用层的应用程序会产生数据,这个数据往往不是简单的一段字符串数据,而是具有一定意义的结构化数据,应用层要想在网络中发送这个结构化数据,就要将其转化成报文结构,而这个将应用程序产生的结构化数据转化成报文的过程就是序列化

数据被序列化成报文后,应用程序进程就可以使用操作系统提供了网络接口,将报文交付给操作系统,操作系统就会完成后续的操作,将数据封装,然后让网卡硬件发送出去。

报文经过网络被接收方主机的网卡硬件接收后,接收方主机的操作系统就会获取网卡硬件中接收到的报文并进行分用,然后在进程调用操作系统提供的网络接口时,将报文交给进程,进程接收到报文后,将报文转化成对应的结构化数据的过程就是反序列化。

image-20231102153656332

程序产生的结构化数据要被序列化,程序接收报文要将其反序列化,不论是序列化和反序列化都要遵守一定规则,这个规则就是双方的“协议”,此外协议还包括如何给有效载荷添加报头成为一个完整报文,如何给完整报文去除报头获得有效载荷、结构化数据的结构。一个结构化的数据,按照一定的规则序列化封装后,再根据序列化的规则进行封装操作的逆操作,也就是反序列化,一定能得到原始的结构化数据,这就是"协议"的作用。

网络计算器

为了更好的体会序列化和反序列化的过程,我们进行编码,实现一个网络计算器。为了实现这个网络计算器,我们需要实现一个进行计算的服务端, 实现一个发送请求的客户端。我们使用客户端把要计算的两个操作数发过去, 然后由服务端进行计算, 最后再把结果返回给客户端。

引入Sock类

要实现的是网络计算器,因此一定是需要使用套接字的,因此我们封装一个Sock类,类内部编写一些套接字操作函数。使用这个类方便后续代码的编写,具体代码实现如下:

static const int backlog = 64;
static const int defaultfd = -1;class Sock
{public:Sock() : _sock(defaultfd){}void Socket() // 创建套接字{_sock = socket(AF_INET, SOCK_STREAM, 0); // TCPif (_sock < 0){LogMessage(Fatal, "create socket error:%s", strerror(errno)); // 打印信息到日志文件中exit(SOCKET_ERROR);}}void Bind(uint16_t port) // 绑定IP地址和端口号{struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY;local.sin_port = htons(port);if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0){LogMessage(Fatal, "bind socket error:%s", strerror(errno));exit(BIND_ERROR);}}void Listen(){if (listen(_sock, backlog) < 0){LogMessage(Fatal, "listen socket error:%s", strerror(errno));exit(LISTEN_ERROR);}}int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in temp;memset(&temp, 0, sizeof(temp));socklen_t len = sizeof(temp);int sock = accept(_sock, (struct sockaddr *)&temp, &len);if (sock < 0){LogMessage(Warning, "accept socket error:%s", strerror(errno));}else{*clientip = inet_ntoa(temp.sin_addr);*clientport = htons(temp.sin_port);}return sock;}int Connect(std::string& serverip, uint16_t& serverport){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(serverip.c_str());server.sin_port = htons(serverport);return connect(_sock, (struct sockaddr*)&server, sizeof(server));}int Fd(){return _sock;}~Sock(){if (_sock != defaultfd){close(_sock);}}private:int _sock;
};

说明: 想了解套接字编写的更多细节可以查看博主的另一篇博客网络套接字编程(二)-CSDN博客,该Sock类内部使用了一个LogMessage函数,该函数的作用是打印日志信息到日志文件中,想了解该日志组件的更多细节可以查看博主的另一篇博客网络套接字编程(三)-CSDN博客。

设计协议

设计协议,指定客户端和服务端序列化和反序列化的具体规则,具体代码如下:

#define SEP " "
#define SEP_LEN strlen(SEP)
#define HEAD_SEP "\r\n"
#define HEAD_SEP_LEN strlen(HEAD_SEP)std::string addHeader(const std::string &send_string)//给有效载荷添加报头
{std::string s;s += std::to_string(send_string.size());s += HEAD_SEP;s += send_string;s += HEAD_SEP;return s;
}std::string removeHeader(int len, const std::string &package)//去除报头还原有效载荷
{    std::string temp = package.substr(package.size() - HEAD_SEP_LEN - len, len);  return temp;
}int readPackage(int sock, std::string &inbuffer, std::string *package)//读取报文函数
{   char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0);//读取数据if (s <= 0)return -1;//读取数据出错inbuffer += buffer;int pos = inbuffer.find(HEAD_SEP, 0);if (pos == std::string::npos) return 0;//读取到的数据中没有完整的报文std::string lenStr = inbuffer.substr(0, pos);//获取有效载荷长度字符串int len = toInt(lenStr);//计算有效载荷长度int targetPackageLen = lenStr.size() + len + 2 * HEAD_SEP_LEN;//计算一个完整报文长度 if (targetPackageLen > inbuffer.size()) return 0;//读取到的数据中没有完整的报文*package = inbuffer.substr(0, targetPackageLen);//获取报文inbuffer.erase(0, targetPackageLen);//清除已读取的报文,为下一次读取做准备return len;
}int stringSplit(const std::string &inStr, const std::string& sep, std::vector<std::string> *result)//对有效载荷字符切割
{size_t start = 0;while(start< inStr.size()){int pos = inStr.find(sep, start);if(pos == std::string::npos)//没找到对应字符串break;result->push_back(inStr.substr(start, pos - start));start = pos + sep.size();}if (start < inStr.size())//将最后一个操作数写入result->push_back(inStr.substr(start));return result->size();
}int toInt(const std::string &str)//将字符串转化成整形数据
{return atoi(str.c_str());
}class Request // 请求结构体
{public:Request(){}Request(int x, int y, char op) : _x(x), _y(y), _op(op){}bool Serialize(std::string *outStr) // 序列化{*outStr = ""; // 清空//将结构化数据转化成有效载荷字符串std::string x_string = std::to_string(_x);std::string y_string = std::to_string(_y);*outStr = x_string + SEP + _op + SEP + y_string;return true;}bool Deserialize(const std::string &inStr) // 反序列化{// 将有效载荷字符串反序列化std::vector<std::string> result;int ret = stringSplit(inStr, SEP, &result);if (ret != 3)//操作符和操作数的数量不正确return false;if (result[1].size() != 1)//操作符错误return false;_x = toInt(result[0].c_str());_y = toInt(result[2].c_str());_op = result[1][0];return true;}void Print()//打印数据{std::cout << "req.x:" << _x << " req.y:" << _y << " req.op" << _op << std::endl; }public:int _x;   // 左操作数int _y;   // 右操作数char _op; // 操作符
};class Response // 相应结构体
{public:Response(){}Response(int result, int exitcode):_result(result), _exitcode(exitcode){}bool Serialize(std::string *outStr) // 序列化{*outStr = ""; // 清空//将结构化数据转化成有效载荷字符串std::string ret_string = std::to_string(_result);std::string excd_string = std::to_string(_exitcode);*outStr = ret_string + SEP + excd_string;return true;}bool Deserialize(const std::string &inStr) // 反序列化{std::vector<std::string> result;int ret = stringSplit(inStr, SEP, &result);if (ret != 2)return false;_result = toInt(result[0].c_str());_exitcode = toInt(result[1].c_str());return true;}void Print()//打印数据{std::cout << "resp.result:" << _result << " req.exitcode:" << _exitcode << std::endl; }public:int _result;   // 计算结果int _exitcode; // 错误码
};

报文的设计

为了让客户端和服务端读取数据后,将每个有效载荷分离开来,在发送数据前,要为其添加应用层报头,报文的结构:有效载荷长度\r\n有效载荷\r\n,在客户端和服务端读取数据时,只有读到一个完整的报文结构,才认为读到了一个有效载荷,才会将该报文结构读取。

因此添加报头的方式如下: 有效载荷 ->有效载荷长度\r\n有效载荷\r\n,对应上述代码中的addHeader函数,参数是有效载荷字符串,返回值是添加报头后报文结构字符串。

去除报头的方式如下: 有效载荷长度\r\n有效载荷\r\n ->有效载荷对应上述代码中的removeHeader函数,参数是报文结构字符串,返回值是去除报头后的有效载荷字符串。

由于实现的是计算器功能,因此我们要处理的数据是类似于1+1的字符串,将其转换成有效载荷后为1 + 1,中间用空格隔开。

读取报文的方式

由于要同读取一个完整报文结构的方式来将每个有效载荷分离开,因此实现了readPackage函数,用于将每个报文结构分割。

该函数有三个参数,第一个参数是读取数据的文件描述符,第二个参数是应用层级别的缓冲区,第三个参数是获取一个完整报文结构输出型参数,从文件中读取数据后,先将数据加载到应用层级缓冲区,然后使用该缓冲区内部的数据进行报文结构的查找,如果当前缓冲区内没有一个完整的报文结构,就退出该函数,等待下一次读取文件。

报文结构查找的方式如下:首先报文的结构是有效载荷长度\r\n有效载荷\r\n,因此先寻找\r\n结构,通过该结构找到有效载荷长度,然后通过有效载荷长度计算该完整报文结构的长度,然后判断缓冲区内部数据的大小是否超过该报文结构长度。最后将这个报文结构写入输出性参数中。

引入Request类和Response类

Request类是客户端向服务端发送请求所使用的结构化数据,类内部实现了将Request类序列化成只包含有效载荷的字符串的Serialize成员函数,还实现了将只包含有效载荷的字符串反序列化成Request类的Deserialize函数。

Response类服务端向客户端发送相应所使用的结构化数据,类内部实现了将Response类序列化成只包含有效载荷的字符串的Serialize成员函数,还实现了将只包含有效载荷的字符串反序列化成Response类的Deserialize函数。

Request类和Response类的序列化和反序列化都使用了stringSplit函数和toInt函数,其中stringSplit函数是有效载荷切割函数,toInt函数是将字符串转化成整形函数。stringSplit函数的实现方式如下:由于有效载荷中操作数和操作符是由空格隔开的,因此使用空格将左操作数、操作符、右操作数分离开来。落实到代码中就是设计了一个存储字符串的vector结构的result变量,查找第一个空格将空格前的数据写入result[0],查找第二个空格将两个空格之间的数据写入result[1],将第二个空格后的数据写入result[2],由于是形如1 + 1的字符串,因此result[0]为左操作数,result[1]为操作数符,result[2]为右操作数

编写服务端类

我们将计算器服务端封装成类,类内部实现服务端的初始化,服务端启动的函数,计算器任务执行函数,具体代码实现如下:

using func_t = std::function<Response(const Request &)>;class TcpServer;class ThreadData
{public:ThreadData(int sock, std::string clientip, uint16_t clientport, TcpServer *tsvr): _sock(sock), _clientip(clientip), _clientport(clientport), _tsvr(tsvr){}public:int _sock;std::string _clientip;uint16_t _clientport;TcpServer *_tsvr;
};class TcpServer
{public:TcpServer(func_t func, uint16_t port) : _func(func), _port(port){}void initServer(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();LogMessage(Info, "Init server done,listensock:%d", _listensock.Fd());}void start(){std::string clientip;uint16_t clientport;while (true){pthread_t tid;int sock = _listensock.Accept(&clientip, &clientport);ThreadData *td = new ThreadData(sock, clientip, clientport, this);pthread_create(&tid, nullptr, ThreadRoutine, td);}}static void *ThreadRoutine(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->_tsvr->ServiceIO(td->_sock, td->_clientip, td->_clientport);delete td;return nullptr;}void ServiceIO(int sock, std::string &clientip, uint16_t clientiport){std::string inbuffer; // 读取报文缓冲区while (true){// 1.读取报文并获得有效载荷std::string package;int n = readPackage(sock, inbuffer, &package);if (n == -1)break;else if (n == 0)continue;package = removeHeader(n, package);// 2.请求反序列化Request req;req.Deserialize(package);req.Print();// 3.处理请求Response res = _func(req); // 业务逻辑!res.Print();// 4.相应序列化std::string send_string;res.Serialize(&send_string);// 5.添加报头并发送报文send_string = addHeader(send_string);send(sock, send_string.c_str(), send_string.size(), 0);}close(sock);}private:uint16_t _port;Sock _listensock;func_t _func; // 网络服务方法--进行计算
};

任务执行实体

服务端主线程初始化进行初始化操作后,服务端每获取一个客户端的连接后,会为其创建一个新线程,由这个新线程来担任任务执行的实体。主线程只做监听和获取连接的操作,新线程来完成计算器的任务。其中,新线程会调用ThreadRoutine函数执行任务。

ThreadData类的作用

由于完成任务的是新线程,因此新线程需要得到完成任务所需的参数,包括进行数据传输的套接字文件描述符、客户端IP地址、客户端端口号、服务端类的指针等。需要服务端类的指针的原因是,任务执行函数ServiceIO函数是服务端类的成员函数,需要使用服务端类的指针调用该函数。其中,ServiceIO函数中的数据计算作为网络服务方法在创建类对象时传入。

启动服务端

首先,启动服务端前,对输入的命令行参数进行纠错处理,其次,创建服务端类后,需要传入数据计算方法和要绑定的端口,具体代码如下:

Response calculate(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._exitcode = 1;elseresp._result = req._x / req._y;break;case '%':if (req._y == 0)resp._exitcode = 2;elseresp._result = req._x % req._y;break;default:resp._exitcode = 3;break;}return resp;
}void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << "port\n" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERROR);}uint16_t port = atoi(argv[1]);std::unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port));tsvr->initServer();tsvr->start();return 0;
}

编写客户端类

和服务端相同,将客户端也封装成类,提供客户端初始化成员函数,提供客户端运行函数,具体代码实现如下:

class TcpClinet
{
public:TcpClinet(std::string serverip, uint16_t serverport) : _serverip(serverip), _serverport(serverport){}void initClient(){_sock.Socket();int n = _sock.Connect(_serverip, _serverport);if (n == -1){LogMessage(Fatal, "connect socket error:", strerror(errno));exit(-1);}}void start(){std::string buffer;while (true){Request req;// 1. 获取操作数和操作符int x;int y;char op;std::cout << "Pleas enter x>";std::cin >> req._x;std::cout << "Pleas enter y>";std::cin >> req._y;std::cout << "Pleas enter op>";std::cin >> req._op;// 2. 将结构体数据序列化std::string req_string;req.Serialize(&req_string);req.Print();// 3. 添加报头req_string = addHeader(req_string);// 4. 发送报文send(_sock.Fd(), req_string.c_str(), req_string.size(), 0);// 5. 接收相应std::string package;int n = 0;do{n = readPackage(_sock.Fd(), buffer, &package);if (n < 0)break;} while (n == 0);if (n < 0)break;// 6. 去掉报头package = removeHeader(n, package);// 7. 反序列化Response resp;resp.Deserialize(package);resp.Print();}}private:Sock _sock;std::string _serverip;uint16_t _serverport;
};

客户端运行

使用start函数运行客户端后,先接收用户输入的计算数据,然后封装成Request类的结构化数据,对其序列化发送,接收数据后反序列化转换成Response类的结构体化数据输出。

启动客户端

和服务端相同,首先,启动服客户端前,对输入的命令行参数进行纠错处理,具体代码如下:

void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << "serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(USAGE_ERROR);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);std::unique_ptr<TcpClinet> tcvr(new TcpClinet(serverip, serverport));tcvr->initClient();tcvr->start();return 0;
}

程序测试

首先启动服务端,并绑定端口号为8083:

image-20231104193352884

启动客户端,并输入服务端的IP地址和端口号:

image-20231104193445281

在客户端分别输入左操作数、右操作数、操作符,查看结果:

image-20231104193615224

再尝试进行除法运算:

image-20231104193709839

由于除数为0,因此返回码为1。

说明: 该程序只有在用户输入的数据符合协议时,才具有健壮性,除此之外可能存在问题,比如,如果客户端和服务器分别在不同的平台下运行,在这两个平台下计算出请求结构体和响应结构体的大小可能会不同,此时就可能会出现一些问题。虽然当前代码存在很多潜在的问题,但这个代码能够让我们直观地体会到协议的作用,这里将其当作一份示意性代码就行了。

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

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

相关文章

Android Studio布局

线性布局 水平或竖直排列子元素的布局容器 相对布局 可针对容器内每个子元素设置相对位置&#xff08;相对于父容器或同级子元素的位置&#xff09; 网格布局 找了下面这篇文章连接可以参考&#xff08;不再赘述&#xff09; GridLayout(网格布局) | 菜鸟教程 (runoob.com) …

用WebStorm运行VUE项目

提示&#xff1a;原来用VS Code开Vue&#xff0c;可是VS Code用Ctrl打不开国际化&#xff0c;下载推荐插件也不好使 文章目录 下载WebStorm运行WebStorm实用插件 下载WebStorm 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; https://www.jetbrains.com/webs…

Hybrid App(原生+H5)开发

介绍 市面上主流的hybrid app框架主要有 React Native&#xff1a;由FaceBook开发&#xff0c;使用JavaScript和React来构建原生应用程序Flutter&#xff1a;由Google开发&#xff0c;使用Dart语言。Flutter使用自己的渲染引擎Ionic&#xff1a;基于 Web 技术&#xff08;HTM…

UE5加载websocket模块为空

今天测试UE 发现工程启动不了&#xff0c;后来看到原来是websocket模块无法加载。 解决的它的方法很简单&#xff0c;这种问题一般会出现在源码版本的引擎或者是停电了&#xff0c;导致UElaunch版本损坏&#xff0c;解决方法是来到源码版本的引擎 这个目录下&#xff1a; D:\…

相机存储卡被格式化了怎么恢复?数据恢复办法分享!

随着时代的发展&#xff0c;相机被越来越多的用户所使用&#xff0c;这也意味着更多的用户面临着相机数据丢失的问题&#xff0c;很多用户在使用相机的过程中&#xff0c;都出现过不小心格式化相机存储卡的情况&#xff0c;里面的数据也将一并消失&#xff0c;相机存储卡被格式…

【案例】3D地球(vue+three.js)

需要下载插件 <template><div class"demo"><div id"container" ref"content"></div></div> </template> <script> import * as THREE from three; // import mapJSON from ../map.json; import { Or…

windows10编译高版本openssl

参考文章 参考文章中的windows编译为低版本&#xff0c;在高版本的openssl编译中已经没有&#xff1a;“ms\do_ms.bat”这个脚本了&#xff0c;现记录下编译过程 1、准备工作 安装ActivePerl&#xff0c;安装后会自动写入环境变量&#xff0c;参照参考文章测试安装成功与否&a…

涉及多种位运算操作混合类题目——通过加转三进制(扩大状态,不变枚举量):CF1033F

https://www.luogu.com.cn/problem/CF1033F 我们发现直接用二进制来做很难做&#xff0c;但我们可以观察其给的表 我们发现如果表示成和的形式是容易进行一一对应的 对于询问的时候&#xff0c;我们直接枚举每位有的和是多少&#xff0c;虽然状态是三次的&#xff0c;但是对于…

STM32HAL-完全解耦面向对象思维的架构-时间轮片法使用(timeslice)

目录 概述 一、开发环境 二、STM32CubeMx配置 三、编码 四、运行结果 五、代码解释 六、总结 概述 timeslice是一个时间片轮询框架&#xff0c;完全解耦的时间片轮询框架&#xff0c;非常适合裸机单片机引用。接下来将该框架移植到stm32单片机运行&#xff0c;单片机…

如何设置OBS虚拟摄像头给钉钉视频会议使用

环境&#xff1a; OBS Studio 29.1.3 Win10 专业版 钉钉7.1.0 问题描述&#xff1a; 如何设置OBS虚拟摄像头给钉钉视频会议使用 解决方案&#xff1a; 1.打开OBS 底下来源这添加视频采集设备 选择OBS虚拟摄像头 2.源那再建一个图像&#xff0c;随便选一张图片 3.点击虚…

学习Opencv(蝴蝶书/C++)相关——1. 前言 和 第1章.概述

文章目录 1. 整体架构1.1 OpenCV3.01.2 Opencv4.xX. Opencv cheatsheet(小抄)1. 整体架构 1.1 OpenCV3.0 对于Opencv3.x版本,网上最常见的图,图自OpenCV Tutorial-Itseez 现在已经不是500+的算法了,而是2500+,详见:About

Keras人工智能神经网络 Regressor 回归 神经网络搭建

前期分享了使用tensorflow来进行神经网络的回归&#xff0c;tensorflow构建神经网络 本期我们来使用Keras来搭建一个简单的神经网络 Keras神经网络可以用来模拟回归问题 (regression)&#xff0c;例如给下面一组数据&#xff0c;用一条线来对数据进行拟合&#xff0c;并可以预…

Apache Pulsar 在腾讯云上的最佳实践

导语 由 StreamNative 主办的 Pulsar Meetup Beijing 2023 在2023年10月14日完美落幕&#xff0c;本次活动大咖云集&#xff0c;来自腾讯、滴滴、华为、智联招聘、RisingWave 和 StreamNative 的行业专家们一起&#xff0c;深入探讨 Pulsar 在生产环境中的最佳应用实践&#x…

openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-144)实现

文章目录 openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-144)实现概述飞达控制底板硬件电路程序的修改END openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-144)实现 概述 现在调试自己的openpnp设备, 在收尾, 将飞达控制板弄好, 能正常控制设备飞达安装平台上装满…

python实现从字符串中识别出省市区信息

从字符串中识别出省市区的信息分别存储,是我们经常会碰到的问题。如果用分词的方法去匹配获取比较麻烦,cpca包提供了便捷的调用函数transform。只要把含省市区的信息放进去,即可返回标准的含省市区的数据框。    本文详细阐述如何安装cpca包、transform函数参数定义,以及…

佳易王羽毛球馆计时计费管理系统软件下载,支持连接灯控

软件特色&#xff1a; 1、功能实用&#xff0c;操作简单&#xff0c;不会电脑也会操作&#xff0c;软件免安装&#xff0c;已内置数据库。佳易王羽毛球馆计时计费系统软件免安装版V17.6&#xff0c;是集计时计费、商品销售、会员管理、备注记事本、定时提醒功能于一体的管理软…

《面向对象软件工程》笔记——1-2章

“学习不仅是一种必要&#xff0c;而且是一种愉快的活动。” - 尼尔阿姆斯特朗 文章目录 第一章 面向对象软件工程的范畴历史方面经济方面维护方面现代软件维护观点交付后维护的重要性 需求、分析和设计方面团队开发方面没有计划&#xff0c;测试&#xff0c;文档阶段的原因面向…

Android应用集成RabbitMQ消息处理指南

Android应用集成RabbitMQ消息处理指南 RabbitMQ1、前言2、RabbitMQ简介2.1、什么是RabbitMQ2.2、RabbitMQ的特点2.3、RabbitMQ的工作原理2.4、RabbitMQ中几个重要的概念 3、在Android Studio中集成RabbitMQ3.1、在Manifest中添加权限&#xff1a;3.2、在build.gradle(:app)下添…

广告引擎检索技术快速学习

目录 一、广告系统与广告引擎介绍 &#xff08;一&#xff09;广告系统与广告粗分 &#xff08;二&#xff09;广告引擎在广告系统中的重要性分析 二、广告引擎整体架构和工作过程 &#xff08;一&#xff09;一般概述 &#xff08;二&#xff09;核心功能架构图 三、标…

2023年【起重机司机(限桥式起重机)】报名考试及起重机司机(限桥式起重机)模拟考试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 起重机司机(限桥式起重机)报名考试参考答案及起重机司机(限桥式起重机)考试试题解析是安全生产模拟考试一点通题库老师及起重机司机(限桥式起重机)操作证已考过的学员汇总&#xff0c;相对有效帮助起重机司机(限桥式起…