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; } }
import java.io.IOException; public interface VoteMsgCoder { byte[] toWire(VoteMsg msg) throws IOException; VoteMsg fromWire(byte[] input) throws IOException; }
基于文本的表示方法
该协议指定使用 US-ASCII 字符集对文本进行编码。消息的开头是一个所谓的"魔术字符串",即一个字符序列,用于接收者快速将投票协议的消息和网络中随机到来的垃圾消息区分开。投票/查询布尔值被编码成字符形式,'v'表示投票消息,'i'表示查询消息。消息的状态,即是否为服务器的响应,由字符'R'指示。状态标记后面是候选人 ID,其后跟的是选票总数,它们都编码成十进制字符串。
VoteMsgTextCoder 类提供了一种基于文本的 VoteMsg 编码方法。
VoteMsgTextCoder.java
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); } }
fromWire()方法首先检查"魔术"字符串,如果在消息最前面没有魔术字符串,则抛出一个异常。这里说明了在实现协议时非常重要的一点:永远不要对从网络来的任何输入进行任何假设。你的程序必须时刻为任何可能的输入做好准备,并能够很好地对其进行处理。在这个例子中,如果接收到的不是期望的消息,fromWire()方法将抛出一个异常,否则,就使用 Scanner 实例,根据空白符一个一个地获取字段。注意,消息的字段数与其是请求消息(由客户端发送)还是响应消息(由服务器发送)有关。如果输入流提前结束或格式错误,fromWire()方法将抛出一个异常。
二进制表示方法
下面我们将展示另一种对投票协议消息进行编码的方法。与基于文本的格式相反,二进制格式使用固定大小的消息。每条消息由一个特殊字节开始,该字节的最高六位为一个"魔术"值 010101。这一点少量的冗余信息为接收者收到适当的投票消息提供了一定程度的保证。该字节的最低两位对两个布尔值进行了编码。消息的第二个字节总是 0,第三、第四个字节包含了 candidateID 值。只有响应消息的最后 8 个字节才包含了选票总数信息。
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; /* Wire Format * 1 1 1 1 1 1 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | Magic |Flags| ZERO | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | Candidate ID | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | | * | Vote Count (only in response) | * | | * | | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ */ 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); } }
发送和接收
通过流发送消息非常简单,只需要创建消息,调用 toWire()方法,添加适当的成帧信息,再写入流。当然,接收消息就要按照相反的顺序执行。这个过程适用于 TCP 协议,而对于 UDP 协议,不需要显式地成帧,因为 UDP 协议中保留了消息的边界信息,
首先我们实现一个投票服务器所用到的服务。当接收到投票消息时,投票服务器将调用 VoteService 类的 handleRequest() 方法对请求进行处理。
import java.util.HashMap; import java.util.Map; public class VoteService { // Map of candidates to number of votes private Map<Integer, Long> results = new HashMap<Integer, Long>(); public VoteMsg handleRequest(VoteMsg msg) { if (msg.isResponse()) { // If response, just send it back return msg; } msg.setResponse(true); // Make message a response // Get candidate ID and vote count int candidate = msg.getCandidateID(); Long count = results.get(candidate); if (count == null) { count = 0L; // Candidate does not exist } if (!msg.isInquiry()) { results.put(candidate, ++count); // If vote, increment count } msg.setVoteCount(count); return msg; } }
import java.io.OutputStream; import java.net.Socket; public class VoteClientTCP { public static final int CANDIDATEID = 888; public static void main(String args[]) throws Exception { if (args.length != 2) { // Test for correct # of args throw new IllegalArgumentException("Parameter(s): <Server> <Port>"); } String destAddr = args[0]; // Destination address int destPort = Integer.parseInt(args[1]); // Destination port Socket sock = new Socket(destAddr, destPort); OutputStream out = sock.getOutputStream(); // Change Bin to Text for a different framing strategy VoteMsgCoder coder = new VoteMsgBinCoder(); // Change Length to Delim for a different encoding strategy Framer framer = new LengthFramer(sock.getInputStream()); // Create an inquiry request (2nd arg = true) VoteMsg msg = new VoteMsg(false, true, CANDIDATEID, 0); byte[] encodedMsg = coder.toWire(msg); // Send request System.out.println("Sending Inquiry (" + encodedMsg.length + " bytes): "); System.out.println(msg); framer.frameMsg(encodedMsg, out); // Now send a vote msg.setInquiry(false); encodedMsg = coder.toWire(msg); System.out.println("Sending Vote (" + encodedMsg.length + " bytes): "); framer.frameMsg(encodedMsg, out); // Receive inquiry response encodedMsg = framer.nextMsg(); msg = coder.fromWire(encodedMsg); System.out.println("Received Response (" + encodedMsg.length + " bytes): "); System.out.println(msg); // Receive vote response msg = coder.fromWire(framer.nextMsg()); System.out.println("Received Response (" + encodedMsg.length + " bytes): "); System.out.println(msg); sock.close(); } }
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class VoteServerTCP { public static void main(String args[]) throws Exception { if (args.length != 1) { // Test for correct # of args throw new IllegalArgumentException("Parameter(s): <Port>"); } int port = Integer.parseInt(args[0]); // Receiving Port ServerSocket servSock = new ServerSocket(port); // Change Bin to Text on both client and server for different encoding VoteMsgCoder coder = new VoteMsgBinCoder(); VoteService service = new VoteService(); while (true) { Socket clntSock = servSock.accept(); System.out.println("Handling client at " + clntSock.getRemoteSocketAddress()); // Change Length to Delim for a different framing strategy Framer framer = new LengthFramer(clntSock.getInputStream()); try { byte[] req; while ((req = framer.nextMsg()) != null) { System.out.println("Received message (" + req.length + " bytes)"); VoteMsg responseMsg = service.handleRequest(coder.fromWire(req)); framer.frameMsg(coder.toWire(responseMsg), clntSock.getOutputStream()); } } catch (IOException ioe) { System.err.println("Error handling client: " + ioe.getMessage()); } finally { System.out.println("Closing connection"); clntSock.close(); } } } }