netty框架的學習筆記 + 一個netty實現websocket通信案例

一、前言

1.什麼是netty?

高性能,事件驅動,異步非堵塞
基於nio的客戶端,服務端編程框架(nio的框架)
穩定性和伸縮性

2.netty的使用場景。

高性能領域
多線程併發領域
異步通信領域

3.學習目錄

io通信
netty入門
websocket入門
netty實現websocket通信案例

二.java io通信

客戶端個數:bio(1:1) 僞異步io(m:n) nio(m:1) aio(m:0)
io類型:bio(阻塞同步io) 僞異步io(阻塞同步io) nio(非阻塞同步io) aio(非阻塞異步io)
api使用難度:bio(簡單) 僞異步io(簡單) nio(複雜) aio(複雜,但比nio簡單)
調試難度:同上
可靠性:bio(差) 僞異步io(差) nio(好) aio(好)
吞吐量:同上

三.netty入門

1.原生nio的缺陷。類庫和api複雜;入門門檻高;工作量和難度大;jdk nio存在bug
2.netty的優勢。api簡單;入門門檻低;性能高;成熟、穩定。

四.websocket入門

1.什麼是websocket?
h5協議規範;握手機制;解決客戶端與服務端實時通信而產生的技術。
2.websocket的優點?
節省通信開銷;服務器主動傳送數據給客戶端;實時通信;
3.websocket建立連接。
客戶端發起握手請求;服務端響應請求;連接建立。
4.websocket生命週期。
打開事件;消息事件;錯誤事件;關閉事件。
5.websocket關閉連接。
服務端關閉底層tcp連接;客戶端發起tcp close。

五.netty實現websocket通信案例。

1.功能介紹

netty開發服務端;html實現客戶端;實現服務端和客戶端的實時交互。

2.代碼實現

2.1存儲工廠的全局配置

package com.websocket.netty;

import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * @author lilinshen
 * @title 存儲工廠的全局配置
 * @description 請填寫相關描述
 * @date 2018/5/23 10:32
 */
public class NettyConfig {
    /**
     * 儲存每一個客戶端進來時的channel對象
     */
    public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}

2.2處理/接收/響應客戶端websocket請求的核心業務處理類

package com.websocket.netty;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;

import java.util.Date;

/**
 * @author lilinshen
 * @title 處理/接收/響應客戶端websocket請求的核心業務處理類
 * @description 請填寫相關描述
 * @date 2018/5/23 10:36
 */
public class MyWebSocketHandler extends SimpleChannelInboundHandler<Object> {

    private WebSocketServerHandshaker handshaker;
    private static final String WEB_SOCKET_URL = "ws://localhost:8888/websocket";

    /**
     * 客戶端與服務端連接的時候調用
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        NettyConfig.channelGroup.add(ctx.channel());
        System.out.println("客戶端與服務端連接開啓...");
    }

    /**
     * 客戶端與服務端斷開連接的時候調用
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        NettyConfig.channelGroup.remove(ctx.channel());
        System.out.println("客戶端與服務端連接關閉...");
    }

    /**
     * 服務端接收客戶端發送過來的數據結束之後調用
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    /**
     * 工程出現異常的時候調用
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 服務端處理客戶端websocket請求的核心方法
     *
     * @param channelHandlerContext
     * @param o
     * @throws Exception
     */
    @Override
    protected void messageReceived(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
        if (o instanceof FullHttpRequest) {
            // 處理客戶端向服務端發起http握手請求的業務
            handHttpRequest(channelHandlerContext, (FullHttpRequest) o);
        } else if (o instanceof WebSocketFrame) {
            // 處理websocket連接業務
            handWebsocketFrame(channelHandlerContext, (WebSocketFrame) o);
        }
    }

