Гарантированнная доставка сообщения по протоколу TCP

276
28 мая 2018, 06:30

Всем привет. Уже несколько дней ломаю голову над одной проблемой. Как то раз нашёл пример многопользовательского клиент-серверного консольного TCP чата. И если я не ошибаюсь, то протокол TCP не гарантирует доставку сообщения. И в случае если сообщение не будет доставлено полностью или доставлено вообще, то надо предпринять какие-то меры. Данная программа если я не ошибаюсь, не предпринимает никаких действий если сообщения не будет доставлено полностью. Вопрос: Какие меры надо предпринять чтобы обеспечить гарантированную доставку сообщения или же, что надо делать если сообщения не будет доставлено полностью? Или может быть программа принимает какие либо меры, если сообщение не будет доставлено полностью? Если не трудно, то покажите,что надо добавить в программу. Программа имеет 2 проекта Клиент и Сервер. Код Клиента: Класс Program:

using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ChatClient
{
    class Program
    {
        static string userName; 
        private const string host = "192.168.0.107"; 
        private const int port = 20113; 
        static TcpClient client; 
        static NetworkStream stream; // Создаём объект NetworkStream, через него можно отправлять сообщения серверу или наоборот получать
        static void Main(string[] args)
        {
            Console.Write("Введите свое имя: ");
            userName = Console.ReadLine();
            client = new TcpClient();
            try
            {
                client.Connect(host, port); //подключение клиента
                stream = client.GetStream(); // возвращает объект NetworkStream
                string message = userName;
                byte[] data = Encoding.Unicode.GetBytes(message); //Присваиваем массиву data перекодированное сообщение
                stream.Write(data, 0, data.Length); // Передаем массив data
                // запускаем новый поток для получения данных
                Thread receiveThread = new Thread(new ThreadStart(ReceiveMessage));
                receiveThread.Start(); //старт потока
                Console.WriteLine("Добро пожаловать, {0}", userName);
                SendMessage();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            finally
            {
                Disconnect();
            }
        }
        // отправка сообщений
        static void SendMessage()
        {
           label1: Console.WriteLine("\nВведите сообщение: ");
            while (true)
            {
                string message = Console.ReadLine();
                byte[] data = Encoding.Unicode.GetBytes(message);
                if (message != "exit")
                {
                    stream.Write(data, 0, data.Length);
                }
                else
                {
                    message = "exit";
                label2: Console.WriteLine("Вы действительно хотите выйти из чата Y / N:");
                    switch (Console.ReadKey().Key)
                    {
                        case ConsoleKey.Y:
                            stream.Write(data, 0, data.Length);
                            Disconnect();
                            break;
                        case ConsoleKey.N:
                            goto label1;
                        default:
                            Console.WriteLine("Введите Y / N\n");
                            goto label2;
                    }    
                }
            }
        }
        // получение сообщений
        static void ReceiveMessage()
        {
            while (true)
            {
                try
                {
                    byte[] data = new byte[64]; // буфер для получаемых данных
                    StringBuilder builder = new StringBuilder();
                    int bytes = 0;
                    do
                    {
                        bytes = stream.Read(data, 0, data.Length);
                        builder.Append(Encoding.Unicode.GetString(data, 0, bytes));
                    }
                    while (stream.DataAvailable);
                    string message = builder.ToString();
                    Console.WriteLine(message);//вывод сообщения
                }
                catch
                {
                    Console.WriteLine("Подключение прервано!"); //соединение было прервано
                    Console.ReadLine();
                    Disconnect();
                }
            }
        }
        static void Disconnect()
        {
            Console.WriteLine("disconect");
            if (stream != null)
                stream.Close();//отключение потока
            if (client != null)
                client.Close();//отключение клиента
            Environment.Exit(0); //завершение процесса  
        }
    }
}

Код Сервера: Класс ServerObject:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading;
namespace ChatServer
{
    public class ServerObject
    {
            static TcpListener tcpListener; // сервер для прослушивания
            List<ClientObject> clients = new List<ClientObject>(); // все подключения
            protected internal void AddConnection(ClientObject clientObject)
            {
                clients.Add(clientObject);
            }
            protected internal void RemoveConnection(string id)
            {
                // получаем по id закрытое подключение
                ClientObject client = clients.FirstOrDefault(c => c.Id == id);
                // и удаляем его из списка подключений
                if (client != null)
                    clients.Remove(client);
            }
            // прослушивание входящих подключений
            protected internal void Listen()
            {
                try
                {
                    tcpListener = new TcpListener(IPAddress.Any, 8888);
                    tcpListener.Start();
                    Console.WriteLine("Сервер запущен. Ожидание подключений...");
                    while (true)
                    {
                        TcpClient tcpClient = tcpListener.AcceptTcpClient(); //Приём ожидающего запроса на подключение
                        ClientObject clientObject = new ClientObject(tcpClient, this);
                        Thread clientThread = new Thread(new ThreadStart(clientObject.Process));
                        clientThread.Start();
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    Disconnect();
                }
            }
            // трансляция сообщения подключенным клиентам
            protected internal void BroadcastMessage(string message, string id)
            {
                byte[] data = Encoding.Unicode.GetBytes(message);
                for (int i = 0; i < clients.Count; i++)
                {
                    if (clients[i].Id != id) // если id клиента не равно id отправляющего
                    {
                        clients[i].Stream.Write(data, 0, data.Length); //передача данных
                    }
                }
            }
            // отключение всех клиентов
            protected internal void Disconnect()
            {
                tcpListener.Stop(); //остановка сервера
                for (int i = 0; i < clients.Count; i++)
                {
                    clients[i].Close(); //отключение клиента
                }
                Environment.Exit(0); //завершение процесса
            }
        }
    }

Класс ClientObject:

using System;
using System.Net.Sockets;
using System.Text;
namespace ChatServer
{
    public class ClientObject
    {
        protected internal string Id { get; private set; }
        protected internal NetworkStream Stream { get; private set; }
        string userName;
        TcpClient client;
        ServerObject server; // объект сервера
        public ClientObject(TcpClient tcpClient, ServerObject serverObject)
        {
            Id = Guid.NewGuid().ToString();
            client = tcpClient;
            server = serverObject;
            serverObject.AddConnection(this);
        }
        public void Process()
        {
            try
            {
                Stream = client.GetStream();
                // получаем имя пользователя
                string message = GetMessage();
                userName = message;
                string s = new String('*', 6);
                message = userName + " вошел в чат";
                // посылаем сообщение о входе в чат всем подключенным пользователям
                server.BroadcastMessage(message, this.Id);
                Console.WriteLine(message);
                // в бесконечном цикле получаем сообщения от клиента
                while (true) 
                {
                    try
                    {
                        message = GetMessage();
                    if (message == "exit")
                        {
                            message = String.Format($"{s}{userName}: покинул чат{s}");
                            Console.Write(message);
                            server.BroadcastMessage(message, this.Id);
                            break;
                        }
                    else
                        {
                            message = String.Format($"{userName}: {message}");
                            Console.WriteLine(message);
                            server.BroadcastMessage(message, this.Id);
                        }
                    }
                    catch
                    {
                        message = String.Format($"{s}{userName}: покинул чат{s}");
                        Console.Write(message);
                        server.BroadcastMessage(message, this.Id);
                        break;
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            finally
            {
                // в случае выхода из цикла закрываем ресурсы
                server.RemoveConnection(this.Id);
                Close();
            }
        }
        // чтение входящего сообщения и преобразование в строку
        private string GetMessage()
        {
            byte[] data = new byte[64]; // буфер для получаемых данных
            StringBuilder builder = new StringBuilder();
            int bytes = 0;
            do
            {
                bytes = Stream.Read(data, 0, data.Length);
                builder.Append(Encoding.Unicode.GetString(data, 0, bytes));
            }
            while (Stream.DataAvailable);
            return builder.ToString();
        }
        // закрытие подключения
        protected internal void Close()
        {
            if (Stream != null)
                Stream.Close();
            if (client != null)
                client.Close();
        }
    }
}

Класс Program:

using System;
using System.Threading;
namespace ChatServer
{
    class Program
    {
            static ServerObject server; // сервер
            static Thread listenThread; // потока для прослушивания
            static void Main(string[] args)
            {
                try
                {
                    server = new ServerObject();
                    listenThread = new Thread(new ThreadStart(server.Listen));
                    listenThread.Start(); //старт потока
                }
                catch (Exception ex)
                {
                    server.Disconnect();
                    Console.WriteLine(ex.Message);
                }
            }
        }
    }

Пытался сделать так как посоветовал PashaPash, но всё без результатно. Как мне посоветовал PashaPash я добавил признак окончания сообщения '/'

do
{
}
while (bytes==(byte)'/');

Если я правильно понял данные должны считываться как-то с помощью StreamReader Но как накапливать данные в MemoryStream не очень понятно?

Answer 1

TCP гарантирует доставку.

Проблема в вашем коде в том, что он предполагает, что то, что передано в один вызов Write, будет вычитано одним вызовом Read на другой стороне. А это не так.

Write не "отправляет пакет". Он просто пишет данные в сокет.

А Read не "читает пакет". Он вычитывает из буфера сокета то, что успело дойти.

Если вы сделали два вызова Write, подождали, и сделали один Read - вычитаются данные обоих вызовов.

Если вы сделали вызов Write, и данные не успели дойти - Read вычитает только начало (начало строки, в вашем случае).

Обработка в цикле с builder.Append - ненадежна, т.к. ваш код читает из локального буфера, очень быстро (быстрее, чем данные идут по сети!), и достигает состояния !Stream.DataAvailable где-то на середине сообщения. Кроме того, он считает, что каждый результат Read может быть преобразован в Unicode строку. И никак не учитывает, что может получить фрагмент с половиной символа в конце или начале.

Надежные способы, на выбор:

  1. Передавать перед каждым сообщением его длину. Не читать напрямую из Stream - обернуть его в BinaryReader, и читать длину (reader.ReadInt32()), потом - ровно столько байт, сколько нужно (reader.GetBytes(messageLength) будет ждать, пока не придет нужное количество байт). Или использовать binaryWriter.Write(message) / binaryReader.ReadString(), который автоматически делают то же самое за вам.
  2. Ввести признак окончания сообщения, например символ переноса строки. Читать до тех пор, пока в прочитанном фрагмент нет соответствующего байта (а не по Stream.DataAvailable). И при этом накапливать вычитанное в MemoryStream, в виде байт чтобы избежать проблем с половиной юникодового символа. В конце преобразовывать в строчку все сообщение целиком.

Пример на коде из вопроса (с минимальными изменениями):

Клиент - просто заменяете работу со stream на работу с reader / writer:

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ChatClient
{
    class Program
    {
        static string userName;
        private const string host = "127.0.0.1";
        private const int port = 8888;
        static TcpClient client;
        static BinaryReader reader;
        static BinaryWriter writer;
        static void Main(string[] args)
        {
            Console.Write("Введите свое имя: ");
            userName = Console.ReadLine();
            client = new TcpClient();
            try
            {
                client.Connect(host, port); //подключение клиента
                var stream = client.GetStream(); // возвращает объект NetworkStream
                reader = new BinaryReader(stream, Encoding.Unicode, true);
                writer = new BinaryWriter(stream, Encoding.Unicode, true);
                writer.Write(userName);
                // запускаем новый поток для получения данных
                Thread receiveThread = new Thread(new ThreadStart(ReceiveMessage));
                receiveThread.Start(); //старт потока
                Console.WriteLine("Добро пожаловать, {0}", userName);
                SendMessage();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            finally
            {
                Disconnect();
            }
        }
        // отправка сообщений
        static void SendMessage()
        {
            label1: Console.WriteLine("\nВведите сообщение: ");
            while (true)
            {
                string message = Console.ReadLine();
                if (message != "exit")
                {
                    writer.Write(message);
                }
                else
                {
                    message = "exit";
                    label2: Console.WriteLine("Вы действительно хотите выйти из чата Y / N:");
                    switch (Console.ReadKey().Key)
                    {
                        case ConsoleKey.Y:
                            writer.Write(message);
                            Disconnect();
                            break;
                        case ConsoleKey.N:
                            goto label1;
                        default:
                            Console.WriteLine("Введите Y / N\n");
                            goto label2;
                    }
                }
            }
        }
        // получение сообщений
        static void ReceiveMessage()
        {
            while (true)
            {
                try
                {
                    string message = reader.ReadString();
                    Console.WriteLine(message);//вывод сообщения
                }
                catch
                {
                    Console.WriteLine("Подключение прервано!"); //соединение было прервано
                    Console.ReadLine();
                    Disconnect();
                }
            }
        }
        static void Disconnect()
        {
            Console.WriteLine("disconect");
            if (client != null)
                client.Close();//отключение клиента
            Environment.Exit(0); //завершение процесса  
        }
    }
}

Сервер - то же самое, с указанием той же кодировки:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading;
namespace ChatServer
{
    public class ServerObject
    {
        static TcpListener tcpListener; // сервер для прослушивания
        List<ClientObject> clients = new List<ClientObject>(); // все подключения
        protected internal void AddConnection(ClientObject clientObject)
        {
            clients.Add(clientObject);
        }
        protected internal void RemoveConnection(string id)
        {
            // получаем по id закрытое подключение
            ClientObject client = clients.FirstOrDefault(c => c.Id == id);
            // и удаляем его из списка подключений
            if (client != null)
                clients.Remove(client);
        }
        // прослушивание входящих подключений
        protected internal void Listen()
        {
            try
            {
                tcpListener = new TcpListener(IPAddress.Any, 8888);
                tcpListener.Start();
                Console.WriteLine("Сервер запущен. Ожидание подключений...");
                while (true)
                {
                    TcpClient tcpClient = tcpListener.AcceptTcpClient(); //Приём ожидающего запроса на подключение
                    ClientObject clientObject = new ClientObject(tcpClient, this);
                    Thread clientThread = new Thread(new ThreadStart(clientObject.Process));
                    clientThread.Start();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Disconnect();
            }
        }
        // трансляция сообщения подключенным клиентам
        protected internal void BroadcastMessage(string message, string id)
        {
            for (int i = 0; i < clients.Count; i++)
            {
                if (clients[i].Id != id) // если id клиента не равно id отправляющего
                {
                    clients[i].SendMessage(message); //передача данных
                }
            }
        }
        // отключение всех клиентов
        protected internal void Disconnect()
        {
            tcpListener.Stop(); //остановка сервера
            for (int i = 0; i < clients.Count; i++)
            {
                clients[i].Close(); //отключение клиента
            }
            Environment.Exit(0); //завершение процесса
        }
    }
}
using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
namespace ChatServer
{
    public class ClientObject
    {
        protected internal string Id { get; private set; }
        string userName;
        TcpClient client;
        ServerObject server; // объект сервера
        BinaryWriter writer;
        public ClientObject(TcpClient tcpClient, ServerObject serverObject)
        {
            Id = Guid.NewGuid().ToString();
            client = tcpClient;
            server = serverObject;
            serverObject.AddConnection(this);
        }
        public void Process()
        {
            try
            {
                var stream = client.GetStream();
                this.writer = new BinaryWriter(stream, Encoding.Unicode, false);
                var reader = new BinaryReader(stream, Encoding.Unicode, false);
                // получаем имя пользователя
                string message = reader.ReadString();
                userName = message;
                string s = new String('*', 6);
                message = userName + " вошел в чат";
                // посылаем сообщение о входе в чат всем подключенным пользователям
                server.BroadcastMessage(message, this.Id);
                Console.WriteLine(message);
                // в бесконечном цикле получаем сообщения от клиента
                while (true)
                {
                    try
                    {
                        message = reader.ReadString(); 
                        if (message == "exit")
                        {
                            message = String.Format($"{s}{userName}: покинул чат{s}");
                            Console.Write(message);
                            server.BroadcastMessage(message, this.Id);
                            break;
                        }
                        else
                        {
                            message = String.Format($"{userName}: {message}");
                            Console.WriteLine(message);
                            server.BroadcastMessage(message, this.Id);
                        }
                    }
                    catch
                    {
                        message = String.Format($"{s}{userName}: покинул чат{s}");
                        Console.Write(message);
                        server.BroadcastMessage(message, this.Id);
                        break;
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            finally
            {
                // в случае выхода из цикла закрываем ресурсы
                server.RemoveConnection(this.Id);
                Close();
            }
        }
        internal void SendMessage(string message)
        {
            this.writer.Write(message);
        }
        public void Close()
        {
            if (client != null)
                client.Dispose();
        }
    }
}
READ ALSO
C# наследования

C# наследования

Есть клас родительский и дочерний клас Foo и FooInherited соответствено

168
Книги и учебные ресурсы по C#

Книги и учебные ресурсы по C#

Вопросы о литературе по различным языкам программирования возникают очень частоЗдесь мы попробуем собрать лучшие ответы и рекомендации...

614
C# вложеные класы

C# вложеные класы

Существуют два класа : один дочерний, второй родительский

194
Заполнить DataGridView из DataTable

Заполнить DataGridView из DataTable

Нужно заполнить строки DataGridView из DataTableПри заполнении, приведённом ниже, уже имеющийся DGV склеивается с DT

254