WebSocket(一):Implement Simple Server and Client

一. 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

posted @ 2013-03-18 11:20  CanMusic  阅读(1246)  评论(0编辑  收藏  举报