模拟实现消息队列项目(系列7) -- 实现BrokerServer

目录

前言

1. 创建BrokerServer类

1.1 启动服务器

1.2 停止服务器

1.3 处理一个客户端的连接

1.3.1 解析请求得到Request对象

1.3.2 根据请求计算响应

1.3.3 将响应写回给客户端

1.3.4 遍历Session的哈希表,把断开的Socket对象的键值对进行删除

2. 处理订阅消息请求详解(补充)

3. 序列化/反序列化实现(补充)

结语


前言

        上一章节,我们定义了本项目的应用层传输协议.并且创建了各种参数类.本章节的目标是对BrokerServer(实现一个TCP服务器)进行实现,对连接进行处理,根据请求计算响应返回给客户端.


1. 创建BrokerServer类

public class BrokerServer {// 当前考虑一个一个服务器中只有一个虚拟主机private VirtualHost virtualHost = new VirtualHost("default");// 使用哈希表表示当前会话,也就是有哪些客户端在和服务器进行通信// key: channelId value:对应对的Socket对象private ConcurrentHashMap<String, Socket> sessions = new ConcurrentHashMap<>();private ServerSocket serverSocket = null;// 引入线程池来处理多个客户端private ExecutorService executorService = null;// 控制服务器是否继续运行private volatile boolean runnable = true;
}

1.1 启动服务器

1. 首先将线程池进行创建,用来处理多个连接.

2. 设置循环用来监听连接

3. 将处理连接交给线程池.

