springboot实现webocket长连接(四)--netty+websocekt+服务端

springboot实现webocket长连接(四)

demo下载地址:多种websocket实现方式,其中有基于spring-websocekt,也有基于netty框架,即下即用。

之前的博客使用了spring-websocket实现了websocket服务端,现在我们利用netty框架实现,更灵活,更性能。在一些复杂场景下,可以通过调整参数提高效率。
之前的实现可以参考:

springboot实现webocket(一)
springboot实现webocket(二)

首先定义一个netty的server端,用于启动端口。

说明一下,netty需要占用一个端口,如果你的项目也提供了web服务,两者端口不能一样。

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 网状服务器
 *
 * @author lukou
 * @date 2023/05/17
 */
public class NettyServer {

    private static final Logger log = LoggerFactory.getLogger(NettyServer.class);

    private int port;
    private Channel channel;
    private EventLoopGroup bossGroup;
    private EventLoopGroup workGroup;
    private ChannelInitializer<SocketChannel> channelInitializer;

    public NettyServer(int port, ChannelInitializer<SocketChannel> channelInitializer) {
        this.port = port;
        this.channelInitializer = channelInitializer;
        bossGroup = new NioEventLoopGroup();
        workGroup = new NioEventLoopGroup();
    }

    /**
     * 开始
     *
     * @throws Exception 异常
     */
    public void start() throws Exception {
        try {
            ServerBootstrap sb = new ServerBootstrap();
            //绑定线程池
            sb.group(bossGroup, workGroup)
                    //指定使用的channel
                    .channel(NioServerSocketChannel.class)
                    //临时存放已完成三次握手的请求的队列的最大长度
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    //禁用nagle算法,不等待,立即发送
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    //当没有数据包过来时超过一定时间主动发送一个ack探测包
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    //允许共用端口
                    .childOption(ChannelOption.SO_REUSEADDR, true)
                    //绑定监听端口
                    .localAddress(this.port)
                    //添加自定义处理器
                    .childHandler(this.channelInitializer);
            //服务器异步创建绑定
            ChannelFuture cf = sb.bind().sync();
            channel = cf.channel();
            log.info("netty服务启动。。正在监听:[{}]", channel.localAddress());
            //关闭服务器通道
            channel.closeFuture().sync();
        } catch (Exception e) {
            throw new Exception("启动netty服务发生异常,端口号:" + this.port, e);
        }
    }

    /**
     * 摧毁
     *
     * @throws Exception 异常
     */
    public void destroy() throws Exception {
        try {
            channel.close().sync();
            workGroup.shutdownGracefully().sync();
            bossGroup.shutdownGracefully().sync();
        } catch (Exception e) {
            throw new Exception("停止netty服务发生异常,端口号:" + this.port, e);
        }

    }

}

接下来,需要实现业务处理逻辑的类,首先定义一个抽象类,将一些公共逻辑放到里面

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import static io.netty.handler.codec.http.HttpMethod.GET;

/**
 * 基本套接字服务器
 * 抽象了一层.<br>
 *
 * @author lukou
 * @date 2023/05/17
 */
public abstract class BaseSocketServer extends SimpleChannelInboundHandler<Object> {

    private static final Logger log = LoggerFactory.getLogger(BaseSocketServer.class);

    /**websocket协议内容*/
    public static final String WEBSOCKET = "websocket";
    public static final String UPGRADE = "Upgrade";

    /**
     * 客户端连接地址
     */
    public static final String ENDPOINT = "/example4/ws";

    /**
     * 连接唯一id,方便链路追踪
     */
    protected String taskId;

    /**
     * 上下文
     */
    protected ChannelHandlerContext context;

    /**
     * websocket握手处理器
     */
    private WebSocketServerHandshaker webSocketServerHandshaker;

    /**
     * 通道活性
     * 客户端与服务端创建链接的时候调用.<br>
     *
     * @param context 上下文
     */
    @Override
    public abstract void channelActive(ChannelHandlerContext context);

    /**
     * 频道不活跃
     * 客户端与服务端断开连接的时候调用.<br>
     *
     * @param context 上下文
     */
    @Override
    public abstract void channelInactive(ChannelHandlerContext context);

    /**
     * 通道读完整
     * 服务端接收客户端发送过来的数据结束之后调用.<br>
     *
     * @param context 上下文
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext context) {
        context.flush();
    }

    /**
     * 例外了
     * 工程出现异常的时候调用.<br>
     *
     * @param context   上下文
     * @param throwable throwable
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext context, Throwable throwable) {
        context.close();
        log.info("taskId:[{}]中发生错误,原因:[{}]", this.taskId, throwable.toString(), throwable);
    }

    /**
     * 通道read0
     * 连接和帧信息.<br>
     *
     * @param ctx ctx
     * @param msg 味精
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
        if (msg instanceof WebSocketFrame) {
            this.handWebSocketFrame(ctx, (WebSocketFrame) msg);
            return;
        }
        if (msg instanceof FullHttpRequest) {
            log.info("taskId:[{}]开始处理websocket握手请求。。", taskId);
            this.httpRequestHandler(ctx, (FullHttpRequest) msg);
            log.info("taskId:[{}]处理websocket握手请求结束。。", taskId);
        }
    }

    /**
     * 用户事件触发
     * 这里设置了一个读超时事件,可以参考{@link Example4WebSocketChannelHandler}中设置
     *
     * @param ctx ctx
     * @param evt evt
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (IdleStateEvent.class.isAssignableFrom(evt.getClass())) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                ctx.close();
                log.info("taskId:[{}]读操作超时。。断开连接。。", this.taskId);
            }
        }
    }

    /**
     * 处理客户端与服务端之间的websocket业务.<br>
     *
     * @param context        上下文
     * @param webSocketFrame 网络套接字框架
     */
    public void handWebSocketFrame(ChannelHandlerContext context, WebSocketFrame webSocketFrame) {
        //判断是否是关闭websocket的指令
        if (webSocketFrame instanceof CloseWebSocketFrame) {
            webSocketServerHandshaker.close(context.channel(), (CloseWebSocketFrame) webSocketFrame.retain());
            log.info("taskId:[{}]接收到关闭帧。。断开连接。。", this.taskId);
            return;
        }
        //判断是否是ping消息
        if (webSocketFrame instanceof PingWebSocketFrame) {
            context.channel().write(new PongWebSocketFrame(webSocketFrame.content().retain()));
            log.info("taskId:[{}]接收到心跳帧。。", this.taskId);
            return;
        }
        //判断是否是二进制消息
        if (webSocketFrame instanceof TextWebSocketFrame) {
            this.handTextWebSocketFrame(context, webSocketFrame);
        }
    }

    /**
     * http请求处理程序
     * http握手请求校验.<br>
     *
     * @param context         上下文
     * @param fullHttpRequest 完整http请求
     */
    private void httpRequestHandler(ChannelHandlerContext context, FullHttpRequest fullHttpRequest) {
        //判断是否http握手请求
        if (!fullHttpRequest.decoderResult().isSuccess() || !(WEBSOCKET.equals(fullHttpRequest.headers().get(UPGRADE)))
                || !GET.equals(fullHttpRequest.method())) {
            sendHttpResponse(context, new DefaultFullHttpResponse(fullHttpRequest.protocolVersion(), HttpResponseStatus.BAD_REQUEST));
            log.error("taskId:{{}}websocket握手内容不正确。。响应并关闭。。", taskId);
            return;
        }
        String uri = fullHttpRequest.uri();
        log.info("taskId:{{}}websocket握手uri[{}]", taskId, uri);
        if (!ENDPOINT.equals(getBasePath(uri))) {
            sendHttpResponse(context, new DefaultFullHttpResponse(fullHttpRequest.protocolVersion(), HttpResponseStatus.NOT_FOUND));
            log.info("taskId:[{}]websocket握手协议不正确。。响应并关闭。。", taskId);
            return;
        }
        WebSocketServerHandshakerFactory webSocketServerHandshakerFactory = new WebSocketServerHandshakerFactory("", null, false);
        webSocketServerHandshaker = webSocketServerHandshakerFactory.newHandshaker(fullHttpRequest);
        if (webSocketServerHandshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(context.channel());
            log.info("taskId:[{}]websocket握手协议版本不正确。。响应并关闭。。", taskId);
            return;
        }
        webSocketServerHandshaker.handshake(context.channel(), fullHttpRequest);
        this.checkOpenInfo(context, fullHttpRequest);
    }

    /**
     * 得到基本路径
     *
     * @param url url
     * @return {@link String}
     */
    public static String getBasePath(String url) {
        if (StringUtils.isEmpty(url)) {
            return null;
        }
        int idx = url.indexOf("?");
        if (idx == -1) {
            return url;
        }
        return url.substring(0, idx);
    }

