UDP+有穷自动状态机构造网络指令系统

news2024/10/5 13:10:35

UDP+有穷自动状态机构造网络指令系统

项目背景

某展厅的小项目,使用Unity制作了一个视频播放器,作为受控端,需要接收解说员手中的“PAD”或“触控屏电脑”等设备发来的控制指令。要求指令系统满足以下功能:

能够随意切换要播放的视频(更换视频URL)
能够控制视频的播放进度(快进x秒、快退x秒、定位于x秒)
能够控制播放器的循环状态(LOOP?)
能够控制视频的播放音量(增加x,减少x,设置为x)
能够控制视频播放器的各类参数,如播放速率、显示模式。
能够查询播放器当前的状态(是否循环,是否正在播放,当期播放速率、当前屏幕拉伸模式)
能够查询当前播放的视频的各类参数,如总时长,当前帧数。
播放器在播放完成等各种情况发生时,能够向注册了事件通知的客户端发送事件通知。

指令系统

如何设计这样的指令系统呢?最简单的方法,就是使用固定形式的定长指令集:建个表,把所有需要用到的指令列出来,每个指令给一个特定的符号,比如,P字符就表示暂停,Y字符表示继续播,再或者0x01表示暂停,0x02表示播放等等。。这种情况下,只要发送这些特定的字符就可以了,好处是指令可以很短,效率可以很高。但是这样会有一些局限,比如你很难携带可变的参数,比如你想将视频的时间定位为1分20秒,那么就得携带1分20秒这个参数过去,这种情况下就会比较麻烦,当然,你可以规定每条指令n个字节,比如第一个字节是指令符,第二个字节到最后一个字节表示参数,这样的话,用一个结构体就可以搞定了。但是每条指令都是定长的,这不容易被扩展,一些不需要参数的指令也存在冗余。

那么,我们就需要一套这样的指令系统:它应该非常容易被解析,指令构造也需要很简洁,能够携带也可以不携带指令的参数,很容易被扩充。

基于上面的考虑,我们尝试构使用字符串造这样一个指令系统:

一上来进行代码解析,理论说明,很容易让人头疼,那先来几个简单的例子:

[TIME+1.5] 这条指令表示,将当前正在播放的视频快进1.5秒。
[TIME-1.5] 这条指令表示,将当前正在播放的视频快退1.5秒。
[TIME=1.5] 这条指令表示,将当前正在播放的视频定位到1.5秒处。

上面的指令非常容易理解,那么抽线并归纳一下:

每一条指令,由指令前缀、指令体、操作符、参数、指令后缀等元素构成。

上述例子中,指令前缀就是“[”,指令后缀就是“]”,指令体就是“SEEK”,操作符就是“+”、“-”、“=”。
为什么要这么搞呢?相信大家都学过《编译原理》,这可是计算机专业的必修课。之所以将指令分成这几个部分,其实就是定义指令系统的词法规则,这样就可以利用有穷自动状态机,很容易的去解析它。

  • 指令前缀和后缀
    其实就是指令的分界符,用于标记一条指令的开始和结束,这类似与C语言中字符串的结束标记“\0”符号,再好比CSV文件中的“,”号,用来分割不同的列。上面例子中,我们分别用“[”和“]”来作为前缀和后缀。由此带来的第一个问题是,我们的指令的其他部分,就不能出现这两个字符了,包括指令体、运算符、还有参数部分,当然,要想解决这个问题也是可以的,那就再引入“转义符”这个概念,比如将连续两个相同的“]”符看做不是指令的结尾,而是本身的符号。为了简单起见,这里不予考虑。因为除了把他们当做分隔符,我们的播放器的指令部分本身,基本上用不到“[”和“]”符号,或者我们约定,指令中不可以出现这两个符号。

  • 指令体
    就是发了什么控制指令,比如SEEK,表示定位;LOOP, 表示循环状态等等。。。

  • 操作符
    例子中,我们有三种操作符,分别是:

    • = 表示设置为绝对的值
    • + 表示增加相对的值
    • - 表示减少相对的值
  • 参数
    表示指令携带的参数数值,根据指令不同,可以是任何数据类型。参数可以被省略,如果参数被省略,则相当于发送了约定的默认值。

