Сегодня утром я написал собственный сервер на Java. В конце концов, я хочу использовать его в качестве бэкэнда для нативного приложения, поэтому я работал над реализацией загрузки файлов. Я тестировал это с помощью простой HTML-формы, которая отправляет свои данные на мой локальный компьютер. Когда я анализирую заголовки HTTP-запроса, я извлекаю Content-Length из раздела данных запроса (далее именуемого «телом сообщения» запроса). Иногда тело сообщения HTTP-запроса содержит имя файла и содержимое, но чаще всего оно пустое, даже если Content-Length и Content-Type (включая границу формы) установлены правильно (ненулевая длина, "--WebKitBoundary..." граница). Я могу обнаружить это и тайм-аут (и нет, увеличение моего тайм-аута не позволяет мне читать больше данных), но тот факт, что HTTP-запрос, похоже, указывает на то, что должны быть данные, когда они не получены, кажется серьезной проблемой.
Сообщение здесь кажется именно тем, что я вижу, но на момент публикации на него не было ответа.
Это класс, который я использую для чтения данных из InputStream установленного соединения Socket:
public class HTTPRequest {
// full request, HTTP verb + URI, meta-data, message body
public final String request, requestline, headers, data;
// regex to find the length of the message body
private static final Pattern contentlength = Pattern.compile("Content-Length\\s*:\\s*(\\d+)");
/*
* when an object is created it reads the entire HTTP request from the stream and sets its constant strings accordingly
*/
public HTTPRequest(InputStream is) throws Exception {
/*
* get everything except the message body
*/
@SuppressWarnings("resource") // closing the Scanner closes the stream, so suppressing the resource leak warning
Scanner s = new Scanner(is);
requestline = s.nextLine();
s.useDelimiter("\r\n\r\n");
headers = s.next();
// get the reported length of the message body, 0 if not present
Matcher m = contentlength.matcher(headers);
int length = 0;
if (m.find()) {
length = Integer.parseInt(m.group(1));
}
// if there is a message body, read it
if (length > 0) {
// this will contain the message bytes
byte[] b = new byte[length];
// number of bytes read, number of consecutive times I read 0 bytes
int read = 0;
int numzeros = 0;
// read until I've read the entire message
while(read < length) {
// read however many bytes are available
int numread = is.read(b, read, is.available());
read += numread;
if (numread == 0) {
numzeros++;
}else {
numzeros = 0;
}
// timeout after not getting any data for 1 second
if (numzeros > 100) {
break;
}
Thread.sleep(10);
}
data = new String(b, 0, read);
// otherwise, no message body
}else {
data = "";
}
// combine all of the parts of the request
request = requestline + "\r\n" + headers + "\r\n\r\n" + data;
}
}
и вот HTML, который я использовал для загрузки файла:
<head>
</head>
<body>
<form action = "http://localhost:54600/api/test/test/uploadFile" method = "post" enctype = "multipart/form-data">
<input name = "name" type = "text" />
<input name = "upload" type = "file" />
<input type = "submit" />
</form>
</body>
Это то, что я прочитал из InputStream:
POST /api/test/test/uploadFile HTTP/1.1
Host: localhost:54600
Connection: keep-alive
Content-Length: 531
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryzkLQnlCjBb2a5sOP
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
что, помимо отсутствия тела сообщения, кажется правильным. Ранее сегодня я обнаружил, что обычно получаю тело сообщения, если загружаю уникальный файл каждый раз, когда отправляю форму (отправка одного и того же файла более одного раза, как правило, не имеет тела сообщения, хотя иногда оно было). Теперь я вообще не могу получить тело сообщения.
Обновление, написав последний абзац, решил еще немного протестировать в другой вкладке. На этот раз я получил тело сообщения:
POST /api/test/test/uploadFile HTTP/1.1
Host: localhost:54600
Connection: keep-alive
Content-Length: 1162
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryErwo4zpzcDBuyDo5
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
------WebKitFormBoundaryErwo4zpzcDBuyDo5
Content-Disposition: form-data; name = "name"
test_name
------WebKitFormBoundaryErwo4zpzcDBuyDo5
Content-Disposition: form-data; name = "upload"; filename = "hello_world.o"
Content-Type: application/octet-stream
ELF [... binary data]
------WebKitFormBoundaryErwo4zpzcDBuyDo5--
Я протестировал еще кое-что на этой новой вкладке, и большую часть времени я получал данные формы, но уже видел, что он не отправляет тело сообщения два или три раза. Кажется, не существует заметного шаблона, когда отправка формы не включает тело сообщения.
У кого-нибудь есть мысли о том, что может происходить?
Спасибо!




В вашем коде две проблемы.
Первый из них, скорее всего, вызовет вашу проблему:
Сканер здесь не лучший выбор, так как он не перестанет читать ваш InputStream на "\r\n\r\n". Сканер хорошо работает только тогда, когда он единственный, кто читает ваш InputStream, а не тогда, когда вы хотите прочитать его напрямую. Сканер попытается сначала заполнить свой внутренний буфер, а затем искать в нем \r\n\r\n. Таким образом, он всегда будет читать дальше этого. И эти байты больше не будут доступны в InputStream во второй части вашей функции.
Таким образом, вы не можете использовать Scanner — вам нужно читать прямо из InputStream, пока вы не увидите \r\n\r\n; только тогда вы будете уверены, что правильно прочитали запрос, еще не прочитав тело запроса.
Вторая проблема заключается в том, что использование InputStream.available() — ненадежный способ чтения данных. Вы очень сильно зависите от сетевого времени, и оно может немного отличаться. Если данные поступают быстро, вы ждете слишком долго, так как выполняете Thread.sleep для каждого чтения. И если он не приходит достаточно быстро, вы можете слишком быстро истечь тайм-аут.
Гораздо более надежный способ чтения:
while(read < length) {
int numread = is.read(b, read, length - read);
if (numread < 0)
break;
read += numread;
}
затем для входящего объекта Socket вы устанавливаете тайм-аут чтения с помощью Socket.setSoTimeout (ссылка) на разумный тайм-аут для ваших целей. Я бы взял хотя бы 30 секунд или минуту - вы не знаете, какая сеть между клиентом и вашим сервером.
В качестве игрушечного проекта, конечно, интересно написать собственный HTTP-сервер. Однако я бы не советовал это для производственного кода. В настоящее время протокол HTTP довольно сложен, и существует множество хороших HTTP-серверов с открытым исходным кодом, которые вы можете использовать в своем приложении напрямую (Tomcat, Jetty, Undertow и другие позволяют вам это делать) — вам не нужно развертывать WAR файл или что-то в этом роде. Вы могу используете API сервлета, но если вы хотите использовать, например, асинхронный HTTP с высокой степенью масштабируемости, вы можете взглянуть на Netty.