使用HttpURLConnection
- 获取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版本
- 创建一个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()
- 如果是发起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,并且由于POST
、PUT
、PATCH
、DELETE
这几种请求类型与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表达式中发起网络请求,并通过传统回调的方式监听请求结果。
- 如果请求成功调用
Continuation
的resume()
方法恢复被挂起的协程,并传入服务器响应的数据,该值会成为suspendCoroutine
函数的返回值。 - 如果请求失败,就调用
Continuation
的resumeWithException()
恢复被挂起的协程,并传入具体的异常原因。 - 这样,不管之后我们要发起多少次网络请求,都不需要再重复进行回调实现了,比如说获取百度首页的响应数据:
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
发起网络请求。