Пишем чат по локальной сети на C# посредством Socket

Здравствуй, %username%!
Сегодня мы займемся весьма интересным и уже освещавшимся на страницах данного сайта вопросом – создание локального чата на языке C#. Данная тема уже описывалась мною здесь http://xnim.ru/blog?id=18. Однако, я решил, что будет хорошо реализовать данный чат более красиво – с помощью сокетов и потоков. А также более подробно описать все действия.
Постараюсь описать разработку локального чата C# на сокетах пошагово. В конце добавлю ссылку на готовый проект.
Итак. Начнем. Для соблюдения общего стиля написания, напишу по аналогии с материалами, которые я отправлял для IT-IMHO
Задача
Написать приложение, реализующее чат по локальной сети, на языке C#, с помощью сокетов. Также данное приложение должно быть многопоточным, чтобы исключить «временные зависания» главной формы при отправке сообщений на несуществующие IP адреса.
Решение
Решение создадим с помощью нескольких простых шагов:
Этап первый – проектируем форму.
Форма будет довольно простой – кидаем туда текстовое поле для ввода IP адреса, два RichTextBox для ввода данный один и второй для отображения общения.
Называем их соответственно IP, ChatBox и Message.
ChatBox’у присваиваем ReadOnly = true, чтобы запретить пользователю изменение содержимого бокса. В конце добавляем кнопку для отправки сообщения. В итоге получаем форму, похожую на:

Этап второй – создаем отправку сообщений – обработку кнопки
Как мы договорились - приложение будет многопоточным. Поэтому, создаем изначально функцию отправки сообщений – ThreadSend.
Так как отправлять сообщения мы будем в отдельном потоке, то мы сможем получить доступ к IP.Text, но не сможем получить доступ к Message.Text- передаваемому сообщению. Поэтому, заведем переменную типа String, в которую будем присваивать передаваемое сообщение(При создании потока отправим параметром строку). Назовем данную переменную MessageText.
Теперь пару слов о передаче сообщения посредством сокетов.
В потоке мы создадим сокет, который конструируется по параметрам AddressFamily, типу сокета и протокола.
Чтобы определить AddressFamily, необходимо (читать – удобно) создать IPEndPoint с адресом и портом к которому будем подключаться и затем получить из него AddressFamily.
После того, как мы создадим сокет, мы попробуем подключиться к данному адресу по заданному порту (в приложении укажем «умолчание» и поставим везде порт = 7000). Если подключиться (на этом шаге) не получится в течение определенного тайм-аута, то мы уведомим о недоступности клиента (упадем в блок catch, уведомим об ошибке и выйдем из потока).
Если подключение удалось, переведем строку в байты и отправим ее методом send, затем закроем сокет.
Вот какой код получается в итоге: (данный код еще чуточку позже доработаем)
  1.      void ThreadSend(object Message)
  2.      {
  3.         try
  4.          {
  5.            String MessageText = "" ;
  6.            if (Message is String)
  7.              MessageText = Message as String;
  8.            else
  9.              throw new Exception("На вход необходимо подавать строку" );
  10.         
  11.            IPEndPoint EndPoint = new IPEndPoint(IPAddress.Parse(IP.Text), 7000 );
  12.            Socket Connector = new Socket(EndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
  13.            Connector.Connect(EndPoint);
  14.            Byte[ ] SendBytes = Encoding.Default.GetBytes(MessageText);
  15.            Connector.Send(SendBytes);
  16.            Connector.Close();
  17.         
  18.         }
  19.         catch (Exception ex)
  20.         {
  21.            MessageBox.Show(ex.Message);
  22.         }
  23.      }

This code was highlighted with code.xnim.ru

Однако, мы хотим, чтобы после того, как сообщение отправилось, мы изменили значение ChatBox – добавили туда отправленное сообщение. Для реализации данной возможности, воспользуемся делегатами и BeginInvoke, которое практически каждый контрол WFA (Windows Forms App).
Делегаты
Подробно о них можно прочитать на MSDN. Перейдем к конкретной реализации.
В нашем случае, мы создадим делегат, который будет принимать строку (которую допишем в конец RichTextBox) и, собственно, сам RichTextBox.
Поэтому создаем делегат:
  1. delegate void SendMsg(String Text, RichTextBox Rtb);

This code was highlighted with code.xnim.ru

Теперь создадим переменную данного класса (класса делегата) и с помощью LinQ тут же реализуем метод. Сделать это можно следующим образом:
  1. SendMsg AcceptDelegate = (String Text, RichTextBox Rtb) = >
  2.         {
  3.            Rtb.Text += Text + "\
    "
    ;  
  4.         };

This code was highlighted with code.xnim.ru

Делегат успешно добавлен. Как теперь добавить его вызов (чтобы он мог изменить ChatBox) в поток? Тут воспользуемся BeginInvoke. Данный метод принимает на вход делегат и параметры данного делегата в виде Object[]. Зададим его:
ChatBox.BeginInvoke(AcceptDelegate, new object[ ] {"Send " +MessageText, ChatBox });
This code was highlighted with code.xnim.ru

Теперь соберем данный код воедино и добавим обработчик кнопки, который создает поток с параметром – сообщением:
  1.       /// <summary >
  2.      /// Отправляет сообщение в потоке на IP, заданный в контроле IP
  3.      /// </summary >
  4.      /// <param name="Message" >Передаваемое сообщение </param >
  5.      void ThreadSend(object Message)
  6.      {
  7.         try
  8.          {
  9.            //Проверяем входной объект на соответствие строке
  10.            String MessageText = "" ;
  11.            if (Message is String)
  12.              MessageText = Message as String;
  13.            else
  14.              throw new Exception("На вход необходимо подавать строку" );
  15.         //Создаем сокет, коннектимся
  16.            IPEndPoint EndPoint = new IPEndPoint(IPAddress.Parse(IP.Text), 7000 );
  17.            Socket Connector = new Socket(EndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
  18.            Connector.Connect(EndPoint);
  19.            //Отправляем сообщение
  20.            Byte[ ] SendBytes = Encoding.Default.GetBytes(MessageText);
  21.            Connector.Send(SendBytes);
  22.            Connector.Close();
  23.            //Изменяем поле сообщений (уведомляем, что отправили сообщение)
  24.            ChatBox.BeginInvoke(AcceptDelegate, new object[ ] {"Send " +MessageText, ChatBox });
  25.         }
  26.         catch (Exception ex)
  27.         {
  28.            MessageBox.Show(ex.Message);
  29.         }
  30.      }
  31.     
  32.      //Делегат доступа к контролам формы
  33.      delegate void SendMsg(String Text, RichTextBox Rtb);
  34.     
  35.      SendMsg AcceptDelegate = (String Text, RichTextBox Rtb) = >
  36.         {
  37.            Rtb.Text += Text + "\
    "
    ;  
  38.         };
  39.      //Обработчик кнопки
  40.      private void Send_Click(object sender, EventArgs e)
  41.      {
  42.         
  43.        new Thread(new ParameterizedThreadStart(ThreadSend)).Start(Message.Text);      
  44.      }

This code was highlighted with code.xnim.ru

Этап третий – прием сообщений
Принимать сообщения будем также в отдельном потоке, который будет создаваться при инициализации формы.
Работать он будет в бесконечном цикле. Заведем TCPListener, который будет при подключении «создавать» сокет, с помощью которого мы считаем пришедшее нам сообщение, а затем повторим данный цикл.
В данной реализации мы также воспользуемся BeginInvoke.
Приведу сразу откомментированный код, ибо работа приема данных похожа на отправку:
  1.      public Form1()
  2.      {
  3.         InitializeComponent();
  4.         //Создаем поток для приема сообщений
  5.        new Thread(new ThreadStart(Receiver)).Start();
  6.      }
  7.      //Метод потока
  8.      protected void Receiver()
  9.      {
  10.         //Создаем Listener на порт "по умолчанию"
  11.         TcpListener Listen = new TcpListener(7000);
  12.         //Начинаем прослушку
  13.         Listen.Start();
  14.         //и заведем заранее сокет
  15.         Socket ReceiveSocket;
  16.        while (true)
  17.         {
  18.            try
  19.            {
  20.              //Пришло сообщение
  21.              ReceiveSocket = Listen.AcceptSocket();
  22.              Byte[ ] Receive = new Byte[ 256];
  23.              //Читать сообщение будем в поток
  24.              using (MemoryStream MessageR = new MemoryStream())
  25.              {
  26.                 //Количество считанных байт
  27.                Int32 ReceivedBytes;
  28.                do
  29.                  {//Собственно читаем
  30.                    ReceivedBytes = ReceiveSocket.Receive(Receive, Receive.Length, 0 );
  31.                    //и записываем в поток
  32.                    MessageR.Write(Receive, 0 , ReceivedBytes);
  33.                    //Читаем до тех пор, пока в очереди не останется данных
  34.                 } while (ReceiveSocket.Available > 0 );
  35.                 //Добавляем изменения в ChatBox
  36.                 ChatBox.BeginInvoke(AcceptDelegate, new object[ ] { "Received " + Encoding.Default.GetString(MessageR.ToArray()), ChatBox });
  37.              }
  38.            }
  39.            catch (System.Exception ex)
  40.            {
  41.              MessageBox.Show(ex.Message);
  42.            }
  43.           
  44.         }
  45.      }

This code was highlighted with code.xnim.ru

Замечу только, что цикл обязательно необходимо использовать с постусловием. Если использовать цикл с предусловием, то мы можем потерять данные – по причине: данных для считывания = 0, потому что сокет только установил соединение и еще не успел передать «первую порцию» данных.
Таким образом
Мы реализовали простой чат по локальной сети (локальный чат) на управляемом языке. Данная реализация получилась красивее предыдущей (за счет потоков) и более удобной для восприятия.
Также весь проект можно скачать здесь – Скачать проект
Для создания «полноценного» (читать удобного) приложения остается добавить вкладки, сохранение адресов и изменить внешний вид =)

2012-11-24