一. 对原先 NettyServer 的改造
上一篇文章《Kotlin + Netty 在 Android 上实现 Socket 的服务端》 ,曾经介绍的 NettyServer 其实只存了最后一次使用的 Channel。
Channel 是 Netty 网络操作抽象类,包括网络的读、写、发起连接、链路关闭等,它是 Netty 网络通信的主体。
在现实的开发中,服务端可能需要的是保存多个 Channel,例如存放到 ConcurrentHashMap。
当客户端连上服务端时,通过 NettyServer 的 addChannel() 将 channel 添加到 Map 中。
当客户端断开服务端时,通过 NettyServer 的 removeChannel() 将 channel 从 Map 中移除。
为了安全考虑,服务端可能会主动断开某个 channel,则通过 NettyServer 的 disConnectChannel() 实现。
private val channelMap = ConcurrentHashMap<String,Channel>() // Map 存储 channelId、Channel fun addChannel(channel: Channel) { channelMap.put(channel.id().asShortText(), channel) } fun removeChannel(channelId:String) { channelMap.remove(channelId) } /** * 根据 channelId,断开 channel */ fun disConnectChannel(channelId:String) { channelMap.get(channelId)?.let { it.close() // 此时不用通过 channelMap 来 remove channelId,因为 NettyService 会监听到断开连接并调用 removeChannel() } }
二. 通过 Service 方式启动 Netty 服务
2.1 NettyService 的实现
由于 App 中存在多个 Activity 会用到 Netty 的相关服务例如接受来自客户端的消息、发送消息到客户端,所以采用 Service 方式来启动 Netty 服务端是一种比较好的选择。
Service 跟 Activity 的交互也可以借助 EventBus 进行通信。
class NettyService : Service() { var handler:Handler = Handler(Looper.getMainLooper()) override fun onCreate() { super.onCreate() startServer() } // 启动 Netty 服务端 private fun startServer() { if (!NettyServer.isServerStart) { NettyServer.setListener(object : NettyServerListener<String>{ /** * 网页发送的 WebSocket 消息的回调 */ override fun onMessageResponseServer(msg: String, ChannelId: String) { LogUtils.d("msg = $msg") val message = GsonUtils.fromJson<RequestMessage>(msg, RequestMessage::class.java) if (message is RequestMessage) { when(message.action) { ...... } } } /** * 监听 Netty Server 的启动 */ override fun onStartServer() { LogUtils.d("NettyServer Start") } /** * 监听 Netty Server 的关闭 */ override fun onStopServer() { LogUtils.d("NettyServer Stop") } /** * 监听 Netty Server 的连接 */ override fun onChannelConnect(channel: Channel) { val insocket = channel.remoteAddress() as InetSocketAddress val clientIP = insocket.address.hostAddress NettyServer.addChannel(channel) LogUtils.d("connect client: $clientIP") handler.post { BusManager.getBus().post(ConnectWifiEvent(clientIP,channel.id().asShortText())) } } /** * 监听 Netty Server 的断开连接 */ override fun onChannelDisConnect(channel: Channel) { val ip = channel.remoteAddress().toString() NettyServer.removeChannel(channel.id().asShortText()) LogUtils.d("disconnect client: $ip") handler.post { BusManager.getBus().post(DisConnectWifiEvent()) } } }) NettyServer.port = 8888 NettyServer.webSocketPath = "/xxx_path" NettyServer.start() } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { return super.onStartCommand(intent, flags, startId) } override fun onDestroy() { if (NettyServer.isServerStart) { // 关闭 Netty Server NettyServer.disconnect() } super.onDestroy() } override fun onBind(intent: Intent): IBinder? { return null } }
2.2 onStartCommand 的坑?
在使用 NettyService 时,发现会遇到如下的异常:
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter intent
NettyService 是通过 startService() 启动,所以会调用 onStartCommand()。而使用 Kotlin 在创建 Service 时,默认的 onStartCommand() 方法是这样的:
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { return super.onStartCommand(intent, flags, startId) }
但是 intent 存在为空的可能性,需要改成:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { return super.onStartCommand(intent, flags, startId) }
三. 对消息的封装
客户端和服务端之间传递的消息类型是 String 类型。即便是这样,还是要稍微定义一下 Message 的格式。
例如:
abstract class Message{ abstract var action:String } // 网页发送过来的消息 data class RequestMessage(override var action:String="",val body:Map<String,String>?=null):Message() // 发送给网页的消息 data class ResponseMessage(override var action:String="",val body:Map<String,String>?=null):Message()
封装一个 NettyManager,它是单例,用于专门发送消息给客户端。
其中 sendMsg(msg: ()->String) 方法,它的参数是函数类型。将函数作为参数,可扩展性会更强。
Kotlin 的函数是第一等公民,函数就是对象,这是 Kotlin 作为函数式编程语言的重要特性。对象可以直接赋值给变量、可以作为某个函数的参数、也可以作为别的函数的返回值,那么函数也可以。
object NettyManager { fun sendXXX() { sendMsg { val responseMsg = ResponseMessage(action="xxx") GsonUtils.toJson(responseMsg) } } /** * 服务端向网页发送生成二维码的消息,并返回生成随机的字符串 */ fun sendDisplayQrcode():String{ val action = "display_qrcode" val map = mutableMapOf<String,String>() val randomString = RandomStringUtils.randomAlphanumeric(6) map.put("qrCode", randomString) sendMsg { val responseMsg = ResponseMessage(action,map) GsonUtils.toJson(responseMsg) } return randomString } ...... private fun sendMsg(msg: ()->String) { NettyServer.sendMsgToWS(msg.invoke(), ChannelFutureListener { channelFuture -> if (channelFuture.isSuccess) { LogUtils.d("write successful") } else { LogUtils.d("write error") } }) } }
服务端发送消息给客户端:
NettyManager.sendXXX()
四. 总结
本文是上一篇《Kotlin + Netty 在 Android 上实现 Socket 的服务端》的延续,介绍了如何做一个 Android 的 Netty 服务端、踩过的坑,以及如何封装消息。