跳到主要内容

Kotlin 网络编程

网络编程是现代应用开发的核心技能。无论是移动应用调用后端 API,还是服务端之间的通信,都离不开 HTTP 请求。本章将全面介绍 Kotlin 中的网络编程技术,包括基础 HTTP 操作、现代 HTTP 客户端库、JSON 序列化、协程网络请求等内容。

HTTP 基础概念

在深入代码之前,让我们先理解 HTTP 请求的基本结构:

请求组成部分

  • 请求行:包含方法(GET、POST、PUT、DELETE 等)、URL 和协议版本
  • 请求头:键值对形式,包含 Content-Type、Authorization 等元信息
  • 请求体:POST/PUT 请求通常包含 JSON、表单数据等

响应组成部分

  • 状态行:包含状态码(200、404、500 等)和状态描述
  • 响应头:包含 Content-Type、Content-Length 等元信息
  • 响应体:实际返回的数据,通常是 JSON 或 XML

理解这些基础概念有助于我们更好地使用各种 HTTP 客户端库。

使用 HttpURLConnection

HttpURLConnection 是 Java 标准库提供的 HTTP 客户端,Kotlin 完全兼容。虽然现在有更好的第三方库,但理解它的工作原理仍然重要。

GET 请求

import java.net.HttpURLConnection
import java.net.URL

fun main() {
val url = URL("https://jsonplaceholder.typicode.com/posts/1")
val connection = url.openConnection() as HttpURLConnection

try {
// 设置请求方法和属性
connection.requestMethod = "GET"
connection.setRequestProperty("Accept", "application/json")
connection.connectTimeout = 5000 // 连接超时 5 秒
connection.readTimeout = 5000 // 读取超时 5 秒

val responseCode = connection.responseCode
println("响应码: $responseCode")

if (responseCode == HttpURLConnection.HTTP_OK) {
// 读取成功响应
val response = connection.inputStream.bufferedReader().readText()
println("响应内容: $response")
} else {
// 读取错误响应
val error = connection.errorStream?.bufferedReader()?.readText()
println("错误: $error")
}
} finally {
// 断开连接
connection.disconnect()
}
}

关键配置说明

  • connectTimeout:建立 TCP 连接的最长等待时间
  • readTimeout:等待服务器响应数据的最长时间
  • setRequestProperty:设置请求头,如 Accept、User-Agent 等

POST 请求

import java.net.HttpURLConnection
import java.net.URL

fun postJson(url: String, jsonBody: String): String {
val connection = URL(url).openConnection() as HttpURLConnection

return connection.use {
// 配置 POST 请求
it.requestMethod = "POST"
it.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
it.setRequestProperty("Accept", "application/json")
it.doOutput = true // 允许写入请求体

// 写入请求体
it.outputStream.bufferedWriter().use { writer ->
writer.write(jsonBody)
writer.flush()
}

// 读取响应
val responseCode = it.responseCode
if (responseCode == HttpURLConnection.HTTP_OK ||
responseCode == HttpURLConnection.HTTP_CREATED) {
it.inputStream.bufferedReader().readText()
} else {
throw RuntimeException("HTTP $responseCode: ${it.errorStream?.bufferedReader()?.readText()}")
}
}
}

fun main() {
val response = postJson(
"https://jsonplaceholder.typicode.com/posts",
"""{"title": "Test Post", "body": "This is a test", "userId": 1}"""
)
println(response)
}

使用 use 自动关闭资源

Kotlin 的 use 扩展函数可以自动关闭实现了 Closeable 接口的资源,确保资源不会泄露:

import java.net.HttpURLConnection
import java.net.URL

fun fetchWithUse(url: String): String {
val connection = URL(url).openConnection() as HttpURLConnection

return connection.use { conn ->
conn.requestMethod = "GET"

// InputStream 也使用 use
conn.inputStream.use { input ->
input.bufferedReader().use { reader ->
reader.readText()
}
}
}
}

use 的工作原理:无论代码块是正常结束还是抛出异常,use 都会在最后调用资源的 close() 方法。

封装简单的 HTTP 客户端

import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

sealed class HttpResult<out T> {
data class Success<T>(val data: T) : HttpResult<T>()
data class Error(val code: Int, val message: String) : HttpResult<Nothing>()
data class Exception(val throwable: Throwable) : HttpResult<Nothing>()
}

