HTTP-сервер Java отправляет фрагментированный ответ
Я работаю над приложением Java, которое имеет встроенный HTTP-сервер, в данный момент сервер реализован с использованием ServerSocketChannel, он прослушивает запросы на порту 1694:
msvrCh = ServerSocketChannel.open();
msvrCh.socket().bind(new InetSocketAddress(mintPort));
msvrCh.configureBlocking(false);
Поток установлен для управления запросами и ответами:
Thread thrd = new Thread(msgReceiver);
thrd.setUncaughtExceptionHandler(exceptionHandler);
thrd.start();
Тема довольно проста:
Runnable msgReceiver = new Runnable() {
@Override
public void run() {
try{
while( !Thread.interrupted() ) {
//Sleep a short period between checks for new requests
try{
Thread.sleep(DELAY_BETWEEN_ACCEPTS);
} catch(Exception ex) {
ex.printStackTrace();
}
SocketChannel cliCh = msvrCh.accept();
if ( blnExit() == true ) {
break;
}
if ( cliCh == null ) {
continue;
}
processRequest(cliCh.socket());
}
} catch (IOException ex) {
ex.printStackTrace();
} finally {
logMsg(TERMINATING_THREAD +
"for accepting cluster connections", true);
if ( msvrCh != null ) {
try {
msvrCh.close();
} catch (IOException ex) {
ex.printStackTrace();
}
msvrCh = null;
}
}
}
};
Основная часть кода для работы с ответом находится в функции processRequest:
private void processRequest(Socket sck) {
try {
//AJAX Parameters
final String AJAX_ID = "ajmid";
//The 'Handler Key' used to decode response
final String HANDLER_KEY = "hkey";
//Message payload
final String PAYLOAD = "payload";
//Post input buffer size
final int REQUEST_BUFFER_SIZE = 4096;
//Double carriage return marks the end of the headers
final String CRLF = "\r\n";
BufferedReader in = new BufferedReader(new InputStreamReader(sck.getInputStream()));
String strAMID = null, strHKey = null, strRequest;
char[] chrBuffer = new char[REQUEST_BUFFER_SIZE];
StringBuffer sbRequest = new StringBuffer();
eMsgTypes eType = eMsgTypes.UNKNOWN;
clsHTTPparameters objParams = null;
int intPos, intCount;
//Extract the entire request, including headers
if ( (intCount = in.read(chrBuffer)) == 0 ) {
throw new Exception("Cannot read request!");
}
sbRequest.append(chrBuffer, 0, intCount);
strRequest = sbRequest.toString();
//What method is being used by this request?
if ( strRequest.startsWith(HTTP_GET) ) {
//The request should end with a HTTP marker, remove this before trying to interpret the data
if ( strRequest.indexOf(HTTP_MARKER) != -1 ) {
strRequest = strRequest.substring(0, strRequest.indexOf(HTTP_MARKER)).trim();
}
//Look for a data marker
if ( (intPos = strRequest.indexOf(HTTP_DATA_START)) >= 0 ) {
//Data is present in the query, skip to the start of the data
strRequest = strRequest.substring(intPos + 1);
} else {
//Remove the method indicator
strRequest = strRequest.substring(HTTP_GET.length());
}
} else if ( strRequest.startsWith(HTTP_POST) ) {
//Discard the headers and jump to the data
if ( (intPos = strRequest.lastIndexOf(CRLF)) >= 0 ) {
strRequest = strRequest.substring(intPos + CRLF.length());
}
}
if ( strRequest.length() > 1 ) {
//Extract the parameters
objParams = new clsHTTPparameters(strRequest);
}
if ( strRequest.startsWith("/") == true ) {
//Look for the document reference
strRequest = strRequest.substring(1);
eType = eMsgTypes.SEND_DOC;
}
if ( objParams != null ) {
//Transfer the payload to the request
String strPayload = objParams.getValue(PAYLOAD);
if ( strPayload != null ) {
byte[] arybytPayload = Base64.decodeBase64(strPayload.getBytes());
strRequest = new String(arybytPayload);
strAMID = objParams.getValue(AJAX_ID);
strHKey = objParams.getValue(HANDLER_KEY);
}
}
if ( eType == eMsgTypes.UNKNOWN
&& strRequest.startsWith("{") && strRequest.endsWith("}") ) {
//The payload is JSON, is there a type parameter?
String strType = strGetJSONItem(strRequest, JSON_LBL_TYPE);
if ( strType != null && strType.length() > 0 ) {
//Decode the type
eType = eMsgTypes.valueOf(strType.toUpperCase().trim());
//What system is the message from?
String strIP = strGetJSONItem(strRequest, JSON_LBL_IP)
,strMAC = strGetJSONItem(strRequest, JSON_LBL_MAC);
if ( strIP != null && strIP.length() > 0
&& strMAC != null && strMAC.length() > 0 ) {
//Is this system known in the cluster?
clsIPmon objSystem = objAddSysToCluster(strIP, strMAC);
if ( objSystem != null ) {
//Update the date/time stamp of the remote system
objSystem.touch();
}
//This is an internal cluster message, no response required
return;
}
}
}
String strContentType = null, strRespPayload = null;
OutputStream out = sck.getOutputStream();
byte[] arybytResponse = null;
boolean blnShutdown = false;
out.write("HTTP/1.0 200\n".getBytes());
switch( eType ) {
case SEND_DOC:
if ( strRequest.length() <= 1 ) {
strRequest = HTML_ROOT + DEFAULT_DOC;
} else {
strRequest = HTML_ROOT + strRequest;
}
logMsg("HTTP Request for: " + strRequest, true);
if ( strRequest.toLowerCase().endsWith(".css") == true ) {
strContentType = MIME_CSS;
} else if ( strRequest.toLowerCase().endsWith(".gif") == true ) {
strContentType = MIME_GIF;
} else if ( strRequest.toLowerCase().endsWith(".jpg") == true ) {
strContentType = MIME_JPG;
} else if ( strRequest.toLowerCase().endsWith(".js") == true ) {
strContentType = MIME_JS;
} else if ( strRequest.toLowerCase().endsWith(".png") == true ) {
strContentType = MIME_PNG;
} else if ( strRequest.toLowerCase().endsWith(".html") == true
|| strRequest.toLowerCase().endsWith(".htm") == true ) {
strContentType = MIME_HTML;
}
File objFile = new File(strRequest);
if ( objFile.exists() == true ) {
FileInputStream objFIS = new FileInputStream(objFile);
if ( objFIS != null ) {
arybytResponse = new byte[(int)objFile.length()];
if ( objFIS.read(arybytResponse) == 0 ) {
arybytResponse = null;
}
objFIS.close();
}
}
break;
case CHANNEL_STS:
strRespPayload = strChannelStatus(strRequest);
strContentType = MIME_JSON;
break;
case CLUSTER_STS:
strRespPayload = strClusterStatus();
strContentType = MIME_JSON;
break;
case MODULE_STS:
strRespPayload = strModuleStatus(strRequest);
strContentType = MIME_JSON;
break;
case NETWORK_INF:
strRespPayload = strNetworkInfo(strRequest);
strContentType = MIME_JSON;
break;
case NODE_STS:
strRespPayload = strNodeStatus(strRequest);
strContentType = MIME_JSON;
break;
case POLL_STS:
strRespPayload = strPollStatus(strRequest);
strContentType = MIME_JSON;
break;
case SYS_STS:
//Issue system status
strRespPayload = strAppStatus();
strContentType = MIME_JSON;
break;
case SHUTDOWN:
//Issue instruction to restart system
strRespPayload = "Shutdown in progress!";
strContentType = MIME_PLAIN;
//Flag that shutdown has been requested
blnShutdown = true;
break;
default:
}
if ( strRespPayload != null ) {
//Convert response string to byte array
arybytResponse = strRespPayload.getBytes();
System.out.println("[ " + strRespPayload.length() + " ]: " + strRespPayload); //HACK
}
if ( arybytResponse != null && arybytResponse.length > 0 ) {
if ( strContentType == MIME_JSON ) {
String strResponse = "{";
if ( strAMID != null ) {
//Include the request AJAX Message ID in the response
if ( strResponse.length() > 1 ) {
strResponse += ",";
}
strResponse += "\"" + AJAX_ID + "\":" + strAMID;
}
if ( strHKey != null ) {
if ( strResponse.length() > 1 ) {
strResponse += ",";
}
strResponse += "\"" + HANDLER_KEY + "\":\"" + strHKey + "\"";
}
if ( strResponse.length() > 1 ) {
strResponse += ",";
}
strResponse += "\"payload\":" + new String(arybytResponse)
+ "}";
arybytResponse = strResponse.getBytes();
}
String strHeaders = "";
if ( strContentType != null ) {
strHeaders += "Content-type: " + strContentType + "\n";
}
strHeaders += "Content-length: " + arybytResponse.length + "\n"
+ "Access-Control-Allow-Origin: *\n"
+ "Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT\n"
+ "Access-Control-Allow-Credentials: true\n"
+ "Keep-Alive: timeout=2, max=100\n"
+ "Cache-Control: no-cache\n"
+ "Pragma: no-cache\n\n";
out.write(strHeaders.getBytes());
out.write(arybytResponse);
out.flush();
}
out.close();
sck.close();
if ( blnShutdown == true ) {
String strSystem = mobjLocalIP.strGetIP();
if ( strSystem.compareTo(mobjLocalIP.strGetIP()) != 0 ) {
//Specified system is not the local system, issue message to remote system.
broadcastMessage("{\"" + JSON_LBL_TYPE + "\":\"" +
eMsgTypes.SHUTDOWN + "\""
+ ",\"" + JSON_LBL_TIME + "\":\"" +
clsTimeMan.lngTimeNow() + "\"}");
} else {
//Shutdown addressed to local system
if ( getOS().indexOf("linux") >= 0 ) {
//TO DO!!!
} else if ( getOS().indexOf("win") >= 0 ) {
Runtime runtime = Runtime.getRuntime();
runtime.exec("shutdown /r /c \"Shutdown request\" /t 0 /f");
System.exit(EXITCODE_REQUESTED_SHUTDOWN);
}
}
}
} catch (Exception ex) {
} finally {
if (sck != null) {
try {
sck.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
Я хотел бы реализовать чанкованный ответ, в настоящее время чанкованные ответы не поддерживаются приведенным выше кодом.
[Edit] Я попытался реализовать фрагментарный ответ, добавив метод:
/**
* @param strData - The data to split into chunks
* @return A string array containing the chunks
*/
public static String[] arystrChunkData(String strData) {
int intChunks = (strData.length() / CHUNK_THRESHOLD_BYTESIZE) + 1;
String[] arystrChunks = new String[intChunks];
int intLength = strData.length(), intPos = 0;
for( int c=0; c<arystrChunks.length; c++ ) {
if ( intPos < intLength ) {
//Extract a chunk from the data
int intEnd = Math.min(intLength - 1, intPos + CHUNK_THRESHOLD_BYTESIZE);
arystrChunks[c] = strData.substring(intPos, intEnd);
}
//Advance data position to next chunk
intPos += CHUNK_THRESHOLD_BYTESIZE;
}
return arystrChunks;
}
Модифицированный processRequest теперь выглядит так:
private void processRequest(Socket sck) {
try {
//AJAX Parameters
final String AJAX_ID = "ajmid";
//The 'Handler Key' used to decode response
final String HANDLER_KEY = "hkey";
//Message payload
final String PAYLOAD = "payload";
//Post input buffer size
final int REQUEST_BUFFER_SIZE = 4096;
//Double carriage return marks the end of the headers
final String CRLF = "\r\n";
BufferedReader in = new BufferedReader(new InputStreamReader(sck.getInputStream()));
String strAMID = null, strHKey = null, strRequest;
char[] chrBuffer = new char[REQUEST_BUFFER_SIZE];
StringBuffer sbRequest = new StringBuffer();
eMsgTypes eType = eMsgTypes.UNKNOWN;
clsHTTPparameters objParams = null;
int intPos, intCount;
//Extract the entire request, including headers
if ( (intCount = in.read(chrBuffer)) == 0 ) {
throw new Exception("Cannot read request!");
}
sbRequest.append(chrBuffer, 0, intCount);
strRequest = sbRequest.toString();
//What method is being used by this request?
if ( strRequest.startsWith(HTTP_GET) ) {
//The request should end with a HTTP marker, remove this before trying to interpret the data
if ( strRequest.indexOf(HTTP_MARKER) != -1 ) {
strRequest = strRequest.substring(0, strRequest.indexOf(HTTP_MARKER)).trim();
}
//Look for a data marker
if ( (intPos = strRequest.indexOf(HTTP_DATA_START)) >= 0 ) {
//Data is present in the query, skip to the start of the data
strRequest = strRequest.substring(intPos + 1);
} else {
//Remove the method indicator
strRequest = strRequest.substring(HTTP_GET.length());
}
} else if ( strRequest.startsWith(HTTP_POST) ) {
//Discard the headers and jump to the data
if ( (intPos = strRequest.lastIndexOf(CRLF)) >= 0 ) {
strRequest = strRequest.substring(intPos + CRLF.length());
}
}
if ( strRequest.length() > 1 ) {
//Extract the parameters
objParams = new clsHTTPparameters(strRequest);
}
if ( strRequest.startsWith("/") == true ) {
//Look for the document reference
strRequest = strRequest.substring(1);
eType = eMsgTypes.SEND_DOC;
}
if ( objParams != null ) {
//Transfer the payload to the request
String strPayload = objParams.getValue(PAYLOAD);
if ( strPayload != null ) {
byte[] arybytPayload = Base64.decodeBase64(strPayload.getBytes());
strRequest = new String(arybytPayload);
strAMID = objParams.getValue(AJAX_ID);
strHKey = objParams.getValue(HANDLER_KEY);
}
}
if ( eType == eMsgTypes.UNKNOWN
&& strRequest.startsWith("{") && strRequest.endsWith("}") ) {
//The payload is JSON, is there a type parameter?
String strType = strGetJSONItem(strRequest, JSON_LBL_TYPE);
if ( strType != null && strType.length() > 0 ) {
//Decode the type
eType = eMsgTypes.valueOf(strType.toUpperCase().trim());
//What system is the message from?
String strIP = strGetJSONItem(strRequest, JSON_LBL_IP)
,strMAC = strGetJSONItem(strRequest, JSON_LBL_MAC);
if ( strIP != null && strIP.length() > 0
&& strMAC != null && strMAC.length() > 0 ) {
//Is this system known in the cluster?
clsIPmon objSystem = objAddSysToCluster(strIP, strMAC);
if ( objSystem != null ) {
//Update the date/time stamp of the remote system
objSystem.touch();
}
//This is an internal cluster message, no response required
return;
}
}
}
String strContentType = null, strRespPayload = null;
OutputStream out = sck.getOutputStream();
byte[] arybytResponse = null;
boolean blnShutdown = false;
//Start the writing the headers
String strHeaders = "HTTP/1.0 200\n"
+ "Date: " + (new Date()).toString() + "\n"
+ "Access-Control-Allow-Origin: *\n"
+ "Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT\n"
+ "Access-Control-Allow-Credentials: true\n"
+ "Keep-Alive: timeout=2, max=100\n"
+ "Cache-Control: no-cache\n"
+ "Pragma: no-cache\n";
out.write(strHeaders.getBytes());
strHeaders = "";
switch( eType ) {
case SEND_DOC:
if ( strRequest.length() <= 1 ) {
strRequest = HTML_ROOT + DEFAULT_DOC;
} else {
strRequest = HTML_ROOT + strRequest;
}
logMsg("HTTP Request for: " + strRequest, true);
if ( strRequest.toLowerCase().endsWith(".css") == true ) {
strContentType = MIME_CSS;
} else if ( strRequest.toLowerCase().endsWith(".gif") == true ) {
strContentType = MIME_GIF;
} else if ( strRequest.toLowerCase().endsWith(".jpg") == true ) {
strContentType = MIME_JPG;
} else if ( strRequest.toLowerCase().endsWith(".js") == true ) {
strContentType = MIME_JS;
} else if ( strRequest.toLowerCase().endsWith(".png") == true ) {
strContentType = MIME_PNG;
} else if ( strRequest.toLowerCase().endsWith(".html") == true
|| strRequest.toLowerCase().endsWith(".htm") == true ) {
strContentType = MIME_HTML;
}
File objFile = new File(strRequest);
if ( objFile.exists() == true ) {
FileInputStream objFIS = new FileInputStream(objFile);
if ( objFIS != null ) {
arybytResponse = new byte[(int)objFile.length()];
if ( objFIS.read(arybytResponse) == 0 ) {
arybytResponse = null;
}
objFIS.close();
}
}
break;
case CHANNEL_STS:
strRespPayload = strChannelStatus(strRequest);
strContentType = MIME_JSON;
break;
case CLUSTER_STS:
strRespPayload = strClusterStatus();
strContentType = MIME_JSON;
break;
case MODULE_STS:
strRespPayload = strModuleStatus(strRequest);
strContentType = MIME_JSON;
break;
case NETWORK_INF:
strRespPayload = strNetworkInfo(strRequest);
strContentType = MIME_JSON;
break;
case NODE_STS:
strRespPayload = strNodeStatus(strRequest);
strContentType = MIME_JSON;
break;
case POLL_STS:
strRespPayload = strPollStatus(strRequest);
strContentType = MIME_JSON;
break;
case SYS_STS:
//Issue system status
strRespPayload = strAppStatus();
strContentType = MIME_JSON;
break;
case SHUTDOWN:
//Issue instruction to restart system
strRespPayload = "Shutdown in progress!";
strContentType = MIME_PLAIN;
//Flag that shutdown has been requested
blnShutdown = true;
break;
default:
}
if ( strRespPayload != null ) {
//Convert response string to byte array
arybytResponse = strRespPayload.getBytes();
}
if ( arybytResponse != null && arybytResponse.length > 0 ) {
boolean blnChunked = false;
if ( strContentType != null ) {
strHeaders += "Content-type: " + strContentType + "\n";
}
if ( strContentType == MIME_JSON ) {
String strResponse = "{";
if ( strAMID != null ) {
//Include the request AJAX Message ID in the response
if ( strResponse.length() > 1 ) {
strResponse += ",";
}
strResponse += "\"" + AJAX_ID + "\":" + strAMID;
}
if ( strHKey != null ) {
if ( strResponse.length() > 1 ) {
strResponse += ",";
}
strResponse += "\"" + HANDLER_KEY + "\":\"" + strHKey + "\"";
}
if ( strResponse.length() > 1 ) {
strResponse += ",";
}
strResponse += "\"payload\":" + new String(arybytResponse)
+ "}";
//How big is the response?
if ( strResponse.length() > CHUNK_THRESHOLD_BYTESIZE ) {
blnChunked = true;
strHeaders += "Transfer-Encoding: chunked\n\n";
out.write(strHeaders.getBytes());
//Slice up the string into chunks
String[] arystrChunks = arystrChunkData(strResponse);
for( int c=0; c<arystrChunks.length; c++ ) {
String strChunk = arystrChunks[c];
if ( strChunk != null ) {
String strLength = Integer.toHexString(strChunk.length()) + "\r\n";
strChunk += "\r\n";
out.write(strLength.getBytes());
out.write(strChunk.getBytes());
}
}
//Last chunk is always 0 bytes
out.write("0\r\n\r\n".getBytes());
} else {
arybytResponse = strResponse.getBytes();
}
}
if ( blnChunked == false ) {
strHeaders += "Content-length: " + arybytResponse.length + "\n\n";
out.write(strHeaders.getBytes());
out.write(arybytResponse);
}
out.flush();
}
out.close();
sck.close();
if ( blnShutdown == true ) {
String strSystem = mobjLocalIP.strGetIP();
if ( strSystem.compareTo(mobjLocalIP.strGetIP()) != 0 ) {
//Specified system is not the local system, issue message to remote system.
broadcastMessage("{\"" + JSON_LBL_TYPE + "\":\"" +
eMsgTypes.SHUTDOWN + "\""
+ ",\"" + JSON_LBL_TIME + "\":\"" +
clsTimeMan.lngTimeNow() + "\"}");
} else {
//Shutdown addressed to local system
if ( getOS().indexOf("linux") >= 0 ) {
//TO DO!!!
} else if ( getOS().indexOf("win") >= 0 ) {
Runtime runtime = Runtime.getRuntime();
runtime.exec("shutdown /r /c \"Shutdown request\" /t 0 /f");
System.exit(EXITCODE_REQUESTED_SHUTDOWN);
}
}
}
} catch (Exception ex) {
} finally {
if (sck != null) {
try {
sck.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
Я прочитал несколько спецификаций для ответов Chunked и, насколько я могу судить, я отправляю данные в правильном формате, однако я ничего не получаю в браузере.
Возможно, я ошибочно предположил, что браузер правильно собрал бы куски в один, но я могу ошибаться. Обработчик на стороне клиента выглядит так:
this.responseHandler = function() {
try {
if ( mobjHTTP == null
|| !(mobjHTTP.readyState == 4 && mobjHTTP.status == 200)
|| !(mstrResponseText = mobjHTTP.responseText)
|| mstrResponseText.length == 0 ) {
//Not ready or no response to decode
return;
}
//Do something with the response
} catch( ex ) {
T.error("responseHandler:", ex);
}
};
Этот обработчик устанавливается в другом месте объекта:
mobjHTTP.onreadystatechange = this.responseHandler;
3 ответа
Решил, не уверен почему, но удалил заголовок:
Transfer-Encoding: chunked
А также длины чанков в начале каждого чанка решили проблему, я все еще записываю данные в 768 байт. Это работает надежно и очень хорошо.
Не уверен, почему я должен был это сделать.
Последний метод для получения кусков из строки данных:
public static String[] arystrChunkData(String strData) {
int intChunks = (strData.length() / CHUNK_THRESHOLD_BYTESIZE) + 1;
String[] arystrChunks = new String[intChunks];
int intLength = strData.length(), intPos = 0;
for( int c=0; c<arystrChunks.length; c++ ) {
if ( intPos < intLength ) {
//Extract a chunk from the data
int intEnd = Math.min(intLength, intPos + CHUNK_THRESHOLD_BYTESIZE);
arystrChunks[c] = strData.substring(intPos, intEnd);
intPos = intEnd;
}
}
return arystrChunks;
}
Цикл для записи чанков, без длин в начале и без 0 байтов в конце чанков:
String[] arystrChunks = arystrChunkData(strResponse);
for( String strChunk : arystrChunks ) {
if ( strChunk != null ) {
out.write(strChunk.getBytes());
}
}
Как я уже отмечал, нет официального ограничения на размер ответа HTTP. TCP делает эту работу за вас. Однако вы всегда можете настроить свой веб-сервер для реализации такой политики, установив Content-Length:: 32-bit Integer max size или 64-bit для современных браузеров (см. Здесь).
Технически, вы можете иметь неограниченное количество ответов, используя Chunked Transfer, как вы указали в своем сообщении. Теоретически это используется для обхода максимальной длины содержимого.
Чаще всего, и если есть такое требование для огромного файла JSON (размером не менее нескольких МБ), вы можете использовать какую-то логику разбиения на страницы через последовательные запросы AJAX. В вашем случае вы могли бы разделить ваши большие данные JSON на куски программным способом и отправить каждую из них с помощью другого запроса AJAX. Затем позвольте Javascript выполнить задачу объединения.
Как правило, JSON-ответ размером в несколько МБ успешно загружается в любом браузере. Я предлагаю вам взглянуть на эту статью; ему 3 года, но я думаю, что сейчас все еще лучше.
Короче говоря, в приведенном выше тесте указано, что JSON размером менее 35 МБ, вероятно, будет успешно загружен в любом современном настольном браузере. Это, однако, может не иметь место для мобильных браузеров. Например, есть некоторые отчеты об ограничениях мобильного сафари для файлов Json размером более 10 МБ.
Если вы передаете пользователя Transfer-Encoding=chunked, то каждому фрагменту данных должен предшествовать размер фрагмента.
См. здесь хорошее объяснение:https://en.wikipedia.org/wiki/Chunked_transfer_encoding