
通过ASP.NET Core开发WebSocket服务器:从零构建双向实时通信
你好,我是源码库的博主。在开发现代Web应用时,我们常常会遇到需要实时推送数据的场景,比如在线聊天室、实时数据仪表盘、协同编辑或是游戏。传统的HTTP请求-响应模式在这里就显得力不从心了。这时,WebSocket协议就成了我们的“利器”。今天,我就来手把手带你用ASP.NET Core搭建一个稳固的WebSocket服务器,并分享一些我实践中踩过的“坑”和心得。
WebSocket提供了全双工、长连接的通信通道,一旦握手建立,客户端和服务器就可以在任何时间互相发送数据,延迟极低。ASP.NET Core对WebSocket的支持非常友好,让我们可以专注于业务逻辑,而不用太操心底层的协议细节。
第一步:创建项目与基础配置
首先,我们创建一个新的ASP.NET Core Web API项目。你可以使用Visual Studio的向导,或者像我一样,在命令行里快速搞定:
dotnet new webapi -n WebSocketDemo
cd WebSocketDemo
接下来,我们需要显式地启用WebSocket支持。虽然在新版ASP.NET Core中,Kestrel服务器默认支持WebSocket,但在中间件管道中启用它仍然是必要步骤。打开 `Program.cs` 文件,在 `var app = builder.Build();` 之后,添加以下代码:
// ... builder.Build() 之后
app.UseWebSockets(); // 启用WebSocket中间件
// ... 之后才是 app.UseAuthorization() 和 app.MapControllers()
踩坑提示:`app.UseWebSockets()` 的调用顺序至关重要!它必须放在 `app.UseRouting()` 之后,但在 `app.UseEndpoints`(或像 `MapControllers` 这样的终端中间件)之前。如果顺序错了,WebSocket请求可能无法被正确拦截和处理。
第二步:创建自定义的WebSocket处理中间件
我们不打算把逻辑全塞在Controller里,创建一个独立的中间件会让结构更清晰,也更容易管理连接的生命周期。在项目根目录新建一个文件夹 `Middleware`,然后添加一个类 `WebSocketServerMiddleware.cs`。
这个中间件的核心是 `InvokeAsync` 方法。它会检查当前HTTP请求是否为WebSocket升级请求,如果是,则接受连接并开始通信循环。
using System.Net.WebSockets;
using System.Text;
namespace WebSocketDemo.Middleware;
public class WebSocketServerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public WebSocketServerMiddleware(RequestDelegate next, ILogger logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 1. 判断是否为WebSocket请求
if (context.WebSockets.IsWebSocketRequest)
{
_logger.LogInformation("收到WebSocket连接请求。");
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await HandleWebSocketConnection(webSocket);
}
else
{
// 如果不是WebSocket请求,交给管道中的下一个中间件
await _next(context);
}
}
private async Task HandleWebSocketConnection(WebSocket webSocket)
{
var buffer = new byte[1024 * 4]; // 4KB的缓冲区
try
{
// 2. 循环接收消息
while (webSocket.State == WebSocketState.Open)
{
var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
// 3. 处理收到的文本消息
string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
_logger.LogInformation($"服务器收到消息: {receivedMessage}");
// 4. 构造回复(这里简单做回声)
string echoMessage = $"Echo: {receivedMessage} at {DateTime.Now:HH:mm:ss}";
var echoBuffer = Encoding.UTF8.GetBytes(echoMessage);
await webSocket.SendAsync(new ArraySegment(echoBuffer),
WebSocketMessageType.Text,
true, // 消息结束标志
CancellationToken.None);
}
else if (result.MessageType == WebSocketMessageType.Close)
{
// 5. 处理客户端发来的关闭请求
_logger.LogInformation("收到关闭帧,准备关闭连接。");
if (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseReceived)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,
"Closed by the server",
CancellationToken.None);
}
break;
}
}
}
catch (WebSocketException ex)
{
_logger.LogError(ex, "WebSocket通信发生异常。");
// 尝试优雅关闭
if (webSocket.State == WebSocketState.Open)
{
await webSocket.CloseAsync(WebSocketCloseStatus.InternalServerError,
"Server error",
CancellationToken.None);
}
}
finally
{
webSocket.Dispose();
_logger.LogInformation("WebSocket连接已关闭并释放。");
}
}
}
实战经验:注意 `WebSocket.ReceiveAsync` 方法。它返回的 `WebSocketReceiveResult` 对象中的 `EndOfMessage` 属性很重要。对于文本或二进制消息,如果消息很大,可能会被分片传输。上面的例子为了简单,假设每次接收都是一个完整的消息(我们传递的缓冲区也足够大)。在生产环境中,你需要一个循环来累积数据,直到 `EndOfMessage` 为 `true`。
第三步:注册中间件并定义访问端点
中间件写好了,我们需要把它插入到请求管道中,并指定一个URL路径来监听WebSocket连接。回到 `Program.cs` 文件。
// ... 在 app.UseWebSockets(); 之后
// 将我们的自定义中间件映射到特定的路径,例如 “/ws”
app.Map("/ws", builder =>
{
builder.UseMiddleware();
});
// ... 其他的Map或Run配置
这样,所有发送到 `ws://yourdomain/ws` 的连接请求,都会被我们的 `WebSocketServerMiddleware` 处理。
第四步:编写一个简单的HTML客户端进行测试
服务器端完成了,我们得有个客户端来测试。在 `wwwroot` 文件夹下(如果没有就新建一个),创建一个 `index.html` 文件。为了快速测试,我们还需要启用静态文件服务。在 `Program.cs` 中 `app.UseRouting()` 之前添加:
app.UseDefaultFiles(); // 默认寻找 index.html
app.UseStaticFiles(); // 启用静态文件服务
然后,编写我们的测试客户端HTML:
WebSocket 客户端测试
ASP.NET Core WebSocket 测试
let socket = null;
const output = document.getElementById('output');
const messageInput = document.getElementById('messageInput');
const btnSend = document.getElementById('btnSend');
const btnDisconnect = document.getElementById('btnDisconnect');
function log(msg) {
output.innerHTML += `[${new Date().toLocaleTimeString()}] ${msg}n`;
}
function connect() {
// 注意协议是 ws 或 wss (对应 http 和 https)
const url = `ws://${window.location.host}/ws`;
socket = new WebSocket(url);
socket.onopen = function(e) {
log("连接已建立!");
btnDisconnect.disabled = false;
messageInput.disabled = false;
btnSend.disabled = false;
};
socket.onmessage = function(event) {
log(`收到服务器回复: ${event.data}`);
};
socket.onclose = function(event) {
log(`连接关闭,代码: ${event.code}, 原因: ${event.reason}`);
btnDisconnect.disabled = true;
messageInput.disabled = true;
btnSend.disabled = true;
socket = null;
};
socket.onerror = function(error) {
log(`WebSocket 错误: ${error.message}`);
};
}
function send() {
if (!socket || socket.readyState !== WebSocket.OPEN) {
alert("连接未就绪!");
return;
}
const message = messageInput.value;
if (!message.trim()) return;
log(`发送: ${message}`);
socket.send(message);
messageInput.value = '';
}
function disconnect() {
if (socket) {
socket.close(1000, "用户主动断开");
}
}
第五步:运行与进阶思考
现在,一切就绪!在命令行运行 `dotnet run`,然后用浏览器打开 `https://localhost:xxxx` (或 `http://localhost:xxxx`),点击“连接”按钮。在输入框里发送消息,你应该会立刻看到服务器的回声回复。
踩坑提示:如果你在部署到生产环境(如IIS、Nginx反向代理后面)时遇到连接问题,很可能是因为代理服务器没有正确配置以支持WebSocket。对于Nginx,需要在 `location` 块中添加 `proxy_set_header Upgrade $http_upgrade;` 和 `proxy_set_header Connection "upgrade";` 等指令。
这个示例是一个简单的起点。在实际项目中,你还需要考虑:
- 连接管理:使用一个静态的 `ConcurrentDictionary` 来存储所有活跃的 `WebSocket` 对象,以便实现广播或向特定客户端发送消息。
- 心跳与超时:定期发送Ping/Pong帧来保持连接活跃,并检测死连接。
- 身份验证与授权:在 `AcceptWebSocketAsync` 之前,从 `HttpContext` 中验证用户身份(如通过JWT Token)。
- 消息协议:定义更复杂的消息格式(如JSON),并包含消息类型、发送者、目标等信息。
希望这篇教程能帮你顺利打开ASP.NET Core实时通信的大门。WebSocket的世界很有趣,动手试试,遇到问题多查文档和社区,你一定能构建出强大的实时应用。编码愉快!

评论(0)