标签(空格分隔): 未分类
在传输消息时,用Java内置的方法和工具确实很用,如:对象序列化,RMI远程调用等。但有时候,针对要传输的特定类型数据,实现自己的方法可能更简单、容易或有效。
本章结束时我们再看一个简单的例子,对在实现别人定义的协议时可能用到的技术进行了介绍。这个例子程序是一个简单的”投票”协议,这里,一个客户端向服务器发送了一个请求消息,消息中包含了一个候选人 ID,范围是 0 至 1000。Vote Reques:投票请求, Candidate:候选人, Vote Count:选票总数程序支持两种请求。一种是查询( inquiry),即向服务器询问给定候选人当前获得的投票总数。服务器发回一个响应消息,包含了原来的候选人 ID 和该候选人当前(查询请求收到时)获得的选票总数。另一种是投票( voting)请求,即向指定候选人投一票。服务器对这种请求也发回响应消息,包含了候选人 ID 和其获得的选票数(包括了刚投的一票)。
在实现一个协议时,定义一个专门的类来存放消息中所包含的信息是大有裨益的。该类提供了操作消息中的字段的方法–同时用来维护不同字段之间的不变量。在我们的例子中,客户端和服务器端发送的消息都非常简单,它们唯一的区别是服务器端发送的消息包含了选票总数和一个表示响应消息(不是请求消息)的标志。因此,我们可以用一个类来表示客户端和服务器端的两种消息。 VoteMsg.java 类展示了每条消息中的基本信息:
- 布尔值 isInquiry,其值为true时表示该消息是查询请求(为false时表示该消息是投票信息);
- 布尔值 isResponse,指示该消息是响应(由服务器发送)还是请求;
- 整型变量 candidateID 指示了候选人的 ID;
- 长整型变量 voteCount 指示出所查询的候选人获得的总选票数。
- 这个类还维护了以下字段间的不变量:
candidateID 的范围是 0 到 1000。
voteCount 在响应消息中只能是一个非零值( isResponse 为 true)。
voteCount 不能为负数
public class VoteMsg {
private boolean isInquiry;
private boolean isResponse;
private int candidateID;
private long voteCount;
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;
}
}
接下来,我们要根据一定的协议来对其进行编解码,我们定义一个 VoteMsgCoder 接口,它提供了对投票消息进行序列化和反序列化的方法。toWrie()方法用于根据一个特定的协议,将投票消息转换成一个字节序列,fromWire()方法则根据相同的协议,对给定的字节序列进行解析,并根据信息的内容返回一个该消息类的实例。
public interface VoteMsgCoder {
byte[] toWire(VoteMsg msg) throws IOException;
VoteMsg fromWire(byte[] input) throws IOException;
}
下面给出两个实现了 VoteMsgCoder 接口的类,一个实现的是基于文本的编码方式 ,一个实现的是基于二进制的编码方式。
首先是用文本方式对消息进行编码的程序。该协议指定使用 ASCII 字符集对文本进行编码。
消息的开头是一个所谓的”魔术字符串“,即一个字符序列,用于快速将投票协议的消息和网络中随机到来的垃圾消息区分开,投票/查询布尔值被编码为字符形似,‘v’代表投票消息,‘i’代表查询消息。是否为服务器发送的响应消息,由字符‘R’指示,状态标记后面是候选人 ID,其后跟的是选票总数,它们都编码成十进制字符串。
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;
@Override
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;
}
@Override
public VoteMsg fromWire(byte[] input) throws IOException {
ByteArrayInputStream msgStream = new ByteArrayInputStream(input);
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()方法简单地创建一个字符串,该字符串中包含了消息的所有字段,并由空白符隔开。 fromWire()方法首先检查”魔术”字符串,如果在消息最前面没有魔术字符串,则抛出一个异常。这里说明了在实现协议时非常重要的一点:永远不要对从网络来的任何输入进行任何假设。你的程序必须时刻为任何可能的输入做好准备,并能够很好地对其进行处理。在这个例子中,如果接收到的不是期望的消息, fromWire()方法将抛出一个异常,否则,就使用 Scanner 实例, 根据空白符一个一个地获取字段。注意,消息的字段数与其是请求消息(由客户端发送)还是响应消息(由服务器发送)有关。如果输入流提前结束或格式错误,fromWire()方法将抛出一个异常。
- 二进制表示方法
下面将展示基于二进制格式对消息进行编码的程序。与基于文本的格式相反,二进制格式使用固定大小的消息,每条消息由一个特殊字节开始,该字节的最高六位为一个”魔术值“010101,该字节的最低两位对两个布尔值进行了编码,消息的第二个字节总是 0,第三、四个字节包含了 candidateID 值,只有响应消息的最后 8 个字节才包含了选票总数信息
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;
@Override
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;
}
@Override
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);
//此方法适用于读取用接口 DataOutput 的 writeShort 方法写入的字节。
//返回: 读取的 16 位值。
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);
}
}