3.5 構建和解析訊息協議
下面看一個簡單的例子。
程式支援兩種請求。一種是查詢(inquiry),即向伺服器詢問給定候選人當前獲得的投票總數。伺服器發回一個響應訊息,包含了原來的候選人ID和該候選人當前(查詢請求收到時)獲得的選票總數。另一種是投票(voting)請求,即向指定候選人投一票。伺服器對這種請求也發迴響應訊息,包含了候選人ID和其獲得的選票數(包括了剛投的一票)。
下面是投票資訊的實體類,包含四個屬性是否查詢訊息、是否返回訊息、候選人編號和投票總數。
package com.suifeng.tcpip.chapter3.vote;/** * 投票訊息實體 * * @author Suifeng * */public class VoteMsg{private boolean isInquery; // 是否查詢訊息private boolean isResponse; // 是否返回的訊息private int candidate; // 候選人編號private long voteCount; // 投票總數private static final int MAX_CANDIDATE_ID = 1000;public VoteMsg(boolean isInquery, boolean isResponse, int candidate,long voteCount) {if ((!isResponse) && voteCount > 0){throw new IllegalArgumentException("Request vote count must be zero");}if (candidate < 0 || candidate > MAX_CANDIDATE_ID){throw new IllegalArgumentException("Bad candidate ID:" + candidate);}if (voteCount < 0){throw new IllegalArgumentException("Total count must be greanter than zero");}this.isInquery = isInquery;this.isResponse = isResponse;this.candidate = candidate;this.voteCount = voteCount;}public boolean isInquery(){return isInquery;}public void setInquery(boolean isInquery){this.isInquery = isInquery;}public boolean isResponse(){return isResponse;}public void setResponse(boolean isResponse){this.isResponse = isResponse;}public int getCandidate(){return candidate;}public void setCandidate(int candidate){if (candidate < 0 || candidate > MAX_CANDIDATE_ID){throw new IllegalArgumentException("Bad candidate ID:" + candidate);}this.candidate = candidate;}public long getVoteCount(){return voteCount;}public void setVoteCount(long voteCount){if (((!isResponse) && voteCount > 0) || voteCount < 0){throw new IllegalArgumentException("Bad vote count");}this.voteCount = voteCount;}@Overridepublic String toString(){String res = "";res = (isInquery ? "inquery" : "vote") + " for cadidate " + candidate;if (isResponse){res = "Response to " + res + " who now has " + voteCount+ " vote(s)";}return res;}}
有了投票的訊息,還需要一定的協議對其進行編碼和解碼。VoteMsgCoder提供了對投票資訊進行編碼和解碼的介面。
package com.suifeng.tcpip.chapter3.vote;import java.io.IOException;/** * 訊息與二進位的轉換介面 * @author Suifeng * */public interface VoteMsgCoder{/** * 將投票資訊轉換成二進位流(根據特定的協議,將訊息轉換成位元組序列) * @param msg投票資訊 * @return * @throws IOException */byte[] toWire(VoteMsg msg) throws IOException;/** * 將二進位流轉換成訊息(根據相同的協議,對位元組序列進行解析,根據訊息的內容構造出一個訊息類的執行個體) * @param input * @throws IOException */VoteMsg fromWire(byte[] input) throws IOException;}
3.5.1 基於文本的表示方法
首先介紹一個用文本方式對訊息進行編碼的版本。該協議指定使用utf-8字元集對文本進行編碼。訊息的開頭是一個所謂的"魔術字串",即一個字元序列,用於接收者快速將投票協議的訊息和網路中隨機到來的垃圾訊息區分開。投票/查詢布爾值被編碼成字元形式,'v'表示投票訊息,'i'表示查詢訊息。訊息的狀態,即是否為伺服器的響應,由字元'R'指示。狀態標記後面是候選人ID,其後跟的是選票總數,它們都編碼成十進位字串。VoteMsgTextCoder類提供了一種基於文本的VoteMsg編碼方法。
package com.suifeng.tcpip.chapter3.vote;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.InputStreamReader;import java.util.Scanner;/** * 使用文本編碼、解碼投票資訊 */public class VoteMsgTextCoder implements VoteMsgCoder{/** * 魔術字串,用於排除冗餘資訊 */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 = "utf-8";/** * 分隔字元 */public static final String DELIMSTR = " ";/** * 編碼最大位元組數 */public static final int MAX_WIRE_LENGTH = 200;@Overridepublic VoteMsg fromWire(byte[] input) throws IOException{ByteArrayInputStream in = new ByteArrayInputStream(input);Scanner s = new Scanner(new InputStreamReader(in,CHARSETNAME));String token = "";boolean isInquery = false;boolean isResponse = false;int candidate = 0 ;long voteCount = 0;token = s.next();// 解析是否是有效魔術字串,防止冗餘資訊if(!MAGIC.equals(token)){throw new IOException("Bad magic String:"+token);}// 記錄查詢標記token = s.next();if(INQSTR.equals(token)){isInquery = true;}else if(VOTESTR.equals(token)){isInquery = false;}else{throw new IOException("Bad vote/inq indicator:"+token);}// 返回標記token = s.next();if(RESPONSESTR.equals(token)){isResponse = true;// 去下一個欄位--候選人編號token = s.next();}else{// 非返回資訊,該欄位是候選人編號isResponse = false;}candidate = Integer.parseInt(token);if(isResponse){token = s.next();// 讀取投票總數voteCount = Long.parseLong(token);}else{voteCount = 0;}return new VoteMsg(isInquery,isResponse,candidate,voteCount);}@Overridepublic byte[] toWire(VoteMsg msg) throws IOException{StringBuilder voteBuilder = new StringBuilder(256);// 魔術字串voteBuilder.append(MAGIC).append(DELIMSTR);// 查詢/投票標記voteBuilder.append((msg.isInquery())? INQSTR: VOTESTR).append(DELIMSTR);if(msg.isResponse()){// 返回標記voteBuilder.append(RESPONSESTR).append(DELIMSTR);}// 候選人編號voteBuilder.append(msg.getCandidate()).append(DELIMSTR);// 投票總數voteBuilder.append(msg.getVoteCount()).append(DELIMSTR);return voteBuilder.toString().getBytes(CHARSETNAME);}}
3.5.2 基於二進位的表示方法
下面將展示另一種對投票協議訊息進行編碼的方法。與基於文本的格式相反,二進位格式使用固定大小的訊息。每條訊息由一個特殊位元組開始,該位元組的最高六位為一個"魔術"值010101。這一點少量的冗餘資訊為接收者收到適當的投票訊息提供了一定程度的保證。該位元組的最低兩位對兩個布爾值進行了編碼。訊息的第二個位元組總是0,第三、第四個位元組包含了candidateID值。只有響應訊息的最後8個位元組才包含了選票總數資訊。
package com.suifeng.tcpip.chapter3.vote;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.DataInputStream;import java.io.DataOutputStream;import java.io.IOException;/** * 使用二進位編碼、解碼投票資訊 * ================================================= * 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | Magic |Flags| Zero | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | Candidate | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | | * | Vote Count(Only inresponse) | * | | * | | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * */public class VoteMsgBinaryCoder implements VoteMsgCoder{/** * 最小位元組數 */public static final int MIN_WIRE_LENGTH = 4;/** * 最大位元組數(包含4個位元組投票總數) */public static final int MAX_WIRE_LENGTH = 8;/** * 魔術數字 010101 00 0000 0000 */public static final int MAGIC = 0x5400;/** * 魔術數字掩碼值 111111 00 0000 0000 */public static final int MAGIC_MASK = 0xfc00;/** * 擷取魔術數位位移位元組數 */public static final int MAGIC_SHIFT = 8;/** * 返回標記掩碼值 0000 0010 0000 0000 */public static final int RESPONSE_FLAG = 0x0200;/** * 查詢標記掩碼值 0000 0001 0000 0000 */public static final int INQUERY_FLAG = 0x0100;@Overridepublic VoteMsg fromWire(byte[] input) throws IOException{System.out.println("input.length="+input.length);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 isResponse = (magic & RESPONSE_FLAG) != 0;// 擷取查詢標記boolean isInquery = (magic & INQUERY_FLAG) != 0;// 擷取候選人編號(2個位元組)int candidate = in.readShort();if(candidate < 0|| candidate > 1000){throw new IOException("Bad candidate ID:"+candidate);}long count = 0;if(isResponse){// 擷取投票總數(4個位元組)count = in.readLong();if(count < 0){throw new IOException("Bad vote count:"+count);}}return new VoteMsg(isInquery,isResponse,candidate,count);}@Overridepublic byte[] toWire(VoteMsg msg) throws IOException{ByteArrayOutputStream byteStream = new ByteArrayOutputStream();DataOutputStream out = new DataOutputStream(byteStream);short magicAndFlags = MAGIC;// 查詢標記if (msg.isInquery()){magicAndFlags |= INQUERY_FLAG;}// 返回標記if (msg.isResponse()){magicAndFlags |= RESPONSE_FLAG;} // 記錄前兩個位元組(6位魔術數字+1位返回標記+1位查詢標記+8位0)out.writeShort(magicAndFlags);// 記錄候選人標記(兩位元組,16位)out.writeShort((short) msg.getCandidate());if (msg.isResponse()){// 總個數(8位元組,64位)out.writeLong(msg.getVoteCount());}out.flush();byte[] msgBytes = byteStream.toByteArray();System.out.println("encode msg byte length="+msgBytes.length);return byteStream.toByteArray();}}