Общие сведения о TCP

Внимание

Класс Socket настоятельно рекомендуется для расширенных пользователей, а не TcpClientTcpListenerдля пользователей.

Для работы с протоколом управления передачей (TCP) можно использовать два варианта: использовать Socket для максимального контроля и производительности, а также использовать вспомогательные TcpClientTcpListener классы. TcpClient и TcpListener создаются на основе System.Net.Sockets.Socket класса и заботятся о деталях передачи данных для удобства использования.

Классы протокола используют базовый Socket класс для предоставления простого доступа к сетевым службам без дополнительных расходов на обслуживание сведений о состоянии или знание сведений о настройке сокетов, относящихся к протоколу. Чтобы использовать асинхронные методы, можно использовать асинхронные Socket методы, предоставляемые классом NetworkStream . Чтобы получить доступ к функциям Socket класса, не предоставляемым классами протокола, необходимо использовать Socket класс.

TcpClient и TcpListener представляет сеть с помощью NetworkStream класса. С помощью метода GetStream возвращается сетевой поток, после чего вызываются методы NetworkStream.ReadAsync и NetworkStream.WriteAsync потока. Он NetworkStream не владеет базовым сокетом классов протокола, поэтому закрытие не влияет на сокет.

Использование TcpClient и TcpListener

Класс TcpClient запрашивает данные из интернет-ресурса с помощью TCP. Методы и свойства абстрактных сведений TcpClient для создания Socket запроса и получения данных с помощью TCP. Так как подключение к удаленному устройству представлено в виде потока, данные можно считывать и записывать с помощью методов работы с потоками платформы .NET Framework.

Протокол TCP устанавливает соединение с удаленной конечной точкой, а затем использует его для отправки и получения пакетов данных. Протокол TCP отвечает за отправку пакетов данных в конечную точку и их сборку в правильном порядке после доставки.

Создание конечной точки IP

При работе System.Net.Socketsс объектом представляется сетевая конечная точка IPEndPoint . Создается IPEndPoint с соответствующим номером IPAddress порта. Прежде чем начать беседу с помощью Socket, создайте канал данных между приложением и удаленным назначением.

В качестве уникального идентификатора службы протокол TCP/IP использует сетевой адрес и номер порта службы. Сетевой адрес определяет определенное назначение сети; Номер порта определяет определенную службу на этом устройстве для подключения. Сочетание сетевого адреса и порта службы называется конечной точкой, которая представлена в .NET классом EndPoint . Потомок EndPoint определяется для каждого поддерживаемого семейства адресов; для семейства IP-адресов класс имеет значение IPEndPoint.

Класс Dns предоставляет службы доменных имен приложениям, используюющим интернет-службы TCP/IP. Метод GetHostEntryAsync запрашивает DNS-сервер для сопоставления понятного доменного имени (например, "host.contoso.com") с числовым интернет-адресом (например 192.168.1.1, ). GetHostEntryAsync возвращает значение Task<IPHostEntry> , которое, когда ожидается, содержит список адресов и псевдонимов для запрошенного имени. В большинстве случаев можно использовать первый адрес из возвращенного массива AddressList. Следующий код получает IPAddress IP-адрес сервера host.contoso.com.

IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("host.contoso.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];

Совет

Для ручного тестирования и отладки метод обычно можно использовать GetHostEntryAsync с результирующий имя узла из Dns.GetHostName() значения для разрешения имени localhost на IP-адрес. Рассмотрим следующий фрагмент кода:

var hostName = Dns.GetHostName();
IPHostEntry localhost = await Dns.GetHostEntryAsync(hostName);
// This is the IP address of the local machine
IPAddress localIpAddress = localhost.AddressList[0];

Центр назначения номеров Интернета (IANA) определяет номера портов для общих служб. Дополнительные сведения см. в разделе IANA: Имя службы и реестр номеров портов транспорта). Другие службы могут использовать номера портов в диапазоне от 1024 до 65535. Следующий код объединяет IP-адрес host.contoso.com с номером порта, чтобы создать удаленную конечную точку для подключения.

IPEndPoint ipEndPoint = new(ipAddress, 11_000);

После определения адреса удаленного устройства и выбора порта, используемого для подключения, приложение может установить подключение к удаленному устройству.

Создание класса TcpClient

