Java HTTP 服务器发送分块响应
Java HTTP server sending chunked response
我正在开发一个 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();
}
}
}
}
我想实现分块响应,目前上面的代码不支持分块响应。
[编辑] 我尝试通过添加以下方法来实现分块响应:
/**
* @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();
}
}
}
}
我已经阅读了几个关于分块响应的规范,据我所知我正在以正确的格式发送数据,但是我在浏览器中没有收到任何东西。
我可能错误地认为浏览器会正确地将这些块拼凑成一个,但我可能是错的。客户端处理程序如下所示:
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;
已解决,不知道为什么,但删除了 header:
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 位整数最大大小或 64 位来配置您的 Web 服务器以实施此类策略(请参阅 here)。
从技术上讲,您可以像您在 post 上声明的那样使用分块传输进行无限制的响应。理论上,这是用来绕过max Content-Length的。
最常见的是,如果对巨大的 JSON 文件有这样的要求(至少一些 MB),您可以通过顺序 AJAX 请求使用某种分页逻辑。在您的情况下,您可以将 JSON 大数据以编程方式拆分为块,然后通过另一个 AJAX 请求发送每个块。然后,让Javascript执行合并任务。
通常,大小为 MB 的 JSON 响应将在任何浏览器上成功加载。我建议你看看 this 文章;已经3岁了,但我想现在情况更好了。
简而言之,上述基准表明 JSON 小于 35 MB 的大小可能会在任何现代桌面浏览器上成功加载。然而,对于移动浏览器而言,情况可能并非如此。例如,对于 >10MB Json 文件的移动 Safari 有一些 reports 限制。
如果您传输用户 Transfer-Encoding=chunked 那么每个数据块之前必须有块的大小。
这里有一个很好的解释:
https://en.wikipedia.org/wiki/Chunked_transfer_encoding
我正在开发一个 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();
}
}
}
}
我想实现分块响应,目前上面的代码不支持分块响应。
[编辑] 我尝试通过添加以下方法来实现分块响应:
/**
* @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();
}
}
}
}
我已经阅读了几个关于分块响应的规范,据我所知我正在以正确的格式发送数据,但是我在浏览器中没有收到任何东西。
我可能错误地认为浏览器会正确地将这些块拼凑成一个,但我可能是错的。客户端处理程序如下所示:
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;
已解决,不知道为什么,但删除了 header:
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 位整数最大大小或 64 位来配置您的 Web 服务器以实施此类策略(请参阅 here)。
从技术上讲,您可以像您在 post 上声明的那样使用分块传输进行无限制的响应。理论上,这是用来绕过max Content-Length的。
最常见的是,如果对巨大的 JSON 文件有这样的要求(至少一些 MB),您可以通过顺序 AJAX 请求使用某种分页逻辑。在您的情况下,您可以将 JSON 大数据以编程方式拆分为块,然后通过另一个 AJAX 请求发送每个块。然后,让Javascript执行合并任务。
通常,大小为 MB 的 JSON 响应将在任何浏览器上成功加载。我建议你看看 this 文章;已经3岁了,但我想现在情况更好了。
简而言之,上述基准表明 JSON 小于 35 MB 的大小可能会在任何现代桌面浏览器上成功加载。然而,对于移动浏览器而言,情况可能并非如此。例如,对于 >10MB Json 文件的移动 Safari 有一些 reports 限制。
如果您传输用户 Transfer-Encoding=chunked 那么每个数据块之前必须有块的大小。
这里有一个很好的解释: https://en.wikipedia.org/wiki/Chunked_transfer_encoding