如何使用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實現網頁與服務器的雙工通信(二)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM