Existe uma maneira de vários processos compartilharem um soquete de escuta?

90

Na programação de soquete, você cria um soquete de escuta e, para cada cliente que se conecta, obtém um soquete de fluxo normal que pode ser usado para lidar com a solicitação do cliente. O sistema operacional gerencia a fila de conexões de entrada nos bastidores.

Dois processos não podem se vincular à mesma porta ao mesmo tempo - por padrão, de qualquer maneira.

Estou me perguntando se há uma maneira (em qualquer sistema operacional conhecido, especialmente Windows) para iniciar várias instâncias de um processo, de modo que todas elas se liguem ao soquete e, assim, compartilhem efetivamente a fila. Cada instância de processo pode então ter um único encadeamento; ele apenas bloquearia ao aceitar uma nova conexão. Quando um cliente se conectava, uma das instâncias de processo ociosas aceitaria esse cliente.

Isso permitiria que cada processo tivesse uma implementação de thread única muito simples, sem compartilhar nada, a menos que por meio de memória compartilhada explícita, e o usuário seria capaz de ajustar a largura de banda de processamento iniciando mais instâncias.

Esse recurso existe?

Edit: Para aqueles que perguntam "Por que não usar tópicos?" Obviamente, os tópicos são uma opção. Mas com vários threads em um único processo, todos os objetos são compartilháveis ​​e muito cuidado deve ser tomado para garantir que os objetos não sejam compartilhados ou sejam visíveis apenas para um thread de cada vez, ou sejam absolutamente imutáveis, e as linguagens mais populares e tempos de execução carecem de suporte integrado para gerenciar essa complexidade.

Ao iniciar um punhado de processos de trabalho idênticos, você obteria um sistema simultâneo no qual o padrão é nenhum compartilhamento, tornando muito mais fácil construir uma implementação correta e escalável.

Daniel Earwicker
fonte
2
Eu concordo, vários processos podem tornar mais fácil criar uma implementação correta e robusta. Escalável, não tenho certeza, depende do domínio do seu problema.
MarkR

Respostas:

92

Você pode compartilhar um soquete entre dois (ou mais) processos no Linux e até no Windows.

No Linux (ou sistema operacional do tipo POSIX), o uso fork()fará com que o filho bifurcado tenha cópias de todos os descritores de arquivo do pai. Qualquer coisa que ele não fechar continuará a ser compartilhada e (por exemplo, com um soquete de escuta TCP) pode ser usado para accept()novos soquetes para clientes. É assim que muitos servidores, incluindo o Apache na maioria dos casos, funcionam.

No Windows, a mesma coisa é basicamente verdadeira, exceto que não há fork()chamada de sistema, então o processo pai precisará usar CreateProcessou algo para criar um processo filho (que pode, é claro, usar o mesmo executável) e precisa passar para ele um identificador herdável.

Tornar um soquete de escuta um identificador herdável não é uma atividade completamente trivial, mas também não é muito complicada. DuplicateHandle()precisa ser usado para criar um identificador duplicado (ainda no processo pai), que terá o sinalizador herdável definido nele. Em seguida, você pode fornecer esse identificador na STARTUPINFOestrutura para o processo filho em CreateProcess como um STDIN, OUTou ERRidentificador (assumindo que você não deseja usá-lo para mais nada).

EDITAR:

Lendo a biblioteca MDSN, parece que WSADuplicateSocketé um mecanismo mais robusto ou correto de fazer isso; ainda não é trivial porque os processos pai / filho precisam descobrir qual identificador precisa ser duplicado por algum mecanismo IPC (embora isso possa ser tão simples quanto um arquivo no sistema de arquivos)

ESCLARECIMENTO:

Em resposta à pergunta original do OP, não, os processos múltiplos não podem bind(); apenas o processo pai original chamaria bind(), listen()etc, os processos filho só iria processar os pedidos por accept(), send(), recv()etc.

MarkR
fonte
3
Vários processos podem ser vinculados especificando-se a opção de soquete SocketOptionName.ReuseAddress.
sipwiz,
Mas de que adianta? De qualquer forma, os processos são mais pesados ​​do que os threads.
Anton Tykhyy,
7
Os processos são mais pesados ​​do que os threads, mas como eles apenas compartilham coisas explicitamente compartilhadas, menos sincronização é necessária, o que torna a programação mais fácil e pode até ser mais eficiente em alguns casos.
MarkR
11
Além disso, se um processo filho travar ou quebrar de alguma forma, é menos provável que afete o pai.
MarkR
3
Também é bom notar que, no Linux, você pode "passar" sockets para outros programas sem usar fork () e não tem uma relação pai / filho, usando Sockets Unix.
Rahly
34

