网络编程学习笔记 - 03Netty入门

103 阅读14分钟

第一个Netty程序

其实就是netty中example中的代码。代码结构为:EchoClient、EchoClientHandler、EchoServer和EchoServerHandler

代码如下,EchoServer:

public class EchoServer {

    private final int port;

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

    public static void main(String[] args) throws InterruptedException {
        int port = 10086;
        EchoServer echoServer = new EchoServer(port);
        System.out.println("服务器即将启动");
        echoServer.start();
        System.out.println("服务器关闭");
    }

    public void start() throws InterruptedException {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        // 线程组
        EventLoopGroup group = new NioEventLoopGroup(1);
        try {
            // 服务端启动必须
            ServerBootstrap b = new ServerBootstrap();
            // 将线程组传入
            b.group(group)
                    // 指定使用NIO进行网络传输
                    .channel(NioServerSocketChannel.class)
                    // 指定服务器监听端口
                    .localAddress(new InetSocketAddress(port))
                    // 服务端每接收到一个连接请求,就会新启一个socket通信,也就是channel,所以下面这段代码的作用就是为这个子channel增加handle
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            // 添加到该子channel的pipeline的尾部
                            ch.pipeline().addLast(serverHandler);
                        }
                    });

            // 异步绑定到服务器,sync()会阻塞直到完成
            ChannelFuture f = b.bind().sync();
            // 阻塞直到服务器的channel关闭
            f.channel().closeFuture().sync();
        } finally {
            // 优雅关闭线程组
            group.shutdownGracefully().sync();
        }
    }
}

EchoServerHandler:

@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    // 客户端读到数据以后,就会执行
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        System.out.println("Server accept:" + in.toString(CharsetUtil.UTF_8));
        ctx.write(in);
    }

    /*** 服务端读取完成网络数据后的处理*/
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }

    /*** 发生异常后的处理*/
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

EchoClient

public class EchoClient {

    private final int port;
    private final String host;

    public EchoClient(int port, String host) {
        this.port = port;
        this.host = host;
    }

    public void start() throws InterruptedException {
        // 线程组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 客户端启动必备
            Bootstrap b = new Bootstrap();
            // 把线程组传入
            b.group(group)
                    // 指定使用NIO进行网络传输
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host, port))
                    .handler(new EchoClientHandler());
            // 连接到远程节点,阻塞直到连接完成
            ChannelFuture f = b.connect().sync();
            // 阻塞程序,直到Channel发生了关闭
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new EchoClient(10086, "127.0.0.1").start();
    }
}

EchoClientHandler:

public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    // 客户端读到数据以后,就会执行
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        System.out.println("client accept: " + msg.toString(CharsetUtil.UTF_8));
    }

    // 连接建立以后
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Netty", CharsetUtil.UTF_8));
        // ctx.fireChannelActive();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        super.userEventTriggered(ctx, evt);
    }
}

重要的类、方法解析

EventLoop

EventLoop可以简单理解为一个线程、EventLoopGroup则可以理解为线程组。

EventLoopGroup group = new NioEventLoopGroup();

服务端使用的是:

ServerBootstrap b = new ServerBootstrap();

客户端使用的是:

Bootstrap b = new Bootstrap();

ServerBootstrap将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap则是由想要连接到远程节点的客户端应用程序所使用的。

引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个,因为服务器需要两组不同的Channel。第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel。

8 netty大致结构.png

Channel是Java NIO的一个基本构造。它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作。

目前,可以把Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。

事件和ChannelHandler、ChannelPipeline

.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel ch) {
        ch.pipeline().addLast(serverHandler);
    }
});

Netty使用不同的事件来通知我们状态的改变或者是操作的状态,这使得能够基于已经发生的事件来触发适当的动作。

可能由入站数据或者相关的状态更改而触发的事件包括:

  • 连接已被激活或者连接失活;
  • 数据读取;
  • 用户事件;
  • 错误事件。

出站事件是未来将会触发的某个动作的操作结果,这些动作包括:

  • 打开或者关闭到远程节点的连接;
  • 将数据写到或者冲刷到套接字。

每个事件都可以被分发给ChannelHandler类中的某个用户实现的方法。

Netty 提供了大量预定义的可以开箱即用的ChannelHandler实现,包括用于各种协议(如HTTP 和SSL/TLS)的ChannelHandler。

ChannelFuture

Netty 中所有的I/O 操作都是异步的。

JDK预置了java.util.concurrent.Future接口,Future提供了一种在操作完成时通知应用程序的方式。它将在未来的某个时刻完成,并提供对其结果的访问。但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以Netty提供了它自己的实现ChannelFuture,用于在执行异步操作的时候使用。每个Netty的出站I/O操作都将返回一个ChannelFuture。

Netty为许多通用协议提供了编解码器和处理器,几乎可以开箱即用,这减少了你在那些相当繁琐的事务上本来会花费的时间与精力。下面会接着介绍http、udp和ws这几种方式。

实现HTTP

代码如下:

public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    private static final byte[] CONTENT = "hello world".getBytes();

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
        if (msg instanceof HttpRequest) {
            HttpRequest req = (HttpRequest) msg;

            FullHttpResponse response =
                    new DefaultFullHttpResponse(req.protocolVersion(), OK, Unpooled.wrappedBuffer(CONTENT));
            response.headers()
                    .set(CONTENT_TYPE, TEXT_PLAIN)
                    .setInt(CONTENT_LENGTH, response.content().readableBytes());

            ChannelFuture f = ctx.write(response);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

public final class HttpHelloWorldServer {

    public static void main(String[] args) throws Exception {
        // 主从多线程Reactor模式
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(Channel ch) {
                            ChannelPipeline p = ch.pipeline();
                            // netty针对http编解码的处理类
                            p.addLast(new HttpServerCodec());
                            // netty针对http编解码的处理类
                            p.addLast(new HttpServerExpectContinueHandler());
                            // 自己的业务处理逻辑
                            p.addLast(new HttpHelloWorldServerHandler());
                        }
                    });

            Channel ch = b.bind(10086).sync().channel();

            System.err.println("Open your web browser and navigate to " + "http://127.0.0.1:10086");

            ch.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

然后浏览器访问http://127.0.0.1:10086,返回结果 hello world

实现UDP

UDP协议

  • 面向无连接的通讯协议;
  • 通讯时不需要接收方确认,属于不可靠的传输;
  • 因为不需要建立连接,所以传输速度快,但是容易丢失数据。

报文组成

  • 源端口:源端口号,在需要对方回信时选用,不需要时可用全0。
  • 目的端口:目的端口号,这在终点交付报文时必须要使用到。
  • 长度:UDP用户数据包的长度,其最小值是8(仅有首部)。
  • 校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃。
  • 数据

UDP是面向无连接的通讯协议,UDP报头由4个域组成,其中每个域各占用2个字节,其中包括目的端口号和源端口号信息,数据报的长度域是指包括报头和数据部分在内的总字节数,校验值域来保证数据的安全。由于通讯不需要连接,所以可以实现广播发送。

UDP通讯时不需要接收方确认,属于不可靠的传输,可能会出现丢包现象,实际应用中要求程序员编程验证。

UDP与TCP位于同一层,但它不管数据包的顺序、错误或重发。因此,UDP不被应用于那些使用虚电路的面向连接的服务,UDP主要用于那些面向查询、应答的服务。

实现UDP单播

单播:定义为发送消息给一个由唯一的地址所标识的单一的网络目的地。面向连接的协议和无连接协议都支持这种模式。代码如下:

public class UdpAnswerSide {

    public final static String ANSWER = "here we go: ";

    public void run(int port) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 和tcp的不同,udp没有接受连接的说法,所以即使是接收端,也使用Bootstrap
            Bootstrap b = new Bootstrap();
            // 由于我们用的是UDP协议,所以要用NioDatagramChannel来创建
            b.group(group)
                    .channel(NioDatagramChannel.class)
                    .handler(new AnswerHandler());
            // 没有接受客户端连接的过程,监听本地端口即可
            ChannelFuture f = b.bind(port).sync();
            System.out.println("answer service start.");
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 10086;
        new UdpAnswerSide().run(port);
    }
}

