一、Netty解决TCP协议数据分包问题思路
我们知道通过TCP协议发送接收数据时,如果数据过大,接收到的数据会是分包的,比如:
+-----+-----+-----+
发送数据是: | ABC | DEF | GHI |
+-----+-----+-----+
而我们想接受到的数据是: | ABCDEFGHI |
该如何处理这种情况呢?Netty提供了一个专门处理TCP协议数据的Handler:LengthFieldBasedFrameDecoder ,它的原理是服务器端和客户端约定一个协议格式:数据包=协议长度+协议体
--------------------------------数据包------------------------------
| 协议长度部分(接收数据长度) | 协议体部分(要接收的数据)|
举个例子,假如我们的TCP客户端发送了10MB字节的数据,如何让Netty服务器一次就接收到这10MB数据呢?那就需要客户端告诉服务端我发送的数据大小是多少,即在发送的数据中加入一个“数据包长度”即可,上面提到的Handler就是用来和客户端约定这个协议格式的,它有几个参数,下面我介绍一下它的参数意义:
int maxFrameLength:定义接收数据包的最大长度,如果发送的数据包超过此值,则抛出异常;
int lengthFieldOffset:长度属性部分的偏移值,0表示长度属性位于数据包头部;
int lengthFieldLength:长度属性的字节长度,如果设置为4,就是我们用4个字节存放数据包的长度;
int lengthAdjustment:协议体长度调节值,修正信息长度,如果设置为4,那么解码时再向后推4个字节;
int initialBytesToStrip:跳过字节数,如我们想跳过长度属性部分。
二、实例-客户端发送10MB字节的数据,Netty服务端一次接收到全部10MB数据
客户端:定义一个消息体,用头部四个字节存放数据包长度
public byte[] send(byte[] sendData) throws UnknownHostException, IOException {
Socket socket = new Socket(serverIp, serverPort);
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream();
byte resultArray[] = null;
try {
// 定义一个发送消息协议格式:|--header:4 byte--|--content:10MB--|
// 获取一个4字节长度的协议体头
byte[] dataLength = intToByteArray(4, sendData.length);
// 和请求的数据组成一个请求数据包
byte[] requestMessage = combineByteArray(dataLength, sendData);
//发送数据-------------------------------
os.write(requestMessage);
os.flush();
//接收数据-------------------------------
resultArray = IOUtils.toByteArray(is);
} catch (Exception e) {
e.printStackTrace();
} finally {
os.close();
is.close();
socket.close();
}
return resultArray;
}
private static byte[] intToByteArray(int byteLength, int intValue) {
return ByteBuffer.allocate(byteLength).putInt(intValue).array();
}
private static byte[] combineByteArray(byte[] array1, byte[] array2) {
byte[] combined = new byte[array1.length + array2.length];
System.arraycopy(array1, 0, combined, 0, array1.length);
System.arraycopy(array2, 0, combined, array1.length, array2.length);
return combined;
}
Netty服务端:定义一个LengthFieldBasedFrameDecoder(1024*1024*1024, 0, 4,0,4)),最大数据量是1GB,长度属性位于数据包头部,占4个字节,协议体调节值为0,跳过头部协议长度四个字节
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("framedecoder",new LengthFieldBasedFrameDecoder(1024*1024*1024, 0, 4,0,4));
pipeline.addLast(new TCPServiceHandler());// 处理业务Handler
}
三、总结:客户端和服务端定义消息格式必须一致
————————————————
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
public class ServerDemo {
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(8888);
System.out.println("启动服务器....");
Socket s = ss.accept();
System.out.println("客户端:" + s.getInetAddress().getLocalHost() + "已连接到服务器");
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
//读取客户端发送来的消息
String mess = br.readLine();
System.out.println("客户端:" + mess);
StringBuffer bufdata = new StringBuffer();
for (int i = 0; i < 53 * 10000; i++) {
bufdata.append(1);
}
System.out.println("客户端:" + bufdata.toString().getBytes().length);
while (true) {
byte[] sendData = bufdata.toString().getBytes();
byte[] dataLength = intToByteArray(4, sendData.length);
byte[] requestMessage = combineByteArray(dataLength, sendData);
OutputStream os = s.getOutputStream();
//发送数据-------------------------------
os.write(requestMessage);
os.flush();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static byte[] intToByteArray(int byteLength, int intValue) {
return ByteBuffer.allocate(byteLength).putInt(intValue).array();
}
private static byte[] combineByteArray(byte[] array1, byte[] array2) {
byte[] combined = new byte[array1.length + array2.length];
System.arraycopy(array1, 0, combined, 0, array1.length);
System.arraycopy(array2, 0, combined, array1.length, array2.length);
return combined;
}
}
import android.os.SystemClock;
import android.util.Log;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.util.CharsetUtil;
public class NettyClient {
private static final String TAG = "NettyClient";
//Bootstrap参数
private EventLoopGroup group;
//写的接口用来接收服务端返回的值
private NettyListener listener;
//通过对象发送数据到服务端
private Channel channel;
//判断是否连接了
private boolean isConnect = false;
//定义的重连到时候用
private static int reconnectNum = Integer.MAX_VALUE;
//是否需要重连
private boolean isNeedReconnect = true;
//是否正在连接
private boolean isConnecting = false;
//重连的时间
private long reconnectIntervalTime = 5000;
public String host;//ip
public int tcp_port;//端口
/**
* 构造 传入 ip和端口
*/
public NettyClient(String host, int tcp_port) {
this.host = host;
this.tcp_port = tcp_port;
}
/**
* 连接方法
*/
public void connect() {
if (isConnecting) {
return;
}
//起个线程
Thread clientThread = new Thread("client-Netty") {
@Override
public void run() {
super.run();
isNeedReconnect = true;
reconnectNum = Integer.MAX_VALUE;
connectServer();
}
};
clientThread.start();
}
//连接时的具体参数设置
private void connectServer() {
synchronized (NettyClient.this) {
ChannelFuture channelFuture = null;//连接管理对象
if (!isConnect) {
isConnecting = true;
group = new NioEventLoopGroup();//设置的连接group
Bootstrap bootstrap = new Bootstrap().group(group)//设置的一系列连接参数操作等
.option(ChannelOption.TCP_NODELAY, true)//屏蔽Nagle算法试图
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(655350))
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() { // 5
@Override
public void initChannel(SocketChannel ch) throws Exception {
// 定义一个发送消息协议格式:|--header:4 byte--|--content:10MB--|
ch.pipeline().addLast("framedecoder",new LengthFieldBasedFrameDecoder(1024*1024*1024, 0, 4,0,4));
ch.pipeline().addLast(new NettyClientHandler(listener));//需要的handlerAdapter
}
});
try {
//连接监听
channelFuture = bootstrap.connect(host, tcp_port).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()) {
Log.e(TAG, "连接成功");
isConnect = true;
channel = channelFuture.channel();
} else {
Log.e(TAG, "连接失败");
isConnect = false;
}
isConnecting = false;
}
}).sync();
// 等待连接关闭
channelFuture.channel().closeFuture().sync();
Log.e(TAG, " 断开连接");
} catch (Exception e) {
e.printStackTrace();
} finally {
isConnect = false;
//STATUS_CONNECT_CLOSED 这我自己定义的 接口标识
listener.onServiceStatusConnectChanged(NettyListener.STATUS_CONNECT_CLOSED);
if (null != channelFuture) {
if (channelFuture.channel() != null && channelFuture.channel().isOpen()) {
channelFuture.channel().close();
}
}
group.shutdownGracefully();
reconnect();//重新连接
}
}
}
}
//断开连接
public void disconnect() {
Log.e(TAG, "disconnect");
isNeedReconnect = false;
group.shutdownGracefully();
}
//重新连接
public void reconnect() {
Log.e(TAG, "reconnect");
if (isNeedReconnect && reconnectNum > 0 && !isConnect) {
reconnectNum--;
SystemClock.sleep(reconnectIntervalTime);
if (isNeedReconnect && reconnectNum > 0 && !isConnect) {
Log.e(TAG, "重新连接");
connectServer();
}
}
}
//发送消息到服务端。 Bootstrap设置的时候我没有设置解码,这边才转的
public boolean sendMsgToServer(String data, ChannelFutureListener listener) {
boolean flag = channel != null && isConnect;
if (flag) {
ByteBuf byteBuf = Unpooled.copiedBuffer(data + System.getProperty("line.separator"),
CharsetUtil.UTF_8);
channel.writeAndFlush(byteBuf).addListener(listener);
}
return flag;
}
//重连时间
public void setReconnectNum(int reconnectNum) {
this.reconnectNum = reconnectNum;
}
public void setReconnectIntervalTime(long reconnectIntervalTime) {
this.reconnectIntervalTime = reconnectIntervalTime;
}
//现在连接的状态
public boolean getConnectStatus() {
return isConnect;
}
public boolean isConnecting() {
return isConnecting;
}
public void setConnectStatus(boolean status) {
this.isConnect = status;
}
public void setListener(NettyListener listener) {
this.listener = listener;
}
}
import android.util.Log;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
private static final String TAG = "NettyClientHandler";
private NettyListener listener;
public NettyClientHandler(NettyListener listener) {
this.listener = listener;
}
//每次给服务器发送的东西, 让服务器知道我们在连接中哎
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.WRITER_IDLE) {
ctx.channel().writeAndFlush("Heartbeat" + System.getProperty("line.separator"));
}
}
}
/**
* 连接成功
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Log.e(TAG, "channelActive");
super.channelActive(ctx);
listener.onServiceStatusConnectChanged(NettyListener.STATUS_CONNECT_SUCCESS);
}
//channelActive 事件当连接建立的时候会触发
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
Log.e(TAG, "channelInactive");
}
//接收消息的地方, 接口调用返回到activity了
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
listener.onMessageResponse(msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 当引发异常时关闭连接。
Log.e(TAG, "exceptionCaught" + cause.getMessage());
listener.onServiceStatusConnectChanged(NettyListener.STATUS_CONNECT_ERROR);
cause.printStackTrace();
ctx.close();
}
}
public interface NettyListener {
//连接成功
public final static byte STATUS_CONNECT_SUCCESS = 1;
//关闭连接
public final static byte STATUS_CONNECT_CLOSED = 0;
//连接失败
public final static byte STATUS_CONNECT_ERROR = 0;
/**
* 当接收到系统消息
*/
void onMessageResponse(Object msg);
/**
* 当连接状态发生变化时调用
*/
public void onServiceStatusConnectChanged(int statusCode);
}
//调用方式
private void initSocketTcp() {
nettyClient = new NettyClient("192.168.137.1", 8888);
if (!nettyClient.getConnectStatus()) {
nettyClient.setListener(WindowUiService.this);
nettyClient.connect();
} else {
nettyClient.disconnect();
}
}