Протокол UDP

Последнее обновление: 07.11.2022

UDP (User Datagram Protocol) представляет сетевой протокол, который позволяет доставить данные на удаленный узел. Для этого передачи сообщений по протоколу UDP нет надобности использовать сервер, данные напрямую передаются от одного узла к другому. Снижаются накладные расходы при передаче, по сравнению с TCP, сами данные передаются быстрее. Все посылаемые сообщения по протоколу UDP называются дейтаграммами. Также через UDP можно передавать широковещательные сообщения для для набора адресов в подсети.

В то же время UDP имеет недостатки по сравнению с TCP. В частности, UDP не гарантирует доставку дейтаграммы конечному адресу. Данный протокол подходит больше для передачи изображений, мультимедийных файлов, потокового аудио, видео и т.п., когда недостаток небольшого количества потерянных при передаче дейтаграмм не сильно скажется на качестве переданных данных. Но не только. Так, новая версия протокола HTTP - HTTP 3 как раз работает поверх UDP (тогда как предыдущие версии HTTP - 1.0, 1.1, 2.0 работали поверх TCP).

В рамках платформы .NET для работы с UDP мы можем использовать стандартный класс System.Net.Sockets.Socket, либо класс System.Net.Sockets.UdpClient, который по сути является надстройкой над UDP-сокетом.

Использование сокетов для работы с UDP

Ключевым классом для отправки и приема данных по протоколу UDP является стандартный класс Socket. Но чтобы сокет работал с протоколом UDP, для него надо установить в качестве типа SocketType.Dgram, а в качестве протокола - ProtocolType.Udp:

using System.Net.Sockets;

using Socket udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

Прослушивание входящих сообщений

UDP-сокеты могут получать и/или отправлять данные. Если сокет должен получать сообщения, то надо привязать его к локальному адресу и одному из портов с помощью метода Bind(), в который передается прослушиваемая конечная точка:

using System.Net;
using System.Net.Sockets;

using var udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

var localIP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5555);
// привязываем сокет к локальной точке 127.0.0.1:5555
// и начинаем прослушивание входящих сообщений
udpSocket.Bind(localIP);

В данном случае сокет будет использовать локальный адрес 127.0.0.1:5555 для получения и отправки данных. После этого можно отправлять и получать сообщения.

Получение данных

Для получения сообщений udp-сокет использует метод ReceiveFrom()/ReceiveFromAsync(). Они имеют ряд версий, поэтому отмечу самые простейшие:

// .NET 7
public Task<SocketReceiveFromResult> ReceiveFromAsync (ArraySegment<byte> buffer, EndPoint remoteEndPoint);
// .NET 7, 6
public Task<SocketReceiveFromResult> ReceiveFromAsync (ArraySegment<byte> buffer, SocketFlags socketFlags, EndPoint remoteEndPoint);

Первая версия через первый параметр - объект ArraySegment<byte> получает данные в виде набора байтов, а через второй параметр - EndPoint получает адрес клиента, которые прислал данные.

Вторая версия также принимает объект SocketFlags, который позволяет настроить параметры получения данных.

Результатом метода является структура SocketReceiveFromResult, которая позволяет получить данные об операции с помощью двух свойств: ReceivedBytes - количество полученных байтов (в случае недуачного выполнения операции, возвращается 0) и RemoteEndPoint - объект EndPoint или адрес клиента, который отправил данные.

Стоит отметить, что асинхронные версии применяются начиная с .NET 6 (а некоторые версии и вовсе были добавлены начиная с .NET 7). Для более раниих версий можно использовать синхронные версии:

public int ReceiveFrom (byte[] buffer, ref EndPoint remoteEP);

Данная версия получает массив байтов для отправки и объект EndPoint, в который будут переданы данные об удаленной конечной точки. Результатом метода является количество присланных байтов.

Пример получения:

using System.Net;
using System.Net.Sockets;
using System.Text;

using var udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

var localIP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5555);
// начинаем прослушивание входящих сообщений
udpSocket.Bind(localIP);
Console.WriteLine("UDP-сервер запущен...");