public class AnswerHandler extends SimpleChannelInboundHandler<DatagramPacket> {

    // 应答的具体内容从常量字符串数组中取得,由nextQuote方法随机获取
    private static final String[] DICTIONARY = {
            "若不披上这件衣裳,众生又怎知我尘缘已断,金海尽干。",
            "只要心中还有放不下的偶像,终有一天,它将化为修行路上的无解业障。",
            "忘了,是新添的垢;记得,是老套的旧。",
            "祸乱人心,倒果为因,师兄如此执着于输赢,可笑,可悲!",
            "人也,兽也,佛也,妖也,众生自有根器,持优劣为次第,可乱来不得。你说,对吗?孙悟空"
    };
    private static final Random r = new Random();

    private String nextQuote() {
        return DICTIONARY[r.nextInt(DICTIONARY.length - 1)];
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
        // 获得请求
        String req = packet.content().toString(CharsetUtil.UTF_8);
        if (UdpQuestionSide.QUESTION.equals(req)) {
            String answer = UdpAnswerSide.ANSWER + nextQuote();
            System.out.println("receive message: " + req);
            // 重新 new 一个DatagramPacket对象,我们通过packet.sender()来获取发送者的消息。重新发送出去!
            ctx.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer(answer, CharsetUtil.UTF_8), packet.sender()));
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
        cause.printStackTrace();
    }
}

public class UdpQuestionSide {

    public final static String QUESTION = "give me more.";

    public void run(int port) {

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    // 由于我们用的是UDP协议,所以要用NioDatagramChannel来创建
                    .channel(NioDatagramChannel.class)
                    .handler(new QuestoinHandler());
            // 不需要建立连接
            Channel ch = b.bind(0).sync().channel();
            // 将UDP请求的报文以DatagramPacket打包发送给接受端
            ch.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer(QUESTION, CharsetUtil.UTF_8),
                            new InetSocketAddress("127.0.0.1", port)))
                    .sync();
            // 不知道接收端能否收到报文,也不知道能否收到接收端的应答报文
            // 所以等待15秒后,不再等待,关闭通信
            if (!ch.closeFuture().await(15000)) {
                System.out.println("timeout.");
            }
        } catch (Exception e) {
            group.shutdownGracefully();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int answerPort = 10086;

        new UdpQuestionSide().run(answerPort);
    }
}

public class QuestoinHandler extends SimpleChannelInboundHandler<DatagramPacket> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg)
            throws Exception {
        // 获得应答,DatagramPacket提供了content()方法取得报文的实际内容
        String response = msg.content().toString(CharsetUtil.UTF_8);
        if (response.startsWith(UdpAnswerSide.ANSWER)) {
            System.out.println(response);
            ctx.close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

answer日志输出:

answer service start.
receive message: give me more.
receive message: give me more.
receive message: give me more.

question日志输出:

here we go: 若不披上这件衣裳,众生又怎知我尘缘已断,金海尽干。

实现UDP广播

广播:传输到网络(或者子网)上的所有主机。

这里不做具体代码演示,因为和单播区别不大,相对重要的就是ip需要改为255.255.255.255

实现WebSocket

代码如下:

Server:

public final class WebSocketServer {

    // 创建 DefaultChannelGroup,用来保存所有已经连接的 WebSocket Channel,群发和一对一功能可以用上
    private final static ChannelGroup channelGroup =
            new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);

    static final boolean SSL = false;

    // 通过ssl访问端口为8443,否则为8080
    static final int PORT = Integer.parseInt(System.getProperty("port", SSL ? "8443" : "8080"));

    public static void main(String[] args) throws Exception {
        // SSL配置
        final SslContext sslCtx;
        if (SSL) {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
        } else {
            sslCtx = null;
        }

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new WebSocketServerInitializer(sslCtx, channelGroup));

            Channel ch = b.bind(PORT).sync().channel();

            System.out.println("打开浏览器访问: " + (SSL ? "https" : "http") + "://127.0.0.1:" + PORT + '/');

            ch.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {

    private final ChannelGroup group;

    /*websocket访问路径*/
    private static final String WEBSOCKET_PATH = "/websocket";

    private final SslContext sslCtx;

    public WebSocketServerInitializer(SslContext sslCtx, ChannelGroup group) {
        this.sslCtx = sslCtx;
        this.group = group;
    }

    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        if (sslCtx != null) {
            pipeline.addLast(sslCtx.newHandler(ch.alloc()));
        }
        // 增加对http的支持
        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new HttpObjectAggregator(65536));

        // Netty提供,支持WebSocket应答数据压缩传输
        pipeline.addLast(new WebSocketServerCompressionHandler());
        // Netty提供,对整个websocket的通信进行了初始化(发现http报文中有升级为websocket的请求),包括握手,以及以后的一些通信控制
        pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true));

        // 浏览器访问时展示index页面
        pipeline.addLast(new ProcessWsIndexPageHandler(WEBSOCKET_PATH));

        // 对websocket的数据进行处理
        pipeline.addLast(new ProcessWsFrameHandler(group));
    }
}

public class ProcessWsFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {

    private final ChannelGroup group;

    public ProcessWsFrameHandler(ChannelGroup group) {
        this.group = group;
    }

    private static final Logger logger
            = LoggerFactory.getLogger(ProcessWsFrameHandler.class);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx,
                                WebSocketFrame frame) throws Exception {
        // 判断是否为文本帧,目前只处理文本帧
        if (frame instanceof TextWebSocketFrame) {
            // Send the uppercase string back.
            String request = ((TextWebSocketFrame) frame).text();
            logger.info("{} received {}", ctx.channel(), request);
            ctx.channel().writeAndFlush(
                    new TextWebSocketFrame(request.toUpperCase(Locale.CHINA)));
            // 群发实现:一对一道理一样
            group.writeAndFlush(new TextWebSocketFrame(
                    "Client " + ctx.channel() + " say:" + request.toUpperCase(Locale.CHINA)));
        } else {
            String message = "unsupported frame type: " + frame.getClass().getName();
            throw new UnsupportedOperationException(message);
        }
    }

    // 重写 userEventTriggered()方法以处理自定义事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 检测事件,如果是握手成功事件,做点业务处理
        if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {

            // 通知所有已经连接的WebSocket 客户端新的客户端已经连接上了
            group.writeAndFlush(new TextWebSocketFrame(
                    "Client " + ctx.channel() + " joined"));

            // 将新的 WebSocket Channel 添加到 ChannelGroup 中,
            // 以便它可以接收到所有的消息
            group.add(ctx.channel());
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}

public class ProcessWsIndexPageHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private final String websocketPath;

    public ProcessWsIndexPageHandler(String websocketPath) {
        this.websocketPath = websocketPath;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
        // 处理错误或者无法解析的http请求
        if (!req.decoderResult().isSuccess()) {
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
            return;
        }

        // 只允许Get请求
        if (req.method() != GET) {
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
            return;
        }

        // 发送index页面的内容
        if ("/".equals(req.uri()) || "/index.html".equals(req.uri())) {
            //生成WebSocket的访问地址,写入index页面中
            String webSocketLocation = getWebSocketLocation(ctx.pipeline(), req, websocketPath);
            System.out.println("WebSocketLocation:[" + webSocketLocation + "]");
            //生成index页面的具体内容,并送往浏览器
            ByteBuf content = MakeIndexPage.getContent(webSocketLocation);
            FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content);

            res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
            HttpUtil.setContentLength(res, content.readableBytes());

            sendHttpResponse(ctx, req, res);
        } else {
            sendHttpResponse(ctx, req,
                    new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND));
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

    // 发送应答
    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
        // 错误的请求进行处理 (code<>200).
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            HttpUtil.setContentLength(res, res.content().readableBytes());
        }

        // 发送应答.
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        //对于不是长连接或者错误的请求直接关闭连接
        if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

    /*根据用户的访问,告诉用户的浏览器,WebSocket的访问地址*/
    private static String getWebSocketLocation(ChannelPipeline cp, HttpRequest req, String path) {
        String protocol = "ws";
        if (cp.get(SslHandler.class) != null) {
            protocol = "wss";
        }
        return protocol + "://" + req.headers().get(HttpHeaderNames.HOST) + path;
    }
}

