Java SpringBoot Netty

前言

众所周知在进行网络连接的时候,建立套接字连接是一个非常消耗性能的事情,特别是在分布式的情况下,用线程池去保持多个客户端连接,是一种非常消耗线程的行为。
那么该通过什么技术去解决上述的问题呢,那么就不得不提一个网络连接的利器——Netty。

正文

Netty

Netty是一个NIO客户端服务器框架:

  • 它可快速轻松地开发网络应用程序,例如协议服务器和客户端。
  • 它极大地简化和简化了网络编程,例如TCP和UDP套接字服务器。

NIO是一种非阻塞IO ,它具有以下的特点

  • 单线程可以连接多个客户端。
  • 选择器可以实现但线程管理多个Channel,新建的通道都要向选择器注册。
  • 一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。
  • selector进行select()操作可能会产生阻塞,但是可以设置阻塞时间,并且可以用wakeup()唤醒selector,所以NIO是非阻塞IO。

    Netty模型selector模式

    它相对普通NIO的在性能上有了提升,采用了:

  • NIO采用多线程的方式可以同时使用多个selector

  • 通过绑定多个端口的方式,使得一个selector可以同时注册多个ServerSocketServer
  • 单个线程下只能有一个selector,用来实现Channel的匹配及复用

2021-08-02-14-30-32-670234.png

半包问题

TCP/IP在发送消息的时候,可能会拆包,这就导致接收端无法知道什么时候收到的数据是一个完整的数据。在传统的BIO中在读取不到数据时会发生阻塞,但是NIO不会。
为了解决NIO的半包问题,Netty在Selector模型的基础上,提出了reactor模式,从而解决客户端请求在服务端不完整的问题。

Netty模型reactor模式

在selector的基础上解决了半包问题。
2021-08-02-14-30-32-825324.png
上图,简单地可以描述为”boss接活,让work干”:manReactor用来接收请求(会与客户端进行握手验证),而subReactor用来处理请求(不与客户端直接连接)。

SpringBoot使用Netty实现远程调用

依赖

  1. <!--lombok-->
  2. <dependency>
  3. <groupId>org.projectlombok</groupId>
  4. <artifactId>lombok</artifactId>
  5. <version>1.18.2</version>
  6. <optional>true</optional>
  7. </dependency>
  8. <!--netty-->
  9. <dependency>
  10. <groupId>io.netty</groupId>
  11. <artifactId>netty-all</artifactId>
  12. <version>4.1.17.Final</version>
  13. </dependency>

服务端部分

NettyServer.java:服务启动监听器

  1. @Slf4j
  2. public class NettyServer {
  3. public void start() {
  4. InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8082);
  5. //new 一个主线程组
  6. EventLoopGroup bossGroup = new NioEventLoopGroup(1);
  7. //new 一个工作线程组
  8. EventLoopGroup workGroup = new NioEventLoopGroup(200);
  9. ServerBootstrap bootstrap = new ServerBootstrap()
  10. .group(bossGroup, workGroup)
  11. .channel(NioServerSocketChannel.class)
  12. .childHandler(new ServerChannelInitializer())
  13. .localAddress(socketAddress)
  14. //设置队列大小
  15. .option(ChannelOption.SO_BACKLOG, 1024)
  16. // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
  17. .childOption(ChannelOption.SO_KEEPALIVE, true);
  18. //绑定端口,开始接收进来的连接
  19. try {
  20. ChannelFuture future = bootstrap.bind(socketAddress).sync();
  21. log.info("服务器启动开始监听端口: {}", socketAddress.getPort());
  22. future.channel().closeFuture().sync();
  23. } catch (InterruptedException e) {
  24. log.error("服务器开启失败", e);
  25. } finally {
  26. //关闭主线程组
  27. bossGroup.shutdownGracefully();
  28. //关闭工作线程组
  29. workGroup.shutdownGracefully();
  30. }
  31. }
  32. }

ServerChannelInitializer.java:netty服务初始化器

  1. /**
  2. * netty服务初始化器
  3. **/
  4. public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
  5. @Override
  6. protected void initChannel(SocketChannel socketChannel) throws Exception {
  7. //添加编解码
  8. socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
  9. socketChannel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
  10. socketChannel.pipeline().addLast(new NettyServerHandler());
  11. }
  12. }

NettyServerHandler.java:netty服务端处理器

  1. /**
  2. * netty服务端处理器
  3. **/
  4. @Slf4j
  5. public class NettyServerHandler extends ChannelInboundHandlerAdapter {
  6. /**
  7. * 客户端连接会触发
  8. */
  9. @Override
  10. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  11. log.info("Channel active......");
  12. }
  13. /**
  14. * 客户端发消息会触发
  15. */
  16. @Override
  17. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  18. log.info("服务器收到消息: {}", msg.toString());
  19. ctx.write("你也好哦");
  20. ctx.flush();
  21. }
  22. /**
  23. * 发生异常触发
  24. */
  25. @Override
  26. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  27. cause.printStackTrace();
  28. ctx.close();
  29. }
  30. }

RpcServerApp.java:SpringBoot启动类

  1. /**
  2. * 启动类
  3. *
  4. */
  5. @Slf4j
  6. @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
  7. public class RpcServerApp extends SpringBootServletInitializer {
  8. @Override
  9. protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
  10. return application.sources(RpcServerApp.class);
  11. }
  12. /**
  13. * 项目的启动方法
  14. *
  15. * @param args
  16. */
  17. public static void main(String[] args) {
  18. SpringApplication.run(RpcServerApp.class, args);
  19. //开启Netty服务
  20. NettyServer nettyServer = new NettyServer ();
  21. nettyServer.start();
  22. log.info("======服务已经启动========");
  23. }
  24. }

客户端部分

NettyClientUtil.java:NettyClient工具类

  1. /**
  2. * Netty客户端
  3. **/
  4. @Slf4j
  5. public class NettyClientUtil {
  6. public static ResponseResult helloNetty(String msg) {
  7. NettyClientHandler nettyClientHandler = new NettyClientHandler();
  8. EventLoopGroup group = new NioEventLoopGroup();
  9. Bootstrap bootstrap = new Bootstrap()
  10. .group(group)
  11. //该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输
  12. .option(ChannelOption.TCP_NODELAY, true)
  13. .channel(NioSocketChannel.class)
  14. .handler(new ChannelInitializer<SocketChannel>() {
  15. @Override
  16. protected void initChannel(SocketChannel socketChannel) throws Exception {
  17. socketChannel.pipeline().addLast("decoder", new StringDecoder());
  18. socketChannel.pipeline().addLast("encoder", new StringEncoder());
  19. socketChannel.pipeline().addLast(nettyClientHandler);
  20. }
  21. });
  22. try {
  23. ChannelFuture future = bootstrap.connect("127.0.0.1", 8082).sync();
  24. log.info("客户端发送成功....");
  25. //发送消息
  26. future.channel().writeAndFlush(msg);
  27. // 等待连接被关闭
  28. future.channel().closeFuture().sync();
  29. return nettyClientHandler.getResponseResult();
  30. } catch (Exception e) {
  31. log.error("客户端Netty失败", e);
  32. throw new BusinessException(CouponTypeEnum.OPERATE_ERROR);
  33. } finally {
  34. //以一种优雅的方式进行线程退出
  35. group.shutdownGracefully();
  36. }
  37. }
  38. }

NettyClientHandler.java:客户端处理器

  1. /**
  2. * 客户端处理器
  3. **/
  4. @Slf4j
  5. @Setter
  6. @Getter
  7. public class NettyClientHandler extends ChannelInboundHandlerAdapter {
  8. private ResponseResult responseResult;
  9. @Override
  10. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  11. log.info("客户端Active .....");
  12. }
  13. @Override
  14. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  15. log.info("客户端收到消息: {}", msg.toString());
  16. this.responseResult = ResponseResult.success(msg.toString(), CouponTypeEnum.OPERATE_SUCCESS.getCouponTypeDesc());
  17. ctx.close();
  18. }
  19. @Override
  20. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  21. cause.printStackTrace();
  22. ctx.close();
  23. }
  24. }

验证

测试接口

  1. @RestController
  2. @Slf4j
  3. public class UserController {
  4. @PostMapping("/helloNetty")
  5. @MethodLogPrint
  6. public ResponseResult helloNetty(@RequestParam String msg) {
  7. return NettyClientUtil.helloNetty(msg);
  8. }
  9. }

访问测试接口

2021-08-02-14-30-33-003377.png

服务端打印信息

2021-08-02-14-30-33-185658.png

客户端打印信息

2021-08-02-14-30-33-497129.png