夜雨聆风学习资料网

ARTICLE · 1102999

保姆级学习开发安卓手机软件实战(六)--连接网络(最小可启动的 Netty Server)

保姆级学习开发安卓手机软件实战(六)--连接网络(最小可启动的 Netty Server)

发财的小手点一下~~~:三年了,我从一罐假蜂蜜里醒了过来,拜托拜托~


因为项目的适用场景: 平板做 TCP Server,多个硬件 Client 连过来,局域网,一对多 这种场景用 NIO(Non-blocking IO)。而Netty 是 Java/Kotlin 生态里最成熟的 NIO 框架,能解决:

  • Selector 管理(自动管理所有连接,不用手动写轮询代码。)
  • 半包/粘包拆包 (自动把完整一条消息给你,不用你手动处理字节缓存、拼接、截断)
  • 心跳检测 (自带IdleStateHandler空闲检测,可自动识别硬件掉线)
  • Channel 生命周期(成功连接、掉线、出错,只需要在对应回调写业务)
  • -群控广播(一个 ChannelGroup 转发给全部活跃连接)

那总结来说,有关网络我们要做的工作:

  1. 平板启动 → Netty Server 启动,监听局域网端口
  2. 硬件 Client 连上平板 → Netty 触发channelActive,把该 Channel 加入 ChannelGroup
  3. 硬件上报数据 → channelRead回调,Netty 自动拆包,拿到完整消息
  4. 平板下发群控指令 → channelGroup.writeAndFlush()广播给所有硬件
  5. 心跳超时 / 设备断电 → channelInactive,自动从 ChannelGroup 移除离线设备

这节我们简单实现1,2,也就是写完后编译运行,Logcat 能看到 “Server 已启动,监听端口: 8000”;用任意 TCP 工具连过来,Logcat 看到 “新连接: …”。

1.加 Netty 依赖

一样的,gradle/libs.versions.toml文件:

[versions] 加:

netty = "4.1.100.Final"

在 [libraries] 区块加 5 行:

netty-common    = { group = "io.netty", name = "netty-common",    version.ref = "netty" }netty-buffer    = { group = "io.netty", name = "netty-buffer",    version.ref = "netty" }netty-transport = { group = "io.netty", name = "netty-transport", version.ref = "netty" }netty-codec     = { group = "io.netty", name = "netty-codec",     version.ref = "netty" }netty-handler   = { group = "io.netty", name = "netty-handler",   version.ref = "netty" }

netty-common:工具类(Future、ConstantPool) netty-buffer:ByteBuf(Netty 自家内存管理,比 byte[] 高效) netty-transport: 核心:Channel、ChannelHandler、EventLoop netty-codec: 编码器/解码器(ByteToMessageDecoder 等) netty-handler: 内置 handler(IdleStateHandler 心跳、LoggingHandler 日志)

这里我们不引完整 netty-all:APK 体积大、方法数多。只引 5 个用得到的。

然后是app/build.gradle.kts文件 android { } 区块加:

packaging {        resources {            excludes += "META-INF/INDEX.LIST"            excludes += "META-INF/io.netty.versions.properties"        }    }

在 dependencies { } 区块加5 行:

implementation(libs.netty.common)implementation(libs.netty.buffer)implementation(libs.netty.transport)implementation(libs.netty.codec)implementation(libs.netty.handler)

做完这一步,按 AS 右上角"Sync Now"按钮。

最后添加授权:

 <uses-permissionandroid:name="android.permission.INTERNET" />

INTERNET 属于 dangerous 级别以下的安装时权限——装 app 时用户看一眼就授权了,不用弹窗。但没声明就等于没授权,内核直接拦。这是 Android 的安全模型:app 能用什么,必须在 Manifest 白纸黑字声明。

Manifest 没声明 INTERNET ↓ Android 内核的权限检查拒绝你的 socket() 系统调用

2.新建 data/remote/TargetServer.kt

这里主要做下面几个步骤:

  1. 创建一个后台线程跑 Netty(不能阻塞主线程)
  2. 在线程里创建 2 个线程组(boss + worker)
  3. 配置启动器,告诉它"新连接来了用什么 channel、用什么 handler"
  4. 绑定端口、阻塞等待关闭。

这里新建文件夹和文件的过程就省略了哈,太多次了。 直接看里面怎么写吧:

package com.example.jstz1.data.remote  // 当前这个类所在的包路径import io.netty.bootstrap.ServerBootstrap       // Netty服务端启动器,用来搭建TCP服务import io.netty.channel.Channel                // Netty通道,代表一条网络连接import io.netty.channel.ChannelInitializer     // 通道初始化器,有新客户端连进来时执行import io.netty.channel.EventLoopGroup         // Netty的线程池,管理网络事件import io.netty.channel.nio.NioEventLoopGroup  // NIO模式的线程池实现import io.netty.channel.socket.SocketChannel   // 客户端接入后的Socket通道import io.netty.channel.socket.nio.NioServerSocketChannel // NIO模式的服务端监听通道import android.util.Log                        // Android日志工具,打印log看运行情况// 单例对象 TargetServer,整个App只存在这一个Netty服务实例object TargetServer {    private const val TAG = "TargetServer"     // 日志标签,过滤日志用,打印日志时写 `Log.i(TAG, "xxx")`,你在 Android Studio 的 Logcat 搜索框输入 `TargetServer`,就能**只过滤出这个 Netty 服务打印的日志**,其他代码的日志直接隐藏。    private var bossGroup: EventLoopGroup? = null // 蹲在端口上,等着有没有新客户端过来发起连接**,只做「接收连接」这件事。    private var workerGroup: EventLoopGroup? = null // 客户端连上之后,**收发数据、读写通信**,交给 worker 来处理,默认CPU*2线程    private var serverChannel: Channel? = null // 服务监听通道;关闭这个通道,服务就不再接收新连接(只有一个)    // 启动服务函数,默认监听8000端口,可以传别的端口    funstart(port: Int = 8000) {			    if (serverChannel?.isActive == true) {			        Log.i(TAG, "Server 已在运行,忽略重复启动")			        return			    }        // 新建后台线程跑Netty服务,Android主线程不能跑阻塞代码,会ANR闪退        Thread({            try {  // sync()会抛出中断异常,用try捕获,防止程序崩溃                bossGroup = NioEventLoopGroup(1) // 创建boss线程池,只分配1个线程,专门用来接受连接                workerGroup = NioEventLoopGroup()// 创建worker线程池,不传参数默认CPU核心数×2个线程                val bootstrap = ServerBootstrap().apply { // 创建Netty服务,启动配置,下面是三个核心配置                    group(bossGroup, workerGroup)         // 把boss、worker两个线程池配置进去,**必须配**                    channel(NioServerSocketChannel::class.java) // 指定服务端使用NIO通道来监听端口,**必须配**                    // `childHandler` 意思:**当产生一个新的子通道 SocketChannel(设备连上了),就用这个初始化器去配置这个新 SocketChannel**,SocketChannel:每一个进门客人的单独对话窗口,来一台设备就新建一条。,**必须配**                    childHandler(object : ChannelInitializer<SocketChannel>() {                        // 新连接建立后,执行这个初始化方法                        override funinitChannel(ch: SocketChannel) {                            Log.i(TAG, "新连接: port") // 打印日志,提示服务启动成功                serverChannel?.closeFuture()?.sync() // 阻塞这个后台线程,一直等到服务通道被关闭(serverChannel.close())这个阻塞**只阻塞这个启动线程,不影响网络通信**。                Log.i(TAG, "服务通道关闭了")            } catch (e: InterruptedException) { // 捕获线程被中断的异常                Log.e(TAG, "线程中断", e)                Thread.currentThread().interrupt()            }        }, "NettyServer").start() // 给线程起名NettyServer,然后启动这个后台线程    }    // 停止服务的函数    funstop() {        serverChannel?.close()// 关闭服务监听通道,不再接收新的客户端连接        // 优雅关闭两个线程池,等正在处理的任务做完再退出线程        bossGroup?.shutdownGracefully()        workerGroup?.shutdownGracefully()        Log.e(TAG, "关闭服务连接")    }}

那最后,我们来测试下是否开启成功,能连接客户端: 运行软件: 后续我想用网络调试助手假装连接模拟器,就需要再做一个操作(每次都需要): cmd中:

adb devicesadb forward tcp:8000 tcp:8000

结果可以看到: 连接成功啦,后续我们就可以用网络调试助手模拟设备发指令啦。

相关学习资料