I am working on a Java application which has a built in HTTP server, at the moment the server is implemented using ServerSocketChannel, it listens on port 1694 for requests:
msvrCh = ServerSocketChannel.open();
msvrCh.socket().bind(new InetSocketAddress(mintPort));
msvrCh.configureBlocking(false);
A thread is installed to manage requests and responses:
Thread thrd = new Thread(msgReceiver);
thrd.setUncaughtExceptionHandler(exceptionHandler);
thrd.start();
The thread is quite simple:
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;
}
}
}
};
The main bulk of the code for dealing with the response is in the function 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();
}
}
}
}
I would like to implemented a chunked response, at present chunked responses are not supported by the code above.
[Edit] I've tried to implement a chunked response by adding the method:
/**
* @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;
}
The modified processRequest now looks like this:
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();
}
}
}
}
I've read several specifications for Chunked responses and as far as I can tell I am sending data in the correct format, however I don't receive anything in the browser.
I may have wrongly assume that the browser would correctly piece together the chunks into one, but I could be wrong. The client side handler looks like this:
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);
}
};
This handler is set-up elsewhere in the object:
mobjHTTP.onreadystatechange = this.responseHandler;
Solved, not sure why, but removing the header:
Transfer-Encoding: chunked
And also the chunk lengths at the beginning of each chunk resolved the issue, I still write the data in 768 byte chunks. This works reliably and very well.
Not sure why I had to do this.
Final method to produce chunks from data string:
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;
}
Loop to write chunks, no lengths at the beginning and no 0 byte at the end of the chunks required:
String[] arystrChunks = arystrChunkData(strResponse);
for( String strChunk : arystrChunks ) {
if ( strChunk != null ) {
out.write(strChunk.getBytes());
}
}