TCP协议是一个面向流的协议,所以他会出现粘包的问题。
TCP和UDP的调查请参考:
《Android中关于TCP socket通信数据大小,内存缓冲区和数据可靠性的一点调查》
客户端代码实现
连接服务器的代码:
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();
}
}
粘包只有在快速发送多个包的时候才会出现。
循环发送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
理想状态:服务端先接受一个package1,在接受一个package2,…,一次一个包一个包的接收,则这样无问题。
粘包的三种情况:
1、先读取到package1的部分数据,然后读取package1的余下数据和package2的全部数据;
2、先读取到package1的全部数据和package2的部分数据,然后读到package2的余下数据;
3、同时一次读取到package1和package2的全部数据。
很明显,我们这个Demo中,一次读取到了两包或者多包数据,属于这里面的第三种情况。
粘包原因:
1、客户端write()时发送的数据大于socket缓冲区的数据,导致需要多次write()才能把数据写入到缓冲去,这样导致read()数据的时候,一次read()的数据不完整。需要read()多次才能将数据读完;
2、发送端write()数据的速度太快,而接收端read()数据的太慢,导致接收方来不及接收每一包数据,造成了一次读取到多包数据的情况。我们这里也是属于这种情况。
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;
}
}
}
}
可以看到mRecvBuffer可以读出了单个的包。
当然这里只解决了粘包的第三种情况,这种情况byte buffer[] = new byte[1024];,读出的缓冲区尽量要大些,这里设为1024,远远比我发送的测试数据要大。所以不会产生第一种和第二种粘包的情况。
这种增加固定的协议,解析帧头和长度,应该是最为灵活的方法。