Netty TCP通訊拆包、粘包處理

tcp是個流協議,所謂流,就是沒有界限的一串數據。tcp底層並不瞭解上層業務的具體含義,它會根據tcp緩衝區的實際情況進行包的劃分,所以在業務上認爲,一個完整的包可能會被tcp拆分爲多個包進行發送,也有可能把多個小的包封裝成一個大的數據包發送。這就是所謂的tcp拆包/粘包問題。

拆包:通常是由於發送較大長度數的據超出了自定義長度,或者超出了相關網絡傳輸協議的長度限制,發送的一包數據被拆分爲多次發送。

粘包:由於前後包之間的發送間隔過短,造成接收端將多條數據當做同一包數據讀取出來。例子如下,

channel.writeAndFlush(sMsg);
channel.writeAndFlush(sMsg);
channel.writeAndFlush(sMsg);

連續多個發送,其實是發送了多個包,對方應該把其看成是多個消息。但是因爲發送的過快,對方几乎一定會把其當作一個包來處理。看成是發送了一條消息。這個就發生了粘包。

netty中解決方案:(注意事項請看文章最後)

1)LineBasedFrameDecoder行分割解碼

SocketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024);

SocketChannel.pipeline().addLast(new StringDecoder());

LineBaseFrameDecoder的工作原理是它依次遍歷ByteBuf中的可讀字節,判斷看是否有"\n"或者"\r\n",如果有,就以此位置爲結束位置,從可讀索引到結束位置區間的字節就組成了一行。它是以換行符爲結束標誌的解碼器,支持攜帶結束符或者不攜帶結束符兩種解碼方式,同時支持配置單行的最大長度。如果連續取到最大長度後仍然沒有發現換行符,就會拋出異常,同時忽略掉之前讀到的異常碼流。

StringDecoder的功能非常簡單,就是將接受到的對象轉換成字符串,然後繼續調用後面的Handler。


LineBasedFrameDecoder+StringDecoder的組合就是按行切換的文本解碼器,它被設計用來支持TCP的粘包和拆包。

 

2)DelimiterBasedFrameDecoder自定義分隔符

// 創建分隔符緩衝對象$_作爲分割符
ByteBuf byteBuf = Unpooled.copiedBuffer("$_".getBytes());
/**
 * 第一個參數:單條消息的最大長度,當達到最大長度仍然找不到分隔符拋異常,防止由於異常碼流缺失分隔符號導致的內存溢出
 * 第二個參數:分隔符緩衝對象
 */
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,byteBuf));
socketChannel.pipeline().addLast(new StringDecoder());

DelimiterBasedFrameDecoder還可以設置對自定義分割付的處理,如下:

ByteBuf delemiter= Unpooled.buffer();
delemiter.writeBytes("$##$".getBytes());//自定義分隔符
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(65535, true, true,delemiter));

//netty實現
DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, boolean failFast,ByteBuf delimiter)

maxLength:
表示一行最大的長度,如果超過這個長度依然沒有檢測到\n或者\r\n,將會拋出TooLongFrameException

failFast:
與maxLength聯合使用,表示超過maxLength後,拋出TooLongFrameException的時機。如果爲true,則超出maxLength後立即拋出TooLongFrameException,不繼續進行解碼;如果爲false,則等到完整的消息被解碼後,再拋出TooLongFrameException異常。

stripDelimiter:
解碼後的消息是否去除分隔符。

delimiters:
分隔符。我們需要先將分割符,寫入到ByteBuf中,然後當做參數傳入。

需要注意的是,netty並沒有提供一個DelimiterBasedFrameDecoder對應的編碼器實現(筆者沒有找到),因此在發送端需要自行編碼,添加分隔符。

 

3)FixedLengthFrameDecoder定長,即發送接受固定長度的包。感覺不大適合我的業務,暫時不考慮使用。

 

注意事項:

1、編碼格式的設置

//字符串編解碼器獲取環境默認編碼格式
pipeline.addLast(
    new StringDecoder(),
    new StringEncoder()
);

 //指定字符串編解碼器編碼格式爲UTF-8
pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));

2、自定義分隔符和解碼的添加順序是,先添加自定義解碼器,然後再添加StringDecoder,否則分割無效。

