Android P解决Socket通信Tcp粘包问题

TCP协议是一个面向流的协议,所以他会出现粘包的问题。

TCP和UDP的调查请参考:
《Android中关于TCP socket通信数据大小,内存缓冲区和数据可靠性的一点调查》

一、TCP服务端和客户端代码实现

客户端代码实现
连接服务器的代码:

protected void connectServerWithTCPSocket() {
   boolean bRun = true;
   try {
       // 创建一个Socket对象,并指定服务端的IP及端口号
       // 本地回路ip:127.0.0.0
       mSocket = new Socket("127.0.0.1", 9897);
   } catch (UnknownHostException e) {
       e.printStackTrace();
   } catch (IOException e) {
       e.printStackTrace();
   }
}

服务端代码实现
在服务端实现对数据的接收:

public void createTcpServerSocket() {
  // 声明一个ServerSocket对象
  ServerSocket serverSocket = null;
  boolean bRun = true;

  try {
      // 创建一个ServerSocket对象,并让这个Socket在8919端口监听
      Log.i(TAG,"new ServerSocket");
      serverSocket = new ServerSocket(9897);
      Log.i(TAG,"new ServerSocket,serverSocket=" + serverSocket);

      // 调用ServerSocket的accept()方法,接受客户端所发送的请求,
      // 如果客户端没有发送数据,那么该线程就停滞不继续
      Socket socket = serverSocket.accept();
      Log.i("sunxiaolin ServerSocket","serverSocket receive data");

      //文本传输
      // 从Socket当中得到InputStream对象
      InputStream inputStream = socket.getInputStream();
      byte buffer[] = new byte[1024];
      int temp = 0;
      int length = 0;
      // 从InputStream当中读取客户端所发送的数据,阻塞状态
      // 从缓冲区读出客户端发送的数据
      while ((temp = inputStream.read(buffer)) != -1) {
          //System.out.println(new String(buffer, 0, temp));
          length += temp;
          Log.i("tcp socket server","recevie buffer=" + printHexBinary(buffer));
      }
  } catch (IOException e) {
      e.printStackTrace();
  }
}

二、TCP粘包问题重现

粘包只有在快速发送多个包的时候才会出现。

循环发送20次数据:

for(int i=0; i<20; i++){
    String title;
    if(i%2 == 0){
        title = "aaa";
    }else{
        title = "bbb";
    }
    //发送字符串的数据
    sendMusicData(0x01,title.getBytes());
}

发送数据的代码:

public static void sendMusicData(int frameId,byte[] data) {
    if( mSocket != null){
		//size为长度
        int size = size = data.length;
        byte buffer[] = new byte[8 + size];
        buffer[0] = (byte)0xA6;
        buffer[1] = 0x6A;
        buffer[2] = 0x03;
        buffer[3] = (byte)((size & 0xff000000) >> 24);
        buffer[4] = (byte)((size & 0x00ff0000) >> 16);
        buffer[5] = (byte)((size & 0x0000ff00) >> 8);
        buffer[6] = (byte)((size & 0x000000ff));
        buffer[7] = (byte)frameId;

        System.arraycopy(data, 0, buffer, 8, size);
        Log.i("tcp socket client","send buffer=" + printHexBinary(buffer));

        try {
        	//发送一段buffer,对buffer包进行了特定协议的封包
            DataOutputStream outputStream = new DataOutputStream(mSocket.getOutputStream());
            outputStream.write(buffer);
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }else{

    }
}

//打印字符数组的代码
private static final char[] hexCode = "0123456789ABCDEF".toCharArray();
public static String printHexBinary(byte[] data) {
    StringBuilder r = new StringBuilder(data.length * 2);
    for (byte b : data) {
        r.append(hexCode[(b >> 4) & 0xF]);
        r.append(hexCode[(b & 0xF)]);
    }
    return r.toString();
}

运行程序,根据打印发现,出现了粘包的问题,即把两个包读到了一个buffer中:

socket client: send buffer=A66A030000000A01476F696E6720486F6D65
socket client: send buffer=A66A0300000008014E657720536F756C
socket client: send buffer=A66A030000000A01476F696E6720486F6D65
socket client: send buffer=A66A0300000008014E657720536F756C
socket client: send buffer=A66A030000000A01476F696E6720486F6D65
socket client: send buffer=A66A0300000008014E657720536F756C
socket client: send buffer=A66A030000000A01476F696E6720486F6D65
socket client: send buffer=A66A0300000008014E657720536F756C
socket client: send buffer=A66A030000000A01476F696E6720486F6D65
socket client: send buffer=A66A0300000008014E657720536F756C
socket client: send buffer=A66A030000000A01476F696E6720486F6D65
socket server: recevie buffer=A66A0300000008014E657720536F756CA66A030000000A01476F696E6720486F6D65A66A0300000008014E657720536F756CA66A030000000A01476F696E6720486F6D65
socket client: send buffer=A66A0300000008014E657720536F756C
socket client: send buffer=A66A030000000A01476F696E6720486F6D65
socket server: recevie buffer=A66A0300000008014E657720536F756CA66A030000000A01476F696E6720486F6D65A66A0300000008014E657720536F756CA66A030000000A01476F696E6720486F6D65
socket client: send buffer=A66A0300000008014E657720536F756C
socket client: send buffer=A66A030000000A01476F696E6720486F6D65
socket client: send buffer=A66A0300000008014E657720536F756C
socket server: recevie buffer=A66A030000000A01476F696E6720486F6D65A66A0300000008014E657720536F756CA66A030000000A01476F696E6720486F6D65A66A0300000008014E657720536F756CA66A030000000A01476F696E6720486F6D65A66A0300000008014E657720536F756CA66A030000000A01476F696E6720486F6D65
socket client: send buffer=A66A030000000A01476F696E6720486F6D65

三、TCP粘包问题原因分析

理想状态:服务端先接受一个package1,在接受一个package2,…,一次一个包一个包的接收,则这样无问题。

粘包的三种情况
1、先读取到package1的部分数据,然后读取package1的余下数据和package2的全部数据;
2、先读取到package1的全部数据和package2的部分数据,然后读到package2的余下数据;
3、同时一次读取到package1和package2的全部数据。

很明显,我们这个Demo中,一次读取到了两包或者多包数据,属于这里面的第三种情况。

粘包原因
1、客户端write()时发送的数据大于socket缓冲区的数据,导致需要多次write()才能把数据写入到缓冲去,这样导致read()数据的时候,一次read()的数据不完整。需要read()多次才能将数据读完;
2、发送端write()数据的速度太快,而接收端read()数据的太慢,导致接收方来不及接收每一包数据,造成了一次读取到多包数据的情况。我们这里也是属于这种情况。

四、TCP粘包问题解决方法

1、增加一个自己定义的传输协议,即增加一个包头,将帧头信息,长度信息包含在里面。包头长度固定,接收端根据包头中的长度去解析一个完整的包数据。

在我们的demo中,定好的协议为:

     byte buffer[] = new byte[8 + size];
     buffer[0] = (byte)0xA6;
     buffer[1] = 0x6A;
     buffer[2] = 0x03;
     buffer[3] = (byte)((size & 0xff000000) >> 24);
     buffer[4] = (byte)((size & 0x00ff0000) >> 16);
     buffer[5] = (byte)((size & 0x0000ff00) >> 8);
     buffer[6] = (byte)((size & 0x000000ff));
     buffer[7] = (byte)frameId;
     System.arraycopy(data, 0, buffer, 8, size);

我们的帧头固定为8个字节。其中buffer[3]-buffer[6]为数据的长度,所以根据这个调整一下接收端数据解析思路。接收端在服务端,修改服务端解析数据代码如下:

            //文本传输
            // 从Socket当中得到InputStream对象
            InputStream inputStream = socket.getInputStream();
            byte buffer[] = new byte[1024];
            int frameSize = 0;
            byte[] mRecvBuffer = {0};
            int mActualReadSize = 0;
            int readSize = 0;
            // 从InputStream当中读取客户端所发送的数据,阻塞状态
            while ((readSize = inputStream.read(buffer)) != -1) {
                Log.i("TcpSocketServer","read totalSize=" + readSize );
                Log.i("TcpSocketServer","read buffer=" + printHexBinary(buffer) );
                mActualReadSize = 0;

                if( readSize > 8 ){
                    frameSize = ( (buffer[3] & 0xff) << 24 | (buffer[4] & 0xff) << 16 | (buffer[5] & 0xff) << 8 | (buffer[6] & 0xff) );
                }

                while( readSize >= frameSize + 8 ){
                    mRecvBuffer = new byte[frameSize + 8];
                    System.arraycopy(buffer, mActualReadSize, mRecvBuffer, 0, frameSize + 8);
                    mActualReadSize += frameSize + 8;
                    Log.i("TcpSocketServer","read mRecvBuffer=" + printHexBinary(mRecvBuffer) );

                    int surplusLength = readSize - (frameSize + 8);
                    readSize -=  (frameSize + 8);
                    if( surplusLength >= 8 ){
                        int head = ((buffer[mActualReadSize] & 0xff) << 8) | buffer[mActualReadSize + 1];
                        if( head == 0xA66A ){
                            frameSize = 0;
                            frameSize = ( (buffer[mActualReadSize + 3] & 0xff) << 24 | (buffer[ mActualReadSize + 4] & 0xff) << 16 | (buffer[ mActualReadSize + 5] & 0xff) << 8 | (buffer[ mActualReadSize + 6] & 0xff) );
                        }else{
                            break;
                        }
                    }
                }
            }

打印如下:
Android P解决Socket通信Tcp粘包问题_第1张图片

可以看到mRecvBuffer可以读出了单个的包。

当然这里只解决了粘包的第三种情况,这种情况byte buffer[] = new byte[1024];,读出的缓冲区尽量要大些,这里设为1024,远远比我发送的测试数据要大。所以不会产生第一种和第二种粘包的情况。

这种增加固定的协议,解析帧头和长度,应该是最为灵活的方法。

你可能感兴趣的:(android,p通信机制)