1. 程式人生 > >Netty應用例項學習

Netty應用例項學習

Netty 是一個 Java NIO 客戶端伺服器框架,使用它可以快速簡單地開發網路應用程式,比如伺服器和客戶端的協議。Netty 大大簡化了網路程式的開發過程比如 TCP 和 UDP 的 socket 服務的開發。

本文旨在通過例項學習Netty的一些用法。

【1】Netty 實現聊天功能

①SimpleChatServerHandler 服務端處理器

handler 是由 Netty 生成用來處理 I/O 事件的,SimpleChatServerHandler例項如下:

package com.nett.chat.handler;

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

/**
 * Created by Janus on 2018/11/20.
 */
public class SimpleChatServerHandler extends SimpleChannelInboundHandler<String> { // (1)

    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception { // (2)
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + " 加入\n");
        }
        channels.add(ctx.channel());
    }
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // (3)
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + " 離開\n");
        }
        channels.remove(ctx.channel());
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception { // (4)
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            if (channel != incoming){
                channel.writeAndFlush("[" + incoming.remoteAddress() + "]" + s + "\n");
            } else {
                channel.writeAndFlush("[you]" + s + "\n");
            }
        }
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception { // (5)
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient:"+incoming.remoteAddress()+"線上");
    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception { // (6)
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient:"+incoming.remoteAddress()+"掉線");
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (7)
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient:"+incoming.remoteAddress()+"異常");
        // 當出現異常就關閉連線
        cause.printStackTrace();
        ctx.close();
    }
}

① SimpleChatServerHandler 繼承自SimpleChannelInboundHandler,這個類實現了ChannelInboundHandler介面,ChannelInboundHandler 提供了許多事件處理的介面方法,然後你可以覆蓋這些方法。現在僅僅只需要繼承 SimpleChannelInboundHandler 類而不是你自己去實現介面方法。

SimpleChannelInboundHandler繼承示意圖如下(ctrl+shift+alt+u IDEA下檢視):
在這裡插入圖片描述

② 覆蓋了 handlerAdded() 事件處理方法。每當從服務端收到新的客戶端連線時,客戶端的 Channel 存入ChannelGroup列表中,並通知列表中的其他客戶端 Channel。

③ 覆蓋了 handlerRemoved() 事件處理方法。每當從服務端收到客戶端斷開時,客戶端的 Channel 移除 ChannelGroup 列表中,並通知列表中的其他客戶端 Channel。

④ 覆蓋了 channelRead0() 事件處理方法。每當從服務端讀到客戶端寫入資訊時,將資訊轉發給其他客戶端的 Channel。其中如果你使用的是 Netty 5.x 版本時,需要把 channelRead0() 重新命名為messageReceived()。

⑤ 覆蓋了 channelActive() 事件處理方法。服務端監聽到客戶端活動。

⑥ 覆蓋了 channelInactive() 事件處理方法。服務端監聽到客戶端不活動。

⑦ exceptionCaught() 事件處理方法是當出現 Throwable 物件才會被呼叫,即當 Netty 由於 IO 錯誤或者處理器在處理事件時丟擲的異常時。在大部分情況下,捕獲的異常應該被記錄下來並且把關聯的 channel 給關閉掉。然而這個方法的處理方式會在遇到不同異常的情況下有不同的實現,比如你可能想在關閉連線之前傳送一個錯誤碼的響應訊息。


② SimpleChatServerInitializer

SimpleChatServerInitializer 用來增加多個的處理類到 ChannelPipeline 上,包括編碼、解碼、SimpleChatServerHandler 等。

例項如下:

public class SimpleChatServerInitializer extends
ChannelInitializer<SocketChannel> {
	@Override
	public void initChannel(SocketChannel ch) throws Exception {
		ChannelPipeline pipeline = ch.pipeline();
		pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
		pipeline.addLast("decoder", new StringDecoder());
		pipeline.addLast("encoder", new StringEncoder());
		pipeline.addLast("handler", new SimpleChatServerHandler());
		System.out.println("SimpleChatClient:"+ch.remoteAddress() +"連線上");
	}
}

③SimpleChatServer

編寫一個 main() 方法來啟動服務端。

SimpleChatServer例項如下:

import com.nett.chat.handler.SimpleChatServerInitializer;
import io.netty.bootstrap.ServerBootstrap;
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;

/**
 * Created by Janus on 2018/11/20.
 */
public class SimpleChatServer {
    private int port;

    public SimpleChatServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // (3)
                    .childHandler(new SimpleChatServerInitializer()) //(4)
                    .option(ChannelOption.SO_BACKLOG, 128) // (5)
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
            System.out.println("SimpleChatServer 啟動了");
            // 繫結埠,開始接收進來的連線
            ChannelFuture f = b.bind(port).sync(); // (7)
            // 等待伺服器 socket 關閉 。
            // 在這個例子中,這不會發生,但你可以優雅地關閉你的伺服器。
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            System.out.println("SimpleChatServer 關閉了");
        }
    }

    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new SimpleChatServer(port).run();
    }
}

至於1234567標註對應的講解,參考博文Netty基礎入門講解,此處不再贅述。


④ SimpleChatClientHandler客戶端處理器

客戶端的處理類比較簡單,只需要將讀到的資訊打印出來即可。

public class SimpleChatClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
        Channel channel = ctx.channel();
        System.out.println(channel+" , "+s);
    }
}

⑤ SimpleChatClientInitializer

與服務端類似,不再贅述。

例項如下:

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * Created by Janus on 2018/11/20.
 */
public class SimpleChatClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
        pipeline.addLast("decoder", new StringDecoder());
        pipeline.addLast("encoder", new StringEncoder());
        pipeline.addLast("handler", new SimpleChatClientHandler());
    }
}

⑥ SimpleChatClient

編寫一個 main() 方法來啟動客戶端。

例項如下:

import com.nett.chat.handler.SimpleChatClientInitializer;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * Created by Janus on 2018/11/20.
 */
public class SimpleChatClient {

    public static void main(String[] args) throws Exception{
        new SimpleChatClient("localhost", 8080).run();
    }
    private final String host;
    private final int port;
    public SimpleChatClient(String host, int port){
        this.host = host;
        this.port = port;
    }
    public void run() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new SimpleChatClientInitializer());
            //這裡使用connect instead of bind
            Channel channel = bootstrap.connect(host, port).sync().channel();
            //從控制檯讀取資料
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            //迴圈將讀取的資料傳送
            while (true) {
                channel.writeAndFlush(in.readLine() + "\r\n");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }
}

⑦ 執行效果

先執行 SimpleChatServer,再可以執行多個 SimpleChatClient,控制檯輸入文字繼續測試。

  • 分別啟動兩個client,服務端顯示如下:
    在這裡插入圖片描述

  • 當第二個客戶端加入時,第一個客戶端收到服務端發來訊息如下:

在這裡插入圖片描述

  • 當第二個客戶端輸入字串時,第二個客戶端接收到伺服器發來的訊息如下:
    在這裡插入圖片描述

  • 當第二個客戶端輸入字串時,第一個客戶端接收到伺服器的訊息如下:

在這裡插入圖片描述

  • 當第二個客戶端關閉時,第一個客戶端收到訊息如下:
    在這裡插入圖片描述

  • 當第二個客戶端關閉時,伺服器端列印如下:

在這裡插入圖片描述


【2】Netty 實現 WebSocket 聊天功能

上面我們用Netty快速實現了一個 Java 聊天程式。現在,我們
要做下修改,加入 WebSocket 的支援,使它可以在瀏覽器裡進行文字聊天。

① WebSocket

