okhttp3

OkHttp3 (三)——创建与执行网络请求

2016-12-21  本文已影响1460人  798fe2ac8685

标签(空格分隔): OkHttp3

版本:1
作者:陈小默
声明:禁止商业,禁止转载

发布于:作业部落简书


[toc]


请求

在OkHttp中,一般的请求方式为:

fun main(args: Array<String>) {
    val client = OkHttpClient()

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

    val call = client.newCall(request)

    val response = call.execute()

    if (response.isSuccessful) {
        // 请求成功
    }
    response.close()
}

当我们使用newCall()函数的时候,OkHttp就会通过传入的请求对象生成一个可执行的Call对象。当我们执行Call对象的execute()方法的时候,就会开始执行网络请求,并且得到响应对象。需要注意的是,此方法的执行方式为同步阻塞的,也就是说该线程会被暂停在这里直到接收到服务端的响应或者发生错误从而继续执行。

异步请求

在有些不能使用直接请求的场景下,比如Android或者某些不希望进程被阻塞的应用,我们就需要通过异步的方式来执行请求,并且在何时的时候回调。写法如下:

fun main(args: Array<String>) {
    val client = OkHttpClient()

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

    val call = client.newCall(request)

    call.enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            // 请求过程中出现错误时回调
            e.printStackTrace()
        }

        override fun onResponse(call: Call, response: Response) {
            // 请求成功时回调
        }
    })
}

enqueue是一个异步请求方法,其中传入的参数Callback是请求回调的接口。注意:当我们使用异步请求的时候,也是需要关闭Response对象的。

在使用异步请求的时候,RealCall的执行流程方法如下:

    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

通过上面的函数,我们可以看到其调用规律:当请求被取消时,回调onFailure,否则回调onResponse并且将获取的response对象传入。这里需要注意的是,response交给调用者之后,OkHttp并没有对其进行任何附带的处理,比如关闭。这就要求调用者必须在回调方法中关闭Response对象,就像这样:

    call.enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            // 请求过程中出现错误时回调
            e.printStackTrace()
        }

        override fun onResponse(call: Call, response: Response) {
            // 请求成功时回调
            
            //无论任何情况都要关闭该对象
            response.close()
        }
    })

处理文本数据

发出请求后,服务器会返回给客户端一些数据,当然,在某些场景下,你可能仅仅会受到服务器的响应头信息而没有任何数据。

response.body()函数能够获取到服务器返回用数据。

在其中,封装了一个名叫string的函数,这个函数能够将接收到的数据重新解码为字符串并返回。在传递文本数据时非常有用。

    call.enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            e.printStackTrace()
        }

        override fun onResponse(call: Call, response: Response) {
            val body = response.body()
            val string = body.string()
            println(string)
            // {"success":true,"message":"hello","data":"服务器接收到了客户端的来信"}
            response.close()
        }
    })

处理字节数组

除了文本信息,我们还可能收到小型文件数据。在数据比较小的情况下,我们可以先以字节数据的形式保存在内存中,然后再将其一次性写入文件。

    call.enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            e.printStackTrace()
        }

        override fun onResponse(call: Call, response: Response) {
            val body = response.body()
            val bytes = body.bytes()
            mImageFile.writeBytes(bytes)
            response.close()
        }
    })

处理字节流

当我们需要下载的文件比较大时,将所有数据保存在字节数组中的方法就已经不再适用了,那么我们需要通过流的形式,将数据保存在文件中。

    call.enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            e.printStackTrace()
        }

        override fun onResponse(call: Call, response: Response) {
            val body = response.body()
            val head = response.header("Content-Disposition")
            //attachment;filename=One+More+Chance.mp4
            
            val oName = head.substring(head.indexOf("=") + 1, head.length)
            val filename = URLDecoder.decode(oName, "utf-8")
            //One More Chance.mp4

            val fOut = File(LOCAL_PATH, filename).outputStream()
            
            val input = body.byteStream()
            val bytes = ByteArray(64 * 1024)
            var len = 0
            do {
                fOut.write(bytes, 0, len)
                len = input.read(bytes)
            } while (len > 0)
            
            fOut.close()
            input.close()
            response.close()
        }
    })

带进度下载文件

如果我们需要在下载过程中获取到下载的进度,我们就需要对下载过程进行封装。

fun download(call: Call, dir: String,
             onStart: (filename: String) -> Unit,
             progress: (current: Long, max: Long, speed: Long) -> Unit,
             onError: (Exception) -> Unit,
             onCompleted: () -> Unit) {
             
    call.enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            onError(e)
        }

        override fun onResponse(call: Call, response: Response) {
            var out: OutputStream? = null
            var input: InputStream? = null
            try {
                //--------获取下载文件的文件名,并判断本地是否有重名文件,如果有就在文件名后加上数字
                val head = response.header("Content-Disposition")
                val oName = head.substring(head.indexOf("=") + 1, head.length)
                var filename = URLDecoder.decode(oName, "utf-8")
                val left = filename.substring(0, filename.lastIndexOf("."))
                val right = filename.substring(filename.lastIndexOf("."), filename.length)
                var file = File(dir, filename)

                var index = 1
                while (file.exists()) {
                    filename = "$left(${index++})$right"
                    file = File(dir, filename)
                }

                //--------将当前文件名回调
                onStart(filename)

                //--------获取输入输出流
                out = File(dir, filename).outputStream()
                input = response.body().byteStream()

                //--------准备下载用到的缓冲区
                val bytes = ByteArray(512 * 1024)
                var len = 0
                var current = 0L
                val max = response.body().contentLength()

                //--------用于计算下载速度
                var timer = System.currentTimeMillis()
                var speed = 0L
                do {
                    current += len
                    speed += len

                    //------时间每超过一秒就回调一次下载进度
                    val currentTimer = System.currentTimeMillis()
                    if (currentTimer - timer > 1000) {
                        progress(current, max, speed)
                        timer = currentTimer
                        speed = 0L
                    }
                    out.write(bytes, 0, len)
                    len = input.read(bytes)
                } while (len > 0)
            } catch (e: Exception) {
                onError(e)
            } finally {
                if (input != null)
                    try {
                        input.close()
                    } catch (e: IOException) {
                    }
                if (out != null)
                    try {
                        out.close()
                    } catch (e: IOException) {
                    }
                response.close()
            }
            onCompleted()
        }
    })
}

上面的函数额外部分有:

文件命名:
将重复文件以编号形式重新命名。

定时回调:
这里采用比较时间的方式来判断是否应该回调。这是一种比较粗糙的计时方式,但是可以满足绝大部分的应用场景。如果对时间要求比较高的话,应该另外开启一个用于计时的线程。

接下来我们看一下如何使用:

fun main(args: Array<String>) {
    val client = OkHttpClient()

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

    val call = client.newCall(request)

    download(call, LOCAL_PATH,
            ::println,
            { current, max, speed ->
                println("当前下载进度: ${current * 100 / max}%  下载速度${speed / 1024}KB/s")
            },
            Exception::printStackTrace,
            {
                println("下载完成")
            })
}
One More Chance(2).mp4
当前下载进度: 1%  下载速度363KB/s
当前下载进度: 2%  下载速度347KB/s
当前下载进度: 4%  下载速度540KB/s
当前下载进度: 7%  下载速度580KB/s
当前下载进度: 8%  下载速度458KB/s
当前下载进度: 10%  下载速度455KB/s
当前下载进度: 12%  下载速度510KB/s
...
当前下载进度: 97%  下载速度386KB/s
当前下载进度: 99%  下载速度453KB/s
下载完成

BufferedSource

由于OkHttp封装了okio,所以可以将响应数据通过BufferedSource的形式返回。

val buffer = response.body().source()

BufferedSource中的其他功能不再赘述。这里只介绍其中一种用法,就是跳过一定长度的数据,接下来从服务器请求并截取字符串:

fun main(args: Array<String>) {
    val client = OkHttpClient()

    val request = Request.Builder()
            .url("$ECHO?message=hello")
            .build()

    val call = client.newCall(request)

    val response = call.execute()

    val buffer = response.body().source()

    buffer.skip(20) //跳过20个字节

    val string = buffer.readString(31, Charset.forName("UTF-8"))    //向后读取31个字节的数据

    println(string)

    response.close()
}

打印结果为:

sage":"hello","data":"服务器

可以看到前面确实是少了一部分数据的。该用法的使用场景通常为文件的断点续传。在支持断点续传的站点,我们是可以直接通过请求头来向站点请求某一段的数据。但是对于不支持断点续传的站点,我们就可以通过BufferedSource的skip在本地跳过一段数据。并且在BufferedSource的read*方法中通常都有一个指定长度的重载,这么一来,一个断点续传的功能就能轻易实现了。

Android中的异步请求

如果你在Android应用中直接使用上述代码进行异步请求,并且在回调方法中操作了UI,那么你的程序就会抛出异常,并且告诉你不能在非UI线程中操作UI。这是因为OkHttp对于异步的处理仅仅是开启了一个线程,并且在线程中处理响应。OkHttp是一个面向于Java应用而不是特定平台(Android)的框架,那么它就无法在其中使用Android独有的Handler机制。于是,当我们需要在Android中进行网络请求并且需要在回调用操作UI的话,就需要自行封装一套带Handler的回调。

接下来我们将上面带进度下载的例子修改为Android适用的版本:

class DownloadUtils(url: String,
                    progress: (current: Long, max: Long, speed: Long) -> Unit,
                    onError: (Exception) -> Unit,
                    onCompleted: (result: ByteArray) -> Unit) {

    private val PROGRESS = 1
    private val ERROR = 2
    private val COMPLETED = 3

    private val TAG_CURRENT = "current"
    private val TAG_MAX = "max"
    private val TAG_SPEED = "speed"

    private val client = OkHttpClient()
    private val handler = object : Handler() {
        override fun handleMessage(msg: Message) {
            when (msg.arg1) {
                PROGRESS -> {
                    val current = msg.data.getLong(TAG_CURRENT, 0)
                    val max = msg.data.getLong(TAG_MAX, 1)
                    val speed = msg.data.getLong(TAG_SPEED, 0)
                    progress(current, max, speed)
                }
                ERROR -> {
                    val error = msg.obj as Exception
                    onError(error)
                }
                COMPLETED -> {
                    val result = msg.obj as ByteArray
                    onCompleted(result)
                }
                else -> super.handleMessage(msg)
            }
        }
    }

    private val call: okhttp3.Call

    init {
        val request = Request.Builder()
                .url(url)
                .build()
        call = client.newCall(request)
    }

    var isCancel = false
        private set

    fun start() {
        call.enqueue(object : okhttp3.Callback {
            override fun onFailure(call: okhttp3.Call, e: IOException) {
                val msg = handler.obtainMessage()
                msg.arg1 = ERROR
                msg.obj = e
                handler.sendMessage(msg)
            }

            override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
                var out: OutputStream? = null
                var input: InputStream? = null
                try {
                    out = ByteArrayOutputStream()
                    input = response.body().byteStream()

                    val bytes = ByteArray(8 * 1024)
                    var len = 0
                    var current = 0L
                    val max = response.body().contentLength()

                    var timer = System.currentTimeMillis()
                    var speed = 0L
                    do {
                        current += len
                        speed += len

                        val currentTimer = System.currentTimeMillis()
                        if (currentTimer - timer > 1000) {
                            val msg = handler.obtainMessage()
                            msg.arg1 = PROGRESS
                            msg.data.putLong(TAG_CURRENT, current)
                            msg.data.putLong(TAG_MAX, max)
                            msg.data.putLong(TAG_SPEED, speed)
                            handler.sendMessage(msg)
                            timer = currentTimer
                            speed = 0L
                        }
                        out.write(bytes, 0, len)
                        len = input.read(bytes)
                    } while (len > 0 && !isCancel)
                    if (isCancel)
                        throw IOException("Cancel")
                    val msg = handler.obtainMessage()
                    msg.arg1 = COMPLETED
                    msg.obj = out.toByteArray()
                    handler.sendMessage(msg)
                } catch (e: IOException) {
                    onFailure(call, e)
                } finally {
                    if (input != null)
                        try {
                            input.close()
                        } catch (e: IOException) {
                        }
                    if (out != null)
                        try {
                            out.close()
                        } catch (e: IOException) {
                        }
                    response.close()
                }
            }
        })
    }

    fun cancel() {
        synchronized(this, {
            if (!isCancel) {
                isCancel = true
                call.cancel()
            }
        })
    }
}

看起来好像比较长,那么我们来看看使用效果

class MainActivity : AppCompatActivity() {

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

        val image = findViewById(R.id.imageView) as ImageView
        val url = "http://192.168.1.112:8080/smart/okhttp/get/cover"

        val utils = DownloadUtils(url,
                { current, max, speed ->
                    Log.e("progress", "当前下载进度: ${current * 100 / max}%  下载速度${speed / 1024}KB/s")
                },
                Exception::printStackTrace,
                {
                    Log.e("completed", "下载完成")
                    val bitmap = BitmapFactory.decodeByteArray(it, 0, it.size)
                    image.setImageBitmap(bitmap)
                })

        utils.start()
    }
}

在控制台看到输出了如下内容

E/progress: 当前下载进度: 10%  下载速度57KB/s
E/progress: 当前下载进度: 15%  下载速度24KB/s
E/progress: 当前下载进度: 15%  下载速度2KB/s
E/progress: 当前下载进度: 15%  下载速度1KB/s
E/progress: 当前下载进度: 18%  下载速度12KB/s
E/progress: 当前下载进度: 19%  下载速度8KB/s
E/progress: 当前下载进度: 24%  下载速度25KB/s
E/progress: 当前下载进度: 27%  下载速度18KB/s
E/progress: 当前下载进度: 34%  下载速度36KB/s
E/progress: 当前下载进度: 48%  下载速度72KB/s
E/progress: 当前下载进度: 76%  下载速度152KB/s
E/completed: 下载完成
上一篇下一篇

猜你喜欢

热点阅读