class SimpleHttpClient(
private val connectTimeout: Int = 10000,
private val readTimeout: Int = 10000
) {
fun get(url: String, headers: Map<String, String> = emptyMap()): HttpResult<String> {
return execute("GET", url, headers, null)
}

fun post(url: String, body: String, headers: Map<String, String> = emptyMap()): HttpResult<String> {
return execute("POST", url, headers + ("Content-Type" to "application/json"), body)
}

fun postForm(url: String, params: Map<String, String>): HttpResult<String> {
val body = params.entries.joinToString("&") { (key, value) ->
"${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}"
}
return execute(
"POST", url,
mapOf("Content-Type" to "application/x-www-form-urlencoded"),
body
)
}

private fun execute(
method: String,
url: String,
headers: Map<String, String>,
body: String?
): HttpResult<String> {
return try {
val connection = URL(url).openConnection() as HttpURLConnection
connection.use { conn ->
conn.requestMethod = method
conn.connectTimeout = connectTimeout
conn.readTimeout = readTimeout
conn.setRequestProperty("Accept", "application/json")

headers.forEach { (key, value) ->
conn.setRequestProperty(key, value)
}

if (body != null) {
conn.doOutput = true
conn.outputStream.bufferedWriter().use { it.write(body) }
}

val responseCode = conn.responseCode
if (responseCode in 200..299) {
HttpResult.Success(conn.inputStream.bufferedReader().readText())
} else {
HttpResult.Error(responseCode, conn.errorStream?.bufferedReader()?.readText() ?: "Unknown error")
}
}
} catch (e: Exception) {
HttpResult.Exception(e)
}
}
}

fun main() {
val client = SimpleHttpClient()

// GET 请求
when (val result = client.get("https://jsonplaceholder.typicode.com/posts/1")) {
is HttpResult.Success -> println("成功: ${result.data.take(100)}...")
is HttpResult.Error -> println("错误: ${result.code} - ${result.message}")
is HttpResult.Exception -> println("异常: ${result.throwable.message}")
}

// POST 请求
when (val result = client.post(
"https://jsonplaceholder.typicode.com/posts",
"""{"title": "Test", "body": "Content", "userId": 1}"""
)) {
is HttpResult.Success -> println("创建成功: ${result.data}")
is HttpResult.Error -> println("创建失败: ${result.code}")
is HttpResult.Exception -> println("网络异常: ${result.throwable.message}")
}
}

设计思路

  • 使用 sealed class 表示可能的结果,方便 when 处理
  • 超时配置可自定义
  • 支持常见的 GET、POST、表单提交

使用 OkHttp

OkHttp 是 Square 公司开发的高性能 HTTP 客户端,是 Android 和服务端开发中最流行的网络库之一。相比 HttpURLConnection,它提供了更好的性能、更简洁的 API 和更强大的功能。

OkHttp 的优势

  • HTTP/2 支持:同一主机的请求共享 Socket 连接
  • 连接池:减少请求延迟
  • 透明 GZIP:自动压缩请求体
  • 响应缓存:避免重复请求
  • 自动重试:网络故障时自动恢复
  • 拦截器:强大的中间件机制

添加依赖

// build.gradle.kts
dependencies {
// 推荐使用 BOM 管理版本
implementation(platform("com.squareup.okhttp3:okhttp-bom:5.3.0"))
implementation("com.squareup.okhttp3:okhttp")
// 日志拦截器(调试用)
implementation("com.squareup.okhttp3:logging-interceptor")
}

GET 请求

import okhttp3.OkHttpClient
import okhttp3.Request

// 创建客户端(通常作为单例使用)
val client = OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build()

fun main() {
// 构建请求
val request = Request.Builder()
.url("https://jsonplaceholder.typicode.com/posts/1")
.header("Accept", "application/json")
.build()

// 执行同步请求
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
println("响应: ${response.body?.string()}")
} else {
println("错误: ${response.code}")
}
}
}

POST 请求

import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.MediaType.Companion.toMediaType

val client = OkHttpClient()

fun postJson() {
val json = """{"title": "Test Post", "body": "Content", "userId": 1}"""
val mediaType = "application/json; charset=utf-8".toMediaType()
val body = json.toRequestBody(mediaType)

val request = Request.Builder()
.url("https://jsonplaceholder.typicode.com/posts")
.post(body)
.build()

client.newCall(request).execute().use { response ->
println("状态码: ${response.code}")
println("响应: ${response.body?.string()}")
}
}

