Ответ 1
Я не могу найти ссылку, но я верю, что сервлет начинает обрабатываться после того, как весь заголовок доступен (все заголовки запросов следуют двумя новыми строками). Поэтому у вас есть getInputStream()
и getReader()
, а не getBody()
, возвращающие String
или byte[]
.
Таким образом, сервлет может начать обрабатывать данные запроса, пока клиент все еще его отправляет, что позволяет сервлетам обрабатывать очень большие объемы данных с небольшим объемом памяти. Например, загрузочный сервлет может читать загруженный байтовый файл байтом и сохранять его на диск без необходимости иметь полное содержимое запроса в памяти одновременно.
Вот сервлет, который я использовал для тестирования (в Scala, извините за это):
@WebServlet(Array("/upload"))
class UploadServlet extends HttpServlet {
@Override
override def doPost(request: HttpServletRequest, response: HttpServletResponse) {
println(request.getParameter("name"));
val input = Source.fromInputStream(request.getInputStream)
input.getLines() foreach println
println("Done")
}
}
Теперь я использую nc
для имитации медленного клиента:
$ nc localhost 8080
Ничего не происходит на стороне сервера. Теперь я вручную отправляю HTTP-заголовки:
POST /upload?name=foo HTTP/1.1
Host: localhost:8080
Content-Length: 10000000
По-прежнему ничего не происходит на стороне сервера. Tomcat принял соединение, но еще не вызвал UploadServlet.doPost
. Но в тот момент, когда я ударил Enter два раза, сервлет печатает параметр name
, но блокирует на getLines()
(getInputStream()
снизу).
Теперь я могу отправлять строки текста (Tomcat ожидает 10000000
bytes) с помощью nc
, и они инкрементно печатаются на стороне сервера (input.getLines()
возвращает блокировку Iterator[String]
до тех пор, пока не появится новая строка).
Обзор сервлетов
-
Tomcat ожидает заголовок HTTP целиком, прежде чем он начнет обработку запроса (передав его в соответствующий сервлет)
-
Тело запроса не должно быть полностью доступно до вызова
doPost()
. Это прекрасно, иначе у нас скоро закончится память. -
То же самое относится к отправке ответа - мы можем сделать это постепенно.
Spring MVC
С Spring MVC вы должны быть осторожны. Рассмотрим следующие два метода (обратите внимание на разные типы аргументов):
@Controller
@RequestMapping(value = Array("/upload"))
class UploadController {
@RequestMapping(value = Array("/good"), method = Array(POST))
@ResponseStatus(HttpStatus.NO_CONTENT)
def goodUpload(body: InputStream) {
//...
}
@RequestMapping(value = Array("/bad"), method = Array(POST))
@ResponseStatus(HttpStatus.NO_CONTENT)
def badUpload(@RequestBody body: Array[Byte]) {
//...
}
}
Ввод /upload/good
будет вызывать метод обработчика goodUpload
, как только будет получен HTTP-заголовок, но он будет заблокирован, если вы попытаетесь прочитать body
InputStream
, если тело еще не получено.
Однако /upload/bad
будет ожидать, пока тело POST
будет доступно, поскольку мы явно запросили весь объект как массив байтов (String
будет иметь тот же эффект): @RequestBody body: Array[Byte]
.
Итак, вам решать, как Spring MVC обрабатывает большие тела запросов.
Уровень TCP/IP
Помните, что HTTP работает поверх TCP/IP. Просто потому, что вы не вызывали getInputStream()
/getReader()
, не означает, что сервер не получает данные от клиента. Фактически, операционная система управляет сетевым сокетом и продолжает получать пакеты TCP/IP, которые не потребляются. Это означает, что данные от клиента переносятся на сервер, но операционная система должна буферизовать эти данные.
Возможно, кто-то более опытный может ответить на то, что происходит в этих ситуациях (на самом деле это не вопрос для этого сайта). O/S может внезапно закрыть розетку, если сервер не будет считывать входящие данные, или он может просто буферизовать его и обменять, если буфер увеличивается до большого? Другим решением может быть прекращение подтверждения клиентских пакетов, что приводит к замедлению/остановке клиента. Действительно зависит от O/S, а не от HTTP/сервлетов.