Web客户端和服务器之间使用HTTP协议通信。
我们按以下方式来部署通信实例:在Kongming20上运行wget客户端程序(一个在命令行下使用的网络下载工具,它支持通过HTTP、HTTPS和FTP协议下载文件),在ernest-laptop上运行squid代理服务器程序(主要用于缓存和转发网络请求,从而提高网络性能、安全性和可用性),客户端通过代理服务器的中转,获取Internet上的主机www.baidu.com的首页文档index.html:
由上图,wget客户和代理服务器之间,以及代理服务器与Web服务器之间都是使用HTTP协议通信的。HTTP是一种应用层协议,它默认使用的传输层协议是TCP。
为了将ernest-laptop设置为Kongming20的HTTP代理服务器,我们需要在Kongming20上设置环境变量http_proxy:
http_proxy环境变量通常用于配置代理服务器的设置,以便在发出HTTP请求时通过代理服务器进行连接。
上图中3128是squid服务器默认使用的端口号(可通过lsof命令查看服务器程序监听的端口号)。设置好环境变量后,Kongming20访问任何Internet上的Web服务器时,其HTTP请求都将首先发送至ernest-laptop的3128端口。
squid代理服务器接收到wget客户端的HTTP请求后,将简单地修改这个请求,然后把它发给最终的目标Web服务器。代理服务器访问的是Internet上的机器,因此它发送的IP数据报都将经过路由器的中转,这一点在图4-1中也有体现。
在HTTP通信链上,客户和目标服务器之间通常存在某些中转代理服务器,他们提供对目标资源的中转访问。一个HTTP请求可能被多个代理服务器转发,后面的服务器称为前面服务器的上游服务器。代理服务器可分为正向代理服务器、反向代理服务器、透明代理服务器。
正向代理要求客户端自己设置代理服务器的地址。客户的每次请求都将直接发送到该代理服务器,并由代理服务器来请求目标资源。比如处于防火墙内的局域网机器要访问Internet,或要访问一些被屏蔽掉的国外网站,就需要使用正向代理服务器。
反向代理则被设置在服务器端,因此客户端无须进行任何设置。反向代理指用代理服务器接收Internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从内部服务器上得到的结果返回给客户端。此时,代理服务器对外就表现为一个真实的服务器。各大网站通常分区域设置多个代理服务器,所以在不同的地方ping同一个域名可能得到不同的IP地址,这些IP地址实际是不同的代理服务器的IP地址。
上图中,正向代理服务器和客户端处于同一个逻辑网络中,该逻辑网络可以是一个本地LAN,也可以是一个更大的网络。反向代理服务器和真正的Web服务器也位于同一个逻辑网络中,这通常由提供网站的公司来配置和管理。
透明代理只能设置在网关上。用户访问Internet的数据报都必然经过网关,如果在网关上设置代理,则该代理对用户来说是透明的。透明代理可看作正向代理的一种特殊情况。
代理服务器通常还提供缓存目标资源的功能(可选),这样用户下次访问同一资源时速度将很快。优秀的开源软件squid、varnish都是提供了缓存能力的代理服务器软件,其中squid支持所有代理方式,而varnish仅能用作反向代理。
我们在ernest-laptop上部署squid代理服务器,只需修改squid服务器的配置文件/etc/squid3/squid.conf,在其中加入下面两行代码(需要root权限,且应该加在合适的位置):
这两行的含义是:允许网络192.168.1.0上的所有机器通过该代理服务器来访问Web服务器。其中192.168.1.0/24
是CIDR(Classless Inter-Domain Routing,无类域间路由)风格的IP地址表示方法,/
前的部分指定网络的IP地址,/
后的部分指定子网掩码中1的位数,它等价于192.168.1.0/255.255.255.255
(IP地址/子网掩码)。
上图中acl localnet src 192.168.1.0/24
定义了一个名为localnet的访问控制列表(ACL,Access Control List,ACL是用于指定允许或拒绝访问的规则集合)。在这里,ACL定义了一个IP地址范围,即192.168.1.0/24,表示从192.168.1.0到192.168.1.255的所有IP地址。
上图中http_access allow localnet
配置了HTTP访问规则。它指定了允许名为localnet的ACL中定义的IP地址范围的客户端访问代理服务器上的HTTP资源。换句话说,只有来自192.168.1.0/24 IP地址范围的客户端才被允许通过代理服务器访问HTTP资源。
上图简单地配置了squid的访问控制,实际应用中,会使用squid提供的更多、更安全的配置,如用户验证等。
在ernest-laptop上执行以下命令,重启squid服务器:
上图中,service是一个脚本程序(/usr/sbin/service),它为/etc/init.d目录下的众多服务器程序(如httpd、vsftpd、sshd、mysqld)等的启动、停止、重启等动作提供了一个统一的管理。现在,Linux程序员越来越倾向于使用service脚本来管理服务器程序了。
执行wget命令前,我们先删除ernest-laptop的ARP高速缓存中路由器对应的项,以便观察TCP/IP通信过程中ARP协议何时起作用,然后使用tcpdump命令抓取整个通信过程中传输的数据包,完整的操作过程如下:
arp命令的-d选项可删除arp高速缓存中指定IP对应的项。tcpdump命令的-s选项指定所捕获数据包的最大大小,超出部分被丢弃;-X选项以十六进制和ASCII码的形式同时显示数据包的内容。wget命令的–header选项在HTTP请求头中添加自定义的HTTP标头(headers),上例中添加的标头含义是客户端希望服务器在响应请求后立即关闭连接。
wget命令的输出显示,HTTP请求先被送到代理服务器的3128端口,且代理服务器正确地返回了文件index.html的内容。
上图通信过程的tcpdump输出如下:
我们一共抓取了43个数据包,这些数据包是两对客户和服务器(wget客户和代理服务器,代理服务器与目标Web服务器)之间通信的全部内容,以上输出把这两组通信的内容放到了一起。为了讨论方便,我们将这43个数据包分为以下4部分:
1.代理服务器访问DNS服务器以查询域名www.baidu.com对应的IP地址,包括数据包8、9。
2.代理服务器查询路由器MAC地址的ARP请求和应答,包括数据包6、7。
3.wget客户端(192.168.1.109)和代理服务器(192.168.1.108)之间的HTTP通信,包括数据包1~5、23~25、32~40、42、43。
4.代理服务器和Web服务器(119.75.218.77)之间的HTTP通信,包括数据包10~22、26~31、41。
数据包8、9表示代理服务器ernest-laptop向DNS服务器(219.239.26.42,首选DNS服务器的IP地址)查询域名ww.baidu.com对应的IP地址,并得到了回复,该回复包括主机规范名(www.a.shifen.com.)和两个IP地址(119.75.218.77和119.75.217.56)。代理服务器执行DNS查询的过程如下:
squid程序通过读取/etc/resolv.conf文件获得DNS服务器的IP地址,然后将控制权传递给内核中的UDP模块。UDP模块将DNS查询报文封装成UDP数据报,同时把源端口号和目标端口号加入UDP数据报头部,然后UDP模块调用IP服务。IP模块将UDP数据报封装成IP数据报,并把源端IP(192.168.1.108)和DNS服务器的IP加入IP数据报头部。接下来,IP模块查询路由表以决定如何发送该IP数据报。根据路由策略,目标地址(219.239.26.42)仅能匹配路由表中的默认路由项,因此该IP数据报先被发送到路由器(IP地址为192.168.1.1),然后通过路由器来转发。因为ernest-laptop的ARP缓存中没有与路由器对应的缓存项(我们手动将其删除了),所以ernest-laptop需要发起一个ARP广播以查询路由器的IP地址,这正式数据包6的内容。路由器通过ARP应答告诉ernest-laptop自己的MAC地址是14:e6:e4:93:5b:78,如数据包7所示。最终,ernest-laptop的以太网驱动程序将IP数据报封装成以太网帧发送给路由器。此后,代理服务器再次发送数据到Internet时不再需要ARP查询,因为ernest-laptop的ARP高速缓存中已经记录了路由器的IP地址和MAC地址的映射关系。
虽然IP数据报是先发送到路由器,再由它转发给目标主机,但其头部的目标IP地址却是最终的目标主机(DNS服务器)的IP地址,而不是中转路由器的IP地址,这说明,IP头部的源端IP地址和目的端IP地址在转发过程中是始终不变的(一种例外是源路由选择)。但帧头部的源端物理地址和目的端物理地址在转发过程中则是一直在变化的。
一般,通过域名访问Internet上的某台主机时,需要使用DNS服务来获取该主机的IP地址,如果我们通过主机名来访问本地局域网上的机器,则可通过本地的静态文件来获得该机器的IP地址。
Linux将目标主机名及其对应的IP地址存储在/etc/hosts配置文件中,当需要查询某个主机名对应的IP地址时,程序先检查这个文件。Kongming20上/etc/hosts文件的内容如下,该内容作者修改过:
其中第一项指出本地回路地址127.0.0.1的名称是localhost,第二项和第三项分别描述了Kongming20和ernest-laptop的IP地址及对应的主机名。
代码清单4-1中,wget命令输出Resolving ernest-laptop... 192.168.1.108
,说明它成功解析了主机名ernest-laptop对应的IP地址。当wget访问某个Web服务器时,它先读取环境变量http_proxy,如果该环境变量被设置,且我们没有阻止wget使用代理服务,则wget将通过http_proxy指定的代理服务器来访问Web服务,但http_proxy环境变量中包括主机名ernest-laptop,因此wget首先读取/etc/hosts配置文件,试图通过它来解析主机名ernest-laptop对应的IP地址,其结果正如wget的输出所示,解析成功。
如果程序在/etc/hosts文件中未找到目标机器名对应的IP地址,它将求助于DNS服务。
用户可通过修改/etc/host.conf文件来自定义系统解析主机名的方法和顺序(一般是先访问本地文件/etc/hosts,再访问DNS服务),Kongming20上的该文件内容为:
第一行表示优先使用/etc/hosts文件来解析主机名(hosts),失败后再使用DNS服务(bind)。第二行表示如果/etc/hosts文件中一个主机名对应多个IP地址,那么解析的结果就包含多个IP地址。/etc/host.conf文件通常仅包含这两行,但它支持更多选项,具体可参考其man手册。
RFC 1123指出,网络上的主机都应该实现一个简单的本地名称查询服务。
wget客户端和代理服务器时间的通信TCP时序图如下:
由上图,TCP连接从建立到关闭的过程中,客户端仅给服务器发送了一个HTTP请求(报文段4),该请求长为136字节(见代码清单4-2中报文段4的length值)。代理服务器用6个报文段(23、24、25、33、35、36)给客户端返回了总长度为8522字节(可从最后一个确认报文段42中的确认值计算得到,该值为8524,减去各1字节的SYN和FIN得到)的HTTP应答。客户端使用了7个TCP报文段(32、34、37、38、39、40、42)来确认这8522字节的HTTP应答数据。
由于我们开启了tcpdump的-X选项,因此可查看具体的HTTP请求和应答内容,请求内容如下:
上图中第一行是请求行,其中GET是请求方法,表示客户端以只读方式来申请资源。常见的HTTP请求方法见下图:
以上请求方法中,HEAD、GET、OPTIONS、TRACE被视为安全的方法,因为它们只是从服务器获得资源或信息,而不对服务器进行任何修改。而POST、PUT、DELETE、PATCH则影响服务器上的资源。
请求方法GET、HEAD、OPTIONS、TRACE、PUT、DELETE被认为是等幂的(idempotent),即多次连续的、重复的请求和只发送一次该请求具有完全相同的效果。而POST方法则不同,连续多次发送同样一个请求可能进一步影响服务器上的资源。
Linux提供了几个命令:HEAD、GET、POST,其含义与HTTP协议中的同名请求方法相同,适合用来快速测试Web服务器。
http://www.baidu.com/index.html
是目标资源的URL,其中http是所谓的scheme,表示获取目标资源需要使用的应用层协议,其他常见的scheme还有ftp、rtsp(Real-Time Streaming Protocol,实时流媒体传输协议)、file(用于在本地文件系统中访问文件的URL协议)等。www.baidu.com
指定资源所在的目标主机。index.html
指定资源文件的名称,这里指的是服务器根目录(站点的根目录,而不是服务器的文件系统根目录)中的索引文件。
HTTP/1.0
表示客户端(wget程序)使用的HTTP版本号是1.0,目前的主流HTTP版本是1.1。
HTTP请求内容中的第2~4行时HTTP请求的头部字段,一个HTTP请求可以包含多个头部字段,一个头部字段用一行表示,包括字段名、冒号、空格、字段的值。HTTP请求中的头部字段可按任意顺序排列。
User-Agent: Wget/1.12(linux-gnu)
表示客户端使用的程序时wget。
Host: www.baidu.com
表示目标主机名是www.baidu.com。HTTP协议规定HTTP请求中必须包含的头部字段就是目标主机名。
Connection: close
是我们执行wget命令时传入的,用来告诉服务器处理完这个HTTP请求后就关闭连接。在旧的HTTP协议中,Web客户端和Web服务器之间的一个TCP连接只能为一个HTTP请求服务,当处理完客户的一个HTTP请求后,Web服务器就主动将TCP连接关闭了,此后,同一客户如果再发送一个HTTP请求,就必须与服务器建立一个新的TCP连接,即同一个客户的多个连续的HTTP请求不能共用一个TCP连接,这称为短连接。长连接指多个请求可以使用同一个TCP连接,长连接在编程上稍微复杂一些,但性能上却有很大提高,它极大减少了网络上为建立TCP连接导致的负荷,同时对于每次请求而言缩减了处理时间。HTTP请求和应答中的Connection头部字段就是专门用于告诉对方一个请求完成后该如何处理连接的,如立即关闭连接(对应的值为close),或保持一段时间以等待后续请求(对应的值为keep-alive)。当用浏览器访问一个网页时,可用netstat命令查看一下浏览器和Web服务器之间的连接是否是长连接,以及该连接保持了多久。
在所有头部字段后,HTTP请求必须包含一个空行,以标识头部字段的结束。请求行和每个头部字段都必须以<CR><LF>
(回车符和换行符)结束,而空行则必须只包含一个<CR><LF>
,不能有其他字符,甚至是空白字符。
在空行后,HTTP请求可包含可选的消息体,如果消息体非空,则HTTP请求的头部字段必须包含描述该消息体长度的Content-Length字段。上例只是为了获取目标服务器上的资源,所以没有消息体。
HTTP应答部分内容如下:
上图中第一行是状态行。HTTP/1.0是服务器使用的HTTP协议的版本号,通常,服务器需要使用和客户端相同的HTTP协议版本。200 OK
是状态码和状态信息,常见的状态码和状态信息见下表:
应答的第2~7行是HTTP应答的头部字段,其表示方法和HTTP请求中的头部字段相同。
Server: BWS/1.0
表示Web服务器程序的名字是BWS(Baidu Web Server)。
Content-Length: 8024
表示目标文档的长度为8024字节,这个值和wget输出的文档长度一致。
Content-Type: text/html;charset =gbk
表示目标文档的MIME类型,其中text是主文档类型,html是子文档类型,text/html表示目标文档index.html是text类型中的html文档。charset是text文档类型的一个参数,用于指定文档的字符编码。
Set-Cookie: BAIDUID=A5B6C72D68CF639CE8896FD79A08FBD8:FG=1; expires=Wed,04 -Jul-42 00:10:47 GMT; path=/; domain=.baidu.com
表示服务器传送一个Cookie给客户端,其中BAIDUID指定Cookie的名字,expires指定Cookie的生存时间,domain和path指定该Cookie生效的域名和路径。
HTTP是一种无状态协议,即每个HTTP请求之间没有任何上下文关系,如果服务器处理后续HTTP请求时需要用到前面的HTTP请求的相关信息,客户端必须重传这些信息,这样就导致HTTP请求必须传输更多的数据。
在交互式Web应用程序兴起后,HTTP协议的这种无状态特性不适应交互式Web应用,因为交互程序通常需要上下文信息,因此我们需要使用额外的手段保持HTTP连接的状态,常见的解决方法就是Cookie,Cookie是服务器发送给客户端的特殊信息(通过HTTP应答的头部字段Set-Cookie),客户每次向服务器发送请求时都要带上这些信息(通过HTTP请求的头部字段Cookie),这样服务器就能区分不同客户了。基于浏览器的自动登录就是用Cookie实现的。
Via: 1.0 localhost(squid/3.0 STABLE181
表示HTTP应答在返回过程中经历过的所有代理服务器的地址和名称,这里的localhost指192.168.1.108,这个头部字段的功能类似IP协议的记录路由功能。
在所有头部字段后,HTTP应答必须包含一个空行,以标识头部字段的结束。状态行和每个头部字段都必须以<CR><LF>
结束,而空行则必须只包含一个<CR><LF>
,不能有其他字符,甚至是空白字符。
空行之后是被请求文档index.html的内容,其长度为8024字节。