在开源盛行的今天,有许多出色的网络通信库可以替代原生的HttpURLConnection,而其中OkHttp无疑是做得最出色的一个。

在使用之前,需要在app/build.gradle文件中的dependencies闭包中添加如下内容:

dependencies {

...

implementation 'com.squareup.okhttp3:okhttp:4.9.3'

}

GET请求部分:

添加完成后,首先需要创建一个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()

REQUEST请求部分:

若发起的是一条REQUEST请求,则会比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方法来发送请求并获取服务器返回的数据即可。

解析XML格式数据

先下载一个apache,具体操作请搜索。

安装并配置成功后,在浏览器进入127.0.0.1的网址,若成功则说明服务器已启动成功。

接下来在Apache\htdocs目录下,新建一个get_data.xml的文件,内容如下:

1

Google Maps

1.0

2

Chrome

2.1

3

Google Play

2.3

此时访问127.0.0.1/get_data.xml网址,则会出现上面的内容。接下来让我们获取并解析这段xml。

Pull解析方式

解析xml的方式较为常用的有:Pull和SAX解析。

private fun sendRequestWithOkHttp(){

thread {

try {

val client = OkHttpClient()//获取实例

//编写请求,并在url中添加访问地址

val request = Request.Builder()//

.url("http://10.0.2.2/get_data.xml")//10.0.2.2在虚拟机中代表本机

.build()

//向服务器提交请求,使用newCall获取服务器返回的数据

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

//解析返回的数据response

val responseData = response.body?.string()

if (responseData != null) {

//传入方法,解析xml

parseXMLWithPull(responseData)

}

} catch (e: Exception) {

e.printStackTrace()

}

}

}

private fun parseXMLWithPull(xmlData: String) {

try {

//创建一个XmlPullParserFactory实例

val factory = XmlPullParserFactory.newInstance()

//借助实例获得XmlPullParser对象

val xmlPullParser = factory.newPullParser()

//调用xmlPullParser的setInput方法,将返回的xml数据设置进去,进而解析

xmlPullParser.setInput(StringReader(xmlData))

//通过getEventType获取当前的解析事件

var eventType = xmlPullParser.eventType

var id = ""

var name = ""

var version = ""

//如果当前的解析事件不等于END_DOCUMENT,说明解析未完成

while (eventType != XmlPullParser.END_DOCUMENT) {

//通过getName方法获取当前节点的名字

val nodeName = xmlPullParser.name

when (eventType) {

//START_TAG:开始解析获取的节点

XmlPullParser.START_TAG -> {

when (nodeName) {

//如果发现节点名等于id、name或version

//就调用nextText方法来获取节点内的具体内容

"id" -> id = xmlPullParser.nextText()

"name" -> name = xmlPullParser.nextText()

"version" -> version = xmlPullParser.nextText()

}

}

//END_TAG:完成解析某个节点

XmlPullParser.END_TAG -> {

if ("app" == nodeName) {

Log.d("MainActivity", "id is $id")

Log.d("MainActivity", "name is $name")

Log.d("MainActivity", "version is $version")

}

}

//调用next方法,获取下一个解析事件

eventType = xmlPullParser.next()

}

} catch (e: Exception) {

e.printStackTrace()

}

}

}

从Andriod9.0开始,应用程序只允许使用https类型的网络请求,http因为有安全隐患默认不再被支持,而apache的就是http。

为了让程序使用http,需要进行如下配置:右击res->New->Directory新建一个xml目录,接着右击xml->New->File,创建一个network_config.xml文件,写入以下内容:

此配置文件的意思是,允许我们以明文的方式在网络上传输数据,而http使用的就是明文传输方式。接着修改AndroidManifest.xml如下:

package="com.example.networktest">

...

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:roundIcon="@mipmap/ic_launcher_round"

android:supportsRtl="true"

android:theme="@style/AppTheme"

android:networkSecurityConfig="@xml/network_config">

...

SAX解析方式

使用SAX解析,通常会新建一个类继承自DefaultHandler,并重写父类的5个方法。

新建一个ContentHandler类继承DefaultHandler:

class ContentHandler : DefaultHandler() {

private var nodeName = ""

private lateinit var id: StringBulider

private lateinit var name: StringBulider

private lateinit var version: StringBulider

//在开始xml解析的时候调用,在此处进行初始化

override fun startDocument() {

id = StringBuilder()

name = StringBuilder()

version = StringBuilder()

}

//在开始解析某个节点时调用

override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {

//localName记录了当前节点名

nodeName = localName

Log.d("ContentHandler", "uri is $uri")

Log.d("ContentHandler", "localName is $localName")

Log.d("ContentHandler", "qName is $qName")

Log.d("ContentHandler", "attributes is $attributes")

}

//在获取节点中内容的时候调用

//需要注意的是,在读取节点中的内容时,此方法可能会被调用多次

override fun characters(ch: CharArray, start: Int, length: Int) {

//根据当前节点名判断将内容添加到哪一个StringBuilder对象中

when (nodeName) {

"id" -> id.append(ch, start, length)

"name" -> name.append(ch, start, length)

"version" -> version.append(ch, start, length)

}

}

//在完成解析某个节点的时候调用

override fun endElement(uri: String, localName: String, qName: String) {

if ("app" == localName) {

//此处的id、name和version中都可能包括回车或换行符

//因此需要使用trim方法来去除

Log.d("ContentHandler", "id is ${id.toString().trim()}")

Log.d("ContentHandler", "name is ${name.toString().trim()}")

Log.d("ContentHandler", "version is ${version.toString().trim()}")

//最后要将StringBuilder清空,不然会影响下一次内容的读取

id.setLength(0)

name.setLength(0)

version.setLength(0)

}

}

override fun endDocument() { }

}

在MainActivity中调用:

class MainActivity : AppCompatActivity() {

...

private fun sendRequestWithOkHttp() {

thread {

try {

val client = OkHttpClient()

val request = Request.Builder()

// 指定访问的服务器地址是计算机本机

.url("http://10.0.2.2/get_data.xml")

.build()

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

val responseData = response.body?.string()

if (responseData != null) {

parseXMLWithSAX(responseData)

}

} catch (e: Exception) {

e.printStackTrace()

}

}

}

...

private fun parseXMLWithSAX(xmlData: String) {

try {

//创建一个SAXParserFactory对象,以获取XMLReader对象

val factory = SAXParserFactory.newInstance()

val xmlReader = factory.newSAXParser().XMLReader

//将ContentHandler的实例设置到XMLReader中

val handler = ContentHandler()

xmlReader.contentHandler = handler

// 开始执行解析

xmlReader.parse(InputSource(StringReader(xmlData)))

} catch (e: Exception) {

e.printStackTrace()

}

}

}

解析JSON格式数据

开始前,在Apache\htdocs目录中新建一个get_data.json,内容如下:

[{"id":"5","version":"5.5","name":"Clash of Clans"},

{"id":"6","version":"7.0","name":"Boom Beach"},

{"id":"7","version":"3.5","name":"Clash Royale"}]

此时访问http://127.0.0.1/get_data.jso,会出现上述内容。

类似的,解析json也有很多种方法,此处我们学习JSONObject和GSON。

JSONObject

class MainActicity : AppCompatActivity() {

...

private fun sendRequestWithOkHttp() {

thread {

try {

val client = OkHttpClient()

val request = Request.Builder()

//10.0.2.2对于模拟器是本机的IP地址

.url("http://10.0.2.2/get_data.json")

.build()

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

//newCall: 发送请求并获取服务器返回的数据

val responseData = response.body?.string()

if (responseData != null) {

parseJSONWithJSONObject(responseData)

}

} catch (e: Exception) {

e.printStackTrace()

}

}

}

...

private fun parseJSONWithJSONObject(jsonData: String) {

try {

//先将服务器返回的数据传入一个JSONArray对象中

val jsonArray = JSONArray(jsonData)

//循环遍历这个JSONArray对象,从中每取出一个元素都是一个JSONObject对象

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")

Log.e("MainActivity", "id is $id")

Log.e("MainActivity", "name is $name")

Log.e("MainActivity", "version is $version")

}

} catch (e: Exception) {

e.printStackTrace()

}

}

}

GSON

GSON并没有被添加到Android的官方API中,需要添加依赖。编辑app/build.gradle文件,在dependencies闭包中添加:

dependencies {

...

implementation 'com.google.code.gson:gson:2.8.5'

}

GSON库的强大之处在于,它可以将一段JSON格式的字符串自动映射成一个对象,不需要我们再手动编写代码进行解析。

比如一段JSON格式的数据如下所示:

{"name":"Tom","age":20}

那我们可以定义一个Person类,并可以如name和age这两个字段,然后只需要调用如下代码就可以将JSON数据自动解析成一个Person对象了:

val gson = Gson()

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

若需要解析的是一个JSON数组,如下:

[{"name":"Tom","age":20}, {"name":"Jack","age":25}, {"name":"Lily","age":22}]

 此时,我们需要借助TypeToken将期望解析成的数据,传入fromJson方法中:

val typeOf = object : TypeToken>() {}.type

val people = gson.fromJson>(jsonData, typeOf)

进入实战。首先新增一个App类,并加入id、name和version3个字段:

class App(val id: String, val name: String, val version: String)

然后修改MainActivity:

class MainActivity : AppCompatActivity() {

private fun sendRequestWithOkHttp() {

thread {

try {

val client = OkHttpClient()

val request = Request.Builder()

//10.0.2.2对于模拟器是本机的IP地址

.url("http://10.0.2.2/get_data.json")

.build()

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

//newCall: 发送请求并获取服务器返回的数据

val responseData = response.body?.string()

if (responseData != null) {

parseJSONWithGSON(responseData)

}

} catch (e: Exception) {

e.printStackTrace()

}

}

}

private fun parseJSONWithGSON(jsonData: String) {

val gson = Gson()

val typeOf = object : TypeToken>() {}.type

val appList = gson.fromJson>(jsonData, typeOf)

for (app in appList) {

Log.e("MainActivity", "id is ${app.id}")

Log.e("MainActivity", "name is ${app.name}")

Log.e("MainActivity", "version is ${app.version}")

}

}

}

使用HTTP访问网络

工作原理特别简单,客户端向服务器发送一条http请求,服务器收到请求后返回数据给客户端,然后客户端解析并处理就可以了。

首先需要获取HttpURLConnection的实例,一般只需要创建一个URL对象,并传入目标的网络地址,然后调用openConnection方法打开这个地址:

val url = URL("https://www.baidu.com")

val connection = url.openConnection() as HttpURLConnection

在得到了HttpURLConnection的实例后,我们可以设置http请求所使用的方法,即获得(GET)或发送(POST)数据:

connection.requestMethod = "GET"

 接下来就可以进行一些比较自由的定制了,如设置连接超时、读取超时的毫秒数:

connection.connectionTimeout = 8000

connection.readTimeout = 8000

之后再调用getInputStream方法,就可以获取服务器返回的输入流,而最后的任务就是对这个输入流进行读取: 

val input = connection.inputStream

 最后调用disconnect方法来关闭我们打开的http链接:

connection.disconnect()

接着我们进入实战:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main)

sendRequestBtn.setOnClickListener {

sendRequestWithHttpURLConnection()

}

}

private fun sendRequestWithHttpURLConnection() {

// 开启线程发起网络请求

thread {

//获取HttpURLConnection实例

var connection: HttpURLConnection? = null

try {

val response = StringBuilder()

//创建一个URL对象,传入目标网址

val url = URL("https://www.baidu.com")

//调用openConnection访问目标网址

connection = url.openConnection() as HttpURLConnection

//自由定制:传入连接超时、读取超时的毫秒数

connection.connectTimeout = 8000

connection.readTimeout = 8000

//获取服务器返回的输入流

val input = connection.inputStream

//对获取到的输入流进行读取

val reader = BufferedReader(InputStreamReader(input))

//使用use函数,能够保证调用者在执行完给定的操作后关闭资源

//因此,use函数仅仅为Closeable的子类所定义使用,如Reader、Writer或Socket

//由于在use代码块的结尾可以自动关闭bufferedReader,所以再次使用

reader.use {

reader.forEachLine {

response.append(it)

}

}

showResponse(response.toString())

} catch (e: Exception) {

e.printStackTrace()

} finally {

//关闭http连接

connection?.disconnect()

}

}

}

private fun showResponse(response: String) {

runOnUiThread {

//ui操作是不允许在子线程进行操作的

//因此在这里进行UI操作,将结果显示到界面上

responseText.text = response

}

}

}

 在运行之前,需要声明网络权限:

上面我们向服务器请求(GET)了数据,那如果我们要发送(POST)数据呢?只需要将http请求的方法改成POST,并在获取输入流之前把要提交的数据写出即可。

需要注意的是,每条发送的数据都要以键值对的形式存在,数据之间使用&符号隔开。 比如我们要向服务器提交用户名和密码:

connection.requestMethod = "POST"

val output = DataOutputStream(connection.outputStream)

output.writebytes("username=admin&password=123456")

网络请求回调的实现方式

因为一个app可能会在很多地方都使用到网络功能,而发送http请求的代码基本是相同的,如果我们每次都去编写一次发送http请求的代码,这显然非常差劲。

通常情况我们会使用工具类:

object HttpUtil {

fun sendHttpRequest(address: String): String {

var connection: HttpURLConnection? = null

try {

val response = StringBuilder()

//创建URL对象,并传入函数接收的目标地址

val url = URL(address)

//调用openConnection访问目标网址

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 {

//关闭http连接

connection?.disconnect()

}

}

}

此后发起http请求,就可以直接写成:

val address = "https://www.baidu.com"

val response = HttpUtil.sendHttpRequest(address)

需要注意的是,网络请求通常是耗时操作,而sendHttpRequest方法内部并没有开启线程,这有可能导致主线程被阻塞。

而这个问题的解决方式也不能是单纯的开启子线程,因为如果把所有的号是逻辑都放在子线程,那sendHttpRequest方法会在服务器还没来得及相应的时候就执行结束了,因此也无法返回服务器响应的数据。

解决该问题的方法在于使用回调机制。首先需要定义一个接口,我们将其命名为HttpCallbackListener:

interface HttpCallbackListener {

fun onFinish(response: String)

fun onError(e: Exception)

}

onFinish方法在服务器响应请求时调用,onError方法在网络操作出错时调用。

修改工具类代码:

object HttpUtil {

//在sendHttpRequest方法中添加了HttpCallbackListener参数

fun sendHttpRequest(address: String, listener: HttpCallbackListener) {

//开启子线程执行具体网络操作

thread {

var connection: HttpURLConnection? = null

try {

val response = StringBuilder()

//创建URL对象,并传入函数接收的目标地址

val url = URL(address)

//调用openConnection访问目标网址

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 {

//关闭http连接

connection?.disconnect()

}

}

}

}

在此我们舍弃了return语句,因为在子线程中是无法通过return来返回数据的,因此我们将服务器传回的数据传入了onFinish方法中。

现在的sendHttpRequest方法接受两个参数,因此我们在调用该方法的时候,还需要传入HttpCallbackListener的实例:

HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {

override fun onFinish(response: String) {

// 得到服务器返回的具体内容

}

override fun onError(e: Exception) {

// 在这里对异常情况进行处理

}

})

上述方法使用的是HttpURLConnection的写法,看起来比较复杂,而使用OkHttp会简单许多:

object HttpUtil {

...

//此处传入了okhttp3.Callback,这个是OkHttp自带的回调接口

fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {

val client = OkHttpClient()

//编写向服务器发送的请求,通过url方法传入目标地址

val request = Request.Builder()

.url(address)

.build()

//此处没有像之前那样调用execute方法

//而是调用了enqueue,并传入okhttp3.Callback参数

client.newCall(request).enqueue(callback)

}

}

在最终的“发送请求并获取服务器返回的地址”这一操作中,我们不再使用先前的execute方法,而是使用enqueue方法,这是因为OkHttp在该方法的内部已经帮我们开好了子线程,然后会在这个子线程中执行http请求,并将最终的请求结果回调到okhttp3.Callback中。

此后我们在调用sendOkHttpRequest方法时就可以这样写:

HttpUtil.sendOkHttpRequest(address, object : Callback {

override fun onResponse(call: Call, response: Response) {

// 得到服务器返回的具体内容

val responseData = response.body?.string()

}

override fun onFailure(call: Call, e: IOException) {

// 在这里对异常情况进行处理

}

})

需要注意的是,无论是使用HttpURLConnection还是OkHttp,最终的回调接口都是在子线程中运行的,因此我们不能在这执行ui操作,除非借助runOnUiThread方法来进行线程转换。

好文阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。