Muitos outros forneceram as razões técnicas pelas quais isso funciona. Aqui estão alguns códigos python que você pode executar para demonstrar isso por si mesmo:

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()

Observe que, de fato, existem dois IDs de processo escutando:

$ 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)

Aqui estão os resultados da execução do telnet e do programa:

$ 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
Anil Vaitla
fonte
2
Portanto, para uma conexão, tanto o pai quanto o filho conseguem. Mas quem consegue a conexão é indeterminista, certo?
Hot.PxL
1
sim, acho que depende de qual processo está agendado para rodar pelo SO.
Anil Vaitla
14

Eu gostaria de acrescentar que os sockets podem ser compartilhados no Unix / Linux via sockets AF__UNIX (sockets inter-processo). O que parece acontecer é que um novo descritor de soquete é criado, algo como um alias do original. Este novo descritor de socket é enviado através do socket AFUNIX para o outro processo. Isso é especialmente útil nos casos em que um processo não pode fazer fork () para compartilhar seus descritores de arquivo. Por exemplo, ao usar bibliotecas que evitam isso devido a problemas de threading. Você deve criar um soquete de domínio Unix e usar libancillary para enviar o descritor.

Vejo:

Para criar soquetes AF_UNIX:

Por exemplo de código:

zachthehack
fonte
13

Parece que essa pergunta já foi respondida totalmente por MarkR e zackthehack, mas eu gostaria de acrescentar que o Nginx é um exemplo do modelo de herança de soquete de escuta.

Aqui está uma boa descrição:

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

                            June 6, 2007
             Md. Mansoor Peerbhoy <[email protected]>

...

Fluxo de um processo de trabalho NGINX

Depois que o processo NGINX principal lê o arquivo de configuração e se ramifica no número configurado de processos de trabalho, cada processo de trabalho entra em um loop onde aguarda quaisquer eventos em seu respectivo conjunto de soquetes.

Cada processo de trabalho começa apenas com os soquetes de escuta, uma vez que ainda não há conexões disponíveis. Portanto, o descritor de evento definido para cada processo de trabalho começa apenas com os soquetes de escuta.

(NOTA) O NGINX pode ser configurado para usar qualquer um dos vários mecanismos de pesquisa de eventos: aio / devpoll / epoll / eventpoll / kqueue / poll / rtsig / select

Quando uma conexão chega em qualquer um dos soquetes de escuta (POP3 / IMAP / SMTP), cada processo de trabalho emerge de sua pesquisa de evento, uma vez que cada processo de trabalho NGINX herda o soquete de escuta. Em seguida, cada processo de trabalho NGINX tentará adquirir um mutex global. Um dos processos de trabalho adquirirá o bloqueio, enquanto os outros voltarão para seus respectivos loops de votação de eventos.

Enquanto isso, o processo de trabalho que adquiriu o mutex global examinará os eventos acionados e criará as solicitações de fila de trabalho necessárias para cada evento acionado. Um evento corresponde a um único descritor de soquete do conjunto de descritores dos quais o trabalhador estava observando os eventos.

Se o evento disparado corresponder a uma nova conexão de entrada, o NGINX aceita a conexão do soquete de escuta. Em seguida, ele associa uma estrutura de dados de contexto ao descritor de arquivo. Este contexto contém informações sobre a conexão (se POP3 / IMAP / SMTP, se o usuário já está autenticado, etc). Em seguida, esse soquete recém-construído é adicionado ao descritor de evento definido para esse processo de trabalho.

O trabalhador agora renuncia ao mutex (o que significa que todos os eventos que chegaram em outros trabalhadores podem ser processados) e começa a processar cada solicitação que foi colocada na fila anteriormente. Cada solicitação corresponde a um evento que foi sinalizado. De cada descritor de soquete que foi sinalizado, o processo de trabalho recupera a estrutura de dados de contexto correspondente que foi anteriormente associada a esse descritor e, em seguida, chama as funções de retorno de chamada correspondentes que executam ações com base no estado dessa conexão. Por exemplo, no caso de uma conexão IMAP estabelecida recentemente, a primeira coisa que o NGINX fará é escrever a mensagem de boas-vindas IMAP padrão no
soquete conectado (* OK, pronto para IMAP4).

Aos poucos, cada processo de trabalho conclui o processamento da entrada da fila de trabalho para cada evento pendente e retorna ao seu loop de pesquisa de eventos. Uma vez que qualquer conexão é estabelecida com um cliente, os eventos geralmente são mais rápidos, pois sempre que o socket conectado estiver pronto para leitura, o evento de leitura é acionado e a ação correspondente deve ser executada.

