天天看點

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下檢視):

Netty應用執行個體學習

② 覆寫了 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,服務端顯示如下:
    Netty應用執行個體學習
  • 當第二個用戶端加入時,第一個用戶端收到服務端發來消息如下:
Netty應用執行個體學習
  • 當第二個用戶端輸入字元串時,第二個用戶端接收到伺服器發來的消息如下:
    Netty應用執行個體學習
  • 當第二個用戶端輸入字元串時,第一個用戶端接收到伺服器的消息如下:
Netty應用執行個體學習
  • 當第二個用戶端關閉時,第一個用戶端收到消息如下:
    Netty應用執行個體學習
  • 當第二個用戶端關閉時,伺服器端列印如下:
Netty應用執行個體學習

【2】Netty 實作 WebSocket 聊天功能

上面我們用Netty快速實作了一個 Java 聊天程式。現在,我們

要做下修改,加入 WebSocket 的支援,使它可以在浏覽器裡進行文本聊天。

① WebSocket

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

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

整個伺服器邏輯如下:

Netty應用執行個體學習

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​​