fun postForm() {
val formBody = okhttp3.FormBody.Builder()
.add("username", "testuser")
.add("password", "password123")
.build()

val request = Request.Builder()
.url("https://example.com/login")
.post(formBody)
.build()

client.newCall(request).execute().use { response ->
println(response.body?.string())
}
}

fun main() {
postJson()
postForm()
}

异步请求

异步请求不会阻塞当前线程,适合在 Android 主线程或协程环境中使用:

import okhttp3.OkHttpClient
import okhttp3.Request

val client = OkHttpClient()

fun fetchAsync(url: String, onSuccess: (String) -> Unit, onError: (Exception) -> Unit) {
val request = Request.Builder().url(url).build()

client.newCall(request).enqueue(object : okhttp3.Callback {
override fun onFailure(call: okhttp3.Call, e: java.io.IOException) {
onError(e)
}

override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
if (response.isSuccessful) {
onSuccess(response.body?.string() ?: "")
} else {
onError(java.io.IOException("HTTP ${response.code}"))
}
}
})
}

fun main() {
fetchAsync(
"https://jsonplaceholder.typicode.com/posts",
onSuccess = { println("成功: ${it.take(100)}...") },
onError = { println("失败: ${it.message}") }
)

// 等待异步请求完成
Thread.sleep(2000)
}

拦截器

拦截器是 OkHttp 最强大的功能之一,可以监控、修改、重试请求和响应:

import okhttp3.OkHttpClient
import okhttp3.Interceptor
import okhttp3.logging.HttpLoggingInterceptor

// 日志拦截器
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY // 打印完整请求和响应
}

// 认证拦截器:自动添加 Token
val authInterceptor = Interceptor { chain ->
val token = "your-auth-token" // 从本地存储获取
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
chain.proceed(request)
}

// 重试拦截器:网络错误时自动重试
val retryInterceptor = Interceptor { chain ->
var request = chain.request()
var response: okhttp3.Response? = null
var exception: Exception? = null

repeat(3) { attempt ->
try {
response?.close()
response = chain.proceed(request)
if (response!!.isSuccessful) return@Interceptor response!!
} catch (e: Exception) {
exception = e
}
Thread.sleep(1000L * (attempt + 1)) // 指数退避
}

response ?: throw exception!!
}

val client = OkHttpClient.Builder()
.addInterceptor(authInterceptor) // 应用拦截器
.addInterceptor(loggingInterceptor) // 日志
.addNetworkInterceptor(retryInterceptor) // 网络拦截器
.build()

应用拦截器 vs 网络拦截器

  • 应用拦截器:不关心中间重定向和重试,只调用一次
  • 网络拦截器:可以观察到完整的请求链,包括缓存、重定向等

缓存配置

import okhttp3.OkHttpClient
import okhttp3.Cache
import java.io.File

val cache = Cache(
File("cacheDir", "http_cache"),
10 * 1024 * 1024 // 10 MB
)

val client = OkHttpClient.Builder()
.cache(cache)
.build()

// 强制使用缓存
val cacheRequest = Request.Builder()
.url("https://example.com/data")
.cacheControl(okhttp3.CacheControl.Builder()
.maxStale(7, java.util.concurrent.TimeUnit.DAYS) // 缓存最多使用 7 天
.build())
.build()

// 强制刷新(不使用缓存)
val noCacheRequest = Request.Builder()
.url("https://example.com/data")
.cacheControl(okhttp3.CacheControl.FORCE_NETWORK)
.build()

使用 Retrofit

Retrofit 是 Square 公司在 OkHttp 基础上构建的类型安全 HTTP 客户端。它通过接口定义 API,将 HTTP 请求转换为 Kotlin 函数调用,大幅简化网络代码。

添加依赖

// build.gradle.kts
dependencies {
implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("com.squareup.retrofit2:converter-gson:3.0.0")
// 协程支持
implementation("com.squareup.retrofit2:converter-kotlinx-serialization:3.0.0")
}

定义 API 接口

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*

// 数据类
data class Post(
val id: Int,
val title: String,
val body: String,
val userId: Int
)

data class CreatePostRequest(
val title: String,
val body: String,
val userId: Int
)

