【Java TCP/IP Socket】构建和解析自定义协议消息(含代码)

在传输消息时,用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个字节才包含了选票总数信息。字节序列格式如下图所示:

代码如下:

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

转自:http://blog.csdn.net/ns_code/article/details/14229253

时间: 2024-11-09 07:27:33

【Java TCP/IP Socket】构建和解析自定义协议消息(含代码)的相关文章

【Java TCP/IP Socket】Socket编程知识点总结

简介 1.协议相当于相互通信的程序间达成的一种约定,它规定了分组报文的结构.交换方式.包含的意义以及怎样对报文所包含的信息进行解析. 2.TCP/IP协议族有IP协议.TCP协议和UDP协议. 3.TCP协议和UDP协议使用的地址叫做端口号,用来区分同一主机上的不同应用程序.TCP协议和UDP协议也叫端到端传输协议,因为他们将数据从一个应用程序传输到另一个应用程序,而IP协议只是将数据从一个主机传输到另一个主机. 4.在TCP/IP协议中,有两部分信息用来确定一个指定的程序:互联网地址和端口号:

一个项目看java TCP/IP Socket编程

前一段时间刚做了个java程序和网络上多台机器的c程序通讯的项目,遵循的是TCP/IP协议,用到了java的Socket编程.网络通讯是java的强项,用TCP/IP协议可以方便的和网络上的其他程序互通消息. 先来介绍下网络协议:     TCP/IP         Transmission Control Protocol 传输控制协议         Internet Protocol 互联网协议     UDP         User Datagram Protocol 用户数据协议

【Java TCP/IP Socket】TCP Socket通信中由read返回值造成的的死锁问题(含代码)

书上示例 在第一章<基本套接字>中,作者给出了一个TCP Socket通信的例子——反馈服务器,即服务器端直接把从客户端接收到的数据原原本本地反馈回去. 书上客户端代码如下: import java.net.Socket; import java.net.SocketException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class TCPEch

【Java TCP/IP Socket】UDP Socket(含代码)

转载请注明出处:http://blog.csdn.net/ns_code/article/details/14128987 UDP的Java支持 UDP协议提供的服务不同于TCP协议的端到端服务,它是面向非连接的,属不可靠协议,UDP套接字在使用前不需要进行连接.实际上,UDP协议只实现了两个功能: 1)在IP协议的基础上添加了端口: 2)对传输过程中可能产生的数据错误进行了检测,并抛弃已经损坏的数据. Java通过DatagramPacket类和DatagramSocket类来使用UDP套接字

【Java TCP/IP Socket】基于线程池的TCP服务器(含代码)

了解线程池 在http://blog.csdn.net/ns_code/article/details/14105457(读书笔记一:TCP Socket)这篇博文中,服务器端采用的实现方式是:一个客户端对应一个线程.但是,每个新线程都会消耗系统资源:创建一个线程会占用CPU周期,而且每个线程都会建立自己的数据结构(如,栈),也要消耗系统内存,另外,当一个线程阻塞时,JVM将保存其状态,选择另外一个线程运行,并在上下文转换(context switch)时恢复阻塞线程的状态.随着线程数的增加,线

【Java TCP/IP Socket】深入剖析socket——TCP套接字的生命周期

建立TCP连接 新的Socket实例创建后,就立即能用于发送和接收数据.也就是说,当Socket实例返回时,它已经连接到了一个远程终端,并通过协议的底层实现完成了TCP消息或握手信息的交换. 客户端连接的建立 Socket构造函数的调用与客户端连接建立时所关联的协议事件之间的关系下图所示: 当客户端以服务器端的互联网地址W.X.Y.Z和端口号Q作为参数,调用Socket的构造函数时,底层实现将创建一个套接字实例,该实例的初始状态是关闭的.TCP开放握手也称为3次握手,这通常包括3条消息:一条从客

Java TCP/UDP socket 编程流程总结

最近正好学习了一点用java socket编程的东西.感觉整体的流程虽然不是很繁琐,但是也值得好好总结一下. Socket Socket可以说是一种针对网络的抽象,应用通过它可以来针对网络读写数据.就像通过一个文件的file handler就可以都写数据到存储设备上一样.根据TCP协议和UDP协议的不同,在网络编程方面就有面向两个协议的不同socket,一个是面向字节流的一个是面向报文的. 对socket的本身组成倒是比较好理解.既然是应用通过socket通信,肯定就有一个服务器端和一个客户端.

内核参数优化之2-1 tcp/ip 标志位报文解析

以下内容纯属虚构,切勿轻易相信! 众所周知,tcp/ip三次握手和四次挥手,均由syn/ack/fin三个标志位报文决定,但是这三个标志位报文,并不是说在构建连接的时候只发送一次的,因为协议不知道网络状况. 故而就存在了以下参数,可以调节发送次数 net.ipv4.tcp_syn_retries 这个参数从字面上来看就是syn标志位报文的重试次数,什么时候发送syn标志位呢?三次握手中,请求端第一次构建连接的时候,默认是5次,但是对于一个处于网络状况好的请 求端,5次显然是多了,因此,我们来个2

mysql错误:Can’t create TCP/IP socket (10106)

昨天晚上十一点半,有个女同学打电话说电脑出问题了,说tomcat和mysql打不开了,各种急!因为后天就要答辩了,这些软件打不开,系统也就运行不起来!大半夜的让我怎么办,只好说明天早起帮看看! 早早的起来了,接过同学的电脑!回到宿舍看了一下,mysql果然打不开报了这样的错误"mysql错误:Can't create TCP/IP socket (10106)",目测是socket端口被占用的原因,然后在打开tomcat,报的错误中也包含了"socket",再一次加