Есть ли способ для нескольких процессов совместно использовать сокет прослушивания?
При программировании сокетов вы создаете сокет прослушивания, а затем для каждого подключаемого клиента вы получаете обычный сокет потока, который вы можете использовать для обработки запроса клиента. ОС управляет очередью входящих соединений за кулисами.
Два процесса не могут связываться с одним и тем же портом одновременно - по умолчанию, в любом случае.
Мне интересно, есть ли способ (в любой известной ОС, особенно в Windows) запустить несколько экземпляров процесса, чтобы все они связывались с сокетом и эффективно разделяли очередь. Каждый экземпляр процесса может быть однопоточным; это просто заблокировало бы при принятии нового соединения. Когда клиент подключился, один из бездействующих экземпляров процесса примет этот клиент.
Это позволило бы каждому процессу иметь очень простую однопоточную реализацию, не разделяющую ничего, кроме как через явную общую память, и пользователь мог бы регулировать пропускную способность обработки, запуская больше экземпляров.
Существует ли такая функция?
Изменить: Для тех, кто спрашивает "Почему бы не использовать темы?" Очевидно, что темы являются опцией. Но с несколькими потоками в одном процессе все объекты доступны для совместного использования, и необходимо позаботиться о том, чтобы объекты либо не использовались совместно, либо были видны только одному потоку за раз, либо были абсолютно неизменными, а большинство популярных языков и во время выполнения отсутствует встроенная поддержка для управления этой сложностью.
Запустив несколько идентичных рабочих процессов, вы получите параллельную систему, в которой по умолчанию нет общего доступа, что значительно упростит создание правильной и масштабируемой реализации.
10 ответов
Вы можете разделить сокет между двумя (или более) процессами в Linux и даже в Windows.
Под Linux (или ОС типа POSIX), используя fork()
заставит разветвленного ребенка иметь копии всех файловых дескрипторов родителя. Все, что он не закрывает, будет по-прежнему использоваться совместно, и (например, с прослушивающим сокетом TCP) может использоваться для accept()
новые розетки для клиентов. Это то, сколько серверов, включая Apache в большинстве случаев, работают.
На винде то же самое в принципе верно, кроме нет fork()
системный вызов, поэтому родительский процесс должен будет использовать CreateProcess
или что-то для создания дочернего процесса (который, конечно, может использовать тот же исполняемый файл) и должен передать ему наследуемый дескриптор.
Создание сокета для прослушивания в качестве наследуемого дескриптора - не совсем тривиальное занятие, но и не слишком сложное. DuplicateHandle()
необходимо использовать для создания дубликата дескриптора (все еще в родительском процессе), для которого будет установлен наследуемый флаг. Тогда вы можете дать эту ручку в STARTUPINFO
структура дочернего процесса в CreateProcess как STDIN
, OUT
или же ERR
обрабатывать (при условии, что вы не хотите использовать его для чего-либо еще).
РЕДАКТИРОВАТЬ:
Читая библиотеку MDSN, кажется, что WSADuplicateSocket
более надежный или правильный механизм для этого; это все еще нетривиально, потому что родительский / дочерний процессы должны решить, какой дескриптор должен быть дублирован каким-либо механизмом IPC (хотя это может быть так же просто, как файл в файловой системе)
ПОЯСНЕНИЯ:
В ответ на первоначальный вопрос ОП, нет, несколько процессов не могут bind()
; только оригинальный родительский процесс будет вызывать bind()
, listen()
и т.д., дочерние процессы будут просто обрабатывать запросы accept()
, send()
, recv()
и т.п.
Большинство других предоставили технические причины, по которым это работает. Вот код 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
Я хотел бы добавить, что сокеты могут использоваться совместно в Unix/Linux через сокеты AF__UNIX (межпроцессные сокеты). Кажется, что происходит то, что создается новый дескриптор сокета, который является своего рода псевдонимом исходного. Этот новый дескриптор сокета отправляется через сокет AFUNIX другому процессу. Это особенно полезно в тех случаях, когда процесс не может использовать fork() для совместного использования своих файловых дескрипторов. Например, при использовании библиотек, которые предотвращают это из-за проблем с многопоточностью. Вы должны создать сокет домена Unix и использовать libancillary для отправки через дескриптор.
Увидеть:
- https://www.linuxquestions.org/questions/programming-9/how-to-share-socket-between-processes-289978/
Для создания сокетов AF_UNIX:
Например, код:
Похоже, что на этот вопрос уже полностью ответили 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 готов).Постепенно каждый рабочий процесс завершает обработку записи рабочей очереди для каждого ожидающего события и возвращается к своему циклу опроса событий. Как только любое соединение установлено с клиентом, события, как правило, происходят быстрее, так как всякий раз, когда подключенный сокет готов к чтению, запускается событие чтения, и необходимо выполнить соответствующее действие.
Не уверен, насколько это соответствует исходному вопросу, но в ядре Linux 3.9 есть патч, добавляющий функцию TCP/UDP: поддержка TCP и UDP для опции сокета SO_REUSEPORT; Новая опция сокетов позволяет нескольким сокетам на одном хосте связываться с одним и тем же портом и предназначена для повышения производительности многопоточных приложений сетевого сервера, работающих поверх многоядерных систем. дополнительную информацию можно найти в LWN-ссылке LWN SO_REUSEPORT в Linux Kernel 3.9, как упомянуто в справочной ссылке:
опция SO_REUSEPORT является нестандартной, но доступна в аналогичной форме в ряде других систем UNIX (особенно в BSD, где возникла идея). Похоже, он предлагает полезную альтернативу для выживания максимальной производительности из сетевых приложений, работающих на многоядерных системах, без необходимости использования схемы разветвления.
Начиная с Linux 3.9, вы можете установить SO_REUSEPORT для сокета, а затем иметь несколько несвязанных процессов, совместно использующих этот сокет. Это проще, чем схема prefork, никаких проблем с сигналом, утечки fd в дочерние процессы и т. Д.
Иметь единственную задачу, единственной задачей которой является прослушивание входящих соединений. Когда соединение получено, оно принимает соединение - это создает отдельный дескриптор сокета. Принятый сокет передается одной из доступных рабочих задач, а основная задача возвращается к прослушиванию.
s = socket();
bind(s);
listen(s);
while (1) {
s2 = accept(s);
send_to_worker(s2);
}
В Windows (и Linux) один процесс может открыть сокет, а затем передать этот сокет другому процессу, так что второй процесс также может затем использовать этот сокет (и передать его по очереди, если он этого пожелает),
Важным вызовом функции является WSADuplicateSocket().
Это заполняет структуру информацией о существующем сокете. Затем эта структура через выбранный вами механизм IPC передается другому существующему процессу (заметьте, я говорю "существующий" - когда вы вызываете WSADuplicateSocket(), вы должны указать целевой процесс, который будет получать передаваемую информацию).
Процесс получения может затем вызвать WSASocket(), передав эту структуру информации, и получить дескриптор нижележащего сокета.
Оба процесса теперь содержат дескриптор одного и того же базового сокета.
Другой подход (который позволяет избежать многих сложных деталей) в Windows, если вы используете HTTP, - это использование HTTP.SYS. Это позволяет нескольким процессам прослушивать разные URL-адреса на одном и том же порту. На сервере 2003/2008/Vista/7 так работает IIS, поэтому вы можете использовать с ним порты. (На XP SP2 HTTP.SYS поддерживается, но IIS5.1 не использует его.)
Другие API высокого уровня (включая WCF) используют HTTP.SYS.
Похоже, что вы хотите, чтобы один процесс прослушивал новых клиентов, а затем передавал соединение, как только вы установили соединение. Сделать это в разных потоках легко, а в.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 решает, какой процесс получает новые соединения, хотя быстрый тест указывает на самый старый процесс, который получает их первым. Что касается того, разделяет ли он, занят ли первый процесс, или что-то подобное, я не знаю.