学习 Android(六)Netty通信实战

简介

在 Android 开发中,Netty 是一个基于 Java NIO 的高性能、异步事件驱动的网络应用框架,主要用于构建需要高效网络通信的客户端或服务端。尽管 Android 开发中常见的 HTTP 请求通常使用 OkHttp 或 Retrofit,但在某些复杂场景下,Netty 能提供更底层的控制和更高的性能。接下来跟着文章一起学习一下 Netty

1. Netty 的核心特性

  • 异步非阻塞模型

    • 基于事件驱动,单线程可处理大量并发连接,避免传统阻塞 I/O 的资源浪费。
  • 协议高度可定制

    • 支持 TCP/UDP/HTTP/WebSocket 等协议,并可以自定义私有协议(如物联网设备通信)。
  • 零拷贝优化

    • 通过 ByteBuf 减少内存复制,提升数据传输效率。
  • 高拓展性

    • 通过 ChannelHandler 链式处理数据(如编解码、加密、压缩等)

2. Netty 在 Android 中的典型应用场景

  • 长连接实时通信
    • 场景:聊天应用、实时推送、在线游戏等等。
    • 优势:相比 HTTP 轮询或 WebSocket(需要依赖 OkHttp),Netty 更轻量且可深度优化通信协议。
    • 示例:通过 Netty 实现自定义心跳机制,保持 TCP 长连接。
  • 自定义协议通信
    • 场景:物联网设备控制、私有二进制协议(如智能家居硬件交互)。
    • 示例:定义二进制数据格式,通过 Netty 编码/解码,降低协议解析开销。
  • 高性能代理或网关
    • 场景:Android 设备作为中间节点转发数据(如本地代理服务器)。
    • 示例:构建一个本地 TCP 代理,转发手机与其他设备的数据。
  • 替代部分 HTTP 场景
    • 场景:高频小数据包传输(如传感器数据上报)。
    • 优势:Netty 的异步模型笔比传统 HTTP 更适合高频率请求。

3. Android 中使用 Netty 的优缺点

优点缺点
1. 高性能,适合高并发、低延迟场景1. 学习曲线陡峭,需理解 NIO 和事件驱动模型
2. 灵活定制协议,适应私有场景2. 增加 APK 体积(Netty 库约 4MB+)
3. 资源占用低,适合长时间运行3. 需手动管理线程和生命周期(如 Android 后台限制)
4. 支持 SSL/TLS 安全传输4. 调试复杂,需熟悉 Netty 的日志和异常处理

4. Netty 与 OkHttp / Retrofit 的对比

特性NettyOkHttp/Retrofit
通信模型异步非阻塞,适合长连接、高并发同步/异步阻塞,适合短连接 HTTP 请求
协议支持支持自定义协议、TCP/UDP/WebSocket主要支持 HTTP/HTTPS/WebSocket
性能高吞吐、低延迟适用于常规 Web 请求,性能足够
开发成本高(需处理编解码、线程安全)低(封装完善,开箱即用)
适用场景实时通信、私有协议、高频数据传输REST API、文件上传下载、简单 WebSocket
  • 何时选择 Netty?
    • 需要实现自定义二进制协议(如物联网设备通信)。
    • 延迟和吞吐量有极高要求(如多人实时游戏)。
    • 长期维护大量 TCP 长连接(如消息推送服务器)。
    • 需要构建本地网络中间件(如代理、端口转发)。

5. 使用 Netty 实现的聊天通信功能

上述大致介绍了一下 Netty 的相关知识,为了对其有进一步的了解,本文将实现一个简单的实时通信功能来进一步的了解 Netty 的功能。

  • 项目准备

    • 创建一个 Kotlin 项目

    • 在项目下新建一个空 module

      在这里插入图片描述

      在这里插入图片描述

      在这里插入图片描述

    • 添加依赖

      在我们默认的 app 模块中和 netty_server 模块中添加下面的依赖

      implementation 'io.netty:netty-all:4.1.9.Final'
      implementation 'com.google.code.gson:gson:2.8.2'
      
  • 本地 Netty 服务器搭建

    我们将在 netty_server 中创建用于处理数据和发送数据的本地服务器,为此我们需要准备一些统一的数据类和搭建服务的初始操作,为了便于统一管理,我们分别创建 modelserver

    model 包下创建

    • MsgType

      object MsgType {
          // 登录消息类型,通常在客户端首次与服务器建立连接时发送此类型,以完成身份验证或注册流程
          // 使用 const val 声明编译时常量,保证在编译阶段就被内联,不占用运行时内存
          const val LOGIN = 1
      
          // 心跳消息类型,用于客户端与服务器之间保持连接活跃或检测链路是否可用
          // 心跳一般以固定间隔发送,可帮助服务器及时发现并清除失效连接
          const val PING = 2
      
          // 文本消息类型,用于传输普通文本内容,如聊天消息或命令字符串
          const val TEXT = 3
      
          // 提示/通知消息类型,用于传输系统提示或通知性内容,例如广播消息或状态更新
          // 所有常量均遵循 Kotlin 官方对 const 属性的命名规范:全大写、下划线分隔(Inspectopedia)
          const val TIPS = 4
      }
      
    • LoginInfo

      // LoginInfo 数据模型类,表示登录接口的响应信息
      // 使用 Gson 注解将 JSON 字段名与 Kotlin 属性映射,并实现 Serializable 以支持跨组件传输
      data class LoginInfo(
          @SerializedName("account")
          var account: String? = "",  // 用户账号,对应 JSON 中的 "account" 字段
      
          @SerializedName("token")
          var token: String? = "",    // 登录成功后返回的认证令牌,对应 JSON 中的 "token" 字段
      
          @SerializedName("code")
          var code: Int? = 0,         // 响应状态码,如 0 表示成功,对应 JSON 中的 "code" 字段
      
          @SerializedName("msg")
          var msg: String? = ""       // 响应消息,通常用于错误提示或操作反馈,对应 JSON 中的 "msg" 字段
      ) : Serializable {
          /**
           * 将当前 LoginInfo 实例序列化为 JSON 字符串
           * 使用 Gson 库提供的 toJson 方法进行对象到 JSON 的转换
           */
          fun toJson(): String {
              val gson = Gson()
              return gson.toJson(this)
          }
      }
      
    • CMessage

      // CMessage 数据模型类,表示客户端与服务端交换的消息实体
      // 使用 Gson 注解将 JSON 字段名与 Kotlin 属性映射,并实现 Serializable 以支持序列化传输
      data class CMessage(
          @SerializedName("from")
          var from: String? = "",        // 发送者标识,对应 JSON 中的 "from" 字段
          @SerializedName("to")
          var to: String? = "",          // 接收者标识,对应 JSON 中的 "to" 字段
          @SerializedName("type")
          var type: Int? = 0,            // 消息类型(如文本、图片、绘图等),对应 JSON 中的 "type"
          @SerializedName("content")
          var content: String? = "",     // 文本内容或其他数据,对应 JSON 中的 "content"
      ) : Serializable {
          /**
           * 将当前 CMessage 实例序列化为 JSON 字符串
           * 利用 Gson 库中的 toJson 方法,将对象转换为对应的 JSON 表示
           */
          fun toJson(): String {
              val gson = Gson()
              return gson.toJson(this)
          }
      }
      

    server 包下创建

    • UserManager

      /**
       * 管理已注册的用户账号信息
       * */
      class UserManager private constructor() {
          // 已注册的账号
          private val loginInfos: MutableSet<LoginInfo> = HashSet()
          // 初始化已注册账号信息
          init {
              // 创建示例账号信息并添加到已注册账号集合中
              val wcy = LoginInfo().apply {
                  account = "test1"
                  token = "123456"
              }
              val wcy2 = LoginInfo().apply {
                  account = "test2"
                  token = "123456"
              }
              loginInfos.add(wcy)
              loginInfos.add(wcy2)
          }
          // 单例模式的实现
          companion object {
              val instance: UserManager by lazy { SingletonHolder.instance }
          }
      
          private object SingletonHolder {
              val instance = UserManager()
          }
          // 验证登录信息是否有效
          fun verify(loginInfo: LoginInfo): Boolean {
              // 判断是否有与传入登录信息匹配的已注册账号信息
              return loginInfos.any { it == loginInfo }
          }
      }
      
    • NettyServerHandler

      class NettyServerHandler : SimpleChannelInboundHandler<String>() {
      
          /**
           * 收到客户端发送的消息时被调用
           * @param ctx 与客户端通信的上下文,包含 Channel 管道等信息
           * @param msg 客户端发来的原始 JSON 字符串
           */
          override fun channelRead0(ctx: ChannelHandlerContext, msg: String?) {
              // 将 JSON 字符串反序列化为 CMessage 对象,以便后续处理
              val gson = Gson()
              val message = gson.fromJson(msg, CMessage::class.java)
      
              // 根据消息类型分发处理逻辑
              when (message.type) {
                  MsgType.PING -> {
                      // 客户端心跳检测
                      println("正在接受来自 ${message.from} 的 ping")
                      // 从全局 ChannelMap 中获取该账号对应的 Channel,并回写相同的 ping 消息
                      val channel = NettyChannelMap.get(message.from!!)
                      channel?.writeAndFlush(message.toJson())
                  }
      
                  MsgType.LOGIN -> {
                      // 登录消息:content 中包含序列化的 LoginInfo
                      val loginInfo = gson.fromJson(message.content, LoginInfo::class.java)
                      // 验证用户名和密码
                      if (UserManager.instance.verify(loginInfo)) {
                          // 登录成功,填充返回码和提示
                          loginInfo.code = 200
                          loginInfo.msg = "success"
                          message.content = loginInfo.toJson()
                          // 通过当前 Channel 将成功消息回写给客户端
                          ctx.channel().writeAndFlush(message.toJson())
                          // 将账号与 Channel 绑定,以便后续发送给该用户
                          NettyChannelMap.add(loginInfo.account!!, ctx.channel())
                          println("${loginInfo.account} login")
                      } else {
                          // 登录失败,设置错误码和原因
                          loginInfo.code = 400
                          loginInfo.msg = "用户名或密码错误"
                          message.content = loginInfo.toJson()
                          ctx.channel().writeAndFlush(message.toJson())
                      }
                  }
      
                  MsgType.TEXT -> {
                      // 文本消息:将内容转发给指定接收者
                      // message.to 包含目标账号
                      val channel = NettyChannelMap.get(message.to!!)
                      channel?.let {
                          // 检查 Channel 是否可写
                          if (it.isWritable) {
                              // 发送消息并在发送完成后添加监听,打印失败日志
                              it.writeAndFlush(message.toJson()).addListener(ChannelFutureListener { future ->
                                  if (!future.isSuccess) {
                                      println("send msg to ${message.to} failed")
                                  }
                              })
                          }
                      }
                  }
              }
      
              // 释放消息资源,避免内存泄漏
              ReferenceCountUtil.release(msg)
          }
      
          /**
           * 当客户端断开连接时被调用
           * @param ctx 与客户端通信的上下文
           */
          override fun channelInactive(ctx: ChannelHandlerContext) {
              // 从 ChannelMap 中移除该客户端对应的映射,清理资源
              NettyChannelMap.remove(ctx.channel())
          }
      }
      
    • NettyServerBootstrap

      package com.example.netty_server.server
      
      import io.netty.bootstrap.ServerBootstrap
      import io.netty.channel.ChannelOption
      import io.netty.channel.nio.NioEventLoopGroup
      import io.netty.channel.socket.SocketChannel
      import io.netty.channel.socket.nio.NioServerSocketChannel
      import io.netty.handler.codec.serialization.ClassResolvers
      import io.netty.handler.codec.serialization.ObjectDecoder
      import io.netty.handler.codec.serialization.ObjectEncoder
      import io.netty.channel.ChannelInitializer
      import io.netty.channel.ChannelPipeline
      import io.netty.channel.ChannelFutureListener
      
      class NettyServerBootstrap(private val port: Int) {
          /**
           * 启动并绑定 Netty 服务器到指定端口
           */
          fun bind() {
              ServerBootstrap()
                  // bossGroup 和 workerGroup:分别处理连接请求与网络读写
                  .group(NioEventLoopGroup(), NioEventLoopGroup())
                  // 指定使用 NIO 的服务端 SocketChannel 实现
                  .channel(NioServerSocketChannel::class.java)
                  // 配置服务端套接字参数:连接队列长度
                  .option(ChannelOption.SO_BACKLOG, 128)
                  // 配置 TCP_NODELAY,禁用 Nagle 算法,减少延迟
                  .option(ChannelOption.TCP_NODELAY, true)
                  // 配置每条连接的套接字参数:启用 TCP keep-alive,检测死连接
                  .childOption(ChannelOption.SO_KEEPALIVE, true)
                  // 设置子通道的 ChannelPipeline 初始化逻辑
                  .childHandler(object : ChannelInitializer<SocketChannel>() {
                      override fun initChannel(socketChannel: SocketChannel) {
                          val pipeline: ChannelPipeline = socketChannel.pipeline()
                          // 添加 Java 对象序列化编码器,用于将出站对象编码为字节
                          pipeline.addLast(ObjectEncoder())
                          // 添加解码器,反序列化入站字节为 Java 对象
                          pipeline.addLast(ObjectDecoder(ClassResolvers.cacheDisabled(null)))
                          // 添加自定义业务处理器,负责处理解码后的消息
                          pipeline.addLast(NettyServerHandler())
                      }
                  })
                  // 绑定服务器端口,返回异步结果
                  .bind(port)
                  // 添加监听器,打印启动结果
                  .addListener(ChannelFutureListener { future ->
                      if (future.isSuccess) {
                          println("netty server start success")
                      } else {
                          println("netty server start filed")
                      }
                  })
          }
      }
      
      
    • NettyChannelMap

      // NettyChannelMap 单例对象,用于在全局维护账号到 Netty Channel 的映射
      // Kotlin 的 object 声明会在首次访问时懒加载并且线程安全,无需手动同步
      object NettyChannelMap {
          // 使用 ConcurrentHashMap 作为底层存储,以支持高并发读写场景
          // Java 的 ConcurrentHashMap 在多线程环境下能提供高效的并发性和锁分段机制
          private val map: MutableMap<String, Channel> = ConcurrentHashMap()
      
          /**
           * 将指定账号与 Channel 绑定
           * @param account 用户账号,作为映射的 key
           * @param channel Netty 的 Channel 实例,用于与客户端通信
           */
          fun add(account: String, channel: Channel) {
              map[account] = channel
          }
      
          /**
           * 根据账号获取已绑定的 Channel
           * @param account 用户账号
           * @return 对应的 Channel,若不存在返回 null
           */
          fun get(account: String): Channel? {
              return map[account]
          }
      
          /**
           * 根据 Channel 删除对应的账号映射
           * 通过遍历 map.entries 查找匹配的 value,获取 key 后再移除,保证只删除指定 Channel 的绑定
           * @param channel 要移除的 Channel 实例
           */
          fun remove(channel: Channel) {
              // entries.find 线性扫描查找匹配项,适用于映射较小或移除不频繁的场景
              val account = map.entries.find { it.value == channel }?.key
              account?.let { map.remove(it) }
          }
      }
      

    创建一个 PushServer

    // 程序入口点:Kotlin 应用的主函数,用于启动整个服务
    // 在 Kotlin 中,无需在类内声明 main,顶层函数即为入口点
    fun main() {
        // 实例化 NettyServerBootstrap,并传入监听端口 8300
        // 在 Netty 中,ServerBootstrap 用于配置并启动异步网络服务器
        val serverBootstrap = NettyServerBootstrap(8300)
        // 绑定到指定端口并启动服务器:ServerBootstrap.bind(port) 会异步创建 ServerChannel 并监听传入连接
        serverBootstrap.bind()
    }
    

    运行结果如下表明服务启动成功

    在这里插入图片描述

ok,至此我们服务端已经完美运行,现在准备实现客户端功能

  • 客户端实现聊天通信

    model 包下创建

    • Callback

      // 通用回调接口,定义了一个带泛型的事件回调方法
      interface Callback<T> {
          /**
           * 事件回调方法,在异步操作完成或发生事件时被调用
           *
           * @param code 操作结果状态码,例如 200 表示成功,非 0 或其他值通常表示不同类型的错误
           * @param msg  与状态码对应的描述信息,成功时可以携带提示,失败时携带错误详情
           * @param t    可选的泛型数据载体,携带具体的业务对象或返回值,默认为 null
           */
          fun onEvent(code: Int, msg: String, t: T? = null)
      }
      
    • CMessage

      // CMessage 数据类,表示客户端与服务器之间交换的消息结构
      data class CMessage(
          @SerializedName("from") var from: String = "",     // 发送者账号,对应 JSON 字段 "from"
          @SerializedName("to") var to: String = "",         // 接收者账号,对应 JSON 字段 "to"
          @SerializedName("type") var type: Int = 0,         // 消息类型(如文本、心跳等),对应 JSON 字段 "type"
          @SerializedName("content") var content: String = ""// 消息内容,对应 JSON 字段 "content"
      ) : Serializable {                                     // 实现 Serializable,支持对象跨网络/本地序列化
      
          /**
           * 将当前 CMessage 对象序列化为 JSON 字符串
           * 通过 Gson 库的 toJson(Object) 方法进行序列化,生成标准 JSON 表示
           */
          fun toJson(): String {
              val gson = Gson()           // 创建线程安全的 Gson 实例
              return gson.toJson(this)    // 使用 Gson.toJson 将对象转换为 JSON 字符串
          }
      }
      
    • LoginInfo

      // LoginInfo 数据类,表示登录接口返回的信息结构
      // 使用 Gson 注解将 JSON 字段名与 Kotlin 属性映射,以支持字段名不一致场景
      // 实现 Serializable 接口,便于对象在 Android 组件或网络上传输时进行序列化与反序列化
      data class LoginInfo(
          @SerializedName("account") var account: String = "",  // 用户账号,对应 JSON 中的 "account"
          @SerializedName("token")   var token:   String = "",  // 登录凭证令牌,对应 JSON 中的 "token"
          @SerializedName("code")    var code:     Int    = 0,  // 响应状态码,如 200 表示成功,对应 JSON 中的 "code"
          @SerializedName("msg")     var msg:      String = ""  // 响应消息,通常用于错误提示或操作反馈,对应 JSON 中的 "msg"
      ) : Serializable {
      
          /**
           * 将当前 LoginInfo 对象序列化为 JSON 字符串
           * 使用 Gson 库的 toJson 方法,可将任意 Kotlin 对象转换为 JSON 表示
           */
          fun toJson(): String {
              val gson = Gson()           // 创建 Gson 实例(线程安全,可复用)
              return gson.toJson(this)    // 调用 toJson(Object) 将对象转换为 JSON 字符串
          }
      }
      
    • LoginStatus

      // LoginStatus 枚举类,用于表示不同的登录流程阶段或状态
      enum class LoginStatus {
          UNLOGIN,     // 未登录状态:用户尚未发起登录或已退出登录
          CONNECTING,  // 连接中状态:客户端正在尝试建立与服务器的连接
          LOGINING,    // 登录中状态:连接成功后,正在向服务器发送登录凭证并等待验证
          LOGINED      // 已登录状态:服务器已验证,通过登录流程,客户端处于在线状态
      }
      
    • MsgType

      // MsgType 接口,用于集中管理应用中的消息类型常量
      // 使用 companion object 声明单例对象,类似于 Java 中的静态成员
      interface MsgType {
          companion object {
              // 登录消息类型
              // 使用 const val 声明编译时常量,相当于 Java 的 public static final
              const val LOGIN: Int = 1
      
              // 心跳检测消息类型,用于客户端与服务器保持活跃连接
              const val PING: Int = 2
      
              // 文本消息类型,表示普通聊天内容
              const val TEXT: Int = 3
      
              // 提示消息类型,通常用于系统通知或状态更新
              const val TIPS: Int = 4  // 常量命名遵循 UPPER_SNAKE_CASE 规范
          }
      }
      

    server 包下创建

    • AppCache

      // AppCache 单例对象,用于在应用运行期间缓存全局共享数据
      object AppCache {
          // 持有 PushService 实例,用于管理与推送服务的连接
          // 可在登录或启动时初始化,并在整个应用中复用该服务对象
          var service: PushService? = null
      
          // 存储当前用户的登录信息(如账号、Token 等),便于在各模块中快速访问
          // 登录成功后设置 myInfo,退出或切换用户时清空该字段
          var myInfo: LoginInfo? = null
      }
      
    • PushService

      /**
       * @Description: 这个类主要使用了 Netty 框架来进行网络通信,通过 SocketChannel 与服务器建立连接,并处理接收到的消息。还包括一些逻辑,如登录、发送消息、处理接收消息等。
       */
      class PushService : Service() {
          private var socketChannel: SocketChannel? = null // SocketChannel用于与推送服务器建立连接
          private var loginCallback: Callback<Void>? = null // 登录回调接口,用于处理登陆结果
          private var receiveMsgCallback: Callback<CMessage>? = null // 接收消息回调接口,用于处理接收到的推送消息
          private var handler: Handler? = null // 用于在主线程中执行回调操作
          private var status = LoginStatus.UNLOGIN // 当前登录状态,默认为登录状态
          override fun onCreate() {
              super.onCreate()
              handler = Handler() // 创建Handler对象,用于在主线程中执行回调操作
              service = this // 设置服务实例到AppCache中
          }
      
          override fun onBind(intent: Intent): IBinder? {
              return null
          }
      
          fun setReceiveMsgCallback(receiveMsgCallback: Callback<CMessage>?) {
              this.receiveMsgCallback = receiveMsgCallback // 设置接收消息回调接口
          }
      
          private fun connect(callback: Callback<Void>) {
              if (status === LoginStatus.CONNECTING) {
                  return  // 如果正在连接中,则直接返回
              }
              updateStatus(LoginStatus.CONNECTING) // 更新登录状态为连接中
              val group = NioEventLoopGroup() // 创建NioEventLoopGroup对象 用于处理I/O的多线程事件循环
              Bootstrap() // 启动客户端或非服务器端的Channel
                  .channel(NioSocketChannel::class.java) // 使用NioSocketChannel作为通道
                  .group(group) // 设置EventLoopGroup
                  .option(ChannelOption.SO_KEEPALIVE, true) // 设置TCP连接选项
                  .option(ChannelOption.TCP_NODELAY, true)
                  .handler(object : ChannelInitializer<SocketChannel>() {
                      @Throws(Exception::class)
                      override fun initChannel(socketChannel: SocketChannel) {
                          val pipeline = socketChannel.pipeline()
                          pipeline.addLast(IdleStateHandler(0, 30, 0)) // 添加心跳检测
                          pipeline.addLast(ObjectEncoder()) // 对象编码器
                          pipeline.addLast(ObjectDecoder(ClassResolvers.cacheDisabled(null))) // 对象解码器
                          pipeline.addLast(ChannelHandle()) // 自定义的消息处理器 用于处理入栈和出栈的数据
                      }
                  })
                  .connect(InetSocketAddress(HOST, PORT))
                  .addListener(ChannelFutureListener { future: ChannelFuture ->
                      if (future.isSuccess) {
                          socketChannel = future.channel() as SocketChannel
                          callback.onEvent(200, "success", null) // 连接成功,回调成功的结果
                      } else {
                          Log.e(TAG, "connect failed")
                          close() // 连接失败,关闭连接
                          // 这里一定要关闭,不然一直重试会引发OOM
                          future.channel().close() // 关闭Channel
                          group.shutdownGracefully() // 关闭EventLoopGroup
                          callback.onEvent(400, "connect failed", null) // 连接失败,回调失败结果
                      }
                  })
          }
      
          fun login(account: String?, token: String?, callback: Callback<Void>?) {
              if (status === LoginStatus.CONNECTING || status === LoginStatus.LOGINING) {
                  return  // 如果正在连接中,则直接返回
              }
              connect(object : Callback<Void> {
                  override fun onEvent(code: Int, msg: String, t: Void?) {
                      if (code === 200) {
                          val loginInfo =
                              LoginInfo()
                          loginInfo.account = account!!
                          loginInfo.token = token!!
                          val loginMsg =
                              CMessage()
                          loginMsg.from = account
                          loginMsg.type = MsgType.LOGIN
                          loginMsg.content = loginInfo.toJson()
                          socketChannel!!.writeAndFlush(loginMsg.toJson()) // Netty 框架中用于向服务器发送数据的方法。
                              .addListener(ChannelFutureListener { future: ChannelFuture ->
                                  if (future.isSuccess) {
                                      loginCallback = callback // 登录消息发送成功,设置登录回调接口
                                  } else {
                                      close() // 登录消息发送失败,关闭连接
                                      updateStatus(LoginStatus.UNLOGIN) // 更新登录状态为未登录
                                      if (callback != null) {
                                          handler!!.post {
                                              callback.onEvent(
                                                  400,
                                                  "failed",
                                                  null
                                              )
                                          } // 回调登录失败结果
                                      }
                                  }
                              })
                      } else {
                          close() // 连接失败,关闭连接
                          updateStatus(LoginStatus.UNLOGIN) // 修改登录状态 为未登录状态
                          if (callback != null) { // 如果回调不为空,回调登录失败结果
                              handler!!.post { callback.onEvent(400, "failed", null) }
                          }
                      }
                  }
              })
          }
      
          // 用于发送消息,并通过回调函数返回发送结果
          fun sendMsg(message: CMessage, callback: Callback<Void>?) {
              if (status !== LoginStatus.LOGINED) {
                  callback!!.onEvent(401, "unlogin", null)
                  return
              }
              // 向socketChannel写入并刷新消息,发送到远程服务器
              socketChannel!!.writeAndFlush(message.toJson())
                  .addListener(ChannelFutureListener { future: ChannelFuture ->
                      if (callback == null) {
                          return@ChannelFutureListener
                      }
                      // 根据发送结果调用不同的回调方法
                      if (future.isSuccess) {
                          handler!!.post { callback.onEvent(200, "success", null) }
                      } else {
                          handler!!.post { callback.onEvent(400, "failed", null) }
                      }
                  })
          }
      
          private fun close() {
              if (socketChannel != null) {
                  socketChannel!!.close()
                  socketChannel = null
              }
          }
      
          private inner class ChannelHandle : SimpleChannelInboundHandler<String?>() {
              // 当 Channel 处于非活动状态(连接断开)时调用
              @Throws(Exception::class)
              override fun channelInactive(ctx: ChannelHandlerContext) {
                  super.channelInactive(ctx)
                  // 连接断开时关闭通道
                  close()
                  // 更新登陆状态为未登录
                  updateStatus(LoginStatus.UNLOGIN)
                  // 重新尝试登录,延迟3000毫秒
                  retryLogin(3000)
              }
      
              // 传递自定义事件或者触发自定义事件时,可以使用 userEventTriggered 方法来处理这些事件。
              // 这个方法在 ChannelPipeline 中的下一个 ChannelInboundHandler 中被触发。
              @Throws(Exception::class)
              override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
                  super.userEventTriggered(ctx, evt)
                  if (evt is IdleStateEvent) {
                      if (evt.state() == IdleState.WRITER_IDLE) {
                          // 空闲了,发个心跳吧
                          val message = CMessage()
                          message.from = myInfo!!.account
                          message.type = MsgType.PING
                          // 向服务器发送心跳
                          ctx.writeAndFlush(message.toJson())
                      }
                  }
              }
      
              // 这是 SimpleChannelInboundHandler 的核心方法,用于处理接收到的消息。在这个方法中,你可以处理接收到的消息,并且通常在这里实现业务逻辑
              @Throws(Exception::class)
              override fun channelRead0(ctx: ChannelHandlerContext, msg: String?) {
                  // 处理接收到的消息
                  val gson = Gson()
                  val message = gson.fromJson(
                      msg,
                      CMessage::class.java
                  )
                  if (message.type == MsgType.LOGIN) {
                      val loginInfo = gson.fromJson(
                          message.content,
                          LoginInfo::class.java
                      )
                      if (loginInfo.code == 200) {
                          updateStatus(LoginStatus.LOGINED)
                          myInfo = loginInfo
                          if (loginCallback != null) {
                              handler!!.post {
                                  loginCallback!!.onEvent(200, "success", null)
                                  loginCallback = null
                              }
                          }
                      } else {
                          close()
                          updateStatus(LoginStatus.UNLOGIN)
                          if (loginCallback != null) {
                              handler!!.post {
                                  loginCallback!!.onEvent(loginInfo.code, loginInfo.msg, null)
                                  loginCallback = null
                              }
                          }
                      }
                  } else if (message.type == MsgType.PING) {
                      Log.d(TAG, "receive ping from server")
                  } else if (message.type == MsgType.TEXT) {
                      Log.d(TAG, "receive text message " + message.content)
                      if (receiveMsgCallback != null) {
                          handler!!.post {
                              receiveMsgCallback!!.onEvent(
                                  200,
                                  "success",
                                  message
                              )
                          }
                      }
                  }
                  ReferenceCountUtil.release(msg)
              }
          }
      
          private fun retryLogin(mills: Long) {
              if (myInfo == null) {
                  return
              }
              // 延迟指定事件后重新登录
              handler!!.postDelayed({
                  login(
                      myInfo!!.account,
                      myInfo!!.token,
                      object : Callback<Void> {
                          override fun onEvent(code: Int, msg: String, t: Void?) {
                              if (code !== 200) {
                                  retryLogin(mills)
                              }
                          }
                      })
              }, mills)
          }
      
          private fun updateStatus(status: LoginStatus) {
              if (this.status !== status) {
                  Log.d(TAG, "update status from " + this.status + " to " + status)
                  // 更新的登录状态
                  this.status = status
              }
          }
      
          companion object {
              private const val TAG = "PushService"
      
              // 推送服务器的主机地址
              private const val HOST = "172.16.27.194"
      
              // 推送服务器的端口号
              private const val PORT = 8300
          }
      }
      

      手动创建服务记得要在 AndroidManifest.xml 对其进行声明 ,否则无效

      <service android:name=".server.PushService" />
      

    ui 包下创建

    • LoginActivity

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical"
          tools:context=".ui.LoginActivity">
      
          <EditText
              android:id="@+id/et_account"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginTop="10dp"
              android:background="@drawable/input"
              android:hint="请输入账号"
              android:inputType="textEmailAddress"
              android:singleLine="true"
              android:textSize="14sp" />
      
          <EditText
              android:id="@+id/et_token"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginTop="10dp"
              android:background="@drawable/input"
              android:hint="请输入密码"
              android:inputType="textPassword"
              android:singleLine="true"
              android:textSize="14sp" />
      
          <Button
              android:id="@+id/btn_login"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="登录" />
      </LinearLayout>
      
      // LoginActivity:用户登录界面,继承自 AppCompatActivity,负责处理用户输入并调用服务端登录接口
      class LoginActivity : AppCompatActivity() {
      
          // 延迟初始化 EditText,用于输入账号,直到第一次访问时通过 findViewById 查找视图
          private val etAccount: EditText by lazy { findViewById<EditText>(R.id.et_account) }
      
          // 延迟初始化 EditText,用于输入令牌(token)
          private val etToken: EditText by lazy { findViewById<EditText>(R.id.et_token) }
      
          // 延迟初始化 Button,用于触发登录操作
          private val btnLogin: Button by lazy { findViewById<Button>(R.id.btn_login) }
      
          // Activity 生命周期入口,当界面创建时调用
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              // 设置当前 Activity 使用的布局文件
              setContentView(R.layout.activity_login)
      
              // 为登录按钮注册点击事件监听器
              btnLogin.setOnClickListener {
                  // 从全局缓存中取出 PushService 实例并调用登录方法,
                  // 将账号和 token 作为参数传入,登录结果通过回调返回
                  service!!.login(
                      etAccount.text.toString(),    // 获取用户输入的账号
                      etToken.text.toString(),      // 获取用户输入的 token
                      object : Callback<Void> {
                          // 异步事件回调,code 表示状态码,msg 表示附加消息
                          override fun onEvent(code: Int, msg: String, t: Void?) {
                              if (code == 200) {
                                  // 登录成功:给用户提示,跳转到 MainActivity 并清除登录页历史
                                  Toast.makeText(
                                      this@LoginActivity,
                                      "登录成功",
                                      Toast.LENGTH_SHORT
                                  ).show()
                                  val intent = Intent(
                                      this@LoginActivity,
                                      MainActivity::class.java
                                  ).apply {
                                      // 清除 Activity 栈中除目标之外的所有页面
                                      addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
                                  }
                                  startActivity(intent)
                                  finish() // 关闭当前登录页面
                              } else {
                                  // 登录失败:向用户展示错误码和错误信息
                                  Toast.makeText(
                                      this@LoginActivity,
                                      "登录失败 code=$code, msg=$msg",
                                      Toast.LENGTH_SHORT
                                  ).show()
                              }
                          }
                      }
                  )
              }
          }
      }
      
    • MainActity

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          xmlns:tools="http://schemas.android.com/tools"
          android:id="@+id/main"
          android:orientation="vertical"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          tools:context=".ui.MainActivity">
      
          <!-- 账号输入 -->
          <EditText
              android:id="@+id/etAccount"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:hint="账号"
              android:inputType="text" />
      
          <!-- 消息输入 -->
          <EditText
              android:id="@+id/etMessage"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:hint="消息"
              android:inputType="textMultiLine" />
      
          <!-- 发送按钮 -->
          <Button
              android:id="@+id/btnSend"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="发送" />
      
          <!-- 聊天记录终端,使用权重填充剩余空间 -->
          <TextView
              android:id="@+id/terminal"
              android:layout_width="match_parent"
              android:layout_height="0dp"
              android:layout_weight="1"
              android:scrollbars="vertical"
              android:padding="8dp"
              android:textIsSelectable="true" />
      
      </LinearLayout>
      
      class MainActivity : AppCompatActivity() {
      
          // 通过懒加载初始化控件(避免重复findViewById)
          private val etAccount by lazy { findViewById<EditText>(R.id.etAccount) }
          private val etMessage by lazy { findViewById<EditText>(R.id.etMessage) }
          private val btnSend by lazy { findViewById<Button>(R.id.btnSend) }
          private val terminal by lazy { findViewById<TextView>(R.id.terminal) }
      
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContentView(R.layout.activity_main)
      
              // 设置TextView支持滚动(当内容超过视图高度时可滚动查看)
              terminal.movementMethod = ScrollingMovementMethod.getInstance()
      
              // 注册消息接收回调(当AppCache.service收到消息时会触发receiveMsgCallback)
              AppCache.service?.setReceiveMsgCallback(receiveMsgCallback)
      
              // 发送按钮点击事件监听
              btnSend.setOnClickListener {
                  // 输入校验:账号或消息为空时直接返回
                  if (etAccount.length() == 0 || etMessage.length() == 0) {
                      return@setOnClickListener
                  }
      
                  // 创建消息对象并填充数据
                  val myAccount = myInfo!!.account
                  val cMessage = CMessage().apply {
                      from = myAccount           // 发送者账号
                      to = etAccount.text.toString() // 接收者账号(来自输入框)
                      type = MsgType.TEXT        // 消息类型为文本
                      content = etMessage.text.toString() // 消息内容(来自输入框)
                  }
      
                  // 通过service发送消息
                  service!!.sendMsg(cMessage, object : Callback<Void> {
                      override fun onEvent(code: Int, msg: String, t: Void?) {
                          when (code) {
                              200 -> { // 发送成功
                                  etMessage.setText(null) // 清空消息输入框
                                  terminal.append("[发送]${cMessage.content}") // 在终端显示发送记录
                              }
                              else -> { // 发送失败
                                  terminal.append("[发送失败]${cMessage.content}, $msg") // 显示错误信息
                              }
                          }
                          terminal.append("\n") // 换行分隔不同消息
                      }
                  })
              }
          }
      
          // 消息接收回调(当收到新消息时触发)
          private val receiveMsgCallback: Callback<CMessage> = object : Callback<CMessage> {
              override fun onEvent(code: Int, msg: String, cMessage: CMessage?) {
                  cMessage?.let {
                      // 在终端显示接收到的消息(格式:[接收]发送者账号:消息内容)
                      terminal.append("[接收]${it.from}:${it.content}\n")
                  }
              }
          }
      }
      

    创建 自定义 BaseApplication

    class BaseApplication : Application() {
        override fun onCreate() {
            super.onCreate()
            // 启动 PushService 服务
            startService(Intent(this, PushService::class.java))
        }
    }
    

    记得要在 AndroidManifest.xml 对其进行声明,否则无效,并且记得要 声明网络权限

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools" >
        <uses-permission android:name="android.permission.INTERNET" />
        <application
            android:name=".BaseApplication"
            android:allowBackup="true"
            android:dataExtractionRules="@xml/data_extraction_rules"
            android:fullBackupContent="@xml/backup_rules"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.MyStudyNettyExample"
            tools:targetApi="31" >
            <activity
                android:name=".ui.LoginActivity"
                android:exported="true" >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <activity
                android:name=".ui.MainActivity"
                android:exported="true" >
            </activity>
    
            <service android:name=".server.PushService" />
        </application>
    
    </manifest>
    

    至此我们的聊天通信项目已经完成,准备两台设备,或者虚拟机,模拟器,我们运行一下,效果如下

    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值