在傳輸消息時(shí),用 Java 內(nèi)置的方法和工具確實(shí)很用,如:對(duì)象序列化,RMI 遠(yuǎn)程調(diào)用等。但有時(shí)候,針對(duì)要傳輸?shù)奶囟愋偷臄?shù)據(jù),實(shí)現(xiàn)自己的方法可能更簡(jiǎn)單、容易或有效。下面給出一個(gè)實(shí)現(xiàn)了自定義構(gòu)建和解析協(xié)議消息的 Demo。
該例子是一個(gè)簡(jiǎn)單的投票協(xié)議。這里,一個(gè)客戶端向服務(wù)器發(fā)送一個(gè)請(qǐng)求消息,消息中包含了一個(gè)候選人的 ID,范圍在 0~1000。程序支持兩種請(qǐng)求:一種是查詢請(qǐng)求,即向服務(wù)器詢問(wèn)候選人當(dāng)前獲得的投票總數(shù),服務(wù)器發(fā)回一個(gè)響應(yīng)消息,包含了原來(lái)的候選人 ID 和該候選人當(dāng)前獲得的選票總數(shù);另一種是投票請(qǐng)求,即向指定候選人投一票,服務(wù)器對(duì)這種請(qǐng)求也發(fā)回響應(yīng)消息,包含了候選人 ID 和獲得的選票數(shù)(包含了剛剛投的一票)。
在實(shí)現(xiàn)一個(gè)協(xié)議時(shí),一般會(huì)定義一個(gè)專門的類來(lái)存放消息中所包含的的信息。在我們的例子中,客戶端和服務(wù)端發(fā)送的消息都很簡(jiǎn)單,唯一的區(qū)別是服務(wù)端發(fā)送的消息還包含了選票總數(shù)和一個(gè)表示相應(yīng)消息的標(biāo)志。因此,可以用一個(gè)類來(lái)表示客戶端和服務(wù)端的兩種消息。下面的 VoteMsg.java 類展示了每條消息中的基本信息:
另外,注意一下幾點(diǎn):
VoteMsg 代碼如下:
public class VoteMsg {
private boolean isInquiry; // true if inquiry; false if vote
private boolean isResponse;// true if response from server
private int candidateID; // in [0,1000]
private long voteCount; // nonzero only in response
public static final int MAX_CANDIDATE_ID = 1000;
public VoteMsg(boolean isResponse, boolean isInquiry, int candidateID, long voteCount)
throws IllegalArgumentException {
// check invariants
if (voteCount != 0 && !isResponse) {
throw new IllegalArgumentException("Request vote count must be zero");
}
if (candidateID < 0 || candidateID > MAX_CANDIDATE_ID) {
throw new IllegalArgumentException("Bad Candidate ID: " + candidateID);
}
if (voteCount < 0) {
throw new IllegalArgumentException("Total must be >= zero");
}
this.candidateID = candidateID;
this.isResponse = isResponse;
this.isInquiry = isInquiry;
this.voteCount = voteCount;
}
public void setInquiry(boolean isInquiry) {
this.isInquiry = isInquiry;
}
public void setResponse(boolean isResponse) {
this.isResponse = isResponse;
}
public boolean isInquiry() {
return isInquiry;
}
public boolean isResponse() {
return isResponse;
}
public void setCandidateID(int candidateID) throws IllegalArgumentException {
if (candidateID < 0 || candidateID > MAX_CANDIDATE_ID) {
throw new IllegalArgumentException("Bad Candidate ID: " + candidateID);
}
this.candidateID = candidateID;
}
public int getCandidateID() {
return candidateID;
}
public void setVoteCount(long count) {
if ((count != 0 && !isResponse) || count < 0) {
throw new IllegalArgumentException("Bad vote count");
}
voteCount = count;
}
public long getVoteCount() {
return voteCount;
}
public String toString() {
String res = (isInquiry ? "inquiry" : "vote") + " for candidate " + candidateID;
if (isResponse) {
res = "response to " + res + " who now has " + voteCount + " vote(s)";
}
return res;
}
}
接下來(lái),我們要根據(jù)一定的協(xié)議來(lái)對(duì)其進(jìn)行編解碼,我們定義一個(gè) VoteMsgCoder 接口,它提供了對(duì)投票消息進(jìn)行序列化和反序列化的方法。toWrie()方法用于根據(jù)一個(gè)特定的協(xié)議,將投票消息轉(zhuǎn)換成一個(gè)字節(jié)序列,fromWire()方法則根據(jù)相同的協(xié)議,對(duì)給定的字節(jié)序列進(jìn)行解析,并根據(jù)信息的內(nèi)容返回一個(gè)該消息類的實(shí)例。
import java.io.IOException;
public interface VoteMsgCoder {
byte[] toWire(VoteMsg msg) throws IOException;
VoteMsg fromWire(byte[] input) throws IOException;
}
面給出兩個(gè)實(shí)現(xiàn)了 VoteMsgCoder 接口的類,一個(gè)實(shí)現(xiàn)的是基于文本的編碼方式 ,一個(gè)實(shí)現(xiàn)的是基于二進(jìn)制的編碼方式。
首先是用文本方式對(duì)消息進(jìn)行編碼的程序。該協(xié)議指定使用 ASCII 字符集對(duì)文本進(jìn)行編碼。消息的開頭是一個(gè)所謂的”魔術(shù)字符串“,即一個(gè)字符序列,用于快速將投票協(xié)議的消息和網(wǎng)絡(luò)中隨機(jī)到來(lái)的垃圾消息區(qū)分開,投票/查詢布爾值被編碼為字符形似,‘v’代表投票消息,‘i’代表查詢消息。是否為服務(wù)器發(fā)送的響應(yīng)消息,由字符‘R’指示,狀態(tài)標(biāo)記后面是候選人 ID,其后跟的是選票總數(shù),它們都編碼成十進(jìn)制字符串。
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
public class VoteMsgTextCoder implements VoteMsgCoder {
/*
* Wire Format "VOTEPROTO" <"v" | "i"> [<RESPFLAG>] <CANDIDATE> [<VOTECNT>]
* Charset is fixed by the wire format.
*/
// Manifest constants for encoding
public static final String MAGIC = "Voting";
public static final String VOTESTR = "v";
public static final String INQSTR = "i";
public static final String RESPONSESTR = "R";
public static final String CHARSETNAME = "US-ASCII";
public static final String DELIMSTR = " ";
public static final int MAX_WIRE_LENGTH = 2000;
public byte[] toWire(VoteMsg msg) throws IOException {
String msgString = MAGIC + DELIMSTR + (msg.isInquiry() ? INQSTR : VOTESTR)
+ DELIMSTR + (msg.isResponse() ? RESPONSESTR + DELIMSTR : "")
+ Integer.toString(msg.getCandidateID()) + DELIMSTR
+ Long.toString(msg.getVoteCount());
byte data[] = msgString.getBytes(CHARSETNAME);
return data;
}
public VoteMsg fromWire(byte[] message) throws IOException {
ByteArrayInputStream msgStream = new ByteArrayInputStream(message);
Scanner s = new Scanner(new InputStreamReader(msgStream, CHARSETNAME));
boolean isInquiry;
boolean isResponse;
int candidateID;
long voteCount;
String token;
try {
token = s.next();
if (!token.equals(MAGIC)) {
throw new IOException("Bad magic string: " + token);
}
token = s.next();
if (token.equals(VOTESTR)) {
isInquiry = false;
} else if (!token.equals(INQSTR)) {
throw new IOException("Bad vote/inq indicator: " + token);
} else {
isInquiry = true;
}
token = s.next();
if (token.equals(RESPONSESTR)) {
isResponse = true;
token = s.next();
} else {
isResponse = false;
}
// Current token is candidateID
// Note: isResponse now valid
candidateID = Integer.parseInt(token);
if (isResponse) {
token = s.next();
voteCount = Long.parseLong(token);
} else {
voteCount = 0;
}
} catch (IOException ioe) {
throw new IOException("Parse error...");
}
return new VoteMsg(isResponse, isInquiry, candidateID, voteCount);
}
}
toWire()方法簡(jiǎn)單地創(chuàng)建一個(gè)字符串,該字符串中包含了消息的所有字段,并由空白符隔開。fromWire()方法首先檢查”魔術(shù)字符串“,如果在消息最前面沒(méi)有魔術(shù)字符串,則拋出一個(gè)異常。在理說(shuō)明了在實(shí)現(xiàn)協(xié)議時(shí)非常重要的一點(diǎn):永遠(yuǎn)不要對(duì)從網(wǎng)絡(luò)中來(lái)的任何輸入進(jìn)行任何假設(shè)。你的程序必須時(shí)刻為任何可能的輸入做好準(zhǔn)備,并能很好的對(duì)其進(jìn)行處理。
下面將展示基于二進(jìn)制格式對(duì)消息進(jìn)行編碼的程序。與基于文本的格式相反,二進(jìn)制格式使用固定大小的消息,每條消息由一個(gè)特殊字節(jié)開始,該字節(jié)的最高六位為一個(gè)”魔術(shù)值“010101,該字節(jié)的最低兩位對(duì)兩個(gè)布爾值進(jìn)行了編碼,消息的第二個(gè)字節(jié)總是 0,第三、四個(gè)字節(jié)包含了 candidateID 值,只有響應(yīng)消息的最后 8 個(gè)字節(jié)才包含了選票總數(shù)信息。字節(jié)序列格式如下圖所示:
http://wiki.jikexueyuan.com/project/java-socket/images/userdefined.png" alt="" />
代碼如下:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class VoteMsgBinCoder implements VoteMsgCoder {
// manifest constants for encoding
public static final int MIN_WIRE_LENGTH = 4;
public static final int MAX_WIRE_LENGTH = 16;
public static final int MAGIC = 0x5400;
public static final int MAGIC_MASK = 0xfc00;
public static final int MAGIC_SHIFT = 8;
public static final int RESPONSE_FLAG = 0x0200;
public static final int INQUIRE_FLAG = 0x0100;
public byte[] toWire(VoteMsg msg) throws IOException {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(byteStream); // converts ints
short magicAndFlags = MAGIC;
if (msg.isInquiry()) {
magicAndFlags |= INQUIRE_FLAG;
}
if (msg.isResponse()) {
magicAndFlags |= RESPONSE_FLAG;
}
out.writeShort(magicAndFlags);
// We know the candidate ID will fit in a short: it's > 0 && < 1000
out.writeShort((short) msg.getCandidateID());
if (msg.isResponse()) {
out.writeLong(msg.getVoteCount());
}
out.flush();
byte[] data = byteStream.toByteArray();
return data;
}
public VoteMsg fromWire(byte[] input) throws IOException {
// sanity checks
if (input.length < MIN_WIRE_LENGTH) {
throw new IOException("Runt message");
}
ByteArrayInputStream bs = new ByteArrayInputStream(input);
DataInputStream in = new DataInputStream(bs);
int magic = in.readShort();
if ((magic & MAGIC_MASK) != MAGIC) {
throw new IOException("Bad Magic #: " +
((magic & MAGIC_MASK) >> MAGIC_SHIFT));
}
boolean resp = ((magic & RESPONSE_FLAG) != 0);
boolean inq = ((magic & INQUIRE_FLAG) != 0);
int candidateID = in.readShort();
if (candidateID < 0 || candidateID > 1000) {
throw new IOException("Bad candidate ID: " + candidateID);
}
long count = 0;
if (resp) {
count = in.readLong();
if (count < 0) {
throw new IOException("Bad vote count: " + count);
}
}
// Ignore any extra bytes
return new VoteMsg(resp, inq, candidateID, count);
}
}