--------笔记来自于书籍《Java TCP/IP Socket编程》。
使用套接字时,通常要么是需要同时创建通信信道两端的程序,要么实现一个给定的协议进行通信。如果知道通信双方都使用java实现,且拥有对协议的完全控制权,那么就可以使用Java的内置工具如Serialiable接口或者远程方法调用(RMI)工具,但是由于某些原因导致,不是最好的解决方法。首先,它们是比较笼统的工具,在通信开销上不能做到最高效。其次自己的类要实现序列化接口,这个很容易出错。所以实现自己的方法,可能更简单、容量已或者更有效。
案例里面的程序主要是实现了“投票”协议,客户端向服务器端发送请求,消息中包含一个候选人ID,范围是0~1000,程序支持了两种请求。
1)查询请求,即向服务器询问给定候选人当前获得的投票总数。服务器发回一个响应消息,包含了原来的候选人的ID和该候选人的获取的选票总数。
2)投票请求,即向指定候选人投一票,服务器对这种请求也发回响应消息,包含了候选人ID和其获取的选票数。
实现协议时,专门一个类用来存放消息中的所包含的信息,提供操作消息中的字段的方法,同时维护不同字段之间的不变量。而且客户端和服务器端的消息比较简单,直接可以使用一个类包含客户端和服务器端的消息。
1.首先定义一个消息类VoteMsg.java,此类包含了客户端和服务器端的消息。对于字段的基本介绍如下:
此类维护一下字段间的不变量。
public class VoteMsg {
private boolean isInquiry;
private boolean isResponse;
private int candidateID;
private long voteCount;
private static final int MAX_CANDIDATE_ID = 1000;
public VoteMsg(boolean isResponse, boolean isInquiry, int candidateID, long voteCount) {
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 vote count must >=0");
}
this.isInquiry = isInquiry;
this.isResponse = isResponse;
this.candidateID = candidateID;
this.voteCount = voteCount;
}
public boolean isInquiry() {
return isInquiry;
}
public void setInquiry(boolean isInquiry) {
this.isInquiry = isInquiry;
}
public boolean isResponse() {
return isResponse;
}
public void setResponse(boolean isResponse) {
this.isResponse = isResponse;
}
public int getCandidateID() {
return candidateID;
}
public void setCandidateID(int candidateID) {
if (candidateID < 0 || candidateID > MAX_CANDIDATE_ID) {
throw new IllegalArgumentException("Bad Candidate ID:" + candidateID);
}
this.candidateID = candidateID;
}
public long getVoteCount() {
return voteCount;
}
public void setVoteCount(long voteCount) {
if ((voteCount != 0 && !isResponse) || voteCount < 0) {
throw new IllegalArgumentException("Bad vote count");
}
this.voteCount = 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定义了两个方法,toWire()方法根据特定的协议将投票信息转换成在字节序列。fromWire()方法是根据协议将字节序列进行解析。
public interface VoteMsgCoder {
//消息的序列化
byte[] toWire(VoteMsg msg) throws IOException;
//消息的反序列化
VoteMsg fromWire(byte[] input) throws IOException;
}
2.基于文本方式对消息编码
基于文本方式对投票信息进行编码,协议使用了ASCII字符集对文本进行编码。消息的开头是“魔术字符串”,即字节序列,用于接收者快速将投票协议的消息和网络中随机到来的垃圾消息区分开。投票/查询布尔值被编码程字符形式,‘v’表示的是投票消息,而‘i’表示的是查询消息。消息的状态,即是否为服务器的响应,由字符‘R’指示,状态标记后面是候选人ID,其后面跟的是选票总数,它们都能编码程十进制字符串。
由于发送的消息是空格隔开的,Scanner类根据空白符一个一个获取所有的字段。
public class VoteMsgTextCoder implements VoteMsgCoder {
/**
* Wire format "VOTEPROTO" <"v"|"i">[][][] Charset is fixed by wire format
*/
public static final String MAGIC = "Voting";
public static final String VOTESTR = "v";
public static final String INQSTR = "i";
public static final String RESPONSETER = "R";
public static final String CHARSETNAME = "US-ASCII";
public static final String DELIMETER = " ";
public static final int MAX_WIRE_LENGTH = 2000;
@Override
public byte[] toWire(VoteMsg msg) throws IOException {
String msgString = MAGIC + DELIMETER + (msg.isInquiry() ? INQSTR : VOTESTR) + DELIMETER
+ (msg.isResponse() ? RESPONSETER + DELIMETER : "") + Integer.toString(msg.getCandidateID()) + DELIMETER
+ Long.toString(msg.getVoteCount());
byte data[] = msgString.getBytes(CHARSETNAME);
return data;
}
@Override
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(RESPONSETER)) {
isResponse = true;
token = s.next();
} else {
isResponse = false;
}
candidateID = Integer.parseInt(token);
if (isResponse) {
token = s.next();
voteCount = Long.parseLong(token);
} else {
voteCount = 0;
}
} catch (IOException e) {
throw new IOException("Parse error");
}
return new VoteMsg(isResponse, isInquiry, candidateID, voteCount);
}
}
3.基于二进制方式对消息进行编码
二进制格式使用固定大小的消息,每条消息都由一个特殊字节开始,该字节的最高六位为一个“魔术值”010101,可以筛选对应的来源的数据,该字节的最低两位对两个布尔值进行了编码(isResponse,isInquiry),消息的第二个自己总是0.第三,第四个字节包含了candidateID值,只有响应消息的最后8个字节才包含了选票总数信息。
public class VoteMsgBinCoder implements VoteMsgCoder {
public static final int MIN_WIRE_LENGTH = 4;
public static final int MAX_WIRE_LENGTH = 16;
public static final int MAGIC = 0x5400;// 0101 0100 0000 0000
public static final int MAGIC_MASK = 0xfc00;// 1111 1100 0000 0000
public static final int MAGIC_SHIFT = 8;
public static final int RESPONSE_FLAG = 0x0200;// 10 0000 0000
public static final int INQUIRE_FLAG = 0x0100;// 1 0000 0000
@Override
public byte[] toWire(VoteMsg msg) throws IOException {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(byteStream);
short magicAndFlags = MAGIC;
if (msg.isInquiry()) {
magicAndFlags |= INQUIRE_FLAG;
}
if (msg.isResponse()) {
magicAndFlags |= RESPONSE_FLAG;
}
out.writeShort(magicAndFlags);
// candidate ID will fit in a short :its>&&<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 {
if (input.length < MIN_WIRE_LENGTH) {
throw new IOException("Runt mssage");
}
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);
}
}
return new VoteMsg(resp, inq, candidateID, count);
}
}
************************************【注意】以下的案例中的Framer接口,LengthFramer类参考另外一篇笔记
【Java TCP/IP Socket编程】----发送和接收数据----消息成帧与解析
4..发送和接收
1)基于TCP套接字发送投票信息和接收信息,其中消息是使用的二进制方式进行编码。
public class VoteClientTCP {
public static final int CANDIDATEID = 888;
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 1234);
OutputStream out = socket.getOutputStream();
VoteMsgCoder coder = new VoteMsgBinCoder();
Framer framer = new LengthFramer(socket.getInputStream());
VoteMsg msg = new VoteMsg(false, true, CANDIDATEID, 0);
byte[] encodedMsg = coder.toWire(msg);
System.out.println("sending Inquiry (" + encodedMsg.length + "bytes):");
System.out.println(msg);
framer.frameMsg(encodedMsg, out);
//发送投票请求
msg.setInquiry(false);
encodedMsg = coder.toWire(msg);
System.out.println("Sending Vote (" + encodedMsg.length + "bytes):");
framer.frameMsg(encodedMsg, out);
//查询信息响应
encodedMsg = framer.nextMsg();
msg = coder.fromWire(encodedMsg);
System.out.println("Received Response (" + encodedMsg.length + "bytes):");
System.out.println(msg);
//投票信息响应
msg = coder.fromWire(framer.nextMsg());
System.out.println("Received Response (" + encodedMsg.length + "bytes):");
System.out.println(msg);
socket.close();
}
}
public class VoteServerTCP {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(1234);
VoteMsgCoder coder = new VoteMsgBinCoder();
VoteService service = new VoteService();
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("Handling client at :" + clientSocket.getRemoteSocketAddress());
Framer framer = new LengthFramer(clientSocket.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), clientSocket.getOutputStream());
}
} catch (IOException e) {
System.out.println("Error handling client:" + e.getMessage());
} finally {
System.out.println("Closing connection");
clientSocket.close();
}
}
}
}
2)基于UDP套接字发送投票信息和接收信息,其中消息是使用文本形式进行的编码。
public class VoteServiceUDP {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(1234);
byte[] buffer = new byte[VoteMsgTextCoder.MAX_WIRE_LENGTH];
VoteMsgCoder coder = new VoteMsgTextCoder();
VoteService service = new VoteService();
while(true) {
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
socket.receive(packet);
byte[] encodedMsg = Arrays.copyOfRange(packet.getData(), 0, packet.getLength());
System.out.println("Handling request from "+packet.getSocketAddress()+"("+encodedMsg.length+"bytes)");
try {
VoteMsg msg = coder.fromWire(encodedMsg);
msg = service .handleRequest(msg);
packet.setData(coder.toWire(msg));
System.out.println("Sending response ("+packet.getLength()+"bytes):");
System.out.println(msg);
socket.send(packet);
}catch(IOException e) {
System.out.println("Parse error in message:"+e.getMessage());
}
}
}
}
public class VoteClientTCP {
public static final int CANDIDATEID = 888;
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 1234);
OutputStream out = socket.getOutputStream();
VoteMsgCoder coder = new VoteMsgBinCoder();
Framer framer = new LengthFramer(socket.getInputStream());
VoteMsg msg = new VoteMsg(false, true, CANDIDATEID, 0);
byte[] encodedMsg = coder.toWire(msg);
System.out.println("sending Inquiry (" + encodedMsg.length + "bytes):");
System.out.println(msg);
framer.frameMsg(encodedMsg, out);
//发送投票请求
msg.setInquiry(false);
encodedMsg = coder.toWire(msg);
System.out.println("Sending Vote (" + encodedMsg.length + "bytes):");
framer.frameMsg(encodedMsg, out);
//查询信息响应
encodedMsg = framer.nextMsg();
msg = coder.fromWire(encodedMsg);
System.out.println("Received Response (" + encodedMsg.length + "bytes):");
System.out.println(msg);
//投票信息响应
msg = coder.fromWire(framer.nextMsg());
System.out.println("Received Response (" + encodedMsg.length + "bytes):");
System.out.println(msg);
socket.close();
}
}
public class VoteService {
private Map results = new HashMap<>();
public VoteMsg handleRequest(VoteMsg msg) {
if(msg.isResponse()) {
return msg;
}
msg.setResponse(true);
int candidate = msg.getCandidateID();
Long count = results.get(candidate);
if(count==null) {
count = 0L;
}
if(!msg.isInquiry()) {
results.put(candidate, ++count);
}
msg.setVoteCount(count);
return msg;
}
}