public final class MakeIndexPage {

    private static final String NEWLINE = "\r\n";

    public static ByteBuf getContent(String webSocketLocation) {
        return Unpooled.copiedBuffer(
                "<html><head><title>Web Socket Test</title></head>"
                        + NEWLINE +
                        "<body>" + NEWLINE +
                        "<script type=\"text/javascript\">" + NEWLINE +
                        "var socket;" + NEWLINE +
                        "if (!window.WebSocket) {" + NEWLINE +
                        "  window.WebSocket = window.MozWebSocket;" + NEWLINE +
                        '}' + NEWLINE +
                        "if (window.WebSocket) {" + NEWLINE +
                        "  socket = new WebSocket(\"" + webSocketLocation + "\");"
                        + NEWLINE +
                        "  socket.onmessage = function(event) {" + NEWLINE +
                        "    var ta = document.getElementById('responseText');"
                        + NEWLINE +
                        "    ta.value = ta.value + '\\n' + event.data" + NEWLINE +
                        "  };" + NEWLINE +
                        "  socket.onopen = function(event) {" + NEWLINE +
                        "    var ta = document.getElementById('responseText');"
                        + NEWLINE +
                        "    ta.value = \"Web Socket opened!\";" + NEWLINE +
                        "  };" + NEWLINE +
                        "  socket.onclose = function(event) {" + NEWLINE +
                        "    var ta = document.getElementById('responseText');"
                        + NEWLINE +
                        "    ta.value = ta.value + \"Web Socket closed\"; "
                        + NEWLINE +
                        "  };" + NEWLINE +
                        "} else {" + NEWLINE +
                        "  alert(\"Your browser does not support Web Socket.\");"
                        + NEWLINE +
                        '}' + NEWLINE +
                        NEWLINE +
                        "function send(message) {" + NEWLINE +
                        "  if (!window.WebSocket) { return; }" + NEWLINE +
                        "  if (socket.readyState == WebSocket.OPEN) {" + NEWLINE +
                        "    socket.send(message);" + NEWLINE +
                        "  } else {" + NEWLINE +
                        "    alert(\"The socket is not open.\");" + NEWLINE +
                        "  }" + NEWLINE +
                        '}' + NEWLINE +
                        "</script>" + NEWLINE +
                        "<form onsubmit=\"return false;\">" + NEWLINE +
                        "<input type=\"text\" name=\"message\" " +
                        "value=\"Hello, World!\"/>" +
                        "<input type=\"button\" value=\"Send Web Socket Data\""
                        + NEWLINE +
                        "       onclick=\"send(this.form.message.value)\" />"
                        + NEWLINE +
                        "<h3>Output</h3>" + NEWLINE +
                        "<textarea id=\"responseText\" " +
                        "style=\"width:500px;height:300px;\"></textarea>"
                        + NEWLINE +
                        "</form>" + NEWLINE +
                        "</body>" + NEWLINE +
                        "</html>" + NEWLINE, CharsetUtil.US_ASCII);
    }

}

client:

public final class WebSocketClient {

    static final String URL = System.getProperty("url", "ws://127.0.0.1:8080/websocket");
    static final String SURL = System.getProperty("url", "wss://127.0.0.1:8443/websocket");

