图图的耳朵

如何使用HTML5的WebSocket实现网页与服务器的双工通信(一)

本系列服务端双工通信包括两种实现方式:一、使用Socket构建;二、使用WCF构建。本文为使用Socket构建服务端的双工通信,客户端同样使用Html5的WebSocket技术进行调用。

一、网页客户端:

 1 <!DOCTYPE html>
 2 <html xmlns="http://www.w3.org/1999/xhtml">
 3 <head>
 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 5 <title></title>
 6 <script src="scripts/jquery-1.10.2.min.js"></script>
 7 <script>
 8 var socket;
 9 //url必须使用ws或者wss(加密)作为头,这个url设定好后,在javascript脚本中可以通过访问websocket对象的url来重新获取
10 //通信建立连接后,就可以双向通信了,使用websocket对象的send方法加json数据便可将任何形式数据传往服务器
11 
12 //通过onmessage事件来接收服务器传送过来数据:
13 //通过onopern事件监听socket打开事件
14 //通过onclose监听socket关闭事件
15 //通过webSocket.close()方法关闭socket连接;
16 //通过readyState属性获取websocket对象状态:
17 //CONNECTION 0 正在连接
18 //OPEN 1 已经连接
19 //CLOSING 2 正在关闭
20 //CLOSE 3 已经关闭
21 $(function () {
22 $('#conn').click(function () {
23 //ws = new WebSocket('ws://' + window.location.hostname + ':' + '4649/Echo/');
24 socket = new WebSocket('ws://localhost:8021/');
25 $('#tips').text('正在连接');
26 
27 socket.addEventListener("open", function (e) {
28 $('#tips').html(
29 '<div>Connected. Waiting for messages...</div>');
30 //window.setInterval(function () {
31 // socket.send("the time is " + new Date());
32 //}, 1000);
33 }, false)
34 
35 socket.addEventListener("message", function (evt) {
36 $('#tips').text(evt.data);
37 });
38 
39 socket.onerror = function (evt) {
40 $('#tips').text(JSON.stringify(evt));
41 }
42 socket.onclose = function () {
43 $('#tips').text('已经关闭');
44 }
45 });
46 
47 $('#close').click(function () {
48 socket.close();
49 });
50 
51 $('#send').click(function () {
52 if (socket.readyState == WebSocket.OPEN) {
53 socket.send($('#content').val());
54 }
55 else {
56 $('#tips').text('连接已经关闭');
57 }
58 });
59 });
60 </script>
61 </head>
62 <body>
63 <form id="form1">
64 <div>
65 <input id="conn" type="button" value="连接" />
66 <input id="close" type="button" value="关闭" />
67 <span id="tips"></span>
68 <input id="content" type="text" />
69 <input id="send" type="button" value="发送" />
70 </div>
71 </form>
72 </body>
73 </html>
View Code

 

二、服务端:

创建控制台应用程序,代码如下:

