ARTICLE · 1102999
保姆级学习开发安卓手机软件实战(六)--连接网络(最小可启动的 Netty Server)
发财的小手点一下~~~:三年了,我从一罐假蜂蜜里醒了过来,拜托拜托~
因为项目的适用场景: 平板做 TCP Server,多个硬件 Client 连过来,局域网,一对多 这种场景用 NIO(Non-blocking IO)。而Netty 是 Java/Kotlin 生态里最成熟的 NIO 框架,能解决:
Selector 管理(自动管理所有连接,不用手动写轮询代码。) 半包/粘包拆包 (自动把完整一条消息给你,不用你手动处理字节缓存、拼接、截断) 心跳检测 (自带 IdleStateHandler空闲检测,可自动识别硬件掉线)Channel 生命周期(成功连接、掉线、出错,只需要在对应回调写业务) -群控广播(一个 ChannelGroup 转发给全部活跃连接)
那总结来说,有关网络我们要做的工作:
平板启动 → Netty Server 启动,监听局域网端口 硬件 Client 连上平板 → Netty 触发 channelActive,把该 Channel 加入 ChannelGroup硬件上报数据 → channelRead回调,Netty 自动拆包,拿到完整消息平板下发群控指令 → channelGroup.writeAndFlush()广播给所有硬件心跳超时 / 设备断电 → 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
这里主要做下面几个步骤:
创建一个后台线程跑 Netty(不能阻塞主线程) 在线程里创建 2 个线程组(boss + worker) 配置启动器,告诉它"新连接来了用什么 channel、用什么 handler" 绑定端口、阻塞等待关闭。
这里新建文件夹和文件的过程就省略了哈,太多次了。 直接看里面怎么写吧:
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
结果可以看到:
连接成功啦,后续我们就可以用网络调试助手模拟设备发指令啦。