3.3 成幀與解析
成幀(framing)技術解決了接收端如何定位訊息的首尾位置的問題。
主要有兩種技術能夠尋找到訊息的結束位置。
·基於界定符(Delimiter-based):訊息的結束由唯一的標記指出,即寄件者在傳輸完資料 後顯示添加一個特殊的位元組序列,這個標記不在傳輸的資料中出現。
·顯示長度:在變長欄位或者訊息前加一個固定大小的欄位,用來指示該欄位或者訊息 中包含多少個位元組。
基於界定符的一個特殊情況是,可以用在TCP串連上傳輸的最後一個訊息上:在發送完這個訊息後,寄件者就簡單的關閉(shutdownOutput()或者close()方法)發送端的TCP串連,接收者在讀取完這個訊息的最後一個位元組後將接收到一個流結束標記(即read方法返回的-1),該標記指示已經到達訊息的末尾。
基於界定符的方法通常用在文本方式編碼的訊息中:定義一個特殊的字元或者字串來標識訊息的結束。接收者只需要簡單的掃描輸入的資訊,來尋找定界序列,並將定界符前邊的字串返回。這個方法的缺點是訊息的本身不能含有定界符。
基於長度的方法要知道訊息長度的上限,寄件者要首先確定訊息的長度,將長度資訊存入一個整數,作為訊息的首碼。如果訊息的長度不大於255個位元組,則需要一個位元組,如果訊息的長度小於65535個位元組,則需要兩個位元組。
下面是一個Framer的介面。它有兩個方法,frameMsg()方法用來添加成幀資訊並將制定訊息輸出到指定的流,nextMsg()則將掃描指定的流,從中抽取一條訊息。
package com.suifeng.tcpip.chapter3.framer;import java.io.IOException;import java.io.OutputStream;public interface Framer{/** * 發送前組裝資料,添加成幀資訊並將制定訊息輸出到指定的流 * @param message * @param out * @throws IOException */void frameMsg(byte[] message, OutputStream out) throws IOException;/** * 解析資料,掃描指定的流,從中抽取一條訊息 * @return * @throws IOException */byte[] nextMsg() throws IOException;}
下面是使用定界符的對Framer介面的實現
package com.suifeng.tcpip.chapter3.framer;import java.io.ByteArrayOutputStream;import java.io.EOFException;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;/** * 使用邊界符處理和解析資料 * @author Suifeng * */public class DelimFramer implements Framer{private InputStream in;private static final int DELIMTER = '\n';public DelimFramer(InputStream in){this.in = in;}@Overridepublic void frameMsg(byte[] message, OutputStream out) throws IOException{for(byte b : message){// 檢查訊息是否包含界定符,如果包含,則要拋出一個異常if(DELIMTER == b){throw new IOException("Message contains delimiter");}}// 寫訊息out.write(message);// 將成幀資訊輸出到流中out.write(DELIMTER);out.flush();}@Overridepublic byte[] nextMsg() throws IOException{ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();int nextByte;// 讀取流中的位元組,知道遇到定界符while((nextByte = in.read()) != DELIMTER){// 到達流末尾if(nextByte == -1){if(messageBuffer.size() == 0){// 訊息已經接收完return null;}else{// 訊息沒有定界符,拋出異常throw new EOFException("Non-empty message without delimiter");}}// 將無定界符的訊息寫入訊息緩衝區messageBuffer.write(nextByte);}// 轉換成位元組數組返回return messageBuffer.toByteArray();}}
下面是基於長度的成幀方法,適用於小於65535自己的訊息。
寄件者先給出訊息的長度,並將長度資訊以big-endian順序寫入兩個位元組的整數中,再將這兩個位元組放到完整的訊息的內容前,連同訊息一起寫入輸出資料流。
下面是具體的代碼實現
package com.suifeng.tcpip.chapter3.framer;import java.io.DataInputStream;import java.io.EOFException;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;public class LengthFramer implements Framer{public static final int MAX_MESSAGE_LENGTH = 65535;public static final int BYTE_MASK = 0xff;public static final int SHORT_MASK = 0xff;public static final int BYTE_SHIFT = 8;private DataInputStream in;public LengthFramer(InputStream in){this.in = new DataInputStream(in);}@Overridepublic void frameMsg(byte[] message, OutputStream out) throws IOException{if(message.length > MAX_MESSAGE_LENGTH){throw new IOException("Message too long");}// 高8位out.write((message.length >> BYTE_SHIFT)&BYTE_MASK);// 低8位out.write(message.length & BYTE_MASK);out.write(message);out.flush();}@Overridepublic byte[] nextMsg() throws IOException{int length = 0;try{length = in.readUnsignedShort();// 兩個位元組}catch(EOFException e){return null;}byte[] msg = new byte[length];in.readFully(msg);// 阻塞等待,直到收到足夠的位元組return msg;}}
3.4 Java特定編碼
當使用通訊端的時候,如果知道(i)通訊雙方都是用Java實現,(ii)擁有對協議的完全控制權,那麼可以使用Java的內建工具如Serializable或者遠程方法調用工具(Remote Method Invocation,RMI)。RMI可以調用不同虛擬機器的方法,並隱藏所有繁瑣的編碼解碼細節。序列化處理了實際的java對象轉換成位元組序列的工作,因此可以在不同的虛擬機器傳遞Java對象。