Пожалуйста, помогите мне понять, что не так с этим кодом веб-прокси - PullRequest
2 голосов
/ 11 августа 2009

Я хочу написать веб-прокси для упражнений, и этот код у меня есть:


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

Сейчас все, что он делает, это:

  • читать HTTP-запрос, который отправляет мой браузер

  • парсинг хоста и порта

  • подключиться к этому хосту и записать данные, полученные от клиента

  • отправить клиенту обратно данные, полученные с хоста

Но ... это не работает все время. Иногда он сделает хороший запрос, иногда нет. Я думаю, что это проблема буферизации, я не уверен. Дело в том, что я добавил flush звонков, а еще ничего.

Можете ли вы определить, что я делаю не так?

EDIT:

  • Я заметил, что если я добавлю несколько вызовов sleep, прокси, похоже, будет "работать" на большем количестве запросов, но не на всех.
  • чтобы получить награду, помогите мне выяснить, что я делаю не так. Какой обычный «алгоритм» используется для веб-прокси? Где я отклоняюсь от этого? Спасибо!

Ответы [ 6 ]

4 голосов
/ 18 августа 2009

Джонатан был на правильном пути. Проблема частично заключается в использовании вами available(). Метод available не говорит "это сделано?" он говорит: «Есть ли в настоящее время какие-либо данные?». Поэтому сразу после того, как вы сделали запрос, данных не будет, и в зависимости от времени сети, которое может произойти и во время обработки, это не означает, что больше ничего не будет, поэтому ваш break преждевременен.

Кроме того, семейство методов InputStream.read(byte[] ...) всегда позволяет возвращать меньше байтов, чем вы запрашиваете. Длина массива или смещение, длина пары ограничивает максимум , но вы всегда можете получить меньше. Итак, ваш код:

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

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

Вот ревизия, основанная на том факте, что InputStream.read(...) никогда не вернется, если не будет конца потока или если доступны некоторые данные (но не обязательно столько, сколько вы просили).

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

Тем не менее, у вас также есть несколько других проблем:

  • вы вытягиваете весь ответ в память, когда вы должны копировать его в цикле байтовой помпы непосредственно в поток вывода ответа клиента (что произойдет, если это ответ на несколько гигабайт)
  • вы используете строки для хранения двоичных данных - что предполагает, что все байты работают нормально в кодировке символов по умолчанию, что может быть верно в UTF-8 или US-ASCII, но не будет работать с другими локалями
3 голосов
/ 22 августа 2009

Во-первых, действительно трудно понять, что именно здесь происходит не так: «Иногда он делает хороший запрос, иногда нет». на самом деле не описывает, что происходит, когда возникает проблема !!

Тем не менее, я все еще был в состоянии понять, что происходит для вас.

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

Решение

Простой ответ на вашу проблему заключается в том, что вам необходимо выполнить синтаксический анализ протокола HTTP, чтобы выяснить, все ли данные были отправлены клиентом, а не полагаться на то, что возвращает available() или read(). Сколько это PITA зависит от того, насколько полно вы хотите поддерживать протокол HTTP. Поддерживать запросы GET довольно просто. Немного сложнее поддерживать POST, которые определяют длину контента. Гораздо сложнее поддерживать «другие» типы кодирования (например, chunked или multipart / byteranges, см. http://tools.ietf.org/html/rfc2616#section-4.4).

В любом случае, я предполагаю, что вы просто пытаетесь заставить работать GET, поэтому для этого вы должны знать, что заголовки и тела HTTP разделены "пустой строкой", разделителем строк HTTP является \ r \ n и что ПОЛУЧАЕТ, не имеет тела. Поэтому клиент завершил отправку запроса GET, когда он передает \ r \ n \ r \ n.

Какой-то код, подобный этому, должен последовательно обрабатывать GET для вас (код не проверен, но он должен довести вас как минимум до 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()
}

Для POST вам нужно добавить к этому, также ища строку «Content-length:» и анализируя значение после этого. Это значение представляет собой размер тела HTTP (то есть бит, который следует после / r / n / r / n конца метки заголовка) в восьмеричных числах . Поэтому, когда вы сталкиваетесь с концом заголовка, вам просто нужно посчитать это число восьмеричных байтов, и вы знаете, что запрос POST завершил передачу.

Вам также необходимо определить тип запроса (GET, POST и т. Д.) - это можно сделать, проверив символы, переданные перед первым пробелом.

Проблема

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

должны быть напечатаны неполные запросы на стандартный вывод.
println(new String(buffer))

с

println(avaliable)

в функции readClientData.

Почему это происходит? Это потому, что available () сообщает вам только то, что в данный момент доступно для чтения из InputStream, а не то, отправил ли клиент все данные, которые он собирается отправить. InputStream, по своей природе, никогда не может фактически сказать, будет ли больше данных (исключение составляет, если нет больше базовых данных для чтения - например, сокет закрыт, конец массива или файла имеет достигнуто и т. д. - это только время чтения () вернет -1 (т. е. EOF)). Вместо этого, код более высокого уровня должен решить, следует ли ему читать больше данных из потока, и он принимает это решение на основе правил для конкретного приложения, которые применяются к данным для конкретного приложения, читаемым InputStream.

В этом случае приложением является HTTP, поэтому вам нужно понять основы протокола HTTP, прежде чем вы начнете работать (cmeerw, вы были на правильном пути).

Когда клиент отправляет HTTP-запрос, клиент открывает сокет для сервера и отправляет запрос. Клиент только закрывает сокет в результате тайм-аута, или при отключении основного сетевого подключения, или в ответ на действие пользователя, которое требует, чтобы сокет был закрыт (приложение закрыто, страница обновлена, остановлен кнопка нажата и т.д.). В противном случае, после отправки запроса, он просто ждет, пока сервер отправит ответ. Как только сервер отправил ответ, сервер закрывает соединение [1].

Там, где ваш код успешно выполняется, данные предоставляются клиентом достаточно быстро и достаточно последовательно, так что InputStream получает дополнительные данные между вашим вызовом read() и вашим последующим вызовом available() на следующей итерации цикла (запомните что InputStream предоставляется с данными "параллельно" к вашему коду, который вызывает его метод read()). Теперь в другом случае, когда ваш код дает сбой, данные InputStream еще не были предоставлены, поэтому, когда ваш код вызывает available(), InputStream правильно возвращает 0, так как больше никаких данных не было предоставлено, так как вы вызвали read() и поэтому он имеет 0 байтов для вас read(). Это условие гонки, о котором говорит Джонатан.

В вашем коде предполагается, что, когда available() возвращает 0, все данные были отправлены клиентом, хотя на самом деле иногда это происходит, а иногда нет (поэтому иногда вы получаете «хороший запрос», а иногда нет). :.)

Поэтому вам нужно что-то лучше, чем available(), чтобы определить, отправил ли клиент все данные.

Проверка EOF при вызове read() (см. Ответ R4an [2]) также не подходит. Должно быть понятно, почему это так - единственный раз, когда read() должен возвращать EOF (-1), это когда сокет закрыт. Это не должно происходить, пока вы не перенаправили запрос целевому прокси, не получили ответ и не отправили этот ответ клиенту, но мы знаем, что он также может быть исключен клиентом. На самом деле вы наблюдаете такое поведение при запуске примера кода - прокси зависает до тех пор, пока в браузере не будет нажата кнопка остановки, в результате чего клиент преждевременно закроет соединение.

Правильный ответ, который вы теперь знаете, - это выполнить синтаксический анализ HTTP и использовать его для определения состояния соединения.

Примечания
[1] Это не является доказательством концептуального прокси, но поскольку оно уже было затронуто, если HTTP-соединение «keep-alive», сервер будет держать соединение открытым и ждать другого запроса от клиента
[2] В этом коде есть ошибка, из-за которой readClientData искажает данные:

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

Второй вызов inStream.read() полностью перезаписывает данные, прочитанные первым вызовом inStream.read(). Также здесь переопределяется bytesRead (недостаточно знакомый с Groovy, чтобы знать, будет ли это ошибкой). Эта строка должна либо читать:

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

или быть полностью удаленным.

1 голос
/ 18 августа 2009

Ry4an делает несколько хороших замечаний. Если вы хотите увидеть, как устроен небольшой, но идеально сформированный прокси, посмотрите на Tiny HTTP Proxy , который написан на Python - вы можете увидеть все проблемы, которые необходимо решить, и это будет довольно просто портировать код на Groovy. Я использовал прокси для тестирования, и он хорошо работает.

0 голосов
/ 17 августа 2009

Клиентский сокет блокируется? Если это так, вы можете попробовать неблокировать ввод-вывод или установить время ожидания сокета.

0 голосов
/ 17 августа 2009

Может ли быть состояние гонки в readClientData (Socket)? Похоже, вы немедленно проверяете, доступны ли данные, но возможно, что данные еще не были получены; вы просто выйдете из цикла, а не будете ждать получения первых данных.

0 голосов
/ 12 августа 2009

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...