//先使用DelimiterBasedFrameDecoder解碼,以自定義的字符作爲分割符
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(65535, true, true,delemiter));
//解碼爲UTF-8字符串
socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));

 

實際開發代碼示例:

以下示例時結合業務需求寫的,有些地方不需要,請自行刪除,僅供參考。

package com.groot.CPMasterController.netty.tcp.server;

import com.groot.CPMasterController.netty.tcp.entity.TCPConst;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.concurrent.Future;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * description:
 * author:groot
 * date: 2019-4-10 12:07
 **/
@Component
@PropertySource(value="classpath:config.properties")
@Slf4j
public class TcpServer {

    //boss事件輪詢線程組
    //處理Accept連接事件的線程,這裏線程數設置爲1即可,netty處理鏈接事件默認爲單線程,過度設置反而浪費cpu資源
    private EventLoopGroup boss = new NioEventLoopGroup(1);
    //worker事件輪詢線程組
    //處理hadnler的工作線程,其實也就是處理IO讀寫 。線程數據默認爲 CPU 核心數乘以2
    private EventLoopGroup worker = new NioEventLoopGroup();

    @Autowired
    TCPServerChannelInitializer TCPServerChannelInitializer;

    @Value("${netty.tcp.server.port}")
    private Integer port;

    //與客戶端建立連接後得到的通道對象
    private Channel channel;

    /**
     * @Author groot
     * @Date 2019/4/13 12:46
     * @Param
     * @return
     * @Description 存儲所有client的channel
     **/

//    public static Map<String, Channel> clientTotalMap = new ConcurrentHashMap<String, Channel>();

    /**
     * @Author groot
     * @Date 2019/4/13 12:46
     * @Param key 鏈接身份,Value channel隊列
     * @return
     * @Description 分類型存儲業務所需channel
     **/
    public static Map<String, Set<Channel>> clientTypeMap = new ConcurrentHashMap<>();

    /**
     * @Author groot
     * @Date 2019/4/13 12:46
     * @Param []
     * @return io.netty.channel.ChannelFuture
     * @Description 開啓Netty tcp server服務
     **/

