TCPClient и TCPListener - NetworkStream - порядок сообщений

у меня есть простой вопрос относительно порядка отправленных и полученных сообщений через классы TCP, я не могу найти 100% ответ, и мой английский не достаточно хорош.

Если у меня есть следующий пример:

Сервер:

        IPAddress IP = IPAddress.Parse("127.0.0.1");
        int Port = 13000;

        TcpListener Server = new TcpListener(IP, Port);
        TcpClient Client = Server.AcceptTcpClient();
        NetworkStream Stream = Client.GetStream();

        Stream.Write(Buffer1, 0, 4);
        //random time
        Stream.Write(Buffer2, 0, 4);
        //random time
        Stream.Write(Buffer3, 0, 4);

и Клиент:

        TCPClient Client = new TcpClient("127.0.0.1", 13000);
        NetworkStream Stream = Client.GetStream();

        Stream.Read(A, 0, 4);
        //random time
        Stream.Read(B, 0, 4);
        //random time
        Stream.Read(C, 0, 4);

Уверен ли я на 100%, что я получу A = Buffer1, B = Buffer2, C = Buffer3?


person Erik Šťastný    schedule 25.11.2016    source источник
comment
Нет! Вы должны проверить возвращаемое значение Stream.Read, чтобы увидеть, сколько байтов вы на самом деле получили. TCP является упорядоченным (это означает, что вы гарантированно получите байты в том порядке, в котором вы их отправили), но также и потоковым (это означает, что пакетирование байтов не гарантируется). Чтобы сделать это более явным: TCP не имеет сообщений. Если вам нужны сообщения, вам придется создавать их самостоятельно (наиболее распространенным подходом является префикс длины). Обратитесь к любому учебнику по сетевому коду, чтобы узнать, как написать правильный цикл приема.   -  person Jeroen Mostert    schedule 25.11.2016
comment
Я уже реализовал этот префикс, первые 4 байта моего сообщения - это длина сообщения, поэтому, с другой стороны, я прочитал первые 4 байта, а затем установил NumberOfBytesToRead на это число для другого чтения. Я просто хотел подтвердить порядок сообщений, на которые вы, вероятно, ответили в первой части вашего комментария :)   -  person Erik Šťastný    schedule 25.11.2016
comment
Будьте осторожны: вам не гарантируется даже получение 4 байтов, если вы запросите 4 байта (они могут быть разделены между пакетами), поэтому даже чтение длины должно выполняться в цикле, пока вы не получите эти 4 байта. И тогда вам нужно будет снова зациклиться, чтобы получить полное сообщение. Неправильное понимание этого, вероятно, является самой распространенной ошибкой в ​​​​сетевом программировании, и особенно коварной, потому что такой код может очень хорошо работать случайно или в тестовой среде, а затем дать сбой в рабочей среде.   -  person Jeroen Mostert    schedule 25.11.2016
comment
Вероятно, это будет мой пример, я отправляю сообщения размером ‹ 1 КБ, и все работает нормально.   -  person Erik Šťastný    schedule 25.11.2016
comment
@JeroenMostert, даже если клиент запрашивает 4 байта, а сервер отправляет 2 и 2 в разных пакетах, клиент будет ждать, пока не прочитает 4 байта.   -  person George Chondrompilas    schedule 25.11.2016
comment
@GeorgeChond Ааа, значит, он будет работать и для очень длинных сообщений, а комментарий Jeroen Mostert не соответствует действительности?   -  person Erik Šťastný    schedule 25.11.2016
comment
@GeorgeChond: это явно не то, что произойдет. Предположим, второй пакет задержан или потерян? Stream.Read будет читать доступные байты. Он не будет ждать, пока будет доступно столько байтов, сколько вы укажете. Именно поэтому он в первую очередь возвращает количество прочитанных байтов — иначе в этом не было бы необходимости.   -  person Jeroen Mostert    schedule 25.11.2016
comment
Для получения дополнительной информации ознакомьтесь с этим ответом полностью. Это описано в разделе № 4.   -  person Jeroen Mostert    schedule 25.11.2016
comment
@JeroenMostert нет, на самом деле он просто заблокирует поток до тех пор, пока не будут прочитаны все указанные байты, если вы не включили тайм-аут.   -  person George Chondrompilas    schedule 25.11.2016
comment
@GeorgeChond: К сожалению, у меня нет времени писать код и доказывать, что это неправильно, с помощью практической демонстрации. Попробуйте сами, если хотите: получите 10 000 байт на стороне клиента, отправьте один байт на стороне сервера, используйте бесконечный тайм-аут (который в любом случае используется по умолчанию). Клиент вернет один байт, не дожидаясь, пока сервер будет достаточно любезен, чтобы отправить еще 9999 байтов. Чтобы получить описанное вами поведение, вам нужно обернуть поток в BinaryReader, который выполняет буферизацию. Stream нет.   -  person Jeroen Mostert    schedule 25.11.2016
comment
@JeroenMostert да, вы правы, меня смутило поведение BinaryReader. Спасибо что подметил это!   -  person George Chondrompilas    schedule 25.11.2016
comment
Итак, если я правильно понимаю, ребята, мне нужно прочитать первые четыре байта, что означает длину сообщения, а затем настроить условие, которое ожидает всех байтов, упомянутых в первых 4 байтах, верно?   -  person Erik Šťastný    schedule 25.11.2016
comment
Вы можете использовать BinaryReader для удобства, потому что он буферизует для вас - читайте длину, а затем полное сообщение. Таким образом, вам не нужен собственный цикл. Но если вы используете NetworkStream напрямую, вы должны зацикливаться, даже чтобы прочитать только первые 4 байта.   -  person Jeroen Mostert    schedule 25.11.2016
comment
Итак, я буду использовать конструктор BinaryReader(Stream), а затем я смогу легко использовать BinaryReader.Read вместо NetworkStream.Read для чтения, верно?   -  person Erik Šťastný    schedule 28.11.2016
comment
Кстати, я нашел это для BinaryReader.Read (Byte[], Int32, Int32): количество байтов, прочитанных в буфер. Это может быть меньше запрошенного количества байтов, если такое количество байтов недоступно, или может быть равно нулю, если достигнут конец потока.   -  person Erik Šťastný    schedule 28.11.2016
comment
Что ж, это было интересное и слишком сложное обсуждение такого простого вопроса. :)   -  person Visual Vincent    schedule 29.11.2016
comment
Чтобы на самом деле ответить на ваш вопрос: TCP хранит данные в том порядке, в котором вы их отправили, поэтому конечная точка всегда будет получать Buffer1, Buffer2 и Buffer3 в этом порядке. Однако как говорили другие, данные могут быть разделены на части или объединены в блоки, поэтому вы должны разработать надежная пакетная система для правильного чтения данных (которая, насколько я понимаю, у вас уже есть: так называемый метод префикса длины).   -  person Visual Vincent    schedule 29.11.2016
comment
Да, как вы сказали, у меня есть префикс 4B, который означает длину сообщения. Итак, я должен циклически читать для получения первых четырех байтов, а затем еще один цикл для получения количества байтов, упомянутых в первых 4 байтах, верно?   -  person Erik Šťastný    schedule 29.11.2016