更省略的约定:
指令携带的参数如果是默认值,则可以省略。另外,操作符如果是“=”,并且参数也使用默认的话,可以连操作符一同省略,例如:[FRAME=0]指令中,由于0是缺省值,因此可以省略掉,因此可以写成[FRAME=],同时,由于操作符是“=”,则指令可以进一步省略为:[FRAME] 。即:[FRAME=0]、[FRAME=][FRAME]三者是等价的。

基于此项目需求的指令列表举例

[TIME=x] 表示,将视频的定位到x秒处。x可以是整数或小数,若省略,默认为0
[TIME+x] 表示,将视频的定位增加x秒,即快进x秒,x可以是整数或小数,若省略,默认为0.1
[TIME-x] 表示,将视频的定位减少x秒,即快退x秒。x可以是整数或小数,若省略,默认为0.1
[FRAME=x] 表示,将视频定位到第x帧。x必须为整数,若省略,默认为0
[FRAME+x] 表示,快进x帧。x必须为整数,若省略,默认为10
[FRAME-x] 表示,快退x帧。x必须为整数,若省略,默认为10
[VOLUME=x] 表示,将音量设置为x。x为0-1之间的小数。若省略,默认为1
[VOLUME+x] 表示,将音量设置增大x。x为0-1之间的小数。若省略,默认为0.1
[VOLUME-x] 表示,将音量设置减小x。x为0-1之间的小数。若省略,默认为0.1
[SPEED=x] 表示,设置播放器的播放速率。x为大于0的小数,默认为1。
[SPEED+x] 表示,增加播放器的播放速率x。默认为0.1。
[SPEED-x] 表示,降低播放器的播放速率x。默认为0.1。
 
 *Time和Frame都可用来定位视频,不同的是Time以时间(秒)为单位,Frame以帧序号为单位。

有一些指令,只有“=”操作符,而没有“+”、“-”操作符,比如:

[URL=x] 设置要播放的视频。x为字符串,如:[URL=d:/movie/demo.mp4]
[LOOP=x] 表示,设置为循环模式,x只能为TRUE或FALSE,忽略大小写。
[EVENT=x] 表示,是否注册事件通知。x为TRUE或FALSE。
[DISPLAY=x] 表示,设置显示模式为x。x为STRETCH、CROP、FIT三者之一,为当视频宽高比和屏幕宽高比不一致时的处理方式:

  • Stretch 视频拉伸为全屏
  • Crop 裁剪视频以适应屏幕
  • Fit 根据屏幕自动适配(留有黑边)。

所有查询参数的指令,也是只有“=”操作符:

[GET=LOOP] 获取当前是否为循环模式。
[GET=URL] 获取当前播放的视频的地址。
[GET=COUNT] 获取当前播放的视频总帧数。
[GET=FRAME] 获取当前播放的帧序列号(第几帧)。
[GET=LENGTH] 获取当前播放的视频的总时长(秒)。
[GET=TIME] 获取当前播放的时间点]
[GET=STATE] 获取当前播放器的状态,返回播放中(PLAY),暂停中(PAUSE),停止中(STOP)三者之一。
[GET=DISPLAY] 获取当前显示模式,返回拉伸(STRETCH)、裁切(CROP)、自动适配(FIT)三者之一。
[GET=EVENT] 获取当前是否注册了事件通知,返回TRUE或FALSE。

当然,还有一些指令是不需要任何运算符和参数的,比如:

[PAUSE] 暂停。
[PLAY] 播放。
[STOP] 停止播放。
[REPLAY] 等价于:[FRAME][PLAY]

当播放事件发生时,播放器会主动向注册了事件通知的所有远端发送事件通知:

[EVENT=LOOPED] 播放完成并开始循环播放。
[EVENT=PREPARE] 播放器准备完成。

如何实现

下面就是重要的实现部分了。

有穷自动状态机构建:

指令解析有穷自动状态机
上图标明了利用有穷自动状态机构建指令解析器的状态迁移图,如果能看明白,那就很容易理解了。代码写起来也很简单:

// 定义三种接受状态
private enum ReceiveState
{
	Start,
	Command,
	Params
}

// 定义指令数据,指令,操作符,参数
private struct CommandNode
{
    public string cmd;
    public char opr;
    public string par;
}

// 线程安全的指令队列
private readonly ConcurrentQueue<CommandNode> commands = new ConcurrentQueue<CommandNode>();

// 有穷自动状态机解析收到的串
private void OnReceiveString(string cmd, IPEndPoint remote)
{
	// 获取远程接收上下文相关的接收状态、指令缓冲、操作符、参数缓冲。
	ReceiveState state = GetRemoteState(remote);
	StringBuilder currPar = GetRemoteCommandBuffer(remote);
    char currOp = GetRemoteOperator(remote);
    StringBuilder currPar = GetRemoteParamsBuffer(remote);

	// 遍历收到的串。此处已考虑粘包、拆包情况。
    foreach (var ch in cmd)
    {
        switch (state)
        {
            case ReceiveState.Start:		// 开始状态
                if (ch == '[')				// 如果是前缀,清空指令,迁移到接收指令状态
                {
                    currCmd.Clear();
                    state = ReceiveState.Command;
                }
                break;

            case ReceiveState.Command:		// 接收指令状态
                switch (ch)
                {
                    case ']':				// 如果遇到后缀符,表示收到无操作符,无参数的指令,压入队列。
                    {
                        if (currCmd.Length > 0)
                        {
                            commands.Enqueue(new CommandNode()
                            {
                                cmd = currCmd.ToString(),
                                opr = '\0',
                                par = null
                            });
                        }
                        state = ReceiveState.Start;  // 迁移状态,重新开始解析
                        break;
                    }
                    case '=':	 // 如果遇到=、+、-字符,记录作为操作符,并迁移到参数接收状态。
                    case '+':
                    case '-':
                        currOp = ch;
                        currPar.Clear();
                        state = ReceiveState.Params;
                        break;
                    default:	// 遇到其他字符
                    {
                    	// 如果长度未超限制,并且字母或数字,记录指令
                        if (currCmd.Length < 1024 && char.IsLetterOrDigit(ch))
                            currCmd.Append(ch);
                        else
                            state = ReceiveState.Start;	// 非法字符或长度超限,迁移状态,重新开始解析
                        break;
                    }
                }
                break;

            case ReceiveState.Params:			// 接收参数状态
                if (ch == ']')					// 遇到后缀,指令结束。将指令、操作符、参数压入队列。
                {
                    if (currCmd.Length > 0)
                    {
                        commands.Enqueue(new CommandNode()
                        {
                            cmd = currCmd.ToString(),
                            opr = currOp,
                            par = currPar.ToString()
                        });
                        state = ReceiveState.Start;
                    }
                }
                else if ( currPar.Length < 1024 )
                    currPar.Append(ch);		// 参数未超长度限制,记录参数
                else
                    state = ReceiveState.Start; // 长度超限,迁移状态,重新开始解析
                break;
        }
    }
}

// 定义指令处理系统
public delegate void OnCommandHander(string cmd, char op, string pars);
public event OnCommandHander OnCommand;

// MonoBehaviour 每帧处理收到的指令,之所以不在接收函数中处理,是因为,通信采用异步方式,处理接受的线程并不是主线程,
// 但在unity中,使用主线程来更新游戏物体引擎相关数据。因此采用指令队列的方式解决。
private void Update()
{
    if (commands.TryDequeue(out CommandNode node))
    {
        OnCommand?.Invoke(node.cmd, node.opr, node.par);
    }
}

有了上面的指令解析系统,就可以定义播放的指令解析行为,例如:

private void Awake()
{
	network.OnCommand += OnReceiveCommand;
}

