Android网络技术

使用HttpURLConnection

  1. 获取HttpURLConnection的实例,一般只需创建一个URL对象,并传入目标网络地址,然后调用openConnection()
val url = URL("https://www.baidu.com")
val connection = url.openConnection() as HttpURLConnection

2.得到HttpURLConnection实例之后,我们可以设置一下HTTP请求所使用的的方法。常用的有GET和POST
connection.requestMethod = "GET"
3.接下来可以进行一些自由的定制了,比如设置连接超时、读取超时的毫秒数,以及服务器希望得到的一些消息头等

connection.connectTimeout = 8000
connection.readTimeout = 8000

4.调用getInputStream()方法就可以获取到服务器返回的输入流了,接下来就是对输入流的读取
val input = connection.inputStream
5.最后关闭HTTP连接
connection.disconnect()

class MainActivity : AppCompatActivity() {

    private lateinit var sendRequestBtn: Button
    private lateinit var responseText: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initView()
        setListener()
    }

    private fun setListener() {
        sendRequestBtn.setOnClickListener {
            //开启子线程发起网络请求
            sendRequestWithHttpURLConnection()
        }
    }

    private fun sendRequestWithHttpURLConnection() {
        thread {
            var connection: HttpURLConnection ?= null
            try {
                val response = StringBuilder()
                val url = URL("https://www.baidu.com")
                connection = url.openConnection() as HttpURLConnection
                connection.connectTimeout = 8000
                connection.readTimeout = 8000
                val input = connection.inputStream
                //对获取到的输入流进行读取
                val reader = BufferedReader(InputStreamReader(input))
                reader.use {
                    reader.forEachLine {
                        response.append(it)
                    }
                }
                showResponse(response.toString())
            }catch (e: Exception){
                e.printStackTrace()
            }finally {
                connection?.disconnect()
            }
        }
    }

    private fun showResponse(toString: String) {
        runOnUiThread{
            responseText.text = toString
        }
    }

    private fun initView() {
        sendRequestBtn = findViewById(R.id.sendRequestBtn)
        responseText = findViewById(R.id.responseText)
    }
}
  • 如果想提交数据给服务端,只需把HTTP请求的方法改成POST,并在获取输入流之前把要提交的数据写出即可
connection.requestMethod = "POST"
val output = DataOutputStream(connection.outputStream)
output.writeBytes("username=admin&password=123")

使用OkHttp

  • OkHttp的项目主页地址是:https://github.com/square/okhttp
  • 上面的网址可以查看当前最新的OkHttp版本
  1. 创建一个OkHttpClient的实例:
    val client = OkHttpClient()
    2.接下来想要发起HTTP请求,就需要创建一个Request对象,并在最终的build()方法之前连缀其它方法来丰富这个Request对象:
val request = Request.Builder()
    .url("https://i.flyme.cn/uc/oauth/membericon/upload?access_token="+ UserInfo.getInstance().getAccess_token())
    .build()

3.之后调用OkHttpClient的newCall()方法来创建一个Call对象,并调用它的execute()方法来发送请求并获取服务器返回的数据:
val response = client.newCall(request).execute()
Response对象就是服务器返回的数据了,可以通过如下写法得到返回的具体内容:
val responseData = response.body?.string()

  1. 如果是发起POST请求,需要先构建一个Request Body对象来存放待提交的参数,并传入Request对象,如下:
val requestBody = FormBody.Builder()
      .add("username", "admin)
      .add("password", "123")
      .build()
val request = Request.Builder()
    .url("https://i.flyme.cn/uc/oauth/membericon/upload?access_token="+ UserInfo.getInstance().getAccess_token())
    .post(requestBody)
    .build()
fun sendRequestWithOkHttp() {
    thread{
        try {
            val client = OkHttpClient()
            val request = Request.Builder()
                    .url("https://www.baidu.com")
                    .build()
            val response = client.newCall(request).execute()
            val responseData = response.body?.string()
            if (responseData != null){
                showResponse(responseData)
            }
        }catch (e: java.lang.Exception){
            e.printStackTrace()
        }
    }
}
  • 在网络上传输数据时最常用的格式有两种:XML和JSON
  • 解析JSON数据
    1.使用JSONObject
    例如JSON数据如下:
[
     {
         "id" : "5", 
         "version" : "5.5", 
         "name" : "AAA"
     },
     {
         "id" : "6", 
         "version" : "6.5",
         "name" : "BBB"
     },
     {
         "id" : "7", 
         "version" : "7.5",
         "name" : "CCC"
     }
]

解析如下(假设已成功拿到服务器返回的数据)

    try {
        val jsonArray = JSONArray(jsonData)
        for (i in 0 until jsonArray.length()){
            val jsonObject = jsonArray.getJSONObject(i)
            val id = jsonObject.getString("id")
            val name = jsonObject.getString("name")
            val version = jsonObject.getString("version")
        }
    }catch (e: Exception){
        e.printStackTrace()
    }

2.使用GSON
解析JSON对象:

val gson= Gson()
val people = gson.fromJson(jsonData, Person::class.java)

如果要解析的是JSON数组,例如:
[{"name":"Tom","age","2"}, {"name":"Y","age":"2"}]
这个时候需要借助TypeToken将期望解析成的数据类型传入fromJson()方法中:

val typeOf = object : TypeToken<List<Person>>(){}.type
val peopleList = gson.fromJson<List<Person>>(jsonData, typeOf)
  • 网络请求回调的实现
interface HttpCallbackListener{
    fun onFinish(response: String)
    fun onError(e: Exception)
}

object HttpUtil {
    
    fun sendHttpRequest(address: String, listener: HttpCallbackListener){
        thread {
            var connection: HttpURLConnection? = null
            try {
                val response = StringBuilder()
                val url = URL(address)
                connection = url.openConnection() as HttpURLConnection
                connection.connectTimeout = 8000
                connection.readTimeout = 8000
                val input = connection.inputStream
                val reader = BufferedReader(InputStreamReader(input))
                reader.use {
                    reader.forEachLine {
                        response.append(it)
                    }
                }
                listener.onFinish(response.toString())
            }catch (e: Exception){
                e.printStackTrace()
                listener.onError(e)
            }finally {
                connection ?.disconnect()
            }
        }
    }
}

使用OkHttp:

object HttpUtil {
    ...
    fun sendOkHttpRequest(address: String, callback: okhttp3.Callback){
        val client = OkHttpClient()
        val request = Request.Builder()
            .url(address)
            .build()
        client.newCall(request).enqueue(callback)
    }
}
    HttpUtil.sendOkHttpRequest("https//www.baidu.com", object : Callback{
        override fun onFailure(call: Call, e: IOException) {
            TODO("Not yet implemented")
        }

        override fun onResponse(call: Call, response: Response) {
            TODO("Not yet implemented")
        }
    })
  • Retrofit
  • 依赖
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'

比如服务器提供了如下接口地址:
DELETE http://example.com/data/<id>
这种接口通常意味着要根据id删除一条指定的数据,而我们在Retrofit当中想要发出这种请求就可以这样写:

interface ExampleService{
    @DELETE("data/{id}")
    fun deleteData(@Path("id") id: String) : Call<ResponseBody>
}

这里使用了@DELETE注解来发出DELETE类型的请求,并使用了@Path注解来动态指定id,并且由于POSTPUTPATCHDELETE这几种请求类型与GET请求不同,它们更多是用于操作服务器上的数据,而不是获取服务器上的数据,所以通常它们对于服务器响应的数据并不关心,这个时候就可以用ResponseBody,表示Retrofit能够接收任意类型的响应数据,并且不会对响应数据进行解析

  • 那么如果我们需要向服务器提交数据该怎么写呢?比如如下接口地址:
    POST http://example.com/data/create {"id":1, "content":"Hello World"}
    使用POST请求来提交数据,需要将数据放到HTTP请求的body部分,这个功能在Retrofit中可以借助@Body注解来完成:
class Data(val id: String, val content: String)

interface ExampleService{
    @DELETE("data/create")
    fun deleteData(@Body data: Data) : Call<ResponseBody>
}

这样当Retrofit发出POST请求时,就会自动将Data对象中的数据转换成JSON格式的文本,并放到HTTP请求的body部分,服务器在收到请求之后只需要从body中将这部分数据解析出来即可。

  • 有些服务器接口还可能会要求我们在HTTP请求的header中指定参数:
    例如:
    GET http://example.com/get_data.json
    User-Agent: okhttp
    Cache-Control: max-age=0
    这些header参数其实就是一个个的键值对,我们可以在Retrofit中直接使用@HeaderMap注解来对它们进行声明:
interface ExampleService{
    @GET("get_data.json")
    fun getData(@HeaderMap Map<String, String> headers) : Call<Data>
}

headers.put("User-Agent", "okhttp")
headers.put("Cache-Control", "max-age=0")

使用协程简化回调

协程简化回调

suspendCoroutine
必须在协程作用域或挂起函数中才能调用,它接收一个Lambda表达式,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码,Lambda表达式的参数列表上会传入一个Continuation参数,调用它的resume()方法或resumeWithException()可以让协程恢复运行

    suspend fun request(address: String): String{
        return suspendCoroutine {
            HttpUtil.sendOkHttpRequest(address, object : Callback{
                override fun onFailure(call: Call, e: IOException) {
                    it.resumeWithException(e)
                }

                override fun onResponse(call: Call, response: Response) {
                    it.resume(response.body.toString())
                }
            })
        }
    }
  • 可以看到,request()函数是一个挂起函数,并且接收一个address参数。
  • request()函数的内部,调用suspendCoroutine函数,这样当前协程就会被立刻挂起,而Lambda表达式中的代码则会在普通线程中执行。
  • 在Lambda表达式中发起网络请求,并通过传统回调的方式监听请求结果。
  • 如果请求成功调用Continuationresume()方法恢复被挂起的协程,并传入服务器响应的数据,该值会成为suspendCoroutine函数的返回值。
  • 如果请求失败,就调用 ContinuationresumeWithException()恢复被挂起的协程,并传入具体的异常原因。
  • 这样,不管之后我们要发起多少次网络请求,都不需要再重复进行回调实现了,比如说获取百度首页的响应数据:
suspend fun getBaiduResponse(){
    try {
        val response = request("https://www.baidu.com/")
        //对服务器响应的数据进行处理
    }catch (e: Exception){
        //对异常情况进行处理
    }
}
  • 由于getBaiduResponse()是一个挂起函数,因此当它调用了request()函数时,当前协程就会被立刻挂起,然后一直等待网络请求成功或失败后,当前协程才能恢复运行,这样即使不使用回调的写法,我们也能获取异步网络请求的响应数据。
  • suspendCoroutine函数也能简化Retrofit来发起的网络请求:
suspend fun <T> Call<T>.await(): T{
    return suspendCoroutine {
       enqueue(object : retrofit2.Callback<T>{
           override fun onFailure(call: Call<T>, t: Throwable) {
               it.resumeWithException(t)
           }

           override fun onResponse(call: Call<T>, response: retrofit2.Response<T>) {
               val body = response.body()
               if (body != null) 
                   it.resume(body)
               else
                   it.resumeWithException(RuntimeException("response is null"))
           }

       })
    }
}

首先await()函数仍然是一个挂起函数,然后给它声明了一个泛型T,并将await()函数定义成了Call<T>的扩展函数,这样所有返回值是Call类型的Retrofit网络请求接口就都可以直接调用await()函数了。
接着,await()函数中使用suspendCoroutine函数来挂起当前协程,并且由于扩展函数的原因,我们现在拥有了Call对象的上下文,那么这里直接调用enqueue()方法让Retrofit发起网络请求。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容