目录
一, 前言
二, 什么是 Socket
三, 如何使用 Socket 进行 http 请求
1, 建立 socket 连接
2,http 协议请求和响应格式解析
3, 进行 http 请求
四, 写在最后
一, 前言
本篇文章是为讲述 okhttp 源码做一个铺垫, 主要是简单讲述一下 socket 的使用, 因为在 okhttp 中网络通讯使用的便是 socket. 但这篇文章不会涉及 okhttp, 会简单阐述下 socket, 然后用代码进行连接后 http 通讯, 话不多说, 开始干!
二, 什么是 Socket
回答这个问题前我们要先看下 TCP/IP 四层模型, 想必这个图大家都有见过, 下面就解释下这四层分别的表现形式是什么(理论解释比较让人摸不着头脑, 所以这里以其表现形式来阐述)
网络接口层: 主要表现为识别 Mac 间比特流的传输
网络层: 表现为 IP 协议
传输层: 表现为 TCP,UDP
应用层: 表现为 Http,Https,RTSP 等(这里的协议比较多, 我们经常使用的 http 协议就属于应用层)
Tip: 顺便说下 TCP 和 UDP 的区别. TCP 提供可靠的通信传输, 类似于打电话, 需要等待另一方的接听, 才能进行真正的通讯; 而 UDP 则不是可靠的, 类似发短信, 只将信息发出, 至于对方有没收到, 这个就不关心了.
而我们关心的 socket 是什么呢? socket 其实是 TCP 连接的抽象, 利用 socket 进行 TCP 的连接(这个解释可能比较片面, 但个人觉得是最为直观的解释, 毕竟全面的解释比较晦涩难懂)
二, 如何使用 Socket 进行 http 请求
1, 建立 socket 连接
在 java 中使用 socket, 其实非常的简单. 如果只是需要一个普通的 socket, 只需通过如下代码, 便可以建立一个 socket 连接
Socket socket = new Socket("ip 或域名", 端口);
如果想建立一个 sslSocket, 用于 https 的通讯 (例如: https://www.baidu.com ) 只需要通过 sslSocketFactory 进行创建 sslSocket 即可. 代码如下:
Socket socket = SSLSocketFactory.getDefault().createSocket("www.baidu.com", 443);
2,http 协议请求和响应格式解析
在使用 socket 进行发起请求前, 我们要先来了解下 http 协议. 简单一点的理解, http 协议其实就是发起一个按照格式约定的字符串, 服务器响应一串按格式组装的数据. 这里不使用教科书式的数据格式, 我们使用从 "Restlet Client" 发起一次请求, 观察其请求报文和响应报文来进行讲解.
Tip:Restlet Client 是一个 API 请求工具, 日常开发中也可以用来向服务器发起请求, 获取数据结构方便调试. 可以在 Chrome 的应用商店下载.
这里使用的 API 是高德的天气预报接口, 点击了 "send" 后, 获取到请求报文和响应报文, 如下图所示
我们先单独说下这次请求的请求报文(第二个红框中内容, 如下所示)
- GET /v3/weather/weatherInfo?city=%E9%95%BF%E6%B2%99&key=13cb58f5884f9749287abbead9c658f2 HTTP/1.1
- Host: restapi.amap.com
(1)第一行为发起请求信息, 其格式为:
发起的请求形式(这里使用的是 GET, 如果为 POST 的话, 这里便为 POST), 即这里的 "GET"
一个空格, 即 " "
请求的路径(不包括域名, 因为域名已经在建立 socket 连接时确定, 如果为 GET 请求, 则参数追加在后面以 "?" 隔开; 如果为 POST 请求, 则请求参数会在 body 中增加, 具体见第四小点), 即这里的 "/v3/weather/weatherInfo?city=长沙&key=13cb58f5884f9749287abbead9c658f2"
一个空格, 即 " "
http 请求的版本, 即 "HTTP/1.1"
\r\n, 此处没有显示出来, 但是自己在组装报文时, 需要增加这个表示一行已经结束
(2)第二行的格式为:
host 字段名, 即 "Host"
一个冒号加一个空格, 即 ":"(敲黑板!! 冒号后面有一个空格, 这个在组装请求报文时, 尤为重要)
host 的内容, 即 "http://restapi.amap.com"
\r\n, 此处没有显示出来, 但是自己在组装报文时, 需要增加这个表示一行已经结束
tip: 这里其实是请求头部, 如果头部参数有多个的话, 就按照这种格式进行拼装. 例如还有一个 "Connection 为 keep-alive" 的头部参数, 则以 "Connection: keep-alive\r\n" 的形式写入输出流中, 具体会在后面的例子中展示.
(3)第三行的格式为:(没想到吧!!! 这里有第三行)
\r\n, 此处没有显示出来, 但是自己在组装报文时, 需要增加这个表示头部参数已写完
(4)如果为 POST 请求, 接下来还需要进行 body 参数的拼装, 这里以 form 表单为例, 拼接上面接口的参数. 规则就是 "键 = 值", 键值对间用 "&" 隔开.
city = 长沙 & key=13cb58f5884f9749287abbead9c658f2
至此一个请求报文便拼装完毕, 将其用输出流写出即可获得服务器的响应报文.
我们接着说下这次请求的响应报文
- HTTP/1.1 200 OK
- Server: Tengine
- Date: Sun, 06 May 2018 08:22:10 GMT
- Content-Type: application/JSON;charset=UTF-8
- Content-Length: 445
- Connection: close
- X-Powered-By: ring/1.0.0
- gsid: 010185222147152559493030300162313551811
- sc: 0.013
- Access-Control-Allow-Origin: *
- Access-Control-Allow-Methods: *
- Access-Control-Allow-Headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,key,x-biz,x-info,platinfo,encr,enginever,gzipped,poiid
- {
- "status":"1","count":"2","info":"OK","infocode":"10000","lives":[{
- "province":"湖南","city":"长沙市","adcode":"430100","weather":"阵雨","temperature":"25","winddirection":"东北","windpower":"7","humidity":"78","reporttime":"2018-05-06 16:00:00"
- },{
- "province":"湖南","city":"长沙县","adcode":"430121","weather":"阵雨","temperature":"25","winddirection":"东北","windpower":"7","humidity":"78","reporttime":"2018-05-06 16:00:00"
- }]
- }
(1)第一行为响应状态, 格式为
http 的版本信息, 即 "HTTP/1.1"
一个空格, 即 " "
状态码, 即 "200"
一个空格, 即 " "
状态, 即 "OK"
\r\n, 此处没有显示出来, 但是解析响应报文时, 需要通过这两个字符进行判断是否一行结束
(2)第二行至第十二行为响应头, 每一行的格式为
头名称, 即 "Server"
一个冒号加一个空格, 即 ":"
头部参数值, 即 "Tengine"
\r\n, 此处没有显示出来, 但是解析响应报文时, 需要通过这两个字符进行判断是否一行结束
(3)第十三行, 格式为
\r\n, 用于区分头部参数和内容的区分
(4)第十四行为响应内容, 格式为
这里便是接口给我们的数据, 即此处给到我们的天气 JSON 数据, 而此处 JSON 的长度为头部中有一个参数为 "Content-Length" 决定的, 例子中内容的长度为 445. 值得一提的是, 有些接口返回的头部参数并没有 "Content-Length" 这一头部参数, 而是返回了 "Transfer-Encoding: chunked" 这样的头部参数, 则表明是以块的形式给到我们数据. 块的形式会以如下格式, 第一行的 "10\r\n" 表明接下来的一行会有 10 个字节的内容, 第二行便是 10 字节的内容, 同样以 "\r\n" 结束一行(\r\n 这两个字符不算在内容长度中), 每一块的格式都按这样的形式, 如果遇到 "0\r\n\r\n" 就说明内容结束.
10\r\n //(注意!!! 这里是 10 是 16 进制, 即如果进行内容读取需要将其进行做 10 进制的转换)
10 字节长度的内容 \ r\n
- // 结束格式
- 0\r\n
- \r\n
至此响应报文解析完毕.
3, 进行 http 请求
逼逼叨逼逼叨了这么久, 很多小伙伴已经很迫不及待的想知道怎么请求和获取响应了. 我们这里便直接上代码, 代码很简单, 并没有什么知识难点.
- public class MySocket {
- public static void main(String[] args) throws IOException {
- // 如果需要进行 https 的请求只需要换成如下一句(https 的默认端口为 443,http 默认端口为 80)
- //Socket socket = SSLSocketFactory.getDefault().createSocket("xxx", 443);
- Socket socket = new Socket("restapi.amap.com", 80);
- // 获取输入流, 即从服务器获取的数据
- final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- // 获取输出流, 即我们写出给服务器的数据
- BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
- // 使用一个线程来进行读取服务器的响应
- new Thread() {
- @Override
- public void run() {
- while (true) {
- String line = null;
- try {
- while ((line = bufferedReader.readLine()) != null) {
- System.out.println("recv :" + line);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }.start();
- bufferedWriter.write("GET /v3/weather/weatherInfo?city=长沙&key=13cb58f5884f9749287abbead9c658f2 HTTP/1.1\r\n");
- bufferedWriter.write("Host: restapi.amap.com\r\n\r\n");
- bufferedWriter.flush();
- }
- }
跑起来后会看到控制台输出如下信息, 这个时候我们就可以按照第二小结中的格式进行解析到一个模型中, 最终返回给 UI 或是逻辑层去使用.
四, 写在最后
OkHttp 中使用 socket 连接后, 进行处理响应便是这样的处理逻辑. 只是它还有对 socket 的复用, 连接进行限制之类的优化处理, 这个在后面的文章中会进行剖析. 如果您期待这样的剖析之旅的话, 给个 "" 加个关注吧! 文章中并没有对头部参数进行说明其含义, 这里也不打算给出, 其实百度一下或 google 都有很多, 需要的时候进行搜查一下即可. 记住我, 我是猛猛的小盆友, 如果我有理解错误或是写的晦涩难懂的地方请与我联系讨论, 共同进步.
来源: https://juejin.im/post/5c307711e51d4552427d0e58