.NET

индекс
121,07

Шаблон многопоточного TCP сервера

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

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

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


Создаем два класса которые позволят передать в дочерний поток список методов для вызова при отлове сообщения на порту.

public class ContextMethod
  {
    private SynchronizationContext context = null;
    /// <summary>
    /// Контекст
    /// </summary>
    public SynchronizationContext Context
    {
      get
      {
        return context;
      }
      set
      {
        context = value.CreateCopy();
      }
    }
    public ContextCallback method;
    /// <summary>
    /// Запуск на выполнение метода в указанном контексте
    /// </summary>
    /// <param name="state"></param>
    public void Run(object state)
    {
      if (context != null)
        context.Send(delegate(object obj) { method(state); }, null);
    }
  }
  /// <summary>
  /// Класс выполняющий метод указанного контекста
  /// </summary>
  public class ListContextMethod
  {
    /// <summary>
    /// Метод
    /// </summary>
    public List<ContextMethod> async_events = new List<ContextMethod>();
    /// <summary>
    /// Вызов методов в указанном контексте
    /// </summary>
    /// <param name="state">Параметр метода</param>
    public void Run(object state)
    {
      foreach (ContextMethod call in async_events)
        call.Run(state);
    }
  }


* This source code was highlighted with Source Code Highlighter.

Тут все просто, в ContextMethod храним контекст объекта чей метод вызовем, ссылку на метод и вызов метода объекта. Ну а в ListContextMethod храним список вызываемых методов нужных нам объектов.

Теперь напишем класс отвечающий за прослушку конкретного порта:

  /// <summary>
  /// Класс для прослушки указанного порта
  /// </summary>
  public class TCPReader
  {
    Socket sock;
    /// <summary>
    /// Прослушиваемый порт
    /// </summary>
    private int port;
    /// <summary>
    /// Прослушивалка
    /// </summary>
    private TcpListener listener;
    /// <summary>
    /// Набор методов исполняющихся после получения данных на порт
    /// </summary>
    private ListContextMethod OnRead = null;
    /// <summary>
    /// Завершение работы
    /// </summary>
    public void End()
    {
      if (sock != null)
        sock.Close();
      if (listener != null)
        listener.Stop();
    }
    /// <summary>
    /// Конструктор с параметрами
    /// </summary>
    public TCPReader(int _port, ListContextMethod onRead)
    {
      port = _port;
      OnRead = onRead;
    }
    /// <summary>
    /// Запуск прослушки порта
    /// </summary>
    public void Start()
    {
      listener = new TcpListener(IPAddress.Any, port);
      listener.Start();
      object[] ret = new object[2];
      try
      {
        sock = listener.AcceptSocket();
        ret[0] = port;
        ret[1] = sock;
        listener.Stop();
      }
      catch
      { }
      if (OnRead != null)
        OnRead.Run(ret);
    }
  }


* This source code was highlighted with Source Code Highlighter.

