Кодирование сервера, который не является «потоком на клиента» - PullRequest
4 голосов
/ 13 июля 2009

Используя .NET, каков базовый алгоритм сервера, который не 'основывается на потоке на клиента'?

Редактировать Я ищу базовый алгоритм на 3, 4 или 5 строк / псевдокод / ​​шаблон, который описывает общий процесс, используемый сервером.

Что-то противоположное этому:


open a server socket // this uses the port the clients know about

while(running)    
  client_socket = server_socket.listen    
  fork(new handler_object(client_socket))

Ответы [ 8 ]

2 голосов
/ 13 июля 2009

У Иана Гриффитса есть превосходное (.NET) введение для обработки нескольких клиентов без необходимости использования потока для каждого клиента.

2 голосов
/ 13 июля 2009

Цикл, который вызывает Select, чтобы определить, какие сокеты (клиенты) готовы, а затем читает и / или записывает данные в эти сокеты, выполняя любую необходимую обработку, когда ни один сокет не готов.

Псевдокод:

loop {

   to_read.add(client1);
   to_read.add(client2);

   select(to_read, timeout = 0.5);

   for client in to_read { // to_read is modified by select
      data = client.read
      handle(data)
   }

   if to_read is empty {
     do_bookeeping()
   }
}
2 голосов
/ 13 июля 2009

В довольно широком и общем способе описания вещей (т. Е. Не специфично для .net или любой другой платформы) сервисы программируются одним из двух способов или комбинацией обоих.

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

пример псевдокода:

function run(connection)
    while(connection is open)
        frobnicate(connection)

function main
    listener = new Listener(port)
    threads = new Collection<Thread>()
    while(running)
        connection = listener.accept()
        threads.add(new Thread(run, connection))
    join(threads)
    exit()

Важные особенности приведенного выше образца:

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

неблокирование: один поток выполнения управляет несколькими соединениями, и, когда какое-либо конкретное соединение останавливается, поскольку оно ожидает передачи данных по сети или чтения с диска, процесс пропускает его и работает на другом подключении.

пример псевдокода:

function main
    connections = new Collection<Connection>()
    listener = new Listener()
    connections.append(listener)
    foreach connection in connections:
        if connection.ready():
            if connection is listener: 
                connections.add(connection.accept() )
            else: 
                if connection is open:
                    nb_frobnicate(connection)
                else: 
                    connections.remove(connection)
        yield()
        if( not running )
            exit()

Особенности этого фрагмента:

  • В многопоточной версии каждый поток обрабатывал только одно соединение. если это соединение не готово к использованию, поток блокируется, и другой поток может сделать Работа. если frobnicate заблокирован в этой реализации, это будет катастрофой. Другой Соединения могут работать, даже если одно соединение не готово. Для решения этой проблемы используется альтернативная функция, которая использует только неблокирующие операции по соединению. эти чтения и записи немедленно возвращаются, и верните значения, которые сообщают вызывающей стороне, какую работу они смогли выполнить. если это случается случается не работа вообще, программа просто должна будет повторить попытку, как только так как он знает, что соединение готово к дальнейшей работе. Я изменил имя на nb_frobnicate, чтобы указать, что эта функция является небрежной.
  • Большинство платформ предоставляют функцию, которая может абстрагировать цикл по соединениям, и проверка, есть ли у каждого данные. Это будет называться либо select(), либо poll() (могут быть доступны оба). Тем не менее, я решил показать это таким образом, потому что Возможно, вы захотите работать не только над сетевым подключением. Насколько я знаю, там нет никакой возможности ждать каждой возможной операции блокировки в общем случае, если вам также необходимо ждать на диске IO (только для Windows), таймеры, аппаратные прерывания (например, обновление экрана) вы будете приходится вручную опрашивать разные источники событий, вроде этого.
  • в дальнем конце цикла - функция yield(), которая спит до прерывания ввода-вывода случается. Это необходимо, если нет ввода-вывода, готового к обработке. Если бы мы этого не делали, программа будет занята - ждет, проверяет и перепроверяет каждый порт и обнаруживает, что данные не готовы, без необходимости тратить процессор. В случае, если программе нужно только ждать сетевой IO (или также диск в системах posix), тогда select/poll может быть вызвано врежим блокировки, и он будет спать, пока сетевое соединение не будет готово. Если у тебя есть чтобы ждать больше событий, вам придется использовать неблокирующую версию эти функции, опросить другие источники событий в соответствии с их идиомой, а затем вывести один раз все бы заблокировать.
  • Здесь нет эквивалента присоединению к потоку из предыдущего примера. Это вместо аналогично неприсоединению. вы могли бы получить похожее поведение удаление слушателя из коллекции опросов, а затем только один раз вызвать exit коллекция пуста
  • важно, что вся программа состоит из простых циклов и вызовов функций. там нет смысла, который несет стоимость переключения контекста. Недостатком этого является то, что программа является строго последовательной и не может использовать преимущества нескольких ядер, если они присутствуют. На практике это редко является проблемой, потому что большинство служб будет IO ограничено скоростью, с которой соединения становятся готовыми, а не стоимостью вымышленная frobnicate() функция.

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

пример этого:

function worker(queue)
    connections = Collection<Connection>()
    while(running)
        while(queue not empty)
            connections.add(queue.pop)
        foreach connection in select(connections)
            if connection is open:
                nb_frobnicate(connection)
            else: 
                connections.remove(connection)

function main
    pool = new Collection<Thread, Queue<Connection> >()
    for index in num_cpus: 
        pool[index] = new Thread(worker), new Queue<Connection>
    listener = new Listener(port)
    while(running)        
        connection = listener.accept()
        selected_thread = 0
        for index in pool.length: 
            if pool[index].load() < pool[selected_thread].load()
                selected_thread = index
        pool[selected_thread].push(connection)
        pool[selected_thread].wake()

заметки об этой программе:

  • Это создает кучу экземпляров однопоточной версии, но так как они многопоточные, они должны общаться друг с другом, так или иначе. это делается с помощью Queue<Connection> для каждой нити.
  • Также обратите внимание, что он использует nb_frobnicate обработчик по той же причине, что и однопоточная программа, и по той же причине, потому что каждый поток обрабатывает более одного соединения.
  • Пул рабочих потоков ограничен в соответствии с количеством имеющихся у нас процессоров.
    поскольку было бы мало пользы от ожидания большего количества потоков, чем возможно бежать сразу. На практике оптимальное количество используемых потоков может варьироваться от приложение к приложению, но количество процессоров часто является достаточно хорошим предположением.
  • Как и прежде, основной поток принимает соединения и передает их рабочим потокам. Если поток уже завершил работу с другими потоками, ожидая их готовности, тогда он просто будет сидеть там, даже если новое соединение уже готово. Чтобы облегчить это, основной поток уведомляет рабочий поток, пробуждая его, вызывая любую блокировку операция, чтобы вернуться. Если бы это было нормальное чтение, оно, вероятно, вернуло бы ошибку код, но так как мы используем select(), он просто вернет пустой список готовых соединений, и поток может очистить свою очередь еще раз.

Редактировать: добавлены примеры кода:

2 голосов
/ 13 июля 2009

Цикл событий. Дождитесь, пока сокеты станут доступными для записи, напишите в них, дождитесь соединений, примите их и т. Д. Этот подход в большинстве случаев более масштабируемый, чем потоки, поскольку обычно вам не нужно больше, чем замыкание, чтобы отслеживать состояние клиента. (Вам, конечно, не нужен целый процесс, как думают серверы prefork.)

1 голос
/ 13 июля 2009

использовать очередь событий и пул потоков, например ::

when [something happens] then 
    [create an event for something]
    [put it in the queue]

также:

while [something in queue]
    if [thread is available] then
        remove [thing] from queue
        run [thing-response] on [available thread]
    else [wait a little while]

эта архитектура вполне масштабируема

1 голос
/ 13 июля 2009

Пул потоков позволит серверу иметь меньше потоков, чем клиентов.

0 голосов
/ 13 июля 2009

Socket.BeginAccept -> в обратном вызове AuthenticatedStream. BeginAuthenticateAsServer -> в обратном вызове Stream. BeginRead -> в обратном вызове после нового BeginRead, обработайте запрос затем Stream . BeginWrite ответ.

Вы можете удалить часть аутентифицированного потока (SSL или Kerberos / NTLM), если вам действительно это не нужно, и тогда она становится: Socket.BeginAccept -> в сокете обратного вызова. BeginReceive -> в обратном вызове после нового BeginReceive, обрабатывает запрос, затем сокет. BeginWrite ответ.

Также см. Пример сокета асинхронного сервера

0 голосов
/ 13 июля 2009

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

...