嵌入式开发WebSocket 服务端与客户端的通信

开启 WebSocket 服务

WebSocket 服务是网页程序、安卓 App、微信小程序等获得数据和服务的接口,是基于TCP 的一种新的网络协议,它实现了浏览器与服务器全双工通信。通过使用 WebSocket,很方便地实现与网页程序、安卓 App 和微信小程序的数据交互。WebSocket 服务端可以与多个 WebSocket 客户端建立通信,服务端可以向所有与之建立 WebSocket 通信的客户端发送数据。WebSocket 服务端工具类WsServices,用于创建 WebSocket 服务端对象。

WsServices 类主要方法与事件
函数名 类型/功能
start 方法:开启 WebSocket 服务端
close 方法:关闭 WebSocket 服务端
send 方法: 向 WebSocket 客户端发送数据
OnOpen 事件: 当 WebSocket 服务端建立网络连接时触发该事件
OnClose 事件: 当 WebSocket 服务端被关闭时触发该事件
OnMessage 事件 :当 WebSocket 服务端接收到客户端的数据时触发该事件
OnError 事件:当 WebSocket 服务端网络连接出现错误时触发该事件。
UeReSend 委托 :该委托的事件 ueReSendEvent 在 OnMessage 事件中调用

在实时数据的窗体加载过程中,为 WsServices 类的委托事件 ueReSendEvent 注册执行函数 UE_reSend

//(6)开启 WebSocket 服务
//(6.1.1)为 WsServices 服务注册回发处理事件并初始化其 frmMain
WsServices.ueReSendEvent += new WsServices.UeReSend(UE_reSend);
WsServices.frmMain = this.frmMain;

WebSocket 客户端的回发数据到达 CS-Monitor 将触发该委托事件,引起 UE_reSend 函数的调用,UE_reSend 的功能为将要回发的数据加入到名为 reSendData 的 FrameData 字典里,定时器 timer_FrmRealtimeData_1S 会在允许时将 reSendData 数据回发至终端。UE_reSend的功能代码如下所示。

public void UE_reSend(string imsi,FrameData frame)
{
 //如新数据的 IMSI 在字典里已存在旧数据,则将旧数据删除
	if (reSendData.ContainsKey(imsi))
	{
 		reSendData.Remove(imsi);
 		reSendData.Add(imsi, frame);
	}
 }

在实时数据窗体加载函数的最后,创建 WebSocket 服务端对象 wssv,设置该服务端的地 址 与 端 口 号 为 frmMain.g_wsTarget 的 内 容 , 设 置 该 服 务 器 的 二 级 目 录 地 址 为frmMain.g_wsDirection,最后启动 WebSocket 服务端。

//(6.1.2)开启 ws 服务,并使用部分设置的地址、端口与目录
wssv = new WebSocketServer(frmMain.g_wsTarget);
wssv.AddWebSocketService<WsServices>(frmMain.g_wsDir);
wssv.Start();

上述过程实现了 WebSocket 通信的启动以及委托事件的注册

WebSocket 服务端与客户端的通信

WebSocket 服务端在开启后将保持对 WebSocket 客户端数据的监听,当有客户端数据或请求到来,将触发 WebSocket 服务端的 OnMessage 事件,在该事件的执行函数中会处理客户端的请求与数据。如果客户端应用向服务端请求一条数据,服务端会向客户端回发包含该数据的数据包,随后客户端在其 OnMessage 事件中将该获取数据包并进行应用;如果客户端发来的是它修改过的回发数据,则服务端会将该数据转发到这条数据对应的终端。

CS-Monitor 程序提供的 WebSocket 服务主要有:(1)实时数据通知;(2)实时/历史数 据发送;(3)实时/历史数据回发。WebSocket 通信交互的数据格式采用 JSON 格式,目前在监测程序中应用了以下两种 JSON 数据格式:(1)格式 1:command(命令,string 类型)+source(发送方,string 类型)+password(密码,string 类型)+value(内容,string 类型); (2)格式 2:command(命令,string 类型)+source(发送方,string 类型)+dest(接收方,string 类型)+password(密码,string 类型)+currentRow(当前帧,int 类型)+totalRows(总帧数,int 类型) +data(数据,List类型)。
黑体加粗字段为键,括号中内容为值的内容与值的类型。
数据格式 1 通常用于服务端推送实时数据到来通知或客户端请求历史数据,故此格式不需要传递数据,所以不包含 data 字段;数据格式 2 通常用于服务端发送实时/历史数据或客户端回发实时/历史数据,故包含 data 字段用来传递实时或历史数据内容。在 JSON 数据格式中,command 字段决定了应用的场景。下表列举了服务端与客户端 WebSocket 通信中的所使用的命令 command 与其对应数据格式和应用场景。嵌入式开发WebSocket 服务端与客户端的通信_第1张图片

WebSocket 服务端与客户端通信流程

