您的位置:首页 > 其它

.NET平台下websocket协议的实现!

2012-06-17 23:48 399 查看
本文完全原创,有问题请多多指教 。

最近一个项目需要做一个客户端运行与浏览器上面的即时通讯程序,原本我写的服务器端程序已经实现了,无奈通信层是在TCP套接字层上,单纯通过浏览器虽然可以实现,但是需要依赖于浏览器插件,而老师的要求是跨平台,并且浏览器插件的方式技术欠缺,所以决定重写浏览器端程序,使用javascript直接支持的通信协议。

传统的B/S架构即时通讯方式通常是使用ajax技术,在浏览器端通过轮询的方式定时的给web服务器发送request请求讯息。这样的通信方式的一个明显的缺点是ajax请求每次必须带有一个HTTP请求的头,有时候一个消息头甚至比有效的消息还长,并且HTTP请求是短连接,频繁的创建撤销套接字对客户端、服务器端来说都是沉重的负担。同时通信的实时性与请求频率成正比,代价极为高昂,于是放弃了这种方式。

最后通过仔细上网查找资料,发现了最新的HTML5技术的websocket协议能够满足项目的需求。什么是websocket?websocket是基于TCP/IP上面实现的一个类似于HTTP的协议,区别是websocket是长连接,并且通信是双向的,服务器可以向浏览器推送信息!

一个简单的浏览器端websocket js代码是这样的:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">

<html>
<head>
<title></title>
<script type="text/javascript">
var wsServer = 'ws://localhost:8000';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) { onOpen(evt) };
websocket.onclose = function (evt) { onClose(evt) };
websocket.onmessage = function (evt) { onMessage(evt) };
websocket.onerror = function (evt) { onError(evt) };
//alert("lian jie cheng gong");
function onOpen(evt) {
alert("Connected to WebSocket server.");
websocket.send("hello");
}
function onClose(evt) {
alert("Disconnected");
}
function onMessage(evt) {
alert('Retrieved data from server: ' + evt.data);
}
function onError(evt) {
alert('Error occured: ' + evt.data);
}
function Button1_onclick() {
var buffer = "Hello";
//for (i = 0; i < 9; i++) buffer += buffer;
websocket.send(buffer); //document.getElementById("Text1").innerHTML
}

</script>
</head>
<body>

<p>
<textarea id="TextArea1" cols="20" name="S1" rows="2"></textarea></p>
<p>
<input id="Text1" type="text" /></p>
<p>
<input id="Button1" type="button" value="button" onclick="return Button1_onclick()" /></p>

</body>
</html>

代码简单但是已经能满足通信的需求,当然,还有更多的函数可以去W3C官网查看。
可是问题出来了,js可以直接调用浏览器中的 websocket api,而服务器端却没有规范的实现,而是由RFC给出协议文档,程序员需要通过阅读文档自己封装socket实现,RFC网址:http://www.ietf.org/mail-archive/web/ietf-announce/current/msg09663.html
虽然网上有部分语言实现websocket的开源项目,比如我项目是在.NET下开发,codeplex上面有个superwebsocket项目相当完善,甚至可以说强大,完全提供了对websocket协议的支持。无奈中文资料甚少,并且个人感觉开发包太过沉重,基本是用于部署ASP.NET网站用的。我试过在winform上面使用,结果是主窗口关闭了而程序仍然后台运行,所以决定自己写一个轻量级的、可定制的websocket服务器,以满足后阶段项目的可扩展性。

首先通过查找一些中文资料对websocket有了一个初步印象(现在关于websocket协议的中文资料还真少),然后试着用C#写了一个简单的socket服务器,结果收到的消息头大相径庭,通过消息头发现我用的谷歌浏览器的websocket版本已经是13了,而网站上关于websocket协议的介绍却是协议制定初期的草稿,所以硬着头皮细读了下RFC6455的关键部分,虽然不是很明白,但是通过一步步调试终于逐渐把问题解决,做了一个初期的demo。服务器具体工作流程是:
首先创建TCP套接字监听指定端口,然后等待连接,浏览器端连接的时候第一个消息必然是一个类HTTP请求,典型的请求是:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 8

这里服务器主要关心的是Sec-WebSocket-Key这个头,他是双方握手是否成功的关键,对于这个的解释,RFC如此描述:

This header field is only used for WebSocket opening handshake.   The |Sec-WebSocket-Key| header is used in the WebSocket opening   handshake.  It is sent from the client to the server to provide part   of the information used by the server to prove that it received a   valid WebSocket opening handshake.  This helps ensure that the server   does not accept connections from non-WebSocket clients (e.g.  HTTP   clients) that are being abused to send data to unsuspecting WebSocket   servers.
大概意思就是客户端发送的用来验证握手的一个header,怎么样得到正确的回复的验证key呢?
For this header, the server has to take the value (as present in the
header, e.g. the base64-encoded [RFC4648] version minus any leading
and trailing whitespace), and concatenate this with the GUID
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" in string form, which is
unlikely to be used by network endpoints that do not understand the
WebSocket protocol.  A SHA-1 hash (160 bits), base64-encoded (see
Section 4 of [RFC4648]), of this concatenation is then returned in
the server's handshake [FIPS.180-2.2002].
大意是将该header的值与字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11连接,然后通过sha1加密,再base64-encoded就得到了正确的响应的key

