Ktor 라이브러리
ktor
라이브러리를 사용하기 위해서 먼저 build.gradle
에 다음과 같은 설정을 해줍니다.
build.gradle(프로젝트)
ktor
라이브러리 버전을 너무 최신으로 했더니 일부 호환되지 않는 라이브러리들이 있어서 여기서는 버전을 1.2.5
로 지정했습니다.
buildscript {
ext.kotlin_version = '1.3.50'
ext.ktor_version = '1.2.5'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
maven { url "https://dl.bintray.com/kotlin/ktor" }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
build.gradle(모듈)
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.snowdeer.webserver"
minSdkVersion 27
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
exclude 'META-INF/*'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "io.ktor:ktor-server-netty:1.2.5"
implementation "ch.qos.logback:logback-classic:1.2.3"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
Main.kt
package com.snowdeer.webserver
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Environment
import android.os.Environment.DIRECTORY_DOCUMENTS
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
companion object {
private const val PERMISSION_REQUEST_CODE = 100
}
private val assetInstaller = AssetInstaller()
private val router = Router()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.i("snowdeer", "[snowdeer] onCreate()")
requestPermission()
}
private fun requestPermission() {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
requestPermissions(permissions, PERMISSION_REQUEST_CODE)
} else {
start()
}
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
PERMISSION_REQUEST_CODE -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(applicationContext, "Permission 완료", Toast.LENGTH_SHORT).show()
start()
} else {
Toast.makeText(applicationContext, "Permission 실패", Toast.LENGTH_SHORT).show()
}
}
}
private fun start() {
val parent = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOCUMENTS)
val staticContentDirectory = "${parent.absolutePath}/static"
assetInstaller.install(this, "static", staticContentDirectory)
router.startServer(this, staticContentDirectory, "/static")
}
}
AssetInstaller.kt
assets
디렉토리에 있는 파일을 단말내 저장소에 복사하는 용도의 클래스입니다.
package com.snowdeer.webserver
import android.content.Context
import android.content.res.AssetManager
import android.util.Log
import java.io.File
import java.io.FileOutputStream
class AssetInstaller {
fun install(ctx: Context, fromDirectory: String, targetDirectory: String) {
ctx?.assets?.let {
recursiveCopy(it, fromDirectory, targetDirectory)
}
}
private fun recursiveCopy(am: AssetManager, src: String, target: String) {
val files = am.list(src)
files?.let {
for (filename in it) {
val filepath = "$src/$filename"
val targetPath = "$target/$filename"
if ((am.list(filepath) == null) || (am.list(filepath)?.size == 0)) {
Log.i("snowdeer", "[snowdeer] copy $filepath to $targetPath")
copyFile(am, filepath, targetPath)
} else {
Log.i("snowdeer", "[snowdeer] $filepath is a directory.")
createDirectory(targetPath)
recursiveCopy(am, filepath, targetPath)
}
}
}
}
private fun createDirectory(path: String) {
val directory = File(path)
if (!directory.exists()) {
directory.mkdirs()
}
}
private fun copyFile(am: AssetManager, src: String, target: String) {
val file = File(target)
if (file.exists()) {
file.delete()
}
val inputStream = am.open(src)
val outputStream = FileOutputStream(target)
var read: Int
val buffer = ByteArray(1024)
while (true) {
read = inputStream?.read(buffer)
if (read == -1) {
break
}
outputStream.write(buffer, 0, read)
}
inputStream?.close()
outputStream.flush()
outputStream.close()
}
}
Router.kt
package com.snowdeer.webserver
import android.content.Context
import android.util.Log
import android.widget.Toast
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.CORS
import io.ktor.http.HttpMethod
import io.ktor.response.respondFile
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import java.io.File
class Router {
fun startServer(ctx: Context, staticContentDirectory: String, staticContentURL: String) {
Log.i("snowdeer", "[snowdeer] startServer()")
Toast.makeText(ctx, "startServer()", Toast.LENGTH_SHORT).show()
val server = embeddedServer(Netty, port = 8080) {
installCors(this)
routing {
staticContentDirectory?.let {
addStaticContentRoute(this, staticContentDirectory, staticContentDirectory, staticContentURL)
}
get("/react") {
Log.i("snowdeer", "[snowdeer] /react is called.")
val file = File("${staticContentDirectory}/web/react.html")
call.respondFile(file)
}
get("/tetris") {
val file = File("${staticContentDirectory}/web/tetris.html")
call.respondFile(file)
}
get("/maker") {
val file = File("${staticContentDirectory}/web/bt_maker.html")
call.respondFile(file)
}
get("/tree") {
val file = File("${staticContentDirectory}/tree/MoveToPoint")
call.respondFile(file)
}
}
}
server.start()
}
private fun installCors(server:Application) {
server.install(CORS) {
method(HttpMethod.Options)
method(HttpMethod.Get)
method(HttpMethod.Post)
method(HttpMethod.Put)
method(HttpMethod.Delete)
method(HttpMethod.Patch)
anyHost()
}
}
private fun addStaticContentRoute(routing: Routing, path: String, parentPath: String, url: String) {
val directory = File(path)
val files = directory.listFiles()
for (f in files) {
if (f.isDirectory) {
val routeUrl = url + f.absolutePath.substring(parentPath.length) + "/{filename}"
Log.i("snowdeer", "[snowdeer] routeUrl($routeUrl)")
routing.get(routeUrl) {
val fileName = call.parameters["filename"]
val file = File("${f.absolutePath}/$fileName")
call.respondFile(file)
}
addStaticContentRoute(routing, f.absolutePath, parentPath, url)
}
}
}
}