Служба WCF с самообслуживанием WebSocket с клиентом Javascript

У меня есть этот самообслуживаемый код службы WebSocket WCF:

Main:

//Create a URI to serve as the base address
Uri httpUrl = new Uri("http://192.168.1.95:8080/service");
//Create ServiceHost
ServiceHost host = new ServiceHost(typeof(WebSocketService), httpUrl);            
//Add a service endpoint
host.AddServiceEndpoint(typeof(IWebSocket), new NetHttpBinding(), "");
//Enable metadata exchange
ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
host.Description.Behaviors.Add(smb);
//Start the Service
host.Open();

Console.WriteLine("Service is host at " + DateTime.Now.ToString());
Console.WriteLine("Host is running... Press <Enter> key to stop");
Console.ReadLine();

Интерфейс:

namespace IWebSocketHostTest
{
    [ServiceContract]
    interface IWebSocketCallBack
    {
        [OperationContract(IsOneWay = true)]
        void Send(int num);
    }

    [ServiceContract(CallbackContract = typeof(IWebSocketCallBack))]
    public interface IWebSocket
    {
        [OperationContract]
        void StartSend();
    }
}

Услуги:

namespace IWebSocketHostTest
{

class WebSocketService : IWebSocket
{
    Timer timer = null;

    List<IWebSocketCallBack> callbackClientList = null;        

    public WebSocketService()
    {
        callbackClientList = new List<IWebSocketCallBack>();

        timer = new Timer(3000);
        timer.Elapsed += new ElapsedEventHandler(sendNumber);
        timer.Start();
    }

    public void StartSend()
    {
        sender.addClient(OperationContext.Current.GetCallbackChannel<IWebSocketCallBack>());            
    }

    private void sendNumber(Object o, ElapsedEventArgs eea)
    {
        timer.Stop();
        var random = new Random();
        int randomNum = random.Next(100);
        foreach (IWebSocketCallBack callback in callbackClientList)
        {
            callback.Send(randomNum);
        }

        timer.Interval = random.Next(1000, 10000);
        timer.Start();
    }

}
}

Это отлично работает, если я добавлю ссылку этой службы в другое приложение .NET. Но мне нужно использовать эту службу из приложения HTML + Javascript, и я действительно потерял в том, как это сделать. Я не мог найти хороший пример или учебник с клиентом Javascript, использующим самообслуживаемую службу WCF WebSocket. Весь код Javascript WebSocket, который я мог найти, кажется очень простым, но я не мог заставить его работать.

Вот мой короткий клиентский тест JavaScript:

var ws = new WebSocket("ws://192.168.1.95:8080/service");
            ws.onopen = function () {
                console.log("WEBSOCKET CONNECTED");
            };

он возвращает "Ошибка WebSocket: неправильный ответ HTTP. Код состояния 400," Плохой запрос ", проверяющий его с помощью Fiddler.

Что мне не хватает? Не могли бы вы дать мне некоторые ссылки на doc, чтобы получить дополнительную информацию или пример кода?

Спасибо!

EDIT:

Теперь я попытался использовать библиотеку Microsoft.ServiceModel.WebSocket, чтобы попытаться заставить ее работать.

Но, во-первых, я не знаю, поддерживается ли она Microsoft по-прежнему, или если она устарела, потому что я не мог найти какую-либо информацию в MSDN и информации в Интернете мало. Во-вторых, метод Open() класса "WebSocketHost" не найден, поэтому я не знаю, как заставить сервер работать...

Вот мой код, я взял его из вопроса на форуме ASP.NET.

using System;
using Microsoft.ServiceModel.WebSockets;

namespace WebSocketTest
{
class Program
{
    static void Main(string[] args)
    {
        var host = new WebSocketHost<EchoService>(new Uri("ws://localhost:8080/echo"));
        host.AddWebSocketEndpoint();
        host.Open();

        Console.Read();

        host.Close();
    }
}

class EchoService : WebSocketService
{

    public override void OnOpen()
    {
        base.OnOpen();
        Console.WriteLine("WebSocket opened.");
    }

