zoukankan      html  css  js  c++  java
  • 通信:成帧与解析

    成帧与解析

    当然,将数据转换成在线路上传输的格式只完成了一半工作,在接收端还必须将接收到的字节序列还原成原始信息。应用程序协议通常处理的是由一组字段组成的离散的信息。成帧(Framing)技术则解决了接收端如何定位消息的首尾位置的问题。无论信息是编码成了文本、多字节二进制数、或是两者的结合,应用程序协议必须指定消息的接收者如何确定何时消息已完整接收。

     如果一条完整的消息负载在一个DatagramPacket中发送,这个问题就变得很简单了:DatagramPacket 负载的数据有一个确定的长度,接收者能够准确地知道消息的结束位置。然而,如果通过TCP套接字来发送消息,情况将变得更复杂,因为TCP协议中没有消息边界的概念。如果一个消息中的所有字段都有固定的长度,同时每个消息又是由固定数量的字段组成的话,消息的长度就能够确定,接收者就可以简单地将消息长度对应的字节数读到一byte[]缓存区中。在TCPEchoClient.java示例程序中我们就是用的这个方法,在该例中我们能从服务器获得消息的字节数。但是如果消息的长度是可变的(例如消息中包含了一些变长的文本字符串),我们事先就无法知道需要读取多少字节。

     如果接收者试图从套接字中读取比消息本身更多的字节,将可能发生以下两种情况之一:如果信道中没有其他消息,接收者将阻塞等待,同时无法处理接收到的消息;如果发送者也在等待接收端的响应信息,则会形成死锁(deadlock);另一方面,如果信道中还有其他消息,则接收者会将后面消息的一部分甚至全部读到第一条消息中去,这将产生一些协议错误。因此,在使用TCP套接字时,成帧就是一个非常重要的考虑因素。

     一些相同的考虑也适用于查找消息中每个字段的边界:接收者需要知道每个字段的结束位置和下一个字段的开始位置。因此,我们在此介绍的消息成帧技术几乎都可以应用到字段上。然而,最简单并使代码最简洁的方法是将这两个问题分开处理:首先定位消息的结束位置,然后将消息作为一个整体进行解析。在此我们专注于将整个消息作为一帧进行处理。

     主要有两个技术使接收者能够准确地找到消息的结束位置:

     基于定界符(Delimiter-based):消息的结束由一个唯一的标记(unique marker,)指出,即发送者在传输完数据后显式添加的一个特殊字节序列。这个特殊标记不能在传输的数据中出现。

     显式长度(Explicit length):在变长字段或消息前附加一个固定大小的字段,用来指示该字段或消息中包含了多少字节。

     基于定界符的方法的一个特殊情况是,可以用在TCP连接上传输的最后一个消息上:在发送完这个消息后,发送者就简单地关闭(使用shutdownOutput()close()方法)发送端TCP连接。接收者读取完这条消息的最后一个字节后,将接收到一个流结束标记(即read()方法返回-1),该标记指示出已经读取到达了消息的末尾。

    基于定界符的方法通常用在以文本方式编码的消息中:定义一个特殊的字符或字符串来标识消息的结束。接收者只需要简单地扫描输入信息(以字节的方式)来查找定界序列,并将定界符前面的字符串返回。这种方法的缺点是消息本身不能包含有定界字符,否则接收者将提前认为消息已经结束。在基于定界符的成帧方法中,发送者要保证满足这个先决条件。幸运的是,填充(stuffing)技术能够对消息中出现才定界符进行修改,从而是接收者不将其识别为定界符。在接收者扫描定界符时,还能识别出修改过的数据,并在输出消息中对其进行还原,从而使其与原始消息一致。这个技术的缺点是发送者和接收者双方都必须扫描消息。

     基于长度的方法更简单一些,不过要使用这种方法必须知道消息长度的上限。发送者先要确定消息的长度,将长度信息存入一个整数,作为消息的前缀。消息的长度上限定义了用来编码消息长度所需要的字节数:如果消息的长度小于256字节,则需要1个字节;如果消息的长度小于65536字节,则需要2个字节,等等。

     为了展示以上技术,我们将介绍下面定义的Framer接口。它有两个方法:frameMsg()方法用来添加成帧信息并将指定消息输出到指定流,nextMsg()方法则扫描指定的流,从中抽取出下一条消息。

    Framer.java

    0 import java.io.IOException;

    1 import java.io.OutputStream;

    2

    3 public interface Framer {

    4 void frameMsg(byte[] message, OutputStream out) throws

    IOException;

    5 byte[] nextMsg() throws IOException;

     

    Framer.java 

    DelimFramer.java类实现了基于定界符的成帧方法,其定界符为"换行"符("\n",字节值10)。 frameMethod()方法并没有实现填充,当成帧的字节序列中包含有定界符时,它只是简单地抛出异常。(扩展该方法以实现填充功能将作为练习留给读者)nextMsg()方法扫描流,直到读取到了定界符,并返回定界符前面的所有字符,如果流为空则返回null。如果累积了一个消息的不少字符,但直到流结束也没有找到定界符,程序将抛出一个异常来指示成帧错误。

     DelimFramer.java

    0 import java.io.ByteArrayOutputStream;

    1 import java.io.EOFException;

    2 import java.io.IOException;

    3 import java.io.InputStream;

    4 import java.io.OutputStream;

    5

    6 public class DelimFramer implements Framer {

    7

    8 private InputStream in; // data source

    9 private static final byte DELIMITER = "\n"; // message

    delimiter

    10

    11 public DelimFramer(InputStream in) {

    12 this.in = in;

    13 }

    14

    15 public void frameMsg(byte[] message, OutputStream out)

    throws IOException {

    16 // ensure that the message does not contain the delimiter

    17 for (byte b : message) {

    18 if (b == DELIMITER) {

    19 throw new IOException("Message contains delimiter");

    20 }

    21 }

    22 out.write(message);

    23 out.write(DELIMITER);

    24 out.flush();

    25 }

    26

    27 public byte[] nextMsg() throws IOException {

    28 ByteArrayOutputStream messageBuffer = new

    ByteArrayOutputStream();

    29 int nextByte;

    30

    31 // fetch bytes until find delimiter

    32 while ((nextByte = in.read()) != DELIMITER) {

    33 if (nextByte == -1) { // end of stream?

    34 if (messageBuffer.size() == 0) { // if no byte read

    35 return null;

    36 } else { // if bytes followed by end of stream: framing

    error

    37 throw new EOFException("Non-empty message without

    delimiter");

    38 }

    39 }

    40 messageBuffer.write(nextByte); // write byte to buffer

    41 }

    42

    43 return messageBuffer.toByteArray();

    44 }

    45 }

    DelimFramer.java  

    1.构造函数:第11-13

    获取消息的输入流作为参数传递给该函数。

    2.frameMsg()方法用于添加帧信息:第15-25

    校验消息形式的有效性:第17-21

    检查消息中是否包含了定界符,如果包含,则抛出一个异常。

    写消息:第22

    将成帧的消息输出到流中。 

    写定界符:第23

    3. nextMsg()方法从输入中提取消息:第27-44

    读取流中的每个字节,直到遇到定界符为止:第32

    处理流的终点:第33-39

    如果在遇到定界符之前就已经到了流的终点,则分两种情况:一是从帧的构造开始或从遇到前一个定界符以来,缓存区已经接收了一些字节,这时程序将抛出一个异常;否则nextMsg()方法将返回null以表示全部消息已接收完。

     将无定界符的字节写入消息缓存区:第40

     将消息缓存区中的内容以字节数组的形式返回:第43

     我们的定界符帧有一个限制,即它不支持多字节定界符。如何对其进行修改以支持多字节定界符将作为练习留给我们的读者。

     LengthFramer.java类实现了基于长度的成帧方法,适用于长度小于65535 (216 ? 1)字节的消息。发送者首先给出指定消息的长度,并将长度信息以big-endian顺序存入两个字节的整数中,再将这两个字节放在完整的消息内容前,连同消息一起写入输出流。在接收端,我们使用DataInputStream以读取整型的长度信息;readFully()  方法将阻塞等待,直到给定数组完全填满,这正是我们需要的。值得注意的是,使用这种成帧方法,发送者不需要检查要成帧的消息内容,而只需要检查消息的长度是否超出了限制。

     LengthFramer.java

    0 import java.io.DataInputStream;

    1 import java.io.EOFException;

    2 import java.io.IOException;

    3 import java.io.InputStream;

    4 import java.io.OutputStream;

    5

    6 public class LengthFramer implements Framer {

    7 public static final int MAXMESSAGELENGTH = 65535;

    8 public static final int BYTEMASK = 0xff;

    9 public static final int SHORTMASK = 0xffff;

    10 public static final int BYTESHIFT = 8;

    11

    12 private DataInputStream in; // wrapper for data I/O

    13

    14 public LengthFramer(InputStream in) throws IOException

    {

    15 this.in = new DataInputStream(in);

    16 }

    17

    18 public void frameMsg(byte[] message, 

    OutputStream out) throws IOException {

    19 if (message.length > MAXMESSAGELENGTH) {

    20 throw new IOException("message too long");

    21 }

    22 // write length prefix

    23 out.write((message.length >> BYTESHIFT) & BYTEMASK);

    24 out.write(message.length & BYTEMASK);

    25 // write message

    26 out.write(message);

    27 out.flush();

    28 }

    29

    30 public byte[] nextMsg() throws IOException {

    31 int length;

    32 try {

    33 length = in.readUnsignedShort(); // read 2 bytes

    34 } catch (EOFException e) { // no (or 1 byte) message

    35 return null;

    36 }

    37 // 0 <= length <= 65535

    38 byte[] msg = new byte[length];

    39 in.readFully(msg); // if exception, it's a framing

    error.

    40 return msg;

    41 }

    42 }

     LengthFramer.java

     1. 构造函数:第14-16 

    获取帧消息源的输入流,并将其包裹在一个DataInputStream中。 

    2. frameMsg()方法用来添加成帧信息:第18-28 

    校验消息长度:第19-21 

    由于我们用的是长为两个字节的字段,因此消息的长度不能超过65535。(注意该值太大而不能存入一个short型整数中,因此我们每次只向输出流写一个字节)。 

    输出长度字段:第23-24 

    添加长度信息(无符号short型整数)前缀,输出消息的字节数。

    输出消息:第26

    3.nextMsg()方法用于从输入流中提取下一帧:第30-41

    读取长度前缀:第32-36

    readUnsignedShort()方法读取两个字节,将它们作为big-endian整数进行解释,并以int型整数返回它们的值。

    读取指定数量的字节:第38-39

    readfully() 将阻塞等待,直到接收到足够的字节来填满指定的数组。

     以字节的形式返回消息:第40

     

    相关下载:

    Java_TCPIP_Socket编程(doc)

    http://download.csdn.net/detail/undoner/4940239

     

    文献来源:

    UNDONER(小杰博客) :http://blog.csdn.net/undoner

    LSOFT.CN(琅软中国) :http://www.lsoft.cn

     

  • 相关阅读:
    设计模式之单例模式
    Java反射之调用内部类
    Java反射之修改常量值
    myBatis之入门示例
    eclipse创建maven工程
    java核心卷轴之集合
    java核心卷轴之泛型程序设计
    sublime常用快捷键
    sublime使用package control安装插件
    sublime使用技巧之添加到右键菜单、集成VI
  • 原文地址:https://www.cnblogs.com/wuyida/p/6301078.html
Copyright © 2011-2022 走看看