Есть ли способ для нескольких процессов совместно использовать прослушивающий сокет?

90

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

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

Мне интересно, есть ли способ (в любой известной ОС, особенно в Windows) запустить несколько экземпляров процесса, чтобы все они были привязаны к сокету и таким образом эффективно разделяли очередь. Тогда каждый экземпляр процесса может быть однопоточным; он просто блокируется при принятии нового соединения. Когда клиент подключился, один из экземпляров незанятого процесса примет этого клиента.

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

Есть ли такая функция?

Изменить: для тех, кто спрашивает «Почему бы не использовать потоки?» Очевидно, потоки - это вариант. Но с несколькими потоками в одном процессе все объекты являются совместно используемыми, и необходимо проявлять большую осторожность, чтобы гарантировать, что объекты либо не являются общими, либо видны только одному потоку за раз, либо являются абсолютно неизменяемыми, и большинство популярных языков и средам выполнения не хватает встроенной поддержки для управления этой сложностью.

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

Дэниел Эрвикер
источник
2
Я согласен, несколько процессов могут упростить создание правильной и надежной реализации. Масштабируемость, я не уверен, это зависит от вашей проблемной области.
MarkR

Ответы:

92

Вы можете использовать сокет между двумя (или более) процессами в Linux и даже Windows.

Под Linux (или ОС типа POSIX) использование fork()приведет к тому, что у разветвленного дочернего элемента будут копии всех дескрипторов родительских файлов. Все, что он не закрывает, будет по-прежнему использоваться совместно и (например, с сокетом прослушивания TCP) может использоваться для accept()новых сокетов для клиентов. Вот сколько серверов, включая Apache в большинстве случаев, работает.

В Windows то же самое в основном верно, за исключением того, что нет fork()системного вызова, поэтому родительский процесс должен будет использовать CreateProcessили что-то для создания дочернего процесса (который, конечно, может использовать тот же исполняемый файл) и должен передать ему наследуемый дескриптор.

Сделать прослушивающий сокет наследуемым дескриптором - не совсем тривиальная задача, но и не слишком сложная. DuplicateHandle()необходимо использовать для создания дублирующего дескриптора (однако все еще в родительском процессе), для которого будет установлен наследуемый флаг. Тогда вы можете дать , что ручку в STARTUPINFOструктуре дочернего процесса в CreateProcess , как STDIN, OUTили ERRручка (если вы не хотите использовать его для чего - нибудь еще).

РЕДАКТИРОВАТЬ:

Читая библиотеку MDSN, кажется, что WSADuplicateSocketэто более надежный или правильный механизм для этого; это все еще нетривиально, потому что родительский / дочерний процессы должны решить, какой дескриптор должен быть продублирован каким-либо механизмом IPC (хотя это может быть так же просто, как файл в файловой системе)

РАЗЪЯСНЕНИЕ:

В ответ на исходный вопрос OP: нет, несколько процессов не могут bind(); только первоначальный родительский процесс будет звонить bind(), и listen()т.д., дочерние процессы будут только обрабатывать запросы от accept(), send(), и recv()т.д.

MarkR
источник
3
Несколько процессов можно связать, указав параметр сокета SocketOptionName.ReuseAddress.
Аарон Клаусон,
Но в чем смысл? В любом случае процессы тяжелее потоков.
Антон Тихий,
7
Процессы более тяжелые, чем потоки, но поскольку они совместно используют только вещи, совместно используемые явно, требуется меньше синхронизации, что упрощает программирование и в некоторых случаях может быть даже более эффективным.
MarkR
11
Более того, если дочерний процесс каким-либо образом выйдет из строя или прервется, он вряд ли повлияет на родительский.
MarkR
4
Также хорошо отметить, что в Linux вы можете «передавать» сокеты другим программам без использования fork () и не иметь отношений родитель / потомок, используя Unix Sockets.
Rahly
35

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

import socket
import os