    public void start() {
        try {
            //啓動類
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boss, worker)//組配置,初始化ServerBootstrap的線程組
                    .channel(NioServerSocketChannel.class)///構造channel通道工廠//bossGroup的通道,只是負責連接
                    .childHandler(TCPServerChannelInitializer)//設置通道處理者ChannelHandler////workerGroup的處理器
                    .option(ChannelOption.SO_BACKLOG, 1024)//socket參數,當服務器請求處理程全滿時,用於臨時存放已完成三次握手請求的隊列的最大長度。如果未設置或所設置的值小於1,Java將使用默認值50。
                    .childOption(ChannelOption.SO_KEEPALIVE, true)//啓用心跳保活機制,tcp,默認2小時發一次心跳
                    .childOption(ChannelOption.TCP_NODELAY, true)//2019年4月15日新增 TCP無延遲
                    .handler(new LoggingHandler(LogLevel.INFO));//2019年4月15日新增  日誌級別info
            //Future:異步任務的生命週期,可用來獲取任務結果
//        ChannelFuture channelFuture1 = serverBootstrap.bind(port).syncUninterruptibly();//綁定端口,開啓監聽,同步等待
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();//綁定端口,開啓監聽,同步等待
            if (channelFuture != null && channelFuture.isSuccess()) {
                channel = channelFuture.channel();//獲取通道
                log.info("Netty tcp server start success, port = {}", port);
            } else {
                log.error("Netty tcp server start fail");
            }
            channelFuture.channel().closeFuture().sync();// 監聽服務器關閉監聽
        } catch (InterruptedException e) {
            log.error("Netty tcp server start Exception e:"+e);
        }finally {
            boss.shutdownGracefully(); //關閉EventLoopGroup,釋放掉所有資源包括創建的線程
            worker.shutdownGracefully();
        }

    }

    /**
     * @Author groot
     * @Date 2019/4/13 12:46
     * @Param []
     * @return void
     * @Description 停止Netty tcp server服務
     **/
    @PreDestroy
    public void destroy() {
        if (channel != null) {
            channel.close();
        }
        try {
            Future<?> future = worker.shutdownGracefully().await();
            if (!future.isSuccess()) {
                log.error("netty tcp workerGroup shutdown fail, {}", future.cause());
            }
            Future<?> future1 = boss.shutdownGracefully().await();
            if (!future1.isSuccess()) {
                log.error("netty tcp bossGroup shutdown fail, {}", future1.cause());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("Netty tcp server shutdown success");
    }


    /**
     * @Author groot
     * @Date 2019/5/8 14:34
     * @Param [identity, msg] 鏈接身份,消息
     * @return void
     * @Description 通過
     **/
    public static void sendMsg(String identity,String msg) {
        send(identity, msg,true);
    }

    /**
     * @Author groot
     * @Date 2019/5/8 14:34
     * @Param [identity, msg] 鏈接身份,消息
     * @return void
     * @Description 通過
     **/
    public static void sendHeart(String identity,String msg) {
        send(identity, msg,false);
    }


    /**
     * @Author groot
     * @Date 2019/5/17 15:38
     * @Param [identity, msg,endFlag] endFlag是否添加結束符
     * @return void
     * @Description 發送
     **/
    public static void send(String identity, String msg,boolean endFlag) {
        //log.info("sendMsg to:{},msg:{}",identity,msg);
        if(StringUtils.isEmpty(identity) || StringUtils.isEmpty(msg))return;
        StringBuffer sMsg = new StringBuffer(msg);
        if(endFlag){
            sMsg.append(TCPConst.MARK_END);//拼接消息截止符
        }
        Set<Channel> channels = TcpServer.clientTypeMap.get(identity);
        if(channels!=null && !channels.isEmpty()){//如果有client鏈接
            //遍歷發送消息
            for (Channel channel:channels){
                channel.writeAndFlush(sMsg).syncUninterruptibly();
            }
        }
    }

    // 這個註解表示在spring boot依賴注入完成後執行一次該方法,但對方法有很嚴格的要求
    @PostConstruct()
    public void init() {
        //需要開啓一個新的線程來執行netty server 服務器
        new Thread(new Runnable() {
            public void run() {
                start();
            }
        }).start();

    }

}

 

package com.groot.CPMasterController.netty.tcp.server;

import com.alibaba.fastjson.JSON;
import com.groot.CPMasterController.common.utils.TimeUtil;
import com.groot.CPMasterController.control.service.ipml.GameControlService;
import com.groot.CPMasterController.netty.tcp.entity.TCPConst;
import com.groot.CPMasterController.netty.websocket.WebSocketServer;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.net.SocketAddress;
import java.util.*;

/**
 * description:
 * author:groot
 * date: 2019-4-10 15:49
 **/
@Component
@ChannelHandler.Sharable
@Slf4j
public class TCPServerChannelHandler extends SimpleChannelInboundHandler<Object> {

    @Resource
    GameControlService gameControlService;

    //msg拼包
    private StringBuffer msgBuffer = new StringBuffer();

    /**
     * 拿到傳過來的msg數據,開始處理
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("Netty tcp server receive data : " + msg);
        //tcp監聽收到消息,解析消息內容
        try {
            if(null==msg)return;
            String sMsg = msg.toString();
            if (TCPConst.MARK_HEART.equals(sMsg)){//心跳
                //TODO 心跳暫時無需處理
//                log.info("Netty tcp server receive heart : " + msg);
            }else{
//                spellPackage(ctx, sMsg);//手動解決拆包粘包
                //netty自定義分隔符解析
                dealMsg(ctx,sMsg);
            }
        } catch (Exception e) {
            log.error("Netty tcp server channelRead0 Exception e:{}",e);
        }

    }

    /**
     * @Author groot
     * @Date 2019/5/17 9:21
     * @Param [ctx, msg]
     * @return void
     * @Description TCP拼包
     **/
    private void spellPackage(ChannelHandlerContext ctx, String msg) {
        if(StringUtils.isNotBlank(msg)){
            synchronized (msgBuffer) {
                StringBuffer msgbf = new StringBuffer(msg);
                String sMsg = msgBuffer.append(msgbf).toString();
                if(sMsg.contains(TCPConst.MARK_HEART)){//包含心跳
                    sMsg=sMsg.replaceAll(TCPConst.MARK_HEART,"");//去心跳
                }
                if(sMsg.contains(TCPConst.MARK_END)){//包含結束符
                    msgBuffer.setLength(0);//清空緩存 需要重新拼接
                    String[] splitMsg = sMsg.trim().split(TCPConst.MARK_END);
                    int arrLen = splitMsg.length;
                    if(arrLen ==1){
                        dealMsg(ctx,splitMsg[0]);
                    }else if(arrLen >1){
                        for(int i = 0; i< arrLen; i++){
                            if(i==0){
                                dealMsg(ctx,splitMsg[0]);
                            }else if(i==arrLen-1){
                                if(msg.endsWith(TCPConst.MARK_END)) {//最後一條是完整的
                                    dealMsg(ctx,splitMsg[i]);
                                }else {//最後一條結尾不是結束符 繼續拼接
                                    msgBuffer.append(splitMsg[i]);//只拼接最後一條不執行
                                }
                            }else {
                                dealMsg(ctx,splitMsg[i]);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * @Author groot
     * @Date 2019/5/16 17:23
     * @Param [ctx, msg]
     * @return void
     * @Description 處理消息
     **/
    private void dealMsg(ChannelHandlerContext ctx,String msg) {
        log.info("dealMsg :::{}",msg);
        gameControlService.dealMassage( ctx,  msg);
    }

    /**
     * 活躍的、有效的通道
     * 第一次連接成功後進入的方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        log.info("tcp client " + getRemoteAddress(ctx) + " connect success");
    }

    /**
     * 不活動的通道
     * 連接丟失後執行的方法(client端可據此實現斷線重連)
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //刪除Channel Map中的失效Client
        log.error("檢測到不活躍的通道-- ip:{} ,即將刪除", getRemoteAddress(ctx));
        removeChannel(ctx.channel());//安全刪除channel
        ctx.close();
    }

    /**
     * 異常處理
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        //發生異常,關閉連接
        log.error(" ip:{} -- 的通道發生異常,即將斷開連接", getRemoteAddress(ctx));
        removeChannel(ctx.channel());//安全刪除channel
        ctx.close();//再次建議close  內部解析的錯誤已經在channelRead0中捕獲 所以這裏的異常 應該是在連接出現異常時出現
    }


    /**
     * 心跳機制,超時處理
     *
     * @param ctx
     * @param evt
     * @throws Exception
     */
    /*@Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        String socketString = ctx.channel().remoteAddress().toString();
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                log.info("Client: " + socketString + " READER_IDLE 讀超時");
                ctx.disconnect();//斷開
            } else if (event.state() == IdleState.WRITER_IDLE) {
                log.info("Client: " + socketString + " WRITER_IDLE 寫超時");
                ctx.disconnect();
            } else if (event.state() == IdleState.ALL_IDLE) {
                log.info("Client: " + socketString + " ALL_IDLE 總超時");
                ctx.disconnect();
            }
        }
    }*/

    /**
     * @Author groot
     * @Date  15:07
     * @Param [id, channel]
     * @return void
     * @Description 添加業務channel
     **/
    public static void addChannel(String id,Channel channel){
        synchronized (TcpServer.clientTypeMap) {
            if(TcpServer.clientTypeMap.get(id)!=null){
                TcpServer.clientTypeMap.get(id).add(channel);
            }else {
                Set<Channel> channels = new HashSet<>();
                channels.add(channel);
                TcpServer.clientTypeMap.put(id,channels);
            }
        }
        log.info("addChannel -- 業務channel size:{}",getNumberOfClients());
    }


    /**
     * @Author groot
     * @Date 2019/5/8 13:34
     * @Param [channel]
     * @return void
     * @Description 安全移除業務channel
     **/
    private void removeChannel(Channel channel){
        synchronized (TcpServer.clientTypeMap) {
            if(!TcpServer.clientTypeMap.isEmpty()){
                Set<String> keys = TcpServer.clientTypeMap.keySet();
                for (String key:keys){
                    Set<Channel> channels = TcpServer.clientTypeMap.get(key);
                    //判斷包含channel
                    if(channels!=null && channels.contains(channel)){
                        //獲取ip
                        SocketAddress socketAddress = channel.remoteAddress();
                        //刪除channel
                        channels.remove(channel);
                        //獲取通知web端
                        String clientName="";
                        switch (key){
                            case TCPConst.ID_CPTIMER:
                                clientName =TCPConst.ID_CPTIMER_NAME;
                                break;
                            case TCPConst.ID_DEVCTRL:
                                clientName =TCPConst.ID_DEVCTRL_NAME;
                                break;
                            case TCPConst.ID_GPSCTRL:
                                clientName =TCPConst.ID_GPSCTRL_NAME;
                                break;
                            default:
                                clientName =TCPConst.ID_UNKNOWN_NAME;
                                break;
                        }
                        warnMsgToWeb(socketAddress.toString().replaceAll("/",""), clientName);
                    }
                }
            }
        }
    }

    /**
     * @Author groot
     * @Date 2019/5/17 13:43
     * @Param [socketAddress, clientName]
     * @return void
     * @Description 提示web連接斷開
     **/
    private void warnMsgToWeb(String ip, String clientName) {
        Map<String,Object> map=new HashMap<>();
        map.put("type","warn");
        map.put("name",clientName);
        map.put("ip",ip);
        map.put("msg","斷開連接");
        map.put("time", TimeUtil.dateFormat(new Date()));
        WebSocketServer.sendInfo(JSON.toJSONString(map),null);
    }

    /**
     * @Author groot
     * @Date 2019/5/8 14:00
     * @Param []
     * @return int
     * @Description 獲取當前channel連接數
     **/
    public static int getNumberOfClients(){
        int count = 0;
        if(TcpServer.clientTypeMap!=null && !TcpServer.clientTypeMap.isEmpty()){
            for (Set<Channel> channels: TcpServer.clientTypeMap.values()) count += channels.size();
        }
        return count;
    }

    /**
     * 獲取client對象:ip+port
     *
     * @param ctx
     * @return
     */
    public String getRemoteAddress(ChannelHandlerContext ctx) {
        String socketString = ctx.channel().remoteAddress().toString();
        return socketString;
    }

    /**
     * 獲取client的ip
     *
     * @param ctx
     * @return
     */
    public String getIPString(ChannelHandlerContext ctx) {
        String socketString = ctx.channel().remoteAddress().toString();
        int colonAt = socketString.indexOf(":");
        String ipString = socketString.substring(1, colonAt);
        return ipString;
    }
}
package com.groot.CPMasterController.netty.tcp.server;

import com.groot.CPMasterController.netty.tcp.entity.TCPConst;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * description: 通道初始化,主要用於設置各種Handler
 * author:groot
 * date: 2019-4-10 14:55
 **/
@Component
public class TCPServerChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Autowired
    TCPServerChannelHandler TCPServerChannelHandler;

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
//        ChannelPipeline pipeline = socketChannel.pipeline();
        //IdleStateHandler心跳機制,如果超時觸發Handle中userEventTrigger()方法
//        pipeline.addLast("idleStateHandler",new IdleStateHandler(15, 0, 0, TimeUnit.MINUTES));
        //字符串編解碼器獲取環境默認編碼格式 ,如UTF-8
//        pipeline.addLast(
//                new StringDecoder(),
//                new StringEncoder()
//        );
        //指定字符串編解碼器編碼格式爲UTF-8
//        pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
//        pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));

        ByteBuf delemiter= Unpooled.buffer();
        delemiter.writeBytes(TCPConst.MARK_END.getBytes());//自定義分隔符
        /**
         * 第一個參數:單條消息的最大長度,當達到最大長度仍然找不到分隔符拋異常,防止由於異常碼流缺失分隔符號導致的內存溢出
         * 第二個參數:分隔符緩衝對象
         */
        //先使用DelimiterBasedFrameDecoder解碼,以自定義的字符作爲分割符
        socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(65535, true, true,delemiter));
        //解碼爲UTF-8字符串
        socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
        //編碼爲UTF-8字符串
        socketChannel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));

        //自定義Handler
        socketChannel.pipeline().addLast("serverChannelHandler", TCPServerChannelHandler);
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章