记得很久看到过一篇博客《从输入网址到网页出现在浏览器中都发生了什么》(名字太长,现在已经记不得了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():
安全性检查
对于type和family进行判断和重新赋值
根据不同的协议调用不同的create函数,返回sock结构体。这里就调用inet_create函数。
sock结构体保存了大量的信息,定义了TCP传输中的大量参数,包括发送缓冲区,接收缓冲区,计时器,标志位,状态,引用计数等等。创建inode,inode是Linux文件系统中的节点。
获得本进程的一个空闲的文件描述符,申请一个新的目录项,将文件操作的函数指针设置到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()操作分两步:
- 关闭socket,但是不释放文件描述符
- 关闭并且释放文件描述符
第三站Native
进入Native层执行socketClose0()
两部释放的具体实现:
- 将socket和fd分开
untagSocket
closefd