I want to implement a simple chat server with the new System.Net.WebSockets classes in .NET 4.5 and later (on Windows 8.1). However, I only find examples making use of those classes in an ASP.NET environment (especially the ones here: http://www.codemag.com/Article/1210051)
I don't have such one, and would like to implement the websocket server as "raw" as possible, but without having to reimplement all the websocket protocol as Microsoft hopefully already did that in .NET 4.5.
I thought of simply instantiating a new WebSocket class like I'd do with a normal Socket, but the constructor is protected. So I went to create a class inheriting from it, but then I noticed I had to implement so many abstract methods and properties that it looked like I'm rewriting the whole logic (especially because I had to implement things like State or SendAsync).
I'm afraid that the MSDN documentation didn't help me. The documentation there has a pre-release status and many comments just say "TBD" or "when its implemented".
Yes.
The easiest way is to use an HTTPListener. If you search for HTTPListener WebSocket you'll find plenty of examples.
In a nutshell (pseudo-code)
HttpListener httpListener = new HttpListener();
httpListener.Prefixes.Add("http://localhost/");
httpListener.Start();
HttpListenerContext context = await httpListener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null);
WebSocket webSocket = webSocketContext.WebSocket;
while (webSocket.State == WebSocketState.Open)
{
await webSocket.SendAsync( ... );
}
}
Requires .NET 4.5 and Windows 8 or later.
I just stumbled on this link that shows how to implement a IHttpHandler using just the System.Net.WebSockets implementation. The handler is required as the .NET WebSocket implementation is dependent on IIS 8+.
using System;
using System.Web;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.WebSockets;
namespace AspNetWebSocketEcho
{
public class EchoHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
if (context.IsWebSocketRequest)
context.AcceptWebSocketRequest(HandleWebSocket);
else
context.Response.StatusCode = 400;
}
private async Task HandleWebSocket(WebSocketContext wsContext)
{
const int maxMessageSize = 1024;
byte[] receiveBuffer = new byte[maxMessageSize];
WebSocket socket = wsContext.WebSocket;
while (socket.State == WebSocketState.Open)
{
WebSocketReceiveResult receiveResult = await socket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), CancellationToken.None);
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
}
else if (receiveResult.MessageType == WebSocketMessageType.Binary)
{
await socket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "Cannot accept binary frame", CancellationToken.None);
}
else
{
int count = receiveResult.Count;
while (receiveResult.EndOfMessage == false)
{
if (count >= maxMessageSize)
{
string closeMessage = string.Format("Maximum message size: {0} bytes.", maxMessageSize);
await socket.CloseAsync(WebSocketCloseStatus.MessageTooLarge, closeMessage, CancellationToken.None);
return;
}
receiveResult = await socket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer, count, maxMessageSize - count), CancellationToken.None);
count += receiveResult.Count;
}
var receivedString = Encoding.UTF8.GetString(receiveBuffer, 0, count);
var echoString = "You said " + receivedString;
ArraySegment<byte> outputBuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(echoString));
await socket.SendAsync(outputBuffer, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
public bool IsReusable
{
get { return true; }
}
}
}
Hope it helped!
Ian's answer definitely was good, but I needed a loop process. The mutex was key for me. This is a working .net core 2 example based on his. I can't speak to scalability of this loop.
using System;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
namespace WebSocketServerConsole
{
public class Program
{
static HttpListener httpListener = new HttpListener();
private static Mutex signal = new Mutex();
public static void Main(string[] args)
{
httpListener.Prefixes.Add("http://localhost:8080/");
httpListener.Start();
while (signal.WaitOne())
{
ReceiveConnection();
}
}
public static async System.Threading.Tasks.Task ReceiveConnection()
{
HttpListenerContext context = await
httpListener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null);
WebSocket webSocket = webSocketContext.WebSocket;
while (webSocket.State == WebSocketState.Open)
{
await webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello world")),
WebSocketMessageType.Text, true, CancellationToken.None);
}
}
signal.ReleaseMutex();
}
}
}
and a test html page for it.
<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Test</title>
<script language="javascript" type="text/javascript">
var wsUri = "ws://localhost:8080/";
var output;
function init()
{
output = document.getElementById("output");
testWebSocket();
}
function testWebSocket()
{
websocket = new WebSocket(wsUri);
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) };
}
function onOpen(evt)
{
writeToScreen("CONNECTED");
doSend("WebSocket rocks");
}
function onClose(evt)
{
writeToScreen("DISCONNECTED");
}
function onMessage(evt)
{
writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
}
function onError(evt)
{
writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
}
function doSend(message)
{
writeToScreen("SENT: " + message);
websocket.send(message);
}
function writeToScreen(message)
{
var pre = document.createElement("p");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
output.appendChild(pre);
}
window.addEventListener("load", init, false);
</script>
<h2>WebSocket Test</h2>
<div id="output"></div>
Here is my complete working example...
Start up a host
namespace ConsoleApp1;
public static class Program
{
public static async Task Main(string[] args)
{
IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddSingleton<Server>();
services.AddHostedService<Server>();
});
IHost host = hostBuilder.Build();
await host.RunAsync();
}
}
Create a server to accept clients and talk to them
using ConsoleApp15.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Net;
using System.Net.WebSockets;
using System.Text;
namespace ConsoleApp15;
public class Server : IHostedService
{
private readonly ILogger<Server> Logger;
private readonly HttpListener HttpListener = new();
public Server(ILogger<Server> logger)
{
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
HttpListener.Prefixes.Add("http://localhost:8080/");
}
public async Task StartAsync(CancellationToken cancellationToken)
{
Logger.LogInformation("Started");
HttpListener.Start();
while (!cancellationToken.IsCancellationRequested)
{
HttpListenerContext? context = await HttpListener.GetContextAsync().WithCancellationToken(cancellationToken);
if (context is null)
return;
if (!context.Request.IsWebSocketRequest)
context.Response.Abort();
else
{
HttpListenerWebSocketContext? webSocketContext =
await context.AcceptWebSocketAsync(subProtocol: null).WithCancellationToken(cancellationToken);
if (webSocketContext is null)
return;
string clientId = Guid.NewGuid().ToString();
WebSocket webSocket = webSocketContext.WebSocket;
_ = Task.Run(async() =>
{
while (webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
{
await Task.Delay(1000);
await webSocket.SendAsync(
Encoding.ASCII.GetBytes($"Hello {clientId}\r\n"),
WebSocketMessageType.Text,
endOfMessage: true,
cancellationToken);
}
});
_ = Task.Run(async() =>
{
byte[] buffer = new byte[1024];
var stringBuilder = new StringBuilder(2048);
while (webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
{
WebSocketReceiveResult receiveResult =
await webSocket.ReceiveAsync(buffer, cancellationToken);
if (receiveResult.Count == 0)
return;
stringBuilder.Append(Encoding.ASCII.GetString(buffer, 0, receiveResult.Count));
if (receiveResult.EndOfMessage)
{
Console.WriteLine($"{clientId}: {stringBuilder}");
stringBuilder = new StringBuilder();
}
}
});
}
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
Logger.LogInformation("Stopping...");
HttpListener.Stop();
Logger.LogInformation("Stopped");
return Task.CompletedTask;
}
}
Create a WithCancellationToken for the Async methods that don't accept a CancellationToken parameter. This is so the server shuts down gracefully when told to.
namespace ConsoleApp15.Extensions;
public static class TaskExtensions
{
public static async Task<T?> WithCancellationToken<T>(this Task<T> source, CancellationToken cancellationToken)
{
var cancellationTask = new TaskCompletionSource<bool>();
cancellationToken.Register(() => cancellationTask.SetCanceled());
_ = await Task.WhenAny(source, cancellationTask.Task);
if (cancellationToken.IsCancellationRequested)
return default;
return source.Result;
}
}
Start the Postman app
File => New
Select "WebSocket request"
Enter the following as the url ws://localhost:8080/
Click [Connect]
Related
I'm learning to work with Blazor WebAssembly and SignalR. I want to have single WebSocket connection (therefore a single HubConnection instance) and share that between all the Blazor components that need to do something over SignalR WebSockets.
I want to call HubConnection.StreamAsync() and then loop over the generated stream as the data comes in (it's an endless stream). I also want it to cancel the stream when component is not shown anymore.
I have 2 problems with this idea:
Only one component is capable of streaming values. Other components get stuck on "await foreach" and never receive any items.
During component's dispose, i trigger the cancellation token, but the server side doesn't receive a cancellation. Only when i refresh the whole browser, the cancellation is triggered.
When i traverse to any other Blazor page and then return to the page where my streaming components are, now no component ever receives items.
I have a vague hunch that those 2 things are somehow connected - somehow on server side, a single hub can only really respond to single StreamAsync() call in single connection?
In the end, i've been unable to find a solution for this. What am i doing wrong? Maybe any of you could help me out on this?
Code
To reproduce, you may follow the instructions:
Start with Blazor WebAssembly with ASP.NET MVC backend project. I use .net core 6, most nugets are with version 6.0.7. I have also installed nuget Microsoft.AspNetCore.SignalR.Client on Blazor Client project.
Do the following changes/updates:
Client - App.razor
Add this code
#using Microsoft.AspNetCore.SignalR.Client
#implements IAsyncDisposable
#inject HubConnection HubConnection
...
#code {
private CancellationTokenSource cts = new();
protected override void OnInitialized()
{
base.OnInitialized();
HubConnection.Closed += error =>
{
return ConnectWithRetryAsync(cts.Token);
};
_ = ConnectWithRetryAsync(cts.Token);
}
private async Task<bool> ConnectWithRetryAsync(CancellationToken token)
{
// Keep trying to until we can start or the token is canceled.
while (true)
{
try
{
await HubConnection.StartAsync(token);
return true;
}
catch when (token.IsCancellationRequested)
{
return false;
}
catch
{
// Try again in a few seconds. This could be an incremental interval
await Task.Delay(5000);
}
}
}
public async ValueTask DisposeAsync()
{
cts.Cancel();
cts.Dispose();
await HubConnection.DisposeAsync();
}
}
Client - Program.cs
Add the following singleton to DI
builder.Services.AddSingleton(sp =>
{
var navMan = sp.GetRequiredService<NavigationManager>();
return new HubConnectionBuilder()
.WithUrl(navMan.ToAbsoluteUri("/string"))
.WithAutomaticReconnect()
.Build();
});
Client - Create a component called "StringDisplay"
#using Microsoft.AspNetCore.SignalR.Client
#inject HubConnection HubConnection
#implements IDisposable
#if(currentString == string.Empty)
{
<i>Loading...</i>
}
else
{
#currentString
}
#code {
private string currentString = string.Empty;
private CancellationTokenSource cts = new();
protected override void OnInitialized()
{
base.OnInitialized();
_ = Consumer();
}
protected override void OnParametersSet()
{
base.OnParametersSet();
_ = Consumer();
}
private async Task Consumer()
{
try
{
cts.Cancel();
cts.Dispose();
cts = new();
var stream = HubConnection.StreamAsync<string>("GetStrings", cts.Token);
await foreach(var str in stream)
{
if(cts.IsCancellationRequested)
break;
currentString = str;
StateHasChanged();
}
}
catch(Exception e)
{
Console.WriteLine(e);
}
}
public void Dispose()
{
cts.Cancel();
cts.Dispose();
}
}
Client - Index.razor
Add the StringDisplay component 3 times onto the page:
<hr />
<StringDisplay /><hr />
<StringDisplay /><hr />
<StringDisplay /><hr />
Server - Create StringGeneratorService.cs
namespace BlazorWebAssembly.Server.Services;
public class StringGeneratorService
{
private readonly PeriodicTimer _timer;
public event Action<string>? OnGenerated;
public StringGeneratorService()
{
_timer = new PeriodicTimer(TimeSpan.FromMilliseconds(200));
Task.Run(TimerRunnerAsync);
}
private async Task TimerRunnerAsync()
{
while (true)
{
await _timer.WaitForNextTickAsync();
var str = Guid.NewGuid().ToString();
OnGenerated?.Invoke(str);
}
}
}
Server - Create StringHub.cs
using BlazorWebAssembly.Server.Services;
using Microsoft.AspNetCore.SignalR;
using System.Runtime.CompilerServices;
namespace BlazorWebAssembly.Server.Hubs
{
public class StringHub : Hub
{
private readonly StringGeneratorService _generatorService;
public StringHub(StringGeneratorService generatorService)
{
_generatorService = generatorService;
}
public async IAsyncEnumerable<string> GetStrings([EnumeratorCancellation] CancellationToken cancellationToken)
{
using var flag = new AutoResetEvent(false);
string currentString = string.Empty;
var listener = (string str) => { currentString = str; flag.Set(); };
_generatorService.OnGenerated += listener;
cancellationToken.Register(() =>
{
_generatorService.OnGenerated -= listener;
});
while (!cancellationToken.IsCancellationRequested)
{
flag.WaitOne();
yield return currentString;
}
yield break;
}
}
}
Server - Program.cs
Register necessary parts
builder.Services.AddSingleton<StringGeneratorService>();
...
app.MapHub<StringHub>("/string");
I have looked through samples in the github repo, but when i develop my process, i get "Connection ID required" when accessing the route that is mapped to a custom ConnectionHandler. My log message is never printed, nor do i land in the implementation with the debugger.
Startup:
builder.Services.AddConnections();
app.UseEndpoints(endpoints =>
{
endpoints.MapConnectionHandler<CustomDelegationHandler>("/proxy/{id}");
});
Implementation:
public class CustomDelegationHandler : ConnectionHandler
{
private readonly ILogger<CustomDelegationHandler> _logger;
public CustomDelegationHandler(ILogger<CustomDelegationHandler> logger)
{
_logger = logger;
}
public override async Task OnConnectedAsync(ConnectionContext connection)
{
_logger.LogWarning("Connection incoming");
while (true)
{
var result = await connection.Transport.Input.ReadAsync();
var buffer = result.Buffer;
try
{
if (!buffer.IsEmpty)
{
var stream = new MemoryStream();
var data = buffer.ToArray();
await stream.WriteAsync(data, 0, data.Length);
stream.Position = 0;
}
else if (result.IsCompleted)
{
break;
}
}
finally
{
connection.Transport.Input.AdvanceTo(buffer.End);
}
}
}
}
You need add the connection like below and it will map the custom ConnectionHandler:
public async Task<IActionResult> Index()
{
var url = "https://localhost:yourPortNumber/proxy/1";
var connection = new HttpConnection(new Uri(url));
await connection.StartAsync();
var bytes = Encoding.UTF8.GetBytes("aaaa");
async Task SendMessage()
{
await connection.Transport.Output.WriteAsync(bytes);
}
// Send the receive concurrently so that back pressure is released
// for server -> client sends
await SendMessage();
return View();
}
Is it possible to trigger the below code by using a trigger URL?
As opposed to triggering by visiting the URL in the browser.
var context = listener.GetContext();
Something like this?
var triggerURL = "http://www.google.ie/";
var request = (HttpWebRequest)WebRequest.Create(triggerURL);
Or is it possible to use a do while loop? I.E do create trigger while get context
Instead of using listener.GetContext(), I was able to satisfy my requirement by using listener.BeginGetContext(new AsyncCallback(ListenerCallback), listener) and listener.EndGetContext(result), utilising the Asynchronous call, GetAsync.
public static string RunServerAsync(Action<string> triggerPost)
{
var triggerURL = "";
CommonCode(ref triggerURL);
if (listener.IsListening)
{
triggerPost(triggerURL);
}
while (listener.IsListening)
{
var context = listener.BeginGetContext(new AsyncCallback(ListenerCallback), listener);
context.AsyncWaitHandle.WaitOne(20000, true); //Stop listening after 20 seconds (20 * 1000).
listener.Close();
}
return plateString;
}
private static async void TriggerURL(string url)
{
var r = await DownloadPage(url);
}
static async Task<string> DownloadPage(string url)
{
using (var client = new HttpClient())
{
using (var r = await client.GetAsync(new Uri(url)))
{
if (r.IsSuccessStatusCode)
{
string result = await r.Content.ReadAsStringAsync();
return result;
}
else
{
return r.StatusCode.ToString();
}
}
}
}
private static void ListenerCallback(IAsyncResult result)
{
try
{
HttpListener listener = (HttpListener)result.AsyncState;
// Use EndGetContext to complete the asynchronous operation.
HttpListenerContext context = listener.EndGetContext(result);
if (context != null)
{
plateString = ProcessRequest(context);
}
else
{
plateString = "No response received!";
}
}
catch (Exception ex)
{
NLogManager.LogException(ex);
}
}
I am trying to use web socket with my bot to communicate with the server. But on run time it throws the System.NullReferenceException. I am running socket in background on a different thread so that it does not interfear with the bot.
I am using WebsocketSharp library.
First message comes in just fine but on second message it throws exception at following line in HumanCollaboratorDialog class.
await context.PostAsync(e.Data);
My Socket Stream Class is as following:
public static class SocketStream
{
public static WebSocket ws;
private static List<string> serverMsg = new List<string>();
public static void initializeSocket()
{
ws = new WebSocket("ws://Some IP:8080/human-collaborator/data");
Debug.WriteLine("****** INITIALIZED SOCKET (should happen only once) *****");
Task.Run(() => startSocketStream());
}
private static void startSocketStream()
{
int attempts = 0;
while (!ws.IsAlive)
{
try
{
attempts++;
ws.Connect();
}
catch (WebSocketException)
{
Debug.WriteLine("Connection attempts: " + attempts.ToString());
}
}
ws.OnOpen += (sender, args) =>
{
Debug.WriteLine("# SOCKET OPENED");
};
ws.OnError += (sender, args) =>
{
Debug.WriteLine("# SOME ERROR OCCURED");
};
ws.OnClose += (sender, args) =>
{
Debug.WriteLine("# SOCKET CLOSED");
};
}
}
I am calling the initializeSocket() method in Global.asx to run it on application level
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
SocketStream.initializeSocket();
}
}
My HumanCollaboratorDialog class is as following:
[Serializable]
public class HumanCollaboratorDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(this.MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
SocketStream.ws.OnMessage += async (sender, e) =>
{
try
{
await context.PostAsync(e.Data);
}
catch (HttpRequestException ex)
{
throw ex;
}
};
Thread.Sleep(500);
string output = message.Text;
SocketStream.ws.Send(output);
Thread.Sleep(500);
context.Wait(MessageReceivedAsync);
}
}
My MessagesController has following POST method:
public virtual async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
await Conversation.SendAsync(activity, () => new HumanCollaboratorDialog());
}
else
{
HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
Neithet e.Data nor context is empty. I think problem is with socket connection or may be i am doing something wrong in SocketStream class. following is the image
Your bot is a web service. Messages are sent to the service by the client (a web page, an application, another service, etc.) and received in the MessagesController's Post method. There's no need to have the socket code on the server for what you're trying to do. Web Sockets are useful for receiving messages on a client from the bot via a Direct Line connection.
Here is an example of using the Bot Framework's Direct Line Client and creating a web socket connection. Notice how the web socket is created from a conversation's StreamUrl:
DirectLineClientCredentials creds = new DirectLineClientCredentials(directLineSecret);
DirectLineClient directLineClient = new DirectLineClient(creds);
Conversation conversation = await directLineClient.Conversations.StartConversationAsync();
using (var webSocketClient = new WebSocket(conversation.StreamUrl))
{
webSocketClient.OnMessage += WebSocketClient_OnMessage;
webSocketClient.Connect();
while (true)
{
string input = Console.ReadLine().Trim();
if (input.ToLower() == "exit")
{
break;
}
else
{
if (input.Length > 0)
{
Activity userMessage = new Activity
{
From = new ChannelAccount(fromUser),
Text = input,
Type = ActivityTypes.Message
};
await directLineClient.Conversations.PostActivityAsync(conversation.ConversationId, userMessage);
}
}
}
}
private static void WebSocketClient_OnMessage(object sender, MessageEventArgs e)
{
// avoid null reference exception when no data received
if (string.IsNullOrWhiteSpace(e.Data))
{
return;
}
var activitySet = JsonConvert.DeserializeObject<ActivitySet>(e.Data);
var activities = from x in activitySet.Activities
where x.From.Id == botId
select x;
foreach (Activity activity in activities)
{
Console.WriteLine(activity.Text);
}
}
This is from a console application that is using the Direct Line to communicate with the Bot and is listening for messages using web sockets here:
https://github.com/Microsoft/BotBuilder-Samples/tree/master/CSharp/core-DirectLineWebSockets
I have this WCF self-hosted WebSocket service code:
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();
Interface:
namespace IWebSocketHostTest
{
[ServiceContract]
interface IWebSocketCallBack
{
[OperationContract(IsOneWay = true)]
void Send(int num);
}
[ServiceContract(CallbackContract = typeof(IWebSocketCallBack))]
public interface IWebSocket
{
[OperationContract]
void StartSend();
}
}
Service:
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();
}
}
}
This works perfect if i add a reference of this service in another .NET application.
But, what i need is to consume this service from an HTML+Javascript application, and i´m realy lost in how to do that. I couldn´t find a good example or tutorial with a Javascript client consuming a self-hosted WCF WebSocket service.
All the Javascript WebSocket code that i could find seems to be very simple, but i couldn´t make it work.
Here is my short JavaScript client test:
var ws = new WebSocket("ws://192.168.1.95:8080/service");
ws.onopen = function () {
console.log("WEBSOCKET CONNECTED");
};
it returns "WebSocket Error: Incorrect HTTP response. Status code 400, Bad Request" testing it with Fiddler.
What am i missing? Could you please give me some doc links to get more information or a code example?
Thank you!
EDIT:
Now i´ve tried using the "Microsoft.ServiceModel.WebSocket" library to try to make it work.
But, first, i don´t know if it´s still maintained by Microsoft or if it is deprecated, because i couldn´t find any information at MSDN and there is few info at internet.
And second, the "Open()" method of the "WebSocketHost" class is not found, so i don´t know how to make the server run...
Here is my code, i´ve taken it from a question at the ASP.NET forum.
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.");
}
}
}
But, like i said before, the "host.Open()" method is not found, so i don´t know if i´m missing some reference or what, because i couldn´t find info about the WebSocketHost class... Any help?
After spending a day with the same task I finally got working solution. Hope it will help someone in the future.
Client 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>
Self hosted server:
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
As of .net 4.5 new way of writing server side have emerged. The benefits are cleaner code and possibility to support secure web sockets (WSS) over 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
}