【Java TCP/IP Socket】應用程式協議中訊息的成幀與解析(含程式碼)
程式間達成的某種包含了資訊交換的形式和意義的共識稱為協議,用來實現特定應用程式的協議叫做應用程式協議。大部分應用程式協議是根據由欄位序列組成的離散資訊定義的,其中每個欄位中都包含了一段以位序列編碼(即二進位制位元組編碼,也可以使用基於文字編碼的方式,但常用協議如:TCP、UDP、HTTP等在傳輸資料時,都是以位序列編碼的)的特定資訊。應用程式協議中明確定義了資訊的傳送者應該如何排列和解釋這些位序列,同時還要定義接收者應該如何解析,這樣才能使資訊的接收者能夠抽取出每個欄位的意義。TCP/IP協議唯一的約束:資訊必須在塊中傳送和接收,而塊的長度必須是8位的倍數,因此,我們可以認為TCP/IP協議中傳輸的資訊是位元組序列。
由於協議通常處理的是由一組欄位組成的離散的資訊,因此應用程式協議必須指定訊息的接收者如何確定何時訊息已被完整接收。成幀技術就是解決接收端如何定位訊息首尾位置問題的,由於協議通常處理的是由一組欄位組成的離散的資訊,因此應用程式協議必須指定訊息的接收者如何確定何時訊息已被完整。主要有兩種技術使接收者能夠準確地找到訊息的結束位置:
1、基於定界符:訊息的結束由一個唯一的標記指出,即傳送者在傳輸完資料後顯式新增的一個特定位元組序列,這個特殊標記不能在傳輸的資料中出現(這也不是絕對的,應用填充技術能夠對訊息中出現的定界符進行修改,從而使接收者不將其識別為定界符)。該方法通常用在以文字方式編碼的訊息中。
2、顯式長度:在變長欄位或訊息前附加一個固定大小的欄位,用來指示該欄位或訊息中包含了多少位元組。該方法主要用在以二進位制位元組方式編碼的訊息中。
由於UDP套接字保留了訊息的邊界資訊,因此不需要進行成幀處理(實際上,主要是DatagramPacket負載的資料有一個確定的長度,接收者能夠準確地知道訊息的結束位置),而TCP協議中沒有訊息邊界的概念,因此,在使用TCP套接字時,成幀就是一個非常重要的考慮因素(在TCP連線中,接收者讀取完最後一條訊息的最後一個位元組後,將受到一個流結束標記,即read()返回-1,該標記指示出已經讀取到了訊息的末尾,非嚴格意義上來講,這也算是基於定界符方法的一種特殊情況
下面給出一個自定義實現上面兩種成幀技術的Demo(書上的例子),先定義一個Framer介面,它由兩個方法:frameMag()方法用來新增成幀資訊並將指定訊息輸出到指定流,nextMsg()方法則掃描指定的流,從中抽取出下一條訊息。
import java.io.IOException;
import java.io.OutputStream;
public interface Framer {
void frameMsg(byte[] message, OutputStream out) throws IOException;
byte[] nextMsg() throws IOException;
}
下面的程式碼實現了基於定界符的成幀方法,定界符為換行符“\n”,frameMsg()方法並沒有實現填充,當成幀的位元組序列中包含有定界符時,它只是簡單地丟擲異常;nextMsg()方法掃描劉,直到讀取到了定界符,並返回定界符前面所有的字元,如果流為空則返回null,如果直到流結束也沒找到定界符,程式將丟擲一個異常來指示成幀錯誤。
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class DelimFramer implements Framer {
private InputStream in; // 資料來源
private static final byte DELIMITER = '\n'; // 定界符
public DelimFramer(InputStream in) {
this.in = in;
}
public void frameMsg(byte[] message, OutputStream out) throws IOException {
for (byte b : message) {
if (b == DELIMITER) {
//如果在訊息中檢查到界定符,則丟擲異常
throw new IOException("Message contains delimiter");
}
}
out.write(message);
out.write(DELIMITER);
out.flush();
}
public byte[] nextMsg() throws IOException {
ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();
int nextByte;
while ((nextByte = in.read()) != DELIMITER) {
//如果流已經結束還沒有讀取到定界符
if (nextByte == -1) {
//如果讀取到的流為空,則返回null
if (messageBuffer.size() == 0) {
return null;
} else {
//如果讀取到的流不為空,則丟擲異常
throw new EOFException("Non-empty message without delimiter");
}
}
messageBuffer.write(nextByte);
}
return messageBuffer.toByteArray();
}
}
下面的程式碼實現了基於長度的成幀方法,適用於長度小於65535個位元組的訊息。傳送者首先給出指定訊息的長度,並將長度資訊以big-endian順序(從左邊開始,由高位到低位傳送)存入2個位元組的整數中,再將這兩個位元組存放在完整的訊息內容前,連同訊息一起寫入輸出流;在接收端,使用DataInputStream讀取整型的長度資訊,readFully()方法將阻塞等待,直到給定的陣列完全填滿。使用這種成幀方法,傳送者不需要檢查要成幀的訊息內容,而只需要檢查訊息的長度是否超出了限制。
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 MAXMESSAGELENGTH = 65535;
public static final int BYTEMASK = 0xff;
public static final int SHORTMASK = 0xffff;
public static final int BYTESHIFT = 8;
private DataInputStream in;
public LengthFramer(InputStream in) throws IOException {
this.in = new DataInputStream(in); //資料來源
}
//對位元組流message新增成幀資訊,並輸出到指定流
public void frameMsg(byte[] message, OutputStream out) throws IOException {
//訊息的長度不能超過65535
if (message.length > MAXMESSAGELENGTH) {
throw new IOException("message too long");
}
out.write((message.length >> BYTESHIFT) & BYTEMASK);
out.write(message.length & BYTEMASK);
out.write(message);
out.flush();
}
public byte[] nextMsg() throws IOException {
int length;
try {
//該方法讀取2個位元組,將它們作為big-endian整數進行解釋,並以int型整數返回它們的值
length = in.readUnsignedShort();
} catch (EOFException e) { // no (or 1 byte) message
return null;
}
// 0 <= length <= 65535
byte[] msg = new byte[length];
//該方法處阻塞等待,直到接收到足夠的位元組來填滿指定的陣列
in.readFully(msg);
return msg;
}
}