class Program
{
//客户端集合
static List<Socket> clients = new List<Socket>();
static byte[] buffer = new byte[1024];
//static bool IsWebSocketClient = false; //客户端连接是否为websocket
static void Main(string[] args)
{
//创建一个新的Socket,这里我们使用最常用的基于TCP的Stream Socket(流式套接字)
Socket SeverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//将该socket绑定到主机上面的某个端口
SeverSocket.Bind(new IPEndPoint(IPAddress.Any, 8021));
//设置Socket为监听状态并设置允许的最大队列数为4
SeverSocket.Listen(4);

//服务端开始异步接受客户端的连接请求
SeverSocket.BeginAccept(AsyncAcceptCallback, SeverSocket);

Console.WriteLine("Sever is ready");

//创建一个时钟,每隔1分钟发送一个心跳包给客户端
SendHeartPackToClients();

Console.Read();
}
#region 创建一个时钟,每隔10秒发送一个心跳包给客户端

private static void SendHeartPackToClients()
{
System.Timers.Timer time = new System.Timers.Timer();
time.Interval = 10 * 1000;
time.Enabled = true;
time.Elapsed += time_Elapsed;
time.Start();
}

static void time_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
SendMsgToAllClients("hi," + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
}
/// <summary>
/// 发送消息给所有连接的客户端
/// </summary>
private static void SendMsgToAllClients(string msg)
{
try
{
foreach (var client in clients)
{
if (client.Connected)
{
client.Send(PackageServerData(msg));
}
}
}
catch (Exception)
{
//TODO
}

}

#endregion
/// <summary>
/// 服务端异步接受连接的回调处理方法
/// </summary>
/// <param name="ar"></param>
private static void AsyncAcceptCallback(IAsyncResult ar)
{
var ServerSocket = ar.AsyncState as Socket;
//异步接受传入的连接,并创建客户端Socket
var ClientSocket = ServerSocket.EndAccept(ar);

//将客户端加入集合中
clients.Add(ClientSocket);

//开始异步接收该客户端发送的消息
ClientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, AsyncReceiveCallback, ClientSocket);

//服务端开始异步接受下一个客户端的连接请求
ServerSocket.BeginAccept(AsyncAcceptCallback, ServerSocket);
}
/// <summary>
/// 异步接收消息的回调处理方法
/// </summary>
/// <param name="ar"></param>
private static void AsyncReceiveCallback(IAsyncResult ar)
{
try
{
var ClientSocket = ar.AsyncState as Socket;
int RevLength = ClientSocket.EndReceive(ar);

string RevMsg = Encoding.UTF8.GetString(buffer, 0, RevLength);

#region WebSocket处理代码
//判断是否为浏览器websocket发过来的请求,若是,则打包服务器握手数据,实现第4次握手
if (RevMsg.Contains("Sec-WebSocket-Key"))
{
//IsWebSocketClient = true;
Console.WriteLine(RevMsg);
ClientSocket.Send(PackageHandShakeData(buffer, RevLength));
}
else {
string AnalyzeMsg = AnalyzeClientData(buffer, RevLength);
Console.WriteLine(AnalyzeMsg);
ClientSocket.Send(PackageServerData("收到您的信息!"));
}
#endregion

//继续接收该客户端下一条发送的消息
ClientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, AsyncReceiveCallback, ClientSocket);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

}
#region 客户端和服务端的响应
/*
* 客户端向服务器发送请求
* 
* GET / HTTP/1.1
* Origin: http://localhost:1416
* Sec-WebSocket-Key: vDyPp55hT1PphRU5OAe2Wg==
* Connection: Upgrade
* Upgrade: Websocket
*Sec-WebSocket-Version: 13
* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
* Host: localhost:8064
* DNT: 1
* Cache-Control: no-cache
* Cookie: DTRememberName=admin
* 
* 服务器给出响应
* 
* HTTP/1.1 101 Switching Protocols
* Upgrade: websocket
* Connection: Upgrade
* Sec-WebSocket-Accept: xsOSgr30aKL2GNZKNHKmeT1qYjA=
* 
* 在请求中的“Sec-WebSocket-Key”是随机的,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个魔幻字符串
* “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用 SHA-1 加密,之后进行 BASE-64编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端
*/
#endregion
/// <summary>
/// 打包服务器握手数据
/// </summary>
/// <returns>The hand shake data.</returns>
/// <param name="handShakeBytes">Hand shake bytes.</param>
/// <param name="length">Length.</param>
private static byte[] PackageHandShakeData(byte[] handShakeBytes, int length)
{
string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, length);
string key = string.Empty;
Regex reg = new Regex(@"Sec\-WebSocket\-Key:(.*?)\r\n");
Match m = reg.Match(handShakeText);
if (m.Value != "")
{
key = Regex.Replace(m.Value, @"Sec\-WebSocket\-Key:(.*?)\r\n", "$1").Trim();
}