richardw
fonte
11

Não tenho certeza se isso é relevante para a questão original, mas no kernel do Linux 3.9 há um patch adicionando um recurso TCP / UDP: suporte TCP e UDP para a opção de soquete SO_REUSEPORT; A nova opção de soquete permite que vários soquetes no mesmo host se vinculem à mesma porta e tem como objetivo melhorar o desempenho de aplicativos de servidor de rede multithread rodando em sistemas multicore. mais informações podem ser encontradas no link LWN LWN SO_REUSEPORT no Linux Kernel 3.9 conforme mencionado no link de referência:

a opção SO_REUSEPORT não é padrão, mas está disponível em uma forma semelhante em vários outros sistemas UNIX (notavelmente, os BSDs, onde a ideia se originou). Parece oferecer uma alternativa útil para obter o máximo desempenho dos aplicativos de rede executados em sistemas com vários núcleos, sem ter que usar o padrão fork.

Walid
fonte
A partir do artigo LWN, quase parece que SO_REUSEPORTcria um pool de threads, onde cada socket está em um thread diferente, mas apenas um socket no grupo executa o accept. Você pode confirmar se todos os sockets no grupo obtêm uma cópia dos dados?
jww
3

Tenha uma única tarefa cujo único trabalho seja ouvir as conexões de entrada. Quando uma conexão é recebida, ele aceita a conexão - isso cria um descritor de soquete separado. O soquete aceito é passado para uma de suas tarefas de trabalho disponíveis e a tarefa principal volta a escutar.

s = socket();
bind(s);
listen(s);
while (1) {
  s2 = accept(s);
  send_to_worker(s2);
}
HUAGHAGUAH
fonte
Como o soquete é passado para um trabalhador? Tenha em mente que a ideia é que um trabalhador seja um processo separado.
Daniel Earwicker
fork () talvez, ou uma das outras idéias acima. Ou talvez você separe completamente o soquete I / O do processamento de dados; enviar a carga útil para processos de trabalho por meio de um mecanismo IPC. O OpenSSH e outras ferramentas OpenBSD usam esta metodologia (sem threads).
HUAGHAGUAH
3

No Windows (e Linux), é possível que um processo abra um soquete e, em seguida, passe esse soquete para outro processo, de modo que esse segundo processo também possa usar esse soquete (e passá-lo adiante, se desejar) .

A chamada de função crucial é WSADuplicateSocket ().

Isso preenche uma estrutura com informações sobre um soquete existente. Essa estrutura então, por meio de um mecanismo IPC de sua escolha, é passada para outro processo existente (observe que digo existente - quando você chama WSADuplicateSocket (), você deve indicar o processo de destino que receberá a informação emitida).

O processo de recebimento pode então chamar WSASocket (), passando essa estrutura de informações e receber um identificador para o soquete subjacente.

Ambos os processos agora mantêm um identificador para o mesmo soquete subjacente.


fonte
2

Parece que o que você deseja é um processo de escuta de novos clientes e, em seguida, desligue a conexão quando você conseguir uma conexão. Fazer isso entre threads é fácil e em .Net você ainda tem os métodos BeginAccept etc. para cuidar de grande parte do encanamento para você. Transferir as conexões através dos limites do processo seria complicado e não teria nenhuma vantagem de desempenho.

Alternativamente, você pode ter vários processos vinculados e escutando no mesmo soquete.

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 + ".");
}

Se você iniciar dois processos, cada um executando o código acima, ele funcionará e o primeiro processo parece obter todas as conexões. Se o primeiro processo for encerrado, o segundo obterá as conexões. Com o compartilhamento de soquete como esse, não sei exatamente como o Windows decide qual processo obtém novas conexões, embora o teste rápido aponte para o processo mais antigo obtendo-as primeiro. Se ele compartilha se o primeiro processo está ocupado ou algo assim, eu não sei.

Sipwiz
fonte
2

Outra abordagem (que evita muitos detalhes complexos) no Windows, se você estiver usando HTTP, é usar HTTP.SYS . Isso permite que vários processos escutem URLs diferentes na mesma porta. No Server 2003/2008 / Vista / 7 é assim que o IIS funciona, então você pode compartilhar portas com ele. (No XP SP2, HTTP.SYS é compatível, mas IIS5.1 não o usa.)

Outras APIs de alto nível (incluindo WCF) usam HTTP.SYS.

Richard
fonte