    /**
     * 发送http响应
     * 服务端发送响应消息.<br>
     *
     * @param context                 上下文
     * @param defaultFullHttpResponse 默认完整http响应
     */
    private void sendHttpResponse(ChannelHandlerContext context, DefaultFullHttpResponse defaultFullHttpResponse) {
        if (defaultFullHttpResponse.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(defaultFullHttpResponse.status().toString(), CharsetUtil.UTF_8);
            defaultFullHttpResponse.content().writeBytes(buf);
            buf.release();
        }
        //服务端向客户端发送数据
        ChannelFuture future = context.channel().writeAndFlush(defaultFullHttpResponse);
        if (defaultFullHttpResponse.status().code() != 200) {
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }

    /**
     * 回复消息给客户端.<br>
     *
     * @param message 消息
     * @return {@link ChannelFuture}
     */
    protected ChannelFuture reply( String message) {
        ChannelFuture channelFuture = context.writeAndFlush(new TextWebSocketFrame(message));
        log.info("taskId:[{}]回复给客户端消息完成:[{}]", this.taskId, message);
        return channelFuture;
    }

    /**
     * 检查打开信息
     * 检验连接打开时的信息.<br>
     *
     * @param context         上下文
     * @param fullHttpRequest 完整http请求
     */
    protected abstract void checkOpenInfo(ChannelHandlerContext context, FullHttpRequest fullHttpRequest);


    /**
     * 手文本框架网络套接字
     * 文本帧处理.<br>
     *
     * @param context        上下文
     * @param webSocketFrame 网络套接字框架
     */
    protected abstract void handTextWebSocketFrame(ChannelHandlerContext context, WebSocketFrame webSocketFrame);

}

实例化

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
 * 服务段实例化
 *
 * @author lukou
 * @date 2023/05/17
 */
@Component
public class MyWebSocketServer extends BaseSocketServer {

    private static final Logger log = LoggerFactory.getLogger(MyWebSocketServer.class);

    @Override
    public void channelActive(ChannelHandlerContext context) {
        this.taskId = UUID.randomUUID().toString().replaceAll("-", "");
        this.context = context;
        log.info("taskId:[{}]有一个新请求进来了。。开始初始化上下文。。。", this.taskId);
    }

    @Override
    public void channelInactive(ChannelHandlerContext context) {
        log.info("taskId:[{}]识别服务触发关闭事件.", this.taskId);
        // 这边可以收尾处理
    }

    @Override
    protected void checkOpenInfo(ChannelHandlerContext context, FullHttpRequest fullHttpRequest) {
        log.info("taskId:[{}]识别服务中websocket握手协议正确。。开始校验其它。。", this.taskId);
    }

    @Override
    protected void handTextWebSocketFrame(ChannelHandlerContext context, WebSocketFrame webSocketFrame) {
        String text = ((TextWebSocketFrame) webSocketFrame).text();
        this.reply(this.taskId + " : " + text + System.currentTimeMillis());
    }
}

之后,将业务处理层绑定到netty的channel上

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
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.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;

import java.util.concurrent.TimeUnit;

/**
 * example4网络套接字通道处理程序
 *
 * @author lukou
 * @date 2023/05/17
 */
public class Example4WebSocketChannelHandler extends ChannelInitializer<SocketChannel> {

    private static final EventExecutorGroup EVENT_EXECUTOR_GROUP = new DefaultEventExecutorGroup(100);

    @Override
    protected void initChannel(SocketChannel ch) {
        // 设置30秒没有读到数据,则触发一个READER_IDLE事件。
        ch.pipeline().addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS));
        // websocket协议本身就是基于http协议的,所以这边也要使用http编解码器
        ch.pipeline().addLast(new HttpServerCodec());
        // 以块的方式来写处理器
        ch.pipeline().addLast(new ChunkedWriteHandler());
        // netty是基于分段请求的,HttpObjectAggregator的作用是将请求分段再聚合,参数是聚合字节的最大长度
        ch.pipeline().addLast(new HttpObjectAggregator(8192));
        // 在管道中添加我们自己的接收数据实现方法
        ch.pipeline().addLast(EVENT_EXECUTOR_GROUP, new MyWebSocketServer());
        ch.pipeline().addLast(new WebSocketServerProtocolHandler(BaseSocketServer.ENDPOINT, null, true, 65536 * 10));
    }

}

之后,就是真正的使用了,这里是选择项目一启动就执行netty服务端,并注入到容器中(这里看个人选择,不一定非要注入到spring中,直接new也一样)。

