Долгий звонок или вечный канал: как C# выводит данные в реальное время
Представьте: вы пишете чат на C#. Или биржевой терминал. Или систему уведомлений. Клиент ждет данных — и ждать долго не готов. Раньше разработчики выкручивались как могли: ставили таймеры, дергали сервер каждые 5 секунд, плодили лишние запросы. Сегодня у нас есть два цивилизованных подхода — Long Polling и WebSocket. Оба позволяют серверу отправлять данные клиенту без его явного запроса. Но работают они принципиально по-разному.
В этой статье разберем, как реализовать оба подхода на C# (ASP.NET Core), сравним их по скорости, нагрузке и сложности, и поймем, что выбрать для вашего проекта. Поехали.
Что такое Long Polling? (И почему это не волшебство)
Long Polling — это эмуляция реального времени поверх обычного HTTP. Клиент посылает запрос на сервер, но сервер не отвечает сразу. Он держит соединение открытым до тех пор, пока не появится новое событие (или не истечет таймаут). Как только данные готовы — сервер отправляет ответ, клиент его обрабатывает и тут же открывает новый «долгий» запрос.
Это хитрый трюк, который работает на любом HTTP-сервере и в любом браузере. Но у него есть цена: каждое соединение висит в памяти сервера, а если клиентов тысячи — это уже проблема.
Пример Long Polling на C# (ASP.NET Core)
Напишем простой контроллер, который симулирует очередь уведомлений. Клиент дергает эндпоинт, сервер ждет новые данные (например, из канала Channel<string>) и отправляет их.
using Microsoft.AspNetCore.Mvc;
using System.Threading.Channels;
[ApiController]
[Route("api/[controller]")]
public class NotificationsController : ControllerBase
{
private static readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
// Эндпоинт для публикации уведомления (например, из другого сервиса)
[HttpPost("publish")]
public async Task<IActionResult> Publish([FromBody] string message)
{
await _channel.Writer.WriteAsync(message);
return Ok("Уведомление отправлено");
}
// Long Polling: клиент ждет, пока не появится новое сообщение
[HttpGet("long-poll")]
public async Task<IActionResult> LongPoll(CancellationToken cancellationToken)
{
// Ждем новое сообщение; если ничего нет — ждем до 30 секунд
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(30));
try
{
var message = await _channel.Reader.ReadAsync(cts.Token);
return Ok(new { data = message });
}
catch (OperationCanceledException)
{
// Таймаут — возвращаем пустой ответ, клиент повторит запрос
return Ok(new { data = (string?)null });
}
}
}
Клиент (JavaScript) будет выглядеть так:
async function startLongPoll() {
while (true) {
const response = await fetch('/api/notifications/long-poll');
const result = await response.json();
if (result.data) {
console.log('Новое уведомление:', result.data);
}
// Сразу начинаем новый poll
}
}
startLongPoll();
Всё просто. Но есть нюанс: если сервер перезагрузится или клиент потеряет соединение — данные потеряются. Long Polling не гарантирует доставку.
WebSocket: двусторонний туннель на C#
WebSocket — это полноценный протокол двусторонней связи поверх TCP. После рукопожатия (обычного HTTP GET с апгрейдом) клиент и сервер общаются напрямую, без накладных расходов на заголовки HTTP. Сервер может отправить сообщение в любой момент, и клиент получит его мгновенно.
В ASP.NET Core WebSocket встроен из коробки. Не нужны сторонние библиотеки (хотя есть SignalR, который всё упрощает, но это уже другая история).
Пример WebSocket сервера на C#
Сначала добавим middleware для WebSocket в Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30) // пинг каждые 30 секунд
});
app.Map("/ws", async (HttpContext context) =>
{
if (context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await HandleWebSocket(webSocket);
}
else
{
context.Response.StatusCode = 400;
}
});
app.Run();
Теперь сам обработчик — читаем и пишем сообщения:
private static async Task HandleWebSocket(WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
while (webSocket.State == WebSocketState.Open)
{
// Ждем сообщение от клиента (или таймаут)
var result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure, "Bye", CancellationToken.None);
}
else if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"Получено: {message}");
// Эхо-ответ
var echo = Encoding.UTF8.GetBytes($"Эхо: {message}");
await webSocket.SendAsync(
new ArraySegment<byte>(echo), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
Клиент (JavaScript) подключается и слушает:
const socket = new WebSocket('wss://localhost:5001/ws');
socket.onopen = () => {
console.log('WebSocket открыт');
socket.send('Привет, сервер!');
};
socket.onmessage = (event) => {
console.log('Сообщение от сервера:', event.data);
};
socket.onclose = () => {
console.log('Соединение закрыто');
};
Всё работает как часы. Но WebSocket не поддерживается некоторыми прокси-серверами и требует более сложной обработки ошибок (разрывы, переподключение).
Long Polling vs WebSocket: честное сравнение
Давайте разложим по полочкам, в чём разница, и когда что использовать.
1. Протокол и накладные расходы
- Long Polling — чистый HTTP. Каждый запрос несет заголовки (cookie, auth, user-agent). Если у вас 10 000 клиентов и каждый опрашивает раз в 2 секунды — это 5 000 HTTP-запросов в секунду только на поддержание «пустых» соединений.
- WebSocket — после рукопожатия заголовков нет. Только данные. Расход трафика в разы меньше.
2. Задержка (latency)
- Long Polling — клиент получает данные не мгновенно, а с задержкой на время нового запроса. Если событие произошло сразу после того, как клиент получил пустой ответ — он узнает об этом только через poll-интервал