使用 netty实现 websocket

1. websocket介绍

WebSocket是一种计算机通信协议,用于在客户端和服务器之间建立持久性的全双工通信连接。它提供了一种在单个TCP连接上进行双向通信的方式,允许服务器主动向客户端发送数据,而不需要客户端首先发送请求。这与传统的HTTP请求-响应模型不同,后者需要客户端发送请求并等待服务器响应。

WebSocket协议的主要特点包括:

  1. 双向通信:WebSocket允许服务器主动向客户端发送数据,而不仅仅是响应客户端的请求。这使得实时应用程序(如聊天应用程序、多人游戏等)的开发更加容易,因为服务器可以即时地将更新推送给客户端。
  2. 持久连接:WebSocket连接在客户端和服务器之间保持活动状态,而不像HTTP连接那样在每个请求之后关闭。这消除了为每个请求建立新连接的开销,减少了网络流量和延迟。
  3. 低开销:由于WebSocket使用较少的头部信息,并且不需要在每个请求中进行完整的握手,所以它具有较低的开销。这使得它在网络带宽有限的环境下更加高效。
  4. 跨域支持:与传统的AJAX请求不同,WebSocket允许在不同域之间进行跨域通信。这使得开发者可以构建具有分布式架构的应用程序,其中前端和后端可以部署在不同的域上。

WebSocket的工作流程如下:

  1. 客户端通过HTTP协议向服务器发送一个特殊的请求,其中包含用于升级连接为WebSocket的标头。
  2. 服务器收到请求后,如果支持WebSocket协议,将返回一个包含特殊标头的响应,用于升级连接。
  3. 一旦连接升级成功,客户端和服务器之间的通信将使用WebSocket协议进行,而不是HTTP。
  4. 客户端和服务器可以通过发送帧(frames)来进行双向通信,每个帧可以包含文本、二进制数据或控制信息。

2. websocket连接过程

客户端依靠发起HTTP握手,告诉服务端进行WebSocket协议通讯,并告知WebSocket协议版本。服务端确认协议版本,升级为WebSocket协议。之后如果有数据需要推送,会主动推送给客户端。

  • 请求头Request Headers
GET /test HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: tFGdnEL/5fXMS9yKwBjllg==
Origin: http://example.com
Sec-WebSocket-Protocol: v10.stomp, v11.stomp, v12.stomp
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Version: 13

首先客户端(如浏览器)发出带有特殊消息头(Upgrade、Connection)的请求到服务器,服务器判断是否支持升级,支持则返回响应状态码101,表示协议升级成功,对于WebSocket就是握手成功。

其中关键的字段就是Upgrade,Connection,告诉 Apache 、 Nginx 等服务器:注意啦,发起的是Websocket协议,不再 使用原先的HTTP。

其中,Sec-WebSocket-Key当成是请求id就好了。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HaA6EjhHRejpHyuO0yBnY4J4n3A=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Sec-WebSocket-Protocol: v12.stomp

Sec-WebSocket-Accept的字段值是由握手请求中的Sec-WebSocket-Key的字段值生成的。成功握手确立WebSocket连接之后,通信时不再使用HTTP的数据帧,而采用WebSocket独立的数据帧。

 3. 代码实现

(1): 创建了一个ServerBootstrap实例,用于引导服务器的启动配置。然后,通过serverBootstrap的一系列方法来配置服务器的选项和处理器。

(2): 使用group方法将主线程组(mainGroup)和工作线程组(workerGroup)设置为服务器的线程组。然后,通过channel方法设置服务器的通道类型为NioServerSocketChannel。接着,使用option方法设置一些选项,如SO_BACKLOG和SO_KEEPALIVE。

(3): 通过handler方法给serverBootstrap添加一个LoggingHandler处理器,用于处理日志。

(4): 通过childHandler方法给serverBootstrap添加一个ChannelInitializer,用于初始化SocketChannel的处理管道。

(5): 在管道的初始化过程中,首先添加了一个IdleStateHandler,用于处理客户端的心跳超时。然后,添加了HttpServerCodec,用于处理HTTP协议的编码和解码。接着,使用ChunkedWriteHandler以块方式写入数据

然后,使用HttpObjectAggregator将HTTP数据聚合为完整的请求。接下来,添加了一个自定义的HttpHeadersHandler,用于保存用户的IP地址。

(6): 添加了WebSocketServerProtocolHandler,将HTTP协议升级为WebSocket协议,并保持长连接。最后,添加了一个自定义的业务逻辑处理器(NETTY_WEB_SOCKET_SERVER_HANDLER)。

(7): 通过调用serverBootstrap.bind(WEBSOCKET_PORT).sync()来启动服务器并绑定指定的端口。这将阻塞当前线程,直到服务器成功启动。

@Slf4j
@Configuration
public class NettyWebSocketServer {
    public static final int WEBSOCKET_POET = 8090;
    public static final NettyWebSocketServerHandler NETTY_WEB_SOCKET_SERVER_HANDLER = new NettyWebSocketServerHandler();
    //创建线程池执行器
    //主事件循环组(mainGroup)负责接受客户端连接请求,并将连接分配给工作事件循环组(workerGroup)中的线程进行处理。通常情况下,主事件循环组只需要一个线程即可。
    private EventLoopGroup mainGroup = new NioEventLoopGroup();
    //工作事件循环组(workerGroup)负责实际的I/O操作,例如读取、写入和处理数据。
    // 工作事件循环组的线程数通过NettyRuntime.availableProcessors()来设置为可用的处理器核心数,这样可以充分利用系统资源。
    private EventLoopGroup workerGroup = new NioEventLoopGroup(NettyRuntime.availableProcessors());

    /**
     *  启动 ws sever
     */
    @PostConstruct
    public void start() throws InterruptedException{
        run();
    }

    /**
     * 销毁
     */
    public void destroy() {
        Future<?> futureMainGroup = mainGroup.shutdownGracefully();
        Future<?> futureWorkerGroup = workerGroup.shutdownGracefully();
        futureMainGroup.syncUninterruptibly();
        futureWorkerGroup.syncUninterruptibly();
        log.info("关闭 ws sever 成功!!!");
    }

    public void run() throws InterruptedException{
        //服务器启动引导对象
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(mainGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 128)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new LoggingHandler(LogLevel.INFO)) // 为 serverBootstrap 添加日志处理器
                .childHandler(new ChannelInitializer<SocketChannel>() {

                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //30 秒客户端没有向服务端发送心跳则关闭连接
                        pipeline.addLast(new IdleStateHandler(30, 0, 0));
                        // 因为使用http协议, 需要 http解码器
                        pipeline.addLast(new HttpServerCodec());
                        // 以块方式写, 添加 chunkedWriter 处理器
                        pipeline.addLast(new ChunkedWriteHandler());
                        /**
                         * 说明:
                         *  1. http数据在传输过程中是分段的,HttpObjectAggregator可以把多个段聚合起来;
                         *  2. 这就是为什么当浏览器发送大量数据时,就会发出多次 http请求的原因
                         */
                        pipeline.addLast(new HttpObjectAggregator(8192));
                        //保存用户ip
                        pipeline.addLast(new HttpHeadersHandler());
                        /**
                         * 说明:
                         *  1. 对于 WebSocket,它的数据是以帧frame 的形式传递的;
                         *  2. 可以看到 WebSocketFrame 下面有6个子类
                         *  3. 浏览器发送请求时: ws://localhost:7000/hello 表示请求的uri
                         *  4. WebSocketServerProtocolHandler 核心功能是把 http协议升级为 ws 协议,保持长连接;
                         *      是通过一个状态码 101 来切换的
                         */
                        pipeline.addLast(new WebSocketServerProtocolHandler("/"));
                        //自定义handler, 处理业务逻辑
                        pipeline.addLast(NETTY_WEB_SOCKET_SERVER_HANDLER);

                    }
                });
        serverBootstrap.bind(WEBSOCKET_POET).sync();
        System.out.println("启动成功");
    }

}

明白了websocket的升级过程,对netty的处理的就比较简单了。websocket初期是通过http请求,进行升级,建立双方的连接。

1.所以编解码器需要用到HttpServerCodec

2.WebSocketServerProtocolHandler是netty进行websocket升级的处理器。在这期间会抹除http相关的信息,比如请求头啥的。如果想获取相关信息,需要在这之前获取。

3.HttpHeadersHandler是我们自己的处理器。赶在websocket升级之前,获取用户的ip地址,然后保存到channel的附件里。

4.NettyWebSocketServerHandler是我们的业务处理器,里面处理客户端的事件。

5.IdleStateHandler实现心跳检测。

  • 测试

发送 ws请求

4. 获取用户ip与 token

/**
 * 获取IP
 * 从HTTP请求的头部信息中获取相关参数,并保存到NettyUtil中的属性中。
 */
public class HttpHeadersHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(io.netty.channel.ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpRequest) {
            FullHttpRequest request = (FullHttpRequest) msg;
            UrlBuilder urlBuilder = UrlBuilder.ofHttp(request.uri());

            //获取token
            String token = Optional.ofNullable(urlBuilder.getQuery()).map(k -> k.get("token")).map(CharSequence::toString).orElse("");
            NettyUtil.setAttribute(ctx.channel(), NettyUtil.TOKEN, token);

            //获取请求路径
            request.setUri(urlBuilder.getPath().toString());
            HttpHeaders headers = request.headers();
            String ip = headers.get("X-Real-IP");
            if(StringUtils.isEmpty(ip)){//如果没经过nginx,就直接获取远端地址
                InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
                ip = address.getAddress().getHostAddress();
            }
            NettyUtil.setAttribute(ctx.channel(), NettyUtil.IP, ip);
            ctx.pipeline().remove(this);
            ctx.fireChannelRead(request);
        }else{
            ctx.fireChannelRead(msg);
        }
    }
}

ChannelHandlerContext是Netty中的一个类,用于表示一个通道处理器上下文。它提供了与通道关联的处理器链的访问,并提供了一些方法来执行常见的通道操作,例如读取数据、写入数据、关闭通道等。ChannelHandlerContext还提供了处理通道中断事件和处理异常的方法。在使用Netty进行网络编程时,通常会在ChannelHandlerContext上添加处理器来处理通道上的各种事件。

/**
 * netty工具类
 */
public class NettyUtil {
    public static AttributeKey<String> TOKEN = AttributeKey.valueOf("token");
    public static AttributeKey<String> IP = AttributeKey.valueOf("ip");
    public static AttributeKey<Long> UID = AttributeKey.valueOf("uid");

    public static AttributeKey<WebSocketServerHandshaker> HANDSHAKER_ATTR_KEY = AttributeKey.valueOf(WebSocketServerHandshaker.class, "HANDSHAKER");

    public static <T> void setAttribute(Channel channel, AttributeKey<T> key, T value) {
        Attribute<T> attr = channel.attr(key);
        attr.set(value);
    }

    public static <T> T getAttr(Channel channel, AttributeKey<T> ip) {
        return channel.attr(ip).get();
    }
}
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Netty是一个基于Java的网络编程框架,它提供了一种简单且高性能的方式来实现WebSocket协议。 要使用Netty实现WebSocket,可以按照以下步骤进行操作: 1. 创建一个新的Netty项目,并添加Netty的依赖。 2. 创建一个WebSocket服务器类,该类需要继承自`io.netty.channel.SimpleChannelInboundHandler`。 3. 在服务器类中,重写`channelRead0`方法,处理接收到的WebSocket消息。 4. 在服务器类中,重写`channelActive`和`channelInactive`方法,处理WebSocket连接的打开和关闭事件。 5. 在服务器类中,重写`exceptionCaught`方法,处理异常情况。 6. 创建一个启动类,在其中创建并配置一个`io.netty.bootstrap.ServerBootstrap`实例。 7. 在启动类中,绑定服务器端口并启动服务器。 下面是一个简单的示例代码,演示了如何使用Netty实现WebSocket服务器: ```java import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; 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.codec.http.websocketx.WebSocketServerProtocolHandler; public class WebSocketServer { public static void main(String[] args) throws Exception { 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()); ch.pipeline().addLast(new HttpObjectAggregator(65536)); ch.pipeline().addLast(new WebSocketServerProtocolHandler("/websocket")); ch.pipeline().addLast(new WebSocketServerHandler()); } }); ChannelFuture future = bootstrap.bind(8080).sync(); future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` 在上面的代码中,`WebSocketServerHandler`是自定义的处理器,用于处理WebSocket消息。你可以根据自己的需求来实现该处理器。 请注意,这只是一个简单的示例,实际的WebSocket服务器可能需要更复杂的处理逻辑。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值