解码出来后得到正确返回信息:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
上面的都是一成不变的头,关键就是最后一个,我们得到的key。
这样便成功进行了握手,接下来就是通信的问题了。


websocket通信并不是简单的发送字符串这么简单,客户端发过来的消息会在开头加上一下有特殊意义的标志位,然后是经过mask的消息

具体消息的结构:

This wire format for the data transfer part is described by the ABNF
[RFC5234] given in detail in this section.  A high level overview of
the framing is given in the following figure.

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/63)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

ws-frame                = frame-fin
frame-rsv1
frame-rsv2
frame-rsv3
frame-opcode
frame-masked
frame-payload-length
[ frame-masking-key ]
frame-payload-data
frame-fin               = %x0 ; more frames of this message follow
/ %x1 ; final frame of this message

frame-rsv1              = %x0
; 1 bit, MUST be 0 unless negotiated otherwise

frame-rsv2              = %x0
; 1 bit, MUST be 0 unless negotiated otherwise

frame-rsv3              = %x0
; 1 bit, MUST be 0 unless negotiated otherwise

frame-opcode            = %x0 ; continuation frame
/ %x1 ; text frame
/ %x2 ; binary frame
/ %x3-7 ; reserved for further non-control frames
/ %x8 ; connection close
/ %x9 ; ping
/ %xA ; pong
/ %xB-F ; reserved for further control frames

frame-masked            = %x0 ; frame is not masked, no frame-masking-key
/ %x1 ; frame is masked, frame-masking-key present

frame-payload-length    = %x00-7D
/ %x7E frame-payload-length-16
/ %x7F frame-payload-length-63

frame-payload-length-16 = %x0000-FFFF

frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF

frame-masking-key       = 4( %0x00-FF ) ; present only if frame-masked is 1

frame-payload-data      = (frame-masked-extension-data
frame-masked-application-data)   ; frame-masked 1
/ (frame-unmasked-extension-data
frame-unmasked-application-data) ; frame-masked 0

frame-masked-extension-data     = *( %x00-FF ) ; to be defined later

frame-masked-application-data   = *( %x00-FF )

frame-unmasked-extension-data   = *( %x00-FF ) ; to be defined later

frame-unmasked-application-data = *( %x00-FF )


看得有点晕,不过耐心看还是勉强能看懂的,就是一个字节数组,前面每个字节的位都有特殊意义,比如第一个字节的第一bit表示消息是否完成,2,3,4bit通常为0,然后后面4bit表示opcode,这里我们是通信的,所以是1(text frame)。mask表示信息是否是经过掩码异或,客户端强制mask上了,所以服务器解码必须unmask。mask需要的key在frame-payload-length后面的四个byte上已经定义。而frame-payload-length是表示本条消息的长度,如果小于125字节则用第二个字节表示,如果等于126则用3-4个字节表示长度,等于127则用3-9字节表示长度。

然后mask后面的就是有效信息了。

一个简单的例子如下

o  A single-frame unmasked text message

*  0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains "Hello")

o  A single-frame masked text message

*  0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58
(contains "Hello")

完全符合我的理解!

还等什么?开始coding!



专门用于接收连接的类:
/// <summary>
///websocket服务器监听类
/// 作者:甘
///日期:2012.06.17
/// </summary>
public class wsServer:SocketAbs
{
public bool SetUp(int listenPort=8000)
{
try
{
s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
s.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Any, listenPort));

SocketAsyncEventArgs e = new SocketAsyncEventArgs();
e.AcceptSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//e.Buffer = new Byte[65536];
e.Completed += new EventHandler<SocketAsyncEventArgs>(Accept_Completed);
//s.AcceptAsync()
s.Listen(200);
s.AcceptAsync(e);
}
catch (System.Exception ex)
{
return false;
}
return true;
}

/// 接收完成的回调函数,得到套接字并处理浏览器端请求头,通过处理密钥并返回,完成握手
/// 然后生成一个session。
private void Accept_Completed(object sender, SocketAsyncEventArgs e)
{
//int count = e.Offset;
try
{
Byte[] buffer=new Byte[1024];
e.AcceptSocket.Receive(buffer);
String s = "HTTP/1.1 101 Switching Protocols"+"\r\n";
addParam(ref s, "Upgrade", "websocket");
addParam(ref s, "Connection", "Upgrade");
addParam(ref s, "Sec-WebSocket-Accept", decodeKey(getParam(System.Text.Encoding.UTF8.GetString(buffer), "Sec-WebSocket-Key")));
s += "\r\n";//注意这里最后要多加一个\R\N,否则浏览器会拒绝该握手
e.AcceptSocket.Send(System.Text.Encoding.UTF8.GetBytes(s));
//e.AcceptSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//e.Buffer = new Byte[65536];
//SocketAsyncEventArgs e1 = new SocketAsyncEventArgs();
//e1.Completed += new EventHandler<SocketAsyncEventArgs>(Accept_Completed);
//e1.SetBuffer(new Byte[1024],0, 1024);
//e.AcceptSocket.ReceiveAsync(e1);
new ChatSession(e.AcceptSocket);
}
catch (System.Exception ex)
{

}

}

