美文网首页Kotlin 实战
基于 Kotlin+Netty 开发的 Android Web

基于 Kotlin+Netty 开发的 Android Web

作者: fengzhizi715 | 来源:发表于2020-03-29 22:15 被阅读0次
    woman-wearing-white-floral-off-shoulder-top-3653167.jpg

    一. 开发背景

    最近半年来,我一直在从事开发公司的自助手机回收机项目。该项目有点类似于 IoT 项目,通过 Android 系统来操作回收机中的各种传感器,以此来控制回收机中的各种硬件。这涉及到各种通信协议,例如串口的通信,还有 TCP、http 协议等。

    在我们的回收机中,Android 上使用的 http 服务来自一个第三方的库,从监控上看最近该库报错有一点多。

    我们回收机本身提供的 TCP、WebSocket 服务均由 Netty 开发,而 http 服务它运行在TCP之上,因此也可以使用 Netty 来提供 http 服务,从而可以减少第三方库的依赖。

    二. AndroidServer 特性

    正是基于上面的开发背景,我最近抽空开发了一个 AndroidServer

    github 地址:https://github.com/fengzhizi715/AndroidServer

    它的特性包括:

    • 支持 Http、TCP、WebSocket 服务
    • 支持 Rest 风格的 API
    • Http 的路由表采用字典树(Tried Tree)实现
    • 开发者可以使用自己的日志库
    • core 模块只依赖 netty-all,不依赖其他第三方库

    三. AndroidServer 设计原理

    3.1 http 服务之 Request、Response

    一个完整的 http 服务一定需要 Request、Response

    /**
     *
     * @FileName:
     *          com.safframework.server.core.http.Request
     * @author: Tony Shen
     * @date: 2020-03-21 12:31
     * @version: V1.0 <描述当前版本功能>
     */
    interface Request {
    
        fun method(): HttpMethod
    
        fun url(): String
    
        fun headers(): MutableMap<String, String>
    
        fun header(name: String): String?
    
        fun cookies(): Set<HttpCookie>
    
        fun params(): MutableMap<String, String>
    
        fun param(name: String): String?
    
        fun content(): String
    }
    
    /**
     *
     * @FileName:
     *          com.safframework.server.core.http.Response
     * @author: Tony Shen
     * @date: 2020-03-21 13:09
     * @version: V1.0 <描述当前版本功能>
     */
    interface Response {
    
        fun setStatus(status: HttpResponseStatus): Response
    
        fun setBodyJson(any: Any): Response
    
        fun setBodyHtml(html: String): Response
    
        fun setBodyData(contentType: String, data: ByteArray): Response
    
        fun setBodyText(text: String): Response
    
        fun addHeader(key: CharSequence, value: CharSequence): Response
    
        fun addHeader(key: AsciiString, value: AsciiString): Response
    
        fun addCookie(cookie: HttpCookie): Response
    }
    

    在 AndroidServer 中他们的实现者分别是:HttpRequest、HttpResponse。

    其中, HttpRequest 包含了 Netty 的 FullHttpRequest,HttpResponse 包含了 Netty 的 Channel、DefaultFullHttpResponse。

    FullHttpRequest 包含了 HttpRequest 和 FullHttpMessage,是一个 HTTP 请求的完全体。

    通过 FullHttpRequest 可以从中提取 http 请求方法、请求头、请求体的具体信息,包括 cookie、parameter 等等。

    Channel 是 Netty 网络操作抽象类,包括网络的读、写、发起连接、链路关闭等,它是 Netty 网络通信的主体。

    Channel代表了一个 Socket 链接。

    通过 DefaultFullHttpResponse 来构造完整的 HttpResponse。

        fun buildFullH1Response(): FullHttpResponse {
            var status = this.status
            val response = DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status?:HttpResponseStatus.OK, buildBodyData())
            response.headers().set(HttpHeaderNames.SERVER, SERVER_VALUE)
            headers.forEach { (key, value) -> response.headers().set(key, value) }
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, buildBodyData().readableBytes())
            return response
        }
    

    因此,最终通过如下的配置完成简单的 http 服务:

            pipeline
                .addLast("http-codec", HttpServerCodec())
                .addLast("aggregator", HttpObjectAggregator(builder.maxContentLength))
                .addLast("request-handler", H1BrokerHandler(routeRegistry))
    
    class H1BrokerHandler(private val routeRegistry: RouteTable): ChannelInboundHandlerAdapter() {
    
        @Throws(Exception::class)
        override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
            if (msg is FullHttpRequest) {
    
                val request = HttpRequest(msg)
                val response = routeRegistry.getHandler(request)?.let {
                    val impl = it.invoke(request, HttpResponse(ctx.channel())) as HttpResponse
                    impl.buildFullH1Response()
                }
                ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
            } else {
                LogManager.w("H1BrokerHandler","unknown message type ${msg}")
            }
            ctx.fireChannelRead(msg)
        }
    }
    

    3.2 字典树

    在 H1BrokerHandler 中,request 请求通过查找路由表来找到对应的 RequestHandler。

    typealias RequestHandler = (Request, Response) -> Response
    

    路由表中定义了多个字典树

    /**
     *
     * @FileName:
     *          com.safframework.server.core.router.RouteTable
     * @author: Tony Shen
     * @date: 2020-03-21 21:28
     * @version: V1.0 <描述当前版本功能>
     */
    object RouteTable {
    
        private val getTrie: PathTrie<RequestHandler> = PathTrie()
        private val postTrie: PathTrie<RequestHandler> = PathTrie()
        private val putTrie: PathTrie<RequestHandler> = PathTrie()
        private val deleteTrie: PathTrie<RequestHandler> = PathTrie()
        private val headTrie: PathTrie<RequestHandler> = PathTrie()
        private val traceTrie: PathTrie<RequestHandler> = PathTrie()
        private val connectTrie: PathTrie<RequestHandler> = PathTrie()
        private val optionsTrie: PathTrie<RequestHandler> = PathTrie()
        private val patchTrie: PathTrie<RequestHandler> = PathTrie()
        private var errorController: RequestHandler?=null
    
        fun registHandler(method: HttpMethod, url: String, handler: RequestHandler) {
            getTable(method).insert(url, handler)
        }
    
        private fun getTable(method: HttpMethod): PathTrie<RequestHandler> =
            when (method) {
                HttpMethod.GET     -> getTrie
                HttpMethod.POST    -> postTrie
                HttpMethod.PUT     -> putTrie
                HttpMethod.DELETE  -> deleteTrie
                HttpMethod.HEAD    -> headTrie
                HttpMethod.TRACE   -> traceTrie
                HttpMethod.CONNECT -> connectTrie
                HttpMethod.OPTIONS -> optionsTrie
                HttpMethod.PATCH   -> patchTrie
            }
    
        /**
         * 支持自定义错误的
         */
        fun errorController(errorController: RequestHandler) {
            this.errorController = errorController
        }
    
        fun getHandler(request: Request): RequestHandler = getTable(request.method()).fetch(request.url(),request.params())
            ?: errorController
            ?: NotFound()
    
        fun isNotEmpty():Boolean = !isEmpty()
    
        fun isEmpty():Boolean = getTrie.getRoot().getChildren().isEmpty()
                && postTrie.getRoot().getChildren().isEmpty()
                && putTrie.getRoot().getChildren().isEmpty()
                && deleteTrie.getRoot().getChildren().isEmpty()
                && headTrie.getRoot().getChildren().isEmpty()
                && traceTrie.getRoot().getChildren().isEmpty()
                && connectTrie.getRoot().getChildren().isEmpty()
                && optionsTrie.getRoot().getChildren().isEmpty()
                && patchTrie.getRoot().getChildren().isEmpty()
    }
    

    在计算机科学中,trie,又称前缀树字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

    字典树的核心思想是空间换时间,它在搜索字符串时是非常地高效,特别适用于构建文本搜索和词频统计等应用。

    在 AndroidServer 中,使用字典树来存储 http 服务的路径和对应的 RequestHandler。正是因为其查找的速度快于正则表达式。

    3.3 Socket 服务

    可以参考之前的文章Kotlin + Netty 在 Android 上实现 Socket 的服务端

    四. AndroidServer 使用

    4.1 http 服务

    通过使用 Service 来提供一个 http 服务,它的 http 服务本身支持 rest 风格、支持跨域、cookies 等。

    class HttpService : Service() {
    
        private lateinit var androidServer: AndroidServer
    
        override fun onCreate() {
            super.onCreate()
            startServer()
        }
    
        // 启动 Http 服务端
        private fun startServer() {
    
            androidServer = AndroidServer.Builder().converter(GsonConverter()).build()
    
            androidServer
                .get("/hello")  { _, response: Response ->
                    response.setBodyText("hello world")
                }
                .get("/sayHi/{name}") { request,response: Response ->
                    val name = request.param("name")
                    response.setBodyText("hi $name!")
                }
                .post("/uploadLog") { request,response: Response ->
                    val requestBody = request.content()
                    response.setBodyText(requestBody)
                }
                .start()
        }
    
        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            return super.onStartCommand(intent, flags, startId)
        }
    
        override fun onDestroy() {
            androidServer.close()
            super.onDestroy()
        }
    
    
        override fun onBind(intent: Intent): IBinder? {
            return null
        }
    
    }
    

    测试:

    curl -v 127.0.0.1:8080/hello
    *   Trying 127.0.0.1...
    * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
    > GET /hello HTTP/1.1
    > Host: 127.0.0.1:8080
    > User-Agent: curl/7.50.1-DEV
    > Accept: */*
    >
    < HTTP/1.1 200 OK
    < server: monica
    < content-type: text/plain
    < content-length: 11
    <
    * Connection #0 to host 127.0.0.1 left intact
    hello world
    
    curl -v -d 测试 127.0.0.1:8080/uploadLog
    *   Trying 127.0.0.1...
    * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
    > POST /uploadLog HTTP/1.1
    > Host: 127.0.0.1:8080
    > User-Agent: curl/7.50.1-DEV
    > Accept: */*
    > Content-Length: 6
    > Content-Type: application/x-www-form-urlencoded
    >
    * upload completely sent off: 6 out of 6 bytes
    < HTTP/1.1 200 OK
    < server: monica
    < content-type: text/plain
    < content-length: 6
    <
    * Connection #0 to host 127.0.0.1 left intact
    测试
    

    4.2 Socket 服务

    Socket 服务,AndroidServer 支持同一个端口同时提供 TCP/WebSocket 服务

    class SocketService : Service() {
    
        private lateinit var androidServer: AndroidServer
    
        override fun onCreate() {
            super.onCreate()
            startServer()
        }
    
        // 启动 Socket 服务端
        private fun startServer() {
            androidServer = AndroidServer.Builder().converter(GsonConverter()).port(8888).logProxy(LogProxy).build()
    
            androidServer
                .socket("/ws", object: SocketListener<String> {
                    override fun onMessageResponseServer(msg: String, ChannelId: String) {
                        LogManager.d("SocketService","msg = $msg")
                    }
    
                    override fun onChannelConnect(channel: Channel) {
                        val insocket = channel.remoteAddress() as InetSocketAddress
                        val clientIP = insocket.address.hostAddress
                        LogManager.d("SocketService","connect client: $clientIP")
    
                    }
    
                    override fun onChannelDisConnect(channel: Channel) {
                        val ip = channel.remoteAddress().toString()
                        LogManager.d("SocketService","disconnect client: $ip")
                    }
    
                })
                .start()
        }
    
        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            return super.onStartCommand(intent, flags, startId)
        }
    
        override fun onDestroy() {
    
            androidServer.close()
            super.onDestroy()
        }
    
    
        override fun onBind(intent: Intent): IBinder? {
            return null
        }
    }
    

    Socket 服务可以使用 :https://github.com/fengzhizi715/NetDiagnose 进行测试

    五. 总结

    AndroidServer 目前基本满足我们项目的需求。
    github 地址:https://github.com/fengzhizi715/AndroidServer

    但是,如果要作为一个通用的 Server,仍有很多不足之处,例如没有支持到 https、HttpSession、HTTP/2 等等。这些是已是下一阶段规划和开发的重点。

    相关文章

      网友评论

        本文标题:基于 Kotlin+Netty 开发的 Android Web

        本文链接:https://www.haomeiwen.com/subject/lnkeuhtx.html