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

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

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

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

Сначала рассмотрим ситуацию, когда сервер отправляет данные, а клиент только их получает. Так, определим для сервера следующий код:

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

var tcpListener = new TcpListener(IPAddress.Any, 8888);

try
{
    tcpListener.Start();    // запускаем сервер
    Console.WriteLine("Сервер запущен. Ожидание подключений... ");

    while (true)
    {
        // получаем подключение в виде TcpClient
        using var tcpClient = await tcpListener.AcceptTcpClientAsync();
        // получаем объект NetworkStream для взаимодействия с клиентом
        var stream = tcpClient.GetStream();
        // определяем данные для отправки - отправляем текущее время
        byte[] data = Encoding.UTF8.GetBytes(DateTime.Now.ToLongTimeString());
        // отправляем данные
        await stream.WriteAsync(data);
        Console.WriteLine($"Клиенту {tcpClient.Client.RemoteEndPoint} отправлены данные");
    }
}
finally
{
    tcpListener.Stop();
}

В качестве примера просто отправляем клиенту текущее время в формате hh:mm:ss. Для этого конвертируем строку в массив байтов и отправляем их с помощью метода stream.WriteAsync(). А на консоль сервера выводим диагностическое сообщение.

На стороне клиента определим следующий код:

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

using TcpClient tcpClient = new TcpClient();
await tcpClient.ConnectAsync("127.0.0.1", 8888);

// буфер для считывания данных
byte[] data = new byte[512];
// получаем NetworkStream для взаимодействия с сервером
var stream = tcpClient.GetStream();
// получаем данные из потока
int bytes = await stream.ReadAsync(data);
// получаем отправленное время
string time = Encoding.UTF8.GetString(data, 0, bytes);
Console.WriteLine($"Текущее время: {time}");

На стороне клиента мы знаем, что сервер отправляет дату в виде строки, и для ее считывания определяем буфер - массив из 512 байтов. С помощью метода stream.ReadAsync() считываем данные из потока, конвертируем байты в строку и выводим ее на консоль.

Запустим сервер и клиент. При обращении к серверу клиент получит текущее время:

Текущее время: 20:16:49

А консоль сервера отобразит ip-адрес клиента:

Сервер запущен. Ожидание подключений...
Клиенту 127.0.0.1:65060 отправлены данные

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

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

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

var tcpListener = new TcpListener(IPAddress.Any, 8888);

try
{
    tcpListener.Start();    // запускаем сервер
    Console.WriteLine("Сервер запущен. Ожидание подключений... ");

    while (true)
    {
        // получаем подключение в виде TcpClient
        using var tcpClient = await tcpListener.AcceptTcpClientAsync();
        // получаем объект NetworkStream для взаимодействия с клиентом
        var stream = tcpClient.GetStream();
        // определяем буфер для получения данных
        byte[] responseData = new byte[1024];
        int bytes = 0; // количество считанных байтов
        var response = new StringBuilder(); // для склеивания данных в строку
        // считываем данные 
        do
        {
            bytes = await stream.ReadAsync(responseData);
            response.Append(Encoding.UTF8.GetString(responseData, 0, bytes));
        }
        while(bytes > 0);
        // выводим отправленные клиентом данные
        Console.WriteLine(response);
    }
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}
finally
{
    tcpListener.Stop(); // останавливаем сервер
}

Здесь получаем у tcpClient поток NetworkStream и используем его для считывания данных от клиента. В частности, с помощью метода stream.ReadAsync() считываем данные из потока в массив байтов. В данном случае предполагаем, что данные будут представлять строку. И после получения из байтов строки выводим ее на консоль.

На клиенте определим простейший код для отправки некоторой строки:

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

using TcpClient tcpClient = new TcpClient();
await tcpClient.ConnectAsync("127.0.0.1", 8888);

// сообщение для отправки
var message = "Hello METANIT.COM";
// считыванием строку в массив байт
byte[] requestData = Encoding.UTF8.GetBytes(message);
// получаем NetworkStream для взаимодействия с сервером
var stream = tcpClient.GetStream();
// отправляем данные
await stream.WriteAsync(requestData);
Console.WriteLine("Сообщение отправлено");

В данном случае с помощью метода stream.WriteAsync() отправляем данные серверу. В качестве данных выступает простая строка.

Запустим сервер и клиент. После успешного подключения и отправки данных на консоли клиента мы увидим соответствующее сообщение:

Сообщение отправлено

