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?
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();
}
}
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.