WebSocket 通過“Upgrade handshake(升級握手)”從標準的 HTTP 或HTTPS 協議轉為 WebSocket。因此,使用 WebSocket 的應用程式將始終以 HTTP/S 開始,然後進行升級。在什麼時候發生這種情況取決於具體的應用。它可以是在啟動時,或當一個特定的 URL 被請求時。

在我們的應用中,當 URL 請求以“/ws”結束時,我們才升級協議為WebSocket。否則,伺服器將使用基本的HTTP/S。一旦升級連線將使用WebSocket 傳輸所有資料。

整個伺服器邏輯如下:
在這裡插入圖片描述

1.客戶端/使用者連線到伺服器並加入聊天
2.HTTP 請求頁面或 WebSocket 升級握手
3.伺服器處理所有客戶端/使用者
4.響應 URI “/”的請求,轉到預設 html 頁面
5.如果訪問的是 URI“/ws” ,處理 WebSocket 升級握手
6.升級握手完成後 ,通過 WebSocket 傳送聊天訊息


② HttpRequestHandler

例項如下:

import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile;

import java.io.File;
import java.io.RandomAccessFile;
import java.net.URISyntaxException;
import java.net.URL;

/**
 * Created by Janus on 2018/11/20.
 */
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> { //1
    private final String wsUri;
    private static final File INDEX;

    static {
        URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
        try {
            String path = location.toURI() + "WebsocketChatClient.html";
            path = !path.contains("file:") ? path : path.substring(5);
            INDEX = new File(path);
        } catch (URISyntaxException e) {
            throw new IllegalStateException("Unable to locate WebsocketChatClient.html", e);
        }
    }

    public HttpRequestHandler(String wsUri) {
        this.wsUri = wsUri;
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        if (wsUri.equalsIgnoreCase(request.getUri())) {
            ctx.fireChannelRead(request.retain()); //2
        } else {
            if (HttpHeaders.is100ContinueExpected(request)) {
                send100Continue(ctx); //3
            }
            RandomAccessFile file = new RandomAccessFile(INDEX, "r");//4
            HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK);
            response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=UTF-8");
            boolean keepAlive = HttpHeaders.isKeepAlive(request);
            if (keepAlive) { //5
                response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, file.length());
                response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
            }
            ctx.write(response); //6
            if (ctx.pipeline().get(SslHandler.class) == null) { //7
                ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
            } else {
                ctx.write(new ChunkedNioFile(file.getChannel()));
            }
            ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); //8
            if (!keepAlive) {
                future.addListener(ChannelFutureListener.CLOSE); //9
            }
            file.close();
        }
    }

    private static void send100Continue(ChannelHandlerContext ctx) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
        ctx.writeAndFlush(response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("Client:" + incoming.remoteAddress() + "異常");
        // 當出現異常就關閉連線
        cause.printStackTrace();
        ctx.close();
    }
}

標註說明如下:

  • 1.擴充套件 SimpleChannelInboundHandler 用於處理 FullHttpRequest資訊

  • 2.如果請求是 WebSocket 升級,遞增引用計數器(保留)並且將它傳遞給在 ChannelPipeline 中的下個 ChannelInboundHandler

  • 3.處理符合 HTTP 1.1的 “100 Continue” 請求

  • 4.讀取預設的 WebsocketChatClient.html 頁面

  • 5.判斷 keepalive 是否在請求頭裡面

  • 6.給客戶端響應

  • 7.寫 index.html 到客戶端,判斷 SslHandler 是否在 ChannelPipeline 來決定是使用 DefaultFileRegion 還是ChunkedNioFile

  • 8.寫並重新整理 LastHttpContent 到客戶端,標記響應完成

  • 9.如果 keepalive 為false,當寫完成時,關閉 Channel