// API 接口定义
interface PostApi {
// GET 请求
@GET("posts")
suspend fun getAllPosts(): List<Post>

@GET("posts/{id}")
suspend fun getPostById(@Path("id") id: Int): Post

@GET("posts")
suspend fun getPostsByUser(@Query("userId") userId: Int): List<Post>

// POST 请求
@POST("posts")
suspend fun createPost(@Body post: CreatePostRequest): Post

// PUT 请求(更新)
@PUT("posts/{id}")
suspend fun updatePost(@Path("id") id: Int, @Body post: Post): Post

// DELETE 请求
@DELETE("posts/{id}")
suspend fun deletePost(@Path("id") id: Int): Unit

// 带多个查询参数
@GET("posts")
suspend fun getPosts(
@Query("userId") userId: Int?,
@Query("_sort") sort: String?,
@Query("_order") order: String?
): List<Post>

// 表单提交
@FormUrlEncoded
@POST("login")
suspend fun login(
@Field("username") username: String,
@Field("password") password: String
): LoginResponse

// 带请求头
@GET("user/profile")
@Headers("Accept: application/json")
suspend fun getProfile(@Header("Authorization") token: String): User
}

data class LoginResponse(val token: String, val user: User)
data class User(val id: Int, val name: String, val email: String)

创建 Retrofit 实例

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

// 单例模式
object ApiClient {
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

// 配置 Gson
private val gson = com.google.gson.GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.create()

private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()

val postApi: PostApi = retrofit.create(PostApi::class.java)
}

使用协程调用 API

import kotlinx.coroutines.*

suspend fun main() = coroutineScope {
val api = ApiClient.postApi

try {
// 获取所有文章
val posts = api.getAllPosts()
println("获取到 ${posts.size} 篇文章")

// 获取单篇文章
val post = api.getPostById(1)
println("文章: ${post.title}")

// 创建新文章
val newPost = api.createPost(
CreatePostRequest(
title = "Kotlin 网络编程",
body = "这是一篇关于 Kotlin 网络编程的文章",
userId = 1
)
)
println("创建成功,ID: ${newPost.id}")

// 并发请求
val deferredPosts = async { api.getPostsByUser(1) }
val deferredPost = async { api.getPostById(1) }

val userPosts = deferredPosts.await()
val singlePost = deferredPost.await()

println("用户文章数: ${userPosts.size}")
println("单篇文章: ${singlePost.title}")

} catch (e: Exception) {
println("网络错误: ${e.message}")
}
}

错误处理

import retrofit2.HttpException
import java.io.IOException
import kotlinx.coroutines.*

// 封装网络请求结果
sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val code: Int, val message: String) : NetworkResult<Nothing>()
data class Exception(val e: Throwable) : NetworkResult<Nothing>()
}

// 安全的 API 调用封装
suspend fun <T> safeApiCall(apiCall: suspend () -> T): NetworkResult<T> {
return try {
NetworkResult.Success(apiCall())
} catch (e: HttpException) {
NetworkResult.Error(e.code(), e.message())
} catch (e: IOException) {
NetworkResult.Exception(e)
} catch (e: Exception) {
NetworkResult.Exception(e)
}
}

// 使用示例
suspend fun main() {
val api = ApiClient.postApi

when (val result = safeApiCall { api.getPostById(1) }) {
is NetworkResult.Success -> {
println("成功: ${result.data.title}")
}
is NetworkResult.Error -> {
println("HTTP 错误: ${result.code} - ${result.message}")
}
is NetworkResult.Exception -> {
println("网络异常: ${result.e.message}")
}
}
}

使用 Ktor 客户端

Ktor 是 JetBrains 开发的 Kotlin 原生 HTTP 客户端,支持多平台(JVM、Android、iOS、Web),特别适合 Kotlin Multiplatform 项目。

添加依赖

// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-client-core:3.0.0")
implementation("io.ktor:ktor-client-cio:3.0.0") // JVM 引擎
implementation("io.ktor:ktor-client-content-negotiation:3.0.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.0")
implementation("io.ktor:ktor-client-logging:3.0.0")
}

基本使用

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*

val client = HttpClient(CIO) {
install(io.ktor.client.plugins.logging.Logging) {
level = io.ktor.client.plugins.logging.LogLevel.INFO
}
}

suspend fun main() = coroutineScope {
// GET 请求
val response: HttpResponse = client.get("https://jsonplaceholder.typicode.com/posts/1")
println("状态码: ${response.status}")
println("响应体: ${response.bodyAsText()}")

// 带参数的请求
val postsResponse = client.get("https://jsonplaceholder.typicode.com/posts") {
parameter("userId", 1)
parameter("_limit", 10)
}
println(postsResponse.bodyAsText())

client.close()
}

