在Android的网络开发过程中,我们通常会使用像Okhttp、Retrofit这种高度封装的网络库,它们完全屏蔽了相关技术细节。但是掌握其中的原理对我们来说是很重要的,要知其然,也要知其所以然,只要掌握了这些原理,你才能更好的理解Okhttp等网络库的源码实现。
网络编程通常会涉及以下几个角色:
怎么去理解它们的关系呢?🤔
例如我们是双十一从马老板家买了部手机,这个时候我们就是客户端,马老板就是服务端。手机要通过快递公司的汽车运送到我们手中。TCP/IP就相当于汽车,但是光有汽车是不够的,还要对汽车 进行分类,不然都是一样的汽车就乱套了,而完成分类的就是HTTP/HTTPS了,HTTP/HTTPS会告诉这些汽车,你是负责送货的(GET),你是负责退货的(POST)等等。
注:文章中部分图片来源于网络,这次就偷个懒,有些流程图就不画了。😁
TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,
TCP协议是HTTP/HTTPS、WebSocket等协议的基础,我们首先来看看它们的报文格式。
关于IP数据报与TCP报文你只需要理解它的结构就行,不用去记它,等到使用的时候不记得了,查一下就好了。
IP数据报
TCP报文
TCP用三次握手(three-way handshake)过程创建一个连接,使用四次分手 关闭一个连接。
三次握手与四次分手的流程如下所示:
三次握手
四次分手
三次握手与四次分手也是个老生常谈的概念,举个简单的例子说明一下。
三次握手
例如你小时候出去玩,经常玩忘了回家吃饭。你妈妈也经常过来喊你。如果你没有走远,在门口的小土堆上玩泥巴,你妈妈会喊:"小新,回家吃饭了"。你听到后会回应:"知道了,一会就回去"。妈妈听 到你的回应后又说:"快点回来,饭要凉了"。这样你妈妈和你就完成了三次握手的过程。😁说到这里你也可以理解三次握手的必要性,少了其中一个环节,另一方就会陷入等待之中。
三次握手的目的是为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误.
四次分手
例如偶像言情剧干净利落的分手,女主对男主说:我们分手吧🙄,男主说:分就分吧😰。女主说:你果然是不爱我了,你只知道让我多喝热水🙄。男主说:事到如今也没什么好说的了,祝你幸福🙃。四次分手完成。说到这里你可以理解 了四次分手的必要性,第一次是女方(客户端)提出分手,第二次是男主(服务端)同意女主分手,第三次是女主确定男主不再爱她,也同意男主分手。第四次两人彻底拜拜(断开连接)。
因为TCP是全双工模式,所以四次分手的目的就是为了可靠地关闭连接。
HTTP(HyperText Transfer Protocol)是一种用于分布式、协作式和超媒体信息系统的应用层协议[1]。HTTP是万维网的数据通信的基础。
HTTP是最常见的应用层协议,我们日常开发中基本上接触到的都是这个协议。
HTTP应用程序是通过相互发送报文工作的,报文是HTTP应用程序之间发送的数据块,报文通常分为请求报文和响应报文两种,请求报文向服务器请求一个动作,响应报文将请求结果返回给客户端。
HTTP请求报文分为三部分:请求行、请求首部、请求实体.
请求报文
GET /his?wd=&from=pc_web&rf=3&hisdata=&json=1&p=3&sid=20740_20742_1424_18280_20417_17001_15840_11910_20744_20705&csor=0&cb=jQuery110206488567241711853_1469936513370&_=1469936513371 HTTP/1.1
Host: www.baidu.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:47.0) Gecko/20100101 Firefox/47.0
Accept: text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, *//*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Referer: https://www.baidu.com/
Cookie: BAIDUID=DB24D5F4AB36694CF00C4877ADA56562:FG=1; BIDUPSID=DB24D5F4AB36694CF00C4877ADA56562; PSTM=1469936050; BDRCVFR[gltLrB7qNCt]=mk3SLVN4HKm; BD_CK_SAM=1; H_PS_PSSID=20740_20742_1424_18280_20417_17001_15840_11910_20744_20705; BD_UPN=133252; H_PS_645EC=96a0XJobAseSCdbn9%2FviULLD7KreCHN4V4HzQtcGacKF8tGu13Nzd6j9PoB2SPPVj1d5; BD_HOME=0; __bsi=11860814506529643127_00_0_I_R_25_0303_C02F_N_I_I_0
Connection: keep-alive
响应报文
HTTP响应报文分为三部分:状态行、响应首部、响应实体。
HTTP/1.1 200 OK
Server: bfe/1.0.8.14
Date: Sun, 31 Jul 2016 03:41:53 GMT
Content-Type: baiduApp/json; v6.27.2.14; charset=UTF-8
Content-Length: 95
Connection: keep-alive
Cache-Control: private
Expires: Sun, 31 Jul 2016 04:41:53 GMT
Set-Cookie: __bsi=12018325985460509248_00_0_I_R_4_0303_C02F_N_I_I_0; expires=Sun, 31-Jul-16 03:41:58 GMT; domain=www.baidu.com; path=/
报文通常由以下部分组成:
方法
方法(method):客户端希望服务器对资源执行的动作。
我们主要讨论我们最常用的两个GET/POST。
GET与POST在本质上都是TCP连接,只是GET直接把参数写在请求行中,而POST把参数放在请求体中。关于这两个方法,要注意以下两点:
状态码
更多关于状态码的细节可以参见HTTP状态码。
首部
首部通常和方法配合工作,共同决定了客户端和服务器能做什么事情。
常见的通用首部
常见的请求首部
常见的响应首部
常见的实体首部
更多关于首部的细节可以参见HTTP首部。
HTTPS是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。HTTPS开发的主要目的,是提供对网站服务器的身份 认证,保护交换数据的隐私与完整性。
如下图所示,可以很明显的看出两个的区别:
注:TLS是SSL的升级替代版,具体发展历史可以参考传输层安全性协议。
HTTP与HTTPS在写法上的区别也是前缀的不同,客户端处理的方式也不同,具体说来:
所以你可以看到,HTTPS比HTTP多了一层与SSL的连接,这也就是客户端与服务端SSL握手的过程,整个过程主要完成以下工作:
SSL握手是一个相对比较复杂的过程,更多关于SSL握手的过程细节可以参考TLS/SSL握手过程
SSL/TSL的常见开源实现是OpenSSL,OpenSSL是一个开放源代码的软件库包,应用程序可以使用这个包来进行安全通信,避免窃听,同时确认另一端连接者的身份。这个包广泛被应用在互联网的网页服务器上。 更多源于OpenSSL的技术细节可以参考OpenSSL。
WebSocket是一种在单个TCP连接上进行全双工通讯的协议,它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握 手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
为什么需要WebSocket,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。而WebSocket可以实现双向通信。一般来说WebSocket是用来实现双工通信的长连接的。HTTP想要达到 这种效果,一般会通过轮询或者long poll来实现,这样比较占用资源且非常被动。
一个典型的WebSocket请求与响应
客户端请求
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
服务器响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/
这里会使用Upgrade: websocket Connection: Upgrade 提示当前发起的是WebSocket协议,注意升级协议。
注:Okhttp已支持WebSocket。
我们同样也来看看WebSocket的报文结构。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
已定义的帧类型:
WebSocket协议的控制帧有3种:
前面我们讲完了几种常用的协议,最后我们再看看和编码相关的知识,这在日常的业务开发中也经常用到。
前面我们已经提到过与内容编码相关的实体首部:
对于我们来说,比较常见到的也需要重点关注的是Content-Type、Content-Encoding这两个。
Content-Type描述的是当前内容的MIME类型,关于MIME类型:
MIME:表示一种主要的对象类型和一个特定的子类型。
它主要有以下几种类型:
Content-Encoding描述的是编码类型,它的意义在于告诉服务端当前客户端支持的编码方式,这样服务端就会根据该编码方式来编码数据。如果没有该首部,则默认认为 客户端支持所有的编码方式。
Accept-Encoding 能够接受的编码方式列表。 Accept-Encoding: gzip:q=1.0, deflate:q=0.5, *:q=0
另外Accept-Encoding还可以用q来表示编码优先级,1.0表示最希望使用的编码,0表示不想接受该编码。
常见的编码类型有:
当然我们常用的就是gzip,Okhttp里面可以利用Okio进行gzip压缩,这里我们也贴下代码。
/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */
final class GzipRequestInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request originalRequest = chain.request();
if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
return chain.proceed(originalRequest);
}
Request compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding", "gzip")
.method(originalRequest.method(), gzip(originalRequest.body()))
.build();
return chain.proceed(compressedRequest);
}
private RequestBody gzip(final RequestBody body) {
return new RequestBody() {
@Override public MediaType contentType() {
return body.contentType();
}
@Override public long contentLength() {
return -1; // We don't know the compressed length in advance!
}
@Override public void writeTo(BufferedSink sink) throws IOException {
BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
body.writeTo(gzipSink);
gzipSink.close();
}
};
}
}
扫一扫
在手机上阅读