本文主要讲述了一个 http request 请求从发出到收到 response 的整个生命周期,希望可以通过对整个流程的一个描述来梳理清楚五层网络协议的定义以及各层之间是如何协作的。
对于后端来说通过 http 请求来进行远程调用是再寻常不过的事了,以 Golang 的 resty 包为例,我们通过下面这个语句来发起一个请求并获得所请求的服务器的 response,简单起见这里我们使用 GET 方法进行请求:
client := resty.New()
resp1, _ := client.R().
EnableTrace().
SetHeaders(headers).
Get("https://httpbin.org/get")
fmt.Println("Response Info:")
fmt.Println(" Status :", resp1.Status())
fmt.Println(" Status Code:", resp1.StatusCode())
fmt.Println(" Proto :", resp1.Proto())
fmt.Println(" Body :\n", resp1)
fmt.Println()
// Explore trace info
fmt.Println("Request Trace Info:")
ti := resp1.Request.TraceInfo()
fmt.Println(" DNSLookup :", ti.DNSLookup)
fmt.Println(" TCPConnTime :", ti.TCPConnTime)
fmt.Println(" TLSHandshake :", ti.TLSHandshake)
fmt.Println(" IsConnReused :", ti.IsConnReused)
fmt.Println(" RemoteAddr :", ti.RemoteAddr.String())
我们在应用层发起请求,应用层是用户的,所以 http 报文的内容都是一些人类可阅读的 ASCII 码点,但计算机只懂得二进制,光纤中认识光信号,所以这个 http 报文还需要经过一一些处理才能穿越那些物理链路发送到我们的目的服务器上。首先来讲讲 http 报文格式
在我们这个例子里我们的请求方法是 GET,GET 和 POST 是最常见的 http 方法,除此以外还包括 DELETE、HEAD、OPTIONS、PUT、TRACE。我们没有传头部字段,也就是 header , http 的头部可以分为两种,一种是通用头部如 Cache-Control、 Connection、Date、Pragma、Transfer-Encoding、Upgrade、Via 等,可以通过它传递一些信息,对通用头部的扩展要求通讯双方都支持此扩展,如果存在不支持的通用头部,一般将会作为实体头部处理。实体头域包含关于实体的原信息,实体头包括 Allow、Content-Base、Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、Etag、Expires、Last-Modified、extension-header。extension-header 允许客户端定义新的实体头,但是这些域可能无法为接受方识别。
在这个请求里我们也没有消息体,URL为 https://httpbin.org/get 是一个简单得不能再简单的 GET 请求。
http 响应报文结构和请求差不多,区别在于状态行,状态码(Status-Code)主要用于机器理解,短语(Reason-Phrase)Status-Code 提供一个简单的文本描述,主要帮助用户理解:
- 1xx : 信息响应类,表示接收到请求并且继续处理
- 2xx : 处理成功响应类,表示动作被成功接收、理解和接受
- 3xx : 重定向响应类,为了完成指定的动作,必须接受进一步处理
- 4xx : 客户端错误,客户请求包含语法错误或者是不能正确执行
- 5xx : 服务端错误,服务器不能正确执行一个正确的请求
几个常见的状态码和短语:
- 200 OK 最好的情况,即处理成功
- 404 Not Found 不希望看到的响应之一,即找不到所请求的资源
- 500 Internal Server Error 不希望看到的响应之二,服务端发生了错误
说完了 http 报文,接下来我们来实践一下,看看上面那段代码发起一个 http 请求,它的运行结果如下:
可以看到我们这个请求是成功了的,对方服务器返回了 200, 短语是 OK,意味着目标服务器成功处理了我们的请求。
输出的 Request Trace Info 信息可以帮助我们理解整个请求的过程,我们一行一行地看:
DNSLookup
http 报文里虽然包含了目的服务器的地址,也就是我们上面输入的 URL,一个 URL 由协议头(http、https、sftp等)+ 域名 + 资源路径组成,在我么这个例子里协议头为https(HTTPS = HTTP + SSL(TLS),它和 http 的区别在于加了一道身份验证所以更安全),域名是 httpbin.org ,资源路径是 /get,也就是我们以 https 协议所约定的方式去获取 httpbin.org 所映射的服务器上的 /get 路径下的资源。
域名由字符串组成,机器是无法读懂的,所以我们需要一个服务去将它解析成机器能读懂的地址,也就是 IP,而这个服务就是 DNS(Domain Name System)域名系统,它是用于实现域名和IP地址相互映射的一个分布式数据库,这里输出的 DNSLookup 的值就是本次请求里花费在 DNS 解析上的时间。
域名解析的过程大致如下:
完整的DNS解析过程有以下几个步骤:
(1)查看浏览器缓存(我们这里是直接通过后端来发起请求,所以没有这一步)
当用户通过浏览器访问某域名时,浏览器首先会在自己的缓存中查找是否有该域名对应的 IP 地址(若曾经访问过该域名且没有清空缓存便存在)。
(2)查看系统缓存
当浏览器缓存中无域名对应 IP 则会自动检查用户计算机系统 Hosts 文件 DNS 缓存是否有该域名对应 IP。
(3)查看路由器缓存
当浏览器及系统缓存中均无域名对应 IP 则进入路由器缓存中检查,以上三步均为客服端的 DNS 缓存。
(4)查看ISP DNS 缓存
当在用户客服端查找不到域名对应 IP 地址,则将进入 ISP DNS 缓存中进行查询。比如你用的是电信的网络,则会进入电信的 DNS 缓存服务器中进行查找。
(5)询问根域名服务器
当以上均未完成,则进入根服务器进行查询。全球仅有 13 台根域名服务器,1 个主根域名服务器,其余 12 为辅根域名服务器。根域名收到请求后会查看区域文件记录,若无则将其管辖范围内顶级域名(如.com、.cn等)服务器 IP 告诉本地 DNS 服务器。
(6)询问顶级域名服务器
顶级域名服务器收到请求后查看区域文件记录,若无记录则将其管辖范围内权威域名服务器的 IP 地址告诉本地 DNS 服务器。
(7)询问权威域名(主域名)服务器
权威域名服务器接受到请求后查询自己的缓存,如果没有则进入下一级域名服务器进行查找,并重复该步骤直至找到正确记录。
(8)保存结果至缓存
本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时将该结果反馈给客户端,客户端通过这个 IP 地址即可访问目标Web服务器。至此,DNS递归查询的整个过程结束。
DNS系统承担着将域名解析成IP地址的重要作用,是计算机之间实现访问互联的关键和基础。因此,DNS解析的安全对于维持网络稳定运行至关重要。企业相关管理者和运营者一定要做好域名及域名解析的安全防护工作,定期进行数据扫描分析,启用全方位DNS风险监测,实时关注DNS运行状态,同时做好妥善的应急备份准备,一旦发现问题,出现故障,第一时间响应解决,才能将DNS故障风险及其带来的损失降至最低点。
通过域名解析服务我们获得了目标服务器的 IP,它会在网络层被用到。
TCPConnTime
建立 TCP (Transmission Control Protocol) 连接所花的时间。TCP 属于传输层协议,除了 TCP 外还有 UDP 也是常用的传输层协议。本文的传输层协议选择了 TCP, 我们在应用层准备好了 http 报文,然后选择一个 tcp server 去传输这个 http 报文给目标服务器,tcp server 通过什么方式进行与目标服务器的沟通对于应用层来说是透明的(即不可见),应用层只需要等待 tcp server 返回的目标服务器的应答结果就好了。
TCP 是面向连接的协议,而 UDP 是无连接的,使用 TCP 进行通信的双方在互相发送消息之前要先建立一个连接,这个连接建立的过程被称为三次握手。我们先来看看 TCP 报文格式:
端口,(port),主要分为物理端口和逻辑端口。我们一般说的都是逻辑端口,用于区分不同的服务。因为网络中一台主机只有一个IP,但是一个主机可以提供多个服务,端口号就用于区分一个主机上的不同服务。一个IP地址的端口通过16bit进行编号,最多可以有65536个端口,标识是从0~65535。
端口号分为系统端口(System Ports)0~ 1023、用户端口(User Ports)1024~ 49151和动态端口号(Dynamic Ports)49152~65535。我们自己的服务一般都绑定在注册端口上。
系统端口(0~ 1023):也叫做公认端口(Well Known Ports),它们紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务的协议。任何TCP/IP实现所提供的服务都用0-1023之间的端口号。我们的私用端口号不应该使用这个区间内的端口,除非你向IANA注册了。例如:80端口实际上总是 http 通讯、443对应着 https(在本文中我们使用的就是 https 协议,在最后一行输出的 RemoteAddr 可以看到目标服务器的端口号就是443)、21对应着 ftp,25对应着 smtp,110对应着 pop3 等。
互联网号码分配局(英语:Internet Assigned Numbers Authority,缩写 IANA),是一家互联网地址指派机构,管理国际互联网中使用的 IP 地址、域名和许多其它参数的机构。 IP 地址、自治系统成员以及许多顶级和二级域名分配的日常职责由国际互联网注册中心(IR)和地区注册中心承担。IANA 是由 ICANN 管理的。
用户端口(1024~ 49151):也叫做注册端口(Registered Ports),从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
动态端口(49152~65535):也叫做私有或动态端口(Private or Ephemeral Ports),从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。只要运行的程序向系统提出访问网络的申请,那么系统就可以从这些端口号中分配一个供该程序使用。比如 49152 端口就是分配给第一个向系统发出申请的程序。在关闭程序进程后,就会释放所占用的端口号。
所以当 client 准备发出网络请求的时候,首先要向系统申请一个端口号作为源端口号,系统会随机从49152~65535中分配一个可用的端口号给这个 client,这样当目标服务器处理完请求要给我们返回数据的时候才能通过这个源端口号找到发出请求的这个端口所对应的服务并把 response 交给这个服务。可以说 http 报文是面向服务的,它是服务与服务之间的交流,传输层是面向进程的,两个端口号标识了两个进程,一个进程里可能会有许多个服务(路由,或者说 API)。
序列号和确认号:
标志位字段:比较常见的标志位 SYN、ACK、FIN 会在 tcp 连接建立与释放的时候使用到,也就是我们常说的三次握手四次挥手。先讲讲三次握手建立连接,
最后是数据段(segment data),这里放着我们完整的 http 请求报文,像这样:
(图片来自 https://www.jianshu.com/p/eadc2f28fd07 这篇博文)
RemoteAddr
这个远端地址包括了目标服务器的 ip 和端口号, 端口号在上文我们已经说过了,它会被写在 tcp 报文里,而 ip 则会被写在网络层的 ip 数据报里。我们先来看看 ip 数据报的结构:
就像 tcp 报文的数据段是一个完整的应用层报文一样,ip 数据报的数据部分指的就是完整的传输层报文,在本文里就是完整的 tcp 报文,如下图所示:
IsConnReused
连接是否复用,说到这个话题又回到了应用层的 http 协议,前面我们说到 http 报文有各种各样的 header 字段,其中有一个叫做 Connection 的通用 header,它的的取值为 Keep-Alive 或 close 。当在 header 里加入 Connection: Keep-Alive 意味着开启长连接,
我们把上面例子里的代码进行一些小小修改,变成这样:
client := resty.New() headers := map[string]string{ "Connection": "Keep-Alive", } resp1, _ := client.R(). EnableTrace(). SetHeaders(headers). Get("https://httpbin.org/get") // Explore trace info fmt.Println("Request Trace Info:") ti := resp1.Request.TraceInfo() fmt.Println(" DNSLookup :", ti.DNSLookup) fmt.Println(" TCPConnTime :", ti.TCPConnTime) fmt.Println(" TLSHandshake :", ti.TLSHandshake) fmt.Println(" IsConnReused :", ti.IsConnReused) fmt.Println(" RemoteAddr :", ti.RemoteAddr.String()) resp2, _ := client.R(). EnableTrace(). SetHeaders(headers). Get("https://httpbin.org/get") fmt.Println() fmt.Println("******** Request with Keep-Alive *******") fmt.Println() // Explore trace info fmt.Println("Request Trace Info:") ti2 := resp2.Request.TraceInfo() fmt.Println(" DNSLookup :", ti2.DNSLookup) fmt.Println(" TCPConnTime :", ti2.TCPConnTime) fmt.Println(" TLSHandshake :", ti2.TLSHandshake) fmt.Println(" IsConnReused :", ti2.IsConnReused) fmt.Println(" RemoteAddr :", ti2.RemoteAddr.String())
这次我们主要想看看 Keep-Alive 的实际效果,所以一些没啥用的信息就不输出了,这段代码的运行效果如下:
可以看到在第二次请求的时候 DNSLookup、 TCPConnTime、TLSHandshake 这三个与连接建立有关的时间花费都变成了0啦!IsConnReused 也由 false 变成了 true,说明这次的连接是复用的第一次请求的时候建立的连接,所以不需要再重新建立连接,那些与连接建立的时间花费自然也变成零了(从图上可以看出来节省了大约2.5s 的时间)。
关于长连接有兴趣的朋友可以看看这篇博文
总结
实践很重要,上学的时候学计网总觉得是一知半解的,书上说分层协作各层解耦,层与层之间是透明的,也就是互相看不见,
标签:http,fmt,端口,域名,Computer,Println,服务器,Networks From: https://www.cnblogs.com/kirizi/p/16994756.html