基于Netty的TCP协议的Socket服务端

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

上一节分享Websocket独立部署的一个设计思路,我们今天接着聊一下基于Netty的TCP协议的Socket服务端如何搭建。这个对于熟悉的人可能很简单,但是对于新手或者不常用的开发者来说,可能一头雾水。

小编在初次使用Socket的时候,都是度娘一大堆,然后抄抄抄,完成自己的任务。至于为什么这么做,完全不知道。这一节将自己的理解分享并记录下来,以备不时之需。

02 服务端案例

2.1 代码展示

java 复制代码
public void start() {
    // 创建线程组
    EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    EventLoopGroup workGroup = new NioEventLoopGroup(5);

    try {
        // 服务端类
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        // 添加组
        serverBootstrap.group(bossGroup, workGroup);
        // 设置NioServerSocketChannel通道
        serverBootstrap.channel(NioServerSocketChannel.class);
        // 连接队列大小
        serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
        // 保持连接
        serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); 
        serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>(){
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                // 设置处理器链,依次执行
                ChannelPipeline pipeline = socketChannel.pipeline();
                pipeline.addLast(new DelimiterBasedFrameDecoder(2048, Unpooled.copiedBuffer("_".getBytes())));
                pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
                pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));
                // 自定义的handler,处理业务逻辑
                pipeline.addLast(new BusinessHandler<>());    
            }
        });

        // 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
        ChannelFuture channelFuture = serverBootstrap.bind(9091).sync();
        log.info("Server started and listen on:{}",channelFuture.channel().localAddress());
        // 对关闭通道进行监听
        channelFuture.channel().closeFuture().sync();
    } catch (Exception e) {
        log.error("信息异常:", e);
    }finally {
        bossGroup.close();
        workGroup.close();
    }
}

2.2 创建线程组

java 复制代码
 // 创建线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workGroup = new NioEventLoopGroup(5);

EventLoopGroup就是一个线程池。在NettySocket中需要两个不同的线程池,分别处理不同的任务。其中bossGroup用来接收客户端连接,通常设置1个线程,而workGroup用来处理I/O操作和业务逻辑,可以根据CPU的核心数指定。

2.3 创建服务端

java 复制代码
// 服务端类
ServerBootstrap bootstrap = new ServerBootstrap();
// 添加组
serverBootstrap.group(bossGroup, workGroup);
// 设置NioServerSocketChannel通道
serverBootstrap.channel(NioServerSocketChannel.class);
// 连接队列大小
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
// 保持连接
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);

服务端引导类创建完成之后,需要设置参数:

  • group():添加线程池组,第一个参数为bossGroup,第二个是workGroup
  • channel():设置NioServerSocketChannel通道
  • option():配置服务端监听的ServerSocketChannel
  • childOption():配置客户端连接的SocketChannel

所以这里的ChannelOption.SO_BACKLOG只能设置在option中,用来指定服务端接收的任务最大队列。ChannelOption.SO_KEEPALIVE用来TCP保活机制,检测死连接,是针对客户端的连接,所以需要设置在childOption上。

ChannelOption.SO_KEEPALIVE也可以不同设置,采用心跳机制来保活。其使用有一定的局限性,通常都会通过心跳机制来代替。

2.4 设置处理链

java 复制代码
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>(){
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        // 设置处理器链,依次执行
        ChannelPipeline pipeline = socketChannel.pipeline();
      
        pipeline.addLast("..."); 
        pipeline.addLast("...");  
    }
});

采用责任链模式,每个处理器处理特定任务,依次执行。里面具体的Handler单独说明。

2.5 绑定端口,同步阻塞

java 复制代码
// 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = serverBootstrap.bind(9091).sync();
// 对关闭通道进行监听
channelFuture.channel().closeFuture().sync();

当前服务端绑定一个端口,客户端就可以通过当前端口连接,并同步阻塞,等待端口绑定成功。最后同步阻塞等待通过关闭。

03 消息处理

消息的处理是接受和推送消息重要部分。Netty框架提供了丰富的处理器,我们可以选择适合自己的处理器。处理器都是实现io.netty.channel.ChannelInboundHandler接口

3.1 框架自带编解码器

Netty框架下的Socket数据传输,默认都是ByteBuf(字节缓冲)。我们使用的时候自然想通过常用的字符串传输,而Netty自然帮我们提供了字符串相关的编解码处理器。

  • io.netty.handler.codec.string.StringDecoder
  • io.netty.handler.codec.string.StringEncoder

通过源码我们可以看到注释:

StringDecoder是将ByteBuf转成字符串的解码器,但是在处理之前必须使用ByteToMessageDecoder先解码,子类包括:

  • io.netty.handler.codec.DelimiterBasedFrameDecoder:分隔符分割
  • io.netty.handler.codec.FixedLengthFrameDecoder:固定长度分割
  • io.netty.handler.codec.LengthFieldBasedFrameDecoder:按照字段长度分割
  • io.netty.handler.codec.LineBasedFrameDecoder:按行分割

这几种方式都是有效防止拆包、粘包的方法。

按照注释的案例,我们就可以配置。而StringEncoder是用来发送消息的解码器,用来将字符串转成ByteBuf

我们这里采用分隔符的方式分割:

java 复制代码
pipeline.addLast(new DelimiterBasedFrameDecoder(2048, Unpooled.copiedBuffer("_".getBytes())));
pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));

而其中DelimiterBasedFrameDecodermaxFrameLength参数用来控制接收消息的最大字节大小,超过就会异常。

3.2 自定义业务处理器

自定义业务处理器是用来处理客户端连接以及消息的。

java 复制代码
@Slf4j
public class BusinessHandler extends SimpleChannelInboundHandler {
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // 建立客户端
        Channel channel = ctx.channel();
        log.info("Socket客户端建立连接:channelId={}", channel.id());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        // 断开链接
        Channel channel = ctx.channel();
        log.info("Socket客户端断开连接:channelId={}", channel.id());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 接受消息
        Channel channel = ctx.channel();
        log.info("Socket收到来自通道channelId[{}]发送的消息:{}", channel.id(), msg);
        // 通过WebSocket将方法发送给客户端
        channel.writeAndFlush(msg + "789_000");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.info("异常:", cause);
    }
}

handlerAdded()

客户端建立连接之后会触发该方法。可以通过ctx.channel()获取来连接的通道(客户端)。连接的通常可以通过io.netty.channel.group.ChannelGroup收集。

java 复制代码
ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE)
channelGroup.add(channel);

向客户端发送消息时,可以通过channelGroup.writeAndFlush()统一给客户端发送消息。

handlerRemoved()

客户端断开连接的时触发,可以通过channelGroup.remove(channel)移除已经关闭的客户端通道

channelRead0()

接收客户端消息的重要方法,通过channel.writeAndFlush()可以直接向客户端发送消息

exceptionCaught()

处理异常的方法

3.3 客户端测试

从图上可以看出,介绍的方法都被触发了。

从图可以看出客户端的也接收到服务端的消息了。

注意

客户端发送的消息:foo test..._

服务端发送的消息:foo test...789_000

客户端接受的消息:foo test...789

客户端和服务端接收到的消息都通过_截断的

04 小结

简单的服务端搭建就已经好了,但是实际应用的时候,还需要考虑心跳机制、以及无效客户端的清理等。TCP协议服务端的介绍就到这里,客户端我们下一期介绍。

相关推荐
涡能增压发动积1 天前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
云烟成雨TD1 天前
Spring AI Alibaba 1.x 系列【6】ReactAgent 同步执行 & 流式执行
java·人工智能·spring
Wenweno0o1 天前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨1 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
swg3213211 天前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
tyung1 天前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald1 天前
SpringBoot - 自动配置原理
java·spring boot·后端
殷紫川1 天前
深入理解 AQS:从架构到实现,解锁 Java 并发编程的核心密钥
java
一轮弯弯的明月1 天前
贝尔数求集合划分方案总数
java·笔记·蓝桥杯·学习心得
chenjingming6661 天前
jmeter线程组设置以及串行和并行设置
java·开发语言·jmeter