    public override void OnMessage(string message)
    {
        Console.WriteLine("Echoing to client:");
        Console.WriteLine(message);

        this.Send(message);
    }

    protected override void OnClose()
    {
        base.OnClose();
        Console.WriteLine("WebSocket closed.");
    }

    protected override void OnError()
    {
        base.OnError();
        Console.WriteLine("WebSocket error occured.");
    }
}
}

Но, как я уже говорил, метод "host.Open()" не найден, поэтому я не знаю, не пропал ли я какая-то ссылка или что, потому что я не мог найти информацию о классе WebSocketHost... Любая помощь?

Ответ 1

Проведя день с той же задачей, я наконец получил рабочее решение. Надеюсь, что это поможет кому-то в будущем.

Клиент JS script:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>WebSocket Chat</title>
<script src="http://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.1.js"></script>
<script type="text/javascript">
    var ws;
    $().ready(function ()
    {
        $("#btnConnect").click(function ()
        {
            $("#spanStatus").text("connecting");

            ws = new WebSocket("ws://localhost:8080/hello");

            ws.onopen = function ()
            {
                $("#spanStatus").text("connected");
            };
            ws.onmessage = function (evt)
            {
                $("#spanStatus").text(evt.data);
            };
            ws.onerror = function (evt)
            {
                $("#spanStatus").text(evt.message);
            };
            ws.onclose = function ()
            {
                $("#spanStatus").text("disconnected");
            };
        });
        $("#btnSend").click(function ()
        {
            if (ws.readyState == WebSocket.OPEN)
            {
                var res = ws.send($("#textInput").val());
            }
            else
            {
                $("#spanStatus").text("Connection is closed");
            }
        });
        $("#btnDisconnect").click(function ()
        {
            ws.close();
        });
    });
</script>
</head>
<body>
<input type="button" value="Connect" id="btnConnect" />
<input type="button" value="Disconnect" id="btnDisconnect" /><br />
<input type="text" id="textInput" />
<input type="button" value="Send" id="btnSend" /><br />
<span id="spanStatus">(display)</span>
</body>
</html>

Самостоятельный сервер:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Text;
using System.Threading.Tasks;

namespace WebSocketsServer
{
    class Program
    {
        static void Main(string[] args)
        {

            Uri baseAddress = new Uri("http://localhost:8080/hello");

            // Create the ServiceHost.
            using(ServiceHost host = new ServiceHost(typeof(WebSocketsServer), baseAddress))
            {
                // Enable metadata publishing.
                ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
                smb.HttpGetEnabled = true;
                smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
                host.Description.Behaviors.Add(smb);

                CustomBinding binding = new CustomBinding();
                binding.Elements.Add(new ByteStreamMessageEncodingBindingElement());
                HttpTransportBindingElement transport = new HttpTransportBindingElement();
                //transport.WebSocketSettings = new WebSocketTransportSettings();
                transport.WebSocketSettings.TransportUsage = WebSocketTransportUsage.Always;
                transport.WebSocketSettings.CreateNotificationOnConnection = true;
                binding.Elements.Add(transport);

                host.AddServiceEndpoint(typeof(IWebSocketsServer), binding, "");

                host.Open();

                Console.WriteLine("The service is ready at {0}", baseAddress);
                Console.WriteLine("Press <Enter> to stop the service.");
                Console.ReadLine();

                // Close the ServiceHost.
                host.Close();
            }
        }
    }

    [ServiceContract(CallbackContract = typeof(IProgressContext))]
    public interface IWebSocketsServer
    {
        [OperationContract(IsOneWay = true, Action = "*")]
        void SendMessageToServer(Message msg);
    }

    [ServiceContract]
    interface IProgressContext
    {
        [OperationContract(IsOneWay = true, Action = "*")]
        void ReportProgress(Message msg);
    }

