从零实现自己的OkHttp(一) 第一个HttpClient

        写在前面

        虽然接触编程有几年了,但一直都是看别人的博文。我最初总是惶恐我写出来的东西会被大佬直呼垃圾。但现在想想倒也不必。我写出当前的理解,第一是对自己成长的一个记录,第二是如果我理解错了也可以让别人发现我的误区,第三是或多或少的帮助那些比我出发晚走的慢的人,就我个人而言我看过很多博客,确实有很多人在我不同的阶段帮助到我。后续我也想陆续的写一些博客,希望这是一个好的开始。

        我们本文要解决的问题是:我们的数据是以什么样的格式通过什么样的方式送到服务端,返回的数据怎么去解析?

 

一、以什么样的格式:HTTP协议

        如果对HTTP较为熟悉的话可以跳过本节

        1.1请求

        HTTP的网络请求的数据格式为:协议头+请求头+空行+请求体。每条数据以"\r\n"作为结束。所以我们在生成请求体的时候需要以这种格式发送给服务器,服务器才能解析做出响应。举个例子,一个正常的GET请求头是这个样子。

        如果GET请求带参数那么他的参数是会在路径后面以?a=1&b=2的格式拼接,请求体为空行。如果POST表单请求,那么请求体处的数据为a=1&b=2。请求头处的参数有许多,具体的有一个

请求头响应头对照表查看所有的请求头和请求头。对我们来说比较重要的是

Connection Host Accept Accept-Encoding Accept-Language。

Connection的值有keep-alive(默认)和close,就是说网络请求是否保持长连接。Host主机地址。Accept表示客户端接受什么样格式的数据,Accept-Encoding表示客户端是否支持压缩。一个三万多字节的数据经过压缩后能达到四五千字节,效果是非常显著的,对于服务器压力、减少流量、加快传输速度也是非常有效的,现在大部分浏览器都支持解压缩。Accept-Language则表示以什么样的语言格式接收,这个需要服务器支持。

1.2 响应

       HTTP的 响应格式和请求类似。响应状态 + 响应头 + 空行 + 数据。每条数据以"\r\n"为结束。举个例子,一个正常的响应头大概长这样。

        当然响应头也不止这些,这里缺少了一些非必要却重要的请求头。在我们使用中常用的请求头有:

        Content-Type:告诉客户端数据格式以及编码类型

        Content-Length:告诉客户端有多少字节的数据(数据定长)

        Transfer-Encoding:告诉客户端以数据块的形式传输(数据不定长)

        Content-Encoding:数据的编码类型

        我们在解析响应的时候需要对这些数据进行提取,根据数据类型做不同的处理。

当然这只是一些粗略的介绍,非本文的重点,更为详细的HTTP知识请看这里

二、以什么样的方式:TCP

         简单点讲其实发起一个网络请求其实就是通过Socket以HTTP的协议格式进行请求和应答。整个过程都是IO流的操作。问题给到JAVA网络编程。Java的IO操作大的可以分为字符流和字节流两个系列。字符流以读写字符为主,比如文本文件,自带buffer,对数据有缓存,读取速率相对较高。字节流以读写字节为主,例如我们的一些图片或视频等文件操作。

        在我们的应用中,如果光看响应头容易误入歧途,想着通过字符流系列的BufferReader去读取每一行对响应头进行提取,但问题是,我们的数据很有可能不是字符而是字节,而且BufferReader和InputStream不能交替使用。原因是BufferReader已经将数据读取到他自己的缓存中去了,InputStream再去读会造成数据丢失或者读不到数据。为了保持最纯正的数据,我们还是以InputStream去读取数据。Java IO 的的知识点也很多,有需要深入了解大家可以看看这个一文带你看懂JAVA IO流,或者翻阅其他资料。

三、准备动手

        理论基础已经差不多了,现在开始将我们的理论付诸实践

3.1 URL解析

fun url(url:String):Request{
    this.url=url
    val urlReal = URL(url)
    host=urlReal.host
    port=urlReal.port
    if (port==-1)
        port=urlReal.defaultPort
    api=urlReal.path
    return this
}

3.2 创建一个默认的请求头

/*添加默认头*/
private fun buildDefaultHeader(){
    addHeader("Host", "$host:$port")
    addHeader("Content-Type","application/x-www-form-urlencoded")
    addHeader("User-Agent","PostmanRuntime/7.15.0")
    addHeader("Accept","*/*")
    addHeader("Cache-Control","no-cache")
    addHeader("Accept-Encoding","br, deflate,gzip")
    addHeader("Connection","keep-alive")
}

/*将请求头转字符串*/
private fun header2String(){
    val stringBuilder =StringBuilder()
    for (it in headerMap){
        stringBuilder.append(it.key)
        stringBuilder.append(": ")
        stringBuilder.append(it.value)
        stringBuilder.append("\r\n")
    }
    /*POST请求加上请求体长度*/
    if (method=="POST"){
        stringBuilder.append("Content-Length")
        stringBuilder.append(": ")
        stringBuilder.append(body.toString().length)
        stringBuilder.append("\r\n")
    }
    headerString=stringBuilder.toString()
}

3.3 将数据发给服务器

/*开启socket,以字节流形式发送给服务器拿到服务器返回的字节流准备处理*/
fun open(request:Request.Builder):Response{
    socket= Socket(request.host,request.port)
    val outputStream = socket.getOutputStream()
    val inputStream = socket.getInputStream()
    println(request.toString())
    outputStream.write(request.toString().toByteArray())
    val response = Response()
    response.dealInput(inputStream)
    socket.close()
    return response
}

