34

基于 Kotlin+Netty 开发的 Android Web Server - 简书

 4 years ago
source link: https://www.jianshu.com/p/377a3b6dd72b?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
12020.03.29 22:15:16字数 1,025阅读 231
webp
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 等等。这些是已是下一阶段规划和开发的重点。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK