получение сообщения с использованием программирования сокетов в c # не работает

1

Я пытаюсь написать программу, используя программирование сокетов. Эта программа просто отправляет сообщение от клиента на сервер.

код клиента выглядит примерно так:

  class Program
    {
        static void Main(string[] args)
        {

            TcpClient client = new TcpClient();
            IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3000);
            client.Connect(serverEndPoint);
            NetworkStream clientStream = client.GetStream();
            ASCIIEncoding encoder = new ASCIIEncoding();
            byte[] buffer = encoder.GetBytes("Hello Server!");
            clientStream.Write(buffer, 0, buffer.Length);
            clientStream.Flush();
        }
    }

Клиент, как вы можете видеть, просто отправляет сервер Hello на сервер. Код сервера выглядит так:

 class Server
    {
        private TcpListener tcpListener;
        private Thread listenThread;

    public Server()
    {
        this.tcpListener = new TcpListener(IPAddress.Any, 3000);
        this.listenThread = new Thread(new ThreadStart(ListenForClients));
        this.listenThread.Start();
    }
    private void ListenForClients()
    {
        this.tcpListener.Start();

        while (true)
        {
            //blocks until a client has connected to the server
            TcpClient client = this.tcpListener.AcceptTcpClient();

            //create a thread to handle communication 
            //with connected client
            Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComm));
            clientThread.Start(client);
        }
    }
    private void HandleClientComm(object client)
    {
        TcpClient tcpClient = (TcpClient)client;
        NetworkStream clientStream = tcpClient.GetStream();

        byte[] message = new byte[4096];
        int bytesRead;

        while (true)
        {
            bytesRead = 0;

            try
            {
                //blocks until a client sends a message
                bytesRead = clientStream.Read(message, 0, 4096);
            }
            catch
            {
                //a socket error has occured
                break;
            }

            if (bytesRead == 0)
            {
                //the client has disconnected from the server
                break;
            }

            //message has successfully been received
            ASCIIEncoding encoder = new ASCIIEncoding();
            System.Diagnostics.Debug.WriteLine(encoder.GetString(message, 0, bytesRead));
            Console.ReadLine();
        }

        tcpClient.Close();
    }

}

И в основном классе я вызываю класс SERVER следующим образом:

 class Program
    {

         Server obj=new Server();
        while (true)
        {

        }
    }

Как видно из кода сервера, я пытаюсь показать сообщение, но оно не работает. Почему это не работает?

С наилучшими пожеланиями

Теги:
sockets

2 ответа

11
Лучший ответ

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


1. Почему программа сервера выходит немедленно?

Причина немедленной выгрузки серверной программы довольно проста: при выводе метода Main весь процесс программы завершается, включая любой поток, принадлежащий этому процессу (например, поток, запущенный в конструкторе класса Server). Чтобы устранить эту проблему, главный метод должен быть заблокирован.

Один общий подход заключается в том, чтобы добавить Console.ReadKey() или Console.ReadLine() качестве последней строки в методе Main, которая не позволит программе сервера выйти до тех пор, пока пользователь не предоставит некоторый ввод на клавиатуре. Однако для конкретного кода, приведенного здесь, этот подход не был бы таким простым решением, потому что метод обработчика соединения с клиентом (HandleClientComm) также читает ввод с клавиатуры клавиатуры (что само по себе может стать проблемой, см. Раздел 5 ниже).

Поскольку я не хочу начинать свой ответ с разработки ввода с клавиатуры, я предлагаю другое и, по общему признанию, более примитивное решение: добавление бесконечного цикла в конец метода Main (который также был включен в отредактированный вопрос):

static void Main(string[] args)
{
    Server srv = new Server();
    for (;;) {}
}

Поскольку код учебника на сервере представляет собой консольное приложение, он все равно может быть прерван нажатием CTRL + C.


2. Сервер, похоже, ничего не делает. Зачем?

Большинство (если не все) ошибок или проблем, возникающих в классах и методах.NET framework, сообщаются через механизм исключительных ситуаций.NET.

И здесь код учебника делает серьезную ошибку в методе сервера для обработки клиентских подключений:

