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;
}
@Override
public 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;
@Override
public 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);
}
@Override
public 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; @Override public 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); } @Override public 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(); } }