美文网首页程序员
netty实现websocket请求实战

netty实现websocket请求实战

作者: 西5d | 来源:发表于2020-09-02 16:34 被阅读0次

描述

WebSocket是html5开始浏览器和服务端进行全双工通信的网络技术。在该协议下,与服务端只需要一次握手,之后建立一条快速通道,开始互相传输数据,实际是基于TCP双向全双工,比http半双工提高了很大的性能,常用于网络在线聊天室等。继续netty实例的学习,本期内容主要做WebSocket实例的实现和相关协议的验证。

WebSocket与http比较

http WebSocket
半双工,可以双向传输,不能同时传输 全双工
消息冗长繁琐,消息头,消息体,换行... 对代理、防火墙、路由器透明
http轮询实现推送请求量大,而comet采用长连接 无头部、Cookie等
- ping/pong帧保持链路激活
- 特点:服务端可以主动传递给客户端,不需要轮询

代码示例和运行结果

服务端

服务端比较简单,是一启动一个端口来处理请求。

主程序

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.junit.Test;

public class NettyWebSocketServer {

    public void run(final int port) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    .addLast(new HttpServerCodec())
                                    .addLast(new HttpObjectAggregator(65535))
                                    .addLast(new ChunkedWriteHandler())
                                    .addLast(new WebSocketServerHandler());
                        }
                    });
            Channel ch = bootstrap.bind(port).sync().channel();
            System.out.println("websocket @" + port);
            ch.closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Test
    public void runServer() {
        run(23123);
    }
}

请求处理handler

核心处理WebSocket请求,重点地方加了注释

import io.netty.buffer.ByteBufUtil;
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.*;
import io.netty.handler.codec.http.websocketx.*;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
    private WebSocketServerHandshaker handshaker;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        //首次请求之后先进行握手,通过http请求来实现
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            handleWebSocketRequest(ctx, (WebSocketFrame) msg);
        }
    }

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
        //解码http失败返回
        if (!request.decoderResult().isSuccess()) {
            sendResponse(ctx, request, new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.BAD_REQUEST, ctx.alloc().buffer()));
            return;
        }

        if (!HttpMethod.GET.equals(request.method())) {
            sendResponse(ctx, request, new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.FORBIDDEN, ctx.alloc().buffer()));
            return;
        }

        //参数分别是ws地址,子协议,是否扩展,最大frame长度
        WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(getWebSocketLocation(request), null, true, 5 * 1024 * 1024);
        handshaker = factory.newHandshaker(request);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), request);
        }
    }

    //SSL支持采用wss://
    private String getWebSocketLocation(FullHttpRequest request) {
        String location = request.headers().get(HttpHeaderNames.HOST) + "/websocket";
        return "ws://" + location;
    }

    private void handleWebSocketRequest(ChannelHandlerContext ctx, WebSocketFrame frame) {
        //关闭
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return;
        }

        //握手 PING/PONG
        if (frame instanceof PingWebSocketFrame) {
            ctx.write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }

        //文本接收和发送
        if (frame instanceof TextWebSocketFrame) {
            String recv = ((TextWebSocketFrame) frame).text();
            String text = "now@" + LocalDateTime.now() + "\n recv:" + recv;

            //这里加了个循环,每隔一秒服务端主动发送内容给客户端
            for (int i = 0; i < 5; i++) {
                String res = i + "-" + text;
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ctx.writeAndFlush(new TextWebSocketFrame(res));
            }
            System.out.println(recv);
            return;
        }

        if (frame instanceof BinaryWebSocketFrame) {
            ctx.write(frame.retain());
        }
    }

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

    private void sendResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse resp) {
        HttpResponseStatus status = resp.status();
        if (status != HttpResponseStatus.OK) {
            ByteBufUtil.writeUtf8(resp.content(), status.toString());
            HttpUtil.setContentLength(req, resp.content().readableBytes());
        }
        boolean keepAlive = HttpUtil.isKeepAlive(req) && status == HttpResponseStatus.OK;
        HttpUtil.setKeepAlive(req, keepAlive);
        ChannelFuture future = ctx.write(resp);
        if (!keepAlive) {
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }

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

客户端

客户端使用了html来实现,可以直接在浏览器打开,代码比较简单,不做多的介绍。本地使用注意ws地址和服务端的一致。

<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
<form onsubmit="return false;">
    <input type="text" name="message" value="test"/>
    <br/>
    <input type="button" value="send", onclick="send(this.form.message.value)"/>
    <hr/>
    resp:
    <br/>
    <textarea id="responseText" style="width: 200px;height:300px;"></textarea>
</form>

</form>
</body>
<script type="text/javascript">
    var socket;
    if (!window.WebSocket) {
        window.WebSocket = window.MozWebSocket;
    }
    if (window.WebSocket) {
        socket = new WebSocket("ws://127.0.0.1:23123/websocket");
        socket.onmessage = function (ev) {
            var ta = document.getElementById('responseText');
            ta.value = '';
            ta.value = ev.data;
        }

        socket.onopen = function (ev) {
            var ta = document.getElementById('responseText');
            ta.value = 'start open websocket ...';
        }

        socket.onclose = function (ev) {
            var ta = document.getElementById('responseText');
            ta.value = '';
            ta.value = 'close websocket ...';
        }
    }else {
        alert('not support websocket !')
    }

    function send(msg) {
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            socket.send(msg);
        }else {
            alert('connect fail !')
        }
    }
