在SpringBoot中整合使用Netty框架的詳細教程
Netty是一個非常優(yōu)秀的Socket框架。如果需要在SpringBoot開發(fā)的app中,提供Socket服務(wù),那么Netty是不錯的選擇。
Netty與SpringBoot的整合,我想無非就是要整合幾個地方
- 讓netty跟springboot生命周期保持一致,同生共死
- 讓netty能用上ioc中的Bean
- 讓netty能讀取到全局的配置
整合Netty,提供WebSocket服務(wù)
這里演示一個案例,在SpringBoot中使用Netty提供一個Websocket服務(wù)。
servlet容器本身提供了websocket的實現(xiàn),但這里用netty的實現(xiàn) :sparkling_heart:
添加依賴
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> </dependency>
是的,不用聲明版本號。因為 spring-boot-dependencies 中已經(jīng)聲明了最新的netty依賴。
通過yaml配置基本的屬性
server: port: 80 logging: level: root: DEBUG management: endpoints: web: exposure: include: "*" endpoint: shutdown: enabled: true netty: websocket: # Websocket服務(wù)端口 port: 1024 # 綁定的網(wǎng)卡 ip: 0.0.0.0 # 消息幀最大體積 max-frame-size: 10240 # URI路徑 path: /channel
App使用了, actuator ,并且開啟暴露了 shutdown
端點,可以讓SpringBoot App優(yōu)雅的停機。 在這里通過 netty.websocket.*
配置 websocket服務(wù)相關(guān)的配置。
通過 ApplicationRunner 啟動Websocket服務(wù)
import java.net.InetSocketAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import org.springframework.stereotype.Component; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; 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.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; import io.netty.handler.stream.ChunkedWriteHandler; import io.springboot.netty.websocket.handler.WebsocketMessageHandler; /** * 初始化Netty服務(wù) * @author Administrator */ @Component public class NettyBootsrapRunner implements ApplicationRunner, ApplicationListener<ContextClosedEvent>, ApplicationContextAware { private static final Logger LOGGER = LoggerFactory.getLogger(NettyBootsrapRunner.class); @Value("${netty.websocket.port}") private int port; @Value("${netty.websocket.ip}") private String ip; @Value("${netty.websocket.path}") private String path; @Value("${netty.websocket.max-frame-size}") private long maxFrameSize; private ApplicationContext applicationContext; private Channel serverChannel; public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public void run(ApplicationArguments args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup); serverBootstrap.channel(NioServerSocketChannel.class); serverBootstrap.localAddress(new InetSocketAddress(this.ip, this.port)); serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new ChunkedWriteHandler()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if(msg instanceof FullHttpRequest) { FullHttpRequest fullHttpRequest = (FullHttpRequest) msg; String uri = fullHttpRequest.uri(); if (!uri.equals(path)) { // 訪問的路徑不是 websocket的端點地址,響應(yīng)404 ctx.channel().writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND)) .addListener(ChannelFutureListener.CLOSE); return ; } } super.channelRead(ctx, msg); } }); pipeline.addLast(new WebSocketServerCompressionHandler()); pipeline.addLast(new WebSocketServerProtocolHandler(path, null, true, maxFrameSize)); /** * 從IOC中獲取到Handler */ pipeline.addLast(applicationContext.getBean(WebsocketMessageHandler.class)); } }); Channel channel = serverBootstrap.bind().sync().channel(); this.serverChannel = channel; LOGGER.info("websocket 服務(wù)啟動,ip={},port={}", this.ip, this.port); channel.closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public void onApplicationEvent(ContextClosedEvent event) { if (this.serverChannel != null) { this.serverChannel.close(); } LOGGER.info("websocket 服務(wù)停止"); } }
NettyBootsrapRunner
實現(xiàn)了 ApplicationRunner, ApplicationListener<ContextClosedEvent>
, ApplicationContextAware
接口。
這樣一來, NettyBootsrapRunner
可以在App的啟動和關(guān)閉時執(zhí)行Websocket服務(wù)的啟動和關(guān)閉。而且通過 ApplicationContextAware
還能獲取到 ApplicationContext
通過IOC管理 Netty 的Handler
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler.Sharable; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.springboot.netty.service.DiscardService; /** * * @author Administrator * */ @Sharable @Component public class WebsocketMessageHandler extends SimpleChannelInboundHandler<WebSocketFrame> { private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketMessageHandler.class); @Autowired DiscardService discardService; @Override protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) throws Exception { if (msg instanceof TextWebSocketFrame) { TextWebSocketFrame textWebSocketFrame = (TextWebSocketFrame) msg; // 業(yè)務(wù)層處理數(shù)據(jù) this.discardService.discard(textWebSocketFrame.text()); // 響應(yīng)客戶端 ctx.channel().writeAndFlush(new TextWebSocketFrame("我收到了你的消息:" + System.currentTimeMillis())); } else { // 不接受文本以外的數(shù)據(jù)幀類型 ctx.channel().writeAndFlush(WebSocketCloseStatus.INVALID_MESSAGE_TYPE).addListener(ChannelFutureListener.CLOSE); } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); LOGGER.info("鏈接斷開:{}", ctx.channel().remoteAddress()); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); LOGGER.info("鏈接創(chuàng)建:{}", ctx.channel().remoteAddress()); } }
handler已經(jīng)是一個IOC管理的Bean,可以自由的使用依賴注入等Spring帶來的快捷功能。由于是單例存在,所有的鏈接都使用同一個hander,所以盡量不要保存任何實例變量。
這個Handler處理完畢客戶端的消息后,給客戶端會響應(yīng)一條: "我收到了你的消息:" + System.currentTimeMillis()
的消息
為了演示在Handler中使用業(yè)務(wù)層,這里假裝注入了一個 DiscardService
服務(wù)。它的邏輯很簡單,就是丟棄消息
public void discard (String message) { LOGGER.info("丟棄消息:{}", message); }
演示
啟動客戶端
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Websocket</title> </head> <body> </body> <script type="text/javascript"> ;(function(){ const websocket = new WebSocket('ws://localhost:1024/channel'); websocket.onmessage = e => { console.log('收到消息:', e.data); } websocket.onclose = e => { let {code, reason} = e; console.log(`鏈接斷開:code=$[code], reason=${reason}`); } websocket.onopen = () => { console.log(`鏈接建立...`); websocket.send('Hello'); } websocket.onerror = e => { console.log('鏈接異常:', e); } })(); </script> </html>
鏈接創(chuàng)建后就給服務(wù)端發(fā)送一條消息: Hello
關(guān)閉服務(wù)端
使用 PostMan 請求服務(wù)器的停機端點
日志
客戶端日志
服務(wù)端日志
2020-06-22 17:08:22.728 INFO 9392 --- [ main] io.undertow : starting server: Undertow - 2.1.3.Final
2020-06-22 17:08:22.740 INFO 9392 --- [ main] org.xnio : XNIO version 3.8.0.Final
2020-06-22 17:08:22.752 INFO 9392 --- [ main] org.xnio.nio : XNIO NIO Implementation Version 3.8.0.Final
2020-06-22 17:08:22.839 INFO 9392 --- [ main] org.jboss.threads : JBoss Threads version 3.1.0.Final
2020-06-22 17:08:22.913 INFO 9392 --- [ main] o.s.b.w.e.undertow.UndertowWebServer : Undertow started on port(s) 80 (http)
2020-06-22 17:08:22.931 INFO 9392 --- [ main] io.springboot.netty.NettyApplication : Started NettyApplication in 4.536 seconds (JVM running for 5.175)
2020-06-22 17:08:23.653 INFO 9392 --- [ main] i.s.n.w.runner.NettyBootsrapRunner : websocket 服務(wù)啟動,ip=0.0.0.0,port=1024
2020-06-22 17:08:28.484 INFO 9392 --- [ XNIO-1 task-1] io.undertow.servlet : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-06-22 17:08:28.484 INFO 9392 --- [ XNIO-1 task-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-06-22 17:08:28.492 INFO 9392 --- [ XNIO-1 task-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 8 ms
2020-06-22 17:08:28.724 INFO 9392 --- [ntLoopGroup-3-1] i.s.n.w.handler.WebsocketMessageHandler : 鏈接創(chuàng)建:/0:0:0:0:0:0:0:1:12093
2020-06-22 17:08:28.790 INFO 9392 --- [ntLoopGroup-3-1] i.s.netty.service.DiscardService : 丟棄消息:Hello
2020-06-22 17:08:33.688 INFO 9392 --- [ Thread-232] i.s.n.w.runner.NettyBootsrapRunner : websocket 服務(wù)停止
2020-06-22 17:08:33.691 INFO 9392 --- [ntLoopGroup-3-1] i.s.n.w.handler.WebsocketMessageHandler : 鏈接斷開:/0:0:0:0:0:0:0:1:12093
2020-06-22 17:08:33.699 INFO 9392 --- [ Thread-232] io.undertow : stopping server: Undertow - 2.1.3.Final
2020-06-22 17:08:33.704 INFO 9392 --- [ Thread-232] io.undertow.servlet : Destroying Spring FrameworkServlet 'dispatcherServlet'
2020-06-22 17:08:33.708 INFO 9392 --- [ Thread-232] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
Netty會在SpringBoot App啟動后啟動,App停止后關(guān)閉,可以正常的對外提供服務(wù) 并且Handler交給IOC管理可以注入Service,完成業(yè)務(wù)處理。
總結(jié)
到此這篇關(guān)于在SpringBoot中整合使用Netty框架的文章就介紹到這了,更多相關(guān)SpringBoot整合Netty框架內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決springboot responseentity<string>亂碼問題
這篇文章主要介紹了解決springboot responseentity<string>亂碼問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07Java中@ConfigurationProperties實現(xiàn)自定義配置綁定問題分析
這篇文章主要介紹了@ConfigurationProperties實現(xiàn)自定義配置綁定問題,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08Java函數(shù)式開發(fā) Optional空指針處理
本文主要介紹Java函數(shù)式開發(fā) Optional空指針處理,這里整理了相關(guān)資料,及示例代碼,有興趣的小伙伴可以參考下2016-09-09如何修改覆蓋spring boot默認日志策略logback詳解
這篇文章主要給大家介紹了關(guān)于如何修改覆蓋spring boot默認日志策略logback的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-10-10