嵌入式开发WebSocket 服务端与客户端的通信_第2张图片
1) 服务端推送通知:recv
在服务端 CS-Monitor 处,终端实时数据的到来触发了 HCICom 的接收事件,执行函数IoT_recv,该函数位于 FrmRealtimeData.cs 中。在 IoT_recv 执行过程中,会先组建 JSON数据包,令 JSON 包 value 的内容等于当前数据库最后一行数据的行号,也就是最新一行数据的行号。最后,通过 WebSocket 以广播的方式把实时数据到来的通知推送出去。具体执行代码如下所示。

//(2.6)通知所有连接 WebSocket 服务的客户端有数据到来
//(2.6.1)组成要发送的 Json 数据
JsonCommand jsonCommand = new JsonCommand();
jsonCommand.command = "recv";
jsonCommand.source = imsiRecv;
jsonCommand.password = "";
jsonCommand.value = rowNum.ToString(); //Json 数据 value 等于数据库最后一条数据行号
JavaScriptSerializer javaScriptSerializer = new JavaScriptSerializer();
javaScriptSerializer.MaxJsonLength = Int32.MaxValue; //取得最大数值
string dataString = javaScriptSerializer.Serialize(jsonCommand); 
//(2.6.2)发送数据至连接 ws 服务的客户端
WebSocketServiceManager bb2 = wssv.WebSocketServices;
bb2.Broadcast(dataString);

如果设备号为 460113003130916 的终端设备发来了实时数据并被存入数据库中的第 491行,则服务端广播的 JSON 包内容应如下所示。{"command":"recv","source":"460113003130916","password":"","value":"497"}

2) 客户端请求数据:ask
在服务端 CS-Monitor 的 WebSocket 服务成功启动之后,客户端 CS-Client 可以建立起与WebSocket 服务端的多对一连接。在与服务端建立起连接之后,如果服务端广播了一条实时数据到来的通知,客户端 CS-Client 的 WebSocket 数据接收事件 OnMessage 可以获取到服务端广播的内容。
CS-Client 的 OnMessage 事 件 执 行 函 数 注 册 发 生 在 实 时 数 据 窗 体 的 加 载 函 数FrmRealtime_Load 中。对于服务端发来的数据,首先要判断其命令 command 格式的合法性,如果该数据命令是合法的,则需要判断该数据对应的终端设备是否被自己侦听,如不是则舍弃此数据,如是则获取这条 JSON 数据。如果该数据的命令 command 内容为“recv”,则组建 JSON 数据,新 JSON 数据的 command 字段内容为“ask”,该命令表示请求一条数据库数据,新 JSON 数据的 value 字段为服务端发来 JSON 数据的 value 值,表示请求数据为服务端推送的那条实时数据。JSON 数据组建完毕后,使用 WebSocket 的 send 方法发送这条JSON 数据。具体执行代码如下。

//(4.3)WebSocket 接收消息的事件
ws.OnMessage += (sender2, e2) =>
{ 
 	…….//此处省略部分代码
	//(4.3.2)判断 JSON 命令格式,进行相应操作 
	switch (jason.command)
	{
		//接收到新数据
		case "recv":
			frmMain.NewestCount = int.Parse(jason.value);
			//通过 IMSI 号甄别信号是否属于被自己侦听的设备
		if (frmMain.g_IMSI.Contains(jason.source))
		{
 			try
 			{
 				rowCount = Convert.ToInt32(jason.value);
 				//用 Jason.value 的值向侦听服务端请求数据
				 if (rowCount != 0)
 					this.Invoke(new EventHandler(delegate
 					{
 						JsonCommand askJason = new JsonCommand();
 						askJason.command = "ask";
 						askJason.source = "CS-Client";
 						askJason.password = "";
 						askJason.value = jason.value;
 						//Jason 字符串转为 Jason 对象
 						var srAsk = new JavaScriptSerializer(); 
 						//实例化一个 js 处理对象
						string dataString = srAsk.Serialize(askJason);
						ws.Send(dataString);
 						frmMain.cmd = 0; //当前正在请求一条实时数据 
 					}
 					));
 			}
 catch { }
} 
break;
 ……//此处省略部分代码
}

如果 CS-Client 接收到 1)步骤发来的实时数据推送,则回发向服务端的 JSON 数据内容应如下所示。
{"command":"ask","source":"CS-Client","password":"","value":"497"}
3) 服务端发送数据:reAsk
在 CS-Monitor 的 OnMessage 事件中,将获取到 CS-Client 的数据请求。如果该 JSON 数据请求的行数合法,即不大于数据库表 Up 当前的最大行数,则在 Up 表里读出这条数据;服务端先要将读出的数据库数据类型转为 FrameData 类型数据 tmpFrmStruct,然后令新组建的 JSON 数据的 data 等于tmpFrmStruct 的数据列表 Parameter 的内容;组建 JSON 数据完毕后,发送该数据。执行代码如下所示。

