TCP-клиент. Класс TcpClient

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

Для создания tcp-клиента платформа .NET предоставляет класс класс TcpClient, который построен поверх сокетов и опять же использует сокет для отправки и получения данных, но при этом упрощает написание некоторых вещей. (Исходный код TcpClient)

Создание TcpClient

Для создания TcpClient применяется один из конструкторов класса:

public TcpClient ();
public TcpClient (System.Net.Sockets.AddressFamily family);
public TcpClient (string hostname, int port);
public TcpClient (System.Net.IPEndPoint localEP);

При использовании всех конструкторов, кроме последнего объекту TcpClient автоматически присваивается наиболее подходящий локальный IP-адрес и порт. Через последний конструктор можно вручную задать локальную конечную точку - объект IPEndPoint, к которой будет привязан TcpClient.

При использовании второго конструктора в него передается либо значение AddressFamily.InterNetwork (для адресов IPv4), либо AddressFamily.InterNetworkV6 (для адресов IPv6).

В третий конструктор передается адрес и порт удаленного узла, к которому клиент собирается подклчаться.

Свойства TcpClient

Свойства TcpClient позволяют настроить состояние объекта или получить информацию о нем. Отметим среди свойств следующие:

  • Available: возвращает количество байтов данных, полученных из сети и доступных для чтения.

  • Client: возвращает или задает объект Socket, который используется объектом TcpClient.

  • Connected: возвращает true, если TcpClient подключен к удаленному узлу.

  • >LingerState: возвращает или устанавливает, доступен ли порт только одному клиенту.

  • NoDelay: указывает, применяется ли задержка, когда буферы отправки и получения не заполнены. Если равно false, то TcpClient отправляет пакеты по сети только тогда, когда наберется достаточное количество данных. Это сделано, потому что отправка небольших кусочков данных может быть неэффективной и может привести к перегрузке. В то же время могут быть ситуации, когда необходимо отправлять небольшие объемы данных или когда необходимо побыстрее получить ответ.

  • ReceiveBufferSize: возвращает или задает размер буфера приема (по умолчанию равно 65536).

  • ReceiveTimeout: возвращает или задает длительность интервала, в течение которого объект TcpClient будет ожидать получение данных после начала операции чтения (по умолчанию равно 0).

  • SendBufferSize: возвращает или задает размер буфера отправки (по умолчанию равно 65536).

  • SendTimeout: возвращает или задает длительность интервала, в течение которого объект TcpClient будет ожидать успешное завершение отправки данных (по умолчанию равно 0).

  • Подключение к серверу

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

    public Task ConnectAsync (System.Net.IPEndPoint remoteEP);
    public Task ConnectAsync (string host, int port);
    public Task ConnectAsync (System.Net.IPAddress address, int port);
    

    Например, покдлючимся к хосту "www.google.com":

    using System.Net.Sockets;
    
    using TcpClient tcpClient = new TcpClient();
    try
    {
        // подключение к www.google.com
        await tcpClient.ConnectAsync("www.google.com", 80);
        Console.WriteLine("Подключение установлено");
    }
    catch(SocketException ex)
    {
        Console.WriteLine(ex.Message);
    }
    

    При неудачной попытке подключения метод Connect/ConnectAsync генерирует исключение типа SocketException.

    Закрытие TcpClient

    После окончания работы с TcpClient его надо закрыть методом Close():

    using System.Net.Sockets;
    
    TcpClient tcpClient = new TcpClient();
    
    await tcpClient.ConnectAsync("www.google.com", 80);
    Console.WriteLine("Подключение установлено");
    
    tcpClient.Close();      // закрываем подключение
    Console.WriteLine(tcpClient.Connected);     // False - подключение закрыто
    

    Либо можно использовать конструкцию using, как в предыдущем примере.

    Отправка и получение данных

    Для отправки и получения данных TcpClient в общем случае использует рассмотренный в прошлой теме класс NetworkStream. Для получения объекта NetworkStream после подключения к серверу у TcpClient можно вызвать метод GetStream():

    using System.Net.Sockets;
    
    using TcpClient tcpClient = new TcpClient();
    await tcpClient.ConnectAsync("www.google.com", 80);
    // получаем поток для взаимодействия с сервером
    NetworkStream stream = tcpClient.GetStream();
    

    Соответственно отправка и получение данных для TcpClient будет производиться также, как было рассмотрено в прошлой статье про NetworkStream.

    Стоит отметить, что в реальности NetworkStream будет использовать для отправки и получения данных тот же объект Socket, что и TcpClient. Кроме того, если закрывается объект TcpClient (в примере выше для этого применяется конструкция using), то вместе с ним автоматически закрывается и связанный с ним объект NetworkStream (а также освобождается связанный с ними сокет).

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

    Для отправки данных у NetworkStream используется метод Write()/WriteAsync():

    public ValueTask WriteAsync (ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default);
    public Task WriteAsync (byte[] buffer, int offset, int count);
    

    Отправляемые данные передаются в виде массива байтов, либо в виде объекта ReadOnlyMemory<byte>, который опять же инкапсулирует массив байтов.

    using System.Net.Sockets;
    using System.Text;
    
    using TcpClient tcpClient = new TcpClient();
    var server = "www.google.com";
    await tcpClient.ConnectAsync(server, 80);
    // получаем поток для взаимодействия с сервером
    NetworkStream stream = tcpClient.GetStream();
    
     // определяем отправляемые данные
    var requestMessage = $"GET / HTTP/1.1\r\nHost: {server}\r\nConnection: Close\r\n\r\n";
    // конвертируем данные в массив байтов
    var requestData = Encoding.UTF8.GetBytes(requestMessage);
    // отправляем данные серверу
    await stream.WriteAsync(requestData);
    

    Сервер, который обрабатывает запросы к "www.google.com" представляет собой http-сервер и обрабатывает запросы по протоколу http. Поэтому наш клиент посылает сообщение, которое соответствует протоколу http.

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

    Для получения данных к NetworkStream применяется методы Read()/ReadAsync(). Так, в примере выше мы посылали http-запрос к серверу "www.google.com". Теперь получим от него ответ:

    using System.Net.Sockets;
    using System.Text;
    
    using TcpClient tcpClient = new TcpClient();
    var server = "www.google.com";
    await tcpClient.ConnectAsync(server, 80);
    var stream = tcpClient.GetStream();
    
    var requestMessage = $"GET / HTTP/1.1\r\nHost: {server}\r\nConnection: Close\r\n\r\n";
    var requestData = Encoding.UTF8.GetBytes(requestMessage);
    await stream.WriteAsync(requestData);
    
    // буфер для получения данных
    var responseData = new byte[512];
    // StringBuilder для склеивания полученных данных в одну строку
    var response = new StringBuilder();
    int bytes;  // количество полученных байтов
    do
    {
        // получаем данные
        bytes = await stream.ReadAsync(responseData);
        // преобразуем в строку и добавляем ее в StringBuilder
        response.Append(Encoding.UTF8.GetString(responseData, 0, bytes));
    }
    while (bytes>0); // пока данные есть в потоке 
    
    // выводим данные на консоль
    Console.WriteLine(response);
    

    теперь для считывания используем цикл do..while. Смотрим, сколько байтов возвращает ReadAsync. И пока он вернет 0 байтов, повторяем цикл. Полученные байты конвертируем в строку и добавляем в StringBuilder. В конце выводим полученное содержимое из StringBuilder на консоль:

    Отправка и получение данных с помощью сокетов TCP и Socket в C# и .NET

    Считывание данные и свойства TcpClient.Available и NetworkStream.DataAvailable

    При чтении данных из потока одна из часто встречающихся задач заключается в определении конца данных, особенно если мы считываем данные в бесконечном цикле. В примере выше это делалось просто - считывали, пока количество реально считанных байтов не достигнет нуля, то есть доступных байтов для чтения просто не останется:

    do
    {
        bytes = await stream.ReadAsync(responseData);
        response.Append(Encoding.UTF8.GetString(responseData, 0, bytes));
    }
    while (bytes>0); // пока данные есть в потоке 
    

    Но у класса TcpClient есть свойство Available, которое возвращает количество доступных для чтения байтов. Кроме того, у класса NetworkStream есть похожее свойство - DataAvailable, которое возвращает true, если в потоке есть доступные для чтения данные. Соответственно возникает вопрос, почему бы не использовать эти свойства? Например, свойство Available:

    do
    {
        bytes = await stream.ReadAsync(responseData);
        response.Append(Encoding.UTF8.GetString(responseData, 0, bytes));
    }
    while (tcpClient.Available > 0); // пока данные есть в потоке 
    

    Или свойство DataAvailable

    do
    {
        bytes = await stream.ReadAsync(responseData);
        response.Append(Encoding.UTF8.GetString(responseData, 0, bytes));
    }
    while (stream.DataAvailable); // пока данные есть в потоке 
    

    В реальности оба этих свойства смотрят на значение свойства Available используемого объекта Socket, которое возвращает количество доступных для чтения байтов. И соответственно здесь мы столкнемся с той же проблемой, что и при работе с классом Socket - свойство Available будет иметь ненулевое значение, если в текущий момент есть доступные данные. Но природа протокола TCP такова, что крупные наборы данных отправляются отдельными пакетами. Какой-то пакет может прийти быстрее, какой-то задержится, какой-то будет потерян, и потребуется переотправка. Поэтому может возникнуть ситуация, что сервер отправил данные, часть данных пришла. В какой-то момент свойство Available у сокета возратило 0, соответственно произошел выход из цикла.

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