private static void addParam(ref String request,String param,String value)
{
request += param + ": " + value + "\r\n";
}
///  通过正则表达式获取KEY
private static String getParam(String request,String param)
{
String pattern = "(?:" + param + ": ).*\r\n";
Regex reg = new Regex(pattern);
Match m=reg.Match(request);
String value = m.Value.Replace(param+": ","");
return value.Replace("\r\n","");
}
///  对KEY与一个RFC定义的字符串连接,然后SHA1加密,在通过BASE64编码即可得到应答的key
private static String decodeKey(String key)
{
key += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
System.Security.Cryptography.SHA1 sha1 = System.Security.Cryptography.SHA1.Create();
return System.Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(key)));
}
}

/// 一下是session类,与客户端通信
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;

namespace GWebSocket
{
/// <summary>
///websocket服务器监听类
/// 作者:甘
///日期:2012.06.17
/// </summary>
public class ChatSession:SocketAbs
{
const int buffersize = 1024*8;
SocketAsyncEventArgs SendEvent = new SocketAsyncEventArgs();

//Byte[] buffer;
public ChatSession(Socket s1)
{
//buffer = new Byte[buffersize];
s=s1;
SendEvent.Completed += new EventHandler<SocketAsyncEventArgs>(Send_Completed);
SendEvent.SetBuffer(BufferMgr.getBytes(buffersize), 0, buffersize);
SocketAsyncEventArgs e = new SocketAsyncEventArgs();
//e.Buffer = new Byte[65536];
e.SetBuffer(BufferMgr.getBytes(buffersize), 0, buffersize);
e.Completed += new EventHandler<SocketAsyncEventArgs>(Recieve_Completed);
s1.ReceiveAsync(e);
}

private void Send_Completed(object sender, SocketAsyncEventArgs e)
{
Console.WriteLine("send ok");
}

private void Recieve_Completed(object sender, SocketAsyncEventArgs e)
{
//int count = e.Offset;
if (e.BytesTransferred==0)//客户端关闭连接
{
}
String str = DecodeClientMsg(e.Buffer, e.BytesTransferred);
if (str != null)
{
Console.WriteLine(str);
//Array.Copy(e.Buffer, SendEvent.Buffer, e.BytesTransferred);
//s.SendAsync(SendEvent);
}
//Console.WriteLine(Encoding.UTF8.GetString(e.Buffer,0,e.BytesTransferred));
Array.Clear(e.Buffer, 0, buffersize);
s.ReceiveAsync(e);

SendMsg("Hello");
}
//发送消息
public bool SendMsg(String msg)
{
//int len = msg.Length;
Array.Clear(SendEvent.Buffer, 0, buffersize);
SendEvent.Buffer[0] = 0x81;
Byte[] buffer = Encoding.UTF8.GetBytes(msg);
int payload_len = buffer.Length;
if (payload_len > buffersize)//超出字符限制
{
return false;
}
else if (payload_len >= 126)//用两个字节存payload_len
{
SendEvent.Buffer[1] = 126;
SendEvent.Buffer[2] = (byte)(payload_len & 0x0000FF00);
SendEvent.Buffer[3] = (byte)(payload_len & 0xff);
Array.Copy(buffer, 0, SendEvent.Buffer, 4, payload_len);
}
else
{
SendEvent.Buffer[1] = (byte)payload_len;
Array.Copy(buffer, 0, SendEvent.Buffer, 2, payload_len);
}
s.SendAsync(SendEvent);
return true;
}

//将接收数据进行解码得到信息
private static String DecodeClientMsg(Byte[] buffer, int len)
{
Byte[] mask=new Byte[4];//四字节掩码
int beginIndex = 0;
if ((buffer[0]&0x80)!=0x80)//信息未完成
{

}

else if (buffer[0]!=0x81)//不是文本信息
{
//return null;
}
else if ((buffer[1]&0x80)!=0x80)//客户端信息没有mask,拒绝
{
//return null;
}
else
{
//信息合法,开始解码
//获取 frame-payload-length的值
int payload_len = buffer[1] & 0x7F;//有效信息长度
if (payload_len==0x7F)//长度是64位
{
}
else if (payload_len==0x7E)//长度是16位
{
Array.Copy(buffer, 4, mask, 0, 4);
//Byte[] len2=new Byte[2];
//Array.Copy(buffer, 2, len2, 0, 2);
payload_len = payload_len & 0x00000000;
payload_len = payload_len | buffer[2];
payload_len = (payload_len << 8) | buffer[3];
beginIndex = 8;
// payload_len=
}
else//长度是8位
{
Array.Copy(buffer, 2, mask, 0, 4);
beginIndex = 6;
}

for (int i = 0; i < payload_len;i++ )
{
buffer[i + beginIndex] =(byte)(buffer[i + beginIndex] ^ mask[i % 4]);
}
return Encoding.UTF8.GetString(buffer, beginIndex, payload_len);
}
return null;
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: