Netty-websocket的使用

使用Netty-websocket构建一个简易的聊天室

What's netty? The king of network programming;


  • 下面是使用Netty编写的一个very very simple 的一个聊天室服务,但是基本上涵盖了Netty的核心使用,直接上代码
  • 关于Netty的知识点,本人计划单独写一篇以做一总结,一方面消化看过的知识,另一方面也希望能帮助到其他的小伙伴

直接上代码:

  • 进行Http请求处理的Handler
package com.netty.websocket.ws;

import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import lombok.extern.slf4j.Slf4j;

/**
 * Http请求处理handler
 *
 * @create 2021-01-18 10:55
 */
@Slf4j
public class HttpRequestHandler extends SimpleChannelInboundHandler {

    private final String wsUrl;

    public HttpRequestHandler(String wsUrl) {
        this.wsUrl = wsUrl;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        //如果发送的是到websocket端点的连接,则传递给下一个Handler
        if (wsUrl.equalsIgnoreCase(request.uri())) {
            ctx.fireChannelRead(request.retain());
        } else {
            if (HttpUtil.is100ContinueExpected(request)) {
                send100Continue(ctx);
            }

            DefaultHttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
            boolean keepAlive = HttpUtil.isKeepAlive(request);
            if (keepAlive) {
                response.headers()
                        .set(HttpHeaderNames.CONTENT_LENGTH, 1024 * 10)
                        .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            }
            ctx.write(response);

            ChannelFuture channelFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            if (!keepAlive){
                //如果不是keep-Alive连接,则在发送完一次response后关闭channel
                channelFuture.addListener(ChannelFutureListener.CLOSE);
            }
        }
    }

    private static void send100Continue(ChannelHandlerContext ctx) {
        DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
        ctx.writeAndFlush(response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("caught exception:",cause);
        ctx.close();
    }
}
  • 处理websocket消息的 WebSocketFrameHandler
package com.netty.websocket.ws;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import lombok.extern.slf4j.Slf4j;

/**
 * TextWebSocketFrame处理handler
 *
 * @create 2021-01-18 14:07
 */
@Slf4j
public class WebSocketFrameHandler extends SimpleChannelInboundHandler {

    private final ChannelGroup group;

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

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        //此处,因为是继承了 SimpleChannelInboundHandler,会在channelRead()方法自动释放资源
        //为了保持异步发送中msg的引用不被清理,需要使用retain()使其引用计数+1
        this.writeAndFlush(ctx,msg.retain());
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        //握手完成后,上线通知
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
            ctx.pipeline().remove(HttpRequestHandler.class);

            TextWebSocketFrame socketFrame = new TextWebSocketFrame("client " + ctx.channel() + " joined");
            writeAndFlush(ctx,socketFrame);

            log.info("client channelId={} joined",ctx.channel().id());
            group.add(ctx.channel());
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    /**
     * 广播除自身以外的其它channel
     * @param ctx
     * @param socketFrame
     */
    private void writeAndFlush(ChannelHandlerContext ctx,TextWebSocketFrame socketFrame) {
        //使用group进行广播
        for (Channel channel : group) {
            if (channel.id()!=ctx.channel().id()){
                channel.writeAndFlush(socketFrame);
            }
        }
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        super.channelRegistered(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        log.info("client channelId={} exited", ctx.channel().id());
        ctx.channel().close();
    }
}
  • websocket服务构建类 ChatServer
package com.netty.websocket.ws;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
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;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.concurrent.ImmediateEventExecutor;
import lombok.extern.slf4j.Slf4j;

/**
 * initializer初始化channelPipeline
 *
 * @create 2021-01-18 14:29
 */
@Slf4j
public class ChatServer {

    private final int port;

    /**
     * 不指定group线程数时默认实现为 NettyRuntime.availableProcessors() * 2
     */
    private final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    private final EventLoopGroup workGroup = new NioEventLoopGroup();
    private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
    private Channel channel;

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

    /**
     * 初始化websocket服务并绑定端口
     * @throws Exception
     */
    public void init() throws Exception {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup,workGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new HttpServerCodec())
                                .addLast(new ChunkedWriteHandler())
                                .addLast(new HttpObjectAggregator(64 * 1024))
                                .addLast(new HttpRequestHandler("/ws"))
                                .addLast(new WebSocketServerProtocolHandler("/ws", true))
                                .addLast(new WebSocketFrameHandler(channelGroup));
                    }
                });
        bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
        //禁用Nagle算法,小数据实时低延迟发送
        bootstrap.childOption(ChannelOption.TCP_NODELAY, true);

        ChannelFuture future = bootstrap.bind(port);
        future.syncUninterruptibly();
        future.addListener(new ChannelFutureListener() {
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                    log.info("Websocket server has bind at port {}", port);
                } else {
                    log.info("Websocket server failed to bind at port {}", port);
                }
            }
        });

        channel = future.channel();
    }

    /**
     * 服务销毁时关闭channel并释放线程资源
     */
    public void destroy(){
        log.info("Websocket server is stopping...");

        try {
            if (channel!=null){
                channel.close();
            }
            channelGroup.close();
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        } catch (Exception e) {
            log.error("Websocket server destroy exception:",e);
        }

        log.info("Websocket server has stopped");
    }
}
  • 初始化服务启动类 ChatServerStarter
package com.netty.websocket.ws;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

/**
 * websocket服务bean
 *
 * @create 2021-01-18 15:36
 */
@Component
@Slf4j
public class ChatServerStarter {

    @Value("${websocket.server.port}")
    private String port;

    static ChatServer chatServer;

    @PostConstruct
    public void serverStart() {
        try {
            chatServer = new ChatServer(Integer.parseInt(port));
            chatServer.init();
        } catch (Exception e) {
            log.error("websocket server start error:", e);
        }
    }

    @PreDestroy
    public void serverDestroy() {
        chatServer.destroy();
    }
}

websocket连接测试可以用这个地址:websocket在线测试
拓展:目前只是有了实时的消息功能,相当与是模拟了一个聊天室,后续可以通过在服务中维护一个队列(可以借助Redis,es等)实现历史记录功能

你可能感兴趣的:(Netty-websocket的使用)