Введение: Почему WebSocket — стандарт для real-time приложений?
Современные веб-приложения требуют мгновенной реакции на действия пользователя. Будь то онлайн-чат, биржевой терминал, многопользовательская игра или система мониторинга — задержки в передаче данных неприемлемы. Традиционный протокол HTTP, работающий по принципу "запрос-ответ", неэффективен для таких сценариев: клиенту приходится постоянно опрашивать сервер (polling), создавая избыточную нагрузку.
На помощь приходит WebSocket — протокол, обеспечивающий постоянное двустороннее соединение между клиентом и сервером. В этой статье мы разберем, как создавать real-time приложения на C# (.NET) с использованием WebSocket, от простого эхо-сервера до полноценного чата. Вы узнаете, как работает протокол, какие инструменты предоставляет платформа .NET, и как избежать типичных ошибок.
Основы WebSocket в .NET: Серверная часть
Платформа .NET предлагает встроенную поддержку WebSocket через пространство имен System.Net.WebSockets. Рассмотрим создание сервера на базе ASP.NET Core.
1. Настройка Middleware для WebSocket
Для начала необходимо добавить поддержку WebSocket в конвейер обработки запросов. В файле Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseWebSockets(); // Включаем поддержку WebSocket
app.Map("/ws", async (HttpContext context) =>
{
if (context.WebSockets.IsWebSocketRequest)
{
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Echo(webSocket);
}
else
{
context.Response.StatusCode = 400;
}
});
app.Run("http://localhost:5000");
async Task Echo(WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
var receiveResult = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
while (!receiveResult.CloseStatus.HasValue)
{
// Отправляем полученные данные обратно клиенту (эхо)
await webSocket.SendAsync(
new ArraySegment<byte>(buffer, 0, receiveResult.Count),
receiveResult.MessageType,
receiveResult.EndOfMessage,
CancellationToken.None);
receiveResult = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(
receiveResult.CloseStatus.Value,
receiveResult.CloseStatusDescription,
CancellationToken.None);
}
Этот код создает эхо-сервер: все, что отправляет клиент, возвращается обратно. Обратите внимание на использование async/await — работа с WebSocket всегда асинхронна.
2. Управление несколькими подключениями
В реальном приложении нужно управлять множеством соединений. Создадим простой менеджер подключений:
public class WebSocketConnectionManager
{
private readonly ConcurrentDictionary<string, WebSocket> _connections = new();
public string AddConnection(WebSocket socket)
{
var id = Guid.NewGuid().ToString();
_connections.TryAdd(id, socket);
return id;
}
public async Task BroadcastMessageAsync(string message)
{
var tasks = _connections.Values.Select(async socket =>
{
if (socket.State == WebSocketState.Open)
{
var bytes = Encoding.UTF8.GetBytes(message);
await socket.SendAsync(
new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
true,
CancellationToken.None);
}
});
await Task.WhenAll(tasks);
}
}
Этот класс позволяет хранить все активные соединения и рассылать сообщения всем подключенным клиентам.
Создание клиента WebSocket на C#
Хотя чаще всего клиентом выступает браузер (JavaScript), иногда необходимо реализовать клиент на C# — например, для десктопного приложения или серверного агрегатора данных.
Пример клиента на .NET
using System.Net.WebSockets;
using System.Text;
class WebSocketClient
{
public static async Task Main()
{
using var ws = new ClientWebSocket();
await ws.ConnectAsync(new Uri("ws://localhost:5000/ws"), CancellationToken.None);
Console.WriteLine("Подключено к серверу. Введите сообщение:");
var sendTask = Task.Run(async () =>
{
while (true)
{
var message = Console.ReadLine();
if (string.IsNullOrEmpty(message)) break;
var bytes = Encoding.UTF8.GetBytes(message);
await ws.SendAsync(
new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
true,
CancellationToken.None);
}
});
var receiveTask = Task.Run(async () =>
{
var buffer = new byte[1024 * 4];
while (ws.State == WebSocketState.Open)
{
var result = await ws.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await ws.CloseAsync(
WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
break;
}
var text = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"Сервер: {text}");
}
});
await Task.WhenAll(sendTask, receiveTask);
}
}
Клиент одновременно отправляет сообщения с консоли и получает ответы от сервера. Это реализует полноценный дуплексный обмен данными.
Real-time чат: Собираем всё вместе
Объединим полученные знания и создадим простой чат. Сервер будет хранить имена пользователей и транслировать сообщения всем подключенным клиентам.
Серверная часть чата
public class ChatWebSocketHandler
{
private static readonly ConcurrentDictionary<string, (WebSocket Socket, string Name)> _users = new();
public static async Task HandleConnection(WebSocket socket)
{
var userId = Guid.NewGuid().ToString();
_users.TryAdd(userId, (socket, "User_" + userId[..5]));
// Отправляем приветственное сообщение
await SendMessage(socket, new { type = "system", message = "Добро пожаловать в чат!" });
var buffer = new byte[1024 * 4];
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
var json = Encoding.UTF8.GetString(buffer, 0, result.Count);
var data = JsonSerializer.Deserialize<ChatMessage>(json);
if (data?.type == "message")
{
var broadcast = new
{
type = "message",
user = _users[userId].Name,
text = data.text,
time = DateTime.Now.ToShortTimeString()
};
await Broadcast(JsonSerializer.Serialize(broadcast));
}
result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
// Удаляем пользователя