HttpRequestHandler 做了下面幾件事,

  • 如果該 HTTP 請求被髮送到URI “/ws”,呼叫 FullHttpRequest 上的 retain(),並通過呼叫 fireChannelRead(msg) 轉發到下一個 ChannelInboundHandler。retain() 是必要的,因為 channelRead() 完成後,它會呼叫 FullHttpRequest 上的 release() 來釋放其資源。
  • 如果客戶端傳送的 HTTP 1.1 頭是“Expect: 100-continue” ,將傳送“100 Continue”的響應。
  • 在響應頭被設定後,寫一個 HttpResponse 返回給客戶端。注意,這是不是 FullHttpResponse,唯一的反應的第一部分。此外,我們不使用writeAndFlush() 在這裡—這個是在最後完成。
  • 如果沒有加密也不壓縮,要達到最大的效率可以是通過儲存 index.html 的內容在一個 DefaultFileRegion實現—這將利用零拷貝來執行傳輸。出於這個原因,我們檢查,看看是否有一個 SslHandler 在 ChannelPipeline 中來判斷是否使用 ChunkedNioFile。
  • 寫 LastHttpContent 來標記響應的結束,並終止它
  • 如果不要求 keepalive ,新增 ChannelFutureListener 到 ChannelFuture 物件的最後寫入,並關閉連線。注意,這裡我們呼叫 writeAndFlush() 來重新整理所有以前寫的資訊。

③ 處理 WebSocket frame

WebSockets 在“幀”裡面來發送資料,其中每一個都代表了一個訊息的一部分。一個完整的訊息利用了多個幀。

WebSocket “Request for Comments” (RFC) 定義了六中不同的 frame。 Netty 給他們每個都提供了一個 POJO 實現 ,而我們的程式只需要使用下面4個幀型別:

  • CloseWebSocketFrame
  • PingWebSocketFrame
  • PongWebSocketFrame
  • TextWebSocketFrame

另外兩種為BinaryWebSocketFrame和ContinuationWebSocketFrame。

在這裡我們只需要顯示處理 TextWebSocketFrame,其他的會由 WebSocketServerProtocolHandler 自動處理。

下面程式碼展示了 ChannelInboundHandler 處理 TextWebSocketFrame,同時也將跟蹤在 ChannelGroup中所有活動的 WebSocket 連線。

TextWebSocketFrameHandler例項如下:

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * Created by Janus on 2018/11/20.
 */
public class TextWebSocketFrameHandler extends
        SimpleChannelInboundHandler<TextWebSocketFrame> {
    
    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx,
                                TextWebSocketFrame msg) throws Exception { // (1)
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            if (channel != incoming) {
                channel.writeAndFlush(new TextWebSocketFrame("[" + incoming.remoteAddress() + "]" + msg.text()));
            } else {
                channel.writeAndFlush(new TextWebSocketFrame("[you]" + msg.text()));
            }
        }
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception { // (2)
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            channel.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + incoming.remoteAddress() + " 加入"));
        }
        channels.add(ctx.channel());
        System.out.println("Client:" + incoming.remoteAddress() + "加入");
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // (3)
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            channel.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + incoming.remoteAddress() + " 離開"));
        }
        System.out.println("Client:" + incoming.remoteAddress() + "離開");
        channels.remove(ctx.channel());
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception { // (5)
        Channel incoming = ctx.channel();
        System.out.println("Client:" + incoming.remoteAddress() + "線上");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception { // (6)
        Channel incoming = ctx.channel();
        System.out.println("Client:" + incoming.remoteAddress() + "掉線");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("Client:" + incoming.remoteAddress() + "異常");
        // 當出現異常就關閉連線
        cause.printStackTrace();
        ctx.close();
    }
}

數字標註說明參考第【1】部分,這裡同樣不再贅述。

TextWebSocketFrameHandler 僅作了幾件事:

  • 當WebSocket 與新客戶端已成功握手完成,通過寫入資訊到 ChannelGroup 中的 Channel 來通知所有連線的客戶端,然後新增新 Channel 到 ChannelGroup
  • 如果接收到 TextWebSocketFrame,呼叫 retain() ,並將其寫、重新整理到 ChannelGroup,使所有連線的WebSocket Channel 都能接收到它。和以前一樣,retain() 是必需的,因為當 channelRead0()返回時,TextWebSocketFrame 的引用計數將遞減。由於所有操作都是非同步的,writeAndFlush() 可能會在以後完成,我們不希望它來訪問無效的引用。