</script>
</html>

试验结果

  1. 请求抓包说明


    ws请求

    如图中,首先是TCP三次握手,然后HTTP请求,建立WebSocket连接。之后客户端请求一次,然后服务端每隔1秒给发送内容到客户端。


    keepAlive
    上图是空闲时候的keepAlive调用
  2. 客户端调用


    服务端关闭的时候效果
    服务端主动发送的效果

    上图是服务端主动发送的效果,随着内容不同数据会变化。

参考资料

[1]示例原始地址:github
[2]《Netty权威指南(第二版)》P213

结语

以上就是本期的内容,后面有时间会继续netty相关实例的文章,确实在网络方面有很多新的收获。

相关文章

  • netty实现websocket请求实战

    描述 WebSocket是html5开始浏览器和服务端进行全双工通信的网络技术。在该协议下,与服务端只需要一次握手...

  • Netty实现WebSocket

    Netty是由JBOSS提供的一款基于NIO的客户、服务器端编程的Java开源框架,用以快速开发高性能、高可靠性的...

  • netty 实现websocket

    1先加入jario.netty netty-all ...

  • 基于netty搭建一个聊天室

    前言 上篇博文(netty实现消息中心(一)思路整理)大概说了下netty websocket消息中心的设计思路,...

  • Netty Websocket服务端心跳检测小计

    Netty websocket服务端心跳检测小计 最近在使用netty搭建了websocket服务器,在做心跳检测...

  • Netty实现websocket聊天

    简介 本文主要参考视频教程,然后自己总结一下而已。 启动类 主要是配置主线程组和从线程组、绑定端口等基本启动net...

  • Netty之WebSocket实现

    Netty是由JBOSS提供的一款基于NIO的客户、服务器端编程的Java开源框架,用以快速开发高性能、高可靠性的...

  • 《Netty 实战》第1,2,3章

    传送门《Netty 实战》第1,2,3章 《Netty 实战》第4,5,6章 《Netty 实战》第7,8,9...

  • 《Netty 实战》第7,8,9章

    传送门《Netty 实战》第1,2,3章 《Netty 实战》第4,5,6章 《Netty 实战》第7,8,9...

  • 《Netty 实战》第12,13章

    传送门《Netty 实战》第1,2,3章 《Netty 实战》第4,5,6章 《Netty 实战》第7,8,9...

网友评论

    本文标题:netty实现websocket请求实战

    本文链接:https://www.haomeiwen.com/subject/elewsktx.html