    public class WebSocketsServer: IWebSocketsServer
    {
        public void SendMessageToServer(Message msg)
        {
            var callback = OperationContext.Current.GetCallbackChannel<IProgressContext>();
            if(msg.IsEmpty || ((IChannel)callback).State != CommunicationState.Opened)
            {
                return;
            }

            byte[] body = msg.GetBody<byte[]>();
            string msgTextFromClient = Encoding.UTF8.GetString(body);

            string msgTextToClient = string.Format(
                "Got message {0} at {1}",
                msgTextFromClient,
                DateTime.Now.ToLongTimeString());

            callback.ReportProgress(CreateMessage(msgTextToClient));
        }

        private Message CreateMessage(string msgText)
        {
            Message msg = ByteStreamMessage.CreateMessage(
                new ArraySegment<byte>(Encoding.UTF8.GetBytes(msgText)));

            msg.Properties["WebSocketMessageProperty"] =
                new WebSocketMessageProperty
                {
                    MessageType = WebSocketMessageType.Text
                };

            return msg;
        }
    }
}

UPDATE

Начиная с .net 4.5 появился новый способ написания серверной части. Преимущества - более чистый код и возможность поддержки защищенных веб-сокетов (WSS) через https.

public class WebSocketsServer
{
    #region Fields

    private static CancellationTokenSource m_cancellation;
    private static HttpListener m_listener;

    #endregion

    #region Private Methods

    private static async Task AcceptWebSocketClientsAsync(HttpListener server, CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            var hc = await server.GetContextAsync();
            if (!hc.Request.IsWebSocketRequest)
            {
                hc.Response.StatusCode = 400;
                hc.Response.Close();
                return;
            }

            try
            {
                var ws = await hc.AcceptWebSocketAsync(null).ConfigureAwait(false);
                if (ws != null)
                {
                    Task.Run(() => HandleConnectionAsync(ws.WebSocket, token));
                }
            }
            catch (Exception aex)
            {
                // Log error here
            }
        }
    }

    private static async Task HandleConnectionAsync(WebSocket ws, CancellationToken cancellation)
    {
        try
        {
                while (ws.State == WebSocketState.Open && !cancellation.IsCancellationRequested)
                {
                    String messageString = await ReadString(ws).ConfigureAwait(false);

                    var strReply = "OK"; // Process messageString and get your reply here;

                    var buffer = Encoding.UTF8.GetBytes(strReply);
                    var segment = new ArraySegment<byte>(buffer);
                    await ws.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);
                }

                await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Done", CancellationToken.None);
        }
        catch (Exception aex)
        {
            // Log error

            try
            {
                await ws.CloseAsync(WebSocketCloseStatus.InternalServerError, "Done", CancellationToken.None).ConfigureAwait(false);
            }
            catch
            {
                // Do nothing
            }
        }
        finally
        {
            ws.Dispose();
        }
    }

    private static async Task<String> ReadString(WebSocket ws)
    {
        ArraySegment<Byte> buffer = new ArraySegment<byte>(new Byte[8192]);

        WebSocketReceiveResult result = null;

        using (var ms = new MemoryStream())
        {
            do
            {
                result = await ws.ReceiveAsync(buffer, CancellationToken.None);
                ms.Write(buffer.Array, buffer.Offset, result.Count);
            }
            while (!result.EndOfMessage);

            ms.Seek(0, SeekOrigin.Begin);

            using (var reader = new StreamReader(ms, Encoding.UTF8))
            {
                return reader.ReadToEnd();
            }
        }
    }

    #endregion

    #region Public Methods

    public static void Start(string uri)
    {
        m_listener = new HttpListener();
        m_listener.Prefixes.Add(uri);
        m_listener.Start();

        m_cancellation = new CancellationTokenSource();
        Task.Run(() => AcceptWebSocketClientsAsync(m_listener, m_cancellation.Token));
    }

    public static void Stop()
    {
        if(m_listener != null && m_cancellation != null)
        {
            try
            {
                m_cancellation.Cancel();

                m_listener.Stop();

                m_listener = null;
                m_cancellation = null;
            }
            catch
            {
                // Log error
            }
        }
    }

    #endregion
}