一. 对原先 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 服务端、踩过的坑,以及如何封装消息。