Пожалуйста, помогите мне понять, что не так с этим кодом веб-прокси
Я хочу написать веб-прокси для упражнений, и этот код у меня есть:
// 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
звонки, а еще ничего.
Можете ли вы определить, что я делаю не так?
РЕДАКТИРОВАТЬ:
- Я заметил, что если я добавлю немного
sleep
звонки, прокси, кажется, "работает" на большее количество запросов, но не на все из них. - чтобы получить награду, помогите мне узнать, что я делаю не так. Какой обычный "алгоритм" используется для веб-прокси? Где я отклоняюсь от этого? Спасибо!
6 ответов
Во-первых, действительно трудно понять, что именно здесь происходит не так: "Иногда он делает хороший запрос, иногда нет". на самом деле не описывает, что происходит, когда возникает проблема!
Тем не менее, я все еще был в состоянии понять, что происходит для вас.
Как вы уже сказали, вы ищете самое простое решение, которое будет работать согласованно, поэтому я буду избегать всего ненужного и не затрагивающего эффективность вашего кода. Кроме того, я сначала дам вам ответ, а затем опишу причину проблемы (это долго, но стоит прочитать:)
Решение
Простой ответ на вашу проблему заключается в том, что вам нужно выполнить синтаксический анализ протокола HTTP, чтобы выяснить, все ли данные были отправлены клиентом, а не полагаться на то, что available()
или же read()
вернуть. Сколько это PITA зависит от того, насколько полно вы хотите поддерживать протокол HTTP. Поддерживать запросы GET довольно просто. Немного сложнее поддерживать POST, которые определяют длину контента. Гораздо сложнее поддерживать "другие" типы кодирования (например, chunked или multipart/byteranges, см. http://tools.ietf.org/html/rfc2616).
В любом случае, я предполагаю, что вы просто пытаетесь заставить работать GET, поэтому для этого вы должны знать, что заголовки и тела HTTP разделены "пустой строкой", разделителем строк HTTP является \r\n и что GET делают не иметь тела. Поэтому клиент завершил отправку запроса 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, по своей природе, никогда не может фактически сказать, будет ли больше данных (исключение составляет, если нет больше базовых данных для чтения - например, сокет закрыт, конец массива или файла имеет достигнут и т. д. - это единственный раз, когда read() вернет -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);
или быть полностью удаленным.
Джонатан был на правильном пути. Проблема частично в вашем использовании 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()
}
Тем не менее, у вас есть несколько других проблем:
- вы вытягиваете весь ответ в память, когда вы должны копировать его в цикле байтовой помпы непосредственно в поток вывода ответа клиента (что произойдет, если это ответ на несколько гигабайт)
- вы используете Strings для хранения двоичных данных - что предполагает, что все байты работают нормально в кодировке CharacterEncoding по умолчанию, которая может быть истинной в UTF-8 или US-ASCII, но не будет работать с другими локалями
Ry4an делает несколько хороших замечаний. Если вы хотите увидеть, как устроен небольшой, но идеально сформированный прокси, посмотрите на Tiny HTTP Proxy, который написан на Python - вы можете увидеть все проблемы, которые необходимо решить, и было бы довольно просто перенести код на Groovy., Я использовал прокси для тестирования, и он работает хорошо.
Может ли быть состояние гонки в readClientData(Socket)? Похоже, вы немедленно проверяете, доступны ли данные, но возможно, что данные еще не получены; вы просто выпадете из цикла, а не будете ждать получения первых данных.
Клиентский сокет блокируется? Если это так, вы можете попробовать неблокировать ввод-вывод или установить время ожидания сокета.
Я предлагаю вам ознакомиться со спецификацией протокола HTTP. HTTP сложнее, чем один запрос-ответ по отдельному TCP-соединению - т. Е. Ваша реализация потерпит неудачу, если клиент или сервер попытается использовать постоянные соединения.