Поделиться через


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

Внимание

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

Для работы с протоколом управления передачей (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-порта для входящих запросов и последующего создания либо Socket, либо TcpClient, которые управляют подключением к клиенту. Метод 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, ConnectAsync, BeginConnect и 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() — это оболочка, объединяющая функциональность Bind и Listen() от Socket.

Рассмотрим следующий код прослушивателя 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 (Send, SendAsync, Receive и ReceiveAsync) напрямую вместо создания NetworkStream.

См. также