    /**
     * 處理客戶端向服務端發起http握手請求的業務
     *
     * @param ctx
     * @param request
     */
    private void handHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
        if (!request.getDecoderResult().isSuccess() || !("websocket").equals(request.headers().get("Upgrade"))) {
            sendHttpResponse(ctx, request, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(WEB_SOCKET_URL, null, false);
        handshaker = wsFactory.newHandshaker(request);
        if (null == handshaker) {
            WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), request);
        }
    }

    /**
     * 服務端向客戶端響應消息
     *
     * @param ctx
     * @param request
     * @param response
     */
    private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, DefaultFullHttpResponse response) {
        if (response.getStatus().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(response.getStatus().toString(), CharsetUtil.UTF_8);
            response.content().writeBytes(buf);
            buf.release();
        }
        // 服務端向客戶端發送數據
        ChannelFuture channelFuture = ctx.channel().writeAndFlush(response);
        if (response.getStatus().code() != 200) {
            channelFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

    /**
     * 處理客戶端與服務端之間的websocket業務
     *
     * @param ctx
     * @param frame
     */
    private void handWebsocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 判斷是否是關閉websocket的指令
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), ((CloseWebSocketFrame) frame).retain());
        }
        // 判斷是否是ping的消息
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        // 判斷是否是二進制消息,如果是二進制消息,拋出異常
        if (!(frame instanceof TextWebSocketFrame)) {
            System.out.println("目前我們不支持二進制消息...");
            throw new RuntimeException("" + this.getClass().getName() + "】不支持消息...");
        }
        // 返回應答消息
        // 獲取客戶端向服務端發送的消息
        String request = ((TextWebSocketFrame) frame).text();
        System.out.println("===>>>" + request);
        TextWebSocketFrame tws = new TextWebSocketFrame(new Date().toString() + ctx.channel().id() + "===>>>" + request);
        // 羣發,服務端向每個連接上來的客戶端發消息
        NettyConfig.channelGroup.writeAndFlush(tws);
    }
}

3.初始化連接時的各個組件

package com.websocket.netty;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;

/**
 * @author lilinshen
 * @title 初始化連接時的各個組件
 * @description 請填寫相關描述
 * @date 2018/5/23 11:12
 */
public class MyWebSocketChannelHander extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast("http-codec", new HttpServerCodec());
        socketChannel.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
        socketChannel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
        socketChannel.pipeline().addLast("handler", new MyWebSocketHandler());
    }
}

4.程序的入口,負責啓動應用

package com.websocket.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * @author lilinshen
 * @title 程序的入口,負責啓動應用
 * @description 請填寫相關描述
 * @date 2018/5/23 11:17
 */
public class Main {
    public static void main(String[] args) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workGroup);
            bootstrap.channel(NioServerSocketChannel.class);
            bootstrap.childHandler(new MyWebSocketChannelHander());
            System.out.println("服務端開啓等待客戶端連接...");
            Channel channel = bootstrap.bind(8888).sync().channel();
            channel.closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 優雅的退出程序
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

5.websocket.html客戶端代碼。

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
    <title>websocket客戶端</title>
    <script type="text/javascript">
        var socket;
        if (!window.WebSocket) {
            window.WebSocket = window.MozWebSocket;
        }
        if (window.WebSocket) {
            socket = new WebSocket("ws://localhost:8888/websocket");
            socket.onmessage = function (ev) {
                var ta = document.getElementById("responseContent");
                ta.value += ev.data + "\r\n";
            }
            socket.onopen = function (ev) {
                var ta = document.getElementById("responseContent");
                ta.value += "您當前的瀏覽器支持websocket,請進行後續操作\r\n";
            }
            socket.onclose = function (ev) {
                var ta = document.getElementById("responseContent");
                ta.value = "";
                ta.value = "websocket連接已經關閉\r\n";
            }
        } else {
            alert("您的瀏覽器不支持websocket");
        }
        function send(message) {
            if (!window.WebSocket) {
                return;
            }
            if (socket.readyState == WebSocket.OPEN) {
                socket.send(message);
            } else {
                alert("websocket連接沒有建立成功");
            }
        }
    </script>
</head>
<body>
<form onsubmit="return false;">
    <input type="text" name="message" value=""/>
    <br/><br/>
    <input onclick="send(this.form.message.value)" type="button" value="發送websocket請求消息"/>
    <hr color="red"/>
    <h2>客戶端接收到服務端返回的應答消息</h2>
    <textarea id="responseContent" style="width:1024px;height:300px;"></textarea>
</form>
</body>
</html>

6.啓動。

1.main.java類是程序的入口,負責啓動應用。
2.將websocket.html在瀏覽器中打開,就可以建立一個websocket連接。

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