当往浏览器地址栏输入一串地址敲下回车键,浏览器就会给我们展示出一个可视化的页面。看似很简单的操作,但背后凝聚了多年来IT人员的辛苦耕耘。我上家公司里经常喜欢拿这个问题来当作面试题,现在我是没有这个当面试官的机会了。
一次请求大致过程包括:域名解析 --> 发起TCP的3次握手 --> 建立TCP连接后发起http请求 --> 服务器响应http请求 --> 浏览器得到html代码 --> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) --> 浏览器对页面进行渲染呈现给用户
当访问一个域名地址时,浏览器首先要把域名解析成公网IP地址,这一步是通过DNS来解析,会访问DNS服务商查找域名对应的IP,查找到后会把结果缓存下来,后面对该域名的访问就不再进行DNS解析,浏览器第一次打开一个网站时会比较慢这个DNS初次解析是慢的原因之一。当然浏览器会先检查缓存中域名对应的IP有没有,然后检查本地的hosts有没有配置该域名,都没有的话才会去访问DNS服务商。如果是内部网络需要通过网关才能上网的,这里还可能会发生一次ARP广播查找网关机器。当同一子网内直接通过IP通信时也需要ARP广播来找到目的机器的Mac地址。
拿到IP之后,客户端首先要通过TCP三次握手来建立连接, TCP协议会对请求数据包进行封装并由IP协议进行传输,ICMP协议进行控制,中间会经过不同的路由器最终到达目的主机的网卡接口,这中间的过程我是讲不明白,也不太清楚,省略。
说说TCP三次握手吧,TCP协议通过三次握手建立一个可靠的连接,TCP是IP的上层协议,反正IP层是并不知道什么三次握手四次挥手,IP只管运输数据。第一次握手:Client首先发送一个连接试探,ACK=0 表示确认号无效,SYN = 1 表示这是一个连接请求或连接接受报文,同时表示这个数据报不能携带数据,seq = x 表示Client自己的初始序号(seq = 0 就代表这是第0号包),这时候Client进入syn_sent状态,表示客户端等待服务器的回复。第二次握手:Server监听到连接请求报文后,如同意建立连接,则向Client发送确认。TCP报文首部中的SYN 和 ACK都置1 ,ack = x + 1表示期望收到对方下一个报文段的第一个数据字节序号是x+1,同时表明x为止的所有数据都已正确收到(ack=1其实是ack=0+1,也就是期望客户端的第1个包),seq = y 表示Server 自己的初始序号(seq=0就代表这是服务器这边发出的第0号包)。这时服务器进入syn_rcvd,表示服务器已经收到Client的连接请求,等待client的确认。第三次握手:Client收到确认后还需再次发送确认,同时携带要发送给Server的数据。ACK 置1 表示确认号ack= y + 1 有效(代表期望收到服务器的第1个包),Client自己的序号seq= x + 1(表示这就是我的第1个包,相对于第0个包来说的),一旦收到Client的确认之后,这个TCP连接就进入Established状态,完成三次握手,就可以发起http请求,发送正常的请求数据包。TCP为什么要进行三次握手呢,大致讲就是为了确认双方机器是否正常,网络通不通,通信协议是否支持。
IP协议只认IP地址,TCP在IP之上添加了端口,通过端口来区分不同的应用程序。当然我们并不是直接操作TCP/IP的,它之上还抽象了一个Socket层,HTTP也是建立在Socket之上进行通信。一个socket是由一个五元组来唯一标示的,即(协议,server_ip, server_port, client_ip, client_port)。只要该五元组中任何一个值不同,则其代表的socket就不同。Socket对外抽象出了bind,listen,accept以及send,write等几个基本的操作。就跟常见的文件操作一样(在Linux看来,什么都可以是文件)。
服务端首先需要帮定端口并监听请求 new ServerSocket(80).accept() ,这里帮定了80端口后,其它应用程序就不能再使用该端口了,一个指定的端口号不能被多个程序共用。这其实是向TCP/IP协议栈声明了其对80端口的占有,以后,所有目标是80端口的TCP数据包都会转发给该程序。客户端会以一个随机端口(大于1024并小于65535)向服务器的WEB程序(如nginx)80端口发起TCP的连接请求,由accept接收。所谓accept函数,其实抽象的是TCP的连接建立过程,当客户端有一个新的请求过来后,accept函数返回的新socket其实指代的是本次创建的连接,accept可以产生多个不同的socket,这个socket跟文件句柄很相似,可以认为它是用来区分不同的连接请求然后回调不同的处理程序。创建的socket中包含了客户端的IP及Port、服务端的IP及Port,这其中服务端的IP及Port都是一样的。服务端Socket虽然只占用了一个端口,但accept创建的Socket在Linux中也是一种特殊的文件,自然也受到Linux文件描述符的限制。建立连接也并不是真在存在这样一条连着的连接。
Socket在Linux上还涉及到epoll,它是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
建立连接后就开始进行HTTP请求报文和响应报文流程,HTTP协议报文是有它特定的格式,关于报文格式一篇文章也讲不下,我也不是很清楚所有报文内容。目前我们使用最流行是HTTP协议是1.1版本,它有更多的请求方法,更精细的缓存控制,持久连接支持。一个WEB站点每天可能要接收到上百万的用户请求,为了提高系统的效率,HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。但是,这也造成了一些性能上的缺陷。为了克服HTTP 1.0的这个缺陷,HTTP 1.1支持持久连接,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接。HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个下载过程所需要的时间。浏览器第一次访问一个网页,首先要建立一个连接需要三次握手,建立连接后也不是一股脑把所有请求都发出去,而是先发一个请求,成功返回后再发起两次请求,然后再是四次请求,这个也是导致第一次访问页面比较慢的一个小原因。
请求到达服务器之后,如果有Http Server,则由Http Server转发请求,如果没有就是直接请求Tomcat之类的应用服务器,由它生成响应内容。响应的内容有可能是HTML,也有可能是JSON数据。在这其中也牵扯到负载均衡、集群、不同级别的缓存、消息处理、数据库操作等等。这里面的东西够写N篇文章,此处省略。
浏览器拿到html文件后,就开始解析其中的html代码,遇到js/css/image等静态资源时,就向服务器端去请求下载(会使用多线程下载,每个浏览器的线程数不一样)。浏览器在请求静态资源时(在未过期的情况下),向服务器端发起一个http请求(询问自从上一次修改时间到现在有没有对资源进行修改),如果服务器端返回304状态码(告诉浏览器服务器端没有修改),那么浏览器会直接读取本地的该资源的缓存文件。
很多大型网站在一个页面中会使用多个不同的域名,这里的主要原因有:1、CDN缓存更方便。2、突破浏览器并发限制,像地图之类的需要大量并发下载图片的站点,这个非常重要,另一个重要因素是节约主域名的连接数,说起来就是分流。3、Cookieless,节省带宽,尤其是上行带宽一般比下行要慢,像主站用户的每次访问,都会带上自己的cookie,挺大的。假如twitter的图片放在主站域名下,那么用户每次访问图片时,request header里就会带有自己的cookie,header里的cookie还不能压缩,而图片是不需要知道用户的cookie,所以这部分带宽就白白浪费了。4、对于UGC的内容和主站隔离,防止不必要的安全问题(上传js窃取主站cookie之类的),正是这个原因要求用户内容的域名不是自己主站的子域名,而是一个完全独立的第三方域名。5、数据做了划分,甚至切到了不同的物理集群,通过子域名来分流比较省事,这个在分系统的时候用的比较多。最后,多域名也不是越多越好,虽然服务器端可以做泛解释,浏览器做dns解释也是耗时间,而且太多域名,如果要走https的话,还有要多买证书和部署的问题。
如果静态资源使用了CDN,则会向CDN请求静态资源。CDN即内容分布网络(Content Delivery Network),它是构筑在现有Internet上的一种先进的流量分配网络。其目的是通过现有的Internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。
浏览器利用自己内部的工作机制,把请求到的静态资源和html代码进行渲染,渲染之后呈现给用户。自此一次完整的HTTP事务宣告完成。
来个题外话,服务器单机最大连接数问题。TCP的端口数最大值为65535,但单个服务器程序可承受最大连接数和这个没有关系,因为服务端实际上只使用到了一个端口。Linux上连接并发数的限制有:可打开文件数限制、内存容量、CPU资源。上面也讲到了一个连接就是一个Socket,一个Socket就是一个打开着的文件。每个连接都要消耗内存,在Linux Epoll下并不是一个连接对应一个线程,而是复用线程。但在Tomcat中一个连接对应一个操作系统线程,线程共享进程的堆空间,但每个线程都有自己的栈空间,一个栈要在内核区及用户区分别各占一块内存。线程和进程一样都是可以使用CPU分片时间,如果并发请求太高,则会导致过多的线程以及网络中断来消耗CPU资源,这个时会产生大量的上下文切换(内核态及用户态切换),这会使CPU大量消耗在任务调度上而不是实际的业务处理中。以及间接导致CPU缓存命中下降及失效问题。也正是这个原因,所以一般都是一台Nginx后面挂了一群Tomcat,Nginx进程个数一般都是CPU逻辑个数(或是CPU个数减1,为了和网络中断错开),避免上下文切换。