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

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

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

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

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

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

IPEndPoint ipPoint = new IPEndPoint(IPAddress.Any, 8888);
using Socket tcpListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

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

    while (true)
    {
        // получаем входящее подключение
        using var tcpClient = await tcpListener.AcceptAsync();
        // определяем данные для отправки - текущее время
        byte[] data = Encoding.UTF8.GetBytes(DateTime.Now.ToLongTimeString());
        // отправляем данные
        await tcpClient.SendAsync(data);
        Console.WriteLine($"Клиенту {tcpClient.RemoteEndPoint} отправлены данные");
    }
}
catch(Exception ex)
{
    Console.WriteLine(ex.Message);
}

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

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

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

using var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
    await tcpClient.ConnectAsync("127.0.0.1", 8888);
    // буфер для считывания данных
    byte[] data = new byte[512];

    // получаем данные из потока
    int bytes = await tcpClient.ReceiveAsync(data);
    // получаем отправленное время
    string time = Encoding.UTF8.GetString(data, 0, bytes);
    Console.WriteLine($"Текущее время: {time}");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

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

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

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

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

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

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

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

using System.Net;
using System.Net.Sockets;
using System.Text;
 
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Any, 8888);
using Socket tcpListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
 
try
{
    tcpListener.Bind(ipPoint);
    tcpListener.Listen();    // запускаем сервер
    Console.WriteLine("Сервер запущен. Ожидание подключений... ");
 
    while (true)
    {
        // получаем подключение в виде TcpClient
        using var tcpClient = await tcpListener.AcceptAsync();
        // определяем буфер для получения данных
        List<byte> response = [];
        byte[] buffer = new byte[512];
        int bytes = 0; // количество считанных байтов
        // считываем данные 
        do
        {
            bytes = await tcpClient.ReceiveAsync(buffer);
            // добавляем полученные байты в список
            response.AddRange(buffer.Take(bytes));
        }
        while (bytes > 0);
        // выводим отправленные клиентом данные
        var responseText = Encoding.UTF8.GetString(response.ToArray());
        Console.WriteLine(responseText);
    }
}
catch(Exception ex)
{
    Console.WriteLine(ex.Message);
}

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

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

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

using var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
    await tcpClient.ConnectAsync("127.0.0.1", 8888);
    // сообщение для отправки
    var message = "Hello METANIT.COM";
    // считыванием строку в массив байт
    byte[] requestData = Encoding.UTF8.GetBytes(message);
    // отправляем данные
    await tcpClient.SendAsync(requestData);
    Console.WriteLine("Сообщение отправлено");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

try
{
    tcpListener.Bind(new IPEndPoint(IPAddress.Any, 8888));
    tcpListener.Listen();    // запускаем сервер
    Console.WriteLine("Сервер запущен. Ожидание подключений... ");

    while (true)
    {
        // получаем подключение в виде TcpClient
        using var tcpClient = await tcpListener.AcceptAsync();

        // буфер для накопления входящих данных
        var buffer = new List<byte>();
        // буфер для считывания одного байта
        var bytesRead = new byte[1];
        // считываем данные до конечного символа
        while (true)
        {
            var count = await tcpClient.ReceiveAsync(bytesRead);
            // смотрим, если считанный байт представляет конечный символ, выходим
            if (count == 0 || bytesRead[0] == '\n') break;
            // иначе добавляем в буфер
            buffer.Add(bytesRead[0]);
        }
        var message = Encoding.UTF8.GetString(buffer.ToArray());
        Console.WriteLine($"Получено сообщение: {message}");
    }
}
catch(Exception ex)
{
    Console.WriteLine(ex.Message);
}

Допустим, здесь в качестве маркера окончания сообщения будет выступать символ \n или перевод строки, который представляет значение 10. Чтобы отследить конечный символ, считываем посимвольно. Для считывания определяем массив из одного байта. Считываем в него по байту и проверяем его значение. И если встретился перевод строки, то заканчиваем чтение данных и конвертируем полученные данные в строку.

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

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

using var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
    await tcpClient.ConnectAsync("127.0.0.1", 8888);
    // сообщение для отправки
    // сообщение завершается конечным символом - \n,
    // который символизирует окончание сообщения
    var message = "Hello METANIT.COM\n";
    // считыванием строку в массив байт
    byte[] requestData = Encoding.UTF8.GetBytes(message);
    // отправляем данные
    await tcpClient.SendAsync(requestData);
    Console.WriteLine("Сообщение отправлено");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

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

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

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

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

