WebSocket 學習筆記--IE,IOS,Android等裝置的相容性問題與程式碼實現
一、背景
公司最近準備將一套產品放到Andriod和IOS上面去,為了統一應用的開發方式,決定用各平臺APP巢狀一個HTML5瀏覽器來實現,其中資料通訊,準備使用WebSocket的方式。於是,我開始在各大瀏覽器上測試。
二、協議分析
2.1 WebSocket的請求包
首先把原來做Socket通訊的程式拿出來,跟蹤下瀏覽器在WebSocket應用請求服務端的時候發的資料包的內容:
IE11:
GET /chat HTTP/1.1 Origin: http://localhost Sec-WebSocket-Key: 98JFoEb6pMLFYhAQATn6hw== Connection: Upgrade Upgrade: Websocket Sec-WebSocket-Version: 13 User-Agent: Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko Host: 127.0.0.1:1333 Cache-Control: no-cache Cookie: s_pers=%20s_20s_nr%3D1390552565333-Repeat%7C1422088565333%3B
FireFox 26.0:
GET /chat HTTP/1.1 Host: 127.0.0.1:1333 User-Agent: Mozilla/5.0 (Windows NT 6.3; rv:26.0) Gecko/20100101 Firefox/26.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Sec-WebSocket-Version: 13 Origin: http://localhost Sec-WebSocket-Key: kO4aF1Gpm1mBwOr6j30h0Q== Connection: keep-alive, Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket
Google Chrome 33.0.1707.0 :
GET /chat HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 127.0.0.1:1333 Origin: http://192.168.137.1 Pragma: no-cache Cache-Control: no-cache Sec-WebSocket-Key: NvxdeWLLsLXkt5DirLJ1yA== Sec-WebSocket-Version: 13 Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits, x-webkit-deflate-frame User-Agent: Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1707.0 Safari/537.36
Apple Safari 5.1.7(7534.57.2)Windows 版本
GET /chat HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: 127.0.0.1:1333
Origin: http://localhost
Sec-WebSocket-Key1: 25 58 510 64 > = 7
Sec-WebSocket-Key2: 13q9% 974M${fdj6 2`Qn32
�����R�q
分析了下這幾種不同的瀏覽器,除了 Safari,其它均使用了最新的 WebSocket版本 即 Sec-WebSocket-Version: 13
但是 Safrai 使用的還是老式的 WebSocket 7.5-7.6版本。
有下面一些文章,對於WebSocket的版本進行了說明:
WebSocket握手總結 http://www.hoverlees.com/blog/?p=1413
基於Websocket草案10協議的升級及基於Netty的握手實現 http://blog.csdn.net/fenglibing/article/details/6852497
WebSocket握手協議 http://blog.163.com/liwei1987821@126/blog/static/17266492820133190211333/
2.2,IE 對WebSocket的支援問題
這裡有必要單獨拿出來說,IE10以後才支援HTML5,因此也要這個版本後的瀏覽器才支援WebSocket,所以預設的Win7下面的瀏覽器都沒法支援。我測試用的是Win8.1的IE11,可以支援WebSocket,效果跟FireFox、Chrome一樣,但有一個惱火的問題,IE的WebSocket它會自動向伺服器端發起“心跳包”,而此時服務端使用SockeAsyncEventArgs 元件,會出問題,需要客戶端多次發資料之後,才會取到正確的客戶端請求的資料。
另外,.NET 4.5內建支援了WebSocket,但要求作業系統是Win8或者 Server2012以上的版本。所以如果生產環境的伺服器沒有這麼新,那麼WebSocketServer只有自己寫了。
2.3,IOS系統上WebSoket問題
Apple 內建的瀏覽器就是 Safrai,那麼IOS上面的瀏覽器 支援的 WebSocket 版本怎麼樣呢 ?
找了下同事的 iPhone 4s,IOS 7.0.1 的版本 ,經過測試 ,正常,跟其它瀏覽器一樣,但不知道其它版本的IOS下面的瀏覽器支援得 怎麼樣。這就奇怪了,為何Windows 桌面版本的Safrai 不行呢 ?
2.4,安卓上的WebSocket問題
很不幸,目前安卓最新的版本 ,內建的瀏覽器外掛仍然不支援WebSocket,而下載的QQ瀏覽器等是可以支援的。但是安卓上面的 App預設使用的都是 Andriod核心的瀏覽器外掛,因此它們沒法支援WebSocket。但是,既然是系統上面執行的 APP了,為何不直接走Socket 通訊方式呢?同事說是為了2個平臺能夠使用同一套Web應用,畢竟應用巢狀在一個瀏覽器裡面對於開發維護還是最方便的。
所以,解決的路徑還是想辦法讓安卓的預設瀏覽器外掛能夠支援WebSocket,查找了下資料,大概有這些資料:
android怎麼整合支援websocket的瀏覽器核心 http://www.oschina.net/question/1049351_116337
在android的webview中實現websocket http://xuepiaoqiyue.blog.51cto.com/4391594/1285791
但同事說,這些方法用過了,就是現在測試的效果,跟真正的WebSocket 相容得不好,使用我的程式測試可以握手連線,但是解析內容上不成功。後來分析,是同事的程式對資料有特殊格式的要求,只要按照他的要求去分析,那麼是可以解析得到正確的結果的。
三、WebSocket 服務端和客戶端實現
最新的WebSocket 13 版本支援的服務端程式碼:
SocketServer 對於WebSocket資訊的處理:
private void ProcessReceive(SocketAsyncEventArgs e)
{
// check if the remote host closed the connection
AsyncUserToken token = (AsyncUserToken)e.UserToken;
if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
{
//increment the count of the total bytes receive by the server
Interlocked.Add(ref m_totalBytesRead, e.BytesTransferred);
Console.WriteLine("The server has read a total of {0} bytes", m_totalBytesRead);
//echo the data received back to the client
//增加自定義處理
string received = System.Text.Encoding.UTF8.GetString(e.Buffer, e.Offset, e.BytesTransferred);
Byte[] sendBuffer = null;
//IE:Upgrade: Websocket
//FireFox: Upgrade: websocket
//Chrome: Upgrade: websocket
if (received.IndexOf("Upgrade: websocket", StringComparison.OrdinalIgnoreCase) > 0)
{
//Web Socket 初次連線
token.Name = "WebSocket";
Console.WriteLine("Accept WebSocket.");
sendBuffer=PackHandShakeData(GetSecKeyAccetp(received));
}
else
{
if (token.Name == "WebSocket")
{
string clientMsg;
if (e.Offset > 0)
{
byte[] buffer = new byte[e.BytesTransferred];
Array.Copy(e.Buffer, e.Offset, buffer, 0, buffer.Length);
clientMsg = AnalyticData(buffer, buffer.Length);
Console.WriteLine("--DEBUG:Web Socket Recive data offset:{0}--",e.Offset);
}
else
{
clientMsg = AnalyticData(e.Buffer, e.BytesTransferred);
}
Console.WriteLine("接受到客戶端資料:" + clientMsg);
if (!string.IsNullOrEmpty(clientMsg))
{
//解析來的真正訊息,執行伺服器處理
//To Do
//
//傳送資料
string sendMsg = "Hello," + clientMsg;
Console.WriteLine("傳送資料:“" + sendMsg + "” 至客戶端....");
sendBuffer = PackData(sendMsg);
}
else
{
sendBuffer = PackData("心跳包");
}
}
else
{
Console.WriteLine("伺服器接收到的資料:[{0}],將進行1000ms的處理。CurrentThread.ID:{1}", received, System.Threading.Thread.CurrentThread.ManagedThreadId);
System.Threading.Thread.Sleep(1000);
Console.WriteLine("執行緒{0} 任務處理結束,傳送資料", System.Threading.Thread.CurrentThread.ManagedThreadId);
// 格式化資料後發回客戶端。
sendBuffer = Encoding.UTF8.GetBytes("Returning " + received);
}
}
//設定傳回客戶端的緩衝區。
e.SetBuffer(sendBuffer, 0, sendBuffer.Length);
//結束
bool willRaiseEvent = token.Socket.SendAsync(e);
if (!willRaiseEvent)
{
ProcessSend(e);
}
}
else
{
CloseClientSocket(e);
}
}
下面是一些相關的WebSocket 處理程式碼,包括握手、打包資料等:
// <summary>
/// 打包握手資訊
/// <remarks> http://www.hoverlees.com/blog/?p=1413 Safari 早期版本不支援標準的version 13,握手不成功。
/// 據測試,最新的IOS 7.0 支援
/// </remarks>
/// </summary>
/// <param name="secKeyAccept">Sec-WebSocket-Accept</param>
/// <returns>資料包</returns>
private static byte[] PackHandShakeData(string secKeyAccept)
{
var responseBuilder = new StringBuilder();
responseBuilder.Append("HTTP/1.1 101 Switching Protocols" + Environment.NewLine);
responseBuilder.Append("Upgrade: websocket" + Environment.NewLine);
responseBuilder.Append("Connection: Upgrade" + Environment.NewLine);
responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine + Environment.NewLine);
//如果把上一行換成下面兩行,才是thewebsocketprotocol-17協議,但居然握手不成功,目前仍沒弄明白!
//responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine);
//responseBuilder.Append("Sec-WebSocket-Protocol: chat" + Environment.NewLine);
return Encoding.UTF8.GetBytes(responseBuilder.ToString());
}
/// <summary>
/// 生成Sec-WebSocket-Accept
/// </summary>
/// <param name="handShakeText">客戶端握手資訊</param>
/// <returns>Sec-WebSocket-Accept</returns>
private static string GetSecKeyAccetp(string handShakeText) //byte[] handShakeBytes, int bytesLength
{
//string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, bytesLength);
string key = string.Empty;
Regex r = new Regex(@"Sec-WebSocket-Key:(.*?)rn");
Match m = r.Match(handShakeText);
if (m.Groups.Count != 0)
{
key = Regex.Replace(m.Value, @"Sec-WebSocket-Key:(.*?)rn", "$1").Trim();
}
byte[] encryptionString = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
return Convert.ToBase64String(encryptionString);
}
/// <summary>
/// 解析客戶端資料包
/// </summary>
/// <param name="recBytes">伺服器接收的資料包</param>
/// <param name="recByteLength">有效資料長度</param>
/// <returns></returns>
private static string AnalyticData(byte[] recBytes, int recByteLength)
{
if (recByteLength < 2) { return string.Empty; }
bool fin = (recBytes[0] & 0x80) == 0x80; // 1bit,1表示最後一幀
if (!fin)
{
return string.Empty;// 超過一幀暫不處理
}
bool mask_flag = (recBytes[1] & 0x80) == 0x80; // 是否包含掩碼
if (!mask_flag)
{
return string.Empty;// 不包含掩碼的暫不處理
}
int payload_len = recBytes[1] & 0x7F; // 資料長度
byte[] masks = new byte[4];
byte[] payload_data;
if (payload_len == 126)
{
Array.Copy(recBytes, 4, masks, 0, 4);
payload_len = (UInt16)(recBytes[2] << 8 | recBytes[3]);
payload_data = new byte[payload_len];
Array.Copy(recBytes, 8, payload_data, 0, payload_len);
}
else if (payload_len == 127)
{
Array.Copy(recBytes, 10, masks, 0, 4);
byte[] uInt64Bytes = new byte[8];
for (int i = 0; i < 8; i++)
{
uInt64Bytes[i] = recBytes[9 - i];
}
UInt64 len = BitConverter.ToUInt64(uInt64Bytes, 0);
payload_data = new byte[len];
for (UInt64 i = 0; i < len; i++)
{
payload_data[i] = recBytes[i + 14];
}
}
else
{
Array.Copy(recBytes, 2, masks, 0, 4);
payload_data = new byte[payload_len];
//修改,WebSocket如果自動觸發了心跳,之後再發送資料,可能出錯,增加下面的判斷
if (recBytes.Length < 6 + payload_len)
Array.Copy(recBytes, 6, payload_data, 0, recBytes.Length - 6);
else
Array.Copy(recBytes, 6, payload_data, 0, payload_len);
}
for (var i = 0; i < payload_len; i++)
{
payload_data[i] = (byte)(payload_data[i] ^ masks[i % 4]);
}
return Encoding.UTF8.GetString(payload_data);
}
/// <summary>
/// 打包伺服器資料
/// </summary>
/// <param name="message">資料</param>
/// <returns>資料包</returns>
private static byte[] PackData(string message)
{
byte[] contentBytes = null;
byte[] temp = Encoding.UTF8.GetBytes(message);
if (temp.Length < 126)
{
contentBytes = new byte[temp.Length + 2];
contentBytes[0] = 0x81;
contentBytes[1] = (byte)temp.Length;
Array.Copy(temp, 0, contentBytes, 2, temp.Length);
}
else if (temp.Length < 0xFFFF)
{
contentBytes = new byte[temp.Length + 4];
contentBytes[0] = 0x81;
contentBytes[1] = 126;
contentBytes[2] = (byte)(temp.Length & 0xFF);
contentBytes[3] = (byte)(temp.Length >> 8 & 0xFF);
Array.Copy(temp, 0, contentBytes, 4, temp.Length);
}
else
{
// 暫不處理超長內容
}
return contentBytes;
}
測試配套的客戶端 HTML程式碼:
<html>
<head>
<meta charset="UTF-8">
<title>Web sockets test</title>
<script type="text/javascript">
var ws;
function ToggleConnectionClicked() {
try {
var ip = document.getElementById("txtIP").value;
ws = new WebSocket("ws://" + ip + ":1333/chat"); //連線伺服器 ws://localhost:1818/chat
ws.onopen = function(event){alert("已經與伺服器建立了連線rn當前連線狀態:"+this.readyState);};
ws.onmessage = function(event){alert("接收到伺服器傳送的資料:rn"+event.data);};
ws.onclose = function(event){alert("已經與伺服器斷開連線rn當前連線狀態:"+this.readyState);};
ws.onerror = function(event){alert("WebSocket異常!");};
} catch (ex) {
alert(ex.message);
}
};
function SendData() {
try{
ws.send("張三.");
}catch(ex){
alert(ex.message);
}
};
function seestate(){
alert(ws.readyState);
}
</script>
</head>
<body>
Server IP:<input type="text" id="txtIP" value="127.0.0.1"/> <button id='ToggleConnection' type="button" onclick='ToggleConnectionClicked();'>連線伺服器</button><br /><br />
<button id='ToggleConnection' type="button" onclick='SendData();'>傳送我的名字:beston</button><br /><br />
<button id='ToggleConnection' type="button" onclick='seestate();'>檢視狀態</button><br /><br />
</body>
</html>
但是上面的程式碼依然無法處理IE的“心跳”資料引起的問題。此時需要修改一下WebSocket對接受到資料的處理方式,如果客戶端傳送的是無效的資料,比如IE的心跳資料 ,那麼直接過濾,不寫入任何資料,將服務端的程式碼做下面的修改即可:
if (sendBuffer != null)
{
//設定傳回客戶端的緩衝區。
e.SetBuffer(sendBuffer, 0, sendBuffer.Length);
//結束
bool willRaiseEvent = token.Socket.SendAsync(e);
if (!willRaiseEvent)
{
ProcessSend(e);
}
}
else
{
ProcessSend(e);
}
這樣,就得到了最終正確的結果了。此問題困擾了我好幾天。下面是執行結果圖: