欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

使用 HTTP 访问网络(Kotlin)

程序员文章站 2022-03-20 08:33:31
文章目录使用 HttpURLConnection使用 HttpURLConnection在过去,Android 上发送 HTTP 请求一般有两种方式:HttpURLConnection 和 HttpClient。不过由于HttpClient 存在 API 数量过多、扩展困难等缺点,Android 团队越来越不建议我们使用这种方式。终于在 Android 6.0 中,HttpClient 的功能被完全移除了,标志着此功能被正式弃用,因此本小节我们就学习一下现在官方建议使用的 HttpURLConnecti...

注:第一行代码(3版)

使用 HttpURLConnection

在过去,Android 上发送 HTTP 请求一般有两种方式:HttpURLConnection 和 HttpClient。不过由于HttpClient 存在 API 数量过多、扩展困难等缺点,Android 团队越来越不建议我们使用这种方式。终于在 Android 6.0 中,HttpClient 的功能被完全移除了,标志着此功能被正式弃用,因此本小节我们就学习一下现在官方建议使用的 HttpURLConnection 的用法。

首先需要获取 HttpURIConnection 的实例,一般只需要创建一个 URL 对象,并传入目标的网络地址,然后调用一下 openConnection() 方法即可,如下所示:

        val url = URL("https://www.baidu.com")
        val connection = url.openConnection() as HttpURLConnection

在得到了 HttpURLConnection 的实例之后,我们可以设置一下 HTTP 请求所使用的的方法。常用的方法主要
有两个:GET 和 POST。GET 表示希望从服务器哪里获取数据,而 POST 则表示希望提交数据给服务器。写法如下:

        connection.requestMethod = "GET"

接下来就可以进行一些*的定制了,比如设置连接超时,读取超时的毫秒数,以及服务器希望得到的一些消息头等。这部分内容根据自己的实际情况进行编写,示例写法如下:

        connection.connectTimeout = 8000
        connection.readTimeout = 8000

之后再调用 getInputStream() 方法就可以获取到服务器返回的输入流了,剩下的任务就是对输入流进行读取:

        val input = connection.inputStream

最后可以调用 disconnect() 方法将这个 HTTP 连接关闭:

        connection.disconnect()

下面就让我们通过一个具体的例子来真正体验一下 HttpURLConnection 的用法。新建一个 NetworkTest 项目,首先修改 activity_main.xml 中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/sendRequestBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send Request" />

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/responseText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </ScrollView>
    
</LinearLayout>

接着修改 MainActivity 中的代码,如下所示:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        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(response: String) {
        runOnUiThread {
            // 在这里进行 UI 操作,将结果显示到界面上
            responseText.text = response
        }
    }
}

可以看到,我们在 “Send Request” 按钮的点击事件里调用了 sendRequestWithHttpURLConnection() 方法,在这个方法中先是开启了一个子线程,然后在子线程里使用 HttpURLConnection 发出一条 HTTP 请求,请求的目标地址就是百度的首页。接着利用 BufferedReader 对服务器返回的流进行读取,并将结果传入 showResponse() 方法中。而在 showResponse() 方法里,则是调用了一个 runOnUIThread() 方法,然后在这个方法的 Lambda 表达式中进行操作,将返回的数据显示到界面上。

注意在开始之前,别忘了还需要声明网络权限:

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

运行程序,并点击 “Send Request” 按钮,结果如下图所示:
使用 HTTP 访问网络(Kotlin)
现在表示已经成功从服务器取得了数据。

如果想要提交数据给服务器应该怎么办呢?只需要将 HTTP 请求的方法改成 POST,并在获取输入流之前把要提交的数据写出即可。注意,每条数据都要以键值对的形式存在,数据与数据之间用 ”&“ 符号隔开。比如说我们想要向服务器提交用户名和密码,就可以这样写:

                connection.requestMethod = "POST"
                val output = DataOutputStream(connection.outputStream)
                output.writeBytes("userName=admin&password=123456")

使用 OkHttp

当然我们并不是只能使用 HttpURLConnection,完全没有其他任何选择,事实上在开源盛行的今天,有许多出色的网络通信库都可以替代原生的 HttpURLConnection,而其中 OkHttp 无疑是做得最出色的一个。

OkHttp 是由鼎鼎大名的 Square 公司开发的,这个公司在开源事业上贡献良多,除了 OkHttp 之外,还开发了 Retrofit、Picasso 等知名的开源项目。OkHttp 不仅在接口封装上做得简单易用,就连在底层实现上也是自成一派,比起原生的 HttpURLConnection,可以说是有过之而无不及,现在已经成了广大 Android 开发者首选的网络通信库。

在使用 OkHttp 之前,我们需要先在项目中添加 OkHttp 库的依赖。编辑 app/build.gradle 文件,在 dependencies 闭包中添加如下内容:

implementation "com.squareup.okhttp3:okhttp:4.1.0"

添加上述依赖库会自动下载两个库:一个是 OkHttp 库,一个是 Okio 库,后者是前者的通信基础。其中 4.1.0 是版本号,你可以访问 OkHttp 的项目主页,查看当前最新的版本是多少。

下面我们来看一下 OkHttp 的具体用法,首先需要创建一个 OkHttpClient 的实例,如下所示:

        val client = OkHttpClient()

接下来如果想要发起一条 HTTP 请求,就需要创建一个 Request 对象:

        val request = Request.Builder().build()

当然,上述代码只是创建了一个空的 Request 对象,并没有什么实际作用,我们可以在最终的 build() 方法之前连缀很多其他方法来丰富这个 Request 对象。比如可以通过 url() 方法来设置目标的网络地址,如下所示:

        val request = Request.Builder()
            .url("https://www.baidu.com")
            .build()

之后调用 OkHttpClient 的 newCall() 方法来创建一个 Call 对象,并调用它的 execute() 方法来发送请求并获取服务器返回的数据,写法如下:

        val response = client.newCall(request).execute()

Response 对象就是服务器返回的数据了,我们可以使用如下写法来得到返回的具体内容:

        val responseData = response.body?.string()

如果是发起一条 POST 请求,会比 GET 请求稍微复杂一点,我们需要先构建一个 Request Body 对象来存放待提交的参数,如下所示:

        val requestBody = FormBody.Builder()
            .add("username", "admin")
            .add("password", "123456")
            .build()

然后在 Request.Builder 中调用一下 post() 方法,并将 RequestBody 对象传入:

        val request = Request.Builder()
            .url("https://www.baidu.com")
            .post(requestBody)
            .build()

接下来的操作就和 GET 请求一样了,调用 execute() 方法来发送请求并获取服务器返回的数据即可。

以上就是 OkHttp 的基本用法。现在我们把 NetWorkTest 这个项目改用 OkHttp 的方式实现一次。

首先布局没有改动,所以直接修改 MainActivity 中的代码,如下所示:

    private 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: Exception) {
                e.printStackTrace()
            }
        }
    }

这里我们并没有做太多的改动,只是添加了一个 sendRequestWithOkHttp() 方法,并在 “Send Request” 按钮的点击事件里调用这个方法。在这个方法中同样还是先开启一个子线程,然后在子线程里使用 OkHttp 发出一条 HTTP 请求,请求的目标地址还是百度的首页,OkHttp 的用法也正如前面所介绍的意义。最后仍然调用了 showResponse() 方法,将服务器返回的数据显示到界面上。

重新运行程序,点击 “Send Request” 按钮后,你会看到和前面同样的运行效果。

网络请求回调的实现方式

在一个应用程序中,很可能会在许多地方都使用到网络功能,而发送 HTTP 请求的代码基本都是相同的,如果我们每次都去编写一遍发送 HTTP 请求的代码,这显然是非常差劲的做法。

通常情况下我们应该将这些通用的网络操作提取到一个公共的类里,并提供一个通用方法,当想要发起网络请求的时候,只需要简单的调用一下这个方法即可,比如使用如下的写法:

    fun sendHttpRequest(address: String): String {
        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
                    )
                }
            }
            return response.toString()
        } catch (e: Exception) {
            e.printStackTrace()
            return e.message.toString()
        } finally {
            connection?.disconnect()
        }
    }

以后每当需要发起一条 HTTP 请求的时候,就可以这样写:

        val address = "https://www.baidu.com"
        val response = HttpUtil.sendHttpRequest(address)

在获取到服务器响应的数据后,我们就可以对它进行解析和处理了。但是需要注意,网络请求通常输入耗时操作,而 sendHttpRequest() 方法的内部并没有开启线程,这样就有可能导致在调用 sendHttpRequest() 方法的时候主线程被阻塞。

你可能会说,很简单嘛,在 sendHttpRequest() 方法内部开启一个线程,不就解决这个问题了吗?其实并没有你想象中那么容易,因为如果我们在 sendHttpRequest() 方法中开启一个线程来发起 HTTP 请求,服务器响应的数据是无法进行返回的。这是由于所有的耗时逻辑都是在子线程里进行的,sendHttpRequest() 方法会在服务器还没来得及响应的时候就执行结束了,当然就无法返回响应的数据了。

那么在遇到这种情况时应该怎么办呢?其实解决办法并不难,只需要使用编程语言的回调机制就可以了。下面就让我们来学习一下回调机制到底是如何使用的。

首先需要定义一个接口,比如将它命名成 HttpCallbackListener,代码如下所示:

interface HttpCallbackListener {
    fun onFinish(response: String)
    fun onError(e: Exception)
}

可以看到,我们在接口中定义了两个方法:onFinish() 方法表示当服务器成功响应我们请求的时候调用,onError() 表示当进行网络操作出现错误的时候调用。这两个方法都带有参数,onFinish() 方法中的参数代表服务器返回的数据,而 onError() 方法中的参数记录着错误的详细信息。

接着修改 HttpUtil 中的代码,如下所示:

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
                        )
                    }
                }
                // 回调 onFinish() 方法
                listener.onFinish(response.toString())
            } catch (e: Exception) {
                e.printStackTrace()
                // 回调 onError() 方法
                listener.onError(e)
            } finally {
                connection?.disconnect()
            }
        }
    }
}

我们首先给 sendHttpRequest() 方法添加了一个 HttpCallbackListener 参数,并在方法的内部开启了一个子线程,然后在子线程里执行具体的网络操作。注意,子线程中是无法通过 return 语句返回数据的,因此这里我们将服务器响应的数据传入了 HttpCallbackListener 的 onFinish() 方法中,如果出现了异常,就将异常原因传入 onError() 方法中。

现在 sendHttpRequest() 方法接收两个参数,因此我们在调用它的时候还需要将 HttpCallbackListener 的实例传入,如下所示:

       HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
            override fun onFinish(response: String) {
                // 得到服务器返回的具体内容
            }

            override fun onError(e: Exception) {
                // 这里对异常情况进行处理
            }
        })

这样当服务器成功响应的时候,我们就可以在 onFinish() 方法里对响应数据进行处理了。类似地,如果出现了异常,就可以在 onErr() 方法里对异常情况进行处理。如此一来,我们就巧妙的利用回调机制将响应数据成功返回给调用方了。

不过你会发现,上述使用 HttpURLConnection 的写法总体来说还是比较复杂的,那么使用 OkHttp 会变得简单吗?答案是肯定的,而且要简单的多,下面我们来具体看一下。在 HttpUtil 中加入一个 sendOkHttpRequest() 方法,如下所示:

    fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {
        val client = OkHttpClient()
        val request = Request.Builder()
            .url(address)
            .build()
        client.newCall(request).enqueue(callback)
    }

可以看到,sendOkHttpRequest() 方法中有一个 okhttp3.CallBack 参数,这个是 OkHttp 库中自带的回调接口,类似于我们刚才自己编写的 HttpCallbackListener。然后在 client.newCall() 之后没有像之前那样一直调用 execute() 方法,而是调用了一个 enqueue() 方法,并把 okthhp3.Callback 参数传入。其实是 OkHttp 在 enqueue() 方法的内部已经帮我们开好子线程了,然后会在子线程中执行 HTTP 请求,并将最终的请求结果回调到 okHttp3.Callback 当中。

那么我们在调用 sendOkHttpRequest() 方法的时候就可以这样写:

        HttpUtil.sendOkHttpRequest(address, object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                // 这里对异常情况进行处理
            }

            override fun onResponse(call: Call, response: Response) {
                // 得到服务器返回的具体内容
                val responseData = response.body?.string()
            }
        })

由此可以看出,OkHttp 的接口设计得确实非常人性化,它将一些常用的功能进行了很好的封装,使得我们只需要编写少量的代码就能完成较为复杂的网络操作。

另外,需要注意的是,不管是使用 HttpURLConnection 还是 OkHttp,最终的回调接口都还是在子线程中运行的,因此我们不可以在这里任何的 UI 操作,除非借助 runOnUiThread() 方法来进行线程交换。

本文地址:https://blog.csdn.net/csdn1225987336/article/details/107958127

相关标签: Kotlin