Ставь лайк и потом смотри, вырабатывай полезную привычку
предисловие
За последние два дня я увидел вопрос о Tomcat в другом сообществе, что весьма интересно. Бывает, что раньше я не задумывался над этой проблемой, поэтому сегодня я расскажу об этом «почему» в сочетании с механизмом Tomcat.В этой статье анализируется стандарт загрузки файлов в HTTP-протокол и механизм Tomcat, он относительно базовый, если он вам не нужен, вы можете сразу перейти к концу статьи.
Загрузка файла по протоколу HTTP
Как мы все знаем, HTTP — этотекстовый протокола как текстовый протокол передает файлы?
Передайте это напрямую... да, это так просто. Текстовый протокол — это только с точки зрения прикладного уровня, а когда речь идет о транспортном уровне, то все данные — байты, разницы нет, никакого дополнительного кодирования и декодирования не требуется.
метод multipart/form-data
Протокол HTTP определяетЗагрузка файлов на основе форм. Определите атрибут ENCTYPE в форме со значением multipart/form-data, а затем добавьте тип файла.<input>Этикетка.
<FORM ENCTYPE="multipart/form-data" ACTION="_URL_" METHOD=POST>
File to process: <INPUT NAME="userfile1" TYPE="file">
<INPUT TYPE="submit" VALUE="Send File">
</FORM>
Этот тип формы multipart/form-data несколько отличается от используемой по умолчанию x-www-form-urlencoded. Хотя они оба являются формами и могут загружать несколько полей, первый может загружать файлы, а последний может передавать только текст.
Теперь давайте посмотрим на протокол этого метода загрузки файла формы.На следующем рисунке показан простой тип multipart/form-data сообщения-запроса:Как видно из приведенного выше рисунка, часть HTTP-заголовка не изменилась, но в Content-Type был добавлен граничный тег, но часть полезной нагрузки совершенно другая.
Роль границы в multipart/form-data заключается в разделении нескольких полей формы.В разделе полезной нагрузки есть граница между первой и последней двумя строками, а также будет граница между каждым полем (часть/элемент ).
Когда сервер читает, ему нужно только сначала получить границу из Content-Type, а затем разделить часть полезной нагрузки через эту границу, чтобы получить все поля.
В сообщении каждого поля есть поле Content-Disposition в качестве части заголовка этого поля. Он записывает текущее имя поля (имя), и, если это файл, также будет атрибут имени файла, а следующая строка будет сопровождаться Content-Type для определения типа файла.
В то время как формы x-www-form-urlencoded и multipart могут выполнять передачу полей, multipart может передавать не только текстовые поля, но и файлы. И этот многокомпонентный метод передачи файлов также является «стандартным», который может поддерживаться различными серверами и напрямую читать файлы.
А x-www-form-urlencoded может передавать только базовые текстовые данные, но если вы принудительно трактуете файл как текст, никто не сможет вас остановить при таком виде передачи, а вот при передаче в виде текста бэкенд должен парситься в строковый режим Накладные расходы на кодирование, когда byte -> str совершенно не нужны и могут привести к ошибкам кодирования...
В сообщении типа x-www-form-urlencoded границ нет, и несколько полей будут проходить через&Сращивание символов и кодирование urlencode для ключа/значенияХотя x-www-form-urlencoded добавляет одноэтапный процесс кодирования, он не добавляет заголовок к каждому полю, и границ нет, а размер пакета намного меньше, чем у составного метода.
В дополнение к этой составной части существует также форма прямой загрузки файлов, но она обычно не используется.
двоичный метод полезной нагрузки
В дополнение к multipart/form-data существует также способ загрузки двоичных полезных данных. Эта бинарная полезная нагрузка - мое собственное имя... потому что в HTTP-протоколе нет описания этого метода (если найдется большой парень, опубликуйте ссылку в области комментариев), но многие HTTP-клиенты поддерживают его.
Например, почтальон:Например ОкХттп:
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("image/png");
RequestBody body = RequestBody.create(mediaType, "<file contents here>");
Request request = new Request.Builder()
.url("localhost:8098/upload")
.method("POST", body)
.addHeader("Content-Type", "image/png")
.build();
Response response = client.newCall(request).execute();
Этот метод очень прост, то есть вся полезная часть используется для хранения данных файла. Как показано на рисунке ниже, весь раздел полезной нагрузки является содержимым файла:Хоть этот метод и прост, клиентская реализация тоже проста, но... сервер не имеет хорошей поддержки. Например, в Tomcat эта форма двоичного файла рассматривается не как файл, а как обычное сообщение.
Анализ механизма обработки Tomcat
Когда Tomcat обрабатывает сообщения в текстовой форме, он сначала читает предыдущую часть заголовка, анализирует Content-Length, чтобы разделить границу сообщения, а оставшаяся часть полезной нагрузки не будет прочитана за один раз, а будет обернута с помощью InputStream во внутренних вызовах. Сокет read для чтения данных RCV_BUF (Когда полный размер пакета больше, чем размер readBuf)
При вызове getParameter/getInputStream для HttpServletRequest и других операциях чтения, связанных с Payload, будет считан Socket RCV_BUF внутри InputStream, и будут считаны данные Payload.
этоВместо того, чтобы читать все данные и временно сохранять их в памяти за один раз, оборачивая InputStream для внутреннего чтения RCV_BUF, фишка в том, что он не хранит данные, а просто делает обертку.Операция чтения прикладного слоя на ServletRequest#inputStream будет перенаправлена на чтение на Socket RCV_BUF.
Однако если прикладной уровень полностью считывает ServletRequest#inputStream, затем преобразует строку и сохраняет ее в памяти, то к Tomcat это не имеет никакого отношения.
Для запросов составного типа в Tomcat предусмотрен специальный механизм обработки. Поскольку multipart предназначен для передачи файлов, Tomcat добавляет концепцию временных файлов при обработке запросов такого типа.При разборе сообщения данные в multipart записываются на диск.
Как показано на рисунке ниже, Tomcat оборачивает каждое поле как DiskFileItem —org.apache.tomcat.util.http.fileupload.disk.DiskFileItem(Этот DiskFileItem не различает файловые и текстовые данные). DiskFileItem далее делится на часть заголовка и часть содержимого. Часть Контента хранится в памяти, а остальное на диске, который разделен sizeThreshold;Однако это значение по умолчанию равно 0., что означает, что весь контент по умолчанию будет сохранен на диск.Поскольку он хранится на диске, его также необходимо считывать с диска при чтении ... Эффективность, естественно, относительно низкая. Поэтому, если это просто текстовое сообщение, не используйте для передачи тип multipart, этот тип будет сброшен на диск.
Есть еще холодное знание, когда Tomcat обрабатывает сообщения составного типа, если поле не является файлом, то он добавит ключ/значение этого поля в карту параметров, то есть эти нефайлы можно получить через запрос. поля файла getParameter/getParameterMap.
//org.apache.catalina.connector.Request#parseParts
if (part.getSubmittedFileName() == null) {
String name = part.getName();
String value = null;
try {
value = part.getString(charset.name());
} catch (UnsupportedEncodingException uee) {
// Not possible
}
......
parameters.addParameter(name, value);
}
Вы должны знать, что этот getParameter может получить только параметры формы (FormParam) и параметры запроса (QueryString), но multipart - это тоже форма, вроде бы в получении параметров нет ничего плохого...
краткое изложение
Tomcat обрабатывает различные типы запросов:
- Если параметры находятся в методе GET queryString (параметры прописаны в url), то все параметры находятся в заголовке и будут считаны в память за один раз
- Если это сообщение типа POST, Tomcat будет читать только часть заголовка, а часть полезной нагрузки не будет активно его читать, а завернет Socket в InputStream для чтения прикладным уровнем.
- Хотя сообщение типа x-www-form-urlencoded не будет активно читаться, многие веб-фреймворки (например, SpringMVC) будут вызывать getParameter и по-прежнему запускать чтение InputStream для чтения RCV_BUF.
- То же самое верно и для бинарной полезной нагрузки, упомянутой выше.Tomcat не инициирует активно операцию чтения.Прикладной уровень должен вызвать ServletRequest#InputStream для выполнения операции чтения для чтения данных RCV_BUF.
- Сообщения составного типа не будут активно читаться, а синтаксический анализ/чтение будет запускаться вызовом HttpServletRequest#getParts; аналогично, многие веб-фреймворки вызывают getParts, поэтому синтаксический анализ будет инициирован.
Зачем сначала писать временный файл, напрямую обертывать InputStream и передавать его на уровень приложения для чтения?
Если прикладной уровень не прочитает RCV_BUF (вовремя), то при заполнении полученных данных RCV_BUF ACK не будет возвращен, а данные клиента также будут храниться в SND_BUF, и данные больше не могут быть отправлены, когда SND_BUF применяется Когда слой заполнен, соединение блокируется.
Следующие причины являются личными мнениями, без поддержки официальных документов.Если у вас есть разные мнения, пожалуйста, оставьте сообщение в области комментариев для обсуждения.
Поскольку multipart обычно используется для передачи файлов, размер файла обычно намного превышает емкость буфера сокета. Поэтому, чтобы не блокировать TCP-соединение, Tomcat прочитает всю полезную часть за один раз, а затем сохранит все части на диск (заголовок находится в памяти, а содержимое — на диске).
Прикладному уровню нужно только прочитать данные Part из DiskFileItem, предоставленного Tomcat.Похоже, что хотя один уровень передается, данные в RCV_BUF могут быть использованы во времени.
С точки зрения эффективности, операция передачи + хранения на диске должна быть намного медленнее, чем отсутствие передачи, но RCV_BUF может потребляться вовремя, чтобы гарантировать, что TCP-соединение не будет заблокировано.
Если он находится под мультиплексированием HTTP2, несколько запросов используют одно и то же TCP-соединение, и если RCV_BUF не используется вовремя, это также приведет к блокировке всех «логических HTTP-соединений».
Так почему же другие типы пакетов не используют временные диски?
Поскольку сообщение маленькое, обычное сообщение с запросом не слишком велико, а обычное сообщение с запросом составляет всего от нескольких тысяч до десятков тысяч, а для обычного текстового сообщения операция чтения также должна быть своевременной и читать все сразу. , а составное сообщение бывает разным, это смешанный способ текст+файл, а может быть и многофайловое.
Например, после получения файла серверу необходимо сделать дамп файла и сбросить его в службу хранения объектов некоторых облачных поставщиков.В настоящее время существует два метода дампа:
- Получите полные данные файла, сохраните их в памяти, а затем вызовите SDK для хранения объектов
- В потоковом режиме при чтении ServletRequest#InputStream запишите в OutputStream SDK.
Метод 1, несмотря на то, что RCV_BUF считывается вовремя, использование памяти слишком велико, и память легко разорвется, что очень неразумно. Метод 2, несмотря на то, что использование памяти очень мало (максимум размер только одного буфера чтения), но поскольку это чтение и запись, и обе стороны подключены к сети, RCV_BUF не может быть использован во времени.
И не только Tomcat, но даже Jetty так обрабатывает multipart.Хотя другие веб-серверы этого не видели, я думаю, что они все должны так обрабатывать.
Ссылаться на
- Apache Tomcat
- Form-based File Upload in HTML - IETF
- «Анализ архитектуры Tomcat» - Лю Гуанжуй
Нелегко быть оригинальным, и несанкционированная перепечатка запрещена. Если моя статья полезна для вас, пожалуйста, поставьте лайк/добавьте в избранное/подпишитесь, чтобы поддержать и поддержать ее ❤❤❤❤❤❤