Por favor me ajude a descobrir o que há de errado com este código de proxy da web

Eu quero escrever um proxy web para o exercício, e este é o código que tenho até agora:


// returns a map that contains the port and the host
def parseHostAndPort(String data) {
    def objMap // this has host and port as keys
    data.eachLine { line ->
        if(line =~ /^(?i)get|put|post|head|trace|delete/) {
            println line
            def components = line.split(" ")
            def resource = components[1]
            def colon = resource.indexOf(":")
            if(colon != -1) {
                URL u = new URL(resource)
                def pHost = u.host
                def pPort = u.port
                return (objMap = [host:pHost,port:pPort])
            }
            else {
                return (objMap = [host:resource,port:80])
            }
        }
    }
    return objMap
}

// reads a http request from a client
def readClientData(Socket clientSocket) {
    def actualBuffer = new StringBuilder()
    InputStream inStream = clientSocket.inputStream
    while(true) {
        def available = inStream.available()
        if(available == 0)
        break;
        println "available data $available"
        def buffer = new byte[available]
        def bytesRead = inStream.read(buffer,0,available)
        actualBuffer << new String(buffer)
    }
    return actualBuffer.toString()
}

def sock = new ServerSocket(9000)
sock.reuseAddress = true
while(true) {
    sock.accept { cli ->
        println "got a client"
        def data = readClientData(cli)
        def parsed = parseHostAndPort(data)
        def host = parsed["host"]
        def port = parsed["port"]

        println "got from client $data"

        def nsock = new Socket(host,port)
        nsock << data // send data received from client to the socket
        nsock.outputStream.flush() 
        def datax = readClientData(nsock)
        println "got back $datax"
        cli << datax // send the client the response
        cli.outputStream.flush()
        cli.close()
    }
}

Agora, tudo que faz é:

leia a solicitação HTTP que meu navegador envia

analisar o host e a porta

conectar-se a esse host e gravar os dados recebidos do cliente

enviar o cliente de volta os dados recebidos do host

Mas ... não funciona o tempo todo. Às vezes, faz um bom pedido, às vezes não. Eu acho que é um problema de buffer, não tenho certeza. A coisa é, eu adicioneiflush chama, e ainda nada.

Você consegue identificar o que estou fazendo de errado?

EDITAR:

Notei que se eu adicionar algunssleep chama, o proxy parece "trabalhar" em um número maior de solicitações, mas não todas elas.para coletar a recompensa, me ajude a descobrir o que estou fazendo de errado. Qual é o "algoritmo" normal usado para um proxy da web? Onde estou me desviando disso? Obrigado!

questionAnswers(6)

QuestionSolution

Primeiro, é realmente difícil saber exatamente o que está errado aqui - "Às vezes, é um bom pedido, às vezes não." realmente não descreve o que está acontecendo quando o problema ocorre !!

Dito isto, eu ainda era capaz de descobrir o que está errado para você.

Como você já disse, você está procurando a solução mais básica que funcionará de forma consistente, por isso evitarei qualquer coisa desnecessária ou a eficiência do seu código. Além disso, vou dar-lhe a resposta primeiro e, em seguida, descrever o que está causando o problema (é longo, mas vale a pena ler :)

Solução

