Java Socket关于消息边界的问题解决(一)

2014-11-24 07:53:48 · 作者: · 浏览: 1

大家在编写Socket有关的程序时,肯定会经常处理“消息边界”的问题。以下是我对“处理消息边界”的一些认识:

场景:

当接收者试图从套接字中读取比消息本身更多的字节时,将可能发生两种情况:

1.如果套接字中没有其他消息,接收者将会阻塞等待,同时无法处理接收到的消息;如果发送者也在等待接收端的响应消息,则会形成死锁;

2.如果套接字中有其他消息,接收者会将后面消息的一部分甚至全部读到第一条消息中去,这将产生一些协议错误;

应用:

当我们使用TCP套接字时,处理“消息边界” 是一个重要的考虑因素;如果使用UDP套接字时,不存在这个问题,因为在DatagramPacket中存放的数据

有一个确定的长度(DatagramPacket.getLength()方法),接收者能够精确的知道“消息边界”(或消息结束位置)

现在介绍两个技术可以使接收者能够精确的找到“消息边界”(也就是消息的结束位置)

1. 基于定界符

消息的结束由一个唯一的标记指出,即发送者在传输消息后显示添加一个特殊标记。这个特殊标记不能在传输消息本身中出现。

2.显示长度

在变长字段或消息前附加一个固定大小的字段,用来指示该字段或消息中包含了多少字节;

注意:

在“基于定界符”中有一个特殊情况是,可以用在TCP连接上传输的最后一个消息上。在发送完这个消息后就可以简单的关闭

(使用socket.shutdownOutput()方法或socket.close()方法)

发送端的TCP连接。接收者读取完这条消息的最后一个字节后,将接收到一个流结束标记(即InputStream.read() 返回 -1),

该标记指出了已经读取到流的末尾。个人感觉从原理上来说这也是一种基于“定界符”技术的实现。----- 我就喜欢使用这个方式

简单明了。


例子:

我们下面就实现一个“基于定界符”的技术,用来处理“消息边界” 问题。


1. 处理定界符的消息类

public class DelimFramer
{

    private static final int DELIMITER = '\n';

    /**
     * 添加成帧信息并将信息写入到输出流
     * 
     * @param message
     * @param out
     * @throws IOException
     */
    public void frameMsg(byte[] message, OutputStream out) throws IOException
    {
        for (byte b : message)
        {
            /*
             * 注意:发送的消息本身不能包含定界符。如果存在,则抛出异常
             */
            if (b == DELIMITER)
            {
                throw new IOException("Message contains delimiter");
            }
        }

        out.write(message);
        out.write(DELIMITER);
        out.flush();
    }

    /**
     * 读入输入流,直到读取到了定界符,并返回定界符前面的所有字符
     * 
     * 1.包含定界符的信息 2.不包含定界符的信息
     * 
     * @return
     * @throws IOException
     */
    public byte[] nextMsg(InputStream in) throws IOException
    {
        ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();
        int nextByte;

        /*
         * 情况一:判断消息中是否包含定界符; 如果输入流读取到了定界符,则返回定界前面的所有字符(不包括定界符)
         */
        while ((nextByte = in.read()) != DELIMITER)
        {
            /*
             * 情况二:判断消息中是否不包含定界符;如果输入流读取到了-1(说明该消息中不包括定界符)
             */
            if (nextByte == -1)
            {
                /*
                 * 判断BytaArrayOutputStream的缓冲区中是否有数据:
                 * 1.如果没有数据:说明从该输入流中没有读取到消息,就到达输入流的末尾 ;
                 * 2.如果有数据:说明从该输入流中读取的消息是一个不带分界符的非空消息;
                 */
                if (messageBuffer.size() == 0)
                {
                    return null;
                }
                else
                {
                    throw new EOFException(
                            "Non-empty message without delimiter");
                }
            }
            messageBuffer.write(nextByte);
        }
        return messageBuffer.toByteArray();
    }
}

2. TCP客户端类

public class TCPClient
{

    public static void main(String[] args) throws UnknownHostException,
            IOException
    {
        Socket client = new Socket(InetAddress.getLocalHost(), 8888);
        OutputStream output = client.getOutputStream();
        InputStream input = client.getInputStream();
        DelimFramer delimFramer = new DelimFramer();

        byte[] msg = new String("Hello").getBytes();
        // 发送消息
        delimFramer.frameMsg(msg, output);

        // 接收消息
        byte[] receiveByte = delimFramer.nextMsg(input);
        String receiveMsg = new String(receiveByte);

        System.out.println("Client receive msg:" + receiveMsg);

        input.close();
        output.close();
        client.close();

    }
}

3.TCP服务端类

public class TCPServer
{

    public static void main(String[] args) throws IOException
    {
        DelimFramer delimFramer = new DelimFramer();

        ServerSocket server = new ServerSocket(8888);
        OutputStream output;
        InputStream input;

        while (true)
        {
            Socket client = server.accept();

            System.out.println("Handing client at "
                    + client.getRemoteSocketAddress());

            output = client.getOutputStream();
            input = client.getInputStream();

            byte[] msg = delimFramer.nextMsg(input);

            System.out.println("Server receive msg:" + new String(msg));

            delimFramer.frameMsg(msg, output);