    public static void main(String[] args) throws Exception {
        URI uri = new URI(URL);
        String scheme = uri.getScheme() == null ? "ws" : uri.getScheme();
        final String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost();
        final int port = uri.getPort();

        if (!"ws".equalsIgnoreCase(scheme) && !"wss".equalsIgnoreCase(scheme)) {
            System.err.println("Only WS(S) is supported.");
            return;
        }

        final boolean ssl = "wss".equalsIgnoreCase(scheme);
        final SslContext sslCtx;
        if (ssl) {
            sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
        } else {
            sslCtx = null;
        }

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00.
            // If you change it to V00, ping is not supported and remember to change
            // HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline.
            final WebSocketClientHandler handler =
                    new WebSocketClientHandler(
                            WebSocketClientHandshakerFactory
                                    .newHandshaker(
                                            uri, WebSocketVersion.V13,
                                            null,
                                            true,
                                            new DefaultHttpHeaders()));

            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline p = ch.pipeline();
                            if (sslCtx != null) {
                                p.addLast(sslCtx.newHandler(ch.alloc(),
                                        host, port));
                            }
                            p.addLast(
                                    //http协议为握手必须
                                    new HttpClientCodec(),
                                    new HttpObjectAggregator(8192),
                                    //支持WebSocket数据压缩
                                    WebSocketClientCompressionHandler.INSTANCE,
                                    handler);
                        }
                    });

            // 连接服务器
            Channel ch = b.connect(uri.getHost(), port).sync().channel();
            // 等待握手完成
            handler.handshakeFuture().sync();

            BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
            while (true) {
                String msg = console.readLine();
                if (msg == null) {
                    break;
                } else if ("bye".equals(msg.toLowerCase())) {
                    ch.writeAndFlush(new CloseWebSocketFrame());
                    ch.closeFuture().sync();
                    break;
                } else if ("ping".equals(msg.toLowerCase())) {
                    WebSocketFrame frame = new PingWebSocketFrame(Unpooled.wrappedBuffer(new byte[]{8, 1, 8, 1}));
                    ch.writeAndFlush(frame);
                } else {
                    WebSocketFrame frame = new TextWebSocketFrame(msg);
                    ch.writeAndFlush(frame);
                }
            }
        } finally {
            group.shutdownGracefully();
        }
    }
}

public class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {

    // 负责和服务器进行握手
    private final WebSocketClientHandshaker handshaker;
    // 握手的结果
    private ChannelPromise handshakeFuture;

    public WebSocketClientHandler(WebSocketClientHandshaker handshaker) {
        this.handshaker = handshaker;
    }

    public ChannelFuture handshakeFuture() {
        return handshakeFuture;
    }

    // 当前Handler被添加到ChannelPipeline时,
    // new出握手的结果的实例,以备将来使用
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        handshakeFuture = ctx.newPromise();
    }

    // 通道建立,进行握手
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        handshaker.handshake(ctx.channel());
    }

    // 通道关闭
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        System.out.println("WebSocket Client disconnected!");
    }

    // 读取数据
    @Override
    public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel ch = ctx.channel();
        // 握手未完成,完成握手
        if (!handshaker.isHandshakeComplete()) {
            try {
                handshaker.finishHandshake(ch, (FullHttpResponse) msg);
                System.out.println("WebSocket Client connected!");
                handshakeFuture.setSuccess();
            } catch (WebSocketHandshakeException e) {
                System.out.println("WebSocket Client failed to connect");
                handshakeFuture.setFailure(e);
            }
            return;
        }

        // 握手已经完成,升级为了websocket,不应该再收到http报文
        if (msg instanceof FullHttpResponse) {
            FullHttpResponse response = (FullHttpResponse) msg;
            throw new IllegalStateException(
                    "Unexpected FullHttpResponse (getStatus=" + response.status() +
                            ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
        }

        // 处理websocket报文
        WebSocketFrame frame = (WebSocketFrame) msg;
        if (frame instanceof TextWebSocketFrame) {
            TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
            System.out.println("WebSocket Client received message: " + textFrame.text());
        } else if (frame instanceof PongWebSocketFrame) {
            System.out.println("WebSocket Client received pong");
        } else if (frame instanceof CloseWebSocketFrame) {
            System.out.println("WebSocket Client received closing");
            ch.close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        if (!handshakeFuture.isDone()) {
            handshakeFuture.setFailure(cause);
        }
        ctx.close();
    }
}

客户端可以使用上面代码,或者网页访问http://127.0.0.1:8080/

简单的日志如下:

WebSocket Client connected!
123
WebSocket Client received message: 123
WebSocket Client received message: Client [id: 0xc2015e3d, L:/127.0.0.1:8080 - R:/127.0.0.1:56790] say:123
WebSocket Client received message: Client [id: 0x65dee4ca, L:/127.0.0.1:8080 - R:/127.0.0.1:56912] joined
WebSocket Client received message: Client [id: 0x65dee4ca, L:/127.0.0.1:8080 - R:/127.0.0.1:56912] say:HELLO, WORLD!