// 接收到了指令处理。指令为cmd,操作符为op,无操作符则为'\0', pars为收到的参数,无则为null
private void OnReceiveCommand(string cmd, char op, string pars)
{
	switch( cmd )
	{
		// 处理TIME设置 的例子。
		case "TIME":
			float time = 0;
			if( ! string.IsNullOrWhiteSpace(pars))
			{
				if(!float.TryParser( pars, out time ))
					break;
			}
			switch( op )
			{
				case '\0':
				case '=':
					player.SetTime( time );
					break;

				case '+':
					player.SetTime( player.GetTime() + time );
					break;
				
				case '-':
					player.SetTime( player.GetTime() - time );
					break;
			}
			break;
	}
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

剑指Offer 第17天 Top K问题 优先级队列解决数据流中位数

目录 剑指 Offer 40. 最小的k个数 剑指 Offer 41. 数据流中的中位数 剑指 Offer 40. 最小的k个数 输入整数数组 arr &#xff0c;找出其中最小的 k 个数。例如&#xff0c;输入4、5、1、6、2、7、3、8这8个数字&#xff0c;则最小的4个数字是1、2、3、4。 示例 1&#xff1a; …

图像处理中的微分算子

摘要 微分算子在图像处理中的作用主要是用在图像的边缘检测&#xff0c;而图像边缘检测必须满足两个条件&#xff1a;一能有效的抑制噪声&#xff0c;二能必须尽量精确定位边缘位置。现在常用的微分算子主要有&#xff1a;Sobel算子&#xff0c;Robert算子&#xff0c;Prewitt…

【数据结构-JAVA】堆和优先级队列

前面介绍过队列&#xff0c;队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能带有优先级&#xff0c;一般出队 列时&#xff0c;可能需要优先级高的元素先出队列&#xff0c;该中场景下&#xff0c;使用队列显然不合适&#xff0c;比如&…

Hugo博客教程(一)

秋风阁——北溪入江流&#xff1a;https://focus-wind.com/ 秋风阁——计算机视觉实验&#xff1a;边缘提取与特征检测 文章目录Hugo博客教程&#xff08;一&#xff09;博客静态博客静态博客的优缺点常见的静态博客HexoHugo动态博客动态博客的优缺点常见的动态博客WordPressTy…

sql进阶教程

sql进阶教程第一章、神奇的sql1.1 CASE 表达式将已有编号方式转换为新的方式并统计用一条 SQL 语句进行不同条件的统计用 CHECK 约束定义多个列的条件关系在 UPDATE 语句里进行条件分支表之间的数据匹配在 CASE 表达式中使用聚合函数本节要点1.2 自连接的用法面向集合语言SQL可…

shiro(二):springboot整合shiro

1. 整合思路 2. 加入jsp相关配置方便测试 2.1 加入依赖&#xff1a; <!--引入JSP解析依赖--> <dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId></dependency> <dependenc…

Golang——包

1、GOPATH 项目代码肯定要需要保存在一个目录中&#xff0c;但是如果目录不统一&#xff0c;每个人有一套自己的目录结构&#xff0c;读取配置文件的位置不统一&#xff0c;输出的二进制运行文件也不统一&#xff0c;这样会导致开发的标准不统一。 所以&#xff0c;产生环境变量…

QEMU安装Windows 11的完整过程

零、环境介绍 宿主机&#xff1a; Ubuntu 22.04.1 LTS Windows 11镜像&#xff1a; Win11_Chinese(Simplified)_x64v1 QEMU版本&#xff1a; qemu-img version 7.1.0 Copyright (c) 2003-2022 Fabrice Bellard and the QEMU Project developers 一、安装过程 1. 创建…

随机过程与排队论(二)

随机试验 如果一个试验E满足下列条件&#xff0c;就称此试验为随机试验&#xff1a; 在相同条件下可以重复进行。每次试验的结果不止一个&#xff0c;并且能事先明确知道试验的所有结果。一次试验结束之前&#xff0c;不能确定哪一个结果会出现。 样本空间、随机事件体 随机…

估值85亿美元!智驾前装赛道又添新“巨头”,已开始量产交付

随着智能汽车技术与供应链的发展&#xff0c;可以看到很多高端汽车也逐渐开始采用过去在L4上才使用的传感器&#xff0c;例如激光雷达。同时&#xff0c;多传感器融合技术也已进入规模化量产阶段&#xff0c;为L2在乘用车上的大规模应用打开了一个新窗口。 而作为L4领域的资深…

Leetcode力扣秋招刷题路-0124

从0开始的秋招刷题路&#xff0c;记录下所刷每道题的题解&#xff0c;帮助自己回顾总结 124. 二叉树中的最大路径和&#xff08;Hard&#xff09; 路径 被定义为一条从树中任意节点出发&#xff0c;沿父节点-子节点连接&#xff0c;达到任意节点的序列。同一个节点在一条路径序…

智能驾驶 车牌检测和识别(五)《C++实现车牌检测和识别(可实时车牌识别)》

智能驾驶 车牌检测和识别&#xff08;五&#xff09;《C实现车牌检测和识别&#xff08;可实时车牌识别&#xff09;》 目录 智能驾驶 车牌检测和识别&#xff08;五&#xff09;《C实现车牌检测和识别&#xff08;可实时车牌识别&#xff09;》 1. 前言 2. 车牌检测模型&a…

栈与队列——滑动窗口最大值

力扣题目链接 239. 滑动窗口最大值 给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 示例 1&#xff1a; 输入&#xff1a…

CATIA等设计类软件实时渲染流化解决方案

CATIA软件在汽车、航空航天、船舶制造、厂房设计(尤其是钢构厂房)、建筑、通用机械制造等领域&#xff0c;提供3D设计和模拟解决方案。可以帮助企业在产品研发领域缩短开发周期&#xff0c;因此使用非常广泛。但随着技术和设备的发展&#xff0c;CATIA模型不仅仅需要在电脑上进…

活体识别5:论文笔记之FeatherNets

说明 这篇文章是这次比赛的第三名&#xff1a;ChaLearn Face Anti-spoofing Attack Detection ChallengeCVPR2019&#xff0c;此次比赛项目是人脸防欺诈攻击检测。 论文标题&#xff1a;《FeatherNets: Convolutional Neural Networks as Light as Feather for Face Anti-spo…

中科蓝讯读取CSV文件中地址来指定地址段烧录

优势&#xff1a;可不需要通过小牛测控来写码&#xff0c;在烧录的时候直接进行读取文件来写码&#xff0c;可节省小牛测控写码并复位耳机的时间 功能&#xff1a;通过读取外置的 excel 表格里面的配置项&#xff0c;实现对 setting 文件里面的特定配置项的值 进行设置&#…

详解 k8s 中的 RBAC

Kubernetes 主要通过 API Server 对外提供服务&#xff0c;对于这样的系统来说&#xff0c;如果不加以安全限制&#xff0c;那么可能导致请求被滥用&#xff0c;甚至导致整个集群崩塌。 Kubernetes 中提供了良好的多租户认证管理机制&#xff0c;RBAC正式其中重要的一个&#…

Linux驱动开发基础__异步通知

目录 1 适用场景 2 使用流程 3 驱动编程 4 应用编程 5 代码 5.1 gpio_key_drv.c 5.2 button_test.c 5.3 Makefile 6 异步通知机制内核代码详解 1 适用场景 在前面引入中断时&#xff0c;我们曾经举过一个例子&#xff1a; 妈妈怎么知道卧室里小孩醒了&#xff1f; 异…

【深度学习】U-Net和FCN具体分析

FCN 相比于普通分类网络而言:FCN把后面几个全连接都换成卷积,这样就可以获得一张2维的feature map,后接softmax获得每个像素点的分类信息,从而解决了像素级分割问题。 整个FCN网络基本原理如图5**(只是原理示意图)**: image经过多个conv和+一个max pooling变为pool1 f…

SpringCloud_Alibaba Sentinel实现熔断与限流

目录一、Sentinel介绍1.官网2.是什么3.能干嘛4.去哪下5.怎么玩二、安装Sentinel控制台1.sentinel组件由2部分组成2.安装步骤三、初始化演示工程1.启动Nacos8848成功2.案例3.启动Sentinel80804.启动微服务84015.启动8401微服务后查看sentienl控制台四、流控规则1.基本介绍2.流控…