Класс TcpClient

129

Поддержка сокетов TCP на платформе .NET значительно усовершенствована по сравнению с предыдущей моделью программирования. Раньше большинство разработчиков, использовавших Visual С++, для реализации любых типов взаимодействия сокетов, обращались к классам CSocket и CAsyncSocket или пользовались библиотеками независимых поставщиков.

Для высокоуровневого программирования TCP встроенная поддержка практически отсутствовала. В .NET для работы с сокетами предоставлено особое пространство имен System.Net.Sockets. Это пространство имен содержит не только такие низкоуровневые классы, как Socket, но и классы высокого уровня TcpClient и TcpListener, предлагающие простые интерфейсы для взаимодействия через TCP.

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

Класс TcpClient обеспечивает TCP-сервисы для соединений на стороне клиента. Он построен на классе Socket и обеспечивает TCP-сервисы на более высоком уровне — в классе TcpClient есть закрытый объект данных m_ClientSocket, используемый для взаимодействия с сервером TCP. Класс TcpClient предоставляет простые методы для соединения через сеть с другим приложением сокетов, отправки ему данных и получения данных от него. Наиболее важные члены класса TcpClient перечислены далее:

Свойства и методы класса TcpClient
Свойство или метод Тип Описание
LingerState LingerOption Устанавливает или возвращает объект LingerOption, содержащий информацию о том, будет ли соединение оставаться открытым после закрытия сокета и как долго.
NoDelay bool Указывает, будет ли сокет задерживать отправку и получение данных, если буфер, назначенный для отправки или получения данных, не заполнен. Если свойство имеет значение false, TCP задержит отправку пакета, пока не будет накоплен достаточный объем данных. Это средство помогает избежать неэффективной отправки через сеть слишком маленьких пакетов.
ReceiveBufferSize int Задает размер буфера для входящих данных (в байтах). Это свойство используется при считывании данных из сокета.
ReceiveTimeout int Задает время в миллисекундах, которое TCpClient будет ждать получения данных после инициирования этой операции. Если это время истечет, а данные не будут получены, возникнет исключение SocketException.
SendBufferSize int Задает размер буфера для исходящих данных.
SendTimeout int Задает время в миллисекундах, которое TcpClient будет ждать подтверждения числа байтов, отправленных удаленному хосту от базового сокета. При истечении времени SendTimeout порождается исключение SocketException.
Close() Закрывает TCP-соединение.
Connect() Соединяется с удаленным хостом TCP.
GetStream() Возвращает объект NetworkStream, используемый для передачи данных между клиентом и удаленным хостом.
Active bool Указывает, есть ли активное соединение с удаленным хостом.
Client Socket Задает базовый объект Socket, используемый объектом TcpClient. Поскольку это защищенное свойство, к базовому сокету можно обращаться, если вы производите ваш класс от TcpClient.

Создание экземпляра класса TcpClient

В классе TcpClient существуют три перегруженных конструктора:

public TcpClient();
public TcpClient(IPEndPoint ipEnd);
public TcpClient(string hostname, int port);

Конструктор, используемый по умолчанию, инициализирует экземпляр TcpClient. Если экземпляр TcpClient создается так, то для установления соединения с удаленным хостом надо вызвать метод Connect().

Второй перегруженный конструктор принимает один параметр типа IPEndPoint. Он инициализирует новый экземпляр класса TcpClient , связанный с указанной конечной точкой. Заметьте, что это не удаленная, а локальная конечная точка. Если попытаться передать конструктору удаленную конечную точку, будет порождено исключение, означающее, что в данном контексте IP-адрес задан некорректно.

Если использовать этот конструктор, то после создания объекта TcpClient, все-таки нужно вызвать метод Connect():

// Создаем локальную конечную точку
IPAddress ipAddr = IPAddress.Parse("140.16.82.66");
IPEndPoint endPoint = new IPEndPoint(ipAddr, 11000);

TcpClient newClient = new TcpClient(endPoint);

// Для создания соединения с сервером надо вызвать connect()
newClient.Connect(ipAddr, 11000);

Параметр, переданный конструктору объекта TcpClient, является локальной конечной точкой, в то время как метод Connect() фактически соединяет клиента с сервером и поэтому принимает в качестве параметра удаленную конечную точку.

Последний перегруженный конструктор создает новый экземпляр класса TcpClient и устанавливает удаленное соединение с использованием в параметрах DNS-имени и номера порта:

TcpClient newClient = new TcpClient("localhost", 80);

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

Установка соединения с хостом

Создав экземпляр класса TcpClient, следующим шагом установим соединение с удаленным хостом. Для соединения клиента с хостом TCP предоставлен метод Connect(). Если для создания экземпляра TcpClient использовать конструктор по умолчанию или локальную конечную точку, то останется лишь вызвать этот метод, иначе, если конструктору были переданы имя хоста и номер порта, попытка вызова метода Connect() породит исключение. Существуют три перегруженных метода Connect():