try
{
    //blocks until a client sends a message
    bytesRead = clientStream.Read(message, 0, 4096);
}
catch
{
    //a socket error has occured
    break;
}

Что делает блок try-catch? Ну, единственное, что он делает, - это поймать любое исключение, которое могло бы ответить на вопрос "Почему? - и затем молча и бесцеремонно отбрасывает эту полезную информацию. Уф...!

Разумеется, имеет смысл перехватывать исключения в потоках обработчика подключения клиента. В противном случае исключение uncaught приведет к отключению не только потока обработчика отказавшего клиента, но и всего приложения консоли сервера. Но эти исключения должны решаться значимым образом, чтобы не терялась информация, которая может помочь в устранении неполадок.

Чтобы предоставить ценную информацию об исключениях, внесенных в наш простой код учебника, блок catch выведет информацию об исключении на консоль (а также позаботится о выдаче любых возможных "внутренних исключений").

Кроме того, используются операторы использования, чтобы гарантировать, что объекты NetworkStream, а также объекты TcpClient должным образом закрыты/удалены, даже в ситуациях, когда исключения вызывают выход из потока подключения клиента.

private void HandleClientComm(object client)
{
    using ( TcpClient tcpClient = (TcpClient) client )
    {
        EndPoint remoteEndPoint = tcpClient.Client.RemoteEndPoint;

        try
        {
            using (NetworkStream clientStream = tcpClient.GetStream() )
            {
                byte[] message = new byte[4096];

                for (;;)
                {
                    //blocks until a client sends a message
                    int bytesRead = clientStream.Read(message, 0, 4096);

                    if (bytesRead == 0)
                    {
                        //the client has disconnected from the server
                        Console.WriteLine("Client at IP address {0} closed connection.", remoteEndPoint);
                        break;
                    }

                    //message has successfully been received
                    Console.WriteLine(Encoding.ASCII.GetString(message, 0, bytesRead));

                    // Console.ReadLine() has been removed.
                    // See last section of the answer about why
                    // Console.ReadLine() was of little use here...
                }
            }
        }
        catch (Exception ex)
        {
            // Output exception information
            string formatString = "Client IP address {2}, {0}: {1}";
            do
            {
                Console.WriteLine(formatString, ex.GetType(), ex.Message, remoteEndPoint);
                ex = ex.InnerException;
                formatString = "\tInner {0}: {1}";
            }
            while (ex != null);
        }
    }
}

(Возможно, вы заметили, что код запоминает IP-адрес клиента (общедоступный) в remoteEndPoint. Причина в том, что объект Socket в свойстве tcpClient.Client будет удален, когда NetworkStream будет закрыт, что происходит, когда область соответствующего использования оператор остается, что, в свою очередь, сделает невозможным доступ к tcpClient.Client.RemoteEndPoint в блоке catch впоследствии.)

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

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

Это довольно сильное указание на то, что с клиентом что-то не так, или что какое-то сетевое устройство имеет какую-то странную проблему. Как это случилось, O/P запускает клиентское и серверное программное обеспечение на том же компьютере, используя IP-адрес "127.0.0.1", что вызывает беспокойство по поводу неисправности сетевых устройств в спорных аргументах и, скорее, указывает на проблему с клиентским программным обеспечением.


3. Что случилось с клиентом?

Запуск клиента не вызывает никаких исключений. Глядя на исходный код, можно ошибочно полагать, что код, по-видимому, в основном в порядке: установлено соединение с сервером, буфер байта, заполненный сообщением и записанный в NetworkStream, затем очищается NetworkStream. Однако NetworkStream не закрыт, что, безусловно, связано с проблемой. Но даже если NetworkStream не был закрыт явно, очистка потока должна была отправить сообщение на сервер в любом случае, или...?

Предыдущий параграф содержит два ошибочных предположения. Первое неправильное предположение связано с тем, что NetworkStream не закрывается должным образом. Что происходит в клиентском коде, так это то, что непосредственно после написания сообщения в NetworkStream клиентская программа завершается.

