【自研项目之分布式IM】05. Socket长连接技术选型与优化

187 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

技术选型

实现长连接服务的技术很多,下面看看目前比较常见的方式:

  1. 基于Java NIO自研。
  2. 基于Netty。主流
  3. 基于gRpc。
  4. 基于RabbitMQ或其他

在JAVA中目前主流的还是使用Netty,不然学的NIO,Recator模型,epoll这些怎么吹的出去。SpringBoot + Netty整合就不说了,大家都懂,不熟悉的就花点时间了解下不难。下面主要说说一些开发时注意的点。

1. 长连接服务只做简单的业务处理

长连接服务在企业应用中不会做过多复杂的业务处理,业务都会抽取到微服务层。我也看过很多网上写的很多基于Netty实现xxx推送或聊天项目,大多都是在Netty这一层处理单聊、群聊等等这些业务的耗时操作,如果个人玩具或者小型的项目可以这么玩。稍微有点量的系统都会将业务处理转到业务服务上处理。应该保持业务简单,只做收发信息、心跳检测、消息确认即可。需要读取数据库或缓存的操作,请使用远程调用进行访问。如果非要在Netty层做耗时操作就另开线程池,决不能阻塞Netty的IO线程。

2. 通讯消息协议选型

下面看看常见的消息协议:

  1. JSON:简单易用,多端实现方便,体积较小。这是中小型项目的主流。
  2. protobuf: 速度快,体积小。主流语言都支持。这是大中小项目的主流
  3. 私有协议:自定义私有协议,可定制化。
  4. 其他:XML等等

怎么做选型?其实正常情况下前3种都可以任选,对于中小型项目已经足够了。JSON和protobuf的编解码Netty已经提供(对私有协议感兴趣的可以等等,后面会讲到)。

3. 消息类型业务逻辑

1. 登录
客户端建立新连接成功后会先发送登录报文,通过远程调用用户服务查询用户合法性。当登录成功后还尝试清除一次集群中该用户在其他节点的缓存(可能存在用户A在SockertA突然断开,重连后连接了SocketB节点,这时候就需要尝试清除SocketA节点的用户缓存,这里使用Redis的发布订阅广播方式,如果丢消息也没关系,还有心跳检测到期清除)。

/**
 * 登录认证
 */
@ChannelHandler.Sharable
public class LoginChannelInboundHandler extends SimpleChannelInboundHandler<BaseCommand> {
    @Override
    protected void channelRead0(ChannelHandlerContext channel, BaseCommand baseCommand) {
        // Step1. gRpc调用远程用户服务校验用户合法性
        ...
        // Step2. 登录成功调用redis的发布订阅清除集群中其他服务的用户缓存
        ...
        // Step3. 若登录成功后续就不需要该入站处理器,所以可以删除
        channel.pipeline().remove(this)
                .addLast(new ServerIdleStateHandler()); // 登录成功加入心跳检测
        channel.fireChannelActive(); // 传递下一个入站
    }
}

2. 心跳
在Linux内核层可以开启TCP KeepAlive机制,默认2小时检测链路是否存活。缺点也很明显不够及时。所以需要实现应用层的心跳检测。
2.1 基于Netty的IdleStateHandler重写channelIdle
2.2 基于channel的Attribute实现,参考下面实现的伪代码

// 定义通用方法
public <T> void setAttribute(String name, T value) {
    AttributeKey<T> sessionIdKey = AttributeKey.valueOf(name);
    this.channel.attr(sessionIdKey).set(value);
}
public <T> T getAttribute(String name) {
    AttributeKey<T> sessionIdKey = AttributeKey.valueOf(name);
    return this.channel.attr(sessionIdKey).get();
}
// 消息类型处理器
protected void channelRead0(ChannelHandlerContext ct, BaseCommand baseCommand) {
    // 先读取上一次写入的心跳时间
    Long time = getAttribute("idleTime");
    if (System.currentTimeMillis() - time > 设置的心跳过期时间) {
        // 断开连接
    }
}
// 还需要启动一个线程池定时检测连接是否超时

4. Netty参数调优

Netty启动类提供了Option参数(Tcp参数)设置,可以根据实际情况进行调整。

bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
// 设置option设置TCP参数
.option(ChannelOption.SO_BACKLOG, 1024)
...more
// 设置连接参数
.childOption(ChannelOption.SO_KEEPALIVE, true)
...more
  1. SO_RCVBUF/SO_SNDBUF
    • soket嵌套字在内核的发送/接收缓冲区大小调整。
  2. TCP_NODELAY
    • 表示立即发送数据,用于设置Nagle算法的启用。Netty默认禁用该算法。
    • Naggle算法会将许多小的数据连接成更大的报文来最小化发送报文数量。
  3. SO_KEEPALIVE
    • 底层TCP协议心跳,默认7200秒(2小时)
  4. SO_LINGER: 表示关闭socket的延迟时间
  5. SO_BACKLOG
    • 服务器接收连接的队列,如果已满会拒绝连接(Linux默认是128)。
    • 这个参数可以适当设置大点,防止新启动的服务或突增流量有较多连接涌入时连接失败。
  6. ALLOCATOR: 指定Netty创建ByteBuf的方式
    • PooledByteBufAllocator.DEFAULT 缓冲池模式
    • UnpooledByteBufAllocator.DEFAULT 非缓冲池,每次都会new一个新的对象
    • DirectByteBufAllocator.DEFAULT 堆外内存分配对象。(默认)

文章若有错误或建议,欢迎留言~