public void Connect(IPEndPoint endPoint); 
public void Connect(IPAddress ipAddr, int port); 
public void Connect(string hostname, int port);

Они достаточно просты, но, тем не менее, на коротких примерах продемонстрируем использование каждого перегруженного метода:

  1. Передача объекта IPEndPoint, представляющего удаленную конечную точку, с которой надо соединиться:

    // Создаем новый экземпляр TcpClient
    TcpClient newClient = new TcpClient();
    
    // Устанавливаем соединение с IPEndPoint
    IPAddress ipAddr = IPAddress.Parse("127.0.0.1");
    IPEndPoint endPoint = new IPEndPoint(ipAddr, 11000);
    
    // Соединяемся с хостом
    newClient.Connect(endPoint);
  2. Передача объекта IPAddress и номера порта:

    // Создаем новый экземпляр TcpClient
    TcpClient newClient = new TcpClient();
    
    // Устанавливаем соединение с IPEndPoint
    IPAddress ipAddr = IPAddress.Parse("127.0.0.1");
    
    // Соединяемся с хостом
    newClient.Connect(ipAddr, 11000);
  3. Передача имени хоста и номера порта:

    // Создаем новый экземпляр TcpClient
    TcpClient newClient = new TcpClient();
    
    // Соединяемся с хостом
    newClient.Connect("127.0.0.1", 11000);

Если соединение будет неудачным или возникнут другие проблемы, порождается исключение SocketException:

try
{
      TcpClient newClient = new TcpClient();

      // Соединяемся с сервером
      newClient.Connect("140.16.82.66", 11000); // В этот момент сокет
                                          // порождает исключение, если
                                          // при соединении возникают проблемы
}
catch (SocketException ex)
{
      Console.WriteLine("Exception: " + ex.ToString());
}

Отправка и получение сообщений

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

NetworkStream tcpStream = newClient.GetStream();

Предположим, что newClient — это экземпляр TcpClient, а соединение с хостом уже установлено. Иначе будет порождено исключение InvalidOperation. Получив поток, используем методы Read() и Write() класса NetworkStream для чтения из приложения хоста и записи к нему. Метод Write() принимает три параметра: массив байтов, содержащий данные, которые надо отправить хосту, позицию в потоке, с которой хотим начать запись, и длину данных:

byte[] sendBytes = Encoding.UTF8.GetBytes("Простой тест");
tcpStream.Write(sendBytes, 0, sendBytes.Length);

Метод Read() имеет точно такой же набор параметров — массив байтов для сохранения данных, которые считываются из потока, позицию начала считывания и число считываемых байтов:

byte[] bytes = new byte[newClient.ReceiveBufferSize];
int bytesRead = tcpStream.Read(bytes, 0, newClient.ReceiveBufferSize);

// Строка, содержащая ответ от сервера
string returnData = Encoding.UTF8.GetString(bytes);

Свойство ReceiveBufferSize класса TcpClient позволяет получить или установить размер (в байтах) буфера для чтения, поэтому используем его как размер массива байтов. Заметьте, что, устанавливая это свойство, мы не ограничиваем число байтов, которое можно считывать каждой операцией, поскольку при необходимости размер буфера будет динамически изменяться, но если задать размер буфера, это сократит накладные расходы.

Закрытие сокета TCP

После взаимодействия с сервером, чтобы освободить все ресурсы, следует вызвать метод Close():

// Закрываем клиентский сокет
newClient.Close();

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

Помимо этой основной функциональности имеются другие возможности. Если требуется обратиться к экземпляру сокета, базовому для объекта TcpClient, например для установки опций методом SetSocketOption(), можно использовать свойство Client, получая доступ к членам соответствующего объекта Socket. Можно также использовать свойство Client, чтобы сделать существующий объект Socket базовым сокетом для объекта TcpClient. Но поскольку это защищенный член класса TcpClient, прежде чем его использовать, наш класс должен наследовать класс TcpClient.

Свойство Client дает возможность защищенного доступа к закрытому члену m_ClientSocket, о котором упоминалось ранее. Класс TcpClient передает сделанные на нем вызовы аналогичному методу класса Socket после проверки параметров и инициализации экземпляра сокета. Объект m_ClientSocket создается в конструкторе, который вызывает закрытый метод initialize(), строящий новый объект Socket, и затем вызывает метод set_Client(), чтобы назначить его свойству Client. Этот метод также устанавливает булево значение m_Active, используемое для отслеживания состояния экземпляра Socket. Он также проверяет наличие излишних соединений объекта Socket и операций, требующих установления соединения.

В общем у сокетов есть масса опций, которые класс TcpClient не охватывает. Если нужно установить или получить какое-либо из этих свойств, не представленных в TcpClient (например, Broadcast или KeepAlive), необходимо унаследовать класс от TcpClient и использовать его член Client.

Пройди тесты
Лучший чат для C# программистов