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 服务端的 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 与其对应数据格式和应用场景。
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 模板的通信实现。