Когда клиентская программа завершает работу, на сервер отправляется сигнал "Соединение завершен" (упрощенный разговор). Если в этот момент сообщение все еще задерживается в некотором буфере, связанным с TCP/IP на стороне отправителя, буфер будет просто отброшен и сообщение больше не будет отправлено. Но даже если сообщение было получено стеком TCP/IP на стороне сервера и хранится в буфере приема, связанным с TCP/IP, более или менее сразу после сигнала "Прекращенный соединение" по-прежнему недействителен этот буфер приема до TCP/IP стек может подтвердить получение этого сообщения и, таким образом, сбой NetworkStream.Read() с вышеупомянутой ошибкой.

Другое ложное предположение (и которое легко упускать из виду кто-то, кто не выполняет регулярное сетевое программирование в.NET) заключается в том, как код пытается использовать NetworkStream.Flush() в попытке принудительно передать сообщение. Давайте рассмотрим раздел "Замечания" документации MSDN NetworkStream.Flush():

Метод Flush реализует метод Stream.Flush; однако, поскольку NetworkStream не буферизирован, он не влияет на сетевые потоки.

Да, вы правильно это прочитали: NetworkStream.Flush() делает точно... Ничего!
(... очевидно, поскольку NetworkStream не хранит никаких данных)

Таким образом, чтобы клиентская программа работала правильно, все, что нужно сделать, - это правильно закрыть клиентское соединение (что также гарантирует, что сообщение отправляется и принимается сервером до того, как соединение будет разорвано). Как уже было показано в приведенном выше коде сервера, мы будем использовать инструкцию using, которая будет заботиться о правильном закрытии и удалении объектов NetworkStream и TcpClient при любых обстоятельствах. Также удаляется вводящий в заблуждение и бесполезный вызов NetworkStream.Flush():

static void Main(string[] args)
{
    IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3000);
    using ( TcpClient client = new TcpClient() )
    {
        client.Connect(serverEndPoint);

        using ( NetworkStream clientStream = client.GetStream() )
        {
            byte[] buffer = Encoding.ASCII.GetBytes("Hello Server!");
            clientStream.Write(buffer, 0, buffer.Length);
        }
    }
}


4. Рекомендации относительно чтения и записи сообщений из/в NetworkStream

Что всегда следует иметь в виду, так это то, что NetworkStream.Read(...) не гарантирует немедленное чтение полного сообщения. В зависимости от размера сообщения NetworkStream.Read(...) может читать только фрагменты сообщения. Причины этого связаны с тем, как клиентское программное обеспечение отправляет данные и как TCP/IP управляет транспортировкой данных через сеть. (Разумеется, с таким коротким сообщением, как "Hello world!", Маловероятно, что вы не столкнетесь с такой ситуацией. Но, в зависимости от вашей серверной и клиентской ОС и используемых драйверов NIC +, вы можете начать просмотр фрагментированных сообщений, если они становятся больше 500-байтов.)

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

Если вы придерживаетесь кодировки ASCII, вы можете выбрать подход, как показано в ответе Александра Бревига, - просто напишите полученные фрагменты сообщения с байтами ASCII в StringBuilder. Однако этот подход работает только надежно, потому что любой символ ASCII представлен одним байтом.

Как только вы используете другую кодировку, которая может кодировать один символ в нескольких байтах (например, для любой кодировки UTF), этот подход не будет работать более надежно. Возможная фрагментация байтовых данных может разбивать последовательность байтов многобайтового символа таким образом, что один вызов NetworkStream.Read просто считывает первый байт такого символа и только последующий вызов NetworkStream.Read будет извлекать остальные байты этого многобайтового символа. Это нарушит декодирование символов, если каждый фрагмент сообщения будет декодирован индивидуально. Таким образом, для таких кодировок, как правило, более безопасно хранить полное сообщение в одном байтовом буфере (массиве) перед выполнением любого текстового декодирования.

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

Но что, если ваш клиент хочет отправить несколько сообщений, или что, если серверу необходимо отправить ответ клиенту? В обоих сценариях клиент не может просто отключиться после отправки первого сообщения. Итак, как сервер узнает, когда сообщение было полностью отправлено?