A resposta simples para o seu problema é que você precisa fazer alguma análise do protocolo HTTP para descobrir se todos os dados foram enviados pelo cliente e não confiar em queavailable() ouread() Retorna. Quanto de um PITA isso depende de quão completamente você deseja suportar o protocolo HTTP. Para suportar pedidos GET, é muito fácil. É um pouco mais difícil suportar os POSTs que especificam o tamanho do conteúdo. É muito mais difícil suportar "outros" tipos de codificação (por exemplo, pedaços ou multipartes / byteshttp://tools.ietf.org/html/rfc2616#section-4.4).

De qualquer forma, eu suponho que você está apenas tentando fazer os GETs funcionarem, então para fazer isso, você precisa saber que os cabeçalhos e bodys HTTP estão separados por uma "linha vazia", ​​que o delimitador de linha do HTTP é \ r \ n e que GETs fazem não tem um corpo. Portanto, um cliente concluiu o envio de uma solicitação GET quando transmite \ r \ n \ r \ n.

Algum código como este deve lidar com GETs consistentemente para você (o código não foi testado, mas deve levá-lo a pelo menos 90%):

def readClientData(Socket clientSocket) {

    def actualBuffer = new StringBuilder()
    def eof = false;

    def emptyLine = ['\r', '\n', '\r', '\n']
    def lastEmptyLineChar = 0

    InputStream inStream = clientSocket.inputStream
    while(!eof) {
        def available = inStream.available()
        println "available data $available"

        // try to read all available bytes
        def buffer = new byte[available]
        def bytesRead = inStream.read(buffer,0,available)

        // check for empty line: 
        //    * iterate through the buffer until the first element of emptyLine is found
        //    * continue iterating through buffer checking subsequent elements of buffer with emptyLine while consecutive elements match
        //    * if any element in buffer and emptyLine do not match, start looking for the first element of emptyLine again as the iteration through buffer continues
        //    * if the end of emptyLine is reached and matches with buffer, then the emptyLine has been found
        for( int i=0; i < bytesRead && !eof; i++ ) {
            if( buffer[i] == emptyLine[lastEmptyLineChar] ){
                lastEmptyLineChar++
                eof = lastEmptyLineChar >= emptyLine.length()
            }
            else {
                lastEmptyLineChar = 0
            }

        }

        // changed this so that you avoid any encoding issues
        actualBuffer << new String(buffer, 0, bytesRead, Charset.forName("US-ASCII"))
    }
    return actualBuffer.toString()
}

Para os POSTs, você precisa adicionar isso também procurando a String "Content-length:" e analisando o valor após isso. Este valor é o tamanho do corpo HTTP (ou seja, o bit que vem depois do fim de / r / n / r / n da marca do cabeçalho)em octais. Então, quando você encontrar o final do cabeçalho, você só precisa contar esse número deoctais de bytes e você sabe que a solicitação POST completou a transmissão.

Você também precisará determinar o tipo de solicitação (GET, POST etc.) - você pode fazer isso inspecionando os caracteres transmitidos antes do primeiro espaço.

Problema

Seu problema é que seureadClientData A função nem sempre lê todos os dados enviados pelo cliente. Como resultado, você está enviando uma solicitação parcial para o servidor e retorna algum tipo de erro. Você deve ver solicitações incompletas impressas de acordo com o padrão se você substituir

println(new String(buffer))

com

println(avaliable)

noreadClientData função.

Por que isso está acontecendo? É porque o available () apenas informa o que está atualmente disponível para ser lido a partir do InputStream e não se o cliente enviou ou não todos os dados que vai enviar. Um InputStream, por sua própria natureza, nunca pode realmente dizer se haverá ou não mais dados (a exceção a isso é se não houver mais dados subjacentes para ler - por exemplo, um soquete é fechado, o final do array ou arquivo tem alcançado, etc. - este é osó time read () retornará -1 (isto é, EOF)). Em vez disso, cabe ao código de nível superior decidir se deve ler mais dados do fluxo e tomar essa decisão com base nas regras específicas do aplicativo que se aplicam aos dados específicos do aplicativo que estão sendo lidos pelo InputStream.

Neste caso, o aplicativo é HTTP, então você precisa entender os fundamentos do protocolo HTTP antes de obter este trabalho (cmeerw, você estava no caminho certo).

Quando uma solicitação HTTP é feita por um cliente, o cliente abre um soquete para o servidor e envia uma solicitação. O cliente fecha o soquete como resultado de um tempo limite, ou a conexão de rede subjacente está sendo desconectada ou em resposta à ação do usuário que requer que o soquete seja fechado (o aplicativo é fechado, a página é atualizada, o botão de parada é pressionado, etc.). Caso contrário, depois de enviar o pedido, ele apenas espera que o servidor envie uma resposta. Depois que o servidor envia a resposta, o servidor fecha a conexão [1].

Onde o seu código é bem-sucedido, os dados são fornecidos pelo cliente de maneira rápida e consistente o suficiente para que o InputStream receba dados adicionais entre sua invocação deread() e sua invocação subseqüente deavailable() na próxima iteração do loop (lembre-se queInputStream está sendo fornecido com dados "em paralelo" para o seu código que está invocando suaread() método). Agora, no outro caso, onde seu código falha, nenhum dado foi fornecido paraInputStream, quando o seu código invocaavailable(), InputStream retorna corretamente 0, uma vez que nenhum dado adicional foi fornecido desde que você invocouread() e, portanto, tem 0 bytes disponíveis para vocêread(). Esta é a condição de corrida que Johnathan está falando.

Seu código assume que quandoavailable() retorna 0 que todos os dados foram enviados pelo cliente quando, na verdade, às vezes tem, e às vezes não tem (por isso, às vezes você recebe um "pedido bom" e outras vezes não :).

Então você precisa de algo melhor queavailable() para determinar se o cliente enviou ou não todos os dados.

Verificando EOF quando você invocaread() (veja a resposta de R4an [2]) também não é adequado. Deve ficar claro porque é esse o caso - a única vezread() é suposto para retornar EOF (-1) é quando o soquete está fechado. Isso não deve acontecer até que você tenha encaminhado a solicitação ao proxy de destino, recebido uma resposta e enviado essa resposta ao cliente, mas sabemos que também pode ser fechado excepcionalmente pelo cliente. Na verdade, você está vendo esse comportamento quando executa o código de amostra - o proxy trava até que o botão Parar seja clicado no navegador, fazendo com que o cliente feche a conexão prematuramente.

A resposta correta, que você sabe agora, é fazer alguma análise do HTTP e usá-lo para determinar o estado da conexão.

Notas
[1] Está além de uma prova de proxy de conceito, mas como já foi mencionado, se a conexão HTTP for "keep-alive" o servidor manterá a conexão aberta e aguardará outra solicitação do cliente
[2] Há um erro neste código que faz com que o readClientData mangle os dados:

byte[] buffer = new byte[16 * 1024];
while((bytesRead = inStream.read(buffer)) >= 0) { // -1 on EOF
    def bytesRead = inStream.read(buffer,0,bytesRead); 
    actualBuffer << new String(buffer)
}

O segundoinStream.read() invocação sobrescreve completamente os dados lidos pela primeira invocação deinStream.read(). Também bytesRead está sendo redefinido aqui (não está familiarizado o suficiente com o Groovy para saber se isso seria ou não um erro). Esta linha deve ler:

bytesRead = bytesRead + inStream.read(buffer,bytesRead,buffer.length()-bytesRead);

ou ser totalmente removido.

 Geo22 de ago de 2009 17:55
obrigado pela resposta muito detalhada.

Poderia haver uma condição de corrida em readClientData (Socket)? Parece que você está verificando imediatamente se os dados estão disponíveis, mas é possível que os dados ainda não tenham sido recebidos; você simplesmente abandonará o loop em vez de esperar que os primeiros dados sejam recebidos.

 Jonathan18 de ago de 2009 15:27
Não tenho certeza qual seria o melhor método para o HTTP 1.1 (que permite conexões persistentes), mas para o HTTP 1.0, você pode ler apenas até atingir o final do fluxo.
 Geo17 de ago de 2009 18:46
como eu esperaria até que os dados estivessem disponíveis?

Ry4an faz alguns bons pontos. Se você quiser ver como um proxy pequeno, mas perfeitamente formado é construído, vejaPequeno proxy HTTP que está escrito em Python - você pode ver todos os problemas que precisam ser endereçados, e seria bastante simples portar o código para o Groovy. Eu usei o proxy para fins de teste e funciona bem.

Eu sugiro que você se familiarize com oEspecificação do protocolo HTTP. O HTTP é mais complicado do que uma única solicitação-resposta em uma conexão TCP separada - ou seja, sua implementação falhará se o cliente ou o servidor tentarem usar conexões persistentes.

 Geo12 de ago de 2009 17:16
Eu entendo o que você está dizendo, mas este não é o caso.
 AviD20 de ago de 2009 12:19
Http spec ainda vale a pena ler, se você está tentando implementar encanamento, como um proxy ...

O bloqueio de soquete do cliente? Em caso afirmativo, você pode querer tentar E / S sem bloqueio ou definir um tempo limite de soquete.

 Jonathan18 de ago de 2009 15:28
Confira java.nio.channels.SocketChannel
 Geo17 de ago de 2009 20:08
como você pode fazer I / O sem bloqueio em java?

Jonathan estava no caminho certo. O problema é em parte o uso deavailable(). O métodoavailable não diz "está feito?" diz "há atualmente algum dado disponível?". Assim, imediatamente após a sua solicitação, não haverá dados disponíveis e, dependendo do tempo da rede, que pode acontecer durante o processamento também, mas isso não significa que não haverá maisbreak é prematuro.

Também oInputStream.read(byte[] ...) família de métodos ésempre permissão para retornar menos bytes do que você pede. O comprimento da matriz ou deslocamento, par de comprimentos, restringemáximo, mas você sempre pode obter menos. Então, esse seu código:

    def buffer = new byte[available]
    def bytesRead = inStream.read(buffer,0,available)
    actualBuffer << new String(buffer)

poderia criar uma matriz grande, mas só obter metade dos dados na leitura, mas ainda acrescentar o buffer completo (com seus elementos de array não lidos à direita) na String.

Aqui está uma revisão que se baseia no fato de queInputStream.read(...) nunca retornará a menos que seja o final do fluxo ou há alguns dados disponíveis (mas não necessariamente o quanto você pediu).

// reads a http request from a client
def readClientData(Socket clientSocket) {
    def actualBuffer = new StringBuilder()
    InputStream inStream = clientSocket.inputStream
    int bytesRead = 0;
    byte[] buffer = new byte[16 * 1024];
    while((bytesRead = inStream.read(buffer)) >= 0) { // -1 on EOF
        def bytesRead = inStream.read(buffer,0,bytesRead); // only want newly read bytes
        actualBuffer << new String(buffer)
    }
    return actualBuffer.toString()
}

Dito isso, você também tem alguns outros problemas:

você está colocando toda a resposta na memória, quando deveria copiá-la em um byte-pump-loop diretamente no fluxo de saída de resposta do cliente (o que acontece se for uma resposta de vários gigabytes)você está usando Strings para armazenar dados binários - o que pressupõe que todos os bytes funcionam bem no CharacterEncoding padrão, o que pode ser verdade em UTF-8 ou US-ASCII, mas não vai funcionar com outras localidades
 AviD20 de ago de 2009 12:18
Sem mencionar todos os outros cabeçalhos importantes! Por exemplo, Set-Cookie, Location (para 302), Authentication-Required e muito mais ...
 Geo18 de ago de 2009 14:42
Obrigado pelas dicas. No entanto, se eu não verificar os dados disponíveis,readClientData blocos até que eu bati no meu navegador. E se eu verificar os dados disponíveis, volto para onde comecei.
 Ry4an Brase21 de ago de 2009 23:00
Hrm, desde que você está indo um soquete em vez da (melhor) URLConnection para o URL solicitado você está passando de volta cabeçalhos, então o problema é definitivamente nas solicitações que você encerrar incorretamente o recebimento de.
 Ry4an Brase18 de ago de 2009 15:52
A verificação de .available () está definitivamente incorreta - não oferece garantias e não é aceitável para o controle de loop. @cmeerw estava apontando que seu navegador não está fechando o soquete porque está deixando-o aberto para reutilização da conexão. Seu proxy deve detectar o fim da solicitação não aguardando EOF (ou .available () == 0), mas sim analisando corretamente solicitações HTTP. Pedidos não solicitados (GET, HEAD, etc.) terminam com "\r\n\r\n"e solicitações encadeadas (POST, PUT, etc.) terminam de acordo com os comprimentos e limites do conteúdo fornecidos. É necessário ler ativamente as solicitações do cliente para saber quando elas terminam.
 Ry4an Brase18 de ago de 2009 16:01
Caramba! Você também está inteiramente esquecendo de enviar de volta os cabeçalhos de resposta HTTP recebidos do servidor. Eles não estão contidos nonSock.inputStream. Eles fornecem ao navegador o código de status (importante) e o Tamanho do conteúdo, o que ajuda o navegador a saber que não há mais dados chegando.

yourAnswerToTheQuestion