Android

Android – Servidor web con servicio en segundo plano

En este post voy a enseñarte a programar un servidor web en Android asegurándonos de que el web server se ejecuta siempre en segundo plano y que no será detenido por el sistema.

Vamos a programar un servidor web en Android usando Kotlin y la librería NanoHTTPD. Dicho web server va a ser iniciado desde un servicio (Service) que muestra una notificación persistente para que el usuario sepa que el servidor web está ejecutándose en segundo plano.

Te voy a enseñar a implementar el servicio, crear la notificación, solicitar los permisos para notificaciones e iniciar el servidor web en Android de manera programada.

Añadir dependencia NanoHTTPD

Esto puede cambiar entre proyectos pero en mi caso primero modifiqué mi libs.versions.toml. En [versions] añadí:

nanohttpd = "2.3.1"

Luego, en [libraries] lo siguiente:

nanohttpd = { group = "org.nanohttpd", name = "nanohttpd", version.ref = "nanohttpd" }

Finalmente mi libs.versions.toml completo quedó así:

[versions]
agp = "8.7.3"
kotlin = "2.0.0"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.0"
composeBom = "2024.04.01"
nanohttpd = "2.3.1"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
nanohttpd = { group = "org.nanohttpd", name = "nanohttpd", version.ref = "nanohttpd" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

 

Luego, en build.gradle.kts añadí lo siguiente en dependencies:

implementation(libs.nanohttpd)

De modo que quedó así:

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "me.parzibyte.puentehttp"
    compileSdk = 35

    defaultConfig {
        applicationId = "me.parzibyte.puentehttp"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
    buildFeatures {
        compose = true
    }
}

dependencies {

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)
    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
    implementation(libs.nanohttpd)
}

Programando servidor web

El servidor web queda así. Toma en cuenta que ahora solo manejo una ruta que muestra un hola mundo, ya a partir de aquí puedes añadir más rutas.

Mi archivo se llama SimpleServer.kt y queda así:

package me.parzibyte.puentehttp

import android.content.Context
import fi.iki.elonen.NanoHTTPD
import fi.iki.elonen.NanoHTTPD.IHTTPSession
import fi.iki.elonen.NanoHTTPD.Method
import fi.iki.elonen.NanoHTTPD.Response
import fi.iki.elonen.NanoHTTPD.newFixedLengthResponse

class SimpleServer(port: Int, private val context: Context) : NanoHTTPD(port) {
    override fun serve(session: IHTTPSession): Response {
        return manejadorHttpPrincipal(session, context);
    }
}

fun manejadorHttpPrincipal(session: IHTTPSession, context: Context): Response {
    if (session.method == Method.GET && session.uri == "/version") {
        return agregarCors(manejarPeticionVersion())
    }
    return agregarCors(newFixedLengthResponse("No encontrado"))
}
fun agregarCors(peticionHttp: Response): Response {
    peticionHttp.addHeader("Access-Control-Allow-Origin", "*")
    peticionHttp.addHeader("Access-Control-Allow-Headers", "*")
    peticionHttp.addHeader("Access-Control-Allow-Methods", "POST,GET,DELETE,PUT,OPTIONS")
    return peticionHttp
}


fun manejarPeticionVersion(): Response {
    return newFixedLengthResponse(
        Response.Status.OK,
        "application/json",
        "Hello"
    )
}

Para usar e iniciar el servidor simplemente debemos hacer lo siguiente:

servidor = SimpleServer(8000, contexto)
servidor.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false)

Todavía no lo vamos a hacer porque esto debe ir dentro del servicio.

Servicio en segundo plano

Ahora vamos a crear un servicio en segundo plano, iniciarlo desde nuestra actividad principal y, dentro de dicho servicio, encender el servidor web en Android en segundo plano.

Comenzamos creando el servicio, en mi caso se llama WebServerService.kt; el nombre es importante porque vamos a registrarlo en el manifiesto.

package me.parzibyte.puentehttp

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import fi.iki.elonen.NanoHTTPD
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class WebServerService() : Service() {
    private lateinit var servidor: SimpleServer
    override fun onCreate() {
        Log.d("MI_TAG", "onCreate")
        super.onCreate()
        val that = this
        val notificacionParaServicio = crearNotificacionParaServicio() 
        startForeground(1, notificacionParaServicio)  
        Log.d("MI_TAG", "Notificación para servicio enviada")
        CoroutineScope(Dispatchers.IO).launch {
            servidor = SimpleServer(8000, that)
            servidor.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false)
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("WebServerService", "onStartCommand")
        return START_STICKY 
    }

    override fun onBind(intent: Intent?): IBinder? {
        Log.d("WebServerService", "onBind")
        return null
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("WebServerService", "onDestroy")
        servidor.stop()
    }

    private fun crearNotificacionParaServicio(): Notification {
        Log.d("MI_TAG", "createNotification")
        val ID_CANAL_NOTIFICACION = "ServicioPuenteHTTP"
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            // Solo hace falta registrar el canal de notificación en android O y superiores,
            // de lo demás se encarga NotificationCompat
            val canalDeNotificacion = NotificationChannel(
                ID_CANAL_NOTIFICACION,
                "Puente HTTP",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(canalDeNotificacion)
        }
        return NotificationCompat.Builder(this, ID_CANAL_NOTIFICACION)
            .setContentTitle("Servicio de impresión")
            .setContentText("Puente ejecutándose en segundo plano")
            .setSmallIcon(R.drawable.ic_launcher_background)  // Usa un icono adecuado
            .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
            .build()
    }
}

Lo importante aquí es que a partir de Android Oreo debemos registrar un canal de notificaciones para mostrar la notificación. Estoy usando NotificationCompat para crear notificaciones sin importar la versión de Android.

Enviar una notificación cuando se crea un servicio en segundo plano (con crearNotificacionParaServicio) es obligatorio en las versiones más recientes de Android. Además, a partir de Android 13 debemos tener permiso de enviar notificaciones y pedir dicho permiso tanto en el manifiesto como en tiempo de ejecución.

Otra parte importante es registrar el servicio en el AndroidManifest.xml dentro de application:

<service android:name=".WebServerService" android:foregroundServiceType="dataSync"/>

Además, debemos añadir los siguientes permisos:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET" />

Solo como referencia, dejo mi AndroidManifest.xml completo:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.PuenteHTTP"
        tools:targetApi="31">
        <service android:name=".WebServerService" android:foregroundServiceType="dataSync"/>
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.PuenteHTTP">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

La parte importante y que realmente levanta el servidor web es el onCreate del servicio en el siguiente bloque:

CoroutineScope(Dispatchers.IO).launch {
    servidor = SimpleServer(8000, that)
    servidor.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false)
}

El puerto del servidor será el 8000 y puedes ajustarlo como prefieras.

Iniciar servidor

Finalmente en nuestra actividad principal iniciamos el servicio en el onCreate. Tengo la siguiente función que lo hace:

private var servidorIniciado = mutableStateOf(false)
private fun iniciarServidorSiTodosLosPermisosHanSidoConcedidos() {

    Toast.makeText(this, "Iniciando...", Toast.LENGTH_LONG).show()
    Log.d("MI_TAG", "Iniciando...")
    try {
        val serviceIntent = Intent(this, WebServerService::class.java)
        ContextCompat.startForegroundService(this, serviceIntent)
        servidorIniciado.value = true
        Log.d("MI_TAG", "Servidor iniciado ")
    } catch (e: Exception) {
        Log.d("MI_TAG", "Excepción ${e.message}")
        Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
        e.printStackTrace()
    }
}

Es importante que notes que servidorIniciado es una variable que está en conjunto con Jetpack Compose. Si tú no usas Compose entonces puedes omitir esas variables.

Ahora ya solo falta iniciar el servidor en el onCreate:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    enableEdgeToEdge()
    setContent {
        PuenteHTTPTheme {
            Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                Greeting(
                    name = "Android",
                    modifier = Modifier.padding(innerPadding)
                )
            }
        }
    }

    iniciarServidorSiTodosLosPermisosHanSidoConcedidos()
}

Por favor toma en cuenta que justo ahora no estoy manejando la solicitud de permisos en tiempo de ejecución para mostrar notificaciones. Lo añadiré más adelante.

Si todo sale bien, tan pronto inicie la aplicación se nos debe mostrar una notificación persistente:

Notificación de servicio en segundo plano – Servidor web Android

Y si navegamos a localhost:8000/version vamos a ver el servidor web manejando las peticiones:

Servidor web local en Android con NanoHTTPD y Kotlin

De ese modo vamos a tener un servidor web en Android ejecutándose siempre en segundo plano, con la seguridad de que el proceso no será detenido por el sistema operativo.

Permiso para mostrar notificaciones

A partir de Android Tiramisú se necesita un permiso solicitado en tiempo de ejecución para mostrar notificaciones, además de declararlo en el manifiesto.

Yo tengo la siguiente función que me devuelve la lista de permisos que necesita mi app, en este caso solo es el de mostrar notificaciones desde Android 13:

 fun obtenerPermisos(): MutableList<String> {
        val permisos = mutableListOf<String>(
        )
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            permisos.add(Manifest.permission.POST_NOTIFICATIONS)
        }
        return permisos;
    }

La siguiente función me permite saber si se han concedido todos los permisos:

private fun todosLosPermisosPermitidos(): Boolean {
    val missingPermissions = obtenerPermisos().filter {
        checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED
    }
    Log.d("MI_TAG", "Los missing son: ${missingPermissions}")
    return missingPermissions.isEmpty()
}

En caso de que los permisos no hayan sido concedidos, los solicito. Primero necesito el request permission launcher que será invocado después de que el usuario acepte o rechace los permisos:

val requestPermissionLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) {
        if (todosLosPermisosPermitidos()) {
            iniciarServidorSiTodosLosPermisosHanSidoConcedidos()
        } else {
            Toast.makeText(
                this,
                "No has concedido todos los permisos y por lo tanto no puedes usar la app",
                Toast.LENGTH_LONG
            ).show()
        }
    }

Luego los solicito así indicando el requestPermissionLauncher previamente creado:

private fun solicitarPermisos() {
    if (!todosLosPermisosPermitidos()) {
        requestPermissionLauncher.launch(obtenerPermisos().toTypedArray())
    }
}

Y mi función que inicia el servidor revisando los permisos es la siguiente:

private fun iniciarServidorSiTodosLosPermisosHanSidoConcedidos() {
    if (!todosLosPermisosPermitidos()) {
        solicitarPermisos()
        return
    }
    Log.d("MI_TAG", "Iniciando...")
    try {
        val serviceIntent = Intent(this, WebServerService::class.java)
        ContextCompat.startForegroundService(this, serviceIntent)
        servidorIniciado.value = true
        Log.d("MI_TAG", "Servidor iniciado ")
    } catch (e: Exception) {
        Log.d("MI_TAG", "Excepción ${e.message}")
        Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
        e.printStackTrace()
    }
}

Resumiendo

  1. Añade las dependencias de NanoHTTPD (libs.versions.toml y build.gradle.kts)
  2. Crea el archivo que va a manejar el servidor (SimpleServer.kt)
  3. Crea el servicio que va a iniciar el servidor (WebServerService.kt)
  4. Solicita e indica los permisos necesarios
  5. Registra el servicio en el manifiesto (AndroidManifest.xml)
  6. Inicia el servicio (que inicia a su vez el web server) cuando quieras, en este caso lo hicimos en el onCreate de la actividad principal (MainActivity.kt)

Por cierto, no es obligatorio usar NanoHTTPD; yo lo he usado porque es muy fácil de implementar. Lo realmente importante es saber manejar el servidor en segundo plano para que no sea detenido por el sistema.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Entradas recientes

Resetear GOOJPRT PT-210 MTP-II (Impresora térmica)

El día de hoy vamos a ver cómo restablecer la impresora térmica GOOJPRT PT-210 a…

1 mes hace

Proxy Android para impresora térmica ESC POS

Hoy voy a enseñarte cómo imprimir en una impresora térmica conectada por USB a una…

1 mes hace

Cancelar trabajo de impresión con C++

En este post te quiero compartir un código de C++ para listar y cancelar trabajos…

2 meses hace

Copiar bytes de Golang a JavaScript con WebAssembly

Gracias a WebAssembly podemos ejecutar código de otros lenguajes de programación desde el navegador web…

3 meses hace

Imprimir PDF con Ghostscript en Windows de manera programada

Revisando y buscando maneras de imprimir un PDF desde la línea de comandos me encontré…

3 meses hace

Hacer pruebas en impresora térmica Bluetooth Android

Esta semana estuve recreando la API del plugin para impresoras térmicas en Android (HTTP a…

3 meses hace

Esta web usa cookies.