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:
User-Agent: Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko
Host: 127.0.0.1:
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:
User-Agent: Mozilla/5.0 (Windows NT 6.3; rv:26.0) Gecko/ 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:
Origin: http://192.168.137.1
Pragma: no-cache
Cache-Control: no-cache
Sec-WebSocket-Key: NvxdeWLLsLXkt5DirLJ1yA==
Sec-WebSocket-Version:
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:
Origin: http://localhost
Sec-WebSocket-Key1: > =
Sec-WebSocket-Key2: 13q9% 974M${fdj6 `Qn32 �����R�q
分析了下这几种不同的浏览器,除了 Safari,其它均使用了最新的 WebSocket版本 即
Sec-WebSocket-Version:
但是 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 > && 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) > )
{
//Web Socket 初次连接
token.Name = "WebSocket";
Console.WriteLine("Accept WebSocket.");
sendBuffer=PackHandShakeData(GetSecKeyAccetp(received)); }
else
{
if (token.Name == "WebSocket")
{
string clientMsg;
if (e.Offset > )
{
byte[] buffer = new byte[e.BytesTransferred];
Array.Copy(e.Buffer, e.Offset, buffer, , 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();
Console.WriteLine("线程{0} 任务处理结束,发送数据", System.Threading.Thread.CurrentThread.ManagedThreadId); // 格式化数据后发回客户端。
sendBuffer = Encoding.UTF8.GetBytes("Returning " + received);
}
} //设置传回客户端的缓冲区。
e.SetBuffer(sendBuffer, , 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:(.*?)\r\n");
Match m = r.Match(handShakeText);
if (m.Groups.Count != )
{
key = Regex.Replace(m.Value, @"Sec\-WebSocket\-Key:(.*?)\r\n", "$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 < ) { return string.Empty; } bool fin = (recBytes[] & 0x80) == 0x80; // 1bit,1表示最后一帧
if (!fin)
{
return string.Empty;// 超过一帧暂不处理
} bool mask_flag = (recBytes[] & 0x80) == 0x80; // 是否包含掩码
if (!mask_flag)
{
return string.Empty;// 不包含掩码的暂不处理
} int payload_len = recBytes[] & 0x7F; // 数据长度 byte[] masks = new byte[];
byte[] payload_data; if (payload_len == )
{
Array.Copy(recBytes, , masks, , );
payload_len = (UInt16)(recBytes[] << | recBytes[]);
payload_data = new byte[payload_len];
Array.Copy(recBytes, , payload_data, , payload_len); }
else if (payload_len == )
{
Array.Copy(recBytes, , masks, , );
byte[] uInt64Bytes = new byte[];
for (int i = ; i < ; i++)
{
uInt64Bytes[i] = recBytes[ - i];
}
UInt64 len = BitConverter.ToUInt64(uInt64Bytes, ); payload_data = new byte[len];
for (UInt64 i = ; i < len; i++)
{
payload_data[i] = recBytes[i + ];
}
}
else
{
Array.Copy(recBytes, , masks, , );
payload_data = new byte[payload_len];
//修改,WebSocket如果自动触发了心跳,之后再发送数据,可能出错,增加下面的判断
if (recBytes.Length < + payload_len)
Array.Copy(recBytes, , payload_data, , recBytes.Length - );
else
Array.Copy(recBytes, , payload_data, , payload_len); } for (var i = ; i < payload_len; i++)
{
payload_data[i] = (byte)(payload_data[i] ^ masks[i % ]);
} 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 < )
{
contentBytes = new byte[temp.Length + ];
contentBytes[] = 0x81;
contentBytes[] = (byte)temp.Length;
Array.Copy(temp, , contentBytes, , temp.Length);
}
else if (temp.Length < 0xFFFF)
{
contentBytes = new byte[temp.Length + ];
contentBytes[] = 0x81;
contentBytes[] = ;
contentBytes[] = (byte)(temp.Length & 0xFF);
contentBytes[] = (byte)(temp.Length >> & 0xFF);
Array.Copy(temp, , contentBytes, , 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("已经与服务器建立了连接\r\n当前连接状态:"+this.readyState);};
ws.onmessage = function(event){alert("接收到服务器发送的数据:\r\n"+event.data);};
ws.onclose = function(event){alert("已经与服务器断开连接\r\n当前连接状态:"+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, , sendBuffer.Length);
//结束
bool willRaiseEvent = token.Socket.SendAsync(e);
if (!willRaiseEvent)
{
ProcessSend(e);
}
}
else
{
ProcessSend(e);
}
这样,就得到了最终正确的结果了。此问题困扰了我好几天。下面是运行结果图:
WebSocket学习笔记IE,IOS,Android等设备的兼容性问的更多相关文章
- WebSocket学习笔记——无痛入门
WebSocket学习笔记——无痛入门 标签: websocket 2014-04-09 22:05 4987人阅读 评论(1) 收藏 举报 分类: 物联网学习笔记(37) 版权声明:本文为博主原 ...
- iOS / Android 移动设备中的 Touch Icons
上次转载了一篇<将你的网站打造成一个iOS Web App>,但偶然发现这篇文章的内容有些是错误的——准确来说也不是错误,只是不适合自半年前来的情况了(也可以说是iOS7 之后的时间)—— ...
- WebSocket 学习笔记
WebSocket 学习笔记 来自我的博客 因为项目原因需要用到双工通信,所以比较详细的学习了一下浏览器端支持的 WebSocket. 并记录一些遇到的问题. 简介 WebSocket 一般是指浏览器 ...
- amazeui学习笔记一(开始使用3)--兼容性列表compatibility
amazeui学习笔记一(开始使用3)--兼容性列表compatibility 一.总结 1.不要用ie做前端测试,不要碰ie,尽量用google 浏览器: 按照微软官方的说法,IE 开发者工具中的浏 ...
- Android(java)学习笔记267:Android线程池形态
1. 线程池简介 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力. 假设一个服务器完成一项任务所需时间为:T1 创建线程时间, ...
- Android(java)学习笔记211:Android线程池形态
1. 线程池简介 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力. 假设一个服务器完成一项任务所需时间为:T1 创建线程时间, ...
- WebSocket 学习笔记--IE,IOS,Android等设备的兼容性问题与代码实现
一.背景 公司最近准备将一套产品放到Andriod和IOS上面去,为了统一应用的开发方式,决定用各平台APP嵌套一个HTML5浏览器来实现,其中数据通信,准备使用WebSocket的方式.于是,我开始 ...
- CCNA学习笔记(1) IOS操作系统 路由器 交换机 启动 自检 以及部分命令
注意:以下内容是以思科为学习环境 IOS操作系统启动: 路由和交换机和个人电脑启动没有区别,都会发送新号表示启动状态,也会进入系统自检.只得注意的是:1.一长两短的响声是显卡报警. 2.一声长鸣是内存 ...
- iOS学习笔记(四)——iOS应用程序生命周期
开发应用程序都要了解其生命周期,开始接触android时也是从应用程序生命周期开始的,android的应用程序生命周期更多是其组件的生命周期,例如Activity.Service.今天我们接触一下iO ...
随机推荐
- HDU 2089 不要62(数位DP)
不要62 Problem Description 杭州人称那些傻乎乎粘嗒嗒的人为62(音:laoer).杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了, ...
- C++ map插入(insert)数据返回值
例子: typedef boost::unordered_map<int, int> UserOnlineMap; UserOnlineMap userOnlineMap_; std::p ...
- js禁止高频率连续点击思路
1.类似react的数据流,点击之后立即设置值为空,当返回值后才可以点击 2.设置定时器,每次进入之前先清空掉定时器,然后开启定时器 <main> <div id="me& ...
- 【转】oracle null
转自:oracle的null和空字符串'' 1.oracle 将 空字符串即''当成null 2.null 与任何值做逻辑运算得结果都为 false,包括和null本身 3.用 is null 判断时 ...
- python的一些学习资料(持续更新中)
Markdown在线编辑器 廖雪峰官方博客[基础入门好资料] python-guide[传说中的巨牛写的] the5fire的技术博客[全职python程序员博客]
- Linux grep和find的区别
这是两个不同的命令,关于grep:Linux系统中grep命令是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹 配的行打印出来.grep全称是Global Regular Expressi ...
- PERL高效代码摘录 - 数组
1. 2个数组找不同 ,,,,,,); ,); %seen=(); foreach(@a_hast_g){ ; } @unique=grep($seen{$_},@a_cl_g); } map { $ ...
- Adding the Test API in The ASP.NET Web API Help Page
1.通过NuGet引用Web API Test Client 引用玩该DLL会生成如下文件: 这里面就是我们的帮助文档界面 2.在项目属性中进行如下设置,勾选XMl文档文件,并设置路径 3.在项目的A ...
- C# 在SQLite数据库中存储图像 z
C# 在SQLite数据库中存储图像 更多 0 C# SQLite 建表语句 CREATE TABLE [ImageStore]([ImageStore_Id] INTEGER NOT NULL ...
- ASP.NET 共用类库
using System; using System.Collections.Generic; using System.Text; using System.Web; using System.We ...