/*** 1. 启动服务器*/public void start() throws IOException {System.out.println("[BrokerServer] 启动!");// newCachedThreadPool自动申请新的线程executorService = Executors.newCachedThreadPool();try {while (runnable){Socket clientSocket = serverSocket.accept();// 把处理连接的逻辑发送给线程池executorService.submit(()->{processConnection(clientSocket);});}}catch (SocketException e){System.out.println("[BrokerServer] 服务器停止运行!");
//            e.printStackTrace();}}

1.2 停止服务器

1. 将标志位runnable设置为false

2. 停止线程池的服务

3. 关闭服务器套接字

/*** 2. 停止服务器*/public void stop() throws IOException {runnable = false;// 停止线程池executorService.shutdownNow();serverSocket.close();}

1.3 处理一个客户端的连接

1. 我们是从请求中获取的信息是二进制文件,我们不能直接使用InputStream和OutputStream,我们借助DataInputStream和DataOutputStream进行操作字节流.

2. 使用DataInputStream进行读取请求的时候,读到末尾的时候会抛出一个异常,我们将这个异常视作为处理正确的业务逻辑.我么catch掉这个异常就可以.

3. 解析得到请求对象

4. 更具请求计算响应

5. 当处理完响应之后,要进行关闭连接,并且将一个连接中其他Channel进行关闭.

/*** 3. 处理一个客户端的连接*    在一个连接中会出现多个请求和多个响应.在一个连接中要循环的处理*/private void processConnection(Socket clientSocket) {try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){try(DataInputStream dataInputStream = new DataInputStream(inputStream);DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){while (true){// 1. 读取并解析请求Request request = readRequest(dataInputStream);// 2. 根据请求计算响应Response response = process(request,clientSocket);// 3. 将响应写回客户端writeResponse(dataOutputStream, response);}}}catch (EOFException | SocketException e){// 处理正确的业务逻辑// 上述进行读取数据的时候,如果数据读到末尾(EOF) ,就会抛出一个异常// 借助这个异常结束上述循环System.out.println("[BrokerServer]  连接关闭! 客户端地址:" +clientSocket.getInetAddress().toString()+ ",端口号: "+clientSocket.getPort());}catch (ClassNotFoundException | MqException e) {e.printStackTrace();} catch (IOException e) {// 处理真正的异常System.out.println("[BrokerServer] connection 出现异常");e.printStackTrace();}finally {try {// 当连接处理完成之后,进行关闭连接clientSocket.close();// 一个连接中可能会包含多个channel,需要把当前这个Socket对应的所有channel进行关闭clearClosedSession(clientSocket);} catch (IOException e) {e.printStackTrace();}}}

1.3.1 解析请求得到Request对象

1. 根据我们自定义的格式,先读前4个字节是请求的类型,在读4个字节是payload的长度,在读就是payload.

2. 读取payload的时候,我们先根据长度创建字符数组,然后按照字符数组进行获取payload,比较读取完的长度是否与原来请求的长度一致,不一致说明有消息的丢失.进行抛出异常.

3. 最后得到完整的请求对象,交给下面的方法进行处理.

 /*** 3.1 解析请求得到Request对象*/private Request readRequest(DataInputStream dataInputStream) throws IOException {Request request = new Request();// 1. 首先读取四个字节,为请求的typerequest.setType(dataInputStream.readInt());// 2. 在读四个字节就是payload的长度request.setLength(dataInputStream.readInt());// 3. 创建字符数组,并进行读取到数组中byte[] payload = new byte[request.getLength()];int n = dataInputStream.read(payload);if (n != request.getLength()){throw new IOException("[BrokerServer] 请求格式出错");}// 4. 将读取的数组内容写入到实体的Request对象中request.setPayload(payload);return request;}

1.3.2 根据请求计算响应

1. 我们根据请求对象的payload进行解析,此处需要注意的是,我们读取到的payload是字节数组,我们需要进行反序列化成字符数组.

2. 根据请求对象的Type值进行区分,到底客户端要调用服务器那些功能.

3. 处理完请求之后就要进行构造响应了.

4. 返回响应对象

/*** 3.2 根据请求计算响应* @param request* @param clientSocket* @return*/private Response process(Request request, Socket clientSocket) throws IOException, ClassNotFoundException, MqException {// 1. 根据request中的payload进行解析//  payload 是根据 request 中 type 进行变化的BasicArguments basicArguments = (BasicArguments) BinaryTool.fromBytes(request.getPayload());// 打印请求的信息System.out.println("[Request] rid=" + basicArguments.getRid()+ ", channelId=" + basicArguments.getChannelId() +"type=" + request.getType() + ",length=" + request.getLength());// 2. 根据type的值,区分调用哪种功能boolean ok = true;if (request.getType() == 0X1){// 1. 创建一个channelsessions.put(basicArguments.getChannelId(),clientSocket);System.out.println("[BrokerServer] 创建channel完成 getChannelId="+ basicArguments.getChannelId());}else if (request.getType() == 0x2){// 2. 销毁一个channelsessions.remove(basicArguments.getChannelId());System.out.println("[BrokerServer] 销毁channel完成 getChannelId="+ basicArguments.getChannelId());}else if (request.getType() == 0x3){// 3. 创建交换机ExchangeDeclareArguments arguments = (ExchangeDeclareArguments) basicArguments;// 调用虚拟主机的功能方法ok = virtualHost.exchangeDeclare(arguments.getExchangeName(),arguments.getExchangeType(),arguments.isDurable(),arguments.isAutoDelete(),arguments.getArguments());System.out.println("[BrokerServer] 创建交换机完成 ExchangeName="+ arguments.getExchangeName());}else if (request.getType() == 0x4){// 4. 销毁交换机ExchangeDeleteArguments arguments = (ExchangeDeleteArguments) basicArguments;ok = virtualHost.exchangeDelete(arguments.getExchangeName());System.out.println("[BrokerServer] 删除交换机完成 ExchangeName="+ arguments.getExchangeName());}else if (request.getType() == 0x5) {// 5. 创建队列QueueDeclareArguments arguments = (QueueDeclareArguments) basicArguments;ok = virtualHost.queueDeclare(arguments.getQueueName(), arguments.isDurable(),arguments.isExclusive(), arguments.isAutoDelete(), arguments.getArguments());System.out.println("[BrokerServer] 创建队列完成 QueueName="+ arguments.getQueueName());} else if (request.getType() == 0x6) {// 6. 删除队列QueueDeleteArguments arguments = (QueueDeleteArguments) basicArguments;ok = virtualHost.queueDelete((arguments.getQueueName()));System.out.println("[BrokerServer] 删除队列完成 QueueName="+ arguments.getQueueName());} else if (request.getType() == 0x7) {// 7. 创建绑定QueueBindArguments arguments = (QueueBindArguments) basicArguments;ok = virtualHost.queueBind(arguments.getQueueName(),arguments.getExchangeName(), arguments.getBindingKey());System.out.println("[BrokerServer] 创建绑定完成 QueueName="+ arguments.getQueueName()+ ",ExchangeName=" + arguments.getExchangeName());} else if (request.getType() == 0x8) {// 8. 删除绑定QueueUnbindArguments arguments = (QueueUnbindArguments) basicArguments;ok = virtualHost.queueUnbind(arguments.getQueueName(), arguments.getExchangeName());System.out.println("[BrokerServer] 删除绑定完成 QueueName="+ arguments.getQueueName()+ ",ExchangeName=" + arguments.getExchangeName());} else if (request.getType() == 0x9) {// 9. 发布消息BasicPublishArguments arguments = (BasicPublishArguments) basicArguments;ok = virtualHost.basicPublish(arguments.getExchangeName(), arguments.getRoutingKey(),arguments.getBasicProperties(), arguments.getBody());System.out.println("[BrokerServer] 发布消息完成 ExchangeName=" + arguments.getExchangeName());}else if (request.getType() == 0xa) {// 10. 订阅消息BasicConsumeArguments arguments = (BasicConsumeArguments) basicArguments;ok = virtualHost.basicConsume(arguments.getConsumeTag(), arguments.getQueueName(), arguments.isAutoAck(), new Consumer() {@Overridepublic void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws IOException, MqException {// 将服务器收到的消息进行推送给客户端// 先知道当前这个收到的消息, 要发给哪个客户端.// 此处 consumerTag 其实是 channelId. 根据 channelId 去 sessions 中查询, 就可以得到对应的// socket 对象了, 从而可以往里面发送数据了// 1. 根据 channelId 找到 socket 对象Socket clientSocket = sessions.get(consumerTag);if (clientSocket == null || clientSocket.isClosed()) {throw new MqException("[BrokerServer] 订阅消息的客户端已经关闭!");}// 2. 构造响应数据SubScribeReturns subScribeReturns = new SubScribeReturns();subScribeReturns.setChannelId(consumerTag);subScribeReturns.setRid(""); // 由于这里只有响应, 没有请求, 不需要去对应. rid 暂时不需要.subScribeReturns.setOk(true);subScribeReturns.setConsumerTag(consumerTag);subScribeReturns.setBasicProperties(basicProperties);subScribeReturns.setBody(body);byte[] payload = BinaryTool.toBytes(subScribeReturns);Response response = new Response();// 0xc 表示服务器给消费者客户端推送的消息数据.response.setType(0xc);// response 的 payload 就是一个 SubScribeReturnsresponse.setLength(payload.length);response.setPayload(payload);// 3. 把数据写回给客户端.//    注意! 此处的 dataOutputStream 这个对象不能 close !!!//    如果 把 dataOutputStream 关闭, 就会直接把 clientSocket 里的 outputStream 也关了.//    此时就无法继续往 socket 中写入后续数据了.DataOutputStream dataOutputStream = new DataOutputStream(clientSocket.getOutputStream());writeResponse(dataOutputStream, response);}});}else if (request.getType() == 0xb) {// 10. 调用 basicAck 确认消息.BasicAckArguments arguments = (BasicAckArguments) basicArguments;ok = virtualHost.basicAck(arguments.getQueueName(), arguments.getMessageId());System.out.println("[BrokerServer] 消费者确认消息完成 QueueName=" + arguments.getQueueName()+ ", MessageId=" + arguments.getMessageId());} else {// 当前的 type 是非法的.throw new MqException("[BrokerServer] 未知的 type! type=" + request.getType());}// 3. 构造响应BasicReturns basicReturns = new BasicReturns();basicReturns.setChannelId(basicArguments.getChannelId());basicReturns.setRid(basicArguments.getRid());basicReturns.setOk(ok);byte[] payload = BinaryTool.toBytes(basicReturns);Response response = new Response();response.setType(request.getType());response.setLength(payload.length);response.setPayload(payload);System.out.println("[Response] rid=" + basicReturns.getRid() + ", channelId=" + basicReturns.getChannelId()+ ", type=" + response.getType() + ", length=" + response.getLength());return response;}

1.3.3 将响应写回给客户端

1. 注意写入类型和长度是写入固定的4个字节,那么我们就使用dataOutputStream.writeInt()

2. 写完响应之后,记得要刷新缓冲区 dataOutputStream.flush();

/*** 3.3 将响应写回给客户端* @param dataOutputStream* @param response*/private void writeResponse(DataOutputStream dataOutputStream, Response response) throws IOException {// 将响应的属性从计算好的响应中进行设置dataOutputStream.writeInt(response.getType());dataOutputStream.writeInt(response.getLength());dataOutputStream.write(response.getPayload());// 刷新缓冲区dataOutputStream.flush();}

1.3.4 遍历Session的哈希表,把断开的Socket对象的键值对进行删除

由于Socket都已经断开连接了,那么存储在内存中的Session也就没有存在的必要了.这个集合中存放的是一个连接中的change对应的Session,当连接断开之后,Channel也就不会再进行工作了,新的连接会创建新的Channel.

注意:我们在使用Map.entrySet进行遍历Map的时候,不要一遍遍历一遍进行删除,这样是不稳定的,我们遍历Map将需要进行移除的Session进行添加到待删除的链表中,最后遍历待删除的数据结构进行删除.


上述就是整个封装好的BrokerServer服务器.


下面呢,我对有关根据请求计算响应中订阅消息这一功能,再进行详细的阐述,这块比较难以理解,因为涉及到回调函数,大家可能不知道这个回调函数掉用的时机是哪里.

2. 处理订阅消息请求详解(补充)

 第二个红框部分是回调函数.

        只有消费者订阅的队列中有消息了,并且轮询的方式选中了这个消费者,才会获得消息的本体,此时线程池才会执行到这个回调方法,此时才拿到消息的本体,可以将消息的属性和本体写入到SubscribeReturn中,进而推送给消费者进行消费消息.如果没有消息给这个消费者,那么也不会进行断开连接,只要服务器不断开连接客户端一直在等待分配的消息进行消费.这一点希望,读者能够进一步的理解.等总结完客户端,那么我就会带着大家,再来理一遍这个订阅消息的这个思路.

3. 序列化/反序列化实现(补充)

要想能进行序列化和反序列化就必须对目标对象进行实现serializable接口.

1. 我们使用ByteArrayOutputStream和ObjectOutputStream进行将一个对象序列化为字节数组(输出的是字节用output)

2. 我们ByteArrayInputStream和ObjectInputStream将一个字节数组反序列化成一个对象(输入的是字节用Input)


结语

        至此,我们就彻底的完成了mqserver 的搭建,只剩下mqclient的搭建,我们在下一系列完成客户端的搭建,请持续关注,谢谢!!!

        完整的项目代码已上传Gitee,欢迎大家访问.👇👇👇

模拟实现消息队列https://gitee.com/yao-fa/advanced-java-ee/tree/master/My-mq

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

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

相关文章

学习pytorch

学习pytorch 1. 环境安装配置镜像源conda命令记录遇到的问题1. torch.cuda.is_available() False 1. 环境安装 B站小土堆视频 配置镜像源 conda config --show channels conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/mainhttp://www.m…

FastAPI 构建 API 高性能的 web 框架(一)

如果要部署一些大模型一般langchainfastapi&#xff0c;或者fastchat&#xff0c; 先大概了解一下fastapi,本篇主要就是贴几个实际例子。 官方文档地址&#xff1a; https://fastapi.tiangolo.com/zh/ 1 案例1:复旦MOSS大模型fastapi接口服务 来源&#xff1a;大语言模型工程…

【产品设计】消息通知系统设计

消息通知可以将内容实时送达用户手机页面&#xff0c;但是泛滥的消息通知会引起用户的反感&#xff0c;也违背了这个设计的初衷。 消息通知可以及时地将状态、内容的更新触达到用户&#xff0c;用户则可以根据收到的消息做后续判断。但是如果没有及时将重要消息触达到用户或者滥…

图像预处理——CV

目录 1.图像预处理 1.1 图像显示与存储原理 1.2 图像增强的目标 1.3 点运算&#xff1a;基于直方图的对比度增强 1.4 形态学处理 1.5 空间域处理&#xff1a;卷积 1.6 卷积的应用&#xff08;平滑、边缘检测、锐化等&#xff09; 1.7 频率域处理&#xff1a;傅里叶变换…

stm32 cubemx ps2无线(有线)手柄

文章目录 前言一、cubemx配置二、代码1.引入库bsp_hal_ps2.cbsp_hal_ps2.h 2.主函数 前言 本文讲解使用cubemx配置PS2手柄实现对手柄的按键和模拟值的读取。 很简单&#xff0c;库已经封装好了&#xff0c;直接就可以了。 文件 一、cubemx配置 这个很简单&#xff0c;不需要…

20个程序员接单平台分享

这题我会&#xff01;接单软件那么多&#xff0c;找到适合自己的最重要&#xff01; V2EX https://www.v2ex.com/ 先给一个“非正常选项”&#xff0c;v2ex上有一个“酷工作”板块&#xff0c;运气好的话可以在这里找到不错的单子&#xff0c;最重要的是带你开启新世界的大门…

企业电子招投标系统源码之电子招投标系统建设的重点和未来趋势 tbms

​ 功能模块&#xff1a; 待办消息&#xff0c;招标公告&#xff0c;中标公告&#xff0c;信息发布 描述&#xff1a; 全过程数字化采购管理&#xff0c;打造从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通供应商门户具备内外协同的能力&#xff0c;为…

C++ 多态性——虚函数

虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派生之后&#xff0c;在类族中就可以实现运行过程的多态。 根据类型兼容规则&#xff0c;可以使用派生类的对象代替基类的对象。如果基类类型的指针指向派生类对象&#xff0c;就可以通过这个指针来访问该对…

机械工业信息研究院:2023年中国生物制药行业报告(附下载)

关于报告的所有内容&#xff0c;公众【营销人星球】获取下载查看 核心观点 医药工业宏观情况分析 2021 年生物制药带动医药工业经 济指标大幅增长。根据统计&#xff0c;2021年规 模以上医药工业增加值同比增长 23.1%&#xff0c;增速较上年同期提升 17.2个百分点&#xff0…

深度学习环境安装依赖时常见错误解决

1.pydantic 安装pydantic时报以下错误&#xff1a; ImportError: cannot import name Annotated from pydantic.typing (C:\Users\duole\anaconda3\envs\vrh\lib\site-packages\pydantic\typing.py) 这个是版本错误&#xff0c;删除装好的版本&#xff0c;重新指定版本安装就…

nginx优化与防盗链

目录 优化&#xff1a; 1.隐藏版本号 2.nginx的日志分割&#xff1a; 3.nginx的页面压缩 4.nginx的图片压缩 5.连接超时&#xff1a; 6.nginx的并发设置&#xff1a; 1、cpu的核心数来进行设置 2、worker进程绑定到cpu中 7.nginx优化之 TIME_WAIT 防盗链 优化&#xf…

STDF - 基于 Svelte 和 Tailwind CSS 打造的移动 web UI 组件库,Svelte 生态里不可多得的优秀项目

Svelte 是一个新兴的前端框架&#xff0c;组件库不多&#xff0c;今天介绍一款 Svelte 移动端的组件库。 关于 STDF STDF 是一个移动端的 UI 组件库&#xff0c;主要用来开发移动端 web 应用。和我之前介绍的很多 Vue 组件库不一样&#xff0c;STDF 是基于近来新晋 js 框架 S…

明年,HarmonyOS不再兼容Android应用!

2023年华为开发者大会&#xff0c;不知道各位老铁们是否观看了&#xff0c;一个震撼的消息就是&#xff0c;首次公开了HarmonyOS NEXT的概念&#xff0c;简而言之就是&#xff0c;这是一款专为开发者打造的预览版操作系统&#xff0c;旨在提供"纯正鸿蒙操作系统"的体…

Flamingo

基于已有的图像模型和文本模型构建多模态模型。输入是图像、视频和文本&#xff0c;输出是文本。 Vision encoder来自预训练的NormalizerFree ResNet (NFNet)&#xff0c;之后经过图文对比损失学习。图片经过图像模型的输出是2D grid&#xff0c;视频按1FPS的频率采样后经过图…

【CSS3】CSS3 动画 ② ( 动画序列 | 使用 from 和 to 定义动画序列 | 定义多个动画节点 | 代码示例 )

文章目录 一、动画序列二、代码示例 - 使用 from 和 to 定义动画序列三、代码示例 - 定义多个动画节点 一、动画序列 定义动画时 , 需要设置动画序列 , 下面的 0% 和 100% 设置的是 动画 在 运行到某个 百分比节点时 的 标签元素样式状态 ; keyframes element-move { 0% { tr…

中国金融四十人论坛:2023年第二季度宏观政策报告(附下载)

关于报告的所有内容&#xff0c;公众【营销人星球】获取下载查看 核心观点 • 运行环境&#xff1a;外部环境方面&#xff0c;全球经济景气回落&#xff0c;会酸交作仍在收秀。内部环演方百&#xff0c;公共支出进一步旅爱&#xff0c;真交利本显考上开&#xff0c;社酸塔这创…

无涯教程-Perl - continue 语句函数

可以在 while 和 foreach 循环中使用continue语句。 continue - 语法 带有 while 循环的 continue 语句的语法如下- while(condition) {statement(s); } continue {statement(s); } 具有 foreach 循环的 continue 语句的语法如下- foreach $a (listA) {statement(s); } co…

36.利用解fgoalattain 有约束多元变量多目标规划问题求解(matlab程序)

1.简述 多目标规划的一种求解方法是加权系数法&#xff0c;即为每一个目标赋值一个权系数&#xff0c;把多目标模型转化为一个单目标模型。MATLAB的fgoalattain()函数可以用于求解多目标规划。 基本语法 fgoalattain()函数的用法&#xff1a; x fgoalattain(fun,x0,goal,weig…

MySQL存储引擎

一、存储引擎简介 存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表的&#xff0c;而不是基于库的&#xff0c;所以存储引擎也可被称为表类型。MySQL默认的存储引擎是InnoDB。 --查询建表语句 show create table 表名; --建表时指定存储引擎…

基于图像形态学处理的目标几何形状检测算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 .................................................... %二进制化图像 Images_bin imbinari…