JSON 序列化

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class Post(val id: Int, val title: String, val body: String, val userId: Int)

val client = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
})
}
}

suspend fun main() = coroutineScope {
// 自动反序列化
val post: Post = client.get("https://jsonplaceholder.typicode.com/posts/1").body()
println("文章: ${post.title}")

// 获取列表
val posts: List<Post> = client.get("https://jsonplaceholder.typicode.com/posts").body()
println("共 ${posts.size} 篇文章")

// POST JSON
val newPost = Post(0, "新文章", "内容", 1)
val createdPost: Post = client.post("https://jsonplaceholder.typicode.com/posts") {
contentType(io.ktor.http.ContentType.Application.Json)
setBody(newPost)
}.body()

println("创建成功: ${createdPost.id}")

client.close()
}

请求配置

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.http.*

suspend fun configuredRequest() {
val client = HttpClient()

// 完整的请求配置
val response = client.post("https://api.example.com/data") {
// 请求头
headers {
append("Authorization", "Bearer token123")
append("Accept", "application/json")
}

// URL 参数
parameter("page", 1)
parameter("limit", 20)

// 请求体
contentType(ContentType.Application.Json)
setBody(mapOf(
"title" to "Ktor 请求",
"content" to "使用 Ktor 发送请求"
))

// 超时配置
timeout {
requestTimeoutMillis = 30000
connectTimeoutMillis = 10000
}
}

println(response.bodyAsText())
client.close()
}

拦截器(插件)

Ktor 使用插件系统实现拦截功能:

import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.observer.*

val client = HttpClient {
// 请求/响应日志
install(io.ktor.client.plugins.logging.Logging) {
logger = object : io.ktor.client.plugins.logging.Logger {
override fun log(message: String) {
println("HTTP: $message")
}
}
level = io.ktor.client.plugins.logging.LogLevel.ALL
}

// 响应观察者
install(ResponseObserver) {
onResponse { response ->
println("响应状态: ${response.status}")
}
}

// 请求超时
install(HttpTimeout) {
requestTimeoutMillis = 30000
connectTimeoutMillis = 10000
socketTimeoutMillis = 60000
}

// 重试
install(HttpRequestRetry) {
maxRetries = 3
retryIf { _, response ->
response.status.value.let { it in 500..599 }
}
retryOnExceptionIf { _, cause ->
cause is java.io.IOException
}
exponentialDelay()
}
}

JSON 序列化

JSON 是现代 Web API 最常用的数据格式。Kotlin 有多种 JSON 序列化方案。

kotlinx.serialization

Kotlin 官方的序列化库,支持多平台,编译时生成序列化代码,性能优秀。

// build.gradle.kts
plugins {
kotlin("jvm") version "2.1.0"
kotlin("plugin.serialization") version "2.1.0"
}

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
}
import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class User(
val id: Int,
val name: String,
val email: String,
@SerialName("created_at")
val createdAt: String? = null, // 可空字段,重命名
val roles: List<String> = emptyList() // 默认值
)

@Serializable
data class ApiResponse<T>(
val code: Int,
val message: String,
val data: T? = null
)

fun main() {
val json = Json {
ignoreUnknownKeys = true // 忽略未知字段
isLenient = true // 宽松模式
encodeDefaults = true // 编码默认值
prettyPrint = true // 美化输出
}

// 序列化:对象 -> JSON
val user = User(1, "张三", "[email protected]", roles = listOf("admin", "user"))
val jsonString = json.encodeToString(user)
println(jsonString)

// 反序列化:JSON -> 对象
val decodedUser = json.decodeFromString<User>("""
{"id": 2, "name": "李四", "email": "[email protected]"}
""")
println(decodedUser)

// 泛型反序列化
val apiResponseJson = """
{"code": 200, "message": "success", "data": {"id": 1, "name": "张三", "email": "[email protected]"}}
"""
val response = json.decodeFromString<ApiResponse<User>>(apiResponseJson)
println("响应: ${response.data?.name}")

// 列表反序列化
val userListJson = """
[{"id": 1, "name": "张三", "email": "[email protected]"}, {"id": 2, "name": "李四", "email": "[email protected]"}]
"""
val users = json.decodeFromString<List<User>>(userListJson)
println("共 ${users.size} 个用户")
}

自定义序列化器

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

// 自定义日期时间序列化器
object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.format(formatter))
}

