UDP+有穷自动状态机构造网络指令系统
创始人
2024-05-22 04:11:01
0

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 commands = new ConcurrentQueue();// 有穷自动状态机解析收到的串
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);elsestate = 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);		// 参数未超长度限制,记录参数elsestate = 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;}
}

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...