Ответы (1)


Нет никакой гарантии, что для каждой операции NetworkStream.Write будут соответствующие операции NetworkStream.Read, которые вернут точные данные, которые были записаны в поток. Вот простой пример соединения TcpListner и TcpClient:

public class NetworkUtils{

    //Client
    TcpClient client = null;
    int port = 40555;
    string serverIpAddress = "127.0.0.1";
    public Mutex mut = new Mutex();
    int byteToExpecting = 0;
    int savedBufferOffset = 0;
    Byte[] saveDataBuffer = new Byte[20000];
    NetworkStream stream;

    public string ServerIpAddress
    {
        get { return serverIpAddress; }
        set { serverIpAddress = value;}
    }

    string lastSentMsg = String.Empty;
    public string LastSentMsg
    {
        get { return lastSentMsg; }
        set { lastSentMsg = value;}
    }

    //Server
    string clientMsg = String.Empty;
    public string ClientMsg
    {
        get { return clientMsg; }
    }
    public void ClearClientMsg()
    {
        clientMsg = String.Empty;
    }

    TcpListener server=null;

    private string errMsg = String.Empty;
    public string ErrMsg
    {
        get { return errMsg; }
        set { errMsg = value;}
    }

    void ConnectToServer()
    {
        client = new TcpClient(serverIpAddress, port);  
    }