override fun deserialize(decoder: Decoder): LocalDateTime {
return LocalDateTime.parse(decoder.decodeString(), formatter)
}
}

@Serializable
data class Event(
val id: Int,
val name: String,
@Serializable(with = LocalDateTimeSerializer::class)
val time: LocalDateTime
)

fun main() {
val event = Event(1, "会议", LocalDateTime.now())
val json = Json { prettyPrint = true }
println(json.encodeToString(event))
}

Gson

Google 提供的 JSON 库,使用简单,但性能略低于 kotlinx.serialization:

// build.gradle.kts
dependencies {
implementation("com.google.code.gson:gson:2.10.1")
}
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import java.util.Date

data class User(val id: Int, val name: String, val email: String)

fun main() {
val gson: Gson = GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.setPrettyPrinting()
.create()

// 序列化
val user = User(1, "张三", "[email protected]")
val json = gson.toJson(user)
println(json)

// 反序列化
val decoded = gson.fromJson(json, User::class.java)
println(decoded)

// 列表反序列化
val listType = object : TypeToken<List<User>>() {}.type
val users = gson.fromJson<List<User>>("""
[{"id": 1, "name": "张三", "email": "[email protected]"}]
""", listType)
println(users)
}

协程网络请求

协程让异步网络请求变得简单直观,避免了回调地狱。

基本协程请求

import kotlinx.coroutines.*
import java.net.HttpURLConnection
import java.net.URL

// 挂起函数:在 IO 调度器执行网络请求
suspend fun fetch(url: String): String = withContext(Dispatchers.IO) {
val connection = URL(url).openConnection() as HttpURLConnection
try {
connection.requestMethod = "GET"
connection.inputStream.bufferedReader().readText()
} finally {
connection.disconnect()
}
}

suspend fun main() = coroutineScope {
println("开始请求...")
val result = fetch("https://jsonplaceholder.typicode.com/posts/1")
println("结果: ${result.take(100)}...")
}

并发请求

使用 async 实现并发请求,大幅提升效率:

import kotlinx.coroutines.*

data class Post(val id: Int, val title: String)
data class Comment(val id: Int, val postId: Int, val body: String)

suspend fun fetchPost(id: Int): Post = withContext(Dispatchers.IO) {
// 模拟网络请求
delay(100)
Post(id, "Post $id")
}

suspend fun fetchComments(postId: Int): List<Comment> = withContext(Dispatchers.IO) {
// 模拟网络请求
delay(150)
listOf(Comment(1, postId, "Comment 1"), Comment(2, postId, "Comment 2"))
}

suspend fun main() = coroutineScope {
// 串行请求:总耗时约 250ms
val startTime1 = System.currentTimeMillis()
val post1 = fetchPost(1)
val comments1 = fetchComments(1)
println("串行耗时: ${System.currentTimeMillis() - startTime1}ms")

// 并发请求:总耗时约 150ms(取决于最慢的请求)
val startTime2 = System.currentTimeMillis()
val deferredPost = async { fetchPost(2) }
val deferredComments = async { fetchComments(2) }

val post2 = deferredPost.await()
val comments2 = deferredComments.await()
println("并发耗时: ${System.currentTimeMillis() - startTime2}ms")

// 批量并发请求
val posts = coroutineScope {
(1..10).map { id ->
async { fetchPost(id) }
}.awaitAll()
}
println("获取 ${posts.size} 篇文章")
}

超时与取消

import kotlinx.coroutines.*
import kotlin.coroutines.cancellation.CancellationException

suspend fun fetchWithTimeout(url: String): String = withTimeoutOrNull(3000) {
// 网络请求
delay(2000) // 模拟耗时操作
"Response from $url"
} ?: "请求超时"

suspend fun main() = coroutineScope {
// 超时处理
val result = withTimeoutOrNull(1000) {
delay(2000)
"完成"
}
println(result) // null(超时)

// 可取消的请求
val job = launch {
repeat(10) { i ->
ensureActive() // 检查是否被取消
println("处理 $i")
delay(500)
}
}

delay(1200)
job.cancel() // 取消协程
println("已取消")
}

错误重试

import kotlinx.coroutines.*
import kotlin.random.Random

// 重试函数
suspend fun <T> retry(
times: Int = 3,
delay: Long = 1000,
block: suspend () -> T
): T {
var lastException: Exception? = null
repeat(times) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
if (attempt < times - 1) {
delay(delay * (attempt + 1)) // 指数退避
}
}
}
throw lastException ?: RuntimeException("Unknown error")
}

