Netty-开发WebSocket服务器

删除回忆录丶 提交于 2020-08-16 14:54:18

Netty-开发http服务器

WebSocket协议

  • 单一的TCP连接,采用全双工模式通信;
  • 对代理、防火墙和路由器透明;
  • 无头部信息、 Cookie 和身份验证;
  • 无安全开销:
  • 通过“ping/pong”帧保持链路激活:
  • 服务器可以 主动传递消息给客户端,不再需要客户端轮询。

WebSocket协议应用背景

  • 协同编辑/编程
  • 点击流数据
  • 股票基金报价
  • 体育实况更新
  • 多媒体聊天
  • 基于位置的应用
  • 在线教育
  • ····

上述应用场景都有一个特点:实时更新,http协议由于是半双工通信,实现实时更新需要轮询,增加网络开销,而WebSocket就是为了解决这一痛点的。

WebSocket协议建立

建立WebSocket连接时,需要通过客户端或者浏览器发出握手请求,请求消息:

websocket.png

为了建立一个WebSocket连接,客户端浏览器首先要向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中附加头信息“Upgrade:WebSocket"表明这是一个申请协议升级的HTTP请求。服务器端解析这些附加的头信息,然后生成应答信息返回给客户端,客户端和服务器端的WebSocket连接就建立起来了,双方可以通过这个连接通道自由地传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动关闭连接。

请求消息中的“Sec-WebSocket-Key"是随机的,服务器端会用这些数据来构造出一个SHA-I的信息摘要,把“Sec-WebSocket-Key"加上一个魔幻字 符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11"。使用SHA-1加密,然后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept"头的值,返回给客户端。

开始Coding~

1、新建maven项目,引入依赖

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.51.Final</version>
</dependency>

2、创建服务端启动代码

public class WebSocketServer {
    public void bind(int port) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline()
                                    //将请求和应答消息编码或者解码为HTTP消息
                                    .addLast(new HttpServerCodec())
                                    //将HTTP消息的多个部分组合成一条完整的HTTP消息,Aggregator:聚合,组合
                                    .addLast(new HttpObjectAggregator(65536))
                                    //向客户端发送html5文件,它主要用于支持浏览器和服务端进行WebSocket通信
                                    .addLast(new ChunkedWriteHandler())
                                    .addLast(new WebSocketServerHandler(port));
                        }
                    });
            Channel channel = bootstrap.bind(port).sync().channel();

            channel.closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

3、创建服务端消息处理代码

public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {

    // 服务器端Web套接字打开和关闭握手基类

    WebSocketServerHandshaker handshaker;

    //webSocket默认端口:8080

    int port;

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

    /**
     * channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 添加
        Global.group.add(ctx.channel());
        System.out.println("客户端与服务端连接开启:" + ctx.channel().remoteAddress().toString());
    }

    /**
     * channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端关闭了通信通道并且不可以传输数据
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // 移除
        Global.group.remove(ctx.channel());
        System.out.println("客户端与服务端连接关闭:" + ctx.channel().remoteAddress().toString());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
        if (o instanceof FullHttpRequest) {
            // 处理http消息(升级协议)
            handlerHttpRequest(channelHandlerContext, (FullHttpRequest) o);
        } else if (o instanceof WebSocketFrame) {
            // 处理websocket消息
            handlerWebSocketFrame(channelHandlerContext, (WebSocketFrame) o);
        }
    }

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

    private void handlerHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
        // 如果不是升级协议消息
        if (!req.getDecoderResult().isSuccess() || !"websocket".equals(req.headers().get("Upgrade"))) {
            sendError(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
            return;
        }
        // 设置连接参数
        ctx.attr(AttributeKey.valueOf("channelId")).set(ctx.channel().id());
        System.out.println(ctx.channel().remoteAddress().toString() + "----" + ctx.channel().id());

        //websocket协议开头为:ws+ip+端口
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:" + this.port, null, false);
        handshaker = wsFactory.newHandshaker(req);
        if (wsFactory == null) {
            //返回不支持websocket 版本
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            //开始握手
            handshaker.handshake(ctx.channel(), req);
        }
    }

    private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 判断是否为关闭链路指令
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame);
            return;
        }
        // ping请求返回pong
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        // 仅支持文本信息
        if (!(frame instanceof TextWebSocketFrame)) {
            throw new UnsupportedOperationException(String.format("%s frame not support", frame.getClass().getName()));
        }
        String text = ((TextWebSocketFrame) frame).text();
        System.out.println(String.format("Client:%s,channelId:%s", text, ctx.attr(AttributeKey.valueOf("channelId")).get()));

        TextWebSocketFrame tws = new TextWebSocketFrame(String.format("服务器收到消息:%s,通道id:%s,当前时间:%s", text, ctx.channel().id(), LocalDateTime.now()));
        if (text.startsWith("$")) {
            //已$开头的单独回复
            ctx.channel().write(tws);
        } else {
            //群发
            Global.group.writeAndFlush(tws);
        }
    }

    private static void sendError(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) {
        if (res.getStatus().code() != 200) {
            ByteBuf byteBuf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(byteBuf);
            byteBuf.release();
            HttpUtil.setContentLength(res, res.content().readableBytes());
        }
        ChannelFuture channelFuture = ctx.channel().writeAndFlush(res);
        if (!HttpUtil.isKeepAlive(req) || res.getStatus().code() != 200) {
            channelFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

}

4、客户端代码

var socket;
    if (!window.WebSocket) {
        window.WebSocket = window.MozWebSocket;
    }
    if (window.WebSocket) {
        socket = new WebSocket("ws://localhost:8080/websocket");
        socket.onmessage = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = "";
            ta.value = event.data
        };
        socket.onopen = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = "打开WebSocket服务正常,浏览器支持WebSocket!";
        };
        socket.onclose = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = "";
            ta.value = "WebSocket 关闭!";
        };
    } else {
        alert("抱歉,您的浏览器不支持WebSocket协议!");
    }

    function send(message) {
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            socket.send(message);
        } else {
            alert("WebSocket连接没有建立成功!");
        }
    }

5、启动服务器

public class Server {
    public static void main(String[] args) throws Exception {
        new WebSocketServer().bind(8080);
    }
}

打开浏览器测试

用谷歌浏览器打开resource文件夹下的WebSocketServer.html文件

websocketopen.png

浏览器支持websocket协议,并成功建立连接。

wm.png

点击发送消息,服务器正常响应。

wa.png

向建立的多个连接发送消息即可广播消息。

wq.png

发送$开头的消息即可单独回复。

GitHub服务端地址:https://github.com/GoodBoy2333/netty-server-maven.git

GitHub客户端地址:https://github.com/GoodBoy2333/netty-client-maven.git

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!