There are a number of optimizations you can make to the low-level communications infrastructure. These optimizations can be difficult to implement, and it is usually easier to buy these types of optimizations than to build them.
Where the distributed application is transferring large amounts of data over a network, the communications layer can be optimized to support compression of the data transfers. In order to minimize compression overhead for small data transfers, the compression mechanism should have a filter size below which compression is not used for data packets.
The JDK documentation includes an extended example of installing a compression layer in the RMI communications layer (the main documentation index page leads to RMI documentation under the "Enterprise Features" heading). The following code illustrates a simple example of adding compression into a communications layer. The bold type shows the extra code required:
void writeTransfer(byte[ ] transferbuffer, int offset, int len) { if (len <= 0) return; int newlen = compress(transferbuffer, offset, len); communicationSocket.write(len); communicationSocket.write(newlen); communicationSocket.write(transferbuffer, offset, newlen); communicationSocket.flush( ); } byte[ ] readTransfer( ) throws IOException { int len = communicationSocket.read( ); if (len <= 0) throw new IOException("blah blah"); int newlen = communicationSocket.read( ); if (newlen <= 0) throw new IOException("blah blah"); int readlen = 0; byte[ ] transferbuffer = new byte[len]; int n; while(readlen < newlen) { //n = communicationSocket.read(transferbuffer, readlen, len-readlen); n = communicationSocket.read(transferbuffer, readlen, newlen-readlen); if (n >= 0) readlen += n; else throw new IOException("blah blah again"); } int decompresslen = decompress(transferbuffer, 0, newlen); if (decompresslen != len) throw new IOException("blah blah decompression"); return transferbuffer; }
Caching at the low-level communications layer is unusual and often a fallback position where the use of the communications layer is spread too widely within the application to retrofit low-level caching in the application itself. But caching is generally one of the best techniques for speeding up client/server applications and should be used whenever possible, so you could consider low-level caching when caching cannot be added directly to the application. Caching at the low-level communications layer cannot be achieved generically. The following code illustrates an example of adding the simplest low-level caching in the communications layer. The bold type shows the extra code required:
void writeTransfer(byte[ ] transferbuffer, int offset, int len) { if (len <= 0) return; //check if we can cache this code CacheObject cacheObj = isCachable(transferbuffer, offset, len); if (cacheObj != null) { //Assume this is simple non-interleaved writes, so we can simply //set this cache obj as the cache to be read. The isCachable( ) //method must have filled in the cache, so it may include a //remote transfer if this is the first time we cached this object. LastCache = cacheObj; return; } else { cacheObj = null; realWriteTransfer(transferbuffer, offset, len); } } void realWriteTransfer(byte[ ] transferbuffer, int offset, int len) { communicationSocket.write(len); communicationSocket.write(transferbuffer, offset, len); communicationSocket.flush( ); } byte[ ] readTransfer( ) throws IOException { if (LastCache != null) { byte[ ] transferbuffer = LastCache.transferBuffer( ); LastCache = null; return transferbuffer; } int len = communicationSocket.read( ); if (len <= 0) throw new IOException("blah blah"); int readlen = 0; byte[ ] transferbuffer = new byte[len]; int n; while(readlen < newlen) { n = communicationSocket.read(transferbuffer, readlen, len-readlen); if (n >= 0) readlen += n; else throw new IOException("blah blah again"); } return transferbuffer; }
Batching can be useful when your performance analysis indicates there are too many network transfers occurring. The standard batching technique uses two cutoff values: a timeout and a data limit. The technique is to catch and hold all data transfers at the batching level (just above the real communication-transfer level) and send all data transfers together in one transfer. The batched transfer is triggered either when the timeout is reached or when the data limit (which is normally the batch buffer size) is exceeded. Most message-queuing systems support this type of batching. The following code illustrates a simple example of adding batching to the communications layer. The bold type shows the extra code required:
//method synchronized since there will be another thread //which sends the batched transfer if the timeout is reached void synchronized writeTransfer(byte[ ] transferbuffer, int offset, int len) { if (len <= 0) return; if (len >= batch.length - 4 - batchSize) { //batch is too full to take this chunk, so send off the last lot realWriteTransfer(batchbuffer, 0, batchSize); batchSize = 0; lastSend = System.currentTimeMillis( ); } addIntToBatch(len); System.arraycopy(transferbuffer, offset, batchBuffer, batchSize, len); batchSize += len; } void realWriteTransfer(byte[ ] transferbuffer, int offset, int len) { communicationSocket.write(len); communicationSocket.write(transferbuffer, offset, len); communicationSocket.flush( ); } //batch timeout thread method void run( ) { int elapsedTime; for(;;) { synchronized(this) { elapsedTime = System.currentTimeMillis( ) - lastSend; if ((elapsedTime >= timeout) && (batchSize > 0)) { realWriteTransfer(batchbuffer, 0, batchSize); batchSize = 0; lastSend = System.currentTimeMillis( ); } } try{Thread.sleep(timeout - elapsedTime);}catch(InterruptedException e){ } } } realReadTransfer( ) throws IOException { //Don't socket read until the buffer has been completely used if (readBatchBufferlen - readBatchBufferOffset > 0) return; //otherwise read in the next batched communication readBatchBufferOffset = 0; int readBatchBufferlen = communicationSocket.read( ); if (readBatchBufferlen <= 0) throw new IOException("blah blah"); int readlen = 0; byte[ ] readBatchBuffer = new byte[readBatchBufferlen]; int n; while(readlen < readBatchBufferlen) { n = communicationSocket.read(readBatchBuffer, readlen, readBatchBufferlen-readlen); if (n >= 0) readlen += n; else throw new IOException("blah blah again"); } } byte[ ] readTransfer( ) throws IOException { realReadTransfer( ); int len = readIntFromBatch( ); if (len <= 0) throw new IOException("blah blah"); byte[ ] transferbuffer = new byte[len]; System.arraycopy(readBatchBuffer, readBatchBufferOffset, transferBuffer, 0, len); readBatchBufferOffset += len; return transferbuffer; }
Multiplexing is a technique where you combine multiple pseudo-connections into one real connection, intertwining the actual data transfers so that they use the same communications pipe. This reduces the cost of having many communications pipes (which can incur a heavy system load) and is especially useful when you would otherwise be opening and closing connections a lot: repeatedly opening connections can cause long delays in responses. Multiplexing can be managed in a similar way to the transfer-batching example in the previous section.