Класс TcpClient предоставляет службы TCP на более высоком уровне абстракции, чем Socket класс. TcpClient используется для создания подключения клиента к удаленному узлу. Зная, как получить IPEndPoint, предположим, что у вас есть IPAddress пара с нужным номером порта. В следующем примере показано, как настроить TcpClient подключение к серверу времени на TCP-порту 13:

var ipEndPoint = new IPEndPoint(ipAddress, 13);

using TcpClient client = new();
await client.ConnectAsync(ipEndPoint);
await using NetworkStream stream = client.GetStream();

var buffer = new byte[1_024];
int received = await stream.ReadAsync(buffer);

var message = Encoding.UTF8.GetString(buffer, 0, received);
Console.WriteLine($"Message received: \"{message}\"");
// Sample output:
//     Message received: "📅 8/22/2022 9:07:17 AM 🕛"

В приведенном выше коде C#:

  • Создает из IPEndPoint известного IPAddress и порта.
  • Создайте экземпляр нового TcpClient объекта.
  • client Подключение на удаленный сервер времени TCP через порт 13.TcpClient.ConnectAsync
  • NetworkStream Используется для чтения данных с удаленного узла.
  • Объявляет буфер чтения байтов 1_024 .
  • Считывает данные из stream буфера чтения.
  • Записывает результаты в виде строки в консоль.

Так как клиент знает, что сообщение небольшое, все сообщение можно считывать в буфер чтения в одной операции. При наличии больших сообщений или сообщений с неопределенной длиной клиент должен использовать буфер более соответствующим образом и читать в цикле while .

Внимание

При отправке и получении сообщений должно быть известно заранее как серверу, Encoding так и клиенту. Например, если сервер взаимодействует с использованием ASCIIEncoding , но клиент пытается использовать UTF8Encoding, сообщения будут неправильно сформированы.

Создание класса TcpListener

Тип TcpListener используется для отслеживания TCP-порта для входящих запросов, а затем создания либо SocketTcpClient для управления подключением к клиенту. Метод Start включает прослушивание порта, а метод Stop отключает его. Метод AcceptTcpClientAsync принимает входящие запросы подключения и создает TcpClient запрос для обработки запроса, а AcceptSocketAsync метод принимает входящие запросы подключения и создает Socket для обработки запроса.

В следующем примере показано создание сервера времени сети с помощью TcpListener tcp-порта 13. При получении входящего запроса на подключение сервер времени сообщает в ответ текущую дату и время с сервера узла.

var ipEndPoint = new IPEndPoint(IPAddress.Any, 13);
TcpListener listener = new(ipEndPoint);

try
{    
    listener.Start();

    using TcpClient handler = await listener.AcceptTcpClientAsync();
    await using NetworkStream stream = handler.GetStream();

    var message = $"📅 {DateTime.Now} 🕛";
    var dateTimeBytes = Encoding.UTF8.GetBytes(message);
    await stream.WriteAsync(dateTimeBytes);

    Console.WriteLine($"Sent message: \"{message}\"");
    // Sample output:
    //     Sent message: "📅 8/22/2022 9:07:17 AM 🕛"
}
finally
{
    listener.Stop();
}

В приведенном выше коде C#:

  • IPEndPoint Создает с помощью IPAddress.Any и портом.
  • Создайте экземпляр нового TcpListener объекта.
  • Start Вызывает метод, чтобы начать прослушивание порта.
  • TcpClient Использует метод из AcceptTcpClientAsync метода для принятия входящих запросов на подключение.
  • Кодирует текущую дату и время в виде строкового сообщения.
  • NetworkStream Используется для записи данных в подключенный клиент.
  • Записывает отправленное сообщение в консоль.
  • Наконец, вызывает Stop метод, чтобы остановить прослушивание порта.

Конечный элемент управления TCP с классом Socket

Оба TcpClient класса и TcpListener внутренне полагаются на Socket класс, то есть все, что можно сделать с этими классами, можно достичь напрямую с помощью сокетов. В этом разделе демонстрируется несколько TcpClient вариантов использования и TcpListener их Socket аналог, который функционально эквивалентен.

Создание сокета клиента

TcpClientКонструктор по умолчанию пытается создать сокет с двумя стеками с помощью конструктора Socket(SocketType, ProtocolType). Этот конструктор создает сокет с двумя стеками, если поддерживается IPv6, в противном случае он возвращается к IPv4.

