基于 Kotlin+Netty 开发的 Android Web Server

简介: 基于 Kotlin+Netty 开发的 Android Web Server

一. 开发背景



最近半年来,我一直在从事开发公司的自助手机回收机项目。该项目有点类似于 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 等等。这些是已是下一阶段规划和开发的重点。

相关文章
|
3月前
|
开发框架 搜索推荐 数据可视化
Django框架适合开发哪种类型的Web应用程序?
Django 框架凭借其强大的功能、稳定性和可扩展性,几乎可以适应各种类型的 Web 应用程序开发需求。无论是简单的网站还是复杂的企业级系统,Django 都能提供可靠的支持,帮助开发者快速构建高质量的应用。同时,其活跃的社区和丰富的资源也为开发者在项目实施过程中提供了有力的保障。
157 62
|
2月前
|
前端开发 安全 JavaScript
2025年,Web3开发学习路线全指南
本文提供了一条针对Dapp应用开发的学习路线,涵盖了Web3领域的重要技术栈,如区块链基础、以太坊技术、Solidity编程、智能合约开发及安全、web3.js和ethers.js库的使用、Truffle框架等。文章首先分析了国内区块链企业的技术需求,随后详细介绍了每个技术点的学习资源和方法,旨在帮助初学者系统地掌握Dapp开发所需的知识和技能。
2025年,Web3开发学习路线全指南
|
3月前
|
设计模式 前端开发 数据库
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第27天】本文介绍了Django框架在Python Web开发中的应用,涵盖了Django与Flask等框架的比较、项目结构、模型、视图、模板和URL配置等内容,并展示了实际代码示例,帮助读者快速掌握Django全栈开发的核心技术。
267 45
|
3月前
|
存储 前端开发 JavaScript
如何在项目中高效地进行 Web 组件化开发
高效地进行 Web 组件化开发需要从多个方面入手,通过明确目标、合理规划、规范开发、加强测试等一系列措施,实现组件的高效管理和利用,从而提高项目的整体开发效率和质量,为用户提供更好的体验。
54 7
|
3月前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
64 2
|
3月前
|
前端开发 API 开发者
Python Web开发者必看!AJAX、Fetch API实战技巧,让前后端交互如丝般顺滑!
在Web开发中,前后端的高效交互是提升用户体验的关键。本文通过一个基于Flask框架的博客系统实战案例,详细介绍了如何使用AJAX和Fetch API实现不刷新页面查看评论的功能。从后端路由设置到前端请求处理,全面展示了这两种技术的应用技巧,帮助Python Web开发者提升项目质量和开发效率。
87 1
|
3月前
|
XML 安全 PHP
PHP与SOAP Web服务开发:基础与进阶教程
本文介绍了PHP与SOAP Web服务的基础和进阶知识,涵盖SOAP的基本概念、PHP中的SoapServer和SoapClient类的使用方法,以及服务端和客户端的开发示例。此外,还探讨了安全性、性能优化等高级主题,帮助开发者掌握更高效的Web服务开发技巧。
|
2天前
|
JavaScript 搜索推荐 Android开发
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
22 8
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
|
14天前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
115 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
12天前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
35 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex

热门文章

最新文章

  • 1
    如何修复 Android 和 Windows 不支持视频编解码器的问题?
  • 2
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 3
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
  • 4
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 5
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
  • 6
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 7
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 8
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
  • 9
    部署使用 CHAT-NEXT-WEB 基于 Deepseek
  • 10
    springSecurity学习之springSecurity过滤web请求