프로그래밍/android

[kotlin] 안드로이드에서 REST API 서버 구현 (NanoHTTPd)

인썸니아 2024. 3. 10. 22:48

일반적으로.. 모바일 폰은 서버로부터 데이터를 받아 동작하는 클라이언트의 역할을 하는 것이 대부분이다. 또한, HTTP REST API 기능을 앱으로 구현한다고 한다면, 클라이언트의 기능을 구현한다고 생각하는 것이 일반적이다. 이동형 장치의 특성상, 수시로 네트워크가 변경되는 모바일 폰에 서버를 구축한다는 것 자체가 적합한 일은 아니다.

 

이유가 어찌 되었든,

기술적으로 모바일에서 REST API 서버를 구축할 수 있는 라이브러리가 있다.

 

## NanoHTTPd

 

GitHub - NanoHttpd/nanohttpd: Tiny, easily embeddable HTTP server in Java.

Tiny, easily embeddable HTTP server in Java. Contribute to NanoHttpd/nanohttpd development by creating an account on GitHub.

github.com

 

2016년 8월에 마지막 릴리즈를 한 매우 오래된 open-source 이다. 하지만, 심플한 편이라 지금도 잘 동작한다.

 

아래 링크의 guide가 정리가 잘 되어 도움이 되었다.

A Guide to NanoHTTPD
https://www.baeldung.com/nanohttpd

 

dependency 설정

앱 수준 gradle 파일에 아래와 같이 nanohttpd를 위한 dependency를 설정한다.

nanohttpd-nanolets 는 multi route를 사용할 경우 필요하다.

dependencies {
    ...
    
    // for REST API server
    implementation("org.nanohttpd:nanohttpd:2.3.1")
    implementation("org.nanohttpd:nanohttpd-nanolets:2.3.1")
    
    ...
}

 

kotlin 적용 코드

아래는 NanoHTTPd 를 적용하여 구현한 테스트 코드 중 일부이다.

// singlton
class RESTManager(private val port: Int) : RouterNanoHTTPD(port) {
    companion object {
        @Volatile private var server: RESTManager? = null
        fun getServer(port: Int): RESTManager {
            return server ?: synchronized(this) {
                server ?: RESTManager(port).also {
                    server = it
                }
            }
        }
    }

    fun addRoute(url: String, routeHandler: Class<*>, looperHandler: Handler) {
        super.addRoute(url, routeHandler, looperHandler)
    }

    fun startServer() {
        Log.i(LOG_TAG, "start rest server with $ipAddr:$port")
        start()
    }
}

// simple http server test
class HTTPManager(private val handler: Handler) : NanoHTTPD(7777) {
    init {
        start(SOCKET_READ_TIMEOUT, false)
    }

    override fun serve(session: IHTTPSession): Response {
        Log.d(LOG_TAG_DEBUG, "URI:: ${session.uri}")
        Log.d(LOG_TAG_DEBUG, "method:: ${session.method}")

        val hashMap = HashMap<String, String>()
        session.parseBody(hashMap)

        val receivedBody = if(hashMap.isNotEmpty()) hashMap["postData"] else session.queryParameterString
        if (receivedBody != null) {
            val data = Json.decodeFromString<RestTestRequest>(receivedBody)
            val msg: Message = Message().apply {
                this.obj = mapOf(session.uri to data)
            }
            handler.sendMessage(msg)
        }

        return newFixedLengthResponse("THIS IS TEST SERVER")
    }
}

// for multi route
class TestRestHandler : RouterNanoHTTPD.GeneralHandler() {
    private lateinit var looperHandler: Handler?

    override fun get(
        uriResource: RouterNanoHTTPD.UriResource,
        urlParams: Map<String, String>,
        session: NanoHTTPD.IHTTPSession
    ): NanoHTTPD.Response {
        val sessingParam = session.parameters!!

        Log.d(LOG_TAG_DEBUG, sessingParam.toString())
        sessingParam.forEach {
            Log.d(LOG_TAG_DEBUG, "${it.key} : ${it.value}")
        }

        return NanoHTTPD.newFixedLengthResponse("Requested: \n$sessingParam\n")
    }

    override fun post(
        uriResource: RouterNanoHTTPD.UriResource?,
        urlParams: MutableMap<String, String>?,
        session: NanoHTTPD.IHTTPSession?
    ): NanoHTTPD.Response {
        Log.d(LOG_TAG_DEBUG, "${this::class.simpleName} POST !!")
        
        if(!::looperHandler.isInitialized)
            looperHandler = uriResource?.initParameter(Handler::class.java)

        val hashMap = HashMap<String, String>()
        session?.parseBody(hashMap)

        val receivedBody = if(hashMap.isNotEmpty()) hashMap["postData"] else session?.queryParameterString
        if (receivedBody != null) {
            // RestTestRequest는 @Serializable data class
            val data = Json.decodeFromString<RestTestRequest>(receivedBody)

            val json = Json.parseToJsonElement(receivedBody)
            json.jsonObject.toMap().forEach {
                Log.d(LOG_TAG_DEBUG, "receivedBody:: ${it.key} : ${it.value}")
            }
            
            // POST로 전달받은 Json Data로부터 필요한 동작들 수행.
            looperHandler?.sendEmptyMessage(data.cmdCode)
        }

        return NanoHTTPD.newFixedLengthResponse("Received body:\n$receivedBody\n")
    }
}

 

 

위 클래스들은 아래와 같이 사용할 수 있다.

 

- HTTP Server

val http = HTTPManager(object : Handler(Looper.getMainLooper()) {
    override fun handleMessage(msg: Message) {
        var uri = ""
        lateinit var data: RestTestRequest
        (msg.obj as Map<*, *>).firstNotNullOf {
            uri = it.key as String
            data = it.value as RestTestRequest
        }
        Log.d(LOG_TAG_DEBUG, "DATA:: $data")
        super.handleMessage(msg)
    }
})

 

- REST API Server (multi route)

private fun startREST(port: Int) {
    restManager = RESTManager.getServer(port)

    // IndexHandler는 nanoHttpd에서 제공하는 기본 handler로 "Hello World!"를 응답한다.
    restManager.addRoute("/", IndexHandler::class.java)
    // runTestCommand 는 looper handler
    restManager.addRoute("/test", RestHandler::class.java, runTestCommand)

    restManager.startServer()
}

 

 

반응형