基于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协议服务端的介绍就到这里,客户端我们下一期介绍。

相关推荐
予枫的编程笔记17 小时前
Elasticsearch深度搜索与查询DSL实战:精准定位数据的核心技法
java·大数据·人工智能·elasticsearch·搜索引擎·全文检索
荒诞硬汉17 小时前
面向对象(三)
java·开发语言
柒.梧.17 小时前
Spring Boot集成JWT Token实现认证授权完整实践
java·spring boot·后端
白露与泡影17 小时前
放弃 IntelliJ IDEA,转 VS Code 了。。
java·ide·intellij-idea
迷雾骑士17 小时前
IDEA中将项目提交到Gitee仓库
java·gitee·intellij-idea
菜鸟233号17 小时前
力扣416 分割等和子串 java实现
java·数据结构·算法·leetcode
奔波霸的伶俐虫17 小时前
redisTemplate.opsForList()里面方法怎么用
java·开发语言·数据库·python·sql
自在极意功。17 小时前
简单介绍SpringAOP
java·spring·aop思想
__万波__17 小时前
二十三种设计模式(二十三)--责任链模式
java·设计模式·责任链模式