byte[] secKeyBytes = SHA1.Create().ComputeHash(
Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
string secKey = Convert.ToBase64String(secKeyBytes);

var responseBuilder = new StringBuilder();
responseBuilder.Append("HTTP/1.1 101 Switching Protocols" + "\r\n");
responseBuilder.Append("Upgrade: websocket" + "\r\n");
responseBuilder.Append("Connection: Upgrade" + "\r\n");
responseBuilder.Append("Sec-WebSocket-Accept: " + secKey + "\r\n\r\n");

//如果把上一行换成下面两行,才是thewebsocketprotocol-17协议,但居然握手不成功,目前仍没弄明白!
//responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine);
//responseBuilder.Append("Sec-WebSocket-Protocol: chat" + Environment.NewLine);

return Encoding.UTF8.GetBytes(responseBuilder.ToString());
}

#region 处理接收的数据
/// <summary>
/// 处理接收的数据
/// 参考 http://www.cnblogs.com/smark/archive/2012/11/26/2789812.html
/// </summary>
/// <param name="recBytes"></param>
/// <param name="length"></param>
/// <returns></returns>
private static string AnalyzeClientData(byte[] recBytes, int length)
{
int start = 0;
// 如果有数据则至少包括3位
if (length < 2) return "";
// 判断是否为结束针
bool IsEof = (recBytes[start] >> 7) > 0;
// 暂不处理超过一帧的数据
if (!IsEof) return "";
start++;
// 是否包含掩码
bool hasMask = (recBytes[start] >> 7) > 0;
// 不包含掩码的暂不处理
if (!hasMask) return "";
// 获取数据长度
UInt64 mPackageLength = (UInt64)recBytes[start] & 0x7F;
start++;
// 存储4位掩码值
byte[] Masking_key = new byte[4];
// 存储数据
byte[] mDataPackage;
if (mPackageLength == 126)
{
// 等于126 随后的两个字节16位表示数据长度
mPackageLength = (UInt64)(recBytes[start] << 8 | recBytes[start + 1]);
start += 2;
}
if (mPackageLength == 127)
{
// 等于127 随后的八个字节64位表示数据长度
mPackageLength = (UInt64)(recBytes[start] << (8 * 7) | recBytes[start] << (8 * 6) | recBytes[start] << (8 * 5) | recBytes[start] << (8 * 4) | recBytes[start] << (8 * 3) | recBytes[start] << (8 * 2) | recBytes[start] << 8 | recBytes[start + 1]);
start += 8;
}
mDataPackage = new byte[mPackageLength];
for (UInt64 i = 0; i < mPackageLength; i++)
{
mDataPackage[i] = recBytes[i + (UInt64)start + 4];
}
Buffer.BlockCopy(recBytes, start, Masking_key, 0, 4);
for (UInt64 i = 0; i < mPackageLength; i++)
{
mDataPackage[i] = (byte)(mDataPackage[i] ^ Masking_key[i % 4]);
}
return Encoding.UTF8.GetString(mDataPackage);
}
#endregion

#region 发送数据
/// <summary>
/// 把发送给客户端消息打包处理(拼接上谁什么时候发的什么消息)
/// </summary>
/// <returns>The data.</returns>
/// <param name="message">Message.</param>
private static byte[] PackageServerData(string msg)
{
byte[] content = null;
byte[] temp = Encoding.UTF8.GetBytes(msg);
if (temp.Length < 126)
{
content = new byte[temp.Length + 2];
content[0] = 0x81;
content[1] = (byte)temp.Length;
Buffer.BlockCopy(temp, 0, content, 2, temp.Length);
}
else if (temp.Length < 0xFFFF)
{
content = new byte[temp.Length + 4];
content[0] = 0x81;
content[1] = 126;
content[2] = (byte)(temp.Length & 0xFF);
content[3] = (byte)(temp.Length >> 8 & 0xFF);
Buffer.BlockCopy(temp, 0, content, 4, temp.Length);
}
return content;
}
#endregion
}
View Code

 

 


 同系列其他文章:如何使用HTML5的WebSocket实现网页与服务器的双工通信(二)

 

posted on 2017-11-29 13:52  图图的耳朵  阅读(5516)  评论(0编辑  收藏  举报