И наконец диспетчер запускающий прослушку в потоки, и отслеживающий запуск прослушки после принятия данных:

  /// <summary>
  /// Список прослушиваемых портов
  /// </summary>
  public static class TCPReaderChain
  {
    //Список потоков прослушивающих порты
    private static Dictionary<int, TCPReader> readers = new Dictionary<int, TCPReader>();

    //Перезапуск прослушки порта
    private static void RestartListen(object data)
    {
      object[] arr = (object[])data;
      if (arr != null && arr.Length > 0)
        if (readers.ContainsKey((int)arr[0]))
          new Thread(readers[(int)arr[0]].Start).Start();
    }

    //Запуск прослушки указанного порта
    public static bool StartListen(int port, ListContextMethod onRead)
    {
      if (!readers.ContainsKey(port))
      {
        onRead.async_events.Add(new ContextMethod
        {
          Context = SynchronizationContext.Current,
          method = RestartListen
        });
        readers.Add(port, new TCPReader(port, onRead));
        Thread th = new Thread(readers[port].Start);
        th.IsBackground = true;
        th.Priority = ThreadPriority.Lowest;
        th.Start();
        return true;
      }
      else return false;
    }
    /// <summary>
    /// Завершение прослушивания портов
    /// </summary>
    public static void EndAllListener()
    {
      foreach (int key in readers.Keys)
      {
        readers[key].End();
      }
      Thread.Sleep(1000);
      readers.Clear();
    }


* This source code was highlighted with Source Code Highlighter.

Можно использовать. Поставить на прослушку нужный порт можно следующим образом:

  private void AddPort(int port)
    {
      ListContextMethod meth = new ListContextMethod();
      meth.async_events.Add(new ContextMethod
      {
        Context = SynchronizationContext.Current,
        method = RecieveData
      });
      TCPReaderChain.StartListen(port, meth);
    }


* This source code was highlighted with Source Code Highlighter.

Где ReceiveData метод принимающий данные. Например так:

  private void RecieveData(object data)
    {
      object[] arr = (object[])data;
      if (arr != null && arr.Length == 2)
      {
        string output = "";
        if (arr[1] is Socket)
        {
          Socket sock = (Socket)arr[1];
          byte[] buf = new byte[sock.ReceiveBufferSize];
          sock.Receive(buf);
          output = Encoding.Default.GetString(buf);
          sock.Send(Encoding.Default.GetBytes("SUCCESS"));
          sock.Close();
        }
        lock (listBox1)
          listBox1.Items.Add(arr[0].ToString() + ":" + output);
      }
    }


* This source code was highlighted with Source Code Highlighter.

Ну и не стоит забывать вызвать TCPReaderChain.EndAllListener(); при закрытии программы.

В аттаче пример, с возможностью тестировать работу.

Еще большая просьба, если кто знает, можно ли определить жив ли еще объект зная только его контекст синхронизации, напишите как это сделать.
+1
20 июля 2010, 18:48
11

комментарии (15)

+7
adontz #
Для справки, есть такой интерфейс — IDisposable.

TCP_Reader, On_Read, on_read — крайне странный стиль именования для .Net. Да и названия неудачные. Надо TcpReader, ReadHandler, readHandler. Да и вообще хорошобы использовать events. Причём On_Read на самом деле OnAccept.

Код небрежный, public fields — зло.

В итоге имеем безумно громоздкую обёртку над методом TcpListener.AcceptSocket и больше ничего. Не густо.
0
Ogoun #
Насчет наименований согласен, в рабочем проекте будет исправлено.
Насчет public fields, это шаблон работы, а не готовый модуль.
Для меня было главным не TcpListener.AcceptSocket обернуть, а фоновое ожидание сигналов.
Если есть пример изящной обертки для реализации функционала, дайте, пожалуйста, ссылку.
0
adontz #
Tсли мы говорим о сервере (асинхронные сокеты в частности и I/O вообще), то
msdn.microsoft.com/en-us/library/system.net.sockets.socket.beginaccept.aspx
А TcpListener в топку.

Справедливости ради надо заметить, что BeginAccept в старых версиях .NetFX глючил пожтому такие обёртки действительно были нужны.

Мой вариант (немного старый, для 3.5) можно посмотреть тут
code.google.com/p/nabu-library/source/browse/#svn/trunk/Source/Library/Nabu.Net

Шаблон клиента, сервера и реализация нескольких протоколов. Сокет абстрагирован, так что теоретически можно покрыть обработчик протокола тестами.
+1
braindamaged #
Код — это, конечно, пиздец. И что такое «фоновое ожидание» сигналов? Из космоса?
0
Ogoun #
Это ожидание сигналов. Можно и из космоса — ru.wikipedia.org/wiki/DTN
+1
XuMiX #
0
Ogoun #
Ага. Спасибо!
+2
OrdJONY #
А почему бы не попробовать вам WCF?
0
Ogoun #
А попробую, обязательно) Но WCF надо хостить в IIS или на сервис. Пока хочу так.
0
vittore #
а в чем проблема хостить вцф на сервис? есть даже готовые обертки, типа www.nservicebus.com/GenericHost.aspx
+1
mezastel #
Тогда вопрос — зачем WCF? :)
0
vittore #
=) есть такое дело
0
Pavel7 #
WCF можно хостить в чем угодно, хоть в консольном приложении.
+2
OrdJONY #
Не надо никакой IIS и сервис! WCF хостится где угодно, даже в консольных приложениях.
Я предпочитаю создавать вручную сервис и клиент, без всяких конфигов в web.config или app.config.
Рекомендую книгу «Основы Windows Communication Foundation для .NET Framework 3.5» Стив Резник, Ричард Крейн, Крис Боуэн.
В интернете были статьи для начинающих, а вот моя для уже немного разбирающихся в wcf www.progblog.ru/Posts/WCF-Chat
+1
Mephistophele #
Каждый созданный объект в шарпе имеет собственный контекст исполнения, используя данный контекст мы можем вызывать методы данного объекта, чем я и воспользуюсь.

Честно, я ничерта не понял.

try
{
sock = listener.AcceptSocket();
ret[0] = port;
ret[1] = sock;
listener.Stop();
}
catch
{ }

Вот из-за такого я иногда сожалею что у нас не средневековье и нет инквизиции.

Дальше читать уже не стал, психику жалко.

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