//判断接收到的 Json 命令类型
switch (jsonRecv.command)
{
 	//收到 CS-Client/Web 网页/APP/微信的数据请求命令
 	case "ask":
 		try
 		{
 			//(1)获得需要回发的数据表 dr
 			int currentRow = Convert.ToInt32(jsonRecv.value);
 			int totalRows = frmMain.sQLUp.count();
 			if (totalRows < currentRow) 
 				goto OnMessage_exit_error1;
			 	DataTable dt = frmMain.sQLUp.selectRow(currentRow);
 			if (dt == null || dt.Rows.Count == 0)
 			{
 				answer.value = "NOT COMMAND 2";
 				goto OnMessage_exit_error1;
 			}
 			DataRow dr = dt.Rows[0];
 			//(2)通过 dr 获得结构数据对象 tmpFrmStruct
 			FrameData tmpFrmStruct = null;
 			string cmd = dr["cmd"].ToString();
 			if (frmMain.g_commandsFrame.ContainsKey(cmd))
 			{
 				tmpFrmStruct = frmMain.g_commandsFrame[cmd];
 			}
 			else
 				return;
 			tmpFrmStruct.dataRowToStruct(dr);
 			//(3)实例并初始化要发送的 Json 对象
 			JsonCommand2 reData = new JsonCommand2();
 			reData.command = "reAsk";
 			reData.source = "CS-Monitor";
			reData.currentRow = currentRow;
 			reData.totalRows = totalRows;
 			reData.password = "";
 			reData.data = tmpFrmStruct.Parameter;
 			//(4)将 Json 对象转换为 Json 字符串
 			string dataString3 = serializer.Serialize(reData);
 			//(5)将数据发送出去
 			Send(dataString3);
 		}
 		catch
 		{
 			answer.value = "NOT COMMAND 3";
 			goto OnMessage_exit_error1;
 		}
 		break;
}

4) 客户端回发数据:send
服务端发来实时数据之后,CS-Client 在其 OnMessage 事件中获取到实时数据,随即将它显示到实时数据窗体的文本框上并使能实时数据界面的 “回发”按键,重置回发时间。执行代码如下所示。

	case "reAsk":
		frmMain.NewestCount = jason2.totalRows;
		//正在接收实时数据
		if (frmMain.cmd == 0)
		{
 			FrameData tmpFrameData = new FrameData();
 			tmpFrameData.Parameter = jason2.data;
 			RealFrameDate = tmpFrameData;
 			string cmd = jason2.data[0].value;
 			//根据前后帧数据格式判断是否需要重新生成标签
 			if (cmd != cmdPrior)
 			{
 				cmdPrior = cmd;
 				m_SyncContext.Post(createLabel, tmpFrameData);//重新创建标签
 			}
 			Thread.Sleep(200);
 			//解析tmpFrmStruct中的数据并显示在相应文本框中
 			……………………..//此处省略部分代码
 			BtnSend.Enabled = true; //设置“回发”按钮有效
 			replyTime = 30; //“回发”时间等于30S
 }
break;

如果在实时数据到达后允许回发的时间内,在实时数据窗体中按下了“回发”按键将触发按键事件。在按键事件中,CS-Client 会将实时数据窗体显示文本框的内容读取到FrameData 对象 frame 中,令新组建的 JSON 数据的 data 段内容等于 frame 的 Parameter 成员,使用 send 方法将其回发给服务端。执行代码如下所示。

private void BtnSend_Click(object sender, EventArgs e)
{
	FrameData frame = RealFrameDate.Clone();
 	//(1)将文本框中内容更新到结构体 frame 中
	 ……………//此处省略部分代码
 	//(2)wsocket 回发
	try
 	{ 
 		JasonCommand2 sendJason = new JasonCommand2();
 		sendJason.command = "send";
 		sendJason.source = "CS-Client";
 		sendJason.password = "";
 		sendJason.dest = write_imsi;
 		sendJason.data = frame.Parameter;
 		//Jason 字符串转为 Jason 对象
 		var serializer = new JavaScriptSerializer(); //实例化一个 js 处理对象
 		string dataString = serializer.Serialize(sendJason);
 		ws.Send(dataString);//回发 
 		frmMain.setToolStripUserOperText("成功写入下行数据表中,待终端发送数据时,将把数据回发给终端");
 	}
 	catch
 	{
 		frmMain.setToolStripUserOperText("写入下行表失败");
 	}
 	BtnSend.Enabled = false; //设置“回发”按钮无效
 	replyTime = 0;
}

以上的流程分析主要是对实时数据交互过程的分析,历史数据的交互实现与实时数据类似,而且历史数据的实现不如实时数据复杂,所以只要充分理解了 WebSocket 服务端与客户端的实时数据交互过程,就能熟练掌握 CS-Monitor 模板的通信实现。

你可能感兴趣的:(嵌入式,websocket,c++,无监督学习)