Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to keep track of established connections using WebSockets

I am trying to get a real-time chat service for cross-platform devices to life. The problem is that System.Net.WebSockets namespace doesn't allow me directly to keep track of an established connection. I could take a sessionID of the current connection but how can I say perform the following action await socket.SendAsync(buffer, WebSocketMessageType.Text, CancellationToken.None) for a specific client?

If I could use Microsoft.WebSockets, I would have the possibility to create a WebSocketCollection() and do something like client.Send(message), but I can't send an ArraySegment<byte[]> through it. I also think this is more intended for AJAX clients and websites and this stuff.

I now have the following code snippet:

public class WSHandler : IHttpHandler
{
    event NewConnectionEventHandler NewConnection;
    public void ProcessRequest(HttpContext context)
    {
        if (context.IsWebSocketRequest)
        {
            context.AcceptWebSocketRequest(ProcessWSChat);
        }
    }

    public bool IsReusable { get { return false; } }
    private async Task ProcessWSChat(AspNetWebSocketContext context)
    {
        WebSocket socket = context.WebSocket;
        int myHash = socket.GetHashCode();
        while (true)
        {
        ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[1024]);
        WebSocketReceiveResult result = await socket.ReceiveAsync(
            buffer, CancellationToken.None);
            if (socket.State == WebSocketState.Open)
            {
                string userMessage = Encoding.ASCII.GetString(
                    buffer.Array, 0, result.Count);
                userMessage = "You sent: " + userMessage + " at " +
                    DateTime.Now.ToLongTimeString() + " from ip " +
                    context.UserHostAddress.ToString();
                buffer = new ArraySegment<byte>(
                    Encoding.ASCII.GetBytes(userMessage));
                await socket.SendAsync(
                    buffer, WebSocketMessageType.Text, true, CancellationToken.None);
            }
            else
            {
                break;
            }
        }
    }
}

How can I extend my project, save the sessions/connections and call a specific user connection in order to send it a message?

like image 709
Flash1232 Avatar asked Oct 20 '22 00:10

Flash1232


2 Answers

Usually, you do not do something like that. The WebSocket should be just and endpoint to another layer or sub-system. For example, an event-driven architecture. On connection, you should start the handler, gather data from the connected socket like the cookie, the URL or whatever, and notify something else that such user with that cookie/url is connected in that socket.

About your code:

  • Do not declare the read buffer inside the loop, you can reuse it.
  • WebSockets always send text data as UTF8, not ASCII.

This would be a way of keep track of your users in a small project, but I recommend you to plug the WebSocket to another message infrastructure like MassTransit:

public class WSHandler : IHttpHandler
{
    public bool IsReusable { get { return false; } }

    public void ProcessRequest(HttpContext context)
    {
        if (context.IsWebSocketRequest)
        {
            context.AcceptWebSocketRequest(ProcessWSChat);
        }
    }

    static readonly ConcurrentDictionary<IPrincipal, WebSocket> _users = new ConcurrentDictionary<IPrincipal, WebSocket>();

    private async Task ProcessWSChat(AspNetWebSocketContext context)
    {
        WebSocket socket = context.WebSocket;
        ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[4096]);

        //Identify user by cookie or whatever and create a user Object
        var cookie = context.Cookies["mycookie"];
        var url = context.RequestUri;

        IPrincipal myUser = GetUser(cookie, url);

        // Or uses the user that came from the ASP.NET authentication.
        myUser = context.User;

        _users.AddOrUpdate(myUser, socket, (p, w) => socket);

        while (socket.State == WebSocketState.Open)
        {
            WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None)
                                                        .ConfigureAwait(false);
            String userMessage = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);

            userMessage = "You sent: " + userMessage + " at " +
                DateTime.Now.ToLongTimeString() + " from ip " + context.UserHostAddress.ToString();
            var sendbuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(userMessage));

            await socket.SendAsync(sendbuffer , WebSocketMessageType.Text, true, CancellationToken.None)
                        .ConfigureAwait(false);
        }

        // when the connection ends, try to remove the user
        WebSocket ows;
        if (_users.TryRemove(myUser, out ows))
        {
            if (ows != socket)
            {
                // whops! user reconnected too fast and you are removing
                // the new connection, put it back
                _users.AddOrUpdate(myUser, ows, (p, w) => ows);
            }
        }
    }

    private IPrincipal GetUser(HttpCookie cookie, Uri url)
    {
        throw new NotImplementedException();
    }
}
like image 195
vtortola Avatar answered Oct 22 '22 16:10

vtortola


I guess reinventing the wheel would be very stupid, so I better use SignalR for this purpose.

Thanks @Jonesy for making me aware of this Microsoft library!

edit:

SignalR doesn't work properly on non-"IIS Express" mode. "Local IIS" causes the whole project to fail.

edit:

SignalR works now. A rewrite rule made it impossible to get the proper path.

like image 30
Flash1232 Avatar answered Oct 22 '22 16:10

Flash1232