// 模拟可能失败的请求
var attemptCount = 0
suspend fun unreliableApi(): String {
attemptCount++
if (Random.nextBoolean()) {
throw IOException("网络错误(第 $attemptCount 次尝试)")
}
return "成功!(第 $attemptCount 次尝试)"
}

suspend fun main() {
try {
val result = retry(times = 5, delay = 500) {
unreliableApi()
}
println(result)
} catch (e: Exception) {
println("最终失败: ${e.message}")
}
}

文件上传下载

上传文件

使用 OkHttp 上传文件:

import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import java.io.File

val client = OkHttpClient()

// 上传单个文件
fun uploadFile(url: String, file: File): String {
val mediaType = "application/octet-stream".toMediaType()
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("description", "文件描述")
.addFormDataPart("file", file.name, file.asRequestBody(mediaType))
.build()

val request = Request.Builder()
.url(url)
.post(requestBody)
.build()

return client.newCall(request).execute().use { response ->
response.body?.string() ?: "No response"
}
}

// 上传多个文件
fun uploadMultipleFiles(url: String, files: List<File>): String {
val mediaType = "image/jpeg".toMediaType()
val builder = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("userId", "123")

files.forEachIndexed { index, file ->
builder.addFormDataPart("files", file.name, file.asRequestBody(mediaType))
}

val request = Request.Builder()
.url(url)
.post(builder.build())
.build()

return client.newCall(request).execute().use { it.body?.string() ?: "" }
}

下载文件

import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream

val client = OkHttpClient()

fun downloadFile(url: String, destFile: File): Boolean {
val request = Request.Builder().url(url).build()

return client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return false

response.body?.byteStream()?.use { input ->
FileOutputStream(destFile).use { output ->
input.copyTo(output)
}
}
true
}
}

// 带进度回调的下载
fun downloadWithProgress(
url: String,
destFile: File,
onProgress: (downloaded: Long, total: Long) -> Unit
): Boolean {
val request = Request.Builder().url(url).build()

return client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return false

val total = response.body?.contentLength() ?: -1L
var downloaded = 0L

response.body?.byteStream()?.use { input ->
FileOutputStream(destFile).use { output ->
val buffer = ByteArray(8192)
var read: Int
while (input.read(buffer).also { read = it } != -1) {
output.write(buffer, 0, read)
downloaded += read
onProgress(downloaded, total)
}
}
}
true
}
}

fun main() {
val url = "https://example.com/largefile.zip"
val file = File("download.zip")

downloadWithProgress(url, file) { downloaded, total ->
val percent = if (total > 0) (downloaded * 100 / total) else 0
print("\r下载进度: $percent% ($downloaded / $total bytes)")
}
println("\n下载完成!")
}

认证机制

Basic 认证

import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request

val client = OkHttpClient()

fun basicAuth() {
val credential = Credentials.basic("username", "password")

val request = Request.Builder()
.url("https://api.example.com/protected")
.header("Authorization", credential)
.build()

client.newCall(request).execute().use { response ->
println(response.body?.string())
}
}

Bearer Token 认证

import okhttp3.OkHttpClient
import okhttp3.Request

class AuthManager {
private var token: String? = null

fun setToken(newToken: String) {
token = newToken
}

fun getToken(): String? = token
}

val authManager = AuthManager()

val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
authManager.getToken()?.let {
request.addHeader("Authorization", "Bearer $it")
}
chain.proceed(request.build())
}
.authenticator(okhttp3.Authenticator { _, response ->
// Token 过期时自动刷新
if (response.code == 401) {
val newToken = refreshToken() // 刷新 token
authManager.setToken(newToken)
response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
} else {
null
}
})
.build()

fun refreshToken(): String {
// 刷新 token 的逻辑
return "new-token"
}

OAuth 2.0

import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class TokenResponse(
val access_token: String,
val token_type: String,
val expires_in: Int,
val refresh_token: String? = null
)