А сервер получит от клиента сообщение и отобразит его на консоли:

Сервер запущен. Ожидание подключений...
Hello METANIT.COM

Стратегии получения данных

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

  • Использование буфера фиксированной длины, когда мы точно знаем, какой именно объем данных будет послан

  • Отправка в ответе информации о размере ответа, получив которую, нам будет проще считать нужное количество байтов

  • Использование маркера окончания ответа, получив который, мы завершим считывание данных

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

Версия с фиксированным буфером довольно очевидная, поэтому рассмотрим две остальных стратегии.

Использование маркера окончания ответа

Определим следующий код сервера:

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

var tcpListener = new TcpListener(IPAddress.Any, 8888);

try
{
    tcpListener.Start();    // запускаем сервер
    Console.WriteLine("Сервер запущен. Ожидание подключений... ");

    while (true)
    {
        // получаем подключение в виде TcpClient
        using var tcpClient = await tcpListener.AcceptTcpClientAsync();
        // получаем объект NetworkStream для взаимодействия с клиентом
        var stream = tcpClient.GetStream();
        // буфер для входящих данных
        var buffer = new List<byte>();
        int bytesRead = '\n';
        // считываем данные до конечного символа
        while((bytesRead = stream.ReadByte())!='\n')
        {
            // добавляем в буфер
            buffer.Add((byte)bytesRead);
        }
        var message = Encoding.UTF8.GetString(buffer.ToArray());
        Console.WriteLine($"Получено сообщение: {message}");
    }
}
finally
{
    tcpListener.Stop(); // останавливаем сервер
}

Здесь с помощью метода stream.ReadByte() считываем каждый байт из потока. Результатом метода является представление байта в виде int. Допустим, здесь в качестве маркера окончания сообщения будет выступать символ \n или перевод строки, который представляет значение 10. И если встретился перевод строки, то заканчиваем чтение данных и конвертируем полученные данные в строку.

В этом случае клиент должен будет добавлять в конец сообщения символ \n:

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

using TcpClient tcpClient = new TcpClient();
await tcpClient.ConnectAsync("127.0.0.1", 8888);

// сообщение для отправки
// сообщение завершается конечным символом - \n,
// который символизирует окончание сообщения
var message = "Hello METANIT.COM\n";
// получаем NetworkStream для взаимодействия с сервером
var stream = tcpClient.GetStream();
// считыванием строку в массив байт
byte[] requestData = Encoding.UTF8.GetBytes(message);
// отправляем данные
await stream.WriteAsync(requestData);
Console.WriteLine("Сообщение отправлено");

Хотя это в какой-то степени несколько примитивное упрощение, поскольку в данном случае мы ограничиваемся отправкой однострочного текста. Но в реальности логика определения конечного маркера может быть более сложной, особенно когда маркер представляет не одним байт/символ, а несколько, но общий принип будет тем же.

Установка размера сообщения

Определим следующий сервер:

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

var tcpListener = new TcpListener(IPAddress.Any, 8888);

try
{
    tcpListener.Start();    // запускаем сервер
    Console.WriteLine("Сервер запущен. Ожидание подключений... ");

    while (true)
    {
        // получаем подключение в виде TcpClient
        using var tcpClient = await tcpListener.AcceptTcpClientAsync();
        // получаем объект NetworkStream для взаимодействия с клиентом
        var stream = tcpClient.GetStream();

        // буфер для считывания размера данных
        byte[] sizeBuffer = new byte[4];
        // сначала считываем размер данных
        await stream.ReadExactlyAsync(sizeBuffer, 0, sizeBuffer.Length);
        // узнаем размер и создаем соответствующий буфер
        int size = BitConverter.ToInt32(sizeBuffer, 0);
        // создаем соответствующий буфер
        byte[] data = new byte[size];
        // считываем собственно данные
        int bytes = await stream.ReadAsync(data);
        var message = Encoding.UTF8.GetString(data, 0, bytes);
        Console.WriteLine($"Размер сообщения: {size} байтов");
        Console.WriteLine($"Сообщение: {message}");
    }
}
finally
{
    tcpListener.Stop(); // останавливаем сервер
}

В данном случае мы предполагаем, что размер будет представлять значение типа int - то есть значение в 4 байта. Соответственно для считывания размера создаем буфер из 4 байт и считываем его с помощью метода stream.ReadExactlyAsync, который считывает определенное количество байт из потока (в данном случае 4 байта):

