简介
在 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 的对比
特性 | Netty | OkHttp/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 中创建用于处理数据和发送数据的本地服务器,为此我们需要准备一些统一的数据类和搭建服务的初始操作,为了便于统一管理,我们分别创建
model
和server
包在
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>
至此我们的聊天通信项目已经完成,准备两台设备,或者虚拟机,模拟器,我们运行一下,效果如下
-