天天看點

【Java TCP/IP Socket】建構和解析自定義協定消息(含代碼)

轉載請注明出處:http://blog.csdn.net/ns_code/article/details/14229253

     在傳輸消息時,用Java内置的方法和工具确實很用,如:對象序列化,RMI遠端調用等。但有時候,針對要傳輸的特定類型的資料,實作自己的方法可能更簡單、容易或有效。下面給出一個實作了自定義建構和解析協定消息的Demo(書上例子)。

     該例子是一個簡單的投票協定。這裡,一個用戶端向伺服器發送一個請求消息,消息中包含了一個候選人的ID,範圍在0~1000。程式支援兩種請求:一種是查詢請求,即向伺服器詢問候選人目前獲得的投票總數,伺服器發回一個響應消息,包含了原來的候選人ID和該候選人目前獲得的選票總數;另一種是投票請求,即向指定候選人投一票,伺服器對這種請求也發回響應消息,包含了候選人ID和獲得的選票數(包含了剛剛投的一票)。

     在實作一個協定時,一般會定義一個專門的類來存放消息中所包含的的資訊。在我們的例子中,用戶端和服務端發送的消息都很簡單,唯一的差別是服務端發送的消息還包含了選票總數和一個表示相應消息的标志。是以,可以用一個類來表示用戶端和服務端的兩種消息。下面的VoteMsg.java類展示了每條消息中的基本資訊:

  •      布爾值isInquiry,true表示該消息是查詢請求,false表示該消息是投票請求;
  •      布爾值isResponse,true表示該消息是伺服器發送的相應消息,false表示該消息為用戶端發送的請求消息;
  •      整型變量candidateID,訓示了候選人的ID;
  •      長整型變量voteCount,訓示出所查詢的候選人獲得的總選票數。

     另外,注意一下幾點:

  •       candidateID的範圍在0~1000;
  •       voteCount在請求消息中必須為0;
  •       voteCount不能為負數

     VoteMsg代碼如下:

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;
  }
}           

     接下來,我們要根據一定的協定來對其進行編解碼,我們定義一個VoteMsgCoder接口,它提供了對投票消息進行序列化和反序列化的方法。toWrie()方法用于根據一個特定的協定,将投票消息轉換成一個位元組序列,fromWire()方法則根據相同的協定,對給定的位元組序列進行解析,并根據資訊的内容傳回一個該消息類的執行個體。

import java.io.IOException;

public interface VoteMsgCoder {
  byte[] toWire(VoteMsg msg) throws IOException;
  VoteMsg fromWire(byte[] input) throws IOException;
}           

     下面給出兩個實作了VoteMsgCoder接口的類,一個實作的是基于文本的編碼方式 ,一個實作的是基于二進制的編碼方式。

     首先是用文本方式對消息進行編碼的程式。該協定指定使用ASCII字元集對文本進行編碼。消息的開頭是一個所謂的”魔術字元串“,即一個字元序列,用于快速将投票協定的消息和網絡中随機到來的垃圾消息區分開,投票/查詢布爾值被編碼為字元形似,‘v’代表投票消息,‘i’代表查詢消息。是否為伺服器發送的響應消息,由字元‘R’訓示,狀态标記後面是候選人ID,其後跟的是選票總數,它們都編碼成十進制字元串。

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);
  }
}           

     toWire()方法簡單地建立一個字元串,該字元串中包含了消息的所有字段,并由空白符隔開。fromWire()方法首先檢查”魔術字元串“,如果在消息最前面沒有魔術字元串,則抛出一個異常。在理說明了在實作協定時非常重要的一點:永遠不要對從網絡中來的任何輸入進行任何假設。你的程式必須時刻為任何可能的輸入做好準備,并能很好的對其進行處理。

     下面将展示基于二進制格式對消息進行編碼的程式。與基于文本的格式相反,二進制格式使用固定大小的消息,每條消息由一個特殊位元組開始,該位元組的最高六位為一個”魔術值“010101,該位元組的最低兩位對兩個布爾值進行了編碼,消息的第二個位元組總是0,第三、四個位元組包含了candidateID值,隻有響應消息的最後8個位元組才包含了選票總數資訊。位元組序列格式如下圖所示:

【Java TCP/IP Socket】建構和解析自定義協定消息(含代碼)

     代碼如下:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

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);
  }
}