/*request的内部类,用于构建request*/
inner class Builder(val host: String,val port: Int,val head:String,val header:String,val body:String){
    override fun toString(): String {
        val stringBuilder =StringBuilder()
        stringBuilder.append(head)
        stringBuilder.append("\r\n")
        stringBuilder.append(header)
        stringBuilder.append("\r\n")
        stringBuilder.append(body)
        stringBuilder.append("\r\n")
        return stringBuilder.toString()
    }
}

3.4 提取响应头

编写我们的readline

private fun readLine(inputStream: InputStream):String{
    val byteArray=ArrayList<Byte>()
    while (inputStream.read().let {
            if (it!=10&&it!=13){
                byteArray.add(it.toByte())
            }
            it
        }!=10){

    }
    return String(byteArray.toByteArray())
}

3.5 提取数据(含GZIP解码)

        (我们以数据全为字符串或json格式为例,暂时不包含文件下载)这里我们需要按情况处理,如果服务器返回的响应头里包含Content-Length,我们可以读取该长度的字节即可。如果响应头里包含Transfer-Encoding那么就算设置了Content-Length也会被忽略。设置了Transfer-Encoding的数据会分块发送,格式第一行为第一个块的长度,另起一行跟相应的长度的字节数据。如果还有数据以同样的格式传输,直到最后一个块长度为0,然后空行结束。如果响应头里包含了Content-Encoding:gzip,那么我们的数据还不能直接转String,不然出来的是一堆乱码,我们需要进行gzip解码,拿到解码后的数据再进行相应的操作。

/*借鉴其它网友的解码方法*/
public static byte[] uncompress(byte[] bytes) {
    if (bytes == null || bytes.length == 0) {
        return null;
    }
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    ByteArrayInputStream in = new ByteArrayInputStream(bytes);
    try {
        GZIPInputStream ungzip = new GZIPInputStream(in);
        byte[] buffer = new byte[256];
        int n;
        while ((n = ungzip.read(buffer)) >= 0) {
            out.write(buffer, 0, n);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return out.toByteArray();
}

四、见真章

4.1 用我们的客户端请求彩云天气(Content-Length版)

/*不带参的GET请求,GZIP加密 彩云key要自己申请,好像每天免费一万条*/
val request = Request()
val build = request.url("http://api.caiyunapp.com/v2.5/彩云key" +
                "/121.6544,25.1552/weather.json").build()?:return
val open = Client().open(build)
println("body: "+open.string())

        请求的结果如下,数据是完整的,因为解码后的字节数太大所以没有截全。

Content-Encoding:gzip情况下字节只有4339,原因是我们在请求头处设置了支持GZIP,如果我们请求头设置不支持gzip他就会以没编码的格式给到我们,将近三万字节左右。

4.2 用我们的客户端请求不定长数据接口(Transfer-Encoding版)

        请求其它的一样,只是接口地址换一下,换成我之前写的一个测试的接口,对比两个响应头发现下面的请求已经没有了Content-length而换成了Transfer-Encoding

        http://59.110.212.105/PharmacyServer/0/selectMedicine.action

        请求结果如下:

 4.3 用我们的客户端发送带json格式数据的POST请求

        该请求带有自己的请求体参数,并且数据以简单的json格式传递,可以设置为POST请求

val request = Request()
val formBody = FormBody()
formBody.addParam("userName","0001")
formBody.addParam("password","123456")
request.post(formBody)
request.addHeader("Content-Type","application/json")
request.setMethod("POST")
val build=request.url("http://59.110.212.105/PharmacyServer/signin.action").build()?:return
val open = Client().open(build)
println("body: "+open.string())

        响应结果如下

五、说点什么

        怎么样,使用起来是不是感觉很熟悉呢?客户端已经能满足基本的GET\POST请求并提取出我们需要的数据。在这里我们其实用到了初中级Java或安卓面试中常面的一些问题,HTTP协议和IO。目前的成果只是一个小阶段,还有很多需要一步步完善的,比如作为安卓开发,网络请求肯定是不能放在主线程的,我们需要进行一个主动的线程检测并抛出我们的异常。

        后续的我计划会在这个基础上加入 拦截器 、GSON解析、实现像retrofit那样的通过自定义注解和接口去实现请求的配置,异常的处理、性能上的提升等等,一步步完善这个体系,当然不可能做出和OKHttp一样的效果,但目的在于将自己的理解表达出来,边运用边表达边收获。

        期待批评与指正。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
生成调用第三方API的代码需要以下步骤: 1. 确认第三方API的使用方式和协议,例如RESTful API、SOAP API、JSON API等。 2. 在Java中导入相关的API库或者使用HTTP请求库,例如HttpClient或者OkHttp。 3. 通过API提供的接口文档,构建相应的请求参数、请求头等信息。 4. 发送HTTP请求并获取API返回的响应数据。 5. 解析响应数据并处理异常情况。 下面是一个简单的使用Java调用第三方API的示例代码: ``` import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; public class ApiCaller { public static void main(String[] args) throws IOException { // 待调用的API地址 String apiUrl = "https://api.example.com/some-endpoint"; // 构建HTTP连接 URL url = new URL(apiUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); // 设置HTTP请求头 connection.setRequestMethod("GET"); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Authorization", "Bearer <your-access-token>"); // 发送HTTP请求 int responseCode = connection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { // 读取API返回的响应数据 BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); // 处理API返回的响应数据 System.out.println(response.toString()); } else { // 处理API返回的错误信息 System.out.println("API调用失败,错误码为:" + responseCode); } connection.disconnect(); } } ``` 这是一个简单的GET请求示例,实际的调用方式会根据API的不同而有所差异。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值