【Java TCP/IP Soket】— 消息边界的问题解决

关于消息边界问题,在TCP套接字处理接收消息中尤为重要,所以大家一定要学会解决它!

场景:

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

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

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

应用:

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

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

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


       1. 基于定界符

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

           注意:

           (1)前提条件:

                使用“基于定界符”的方法来解决消息边界问题时,消息本身不能包含有“定界符”,否则接收者将提前认为消息已经结束;

           (2)特殊的实现:

                使用Socket.close( )或Socket.shutdownOutput( ) 来实现“基于定界符”的方法在“基于定界符”中有一个特殊情况是,可以用在TCP连接上传输的最后一个消息

                上。 在发送完这个消息后,发送者就可以简单的关闭(使用socket.shutdownOutput()方法或socket.close()方法)发送端的TCP连接。接收者读取完这条消息

                的最后一个字节后,将接收到一个流结束标记(即InputStream.read() 返回 -1),该标记指出了已经读取到流的末尾;

           (2)应用场景:

               “基于定界符”的方法通常用在“以文本方式编码的消息”中,不能用在“以二进制方式编码的消息”中(例如图片、MP3),其中最大的一个原因就是:接收者需要遍历

                消息信息来查找“定界符”(定界符:其实就是使用一个特殊的字符或字符串来标识消息的结束)。假如这个消息信息是一个图片,你在图片(二进制文件)中去查找

                一个“字符”合适吗?肯定不合适,二进制肯定不能与字符来进行比较;


       2.显示长度

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

           注意:使用这种方式必须要知道消息的上限,但是,假如在无意间发送的消息超过了消息的上限,如果不处理妥当,将会发生消息丢失;

          

例子:

一.基于定界符的实现例子

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

这个例子还有一个缺点,就是只考虑了“定界符”是单字节的情况,对于多字节的情况没有考虑。自己也没有找到什么好的办法,如果大家有知道的请回复一下。


2.使用定界符的“特殊的实现”(close( )或shutdownOutput( )方法), 解决消息边界问题:

(1)TCP客户端

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

public class TestClient
{
    public static void main(String[] args) throws UnknownHostException,
            IOException
    {
        byte[] msg = new String("Hello Server!").getBytes();

        Socket client = new Socket(InetAddress.getLocalHost(), 8888);
        OutputStream output = client.getOutputStream();
        InputStream input = client.getInputStream();

        output.write(msg);
        output.flush();

        client.shutdownOutput();

        ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
        int readSize = 0;
        byte[] temp = new byte[1024];
        while ((readSize = input.read(temp)) != -1)
        {
            byteArray.write(temp, 0, readSize);
        }

        byte[] recvByte = byteArray.toByteArray();

        System.out.println("Client receive message:" + new String(recvByte));

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

(2)TCP服务端

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TestServer
{
    public static void main(String[] args) throws IOException
    {
        ServerSocket server = new ServerSocket(8888);
        byte[] msg = new String("Hello Client!").getBytes();
        while (true)
        {

            Socket client = server.accept();

            System.out.println("Handling clint at:"
                    + client.getRemoteSocketAddress());

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

            ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
            byte[] temp = new byte[1024];
            int readSize = 0;
            while ((readSize = input.read(temp)) != -1)
            {
                byteArrayOut.write(temp, 0, readSize);
            }
            byte[] recvByte = byteArrayOut.toByteArray();
            System.out
                    .println("Server receive message:" + new String(recvByte));

            output.write(recvByte);
            output.flush();
            client.shutdownOutput();

            output.close();
            input.close();
        }
    }
}

   注意:

   使用该方法 适用于 客户端与服务端的两次握手通信,一般能够瞒住大部分业务逻辑需求。两次握手通信为:客户端发送消息 服务端接收、服务端发送消息 客户端接收;

   如果要实现多次握手通信,请使用 “自定义定界符” 方式实现。


二. 显示长度的实现例子

   前面已经说过,使用 “显示长度” 的方式必须要知道 “消息长度的上限”,所以我们可以使用DataInputStream类来读取消息长度,它提供了两个方法,分别为:

   DataInputStream.readUnsignedByte( ): 读取此输入流的下一个字节并返回”无符号 8 位数“, 所以它的取值范围为:0 ~ 255 (2^8-1) , 所以, 消息长度上限为: 255;

   DataInputStream.readUnsignedShort():读取此输入流的下两个字节并返回” 一个无符号 16 位整数“ , 所以它的取值范围为:0 ~ 65535 (2^16-1), 所以, 消息长度上限为: 65535;

  (1). 处理定界符的消息类:

  

public class LengthFramer implements Framer
{

    public static final int MAXMESSAGELENGTH = 65535;

    public static final int BYTEMASK = 255;

    public static final int SHORTMASK = 65535;

    public static final int BYTESHTFT = 8;

    @Override
    public void frameMsg(byte[] message, OutputStream output)
            throws IOException
    {
        /**
         * 这里的接收端接收的消息长度上限为65535个byte,所以这里必须判断发送消息的长度上限。 如果超出消息长度上限,超出的部分会被忽略
         */
        if (message.length > MAXMESSAGELENGTH)
        {
            throw new IOException("message to long");
        }
        // 这里使用了Java中的移位运算与位运算,将发送的消息长度拆分为2个字节并发送(readUnsignedShort()方法:读取输入流的下两个字节,所以这里必须将消息长度拆分为2个字节发送)
        output.write((message.length >> BYTESHTFT) & BYTEMASK);
        output.write(message.length & BYTEMASK);

        output.write(message);
        output.flush();
    }

    @Override
    public byte[] nextMsg(InputStream input) throws IOException
    {
        int length;
        DataInputStream dataInput;
        try
        {
            /**
             * 使用readUnsignedShort()返回的最大值为65535,所以接收msg数组的长度最大为65535,所以,
             * 接收消息长度的上限为65535个字节
             */
            dataInput = new DataInputStream(input);
            length = dataInput.readUnsignedShort();
        }
        catch (EOFException e)
        {
            return null;
        }
        byte[] msg = new byte[length];
        dataInput.readFully(msg);
        return msg;
    }
}  

   注意:

   使用 “显示长度” 的方式 处理消息边界有一个弊端,就是必须要知道消息长度上限。但是,在实际应用中,我们发送的消息长度往往都在不经意间超出了消息长度,如果不处理妥当

   这时候就会造成消息的丢失,所以,这个方法也不实用,大概了解一下吧。


  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值