Netty 处理 TCP协议数据分包问题,大数据传输

一、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();
        }
    }

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值