模拟实现消息队列项目(系列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
相关文章:
![](https://img-blog.csdnimg.cn/6315b86774c64fac8f0cede7910f5ef6.png)
模拟实现消息队列项目(系列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. 处理订阅消息请求详解(补充) …...
![](https://img-blog.csdnimg.cn/b037a55c7de24729bd1858df8315b861.png)
vscode插件不能搜索安装
1 现象 vscode搜索自己的插件,报错: Error while fetching extensions. HXR failed2 原因 之前用vscode开发golang语言,设置了proxy代理,所以导致错误,删除即可 重启vscode 3 结果...
![](https://img-blog.csdnimg.cn/e7942ecb5f3c41ce8348e253f63ebe1a.png)
路由器工作原理(第二十九课)
路由器工作原理(第二十九课) 一图胜过千言 1) 路由:数据从一个网络到另外一个网络之间转发数据包的过程称为路由 2) 路由器:连接不同网络,实现不同网段之间的通信 3)路由表:路由器选择数据的传输路径的依据 原始的路由表 Destination/Mask Proto Pre Cost …...
![](https://www.ngui.cc/images/no-images.jpg)
linux log 日志
/* author: hjjdebug * date: 2023年 08月 08日 星期二 13:18:08 CST * descriptor: linux log 日志 * destinator: 搞清linux 下log 日志 * 下面代码编译通过即可运行 */ #include <stdio.h> #include <syslog.h> int main(void) { // 打开系统日志, 可…...
![](https://www.ngui.cc/images/no-images.jpg)
uniapp获取当前页面高度
设置动态高度:style"{height: pageHeightpx}" <view class"uni-content" :style"{height: pageHeightpx}" >... </view>获取当前页面高度: onLoad() {// 获取当前窗口高度this.pageHeight uni.getSystemInfoSync().wi…...
![](https://img-blog.csdnimg.cn/43b9e79c4ec74840819c7e90fd717e3c.png)
Java课题笔记~ Spring 集成 MyBatis
Spring 集成 MyBatis 将 MyBatis 与 Spring 进行整合,主要解决的问题就是将 SqlSessionFactory 对象交由 Spring 来管理。所以该整合,只需要将 SqlSessionFactory 的对象生成器SqlSessionFactoryBean 注册在 Spring 容器中,再将其注入给 Dao…...
![](https://www.ngui.cc/images/no-images.jpg)
分布式系统理论基础
文章目录 介绍目标 正文CAPConsistencyAvailabilityPartition tolerance BASEBasically AvailableSoft StateEventually Consistent ACIDatomicityconsistencyisolationdurability 参考文档 介绍 分布式系统面临的场景往往是众口难调,“这也要,那也要”…...
![](https://www.ngui.cc/images/no-images.jpg)
mfc 编辑框限制
DoDataExchange由框架调用,作用是交互并且验证对话框数据,主要由(DDX) 和 (DDV)宏实现。 永远不要直接调用这个函数,而是通过UpdateData(TRUE/FALSE)实现控件与变量之间值的传递。 当然你也可以不使用DoDataExchange而完成控件与变量之间值…...
![](https://img-blog.csdnimg.cn/c7939fedd203484e881e58970cfde6cd.png)
web基础与tomcat环境部署
一. 简述静态网页和动态网页的区别。 请求响应信息,发给客户端进行处理,由浏览器进行解析,显示的页面称为静态页面。处理文件类型如.html、jpg、.gif、.mp4、.swf、.avi、.wmv、.flv等 请求响应信息,发给事务端进行处理࿰…...
![](https://www.ngui.cc/images/no-images.jpg)
Go 变量
在Go中,有不同的变量类型,例如: int 存储整数(整数),例如123或-123float32 存储浮点数字,带小数,例如19.99或-19.99string - 存储文本,例如“ Hello World”。字符串值用…...
![](https://img-blog.csdnimg.cn/9d3779ae973a43d587c56ee650440dc5.png)
【雷达通信】非相干多视处理(CSA)(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
![](https://img-blog.csdnimg.cn/09a2c6bb9fd24f4d8a222a45bd7debc0.png)
73. 矩阵置零
题目链接:力扣 解题思路: 方法一:比较容易想到的方向,使用两个数组row和col保存有0的行或者列,然后将有0的那一行或那一列的所有元素都设置为0 AC代码 class Solution {public void setZeroes(int[][] matrix) {in…...
![](https://www.ngui.cc/images/no-images.jpg)
‘大数据技术与应用’和‘数据科学与大数据技术’有什么区别
一、侧重点不同 ‘大数据技术与应用’主要侧重于大数据的存储、处理和分析技术、包括数据挖掘、机器学习、数据仓库、分布式计算等方面的研究,旨在开发大数据相关的应用程序和系统,以满足商业和企业的需求。 ‘数据科学与大数据技术’则更加注重数据本…...
![](https://www.ngui.cc/images/no-images.jpg)
没有jsoup,rust怎么解析html呢?
在 Rust 中,你可以使用各种库来解析网页内容。一个常用的库是 reqwest ,它提供了一个简单的方式来发送 HTTP 请求并获取网页内容。另外,你可以使用 scraper 或 select 等库来解析 HTML 或 XML 格式的网页内容。 下面是一个使用 reqwest 和 sc…...
![](https://img-blog.csdnimg.cn/75cdf47cdb274013aca883c215af2ac0.png)
【C高级】Day4 shell脚本 排序
1. 整理思维导图 2. 写一个函数,获取用户的uid和gid并使用变量接收 #!/bin/bash function getid() {uidid -ugidid -g }getid echo "uid$uid" echo "gid$gid"3. 整理冒泡排序、选择排序和快速排序的代码 #include <myhead.h>void Inp…...
![](https://img-blog.csdnimg.cn/img_convert/26e0f2248b32339b3a4eefcd347a9559.png)
大模型开发(十六):从0到1构建一个高度自动化的AI项目开发流程(中)
全文共1w余字,预计阅读时间约40~60分钟 | 满满干货(附代码),建议收藏! 本文目标:通过LtM提示流程实现自动构建符合要求的函数,并通过实验逐步完整测试code_generate函数功能。 代码下载点这里 一、介绍 此篇文章为…...
![](https://www.ngui.cc/images/no-images.jpg)
【深入了解pytorch】PyTorch强化学习:强化学习的基本概念、马尔可夫决策过程(MDP)和常见的强化学习算法
【深入了解pytorch】PyTorch强化学习:强化学习的基本概念、马尔可夫决策过程(MDP)和常见的强化学习算法 PyTorch强化学习:介绍强化学习的基本概念、马尔可夫决策过程(MDP)和常见的强化学习算法引言强化学习的基本概念状态(State)动作(Action)奖励(Reward)策略(Pol…...
![](https://img-blog.csdnimg.cn/fc711854d5764fb69322ef527326e7b8.png)
尚硅谷张天禹Vue2+Vue3笔记(待续)
简介 什么是Vue? 一套用于构建用户界面的渐进式JavaScript框架。将数据转变成用户可看到的界面。 什么是渐进式? Vue可以自底向上逐层的应用 简单应用:只需一个轻量小巧的核心库 复杂应用:可以引入各式各样的Vue插件 Vue的特点是什么? 1.采…...
![](https://img-blog.csdnimg.cn/d4e6665cffbd4a93afdb5d22f70b9f4f.png)
深度学习(35)—— StarGAN(2)
深度学习(34)—— StarGAN(2) 完整项目在这里:欢迎造访 文章目录 深度学习(34)—— StarGAN(2)1. build model(1)generator(2&#…...
![](https://img-blog.csdnimg.cn/img_convert/1cab64cc4c49a76eee6bc448c0423ed5.png)
连续四年入选!三项荣耀!博云科技强势上榜Gartner ICT技术成熟度曲线
日,全球知名咨询公司Gartner发布了2023年度的《中国ICT技术成熟度曲线》(《Hype Cycle for ICT in China, 2023》,以下简称“报告”)。令人瞩目的是,博云科技在报告中荣获三项殊荣,入选云原生计算ÿ…...
![](https://img-blog.csdnimg.cn/b171e53666e24009a888910a799db4a1.png#pic_center)
Docker实战-操作Docker容器实战(一)
导语 在之前的分享中,我们介绍了关于如何去操作Docker镜像,下面我们来看看如何去操作容器。 简单来讲,容器是镜像运行的一个实例,与镜像不同的是镜像只能作为一个静态文件进行读取,而容器是可以在运行时进行写入操…...
![](https://img-blog.csdnimg.cn/d3bf084a0add40198fd8efd7fd3c5c84.png)
c#设计模式-行为型模式 之 观察者模式
定义: 又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者 对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自 …...
![](https://img-blog.csdnimg.cn/31adc79e643a4dea87ef7ede8492399c.png)
开窗积累之学习更新版
1. 开窗使用1之 count range between current row and current row 将相同排序字段的值进行函数计算 selectsku_id,substr(create_date,1,7) date_month,order_id,create_date,sku_num*price,sum(sku_num*price) over (partition by sku_id order by substr(create_date,1,7)…...
![](https://img-blog.csdnimg.cn/c41c23e7b1574a658ac2b9b4c697a100.png#pic_center)
ffplay简介
本文为相关课程的学习记录,相关分析均来源于课程的讲解,主要学习音视频相关的操作,对字幕的处理不做分析 ffplay播放器的意义 ffplay.c是FFmpeg源码⾃带的播放器,调⽤FFmpeg和SDL API实现⼀个⾮常有⽤的播放器。 ffplay实现了播…...
![](https://www.ngui.cc/images/no-images.jpg)
mysql之limit语句详解
一、介绍 LIMIT是MySQL内置函数,其作用是用于限制查询结果的条数。 二、使用 1. 语法格式 LIMIT [位置偏移量,] 行数 其中,中括号里面的参数是可选参数,位置偏移量是指MySQL查询分析器要从哪一行开始显示,索引值从0开始ÿ…...
![](https://www.ngui.cc/images/no-images.jpg)
4.while循环
1、while语句的语法结构如下: while语句可以在条件表达式为真的前提下,循环执行指定的一段代码,直到表达式不为真时结束循环。 1.1while语法结构 while(条件表达式){// 循环体} 执行思路: 1、执行思路 当条件表达式结果为tru…...
![](https://img-blog.csdnimg.cn/c73d0b0c92c94b4ebee235112127ed68.jpeg#pic_center)
【雕爷学编程】 MicroPython动手做(35)——体验小游戏2
知识点:什么是掌控板? 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片,支持WiFi和蓝牙双模通信,可作为物联网节点,实现物联网应用。同时掌控板上集成了OLED…...
![](https://www.ngui.cc/images/no-images.jpg)
mouseover 和 mouseenter
mouseover 和 mouseenter 事件是 JavaScript 中常用的两个鼠标事件,它们有一些区别: 触发条件: mouseover 事件在鼠标指针从元素外部进入元素内部时触发,包括子元素。换句话说,只要鼠标进入元素或其子元素,就会触发 mo…...
![](https://img-blog.csdnimg.cn/72eb640e48414ad1b229141284bc3d43.png)
[JavaScript游戏开发] 绘制Q版地图、键盘上下左右地图场景切换
系列文章目录 第一章 2D二维地图绘制、人物移动、障碍检测 第二章 跟随人物二维动态地图绘制、自动寻径、小地图显示(人物红点显示) 第三章 绘制冰宫宝藏地图、人物鼠标点击移动、障碍检测 第四章 绘制Q版地图、键盘上下左右地图场景切换 文章目录 系列文章目录前言一、本章节…...
![](https://img-blog.csdnimg.cn/b58aa9fd984b4e8cb43438529e2c20ff.png)
CI/CD持续集成持续发布(jenkins)
1.背景 在实际开发中,我们经常要一边开发一边测试,当然这里说的测试并不是程序员对自己代码的单元测试,而是同组程序员将代码提交后,由测试人员测试; 或者前后端分离后,经常会修改接口,然后重新…...
![](https://img-blog.csdnimg.cn/20190328202016365.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3h1YnVodWk=,size_16,color_FFFFFF,t_70)
什么网站做水果蔬菜批发/网络推广营销网
yolo(You Only Look Once)是一个高速的物体识别算法,在这里我不详细赘述论文的内容,记录一下我在环境中进行物体识别的整个过程。 YOLOv1的原文:https://arxiv.org/pdf/1506.02640.pdf YOLOv2原文:https://…...
![](/images/no-images.jpg)
网站优化电话/竞价被恶意点击怎么办
https://blog.csdn.net/lu2631605872/article/details/78226486...
![](https://img-blog.csdnimg.cn/20200208113704315.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxOTg1Mjkz,size_16,color_FFFFFF,t_70)
公司做了网站怎么做推广/查权重工具
selenium安装 winR,输入cmd,进入命令行,然后输入 pip install seleniumwebdriver的配置 这里我们安装chrome的webdriver使用 首先查看你的谷歌浏览器的版本chrome://version/ 例如: 然后参照版本,我们下载对应的谷…...
![](/images/no-images.jpg)
免费建网站域名/超链接友情外链查询
简介SQL Server 2012中在Order By子句之后新增了OFFSET和FETCH子句来限制输出的行数从而达到了分页效果。相比较SQL Server 2005/2008的ROW_Number函数而言,使用OFFSET和FETCH不仅仅是从语法角度更加简单,并且拥有了更优的性能(看到很多人下过这个结论&a…...
wordpress 标题分隔符/免费数据分析网站
很多运营人都会遇到这样的问题:当去到新的公司或者要运营一个新的项目的时候,应该如何快速熟悉项目,建议自己的运营体系呢?针对这种困惑,笔者将给出一些方法与建议。 全文共 2748 字,阅读需要 6 分钟 ——…...
![](https://img-blog.csdnimg.cn/20201109164813709.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2h3eDgwMjc0Ng==,size_16,color_FFFFFF,t_70#pic_center)
简洁汽车配件网站模板/百度知道网页版登录入口
什么是冒泡排序? 是通过挨个的比较,将最大(或最小)数字层层交换至所有数据的开头,每冒一次,数据头部往后移一位,经过N次冒泡之后,得到最终排序!!!…...