byte[] data = new byte[256]; // буфер для получаемых данных
//адрес, с которого пришли данные
EndPoint remoteIp = new IPEndPoint(IPAddress.Any, 0);
// получаем данные в массив data
var result = await udpSocket.ReceiveFromAsync(data, remoteIp);
var message = Encoding.UTF8.GetString(data, 0, result.ReceivedBytes);

Console.WriteLine($"Получено {result.ReceivedBytes} байт");
Console.WriteLine($"Удаленный адрес: {result.RemoteEndPoint}");
Console.WriteLine(message);     // выводим полученное сообщение

Назовем эту программу условно udp-сервер. Здесь udp-сокет связан с конечной точкой 127.0.0.1:5555. Именно на этот адрес будут отправляться данные. Для их считывания определен массив из 256 байт (предположим, что отправляемые данные будут не больше 256 байт). С помощью метода udpSocket.ReceiveFromAsync() получаем данные в массив data, а в переменную remoteIp получаем адрес, с которого пришли данные.

var result = await udpSocket.ReceiveFromAsync(data, remoteIp);

Причем, хотя мы получаем в remoteIp новое значение, но сам этот объект перед передачей в метод все равно надо инициализировать. В данном случае по умолчанию эта конечная точка представляет произвольный локальный адрес и порт 0, но это не приницпиально.

EndPoint remoteIp = new IPEndPoint(IPAddress.Any, 0);

После получения данных выводим результат на консоль.

Отправка данных

Для отправки данных применяются методы SendTo()/SendToAsync(). Отмечу некоторые версии этих методов:

// .NET 7+
public Task<int> SendToAsync (ArraySegment<byte> buffer, EndPoint remoteEP);
// .NET 6+
public Task<int> SendToAsync (ArraySegment<byte> buffer, SocketFlags socketFlags, EndPoint remoteEP);

В первом случае метод получае отправляемые данные в виде объекта ArraySegment<byte> и адрес отправки в виде конечной точки EndPoint. Во втором случае также передается значение SocketFlags для настройки параметров отправки. Результатом методов является количество отправленных байтов.

Стоит учитывать, что асинхронные версии были добавлены в .NET 6 и 7. Но синхронные методы, которые доступны и для предыдущих версий .NET, будут аналогичны. Например:

public int SendTo (byte[] buffer, EndPoint remoteEP);

Здесь первый параметр представляет массив отправляемых байтов, а второй - адрес отправки. Результат метода - количество отправленных байтов.

Стоит отметить, что для отправки необязательно перед методом SendTo вызывать метод Bind.

Например, определим для вышеопределенного udp-сервера программу udp-клиента, которая будет отправлять данные:

using System.Net;
using System.Net.Sockets;
using System.Text;

using var udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

string message = "Hello METANIT.COM";
byte[] data = Encoding.UTF8.GetBytes(message);
EndPoint remotePoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5555);
int bytes = await udpSocket.SendToAsync(data, remotePoint);
Console.WriteLine($"Отправлено {bytes} байт");

В данном случае строка "Hello METANIT.COM" конвертируется в массив байтов и отправляется по адресу 127.0.0.1:5555.

Запустим UDP-сервер, а затем udp-клиента. В итоге udp-клиент отправит данные на сервер строку, и консоль udp-сервера отобразит полученные данные:

UDP-сервер запущен...
Получено 17 байт
Удаленный адрес: 127.0.0.1:52679
Hello METANIT.COM

Размер буфера при получении данных

Одной из распространенных проблем при получении данных через udp-сокета представляет несоответствие размера используемого буфера размеру дейтаграммы. Так, если размер буфера меньше, чем размер дейтаграммы, то выполнение программы завершится ошибкой:

Unhandled exception. System.Net.Sockets.SocketException (10040): Сообщение, отправленное на сокет датаграмм, было больше, чем буфер внутренних сообщений или был превышен иной сетевой параметр. Также возможно, что буфер для принятия сообщения был меньше, чем размер сообщения.

Единственным возможным решением данной проблемы является установка соответствующего размера буфера. Так, в примере выше буфер представлял массив из 256 байт. В качестве страховки можно установить буфер размером 65535 байт - это максимально возможный размер дейтаграммы udp.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850