Рассмотрим следующий код TCP-клиента:

using var client = new TcpClient();

Предыдущий код TCP-клиента функционально эквивалентен следующему коду сокета:

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

TcpClient(AddressFamily) Конструктор

Этот конструктор принимает только три AddressFamily значения, в противном случае создается исключение ArgumentException. Допустимые значения:

Рассмотрим следующий код TCP-клиента:

using var client = new TcpClient(AddressFamily.InterNetwork);

Предыдущий код TCP-клиента функционально эквивалентен следующему коду сокета:

using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

TcpClient(IPEndPoint) Конструктор

При создании сокета этот конструктор также привязывается к предоставленному локальномуIPEndPoint объекту. Свойство IPEndPoint.AddressFamily используется для определения семейства адресов сокета.

Рассмотрим следующий код TCP-клиента:

var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var client = new TcpClient(endPoint);

Предыдущий код TCP-клиента функционально эквивалентен следующему коду сокета:

// Example IPEndPoint object
var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);

TcpClient(String, Int32) Конструктор

Этот конструктор попытается создать двойной стек, аналогичный конструктору по умолчанию, и подключить его к удаленной конечной точке DNS, определенной hostname с помощью пары.port

Рассмотрим следующий код TCP-клиента:

using var client = new TcpClient("www.example.com", 80);

Предыдущий код TCP-клиента функционально эквивалентен следующему коду сокета:

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);

Подключиться к серверу

Все Connect, ConnectAsyncBeginConnect и EndConnect перегрузки в TcpClient функционально эквивалентны соответствующим Socket методам.

Рассмотрим следующий код TCP-клиента:

using var client = new TcpClient();
client.Connect("www.example.com", 80);

Приведенный выше TcpClient код эквивалентен следующему коду сокета:

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);

Создание сокета сервера

Подобно TcpClient экземплярам, имеющим функциональную эквивалентность со своими необработанными Socket аналогами, в этом разделе конструкторы сопоставляют TcpListener конструкторы с соответствующим кодом сокета. Первый конструктор, который следует рассмотреть, — это TcpListener(IPAddress localaddr, int port).

var listener = new TcpListener(IPAddress.Loopback, 5000);

Предыдущий код прослушивателя TCP функционально эквивалентен следующему коду сокета:

var ep = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

Запуск прослушивания на сервере

Метод Start() — это оболочка, Socketобъединяющая функции Bind и Listen() функции.

Рассмотрим следующий код прослушивателя TCP:

var listener = new TcpListener(IPAddress.Loopback, 5000);
listener.Start(10);

Предыдущий код прослушивателя TCP функционально эквивалентен следующему коду сокета:

var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);
try
{
    socket.Listen(10);
}
catch (SocketException)
{
    socket.Dispose();
}

Принятие подключения к серверу

Под капотом входящие TCP-подключения всегда создают новый сокет при принятии. TcpListener может принимать экземпляр напрямую Socket (через AcceptSocket() или) или AcceptSocketAsync()может принимать TcpClient (через AcceptTcpClient() и AcceptTcpClientAsync()).

Рассмотрим следующий TcpListener код:

var listener = new TcpListener(IPAddress.Loopback, 5000);
using var acceptedSocket = await listener.AcceptSocketAsync();

// Synchronous alternative.
// var acceptedSocket = listener.AcceptSocket();

Предыдущий код прослушивателя TCP функционально эквивалентен следующему коду сокета:

var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
using var acceptedSocket = await socket.AcceptAsync();

// Synchronous alternative
// var acceptedSocket = socket.Accept();

Создание отправки NetworkStream и получения данных

При TcpClient этом необходимо создать экземпляр NetworkStream с GetStream() помощью метода, чтобы иметь возможность отправлять и получать данные. При этом Socketнеобходимо вручную создать NetworkStream .

Рассмотрим следующий TcpClient код:

using var client = new TcpClient();
using NetworkStream stream = client.GetStream();

Эквивалентно следующему коду сокета:

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

// Be aware that transferring the ownership means that closing/disposing the stream will also close the underlying socket.
using var stream = new NetworkStream(socket, ownsSocket: true);

Совет

Если код не нужен для работы с экземпляромStream, можно использовать Socketметоды отправки и получения напрямую ReceiveReceiveAsyncSendSendAsyncвместо создания.NetworkStream

См. также