import com.example.wsdemo.websocketserver.example4.netty.Example4WebSocketChannelHandler;
import com.example.wsdemo.websocketserver.example4.netty.NettyServer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ServerConfig {

    @Value("${netty.websocket.port:8081}")
    private int port;


    @Bean("example4WebSocketChannelHandler")
    public Example4WebSocketChannelHandler example4WebSocketChannelHandler() {
        return new Example4WebSocketChannelHandler();
    }

    @Bean("nettyServer")
    public NettyServer nettyServer(Example4WebSocketChannelHandler example4WebSocketChannelHandler) {
        return new NettyServer(this.port, example4WebSocketChannelHandler);
    }
}
import com.example.wsdemo.websocketserver.example4.netty.NettyServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import javax.annotation.Resource;

/**
 * example4网络套接字服务端启动初始化
 *
 * @author lukou
 * @date 2023/05/17
 */
@Component
public class Example4WebSocketStartInit implements CommandLineRunner {

    private static final Logger log = LoggerFactory.getLogger(Example4WebSocketStartInit.class);

    @Resource
    private NettyServer nettyServer;

    /**
     * 需要异步启动,不然会阻塞主线程
     * 这里自定义一个线程启动,也可以在方法上加上注解@Async,一样的效果
     *
     * @param args arg游戏
     */
    @Override
    public void run(String... args) {
        new Thread(() -> {
            try {
                nettyServer.start();
            } catch (Exception e) {
                log.error("识别服务中netty服务启动报错!", e);
            }
        }).start();
    }

    @PreDestroy
    public void destroy() {
        if (nettyServer != null) {
            try {
                nettyServer.destroy();
            } catch (Exception e) {
                log.error("停止netty服务发生异常!", e);
            }
        }
        log.info("netty识别服务已经销毁。。");
    }
}

启动测试,出现如下就代表服务启动成功了。

 .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.5.RELEASE)

2023-05-18 14:36:06.423  INFO 3624 --- [           main] c.e.w.w.WebsocketServerApplication       : Starting WebsocketServerApplication on qianpeng with PID 3624 (D:\projects\websocket-max\websocket-server\target\classes started by 钱鹏 in D:\projects\websocket-max)
2023-05-18 14:36:06.425  INFO 3624 --- [           main] c.e.w.w.WebsocketServerApplication       : No active profile set, falling back to default profiles: default
2023-05-18 14:36:06.957  INFO 3624 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 9000 (http)
2023-05-18 14:36:06.963  INFO 3624 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-05-18 14:36:06.963  INFO 3624 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.39]
2023-05-18 14:36:07.012  INFO 3624 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-05-18 14:36:07.012  INFO 3624 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 560 ms
2023-05-18 14:36:07.078  INFO 3624 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'clientInboundChannelExecutor'
2023-05-18 14:36:07.079  INFO 3624 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'clientOutboundChannelExecutor'
2023-05-18 14:36:07.164  INFO 3624 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'defaultSockJsTaskScheduler'
2023-05-18 14:36:07.181  INFO 3624 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'messageBrokerTaskScheduler'
2023-05-18 14:36:07.186  INFO 3624 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'brokerChannelExecutor'
2023-05-18 14:36:07.319  INFO 3624 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 9000 (http) with context path ''
2023-05-18 14:36:07.320  INFO 3624 --- [           main] o.s.m.s.b.SimpleBrokerMessageHandler     : Starting...
2023-05-18 14:36:07.320  INFO 3624 --- [           main] o.s.m.s.b.SimpleBrokerMessageHandler     : BrokerAvailabilityEvent[available=true, SimpleBrokerMessageHandler [DefaultSubscriptionRegistry[cache[0 destination(s)], registry[0 sessions]]]]
2023-05-18 14:36:07.320  INFO 3624 --- [           main] o.s.m.s.b.SimpleBrokerMessageHandler     : Started.
2023-05-18 14:36:07.325  INFO 3624 --- [           main] c.e.w.w.WebsocketServerApplication       : Started WebsocketServerApplication in 1.11 seconds (JVM running for 1.619)
2023-05-18 14:36:07.679  INFO 3624 --- [       Thread-4] c.e.w.w.example4.netty.NettyServer       : netty服务启动。。正在监听:[/0:0:0:0:0:0:0:0:8081]

websocket的访问地址为:ws://localhost:8081/example4/ws

端口不再是项目的端口了。

另外,启动nettyserver的时候需要另起一个线程,不能直接在主线程中,不然会阻塞在那边的。

欢迎指正!

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值