一. WebSocket Server
首先选择安装nonocast实现的Nonocast.Http,详情可参见:http://nohttp.codeplex.com
可以通过VS自身的PackageManager下载,在Tools->Library Package Manager->Package Manager Console中输入“Install-Package Nonocast.Http”即可。
1. 创建Service类
public class RemoteService : SmallHTTPServiceBase { public ActionResult Default() { ActionResult result = new HtmlResult("wwwroot/Default.xhtml", DateTime.Now); return result; } public ActionResult Update() { return new EmptyActionResult(); } public override void Receive(TcpClient client, string data) { Console.WriteLine("[Client: ]{0}", data); }
}
我们可以看到其中有一个奇怪的地方“wwwroot/Default.xhtml”,这是一个Html的模板页面,通过访问http://127.0.0.1:7005/Action/Default,用户可以看到Default模板中的网页内容。并且对于模板的编程支持了Razor。参数DateTime.Now则是传给模板的Model。
所以Default.xhtml可以是如下形式:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml">
<head> <meta charset="utf-8" /> <title>Default HTML</title>
</head>
@using System;
@{ DateTime time = (DateTime)Model;
}
<body> <h2>@time</h2>
</body>
</html>
2. Open & Close
RemoteService service = new RemoteService();
service.Open();
...
service.Close();
3. 向所有客户端连接广播消息
service.Notify(“Hello World!”);
这样一个简单的WebSocket Server完成了。下面看看Client如何进行。
二. WebSocket Client
在.Net 4.5中引入了ClientWebSocket,但是可惜的是它只能在Win8 和 Server 2008平台使用。
那么对于广大的Win7和XP用户就只能自己实现或者使用其他第三方类库。
下面我们就来自己写一个简单的Client。
public class WebSocket { public event Action OnOpen; public event Action OnClose; public event WebSocketEventHandler OnMessage; public WebSocket(string address) { this.uri = new Uri(address); string protocol = uri.Scheme; if (!protocol.Equals("ws") && !protocol.Equals("wss")) { throw new ArgumentException("Unsupported protocol: " + protocol); } } public void Open() { string host = uri.DnsSafeHost; string path = uri.PathAndQuery; string origin = "http://" + host; client = CreateSocket(uri); stream = client.GetStream(); int port = ((IPEndPoint)client.Client.RemoteEndPoint).Port; if (port != 80) { host = host + ":" + port; } string request = @"GET {0} HTTP/1.1 Upgrade: WebSocket Connection: Upgrade Host: {1} Origin: {2} Sec-WebSocket-Key: 000 "; request = string.Format(request, path, host, origin); byte[] sendBuffer = Encoding.UTF8.GetBytes(request); stream.Write(sendBuffer, 0, sendBuffer.Length); StreamReader reader = new StreamReader(stream); // Handshake string header = reader.ReadLine(); header = reader.ReadLine(); header = reader.ReadLine(); header = reader.ReadLine(); header = reader.ReadLine(); hasHandshake = true; stream.BeginRead(readBuffer, 0, readBuffer.Length, new AsyncCallback(OnReadAsyncCallback), null); if (OnOpen != null) { OnOpen(); } } public void Close() { stream.Close(); client.Close(); if (OnClose != null) { OnClose(); } } public void Send(string message) { if (!hasHandshake) { throw new InvalidOperationException("Handshake not complete"); } byte[] buf = Encoding.UTF8.GetBytes(message); List<byte> sendBuffer = new List<byte>(); sendBuffer.Add(0x80); sendBuffer.Add((byte)(Convert.ToByte(buf.Length) | 0x80)); sendBuffer.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00 }); sendBuffer.AddRange(buf); stream.Write(sendBuffer.ToArray(), 0, sendBuffer.Count); stream.Flush(); } private void OnReadAsyncCallback(IAsyncResult ar) { stream.Read(readBuffer, 0, 1); int len = Convert.ToInt32(readBuffer[0]); len = stream.Read(readBuffer, 0, len); var s = Encoding.UTF8.GetString(readBuffer, 0, len); if (OnMessage != null) { OnMessage(this, new WebSocketEventArgs { TextData = s }); } stream.BeginRead(readBuffer, 0, readBuffer.Length, new AsyncCallback(OnReadAsyncCallback), null); } private static TcpClient CreateSocket(Uri arg) { string scheme = arg.Scheme; string host = arg.DnsSafeHost; int port = arg.Port; if (port <= 0) { if (scheme.Equals("wss")) port = 443; else if (scheme.Equals("ws")) port = 80; else throw new ArgumentException("Unsupported scheme"); } if (scheme.Equals("wss")) throw new NotImplementedException("SSL support not implemented yet"); else return new TcpClient(host, port); } private Uri uri; private bool hasHandshake; private TcpClient client; private NetworkStream stream; private byte[] readBuffer = new byte[4096]; } public delegate void WebSocketEventHandler(object sender, WebSocketEventArgs e); public class WebSocketEventArgs : EventArgs { public WebSocketEventArgs() { } public string TextData { get; set; } }
其中WebSocketServer的URI地址为如下形式:ws://127.0.0.1:7005/x
在通过TcpClient创建的Socket连接服务器,以上面的URI地址为例,IP为127.0.0.1,Port为7005,连接成功之后需要和WebSocket服务进行握手协议,向服务器发送如下内容:
GET /x HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: 127.0.0.1:7005
Origin: http://127.0.0.1
Sec-WebSocket-Key: 000
这时服务器端会反馈如下内容:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: h3596CBGNqC8QQzCKFeESSgpLBA=
Client将反馈信息接收完整之后方可进行正常的收发通信。
首先从接收说起:
服务端发送过来的数据包结构是如下形式的:
[length(1字节)][content(length字节)]
所以在接收数据的时候要先收length长度,再收content内容,如下:
private void OnReadAsyncCallback(IAsyncResult ar) { stream.Read(readBuffer, 0, 1); int len = Convert.ToInt32(readBuffer[0]); len = stream.Read(readBuffer, 0, len); var s = Encoding.UTF8.GetString(readBuffer, 0, len); if (OnMessage != null) { OnMessage(this, new WebSocketEventArgs { TextData = s }); } stream.BeginRead(readBuffer, 0, readBuffer.Length, new AsyncCallback(OnReadAsyncCallback), null); }
其次是发送,这就有些复杂了,根据发送的数据包大小不同,所使用的数据协议也会不同,小于128字节是一种协议,大于128字节又是一种协议。我们先简单些考虑小于128字节的情况,今后再进行扩展。
小于128字节时的数据包接口如下形式:
[0x80(1字节)][length(1字节)][mask(4字节)][content(length字节)]
其中length还需要和0x80进行OR操作
所以发送小型数据的时候如下方式实现:
public void Send(string message) { if (!hasHandshake) { throw new InvalidOperationException("Handshake not complete"); } byte[] buf = Encoding.UTF8.GetBytes(message); List<byte> sendBuffer = new List<byte>(); sendBuffer.Add(0x80); sendBuffer.Add((byte)(Convert.ToByte(buf.Length) | 0x80)); sendBuffer.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00 }); sendBuffer.AddRange(buf); stream.Write(sendBuffer.ToArray(), 0, sendBuffer.Count); stream.Flush(); }
通过代码可以看到,发送前先判断是否和服务器进行过握手协议,然后将内容UTF8编码,然后根据协议结构依次发送fin,length,mask,content.
三. 结尾
一个简单的WebSocket C/S实现了,虽然还有些缺陷,但是基本可以满足小数据量的网络通信,可以算是一个小型的IIS。
下面附上Example代码:code