分析wget与curl发送web请求方式的区别

首先申明标题党!

前几天,有一张图片访问404(从第三方站点拉去),后来查到原因是下载超过5s(wget带了超时参数--timeout=5),所以下载失败。但是直接访问原图又是非常快,基本感觉不到延时。开始怀疑是服务器网络原因,用host获取该域名的ip地址,无法ping通。

初步结论是:网络原因,无法ping通

服务器可能会设置禁ping,于是我就用wget不带超时下载了一次,发现确实可以下载,只是耗时非常严重,如图:

wget下载耗时

花了20s,才将此图完整下下来。换成curl来下载此图,结果如图:

结果就很明显,curl仅用1s的时间下载此图,而wget却用了20s才做完相同的事儿。我这并不是说curl的性能优于wget啊,换成其他网站url,时间消耗就差不多。那到底是什么原因才导致了如此巨大的差异了,勾起了我的好奇心。

 

用strace、tcpdump分别跟踪了wget和curl,摘取了相对核心的地方来做分析。

wget的strace结果

由于strace的内容比较多比较杂,为了方便分析摘出了比较核心的几处

  1. 11:52:37.877356 execve("/usr/bin/wget", ["wget""http://newpic.jxnews.com.cn/0/11"], [/* 30 vars */]) = 0 
  2.  
  3. 11:52:37.899984 socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 3 
  4. 11:52:37.900043 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, 28) = 0 
  5. 11:52:37.900114 fcntl(3, F_GETFL)       = 0x2 (flags O_RDWR) 
  6. 11:52:37.900165 fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0 
  7. 11:52:37.900215 poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}]) 
  8. 11:52:37.900284 sendto(3, "\353J\1\0\0\1\0\0\0\0\0\0\6newpic\6jxnews\3com\2c"..., 38, MSG_NOSIGNAL, NULL, 0) = 38 
  9. 11:52:37.900773 poll([{fd=3, events=POLLIN}], 1, 5000) = 0 (Timeout) 
  10. 11:52:47.900462 poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}]) 
  11. 11:52:47.900529 sendto(3, "\353J\1\0\0\1\0\0\0\0\0\0\6newpic\6jxnews\3com\2c"..., 38, MSG_NOSIGNAL, NULL, 0) = 38 
  12. 11:52:47.900606 poll([{fd=3, events=POLLIN}], 1, 5000) = 0 (Timeout) 
  13. 11:52:57.900996 close(3)                = 0 
  14.  
  15.  
  16. 11:52:57.901134 socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 3 
  17. 11:52:57.901192 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, 28) = 0 
  18. 11:52:57.901270 fcntl(3, F_GETFL)       = 0x2 (flags O_RDWR) 
  19. 11:52:57.901322 fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0 
  20. 11:52:57.901374 poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}]) 
  21. 11:52:57.901436 sendto(3, "c\237\1\0\0\1\0\0\0\0\0\0\6newpic\6jxnews\3com\2c"..., 50, MSG_NOSIGNAL, NULL, 0) = 50 
  22. 11:52:57.901646 poll([{fd=3, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}]) 
  23. 11:52:57.902350 ioctl(3, FIONREAD, [125]) = 0 
  24. 11:52:57.902421 recvfrom(3, "c\237\201\203\0\1\0\0\0\1\0\0\6newpic\6jxnews\3com\2c"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, [16]) = 125 
  25. 11:52:57.902516 close(3)                = 0 
  26.  
  27.  
  28. 11:52:57.902592 socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 3 
  29. 11:52:57.902647 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, 28) = 0 
  30. 11:52:57.902707 fcntl(3, F_GETFL)       = 0x2 (flags O_RDWR) 
  31. 11:52:57.902757 fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0 
  32. 11:52:57.902808 poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}]) 
  33. 11:52:57.902881 sendto(3, "\343\25\1\0\0\1\0\0\0\0\0\0\6newpic\6jxnews\3com\2c"..., 38, MSG_NOSIGNAL, NULL, 0) = 38 
  34. 11:52:57.903027 poll([{fd=3, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}]) 
  35. 11:52:57.983323 ioctl(3, FIONREAD, [92]) = 0 
  36. 11:52:57.983394 recvfrom(3, "\343\25\201\200\0\1\0\1\0\2\0\0\6newpic\6jxnews\3com\2c"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, [16]) = 92 
  37. 11:52:57.983482 close(3)                = 0 
  38.  
  39.  
  40.  
  41. 11:52:57.983584 write(2, "59.52.28.153", 12) = 12 
  42. 11:52:57.983707 write(2, "\n", 1)       = 1 
  43. 11:52:57.983825 open("/usr/share/locale/zh.GBK/LC_MESSAGES/wget.mo", O_RDONLY) = -1 ENOENT (No such file or directory) 
  44. 11:52:57.983905 open("/usr/share/locale/zh/LC_MESSAGES/wget.mo", O_RDONLY) = -1 ENOENT (No such file or directory) 
  45. 11:52:57.983979 write(2, "Connecting to newpic.jxnews.com."..., 55) = 55 
  46. 11:52:57.984073 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3 
  47. 11:52:57.984139 connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("59.52.28.153")}, 16) = 0 
  48. 11:52:58.045641 write(2, "\322\321\301\254\275\323\241\243\n", 9) = 9 
  49. 11:52:58.045760 select(4, NULL, [3], NULL, {900, 0}) = 1 (out [3], left {900, 0}) 
  50. 11:52:58.045832 write(3, "GET /0/11/84/34/11843462_991618."..., 155) = 155 

在11:52:37这一刻,执行wget命令。第一个较为重要的动作是做dns解析,向域名服务器10.55.21.254发起了两次数据包的长度是38字节查询请求,得到两次timeout,在11:52:57正式关闭了请求。

系统尝试向dns服务器10.55.21.254发送长度为50字节的数据包的查询请求,并获得返回结果(这里无法知道获知返回结果)。

系统在获得返回结果后,第四次向10.55.21.254发送请求,获得返回结果后,得到url的ip地址,发起了http请求。

 

通过已经分析,可以得到几个结论:

1、对域名解析方式上的差异导致wget和curl的耗时差别巨大

2、第三次改变数据包的查询,虽然获取得了返回,但并没有获取域名的ip

 

wget的tcpdump结果

tcpdump的结果图:

相对于strace的日志,tcpdump的就比较干净,为了方便分析,还是摘除了关键部分

  1. 11:52:37.900351 IP wapcms-169.54429 > dns.domain:  60234+ AAAA? newpic.jxnews.com.cn. (38) 
  2. 11:52:47.900568 IP wapcms-169.54429 > dns.domain:  60234+ AAAA? newpic.jxnews.com.cn. (38) 
  3.  
  4. 11:52:57.901476 IP wapcms-169.46106 > dns.domain:  25503+ AAAA? newpic.jxnews.com.cn.localdomain. (50) 
  5. 11:52:57.902233 IP dns.domain > wapcms-169.46106:  25503 NXDomain 0/1/0 (125) 
  6.  
  7. 11:52:57.902924 IP wapcms-169.58634 > dns.domain:  58133+ A? newpic.jxnews.com.cn. (38) 
  8. 11:52:57.983135 IP dns.domain > wapcms-169.58634:  58133 1/2/0 A 59.52.28.153 (92) 

很清晰的与strace的结果对应上了,两次38字节的数据包timeout,一次50字节的数据包,获得返回

NXDomain 0/1/0 (125),一次38字节的请求获得域名的ip地址:59.52.28.153

 

curl的strace结果

 

  1. execve("/usr/bin/curl", ["curl""http://newpic.jxnews.com.cn/0/11""-o""1.jpg"], [/* 31 vars */]) = 0 
  2.         = 0 
  3. socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 3 
  4. connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, 28) = 0 
  5. fcntl(3, F_GETFL)                       = 0x2 (flags O_RDWR) 
  6. fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK)    = 0 
  7. poll([{fd=3, events=POLLOUT}], 1, 0)    = 1 ([{fd=3, revents=POLLOUT}]) 
  8. sendto(3, "\370T\1\0\0\1\0\0\0\0\0\0\6newpic\6jxnews\3com\2c"..., 38, MSG_NOSIGNAL, NULL, 0) = 38 
  9. poll([{fd=3, events=POLLIN}], 1, 5000)  = 1 ([{fd=3, revents=POLLIN}]) 
  10. ioctl(3, FIONREAD, [92])                = 0 
  11. recvfrom(3, "\370T\201\200\0\1\0\1\0\2\0\0\6newpic\6jxnews\3com\2c"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, [16]) = 92 
  12. close(3)   
  13.                               = 0 
  14. socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3 
  15. fcntl(3, F_GETFL)                       = 0x2 (flags O_RDWR) 
  16. fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK)    = 0 
  17. connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("120.203.216.133")}, 16) = -1 EINPROGRESS (Operation now in progress) 
  18. poll([{fd=3, events=POLLOUT}], 1, 300000) = 1 ([{fd=3, revents=POLLOUT}]) 
  19. getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0 
  20. sendto(3, "GET /0/11/84/34/11843462_991618."..., 194, MSG_NOSIGNAL, NULL, 0) = 194 

很明显的发现,curl通过一次dns查询就获取了url的ip地址,然后就发起http请求。

 

curl的tcpdump结果

 

  1. 15:19:39.744520 IP wapcms-169.48990 > dns.domain:  63572+ A? newpic.jxnews.com.cn. (38) 
  2. 15:19:39.744905 IP wapcms-169.38502 > dns.domain:  13836+ PTR? 254.21.55.10.in-addr.arpa. (43) 
  3. 15:19:39.745626 IP dns.domain > wapcms-169.38502:  13836* 1/1/0 (86) 
  4. 15:19:39.793797 IP dns.domain > wapcms-169.48990:  63572 1/2/0 A 120.203.216.133 (92) 
  5. 15:19:39.793890 IP wapcms-169.31975 > dns.domain:  54867+ PTR? 133.216.203.120.in-addr.arpa. (46) 
  6. 15:19:39.848460 IP dns.domain > wapcms-169.31975:  54867 NXDomain 0/1/0 (102) 

 

 

从上面基本上可以得到结论,wget与curl在域名解析上有很大的不同。

 

通过查阅dns协议和tcpdump的输出数据格式得到结论是:

wget兼容IPv6,而curl不兼容,该url的dns解析20s的时间发生在解析IPv6上

引申:
host命令兼容IPv6 dig不兼容 (均为不带任何附加参数)

 追加证明:

wget强制使用IPv4进行dns的结果如图:

 

 

注:

dns协议见附件,相关附带知识如下:

tcpdupm中dns数据包格式

域名服务请求的格式是

src>dst:idop?flagsqtypeqclassname(len)

h2opolo.1538>helios.domain:3+A?ucbvax.berkeley.edu.(37)

主机h2opolo访问helios上的域名服务,询问和ucbvax.berkeley.edu.关联的地址记录(qtype=A).查询号是 `3'.` +'表明设置了递归请求标志.查询长度是37字节,不包括UDP和IP头.查询操作是普通的Query操作,因此op域可以忽略.如果op设置成其他什么 东西,它应该显示在`3'和`+'之间.类似的,qclass是普通的C_IN类型,也被忽略了.其他类型的qclass应该在`A'后面显示.
Tcpdump 会检查一些不规则情况,相应的结果作为补充域放在方括号内:如果某个查询包含回答,名字服务或管理机构部分,就把ancount,nscount,或 arcount显示成`[na]',`[nn]'或`[nau]',这里的n代表相应的数量.如果在第二和第三字节中,任何一个回答位(AA,RA或 rcode)或任何一个`必须为零'的位被置位,就显示`[b2&3=x]',这里的x是报头第二和第三字节的16进制数.

UDP名字服务回答

名字服务回答的格式是

src>dst:idoprcodeflagsa/n/autypeclassdata(len)

helios.domain>h2opolo.1538:33/3/7A128.32.137.3(273)
helios.domain>h2opolo.1537:2NXDomain*0/1/0(97)


第一个例子里,helios回答了h2opolo发出的标识为3的询问,一共是3个回答记录,3个名字服务记录和7个管理结构记录.第一个回答纪录的类型是 A(地址),数据是internet地址128.32.137.3.回答的全长为273字节,不包括UDP和IP报头.作为A记录的class (C_IN)可以忽略op(询问)和rcode(NoError).
在第二个例子里,helios对标识为2的询问作出域名不存在(NXDomain)的回答,没有回答记录,一个名字服务记录,而且没有管理结构.
`*'表明设置了权威回答(authoritativeanswer).由于没有回答记录,这里就不显示type,class和data.

其他标志字符可以显示为`-'(没有设置递归有效(RA))和`|'(设置消息截短(TC)).如果`问题'部分没有有效的内容,就显示`[nq]'.

注意名字服务的询问和回答一般说来比较大,68字节的snaplen可能无法捕捉到足够的报文内容.如果你的确在研究名字服务的情况,可以使用-s选项增大捕捉缓冲区.`-s128'应该效果不错了.

 

IPv6的正向解析与反向解析

a. 正向解析  

    IPv4的地址正向解析的资源记录是“A”记录。IPv6地址的正向解析目前有两种资源记录,即,“AAAA”和“A6”记录。其中, “AAAA”较早提出<4>,它是对“A”记录的简单扩展,由于IP地址由32位扩展到128位,扩大了4倍,所以资源记录由“A”扩大成4 个“A”。“AAAA”用来表示域名和IPv6地址的对应关系,并不支持地址的层次性。

   “A6”在RFC2874<5>中提出,它是把一个IPv6地址与多个“A6”记录建立联系,每个“A6”记录都只包含了IPv6地址的一部分,结合后拼装成一个完整的IPv6地址。“A6”记录支持一些“AAAA”所不具备的新特性,如地址聚合,地址更改(Renumber)等。

   首先,“A6”记录方式根据TLA、NLA和SLA的分配层次把128位的IPv6的地址分解成为若干级的地址前缀和地址后缀,构成了一个地址链。每个地址前缀和地址后缀都是地址链上的一环,一个完整的地址链就组成一个IPv6地址。这种思想符合IPv6地址的层次结构,从而支持地址聚合。

   其次,用户在改变ISP时,要随ISP改变而改变其拥有的IPv6地址。如果手工修改用户子网中所有在DNS中注册的地址,是一件非常繁琐的事情。而在用“A6”记录表示的地址链中,只要改变地址前缀对应的ISP名字即可,可以大大减少DNS中资源记录的修改。并且在地址分配层次中越靠近底层,所需要改动的越少。  

b. 反向解析  

     IPv6反向解析的记录和IPv4一样,是“PTR”,但地址表示形式有两种。一种是用 “.”分隔的半字节16进制数字格式(Nibble Format),低位地址在前,高位地址在后,域后缀是“IP6.INT.”。另一种是二进制串(Bit-string)格式,以“\<”开头, 16进制地址(无分隔符,高位在前,低位在后)居中,地址后加“>”,域后缀是“IP6.ARPA.”。半字节16进制数字格式与“AAAA”对应,是对IPv4的简单扩展。二进制串格式与“A6”记录对应,地址也象“A6”一样,可以分成多级地址链表示,每一级的授权用“DNAME”记录。和 “A6”一样,二进制串格式也支持地址层次特性。

     总之,以地址链形式表示的IPv6地址体现了地址的层次性,支持地址聚合和地址更改。但是,由于一次完整的地址解析分成多个步骤进行,需要按照地址的分配层次关系到不同的DNS服务器进行查询。所有的查询都成功才能得到完整的解析结果。这势必会延长解析时间,出错的机会也增加。因此,需要进一步改进DNS地址链功能,提高域名解析的速度才能为用户提供理想的服务。

 

 



 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章