def main():
    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversocket.bind(("127.0.0.1", 8888))
    serversocket.listen(0)

    # Child Process
    if os.fork() == 0:
        accept_conn("child", serversocket)

    accept_conn("parent", serversocket)

def accept_conn(message, s):
    while True:
        c, addr = s.accept()
        print 'Got connection from in %s' % message
        c.send('Thank you for your connecting to %s\n' % message)
        c.close()

if __name__ == "__main__":
    main()

Обратите внимание, что на самом деле прослушиваются два идентификатора процесса:

$ lsof -i :8888
COMMAND   PID    USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Python  26972 avaitla    3u  IPv4 0xc26aa26de5a8fc6f      0t0  TCP localhost:ddi-tcp-1 (LISTEN)
Python  26973 avaitla    3u  IPv4 0xc26aa26de5a8fc6f      0t0  TCP localhost:ddi-tcp-1 (LISTEN)

Вот результаты запуска telnet и программы:

$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to parent
Connection closed by foreign host.
$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to child
Connection closed by foreign host.
$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to parent
Connection closed by foreign host.

$ python prefork.py 
Got connection from in parent
Got connection from in child
Got connection from in parent
Анил Вайтла
источник
2
Таким образом, одно соединение получает либо родитель, либо ребенок. Но кто понимает, что связь недетерминирована, верно?
Hot.PxL
1
да, я думаю, это зависит от того, какой процесс запланирован для запуска ОС.
Анил Вайтла
14

Я хотел бы добавить, что сокеты могут совместно использоваться в Unix / Linux через сокеты AF__UNIX (межпроцессные сокеты). Похоже, что происходит создание нового дескриптора сокета, который является своего рода псевдонимом исходного. Этот новый дескриптор сокета отправляется через сокет AFUNIX другому процессу. Это особенно полезно в случаях, когда процесс не может fork () поделиться своими файловыми дескрипторами. Например, при использовании библиотек, предотвращающих это из-за проблем с потоками. Вы должны создать сокет домена Unix и использовать libancillary для отправки дескриптора.

Видеть:

Для создания сокетов AF_UNIX:

Например код:

zachthehack
источник
13

Похоже, на этот вопрос уже был дан полный ответ от MarkR и zackthehack, но я хотел бы добавить, что Nginx является примером модели наследования прослушивающих сокетов.

Вот хорошее описание:

         Implementation of HTTP Auth Server Round-Robin and
                Memory Caching for NGINX Email Proxy

                            June 6, 2007
             Md. Mansoor Peerbhoy <mansoor@zimbra.com>

...

Поток рабочего процесса NGINX

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

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

(ПРИМЕЧАНИЕ) NGINX можно настроить для использования любого из нескольких механизмов опроса событий: aio / devpoll / epoll / eventpoll / kqueue / poll / rtsig / select

Когда соединение поступает на любой из слушающих сокетов (POP3 / IMAP / SMTP), каждый рабочий процесс выходит из своего опроса событий, поскольку каждый рабочий процесс NGINX наследует слушающий сокет. Затем каждый рабочий процесс NGINX будет пытаться получить глобальный мьютекс. Один из рабочих процессов получит блокировку, тогда как другие вернутся к своим соответствующим циклам опроса событий.

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

Если инициированное событие соответствует новому входящему соединению, NGINX принимает соединение от прослушивающего сокета. Затем он связывает структуру данных контекста с дескриптором файла. Этот контекст содержит информацию о соединении (POP3 / IMAP / SMTP, аутентифицирован ли пользователь и т. Д.). Затем этот вновь созданный сокет добавляется в набор дескрипторов события для этого рабочего процесса.

Рабочий теперь отказывается от мьютекса (что означает, что любые события, поступившие на других рабочих, могут продолжаться), и начинает обработку каждого запроса, который ранее был поставлен в очередь. Каждый запрос соответствует событию, о котором было сообщено. Из каждого дескриптора сокета, о котором было сообщено, рабочий процесс извлекает соответствующую структуру данных контекста, которая ранее была связана с этим дескриптором, а затем вызывает соответствующие функции обратного вызова, которые выполняют действия в зависимости от состояния этого соединения. Например, в случае недавно установленного IMAP-соединения первое, что сделает NGINX, - это записать стандартное приветственное сообщение IMAP в
подключенный сокет (* OK IMAP4 готов).

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

Ричард
источник
11

Не уверен, насколько это соответствует исходному вопросу, но в ядре Linux 3.9 есть патч, добавляющий функцию TCP / UDP: поддержка TCP и UDP для параметра сокета SO_REUSEPORT; Новая опция сокета позволяет нескольким сокетам на одном хосте связываться с одним и тем же портом и предназначена для повышения производительности приложений многопоточного сетевого сервера, работающих поверх многоядерных систем. дополнительную информацию можно найти в ссылке LWN LWN SO_REUSEPORT в Linux Kernel 3.9, как указано в ссылке ссылки:

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

Валид
источник
Из статьи LWN это почти похоже на SO_REUSEPORTсоздание пула потоков, где каждый сокет находится в другом потоке, но только один сокет в группе выполняет accept. Можете ли вы подтвердить, что все сокеты в группе получают копию данных?
jww 08
4

Начиная с Linux 3.9, вы можете установить SO_REUSEPORT для сокета, а затем использовать этот сокет для нескольких несвязанных процессов. Это проще, чем схема prefork, больше никаких проблем с сигналом, утечки fd в дочерние процессы и т. Д.

Linux 3.9 представил новый способ написания серверов сокетов

Параметр сокета SO_REUSEPORT

Бенуа
источник
3

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

s = socket();
bind(s);
listen(s);
while (1) {
  s2 = accept(s);
  send_to_worker(s2);
}
HUAGHAGUAH
источник
Как сокет передается рабочему? Имейте в виду, что идея состоит в том, что воркер - это отдельный процесс.
Дэниел Эрвикер,
fork (), возможно, или одну из других идей выше. Или, может быть, вы полностью отделите ввод-вывод сокета от обработки данных; отправить полезную нагрузку рабочим процессам через механизм IPC. OpenSSH и другие инструменты OpenBSD используют эту методологию (без потоков).
HUAGHAGUAH,
3

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

Ключевым вызовом функции является WSADuplicateSocket ().

Это заполняет структуру информацией о существующем сокете. Затем эта структура через выбранный вами IPC-механизм передается другому существующему процессу (обратите внимание, что я говорю «существующий» - когда вы вызываете WSADuplicateSocket (), вы должны указать целевой процесс, который получит переданную информацию).

Затем принимающий процесс может вызвать WSASocket (), передав эту структуру информации, и получить дескриптор базового сокета.

Оба процесса теперь хранят дескриптор одного и того же базового сокета.


источник
2

Похоже, вы хотите, чтобы один процесс слушал новых клиентов, а затем передал соединение, как только вы его установили. Сделать это через потоки легко, и в .Net у вас даже есть методы BeginAccept и т. Д., Которые позаботятся о многих сантехнических работах за вас. Передача соединений через границы процесса будет сложной задачей и не даст никаких преимуществ в производительности.

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

TcpListener tcpServer = new TcpListener(IPAddress.Loopback, 10090);
tcpServer.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
tcpServer.Start();

while (true)
{
    TcpClient client = tcpServer.AcceptTcpClient();
    Console.WriteLine("TCP client accepted from " + client.Client.RemoteEndPoint + ".");
}

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

Аарон Клаусон
источник
2

Другой подход (который позволяет избежать многих сложных деталей) в Windows, если вы используете HTTP, - это использовать HTTP.SYS . Это позволяет нескольким процессам прослушивать разные URL-адреса на одном и том же порту. На Server 2003/2008 / Vista / 7 IIS работает так, поэтому вы можете использовать его порты. (В XP SP2 HTTP.SYS поддерживается, но IIS5.1 не использует его.)

Другие API высокого уровня (включая WCF) используют HTTP.SYS.

Ричард
источник