try
{
    tcpListener.Bind(new IPEndPoint(IPAddress.Any, 8888));
    tcpListener.Listen();    // запускаем сервер
    Console.WriteLine("Сервер запущен. Ожидание подключений... ");

    while (true)
    {
        // получаем подключение в виде TcpClient
        using var tcpClient = await tcpListener.AcceptAsync();

        // буфер для входящих данных
        var buffer = new List<byte>();
        int bytesRead = '\n';
        // считываем данные до конечного символа
        while (true)
        {
            bytesRead = tcpClient.ReadByte();
            if(bytesRead== '\n' || bytesRead==0) break;
            // добавляем в буфер
            buffer.Add((byte)bytesRead);
        }
        var message = Encoding.UTF8.GetString(buffer.ToArray());
        Console.WriteLine($"Получено сообщение: {message}");
    }
}
catch(Exception ex)
{
    Console.WriteLine(ex.Message);
}

public static class SocketExtension
{
    public static int ReadByte(this Socket socket)
    {
        byte b = 0;
        var buffer = new Span<byte>(ref b);
        var count= socket.Receive(buffer);
        return count== 0 ? -1 : b;
    }
}

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

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

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

using Socket tcpListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
    tcpListener.Bind(new IPEndPoint(IPAddress.Any, 8888));
    tcpListener.Listen();    // запускаем сервер
    Console.WriteLine("Сервер запущен. Ожидание подключений... ");

    while (true)
    {
        // получаем подключение в виде TcpClient
        using var tcpClient = await tcpListener.AcceptAsync();

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

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

byte[] sizeBuffer = new byte[4];
await tcpClient.ReceiveAsync(sizeBuffer);
int size = BitConverter.ToInt32(sizeBuffer, 0);
byte[] data = new byte[size];
int bytes = await tcpClient.ReceiveAsync(data);

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

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

using var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
    await tcpClient.ConnectAsync("127.0.0.1", 8888);
    // сообщение для отправки
    var message = "Hello METANIT.COM";
    // считыванием строку в массив байт
    byte[] data = Encoding.UTF8.GetBytes(message);
    // определяем размер данных
    byte[] size = BitConverter.GetBytes(data.Length);
    // отправляем размер данных
    await tcpClient.SendAsync(size);
    // отправляем данные
    await tcpClient.SendAsync(data);
    Console.WriteLine("Сообщение отправлено");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

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

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

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

await tcpClient.SendAsync(size);
await tcpClient.SendAsync(data);

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

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

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

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

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

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

try
{
    tcpListener.Bind(new IPEndPoint(IPAddress.Any, 8888));
    tcpListener.Listen();    // запускаем сервер
    Console.WriteLine("Сервер запущен. Ожидание подключений... ");

    while (true)
    {
        // получаем подключение в виде TcpClient
        using var tcpClient = await tcpListener.AcceptAsync();

        // буфер для накопления входящих данных
        var buffer = new List<byte>();
        // буфер для считывания одного байта
        var bytesRead = new byte[1];
        while (true)
        {
            // считываем данные до конечного символа
            while (true)
            {
                var count = await tcpClient.ReceiveAsync(bytesRead);
                // смотрим, если считанный байт представляет конечный символ, выходим
                if (count == 0 || bytesRead[0] == '\n') break;
                // иначе добавляем в буфер
                buffer.Add(bytesRead[0]);
            }
            var message = Encoding.UTF8.GetString(buffer.ToArray());
            // если прислан маркер окончания взаимодействия,
            // выходим из цикла и завершаем взаимодействие с клиентом
            if (message == "END") break;
            Console.WriteLine($"Получено сообщение: {message}");
            buffer.Clear();
        }
    }
}
catch(Exception ex)
{
    Console.WriteLine(ex.Message);
}

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

while (true)
{
    while (true)
    {
        var count = await tcpClient.ReceiveAsync(bytesRead);
        if (count == 0 || bytesRead[0] == '\n') break;
        buffer.Add(bytesRead[0]);
    }

Если пришла команда "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 var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
    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" };
    foreach (var message in messages)
    {
        // считыванием строку в массив байт
        byte[] data = Encoding.UTF8.GetBytes(message);
        // отправляем данные
        await tcpClient.SendAsync(data);
    }
    Console.WriteLine("Все сообщения отправлены");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

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

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