Очень простой и надежный метод - позволить клиенту добавить сообщение с коротким целым значением (2 байта) или целочисленным значением (4 байта), которое определяет длину сообщения в байтах. Таким образом, после получения этих 2 байтов (или 4 байта) сервер будет знать, сколько еще байтов необходимо прочитать для получения полного сообщения.

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

Клиент, использующий BinaryWriter.Write (строка):

static void Main(string[] args)
{
    IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3000);
    using ( TcpClient client = new TcpClient() )
    {
        client.Connect(serverEndPoint);

        using ( BinaryWriter writer = new BinaryWriter(client.GetStream(), Encoding.ASCII) )
        {
            writer.Write("Hello Server!");
        }
    }
}

Обработчик соединения с клиентским сервером с помощью BinaryReader.ReadString():

private void HandleClientComm(object client)
{
    using ( TcpClient tcpClient = (TcpClient) client )
    {
        EndPoint remoteEndPoint = tcpClient.Client.RemoteEndPoint;

        try
        {
            using ( BinaryReader reader = new BinaryReader(tcpClient.GetStream(), Encoding.ASCII) )
            {
                for (;;)
                {
                    string message = reader.ReadString();
                    Console.WriteLine(message);
                }
            }
        }
        catch (EndOfStreamException ex)
        {
            Console.WriteLine("Client at IP address {0} closed the connection.", remoteEndPoint);
        }
        catch (Exception ex)
        {
            string formatString = "Client IP address {2}, {0}: {1}";
            do
            {
                Console.WriteLine(formatString, ex.GetType(), ex.Message, remoteEndPoint);
                ex = ex.InnerException;
                formatString = "\tInner {0}: {1}";
            }
            while (ex != null);
        }
    }
}

Вы можете заметить особую обработку исключения EndOfStreamException. Это исключение используется BinaryReader, чтобы указать, что конец потока достигнут; т.е. соединение было закрыто клиентом. Вместо того, чтобы печатать его, как и любое другое исключение (и которое, таким образом, может быть неправильно понято как ошибка, которая действительно может быть в разных сценариях приложения), на консоли выводится конкретное сообщение, чтобы тот факт, что соединение закрыто очень Чисто.

(Примечание: если вы намерены подключить и обменять данные вашим клиентским программным обеспечением с сторонним серверным программным обеспечением, BinaryWriter.Write (строка) может быть или не быть допустимым вариантом, так как он использует ULEB128 для кодирования длины байта string. В тех случаях, когда BinaryWriter.Write(string) не представляется возможным, вы, вероятно, вполне можете использовать некоторую комбинацию BinaryWriter.Write(short)/BinaryWriter.Write(int) в сочетании с BinaryWriter.Write(byte []) для добавления данных байта сообщения с помощью значения messageLength.)


5. Вход в консольную клавиатуру

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

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

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

  • 1
    «Один байт равен ровно одному символу ASCII»: на самом деле существует только несколько кодировок набора символов, в которых это верно для всех возможных значений байтов. CP437 один. ASCII и Windows-1252 нет.
  • 0
    @ TomBlodget, согласился - то, как я это сформулировал, было отчасти назад и неправильно. Извините :) Отредактировал мой ответ, чтобы сделать предполагаемый аргумент более ясным: каждый символ ASCII представлен одним байтом.
Показать ещё 7 комментариев
-1

Попробуйте изменить это

try
{
    //blocks until a client sends a message
    bytesRead = clientStream.Read(message, 0, 4096);
}

К этому

StringBuilder myCompleteMessage = new StringBuilder();
try {
    do{
        bytesRead = clientStream.Read(message, 0, message.Length);
        myCompleteMessage.AppendFormat("{0}", Encoding.ASCII.GetString(message, 0, bytesRead );                             
    } while(clientStream.DataAvailable);
}
///catch and such....
System.Diagnostics.Debug.WriteLine(myCompleteMessage);
  • 0
    Не работает
  • 0
    Да, более или менее это после исключения преждевременных выходов Main-выхода и возможных исключений, возникающих при настройке TcpClient.
Показать ещё 4 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню