从OkHTTP请求到读取回复都发生了什么

记得很久看到过一篇博客《从输入网址到网页出现在浏览器中都发生了什么》(名字太长,现在已经记不得了T T),佩服于作者深厚的技术积淀,能够从上到下将整个过程梳理的一清二楚。

从接触Android开发以来,我接触了Volley和OkHttp等优秀的网络库,加上之前对Linux也有所接触,就心血来潮,想要仿照他人写一篇从上到下梳理Android下网络报文发送逻辑的文章。我虽功力未到如此境界,但也想尽力梳理一遍我所知道的部分。

一句话总结:
我们从OkHttp开始,通过调用JAVA的方法进行DNS解析,获取ip地址。然后通过OkHttp的连接池建立TCP三次握手,之后进行高效的数据传送,最后读出回复内容包文。

由于Socket背后代表的TCP/IP协议栈与Linux下的VFS无缝对接,所以我们对Socket返回的文件描述的读写都会进入Linux TCP/IP协议栈中,并发送给远程服务器。

首先,我们从这一段代码开始这次旅程。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    sendRequest();
}

private void sendRequest() {
    String url = "http://www.jianshu.com";
    OkHttpClient okHttpClient = new OkHttpClient();
    Request request = new Request.Builder()
            .url(url)
            .build();
    final Call call = okHttpClient.newCall(request);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {}

        @Override
        public void onResponse(Call call, Response response) throws IOException {
           Log.d(TAG, response.body().string());
        });
    }
}

这是一段非常简单的OkHttp发送网络请求的代码,用来访问特定网页,若访问能够成功,则会在日志中将网页内容打印出来。当我们打开了这个APP,到日志中出现了网页内容,要经过好几个车站哩,大概有这样几站。

[网络框架]-->[Framework]-->[Native]-->[Linux]

由于我们使用了OkHttp框架,所以会有网络框架这一站,当然你也可以直接使用Java的Socket接口进行编程。无论采用哪一种方式,第二站就会进入到FrameWork站,利用Java接口实现网络功能,但这一站也不是直接和操作系统打交道,通过虚拟机Android Runtime提供的接口进入第三站Native层,这一层更靠近操作系统,并且最终调用操作系统提供的Libc库与操作系统打交道,即进入了第四站。接下去的事情就是操作系统使用Tcp/Ip协议栈发送报文和接收报文的过程了。

代码首先创建一个OkHttpClient和一个网络请求类Request,然后再将Request放置于OkHttp工作队列实现异步执行。本篇文章就从call.enqueue()开始分析。

从队列中取出Request

OkHttp允许同步执行请求和异步执行请求,本文分析异步执行请求的过程。在异步请求的模式下,OkHttp内部维护了线程池,这个线程池没有核心线程,不设置非核心线程数目上限,这表明一旦有Request进入队列会马上被线程池处理。异步模式下使用2个的队列。一个是异步执行队列,一个是准备队列。 但是okhttp默认限制了同时处理request的上限为64个,当超过64个时就暂时放到ready队列,执行完一个request之后再把request从ready放到running队列。

Http协议对于同一个客户端同一域名的并发量限制为2个,但是目前的客户端基本无视这一规定,chrome浏览器一般对同一域名的并行tcp连接是6个,OkHttp规定为5个,如果超过也是先放入ready队列。也就是说,如果我们连续向www.jianshu.com发出了6个Request,那么第六个Request会首先进入准备队列,当前5个Request有一个收到了回复之后,才可以发送这一个Request。

建立连接


第一站OkHttp

好了,OkHttp取出了"www.jianshu.com"的Request,开始建立连接。众所周知,Http/1.1就默认开启keep-alive选项,因此,OkHttp会首先从ConnectionPool(连接池)中寻找是否有建立好的与"www.jianshu.com"的连接,如果可以找到的话就直接上车吧~如果没有的话,就只能新建一个连接了。

OkHttp将Ip,Hostname和Proxy等信息封装成Route结构,并且用Set维护一个RouteDataBase,用来保存哪些地址是成功访问的,哪些是失败访问。当新建连接时,OkHttp首先从RouteDataBase中查找所需的信息。//TODO 添加代理部分 http://www.jianshu.com/p/5c98999bc34f
获取满足条件的地址,最后调用RealConnection的connct()方法建立连接。成功建立之后将其置于连接池中,并且在RouteDataBase中添加Route。

第二站Framework

[SocksSocketImpl::connect]-->[AbstractPlainSocketImpl::connect]
-->[PlainSocketImpl::socketConnect]

SocksSocketImpl是PlainSockImpl的子类,PlainSockImpl又是AbstractPlainSockImpl的子类。
SocketSocketImpl只是简单的调用父类的connect方法,AbstractPlainSockImpl::connect为Socket添加Ip与port,然后调用子类的socketConnect方法,并进入了Native层。

第三站Native

创建socket:进入/libcore/ojluni/src/main/native/PlainSocketImpl.c中的PlainSocketImpl_socketCreate根据Java层传入的参数判断TCP或者UDP。fd = JVM_Socket(domain, type, 0))创建socket,并在此函数体内进入第四站。创建成功之后,将fd设置到JAVA层的响应参数中。

connect:进入/libcore/ojluni/src/main/native/PlainSocketImpl.c中的PlainSocketImpl_socketConnect
首先从JAVA层中获取必要的信息,然后调用NET_InetAddressToSockaddr设置struct sockaddr由于我们设置了timeout=0,所以Native设置了非阻塞模式创建socket连接fcntl(fd, F_SETFL, flags|O_NON_BLOCK)然后调用connect()函数,这个函数就是由Linux提供的LibC库中的函数了。由于我们设置了非阻塞模式,所以此函数立马返回,那么如何知道连接的结果呢?这里,Native使用了I/O复用函数,可以选择使用POLL,也可以选择使用Select,由编译选项决定。Select函数和Poll函数的工作原理类似,都是以轮询的方式查询文件描述符是否准备就绪,一旦fd准备就绪就返回,然后我们就可以读取fd中的数据。唯一不同的是select只支持read,write,exception三个事件而poll函数可以定义多个事件,比select更加的“聪明”。

如果我们监听的fd(即socket fd)有写数据,则我们获得了一个本地端口,将其端口通过反射在Java类中设置。

第四站Linux

第三站调用创建socket和connect()函数后就进入了第四站。

[Socket]-->[VFS]-->[Sockfs]-->[TCP/IP]

进入socket_create():

  1. 安全性检查

  2. 对于type和family进行判断和重新赋值

  3. 根据不同的协议调用不同的create函数,返回sock结构体。这里就调用inet_create函数。
    sock结构体保存了大量的信息,定义了TCP传输中的大量参数,包括发送缓冲区,接收缓冲区,计时器,标志位,状态,引用计数等等。

  4. 创建inode,inode是Linux文件系统中的节点。

  5. 获得本进程的一个空闲的文件描述符,申请一个新的目录项,将文件操作的函数指针设置到inode节点中

先初始化路径path:其目录项的父目录项为超级块对应的根目录,名称为空,操作对象为sockfs_dentry_operations,对应的索引节点对象为sock套接字关联的索引节点对象,即SOCK_INODE(sock);装载点为sock_mnt。

申请一个此路径下的file,sock->file = file; file->private_data = sock;file和sock双向绑定。最后返回fd即我们创建的Socket的fd。
这样就将socket与文件系统绑定了,因此我们可以对socket进行文件操作。

#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, socklen_t address_len);

如果连接建立成功则返回0,否则连接建立失败。在connect函数中进行TCP三次握手,建立TCP连接。

读写数据


第一站OkHttp

OkHttp支持Chunk方式写入数据,这种方式类似于Ip的分包,将数据分包传送,当客户端接收到一个CHunk就可以立马进行处理,而不必等到所有数据都接收完毕才可以处理。好吧,我们暂时不分析这个过程。最简单的,OkHttp使用了Okio进行io。
Okio中提供了读数据类型Source和写数据类型Sink,分别对应原生的InputStream和OutputStream。Okio提供了常用数据读写的处理类,简化了读写操作,并且增加了缓冲区管理,能够更加高效的管理和使用内存。

整个包裹流:
RealBufferedSink(newFixedLengthSink(RealBufferedSink(AsyncTimeout::sink(Sink(socket.outputStream)))))

Okio是高效的io框架,使用Segment结构体保存数据,并且使用缓冲池来缓存Segment减少GC。同时,一个Segment存储使用率大于50%,以保证内存使用效率。

第二站Framework

封装了这么多层,最后调用SocketOutputStream进行写数据操作。在SocketOutputStream中最终调用private native void writeba_native(byte[] b, int off, int len, FileDescriptor fd) throws IOException;进入Native

第三站Native

进入Native,首先获取socket描述符,获取数据所在的数组,再调用socket_write_all,在这个函数中,使用了sendmsg函数进行。

第四站Linux

由于socketfd_file_ops没有定义read操作,所以进入LinuxVFS层后,行为被转发到vfs_read,并进入do_sync_read.
sendmsg是通用的数据读写函数,可以用于跨进程传输数据并且可以在传输过程中使用命令控制传输过程。socket在此处使用此方法,将数据写入Linux内核。

关闭连接


第一站OkHttp

由于OKHttp使用了连接池的概念,所以socket.close的动作由连接池管理,当5分钟没有通信后,连接池就会将连接关闭。最终会调用socket.close()

第二站Framework

AbstractPlainSocketImpl()::close()这里使用了引用计数,当某socket不再被任何对象引用时,才真正执行close操作。close()操作分两步:

  1. 关闭socket,但是不释放文件描述符
  2. 关闭并且释放文件描述符

第三站Native

进入Native层执行socketClose0()
两部释放的具体实现:

  1. 将socket和fd分开untagSocket
  2. closefd

第四站Linux

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 1三个相关数据结构. 关于socket的创建,首先需要分析socket这个结构体,这是整个的核心。 104 str...
    ice_camel阅读 2,808评论 1 8
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,834评论 25 707
  • iPhone的标准推荐是CFNetwork 库编程,其封装好的开源库是 cocoa AsyncSocket库,用它...
    Ethan_Struggle阅读 2,230评论 2 12
  • 最近在学习Python看了一篇文章写得不错,是在脚本之家里的,原文如下,很有帮助: 一、网络知识的一些介绍 soc...
    qtruip阅读 2,692评论 0 6
  • 坐公交车的时候总喜欢看着窗外。 看着那些转瞬即逝的光影。 头微微发晕。 只是忽然。 很想你。 用双手遮住漫天飞扬的...
    yzwjjx阅读 161评论 0 0