④ WebsocketChatServerInitializer

由於 Netty 處理了其餘大部分功能,唯一剩下的我們現在要做的是初始化 ChannelPipeline 給每一個建立的新的Channel 。做到這一點,我們需要一個ChannelInitializer。

public class WebsocketChatServerInitializer extends
        ChannelInitializer<SocketChannel> {
    //1擴充套件 ChannelInitializer
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        //2新增 ChannelHandler 到 ChannelPipeline
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new HttpObjectAggregator(64*1024));
        pipeline.addLast(new ChunkedWriteHandler());
        pipeline.addLast(new HttpRequestHandler("/ws"));
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
        pipeline.addLast(new TextWebSocketFrameHandler());
    }
}

initChannel() 方法設定 ChannelPipeline 中所有新註冊的 Channel,安裝所有需要的ChannelHandler。


⑤ WebsocketChatServer

編寫一個 main() 方法來啟動服務端。

例項如下:

import com.netty.chat.handler.WebsocketChatServerInitializer;
import io.netty.bootstrap.ServerBootstrap;
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;

/**
 * Created by Janus on 2018/11/20.
 */
public class WebsocketChatServer {
    private int port;
    public WebsocketChatServer(int port) {
        this.port = port;
    }
    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // (3)
                    .childHandler(new WebsocketChatServerInitializer()) //(4)
                    .option(ChannelOption.SO_BACKLOG, 128) // (5)
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
            System.out.println("WebsocketChatServer 啟動了");
            // 繫結埠,開始接收進來的連線
            ChannelFuture f = b.bind(port).sync(); // (7)
            // 等待伺服器 socket 關閉 。
            // 在這個例子中,這不會發生,但你可以優雅地關閉你的伺服器。
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            System.out.println("WebsocketChatServer 關閉了");
        }
    }
    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new WebsocketChatServer(port).run();
    }
}

至於1234567標註對應的講解,參考博文Netty基礎入門講解,此處不再贅述。


⑥ WebsocketChatClient.html

在程式的 resources/templates 目錄下,我們建立一個 WebsocketChatClient.html 頁面來作為客戶端。

例項如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Chat</title>
</head>
<body>
<script type="text/javascript">
    var socket;
    if (!window.WebSocket) {
        window.WebSocket = window.MozWebSocket;
    }
    if (window.WebSocket) {
        socket = new WebSocket("ws://localhost:8080/ws");
        socket.onmessage = function(event) {
            var ta = document.getElementById('responseText');
            ta.value = ta.value + '\n' + event.data
        };
        socket.onopen = function(event) {
            var ta = document.getElementById('responseText');
            ta.value = "連線開啟!";
        };
        socket.onclose = function(event) {
            var ta = document.getElementById('responseText');
            ta.value = ta.value + "連線被關閉";
        };
    } else {
        alert("你的瀏覽器不支援 WebSocket!");
    }
    function send(message) {
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            socket.send(message);
        } else {
            alert("連線沒有開啟.");
        }
    }
</script>
    <form onsubmit="return false;">
        <h3>WebSocket 聊天室:</h3>
        <textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
        <br>
        <input type="text" name="message" style="width: 300px" value="Welcome !">
        <input type="button" value="傳送訊息" onclick="send(this.form.message.value)">
        <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空聊天記錄">
    </form>
    <br>
    <br>
</body>
</html>

⑦ 執行效果

先執行 WebsocketChatServer,再開啟多個瀏覽器頁面實現多個 客戶端訪問 http://localhost:8080

在這裡插入圖片描述

專案原始碼地址:https://github.com/JanusJ/Netty