在开源盛行的今天,有许多出色的网络通信库可以替代原生的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方法来发送请求并获取服务器返回的数据即可。
先下载一个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。
解析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如下:
...
...
使用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()
}
}
}
开始前,在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。
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并没有被添加到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请求,服务器收到请求后返回数据给客户端,然后客户端解析并处理就可以了。
首先需要获取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方法来进行线程转换。