返回

Kotlin安卓直连MySQL失败?IP/配置/防火墙错误及API方案

Android

Kotlin Android 直连 MySQL 失败?解密 Communications link failure

写 Android 应用的时候,有时候想图个方便,直接从 App 里连本地电脑上跑的 MySQL 或者 MariaDB 数据库,比如用 XAMPP 搭的环境。但很可能,你会碰上一个头疼的错误:

CommunicationsException: Communications link failure

具体报错信息可能长这样:

2021-08-29 15:27:54.859 7973-7973/com.example.invoices D/Amado: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.

代码看上去可能挺简单,类似下面这样,用了标准的 JDBC 来尝试连接:

package com.example.invoices
import android.util.Log
import java.sql.SQLException
import java.sql.DriverManager
import java.sql.Connection
import java.lang.Class.forName // 需要显式加载驱动 (虽然现代 JDBC 可能不需要)

object DBConncection {
    // 注意:这是一个极其不推荐的做法,仅作分析用!
    // 实际网络请求必须在后台线程执行
    @JvmStatic
    fun connection(){
        try{
            // 尝试加载驱动(可选,JDBC 4.0+ 规范下通常自动加载)
            // Class.forName("com.mysql.cj.jdbc.Driver") 

            Log.d("Amado", "尝试连接数据库...")
            // !!!关键问题点:连接地址用了 127.0.0.1 !!!
            val conn = DriverManager.getConnection(
                "jdbc:mysql://127.0.0.1:3306/invoices", "root", "123");

            Log.d("Amado","数据库连接成功!") // 如果能到这里,说明连上了
            conn.close() // 别忘了关闭连接
        }
        catch (e: SQLException){
            Log.e("Amado","数据库连接失败: ${e.message}")
            Log.e("Amado", e.toString()) // 打印详细错误栈
        }
        catch (e: Exception) {
            // 捕获其他可能的异常,比如 ClassNotFoundException
            Log.e("Amado", "发生其他错误: ${e.message}")
            Log.e("Amado", e.toString())
        }
    }
}

明明 XAMPP 里的数据库服务跑得好好的,phpMyAdmin 也能访问,怎么到了 Android App 这儿就不行了呢?网上的方法试了一圈也没用,到底问题出在哪?

为什么会连接失败?

这个 Communications link failure 错误,字面意思是“通信连接失败”。原因通常和网络有关,具体到这个场景,主要是以下几个可能性:

  1. IP 地址用错了: 127.0.0.1 的误解

    • 最常见的原因!在你的电脑上,127.0.0.1 (或者 localhost) 指的是本机。但在 Android 模拟器或者真机里,127.0.0.1 指的是 模拟器或设备本身,而不是你运行 XAMPP 的那台电脑。你的 App 尝试连接的是设备自己的 3306 端口,那里当然没有 MySQL 服务。
  2. 网络不可达或防火墙拦截

    • 即使你用了正确的电脑 IP 地址,电脑的防火墙(Windows 防火墙、其他安全软件)也可能阻止了来自局域网其他设备(你的手机或模拟器)对 3306 端口的访问请求。
  3. 数据库配置问题

    • MySQL/MariaDB 默认可能只监听本地回环地址 (127.0.0.1) 的连接请求,拒绝来自其他 IP 地址的连接。你需要修改配置,让它监听你电脑的局域网 IP 或所有 IP (0.0.0.0)。
    • 数据库用户 root (或其他用户) 可能没有被授权从你的手机或模拟器的 IP 地址进行连接。默认情况下,root 用户通常只允许从 localhost 登录。
  4. Android 网络权限

    • Android 应用如果需要访问网络,必须在 AndroidManifest.xml 文件里声明 INTERNET 权限。没加的话,任何网络请求都会失败(虽然通常是 SecurityException 或类似错误,但也可能间接导致连接问题)。
  5. 网络操作在主线程 (虽然报错不是 NetworkOnMainThreadException)

    • 上面的示例代码没有展示 DBConncection.connection() 是在哪里调用的。所有网络相关的操作(包括数据库连接)都必须在后台线程执行,否则会阻塞 UI 线程,导致 NetworkOnMainThreadException 应用崩溃。虽然你看到的报错是 CommunicationsException,但这仍然是一个必须遵守的规则。

为什么不应该直接从 App 连接数据库?

在我们尝试“修复”这个连接之前,必须先强调一个更重要的问题:通常情况下,你不应该、也绝不应该直接从移动 App 连接到你的后端数据库!

这么做有几个严重的弊端:

  1. 极大的安全风险!

    • 数据库凭证暴露: 连接数据库需要用户名和密码。把这些凭证硬编码或者存储在 App 里,意味着任何反编译你 App 的人都能轻易拿到,直接访问你的数据库,后果不堪设想。
    • 数据库端口暴露: 为了让 App 能连上,你可能需要把数据库服务器的端口(如 3306)暴露在局域网甚至公网上。这等于给攻击者敞开了大门。
    • 缺乏访问控制: 数据库通常只有比较粗粒度的权限控制。很难精细地限制一个 App 用户只能访问他自己的数据。
  2. 糟糕的可扩展性

    • 数据库连接是非常宝贵的资源。如果每个 App 实例都直接和数据库建立连接,当用户量增大时,会迅速耗尽数据库的最大连接数,导致服务瘫痪。专业的做法是在服务器端维护一个高效的连接池。
  3. 维护噩梦

    • 数据库结构(表、字段)或者业务逻辑稍微一变,所有用户的 App 可能都需要更新才能正常工作。这非常僵硬。
    • 把业务查询逻辑(SQL 语句)散落在 App 代码里,难以管理和优化。
  4. 性能和体验问题

    • 移动网络环境不稳定,延迟高。App 直接和数据库进行多次数据交互,体验会很差。通常需要一个中间层来优化数据传输。

正确的姿势:拥抱后端 API

解决上述所有问题的标准方法,是引入一个后端应用程序接口 (Backend API) ,通常是 RESTful API

整个流程变成这样:

  1. 数据库 仍然安全地运行在你的服务器(或者本地电脑)上,不直接暴露给外部网络。
  2. 你开发一个 后端服务 (使用 Java/Kotlin + Spring Boot/Ktor, Python + Django/Flask, Node.js + Express 等等)。
  3. 这个后端服务负责连接数据库,执行业务逻辑(增删改查等)。
  4. 后端服务提供 HTTP(S) 接口 (API Endpoints),比如 GET /api/invoices 获取发票列表,POST /api/invoices 创建新发票。
  5. 你的 Android App 通过 HTTP(S) 请求(使用 Retrofit, Ktor Client, Volley 等库)来调用这些 API 接口。
  6. 后端服务处理请求,操作数据库,然后将结果(通常是 JSON 格式)返回给 App。
  7. App 接收到 JSON 数据,解析并展示给用户。

这种架构的好处显而易见:

  • 安全: 数据库凭证和连接只存在于受控的后端服务器。App 只需和 API 打交道,可以通过 Token 等方式进行认证授权。
  • 可控: 业务逻辑集中在后端,修改后无需更新 App(除非 API 接口本身发生不兼容变化)。数据库结构变化对 App 透明。
  • 高效: 后端可以实现数据库连接池、缓存等优化。API 可以设计得更适合移动端,一次请求返回所需数据。
  • 解耦: 前后端分离,可以独立开发、测试和部署。同一套后端 API 可以服务于 Android、iOS、Web 等多个客户端。

方案一:修复(但不推荐的)直接连接尝试

虽然强烈不推荐,但如果你只是想在本地开发环境中,理解一下为什么之前的代码连不上,并且愿意承担相应的风险,可以尝试以下步骤来“修复”它:

重要提示: 这仅适用于受信任的本地开发网络环境!绝不要在生产环境或任何不受信任的网络中这样做!

步骤 1: 找到运行 XAMPP 的电脑的 IP 地址

  • Windows: 打开命令提示符 (cmd),输入 ipconfig,查找你的“无线局域网适配器 WLAN”或“以太网适配器以太网”下的 IPv4 地址,通常是 192.168.x.x10.x.x.x 之类的内网 IP。
  • macOS/Linux: 打开终端,输入 ifconfigip addr,查找类似 en0eth0 网卡下的 inet 地址。

假设你找到的 IP 是 192.168.1.100

步骤 2: 修改 Kotlin 代码中的连接字符串

127.0.0.1 替换成你电脑的实际 IP 地址:

// ... 省略其他代码 ...
try {
    // ...
    Log.d("Amado", "尝试连接数据库 IP: 192.168.1.100") // 使用你的实际IP
    val conn = DriverManager.getConnection(
        // 把 127.0.0.1 换成你电脑的局域网 IP
        "jdbc:mysql://192.168.1.100:3306/invoices", 
        "root", // 用户名
        "123"   // 密码
    );
    Log.d("Amado", "数据库连接成功!")
    conn.close()
} 
// ... catch 语句 ...

步骤 3: 配置 MariaDB/MySQL 允许远程连接

  • 修改配置文件: 找到 MariaDB/MySQL 的配置文件。在 XAMPP 里,通常可以在 XAMPP 控制面板点击 MySQL 对应的 "Config" 按钮找到 my.ini (Windows) 或 my.cnf (Linux/macOS)。

    • 打开这个文件,找到 [mysqld][mariadb] 段落。
    • 查找 bind-address 这一行。如果它被设置为 127.0.0.1,表示只接受来自本机的连接。你需要:
      • 将其改成 0.0.0.0,表示接受来自任何 IP 地址的连接(安全性较低,但局域网开发时常用)。
      • 或者,更安全一点,改成你电脑的局域网 IP,比如 192.168.1.100
    • 如果找不到 bind-address 这一行,或者它被注释掉了(行首有 #),数据库可能默认就允许所有 IP 连接(取决于版本和发行版)。
    • 修改后保存文件 ,并重启 MariaDB/MySQL 服务 (在 XAMPP 控制面板停止再启动)。
  • 授权用户: 你需要确保连接使用的数据库用户(比如 root)被授权可以从你的 Android 设备/模拟器的 IP 地址连接。

    • 打开 phpMyAdmin 或使用 MySQL 命令行客户端。
    • 执行类似以下的 SQL 命令 (将 your_password 换成 root 用户的实际密码):
    -- 允许 root 用户从任何 IP 地址 (%) 连接,使用密码 'your_password'
    -- 警告:'%' 非常不安全,仅用于本地测试!
    GRANT ALL PRIVILEGES ON invoices.* TO 'root'@'%' IDENTIFIED BY 'your_password'; 
    
    -- 更安全一点,如果你知道 Android 设备/模拟器的固定 IP(比如 192.168.1.150)
    -- GRANT ALL PRIVILEGES ON invoices.* TO 'root'@'192.168.1.150' IDENTIFIED BY 'your_password';
    
    -- 修改完权限后,刷新权限使其生效
    FLUSH PRIVILEGES;
    
    • 安全建议:
      • 永远不要在生产环境中使用 '%' 来授权。
      • 为你的应用创建一个专用的数据库用户,只授予它访问所需数据库和表(invoices.*)的最小必要权限(比如 SELECT, INSERT, UPDATE, DELETE),而不是 ALL PRIVILEGES
      • 使用强密码。

步骤 4: 检查电脑防火墙

  • 确保你的电脑防火墙允许来自局域网对 TCP 端口 3306 的入站连接。具体操作方法取决于你的操作系统和防火墙软件。你可能需要添加一条新的入站规则。

步骤 5: 在 AndroidManifest.xml 中添加网络权限

打开你的 App 项目中的 app/src/main/AndroidManifest.xml 文件,在 <manifest> 标签内,确保有以下这行:

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

步骤 6: 将数据库连接操作放到后台线程

严禁在 Android 主线程(UI 线程)执行网络请求。使用 Kotlin Coroutines 是个好方法:

import kotlinx.coroutines.*

// ... 在你的 Activity 或 ViewModel 中 ...

// 创建一个 CoroutineScope,最好关联 ViewModel 生命周期
// private val job = Job()
// private val scope = CoroutineScope(Dispatchers.Main + job) 

fun connectToDatabaseInBackground() {
    // 使用 IO 调度器执行网络/数据库操作
    // GlobalScope 不推荐在实际项目中使用,这里仅作示例简化
    // 最好使用 lifecycleScope (Activity/Fragment) 或 viewModelScope (ViewModel)
    GlobalScope.launch(Dispatchers.IO) { 
        DBConncection.connection() // 调用你的连接方法
        // 如果需要在 UI 上显示结果,切回主线程
        // withContext(Dispatchers.Main) {
        //     // 更新 UI ...
        // }
    }
}

// 不要忘记在适当的时候取消 Coroutine (比如 Activity 的 onDestroy 或 ViewModel 的 onCleared)
// override fun onDestroy() {
//     super.onDestroy()
//     job.cancel() // 取消所有在这个 scope 启动的协程
// }

// 调用:
// connectToDatabaseInBackground() 

完成以上所有步骤后,再次运行你的 App,这次它 应该 能够连接到你本地电脑上的 MariaDB/MySQL 数据库了(前提是网络畅通、配置无误)。

再次强调: 这种直接连接的方式,即使在本地开发网络能跑通,也极其不推荐用于任何实际应用场景。

方案二:构建后端 API (推荐方案)

这才是解决问题的根本之道。虽然听起来多了个“后端”,但长远来看,这会让你受益无穷。

原理:

你的 Android App 不再直接跟数据库说话,而是通过互联网 (HTTP/HTTPS) 请求你开发的后端 API。后端 API 作为中间人,负责验证请求、执行业务逻辑、和数据库交互,然后把结果返回给 App。

技术选型:

你可以用很多技术栈来构建后端 API。既然你的 App 是 Kotlin 写的,可以考虑:

  • Ktor: 一个 Kotlin 原生的异步 Web 框架,轻量、灵活。
  • Spring Boot (with Kotlin): 非常流行和成熟的 Java/Kotlin 框架,生态完善,功能强大。

当然,用 Node.js, Python (Flask/Django), PHP (Laravel), Ruby on Rails 等也完全可以。

后端代码示例 (概念性 Ktor):

这是一个极其简化的 Ktor 后端代码片段,演示如何提供一个获取发票列表的 API:

// Ktor 服务器端代码 (运行在你的电脑或服务器上, 不是 Android App 里!)
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.serialization.kotlinx.json.* // Ktor JSON 支持
import io.ktor.server.plugins.contentnegotiation.*
import kotlinx.serialization.Serializable
import java.sql.DriverManager

@Serializable // 需要 kotlinx.serialization 库
data class Invoice(val id: Int, val description: String, val amount: Double)

fun main() {
    embeddedServer(Netty, port = 8080) { // 后端 API 运行在 8080 端口
        install(ContentNegotiation) {
            json() // 支持 JSON 序列化
        }
        routing {
            get("/api/invoices") { // 定义一个 GET /api/invoices 接口
                try {
                    // --- 这里是后端连接数据库 ---
                    // 注意:实际项目中应该用连接池!
                    val dbHost = "127.0.0.1" // 后端连接自己机器上的数据库用 127.0.0.1
                    val dbPort = 3306
                    val dbName = "invoices"
                    val dbUser = "api_user" // 推荐为 API 创建专用数据库用户
                    val dbPassword = "secure_password" // 从配置读取,不要硬编码

                    val conn = DriverManager.getConnection(
                        "jdbc:mysql://$dbHost:$dbPort/$dbName", dbUser, dbPassword
                    )
                    val statement = conn.prepareStatement("SELECT id, description, amount FROM invoices")
                    val resultSet = statement.executeQuery()

                    val invoices = mutableListOf<Invoice>()
                    while (resultSet.next()) {
                        invoices.add(
                            Invoice(
                                resultSet.getInt("id"),
                                resultSet.getString("description"),
                                resultSet.getDouble("amount")
                            )
                        )
                    }
                    resultSet.close()
                    statement.close()
                    conn.close() // 在实际项目中应归还连接给连接池

                    // 将查询结果序列化为 JSON 并返回给客户端 (App)
                    call.respond(invoices) 

                } catch (e: Exception) {
                    call.respondText("Error fetching invoices: ${e.localizedMessage}", status = io.ktor.http.HttpStatusCode.InternalServerError)
                }
            }
            // 可以添加 POST, PUT, DELETE 等其他 API 接口
        }
    }.start(wait = true)
}
  • 这段代码需要在后端环境运行(比如在你电脑上直接跑,或者部署到服务器)。
  • 它启动一个 Web 服务,监听 8080 端口。
  • 当收到 /api/invoices 的 GET 请求时,它会连接数据库 (这次用 127.0.0.1 是对的,因为 Ktor 服务和数据库在同一台机器),查询数据,然后把结果转成 JSON 返回。
  • 进阶技巧: 实际后端项目会使用 ORM 框架 (如 Exposed for Kotlin, JPA/Hibernate for Spring) 来简化数据库操作,并使用数据库连接池 (如 HikariCP) 来高效管理连接。

Android 端代码 (使用 Retrofit 和 Coroutines):

在你的 Android App 里,你需要使用 HTTP 客户端库来调用上面创建的 API。Retrofit 是一个非常流行的选择。

  1. 添加依赖 (build.gradle.kts - app):
dependencies {
    // Retrofit & Gson/Moshi/Kotlinx Serialization Converter
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    // implementation("com.squareup.retrofit2:converter-gson:2.9.0") // 如果用 Gson
    implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") // 如果用 kotlinx.serialization
    implementation("com.squareup.okhttp3:okhttp:4.10.0") // OkHttp 核心库
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") // Kotlinx Serialization

    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
}
// 确保在 app build.gradle(.kts) 或 project build.gradle(.kts) 中应用了 kotlinx-serialization 插件
// plugins {
//     id("org.jetbrains.kotlin.plugin.serialization") version "..." // 版本需匹配 Kotlin 版本
// }
  1. 定义数据类 (与后端对应):
import kotlinx.serialization.Serializable

@Serializable // 同样需要可序列化
data class Invoice(val id: Int, val description: String, val amount: Double)
  1. 创建 Retrofit Service 接口:
import retrofit2.http.GET
import retrofit2.Response // 使用 Response 获取更详细的 HTTP 响应信息

interface InvoiceApiService {
    // 注意 baseURL 需要在创建 Retrofit 实例时指定
    // API 地址应该是后端服务运行的地址和端口
    // 如果 Ktor 在本机 192.168.1.100 的 8080 端口运行:
    // Base URL: "http://192.168.1.100:8080/"
    @GET("api/invoices") 
    suspend fun getInvoices(): Response<List<Invoice>> // 使用 suspend 支持 Coroutines, 返回 Response 包裹结果
}
  1. 创建 Retrofit 实例:
import retrofit2.Retrofit
import com.jakewharton.retrofit.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType

object RetrofitClient {
    // !!! 把这里的 IP 换成你运行 Ktor 服务的电脑的 IP !!!
    // 如果是模拟器访问本机,可以用特殊 IP 10.0.2.2 指代宿主机
    // private const val BASE_URL = "http://10.0.2.2:8080/" 
    private const val BASE_URL = "http://192.168.1.100:8080/" // 用实际局域网 IP

    private val json = Json { ignoreUnknownKeys = true } // 配置 kotlinx.serialization

    val instance: InvoiceApiService by lazy {
        val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
            // .addConverterFactory(GsonConverterFactory.create()) // 如果用 Gson
            .build()
        retrofit.create(InvoiceApiService::class.java)
    }
}
  1. 在后台线程调用 API:
import kotlinx.coroutines.*
import android.util.Log

// ... 在你的 Activity 或 ViewModel ...
// private val job = Job()
// private val scope = CoroutineScope(Dispatchers.Main + job)

fun fetchInvoicesFromApi() {
    // GlobalScope 仅作示例,实际使用 lifecycleScope 或 viewModelScope
    GlobalScope.launch(Dispatchers.IO) {
        try {
            val response = RetrofitClient.instance.getInvoices() // 调用 API
            if (response.isSuccessful) {
                val invoices = response.body() // 获取 List<Invoice>
                if (invoices != null) {
                    Log.d("Amado", "获取到发票: ${invoices.size} 条")
                    // 在主线程更新 UI
                    withContext(Dispatchers.Main) {
                       // 更新你的 RecyclerView 或其他 UI 组件
                       // adapter.submitList(invoices) 
                    }
                } else {
                     Log.w("Amado", "API 返回了空数据体")
                }
            } else {
                Log.e("Amado", "API 请求失败: ${response.code()} ${response.message()}")
                // 处理错误,比如 response.errorBody()?.string()
            }
        } catch (e: Exception) {
            Log.e("Amado", "网络请求异常", e)
            // 处理网络连接、超时等异常
        }
    }
}

// 不要忘记取消 CoroutineScope
// ...

// 调用:
// fetchInvoicesFromApi()

安全建议:

  • 使用 HTTPS: 对于所有生产环境 API,必须使用 HTTPS 加密通信,防止数据在传输过程中被窃听或篡改。这需要在后端服务器配置 SSL/TLS 证书。Android 默认会阻止明文 HTTP 通信(对于 target SDK 28+),需要在 AndroidManifest.xml 或网络安全配置中显式允许(不推荐),或者最好是使用 HTTPS。
  • API 认证与授权: 不能让任何人都能调用你的 API。需要实现认证机制(比如用户登录获取 Token,之后请求都带上 Token)和授权(检查用户是否有权限执行某个操作)。
  • 输入验证: 后端 API 必须严格验证所有来自客户端的输入,防止 SQL 注入、跨站脚本 (XSS) 等攻击。
  • 服务器安全: 确保运行后端服务的服务器本身是安全的,及时更新系统和依赖库。

对比一下方案一和方案二,你会发现方案二虽然初始步骤多了(要写后端代码),但它解决了所有根本问题,更安全、更健壮、更专业。这才是 Android 开发中与后端数据交互的正确方式。