class OAuthClient(
private val clientId: String,
private val clientSecret: String,
private val tokenUrl: String
) {
private val client = OkHttpClient()
private val json = Json { ignoreUnknownKeys = true }

private var accessToken: String? = null
private var refreshToken: String? = null
private var expiresAt: Long = 0

// 获取 token
suspend fun getToken(authCode: String): TokenResponse {
val formBody = FormBody.Builder()
.add("grant_type", "authorization_code")
.add("code", authCode)
.add("client_id", clientId)
.add("client_secret", clientSecret)
.build()

val request = Request.Builder()
.url(tokenUrl)
.post(formBody)
.build()

return client.newCall(request).execute().use { response ->
val body = response.body?.string() ?: throw RuntimeException("Empty response")
json.decodeFromString(body)
}.also { token ->
accessToken = token.access_token
refreshToken = token.refresh_token
expiresAt = System.currentTimeMillis() + token.expires_in * 1000
}
}

// 刷新 token
suspend fun refreshAccessToken(): TokenResponse {
val formBody = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("refresh_token", refreshToken ?: "")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.build()

val request = Request.Builder()
.url(tokenUrl)
.post(formBody)
.build()

return client.newCall(request).execute().use { response ->
json.decodeFromString(response.body?.string() ?: "")
}
}

// 检查是否需要刷新
fun needsRefresh(): Boolean {
return System.currentTimeMillis() > expiresAt - 60000 // 提前 1 分钟刷新
}
}

最佳实践

1. 使用单例 HTTP 客户端

// 推荐:单例模式
object HttpClient {
val instance: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build()
}

// 避免:每次请求都创建新客户端
// val client = OkHttpClient() // 不推荐!

原因:OkHttp 内部维护连接池和线程池,频繁创建会浪费资源。

2. 正确处理生命周期

import kotlinx.coroutines.*

class UserRepository(private val scope: CoroutineScope) {
private val client = OkHttpClient()

fun fetchUser(id: Int, onResult: (Result<User>) -> Unit) {
scope.launch {
try {
val user = withContext(Dispatchers.IO) {
// 网络请求
fetchUserFromApi(id)
}
onResult(Result.success(user))
} catch (e: Exception) {
onResult(Result.failure(e))
}
}
}

fun cancel() {
// 取消所有请求
scope.cancel()
}
}

3. 统一错误处理

sealed class AppError {
data class Network(val message: String) : AppError()
data class Http(val code: Int, val message: String) : AppError()
data class Parse(val message: String) : AppError()
data class Unknown(val throwable: Throwable) : AppError()
}

fun handleError(error: AppError): String {
return when (error) {
is AppError.Network -> "网络连接失败,请检查网络设置"
is AppError.Http -> "服务器错误 (${error.code})"
is AppError.Parse -> "数据解析失败"
is AppError.Unknown -> "未知错误: ${error.throwable.message}"
}
}

4. 合理使用缓存

import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.OkHttpClient
import java.io.File
import java.util.concurrent.TimeUnit

fun createCachedClient(cacheDir: File): OkHttpClient {
val cache = Cache(File(cacheDir, "http_cache"), 50 * 1024 * 1024) // 50 MB

return OkHttpClient.Builder()
.cache(cache)
.build()
}

// 强制使用缓存
fun forceCacheRequest() {
val cacheControl = CacheControl.Builder()
.onlyIfCached()
.maxStale(7, TimeUnit.DAYS)
.build()

val request = Request.Builder()
.url("https://api.example.com/data")
.cacheControl(cacheControl)
.build()
}

5. 日志与调试

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor

fun createDebugClient(): OkHttpClient {
val logging = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}

return OkHttpClient.Builder()
.addInterceptor(logging)
.build()
}

小结

本章我们全面学习了 Kotlin 网络编程:

  1. HttpURLConnection:Java 标准库,理解底层原理
  2. OkHttp:高性能 HTTP 客户端,支持拦截器、缓存
  3. Retrofit:类型安全的 HTTP 客户端,与协程完美集成
  4. Ktor:Kotlin 原生客户端,支持多平台
  5. JSON 序列化:kotlinx.serialization 和 Gson
  6. 协程网络请求:并发请求、超时处理、错误重试
  7. 文件操作:上传下载、进度监控
  8. 认证机制:Basic、Bearer Token、OAuth 2.0
  9. 最佳实践:单例客户端、错误处理、缓存策略

选择合适的网络库取决于项目需求:

  • 简单请求:OkHttp
  • REST API:Retrofit
  • 多平台项目:Ktor

练习

  1. 使用 OkHttp 实现一个支持 GET/POST 的 HTTP 客户端封装
  2. 使用 Retrofit 定义 GitHub API 接口并获取用户信息
  3. 实现带重试机制的网络请求函数
  4. 使用协程并发请求多个 API 并合并结果
  5. 实现文件下载功能,支持进度显示和断点续传

参考资料