ASP.NET Core 中的 WebSocket 支持
WebSocket
HTML5 开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议,基于 TCP 传输协议,并复用 HTTP 的握手通道。
特点:全双工通讯,服务器可以主动向客户推送消息,客户也可以主动向服务器发送消息,支持二进制通信。
WebSocket 属性
Socket.readyState
0 - 表示连接尚未建立。
1 - 表示连接已建立,可以进行通信。
2 - 表示连接正在进行关闭。
3 - 表示连接已经关闭或者连接不能打开
WebSocket 事件
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发
WebSocket 方法
Socket.send():使用连接发送数据
Socket.close():关闭连接
WS: WebSocket
WSS: WebSocket Secure
WS是非安全的,WSS是安全的。非安全的没有证书,安全的需要SSL证书。
WS的体现形式是TCP + WS AS WS ,WSS的体现形式是TCP + TLS + WS AS WSS。
WS一般默认是80端口,而WSS默认是443端口,大多数网站用的就是80和433端口。
http协议下使用ws,https协议下使用wss。
webSocket = new WebSocket("ws://localhost:port/hub");
webSocket = new WebSocket("wss://localhost:port/hub");
1、配置中间件
app.UseWebSockets();
KeepAliveInterval - 向客户端发送“ping”帧的频率,以确保代理保持连接处于打开状态。 默认值为 2 分钟。
AllowedOrigins - 用于 WebSocket 请求的允许的 Origin 标头值列表。 默认情况下,允许使用所有源。 有关详细信息,请参阅以下“WebSocket 源限制”。
2、Startup.cs
namespace WebAPI
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
var webSocketOptions = new WebSocketOptions()
{
KeepAliveInterval = TimeSpan.FromSeconds(120),
};
app.UseWebSockets(webSocketOptions);
app.Use(async (context, next) =>
{
if (context.WebSockets.IsWebSocketRequest)
{
using (IServiceScope scope = app.ApplicationServices.CreateScope())
{
//do something
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Echo(context, webSocket);
}
}
else
{
//Hand over to the next middleware
await next();
}
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
private async Task Echo(HttpContext context, WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
}
}
NET 6
Program.cs
//加入WebSocket處理服務
builder.Services.AddSingleton<WebSocketHandler>();
#region WebSocket 方式1
app.UseWebSockets();
app.UseMiddleware<WebSocketHandler>();
#endregion
#region WebSocket 方式2
//加入 WebSocket 功能
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30)
});
// Middleware 处理
app.Use(async (context, next) =>
{
if (context.Request.Path == "/ws")
{
if (context.WebSockets.IsWebSocketRequest)
{
using (WebSocket ws = await context.WebSockets.AcceptWebSocketAsync())
{
var wsHandler = context.RequestServices.GetRequiredService<WebSocketHandler>();
await wsHandler.ProcessWebSocket(ws);
}
}
else
context.Response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest;
}
else await next();
});
#endregion
WebSocketHandler.cs
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Net.WebSockets;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace NurseCall.WebAPI
{
public class WebSocketHandler
{
ConcurrentDictionary<int, WebSocket> WebSockets = new ConcurrentDictionary<int, WebSocket>();
private readonly ILogger<WebSocketHandler> logger;
public WebSocketHandler(ILogger<WebSocketHandler> logger)
{
this.logger = logger;
}
public async Task ProcessWebSocket(WebSocket webSocket)
{
WebSockets.TryAdd(webSocket.GetHashCode(), webSocket);
var buffer = new byte[1024 * 4];
var res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
var userName = "anonymous";
while (!res.CloseStatus.HasValue)
{
var cmd = Encoding.UTF8.GetString(buffer, 0, res.Count);
if (!string.IsNullOrEmpty(cmd))
{
logger.LogInformation(cmd);
if (cmd.StartsWith("/USER "))
userName = cmd.Substring(6);
else
Broadcast($"{userName}:\t{cmd}");
}
res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(res.CloseStatus.Value, res.CloseStatusDescription, CancellationToken.None);
WebSockets.TryRemove(webSocket.GetHashCode(), out var removed);
Broadcast($"{userName} left the room.");
}
public void Broadcast(string message)
{
var buff = Encoding.UTF8.GetBytes($"{DateTime.Now:MM-dd HH:mm:ss}\t{message}");
var data = new ArraySegment<byte>(buff, 0, buff.Length);
Parallel.ForEach(WebSockets.Values, async (webSocket) =>
{
if (webSocket.State == WebSocketState.Open)
await webSocket.SendAsync(data, WebSocketMessageType.Text, true, CancellationToken.None);
});
}
}
}
3、html页面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<style>
table {
border: 0
}
.commslog-data {
font-family: Consolas, Courier New, Courier, monospace;
}
.commslog-server {
background-color: red;
color: white
}
.commslog-client {
background-color: green;
color: white
}
</style>
</head>
<body>
<h1>WebSocket测试页</h1>
<p id="stateLabel">准备连接...</p>
<div>
<label for="connectionUrl">WebSocket服务器URL:</label>
<input id="connectionUrl" />
<button id="connectButton" type="submit">连接</button>
</div>
<div>
<label for="sendMessage">消息发送:</label>
<input id="sendMessage" disabled />
<button id="sendButton" type="submit" disabled>发送</button>
<button id="closeButton" disabled>关闭Socket</button>
</div>
<p>注意:当连接到默认服务器(即地址栏中的服务器;)时,消息“ ServerClose”将导致服务器关闭连接。同样,消息“ ServerAbort”将导致服务器在不关闭握手的情况下强行终止连接</p>
<h2>通讯日志</h2>
<table style="width: 800px">
<thead>
<tr>
<td style="width: 100px">发送</td>
<td style="width: 100px">接收</td>
<td>数据</td>
</tr>
</thead>
<tbody id="commsLog"></tbody>
</table>
<script>
var connectionUrl = document.getElementById("connectionUrl");
var connectButton = document.getElementById("connectButton");
var stateLabel = document.getElementById("stateLabel");
var sendMessage = document.getElementById("sendMessage");
var sendButton = document.getElementById("sendButton");
var commsLog = document.getElementById("commsLog");
var socket;
var scheme = document.location.protocol == "https:" ? "wss" : "ws";
var port = document.location.port ? (":" + document.location.port) : "";
connectionUrl.value = scheme + "://" + document.location.hostname + port;
function updateState() {
function disable() {
sendMessage.disabled = true;
sendButton.disabled = true;
closeButton.disabled = true;
}
function enable() {
sendMessage.disabled = false;
sendButton.disabled = false;
closeButton.disabled = false;
}
connectionUrl.disabled = true;
connectButton.disabled = true;
if (!socket) {
disable();
} else {
switch (socket.readyState) {
case WebSocket.CLOSED:
stateLabel.innerHTML = "关闭";
disable();
connectionUrl.disabled = false;
connectButton.disabled = false;
break;
case WebSocket.CLOSING:
stateLabel.innerHTML = "关闭中...";
disable();
break;
case WebSocket.CONNECTING:
stateLabel.innerHTML = "连接中...";
disable();
break;
case WebSocket.OPEN:
stateLabel.innerHTML = "打开";
enable();
break;
default:
stateLabel.innerHTML = "未知的WebSocket状态: " + socket.readyState;
disable();
break;
}
}
}
closeButton.onclick = function () {
if (!socket || socket.readyState != WebSocket.OPEN) {
alert("socket没有连接");
}
socket.close(1000, "从客户端关闭");
}
sendButton.onclick = function () {
if (!socket || socket.readyState != WebSocket.OPEN) {
alert("socket未连接");
}
var data = sendMessage.value;
socket.send(data);
commsLog.innerHTML += '<tr>' +
'<td class="commslog-client">客户端</td>' +
'<td class="commslog-server">服务端</td>' +
'<td class="commslog-data">' + data + '</td>'
'</tr>';
}
connectButton.onclick = function () {
stateLabel.innerHTML = "连接中";
socket = new WebSocket(connectionUrl.value);
socket.onopen = function (event) {
updateState();
commsLog.innerHTML += '<tr>' +
'<td colspan="3" class="commslog-data">连接已打开</td>' +
'</tr>';
};
socket.onclose = function (event) {
updateState();
commsLog.innerHTML += '<tr>' +
'<td colspan="3" class="commslog-data">Connection closed. Code: ' + event.code + '. Reason: ' + event.reason + '</td>' +
'</tr>';
};
socket.onerror = updateState;
socket.onmessage = function (event) {
commsLog.innerHTML += '<tr>' +
'<td class="commslog-server">服务端</td>' +
'<td class="commslog-client">客户端</td>' +
'<td class="commslog-data">' + event.data + '</td>'
'</tr>';
};
};
</script>
</body>
</html>
*