    public bool ClientSendMsg(string message)
    {
        try{

            ConnectToServer();

            Byte[] lengthByteArr = IntToByteArr(message.Length);
            client.GetStream().Write(lengthByteArr, 0, lengthByteArr.Length);

            Byte[] data = Encoding.ASCII.GetBytes(message);
            client.GetStream().Write(data, 0, data.Length);

            client.GetStream().Close();
        }
        catch (Exception e) 
        {
            errMsg = e.Message;
        }

        return errMsg.Length == 0;
    }

    public bool LaunchServer() {
        try {
            IPAddress localAddr = IPAddress.Parse("127.0.0.1");
            server = new TcpListener(localAddr, port);
            server.Start();
            ListenToClients();
        }
        catch(Exception e)
        {
            server.Stop();
        }

        return errMsg.Length == 0;
    }

    void ProcessInformation(IAsyncResult result)
    {
        try{

            TcpClient client;
            client = server.EndAcceptTcpClient(result);
            stream = client.GetStream();
            stream.BeginRead(saveDataBuffer, 0, sizeof(Int32), new AsyncCallback(callbackGetHeadrer), null);
            ListenToClients ();
        }
        catch(Exception e)
        {
            errMsg = e.Message;
            server.Stop();
        }
    }

    void callbackGetHeadrer (IAsyncResult asyncResult) { 
        int lenToRead = stream.EndRead(asyncResult);

        savedBufferOffset = 0;
        byteToExpecting = ByteArrToInt (saveDataBuffer);
        saveDataBuffer = new byte[byteToExpecting];
        stream.BeginRead (saveDataBuffer, 0, byteToExpecting, callback, null);
    }

    void callback (IAsyncResult asyncResult) { 

        int lenToRead = stream.EndRead(asyncResult);

        byteToExpecting -= lenToRead;
        savedBufferOffset += lenToRead;

        /*No one is gurentee that the 'lenToRead' will be correspanding to NetworkStream.Write execution order.
        We need to keep read from the stream until we will get waht we are expecting accrding 'byteToExpecting'
        So here we are keep calling 'stream.BeginRead'.*/
        if (byteToExpecting > 0) {
            stream.BeginRead (saveDataBuffer, savedBufferOffset, byteToExpecting, callback, null);
        } 
        else{
            mut.WaitOne();
            clientMsg = System.Text.Encoding.ASCII.GetString(saveDataBuffer,0, saveDataBuffer.Length);
            mut.ReleaseMutex();

            savedBufferOffset = 0;
            stream.Close();
            client.Close();
        }
    }

    bool ListenToClients()
    {
        try{
            server.BeginAcceptTcpClient( new AsyncCallback( ProcessInformation), server);
        }
        catch(Exception e)
        {
            errMsg = e.Message;
            server.Stop();
        }

        return errMsg.Length == 0;
    }

    public Byte[] IntToByteArr(Int32 intValue)
    {
        byte[] intBytes = BitConverter.GetBytes(intValue);

        if (BitConverter.IsLittleEndian)
            Array.Reverse(intBytes);
        return intBytes;
    }

    public Int32 ByteArrToInt(Byte[] intByteArr)
    {
        Int32 Int32_NUM_OF_BYTES = 4;
        Byte[] buffer = new Byte[Int32_NUM_OF_BYTES];

        for (int i = 0; i < Int32_NUM_OF_BYTES; ++i)
            buffer [i] = intByteArr [i];

        if (BitConverter.IsLittleEndian)
            Array.Reverse (buffer);

        return BitConverter.ToInt32 (buffer, 0);
    }
}

Обратите внимание, что callbackGetHeadrer отвечает за получение размера данных, который мы ожидаем получить. позже мы продолжаем читать из потока, используя «stream.BeginRead», пока не получим то, что ожидаем, в отношении последовательности операции «stream.Write».

person user3455965    schedule 03.05.2017
comment
Да, вы правы, я использовал что-то похожее. Кстати, из .NET 4.5 вы можете использовать асинхронное ожидание. NetworkStream имеет новые методы, такие как .ReadAsync и .WriteAsync, которые намного удобнее для пользователя, чем старые .BeginRead. - person Erik Šťastný; 03.05.2017