In this article, we will introduce RFC 6455 WebSocket specification in detail and configure a general. NET 5 Application to communicate with SignalR through WebSocket connection.
We'll go deep into the underlying concepts to understand what's happening at the bottom.
About WebSocket
WebSocket is introduced to realize two-way communication between client and server. One of the pain points of HTTP 1.0 is to create and close connections every time a request is sent to the server. However, in HTTP 1.1, persistent connection (RFC 2616) is introduced by using the persistent connection mechanism. In this way, the connection can be reused by multiple requests -- which reduces latency because the server knows that the client does not need to start during the handshake of each request.
WebSocket is built on the HTTP 1.1 specification because it allows persistent connections. Therefore, when you first create a WebSocket connection, it is essentially an HTTP 1.1 request (described in detail later). This enables real-time communication between the client and the server. Simply put, the following figure describes what happens during the initiation (handshake), data transmission, and closing of a WS connection. We will study these concepts in more depth later.
The protocol includes two parts: handshake and data transmission.
handshake
Let's start with a handshake.
Simply put, WebSocket connections are based on HTTP (and TCP as transport) on a single port. The following is a summary of these steps.
-
The server must listen for incoming TCP socket connections. This can be any port you assign - usually 80 or 443.
-
The client initiates a handshake through an HTTP GET request (otherwise the server will not know who to talk to) - this is the "Web" part of "WebSockets". In the message header, the client requests the server to upgrade the connection to WebSocket.
-
The server sends a handshake response telling the client that it will change the protocol from HTTP to WebSocket.
-
The client and server negotiate connection details. Either party can withdraw.
Here is a typical open (client) handshake request.
GET /ws-endpoint HTTP/1.1 Host: example.com:80 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: L4kHN+1Bx7zKbxsDbqgzHw== Sec-WebSocket-Version: 13
Notice how the client sends the Connection: Upgrade and Upgrade: websocket headers in the request.
Moreover, the server handshake response.
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: CTPN8jCb3BUjBjBtdjwSQCytuBo=
data transmission
The next key concept we need to understand is data transmission. Either party can send a message at any given time - because it is a full duplex communication protocol.
The message consists of one or more frames. The types of frames can be text (UTF-8), binary, and control frames (for example, 0x8 (Close), 0x9 (Ping), and 0xA (Pong)).
install
Let's take action and see how it works.
First, create an ASP.NET 5 WebAPI project.
dotnet new webapi -n WebSocketsTutorial dotnet new sln dotnet sln add WebSocketsTutorial
Now add SignalR to the project.
dotnet add WebSocketsTutorial/ package Microsoft.AspNet.SignalR
Sample code
We first add WebSockets middleware to our web API Application. Open Startup.cs and add the following code to the Configure method.
In this tutorial, I like to keep it simple. Therefore, I am not going to discuss SignalR. It will be based entirely on WebSocket communication. You can also use the original WebSockets to achieve the same function. If you want to make things easier, you don't need to use SignalR.
app.UseWebSockets();
Next, we will delete the default weatherforecast controller and add a new controller called WebSocketsController. Note that we will only use a controller action instead of intercepting the request pipeline.
The complete code of this controller is shown below.
using System; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace WebSocketsTutorial.Controllers{ [ApiController] [Route("[controller]")] public class WebSocketsController : ControllerBase { private readonly ILogger<WebSocketsController> _logger; public WebSocketsController(ILogger<WebSocketsController> logger) { _logger = logger; } [HttpGet("/ws")] public async Task Get() { if (HttpContext.WebSockets.IsWebSocketRequest) { using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); _logger.Log(LogLevel.Information, "WebSocket connection established"); await Echo(webSocket); } else { HttpContext.Response.StatusCode = 400; } } private async Task Echo(WebSocket webSocket) { var buffer = new byte[1024 * 4]; var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); _logger.Log(LogLevel.Information, "Message received from Client"); while (!result.CloseStatus.HasValue) { var serverMsg = Encoding.UTF8.GetBytes($"Server: Hello. You said: {Encoding.UTF8.GetString(buffer)}"); await webSocket.SendAsync(new ArraySegment<byte>(serverMsg, 0, serverMsg.Length), result.MessageType, result.EndOfMessage, CancellationToken.None); _logger.Log(LogLevel.Information, "Message sent to Client"); result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); _logger.Log(LogLevel.Information, "Message received from Client"); } await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); _logger.Log(LogLevel.Information, "WebSocket connection closed"); } } }
This is what we do.
1. Add a new route named ws /.
2. Check whether the current request passes through WebSockets, otherwise 400 will be thrown.
3. Wait until the client initiates the request.
4. Enter a loop until the client closes the connection.
5. In the loop, we will send the "server: Hello. You said: < client's message >" message and send it back to the client.
6. Wait until the client sends another request.
Note that after the initial handshake, the server does not need to wait for the client to send a request to push the message to the client. Let's run the application and see if it works.
dotnet run --project WebSocketsTutorial
After running the application, visit https://localhost:5001/swagger/index.html , you should see the Swagger UI.
Now we'll see how to make the client and server communicate with each other. In this demo, I will use Chrome's devtools (open new tab → check or press F12 → console tab). However, you can select any client.
First, we will create a WebSocket connection to the server endpoint.
let webSocket = new WebSocket('wss://localhost:5001/ws');
What it does is initiate a connection between the client and the server. wss: / / is a WebSockets security protocol, because our web API application is served through TLS.
You can then send the message by calling the webSocket.send() method. Your console should be similar to the console below.
Let's take a closer look at WebSocket connections
If you go to the Network tab, filter out the request through the WS tab and click the last request called WS.
Click the Messages tab and check the Messages that are passed back and forth. During this period, if you call the following command, you will be able to see "This was sent from the Client!". Try it!
webSocket.send("Client: Hello");
As you can see, the server does need to wait for the client to send a response (i.e. after the initial handshake), and the client can send messages without being blocked. This is full duplex communication. We have discussed the data transmission aspect of WebSocket communication. As an exercise, you can run a loop to push the message to the client to see how it works.
In addition, the server and client can ping pong to see if the client is still alive. This is a practical feature in WebSockets! If you really want to see these packets, you can use tools like WireShark to understand them.
How does it shake hands? Well, if you jump to the Headers tab, you will be able to see the request response title we talked about in the first part of this article.
You can also try webSocket.close(), so that we can completely cover the open data close loop.
conclusion
If you are interested in the RFC of WebSocket, please visit RFC 6455 and read it. This article only touches the surface of WebSocket. There are many other things we can discuss, such as security, load balancing, proxy and so on.
Using websocket in. net core
It feels like a long holiday. Post a simple example of websocket.
Overall file structure
- project(WebSockets.Test) |-- Extensions | |-- SocketsExtension.cs |-- Handlers | |-- WebSocketMessageHandler.cs |-- SocketsManager | |-- SocketsHandler.cs | |-- SocketsManager.cs | |-- SocketsMiddleware.cs |-- Program.cs |-- Startup.cs
These files are generally required. This is the most basic example and can be modified as needed.
1. Create a class to save WebSocket
This class is used to save all WebSocket s.
// File: SocketsManager/SocketsManager.cs using System; using System.Collections.Concurrent; using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; namespace WebSockets.Test.SocketsManager { public class SocketsManager { private readonly ConcurrentDictionary<string, WebSocket> _connections = new ConcurrentDictionary<string, WebSocket>(); /// <summary> ///Gets the dictionary collection of all sockets /// </summary> /// <returns></returns> public ConcurrentDictionary<string, WebSocket> GetAllConnections() { return _connections; } /// <summary> ///Gets the socket of the specified id /// </summary> /// <param name="id"></param> /// <returns></returns> public WebSocket GetSocketById(string id) { return _connections.FirstOrDefault(x => x.Key == id).Value; } /// <summary> ///Get its id according to socket /// </summary> /// <param name="socket"></param> /// <returns></returns> public string GetId(WebSocket socket) { return _connections.FirstOrDefault(x => x.Value == socket).Key; } /// <summary> ///Delete the socket with the specified id and close the link /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task RemoveSocketAsync(string id) { _connections.TryRemove(id, out var socket); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "socket connection closed", CancellationToken.None); } /// <summary> ///Add a socket /// </summary> /// <param name="socket"></param> public void AddSocket(WebSocket socket) { _connections.TryAdd(CreateId(), socket); } /// <summary> ///Create id /// </summary> /// <returns></returns> private string CreateId() { return Guid.NewGuid().ToString("N"); } } }
2. Create a base class for managing and operating WebSocket s
This class is designed to handle the connection and disconnection of socket s, and receive and send messages. It belongs to the base class.
// File: SocketsManager/SocketsHandle.cs using System; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace WebSockets.Test.SocketsManager { public abstract class SocketsHandler { protected SocketsHandler(SocketsManager sockets) { Sockets = sockets; } public SocketsManager Sockets { get; set; } /// <summary> ///Connect a socket /// </summary> /// <param name="socket"></param> /// <returns></returns> public virtual async Task OnConnected(WebSocket socket) { await Task.Run(() => { Sockets.AddSocket(socket); }); } /// <summary> ///Disconnect the specified socket /// </summary> /// <param name="socket"></param> /// <returns></returns> public virtual async Task OnDisconnected(WebSocket socket) { await Sockets.RemoveSocketAsync(Sockets.GetId(socket)); } /// <summary> ///Send a message to the specified socket /// </summary> /// <param name="socket"></param> /// <param name="message"></param> /// <returns></returns> public async Task SendMessage(WebSocket socket, string message) { if (socket.State != WebSocketState.Open) return; await socket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(message)), WebSocketMessageType.Text, true, CancellationToken.None); } /// <summary> ///Send a message to the socket with the specified id /// </summary> /// <param name="id"></param> /// <param name="message"></param> /// <returns></returns> public async Task SendMessage(string id, string message) { await SendMessage(Sockets.GetSocketById(id), message); } /// <summary> ///Send messages to all sockets /// </summary> /// <param name="message"></param> /// <returns></returns> public async Task SendMessageToAll(string message) { foreach (var connection in Sockets.GetAllConnections()) await SendMessage(connection.Value, message); } /// <summary> ///Message received /// </summary> /// <param name="socket"></param> /// <param name="result"></param> /// <param name="buffer"></param> /// <returns></returns> public abstract Task Receive(WebSocket socket, WebSocketReceiveResult result, byte[] buffer); } }
3. Middleware for creating WebSocket
// File: SocketsManager/SocketsMiddleware.cs using System; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace WebSockets.Test.SocketsManager { public class SocketsMiddleware { private readonly RequestDelegate _next; public SocketsMiddleware(RequestDelegate next, SocketsHandler handler) { _next = next; Handler = handler; } private SocketsHandler Handler { get; } public async Task InvokeAsync(HttpContext context) { if (context.WebSockets.IsWebSocketRequest) { // Convert the current connection to a ws connection var socket = await context.WebSockets.AcceptWebSocketAsync(); await Handler.OnConnected(socket); // buffer for receiving messages var buffer = new byte[1024 * 4]; // Determine the connection type and perform corresponding operations while (socket.State == WebSocketState.Open) { // After this sentence is executed, the buffer is the body of the received message, which can be converted as needed. var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); switch (result.MessageType) { case WebSocketMessageType.Text: await Handler.Receive(socket, result, buffer); break; case WebSocketMessageType.Close: await Handler.OnDisconnected(socket); break; case WebSocketMessageType.Binary: break; default: throw new ArgumentOutOfRangeException(); } } } else { await _next(context); } } } }
4. Create WebSocket management subclass
Multiple can be created for personalization, mainly because the abstract method of receiving is set above, so the Receive method must be overridden. If you don't need it, you can remove the abstraction of the base class and write it directly in the base class.
In order to show the effect, the message prompt when joining and leaving is added. At the same time, the received messages are forwarded directly to everyone.
// File: Handlers/WebSocketMessageHandler.cs using System.Net.WebSockets; using System.Text; using System.Threading.Tasks; using WebSockets.Test.SocketsManager; namespace WebSockets.Test.Handlers { public class WebSocketMessageHandler : SocketsHandler { public WebSocketMessageHandler(SocketsManager.SocketsManager sockets) : base(sockets) { } public override async Task OnConnected(WebSocket socket) { await base.OnConnected(socket); var socketId = Sockets.GetId(socket); await SendMessageToAll($"{socketId}Joined"); } public override async Task OnDisconnected(WebSocket socket) { await base.OnDisconnected(socket); var socketId = Sockets.GetId(socket); await SendMessageToAll($"{socketId}I am leaving"); } public override async Task Receive(WebSocket socket, WebSocketReceiveResult result, byte[] buffer) { var socketId = Sockets.GetId(socket); var message = $"{socketId} Message sent:{Encoding.UTF8.GetString(buffer, 0, result.Count)}"; await SendMessageToAll(message); } } }
5. Create injection extension
It's OK to write directly in Startup.cs, but it's a good habit to write each injected content to a file separately.
// File: Extensions/SocketsExtension.cs using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using WebSockets.Test.SocketsManager; namespace WebSockets.Test.Extensions { public static class SocketsExtension { public static IServiceCollection AddWebSocketManager(this IServiceCollection services) { services.AddTransient<SocketsManager.SocketsManager>(); var exportedTypes = Assembly.GetEntryAssembly()?.ExportedTypes; if (exportedTypes == null) return services; foreach (var type in exportedTypes) if (type.GetTypeInfo().BaseType == typeof(SocketsHandler)) services.AddSingleton(type); return services; } public static IApplicationBuilder MapSockets(this IApplicationBuilder app, PathString path, SocketsHandler socket) { return app.Map(path, x => x.UseMiddleware<SocketsMiddleware>(socket)); } } }
6. Configure Startup.cs
Inject the above contents into the startup item.
Add in ConfigureServices:
services.AddWebSocketManager();
Then add in Configure:
app.UseWebSockets(); app.MapSockets("/ws", serviceProvider.GetService<WebSocketMessageHandler>());
Just.
If you are prompted that the serviceProvider cannot be found, add the following to the parameters of Configure:
IServiceProvider serviceProvider
Just.
The complete contents are as follows:
public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddWebSocketManager(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseWebSockets(); // Configuration path app.MapSockets("/ws", serviceProvider.GetService<WebSocketMessageHandler>()); app.UseStaticFiles(); }
7. Testing
The above has been completed. Now you can run. For testing, write the simplest page.
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WebSocket web client</title> </head> <body> <h1>WebSocket Web Client</h1> <br /> <input type="text" placeholder="enter your message" id="message"> <button id="sendBtn">Send</button> <ul id="messageList"></ul> <script> // Modify according to the actual address and port, and other contents do not need to be modified const uri = "ws://localhost:5000/ws"; socket = new WebSocket(uri); socket.onopen = function (e) { console.log("websocket estabished!"); } socket.onclose = function (e) { console.log('websocket closed!'); } socket.onmessage = function (e) { appendItem(list, e.data); console.log(e.data); } const list = document.getElementById("messageList"); const btn = document.getElementById("sendBtn"); btn.addEventListener("click", function () { console.log("sending message~~~"); var messgae = document.getElementById("message"); socket.send(message.value) }) function appendItem(list, message) { const li = document.createElement("li"); li.appendChild(document.createTextNode(message)); list.appendChild(li); } </script> </body> </html>
Test effect:
With the above example, the simplest chat room model can be implemented.
WebSocketsSample of. NET Core learning notes
1. Server
The code is as follows:
Program:
using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; namespace WebSocketsServer { public class Program { public static void Main(string[] args) { BuildWebHost(args).Run(); } public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .Build(); } }
Startup:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace WebSocketsServer { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // configure keep alive interval, receive buffer size app.UseWebSockets(); app.Map("/samplesockets", app2 => { // middleware to handle websocket request app2.Use(async (context, next) => { if (context.WebSockets.IsWebSocketRequest) { var webSocket = await context.WebSockets.AcceptWebSocketAsync(); await SendMessagesAsync(context, webSocket, loggerFactory.CreateLogger("SendMessages")); } else { await next(); } }); }); app.Run(async (context) => { await context.Response.WriteAsync("Web Sockets sample"); }); } private async Task SendMessagesAsync(HttpContext context, WebSocket webSocket, ILogger logger) { var buffer = new byte[4096]; WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); while (!result.CloseStatus.HasValue) { if (result.MessageType == WebSocketMessageType.Text) { string content = Encoding.UTF8.GetString(buffer, 0, result.Count); if (content.StartsWith("REQUESTMESSAGES:")) { string message = content.Substring("REQUESTMESSAGES:".Length); for (int i = 0; i < 10; i++) { string messageToSend = $"{message} - {i}"; if (i == 9) { messageToSend += ";EOS"; // send end of sequence to not let the client wait for another message } byte[] sendBuffer = Encoding.UTF8.GetBytes(messageToSend); await webSocket.SendAsync(new ArraySegment<byte>(sendBuffer), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None); logger.LogDebug("sent message {0}", messageToSend); await Task.Delay(1000); } } if (content.Equals("SERVERCLOSE")) { await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Bye for now", CancellationToken.None); logger.LogDebug("client sent close request, socket closing"); return; } else if (content.Equals("SERVERABORT")) { context.Abort(); } } result = await webSocket.ReceiveAsync(buffer, CancellationToken.None); } } } }
launchSettings.json
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:58167/", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WebSocketsServer": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:58168/" } } }
2. Client
Program.cs
The code is as follows:
using System; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace WebSocketClient { class Program { static async Task Main() { Console.WriteLine("Client - wait for server"); Console.ReadLine(); await InitiateWebSocketCommunication("ws://localhost:58167/samplesockets"); //"ws://localhost:6295/samplesockets" //http://localhost:58167/ Console.WriteLine("Program end"); Console.ReadLine(); } static async Task InitiateWebSocketCommunication(string address) { try { var webSocket = new ClientWebSocket(); await webSocket.ConnectAsync(new Uri(address), CancellationToken.None); await SendAndReceiveAsync(webSocket, "A"); await SendAndReceiveAsync(webSocket, "B"); await webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes("SERVERCLOSE")), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None); var buffer = new byte[4096]; var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); Console.WriteLine($"received for close: " + $"{result.CloseStatus} " + $"{result.CloseStatusDescription} " + $"{Encoding.UTF8.GetString(buffer, 0, result.Count)}"); await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Bye", CancellationToken.None); } catch (Exception ex) { Console.WriteLine(ex.Message); } } static async Task SendAndReceiveAsync(WebSocket webSocket, string term) { byte[] data = Encoding.UTF8.GetBytes($"REQUESTMESSAGES:{term}"); var buffer = new byte[4096]; await webSocket.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None); WebSocketReceiveResult result; bool sequenceEnd = false; do { result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); string dataReceived = Encoding.UTF8.GetString(buffer, 0, result.Count); Console.WriteLine($"received {dataReceived}"); if (dataReceived.Contains("EOS")) { sequenceEnd = true; } } while (!(result?.CloseStatus.HasValue ?? false) && !sequenceEnd); } } }