await stream.ReadExactlyAsync(sizeBuffer, 0, sizeBuffer.Length);

Считав размер, мы можем конвертировать его в число int с помощью статического метода BitConverter.ToInt32(), определить буфер соответствующей длины и считать в него данные.

На стороне клиента определим следующий код:

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

using TcpClient tcpClient = new TcpClient();
await tcpClient.ConnectAsync("127.0.0.1", 8888);

// сообщение для отправки
var message = "Hello METANIT.COM";
// получаем NetworkStream для взаимодействия с сервером
var stream = tcpClient.GetStream();
// считыванием строку в массив байт
byte[] data = Encoding.UTF8.GetBytes(message);
// определяем размер данных
byte[] size = BitConverter.GetBytes(data.Length);
// отправляем размер данных
await stream.WriteAsync(size);
// отправляем данные
await stream.WriteAsync(data);
Console.WriteLine("Сообщение отправлено");

Здесь происходит обратный процесс. Сначала получаем размер сообщения в массив байтов методом BitConverter.GetBytes():

byte[] size = BitConverter.GetBytes(data.Length);

Отправляем размер в виде четырех байтов и затем отправляем сами данные:

await stream.WriteAsync(size);
await stream.WriteAsync(data);

В итоге после отправки клиентом данных консоль сервера отобразит размер данных и сами данные:

Сервер запущен. Ожидание подключений...
Размер сообщения: 17 байтов
Сообщение: Hello METANIT.COM

Множественная отправка и получение

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

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

var tcpListener = new TcpListener(IPAddress.Any, 8888);

try
{
    tcpListener.Start(); 
    Console.WriteLine("Сервер запущен. Ожидание подключений... ");

    while (true)
    {
        // получаем подключение в виде TcpClient
        using var tcpClient = await tcpListener.AcceptTcpClientAsync();
        // получаем объект NetworkStream для взаимодействия с клиентом
        var stream = tcpClient.GetStream();
        // буфер для входящих данных
        var buffer = new List<byte> ();
        int bytesRead = 10;
        while(true)
        {
            // считываем данные до конечного символа
            while ((bytesRead = stream.ReadByte()) != '\n')
            {
                // добавляем в буфер
                buffer.Add((byte)bytesRead);
            }
            var message = Encoding.UTF8.GetString(buffer.ToArray());
            // если прислан маркер окончания взаимодействия,
            // выходим из цикла и завершаем взаимодействие с клиентом
            if (message == "END") break;    
            Console.WriteLine($"Получено сообщение: {message}");
            buffer.Clear();
        }
    }
}
finally
{
    tcpListener.Stop();
}

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

while(true)
{
    // считываем данные до конечного символа
    while ((bytesRead = stream.ReadByte()) != '\n')
    {
        // добавляем в буфер
        buffer.Add((byte)bytesRead);
    }

Если пришла команда "END", выходит из бесконечного цикла:

    var message = Encoding.UTF8.GetString(buffer.ToArray());
    // если прислан маркер окончания взаимодействия,
    // выходим из цикла и завершаем взаимодействие с клиентом
     if (message == "END") break;    
    Console.WriteLine($"Получено сообщение: {message}");
    buffer.Clear();
}

Для работы с этим сервером определим следующий клиент:

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

using TcpClient tcpClient = new TcpClient();
await tcpClient.ConnectAsync("127.0.0.1", 8888);

// сообщения для отправки
// сообщение завершается конечным символом - \n,
// который символизирует окончание сообщения
var messages = new string[] { "Hello METANIT.COM\n", "Hello Tcplistener\n", "Bye METANIT.COM\n", "END\n" };
// получаем NetworkStream для взаимодействия с сервером
var stream = tcpClient.GetStream();
foreach (var message in messages)
{
    // считыванием строку в массив байт
    byte[] data = Encoding.UTF8.GetBytes(message);
    // отправляем данные
    await stream.WriteAsync(data);
}
Console.WriteLine("Все сообщения отправлены");

Здесь каждое отправляемое сообщение оканчивается терминальным символом \n, кроме того, последнее сообщение представляет команду окончания взаимодействия "END". В итоге при подключении этого клиента сервер оторазит на консоли все присланные сообщения, кроме команды "END", которая завершит взаимодействие клиента и сервера:

Сервер запущен. Ожидание подключений...
Получено сообщение: Hello METANIT.COM
Получено сообщение: Hello Tcplistener
Получено сообщение: Bye METANIT.COM
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850