分类 计算机基础 下的文章

背景

今年,我们做了一个平台项目,平台嘛,免不了对外提供Http/Https的接口,有时候会听到研发同学说:“咱们接口就都是POST方法了,不要定义其他方法,没啥大用。”我不禁想起了RESTFUL风格,其实HTTP协议的这些动词,是有专门含义和用法的,正好可以通过此文回忆和记录下,看看是不是其他方法,没啥大用。

HTTP不同的动词

我们可以定义两种编程逻辑,分别是业务逻辑和控制逻辑。

业务逻辑。就是你实现业务需求的功能的代码,就是跟用户需求强相关的代码。比如,把用户提交的数据保存起来,查询用户的数据,完成一个订单交易,为用户退款……等等,这些是业务逻辑。控制逻辑。就是我们用于控制程序运行的非功能性的代码。比如,用于控制程序循环的变量和条件,使用多线程或分布式的技术,使用HTTP/TCP协议,使用什么样数据库,什么样的中间件……等等,这些跟用户需求完全没关系的东西。

网络协议也是一样的,一般来说,几乎所有的主流网络协议都有两个部分,一个是协议头,一个是协议体。协议头中是协议自己要用的数据,协议体才是用户的数据。所以,协议头主要是用于协议的控制逻辑,而协议体则是业务逻辑。
HTTP的动词(或是Method)是在协议头中,所以,其主要用于控制逻辑。
下面是HTTP的动词规范,一般来说,REST API 需要开发人员严格遵循下面的标准规范。
http协议动词.png
其中,PUT 和 PACTH 都是更新业务资源信息,如果资源对象不存在则可以新建一个,但他们两者的区别是,PUT 用于更新一个业务对象的所有完整信息,就像是我们通过表单提交所有的数据,而 PACTH 则对更为API化的数据更新操作,只需要更需要更新的字段(参看 RFC 5789 )。
注意:我在这个表格的最后一列中加入了“是否幂等”的,API的幂等对于控制逻辑来说是一件很重要的事。所谓幂等,就是该API执行多次和执行一次的结果是完全一样的,没有副作用。
POST 用于新增加数据,比如,新增一个交易订单,这肯定不能是幂等的
DELETE 用于删除数据,一个数据删除多次和删除一次的结果是一样的,所以,是幂等的
PUT 用于全部数更新,所以,是幂等的。
PATCH用于局部更新,比如,更新某个字段 cnt = cnt+1,明显不可以能是幂等操作。
幂等这个特性对于远程调用是一件非常关键的事,就是说,远程调用有很多时候会因为网络原因导致调用timeout,对于timeout的请求,我们是无法知道服务端是否已经是收到请求并执行了,此时,我们不能贸然重试请求,对于不是幂等的调用来说,这会是灾难性的。比如像转帐这样的业务逻辑,转一次和转多次结果是不一样的,如果重新的话有可能就会多转了一次。所以,这个时候,如果你的API遵从了HTTP动词的规范,那么你写起程序来就可以明白在哪些动词下可以重试,而在哪些动词下不能重试。如果你把所有的API都用POST来表达的话,就完全失控了。

除了幂等这样的控制逻辑之外,你可能还会有如下的这些控制逻辑的需求:

缓存。通过CDN或是网关对API进行缓存,很显然,我们要在查询GET 操作上建议缓存。
流控。你可以通过HTTP的动词进行更粒度的流控,比如:限制API的请用频率,在读操作上和写操作上应该是不一样的。
路由。比如:写请求路由到写服务上,读请求路由到读服务上。
权限。可以获得更细粒度的权限控制和审计。
监控。因为不同的方法的API的性能都不一样,所以,可以区分做性能分析。
压测。当你需要压力测试API时,如果没有动词的区分的话,我相信你的压力测试很难搞吧。
……等等
也许,你会说,我的业务太简单了,没有必要搞这么复杂。OK,没有问题,但是我觉得你最差的情况下,也是需要做到“读写分离”的,就是说,至少要有两个动词,GET 表示是读操作,POST表示是写操作。

Restful复杂查询

一般来说,对于查询类的API,主要就是要完成四种操作:排序,过滤,搜索,分页。下面是一些相关的规范。参考于两个我觉得写的最好的Restful API的规范文档,Microsoft REST API Guidelines,Paypal API Design Guidelines。

排序。对于结果集的排序,使用 sort 关键字,以及 {field_name}|{asc|desc},{field_name}|{asc|desc} 的相关语法。比如,某API需要返回公司的列表,并按照某些字段排序,如:GET /admin/companies?sort=rank|asc 或是 GET /admin/companies?sort=rank|asc,zip_code|desc

过滤。对于结果集的过滤,使用 filter 关键字,以及 {field_name} op{value} 的语法。比如: GET /companies?category=banking&location=china 。但是,有些时候,我们需要更为灵活的表达式,我们就需要在URL上构造我们的表达式。这里需要定义六个比较操作:=,<,>,<=,>=,以及三个逻辑操作:and,or,not。(表达式中的一些特殊字符需要做一定的转义,比如:>= 转成 ge)于是,我们就会有如下的查询表达式:GET /products?$filter=name eq 'Milk' and price lt 2.55 查找所有的价柗小于2.55的牛奶。

搜索。对于相关的搜索,使用 search 关键字,以及关键词。如:GET /books/search?description=algorithm 或是直接就是全文搜索 GET /books/search?key=algorithm 。

分页。对于结果集进行分页处理,分页必需是一个默认行为,这样不会产生大量的返回数据。

使用page和per_page代表页码和每页数据量,比如:GET /books?page=3&per_page=20。
可选。上面提到的page方式为使用相对位置来获取数据,可能会存在两个问题:性能(大数据量)与数据偏差(高频更新)。此时可以使用绝对位置来获取数据:事先记录下当前已获取数据里最后一条数据的ID、时间等信息,以此获取 “该ID之前的数据” 或 “该时刻之前的数据”。示例:GET /news?max_id=23454345&per_page=20 或 GET /news?published_before=2011-01-01T00:00:00Z&per_page=20。
另外,对于一些更为复杂的操作,建议通过分别调用多个API的方式来完成,虽然这样会增加网络请求的次数,但是这样的可以让后端程序和数据耦合度更小,更容易成为微服务的架构。

最后,如果你想在Rest中使用像GraphQL那样的查询语言,你可以考虑一下类似 OData 的解决方案。OData 是 Open Data Protocol 的缩写,最初由 Microsoft 于 2007 年开发。它是一种开放协议,使您能够以简单和标准的方式创建和使用可查询和可互操作的 RESTful API。

总结

所以,可以回答开始的问题了,这些HTTP动词不是不是没啥大用,应该是非常有用,软件程序员对其正确使用,应该是作为一名工程师的“正常”操作,就像家里装修的时候,泥水匠贴瓷砖时,需要保证每片瓷砖贴的严丝合缝一样。接口动词的正确选用,也是对于调用方的负责,否则别人调用错误,你在家里,也没法好好休息,老是跑来找你这个接口要怎么用,怎么搞,岂不是大大浪费沟通效率。

背景

之前有篇文章介绍了TCP建立连接和断开连接时的状态转移流程,没有详细去讲建立完连接之后的传输控制。这篇文章会就几个常见的TCP传输相关的问题做下简单分享,因为TCP/IP协议很复杂,也不可能通过一篇文章完全写清楚。可能更多的内容,还是要查阅专业的相关书籍和网站。好了,废话不多说,今天会涉及这么几个问题的初步探讨:

  1. TCP滑动窗口机制作用是什么?
  2. TCP的重传机制和快速重传机制是什么?有什么区别?
  3. TCP拥塞控制机制是什么?

TCP滑动窗口机制

滑动窗口其实就是用来控制发送速率,因为网络是复杂多变的,有时候就会阻塞住,而有时候又很通畅。不让让网络抖动的时候,去加剧网络质量的下降,从而造成不必要的雪崩。所以发送方需要知道接收方的情况,好控制一下发送的速率,不至于蒙着头一个劲儿的发然后接受方都接受不过来。因此 TCP 就有个叫滑动窗口的东西来做流量控制,也就是接收方告诉发送方我还能接受多少数据,然后发送方就可以根据这个信息来进行数据的发送。
以下是发送方维护的窗口,就是黑色圈起来的。
TCP滑动窗口示意图.png
图中的 #1 是已收到 ACK 的数据,#2 是已经发出去但是还没收到 ACK 的数据,#3 就是在窗口内可以发送但是还没发送的数据。#4 就是还不能发送的数据。
然后此时收到了 36 的 ACK,并且发出了 46-51 的字节,于是窗口向右滑动了。
TCP滑动窗口示意图2.jpg
TCP/IP Guide 上还有一张完整的图,画的十分清晰,大家看一下。
完整TCP滑动窗口示意图.png
那么如果接受方回复的窗口一直是0,那怎么办?
上文已经说了发送方式根据接收方回应的 window 来控制能发多少数据,如果接收方一直回应 0,那发送方就杵着?你想一下,发送方发的数据都得到 ACK 了,但是呢回应的窗口都是 0 ,这发送方此时不敢发了啊,那也不能一直等着啊,这 Window 啥时候不变 0 啊?于是 TCP 有一个 Zero Window Probe 技术,发送方得知窗口是 0 之后,会去探测探测这个接收方到底行不行,也就是发送 ZWP 包给接收方。具体看实现了,可以发送多次,然后还有间隔时间,多次之后都不行可以直接 RST。
假设接收方每次回应窗口都很小怎么办?
你想象一下,如果每次接收方都说我还能收 1 个字节,发送方该不该发?TCP + IP 头部就 40 个字节了,这传输不划算啊,如果傻傻的一直发这就叫 Silly Window。那咋办,一想就是发送端等着,等养肥了再发,要么接收端自己自觉点,数据小于一个阈值就告诉发送端窗口此时是 0 算了,也等养肥了再告诉发送端。发送端等着的方案就是纳格算法,这个算法相信看一下代码就知道了。
伪代码.png
简单的说就是当前能发送的数据和窗口大于等于 MSS 就立即发送,否则再判断一下之前发送的包 ACK 回来没,回来再发,不然就攒数据。
接收端自觉点的方案是 David D Clark’s 方案,如果窗口数据小于某个阈值就告诉发送方窗口 0 别发,等缓过来数据大于等于 MSS 或者接受 buffer 腾出一半空间了再设置正常的 window 值给发送方。
对了提到纳格算法不得不再提一下延迟确认,纳格算法在等待接收方的确认,而开启延迟确认则会延迟发送确认,会等之后的包收到了再一起确认或者等待一段时候真的没了再回复确认。
这就相互等待了,然后延迟就很大了,两个不可同时开启。

TCP的重传机制和快速重传机制

前面我们提到 TCP 要提供可靠的传输,那么网络又是不稳定的如果传输的包对方没收到却又得保证可靠那么就必须重传。
TCP 的可靠性是靠确认号的,比如我发给你1、2、3、4这4个包,你告诉我你现在要 5 那说明前面四个包你都收到了,就是这么回事儿。
不过这里要注意,SeqNum 和 ACK 都是以字节数为单位的,也就是说假设你收到了1、2、4 但是 3 没有收到你不能 ACK 5,如果你回了 5 那么发送方就以为你5之前的都收到了。
所以只能回复确认最大连续收到包,也就是 3。
而发送方不清楚 3、4 这两个包到底是还没到呢还是已经丢了,于是发送方需要等待,这等待的时间就比较讲究了。
如果太心急可能 ACK 已经在路上了,你这重传就是浪费资源了,如果太散漫,那么接收方急死了,这死鬼怎么还不发包来,我等的花儿都谢了。
所以这个等待超时重传的时间很关键,怎么搞?聪明的小伙伴可能一下就想到了,你估摸着正常来回一趟时间是多少不就好了,我就等这么长。
这就来回一趟的时间就叫 RTT,即 Round Trip Time,然后根据这个时间制定超时重传的时间 RTO,即 Retransmission Timeout。
不过这里大概只好了 RTO 要参考下 RTT ,但是具体要怎么算?首先肯定是采样,然后一波加权平均得到 RTO。
RFC793 定义的公式如下:
先采样 RTT 2、SRTT = ( ALPHA SRTT ) + ((1-ALPHA) RTT) 3、RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]]
ALPHA 是一个平滑因子取值在 0.8~0.9之间,UBOUND 就是超时时间上界-1分钟,LBOUND 是下界-1秒钟,BETA 是一个延迟方差因子,取值在 1.3~2.0。
但是还有个问题,RTT 采样的时间用一开始发送数据的时间到收到 ACK 的时间作为样本值还是重传的时间到 ACK 的时间作为样本值?
采样RTT.png
从图中就可以看到,一个时间算长了,一个时间算短了,这有点难,因为你不知道这个 ACK 到底是回复谁的。
所以怎么办?发生重传的来回我不采样不就好了,我不知道这次 ACK 到底是回复谁的,我就不管他,我就采样正常的来回。
这就是 Karn / Partridge 算法,不采样重传的RTT。
但是不采样重传会有问题,比如某一时刻网络突然就是很差,你要是不管重传,那么还是按照正常的 RTT 来算 RTO, 那么超时的时间就过短了,于是在网络很差的情况下还疯狂重传加重了网络的负载。
因此 Karn 算法就很粗暴的搞了个发生重传我就将现在的 RTO 翻倍,哼!就是这么简单粗暴。
但是这种平均的计算很容易把一个突然间的大波动,平滑掉,所以又搞了个算法,叫 Jacobson / Karels Algorithm。
它把最新的 RTT 和平滑过的 SRTT 做了波计算得到合适的 RTO,公式我就不贴了,反正我不懂,不懂就不哔哔了。
那快速重传机制又是什么,既然有了重传机制,为啥又要这个?超时重传是按时间来驱动的,如果是网络状况真的不好的情况,超时重传没问题,但是如果网络状况好的时候,只是恰巧丢包了,那等这么长时间就没必要。
于是又引入了数据驱动的重传叫快速重传,什么意思呢?就是发送方如果连续三次收到对方相同的确认号,那么马上重传数据。
因为连续收到三次相同 ACK 证明当前网络状况是 ok 的,那么确认是丢包了,于是立马重发,没必要等这么久。
快速重传机制.png
看起来好像挺完美的,但是你有没有想过我发送1、2、3、4这4个包,就 2 对方没收到,1、3、4都收到了,然后不管是超时重传还是快速重传反正对方就回 ACK 2。这时候要重传 2、3、4 呢还是就 2 呢?
所以就引入了SACK,即 Selective Acknowledgment,它的引入就是为了解决发送方不知道该重传哪些数据的问题。我们来看一下下面的图就知道了。
SACK.png
SACK 就是接收方会回传它已经接受到的数据,这样发送方就知道哪一些数据对方已经收到了,所以就可以选择性的发送丢失的数据。
如图,通过 ACK 告知我接下来要 5500 开始的数据,并一直更新 SACK,6000-6500 我收到了,6000-7000的数据我收到了,6000-7500的数据我收到了,发送方很明确的知道,5500-5999 的那一波数据应该是丢了,于是重传。
而且如果数据是多段不连续的, SACK 也可以发送,比如 SACK 0-500,1000-1500,2000-2500。就表明这几段已经收到了。
说完了SACK,不得不提D-SACK,这又是什么东西?D-SACK 其实是 SACK 的扩展,它利用 SACK 的第一段来描述重复接受的不连续的数据序号,如果第一段描述的范围被 ACK 覆盖,说明重复了,比如我都 ACK 到6000了你还给我回 SACK 5000-5500 呢?
说白了就是从第一段的反馈来和已经接受到的 ACK 比一比,参数是 tcp_dsack,Linux 2.4 之后默认开启。
那知道重复了有什么用呢?1、知道重复了说明对方收到刚才那个包了,所以是回来的 ACK 包丢了。2、是不是包乱序的,先发的包后到?3、是不是自己太着急了,RTO 太小了?4、是不是被数据复制了,抢先一步呢?

TCP拥塞控制机制

有滑动窗口了为什么还要拥塞控制,前面已经提到了,加了拥塞控制是因为 TCP 不仅仅就管两端之间的情况,还需要知晓一下整体的网络情形,毕竟只有大家都守规矩了道路才会通畅。前面我们提到了重传,如果不管网络整体的情况,肯定就是对方没给 ACK ,那我就无脑重传。如果此时网络状况很差,所有的连接都这样无脑重传,是不是网络情况就更差了,更加拥堵了?然后越拥堵越重传,一直冲冲冲!然后就 GG 了。
主要有以下几个步骤来搞:1、慢启动,探探路。2、拥塞避免,感觉差不多了减速看看 3、拥塞发生快速重传/恢复。
拥塞控制.png
这块确实还不怎么理解,晚点有机会再补充,

总结

TCP/IP实在是很复杂的一个协议,其中的工程设计凝结了很多大师的智慧,在下不才,这篇文章也只能管中窥豹,略作启发,更多详细内容,还是推荐阅读专业的书籍。

引用内容

http://www.tcpipguide.com/
https://www.ionos.com/digitalguide/server/know-how/introduction-to-tcp/
https://www.ibm.com/developerworks/cn/linux/l-tcp-sack/
https://coolshell.cn/articles/11564.html/
https://tools.ietf.org/html/rfc793https://nmap.org/book/tcpip-ref.html

背景介绍

在《UNIX网络编程》经典书籍中,对TCP连接建立和断开描述了11种TCP状态,书里面有一张经典图片。

TCP的11种状态转移图.png

头一次看(或者过一段时间看)上面这张图,脑袋都会变大,我写这篇文章,就想记录下如何理解这张完整的状态转移图。作用有两个:
一、深入理解TCP的状态转移;
二、针对实际工作种会出现的大量TCP状态(比如TIME_WAIT、CLOSE_WATI),有针对性解决问题的思路。

整体理解

首先,这张图画的是TCP所有的11种状态,以及他们之间转移的条件,可以这样理解,图描述的是一个整体全貌,是所有状态拼在一起的内容,这也就是为啥,我们看着觉得特别复杂。简单来说,我认为在单一的一次TCP传输数据(建立连接->传输数据->断开连接),不会出现所有的11种状态。我总结下来,某一次的TCP连接会有如下几种情况:
一、客户端建立连接,客户端和服务端传输数据,客户端关闭连接;
二、客户端建立连接,客户端和服务端传输数据,服务端关闭连接;
三、客户端建立连接,客户端和服务端传输数据,客户端和服务端同时关闭连接;
也很容易理解,建立连接,只能是客户端主动,只有关闭连接,我们才需要分类讨论。

客户端关闭连接

这个应该是最常规的流程了,从整体图中分离得到的状态转移图如下:

TCP客户端断开连接示意图.png

如果转化位流程图,如下:
TCP客户端断开连接流程图.png

上面这两张图,应该是比较清晰,也不用太多的解释了吧?有时间补充实际操作的代码,结合代码能够看到状态转移,我想理解会更加深刻的。

服务端关闭连接

和上面客户端关闭连接一样,我也整理两张容易理解的图,
服务端关闭连接示意图.png
服务端关闭连接流程图.png

客户端和服务端同时关闭连接

这个情况就相对复杂些,分2种情况讨论下。第一种情况两端同时关闭连接,也同时收到对方发送的FIN包,如下图:

两边同时关闭同时收到FIN包.png

第二种情况两端同时关闭连接,但其中一方先收到FIN包(以客户端先收到FIN包为例,反之亦然),如下图:

双方同时关闭连接一方先收到FIN包.png

异常状态的讨论

实际工作中,会经常遇到服务器出现大量TIME_WAIT或者CLOSE_WATI的状态。
TIME_WAIT根据上面的分析,主动关闭连接方在ACK对端FIN包后处于的状态,也被称为2MSL等待状态。每个具体 TCP 实现必须选择一个报文段最大生存时间 MSL(Maximum Segment Lifetime)。它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。所以TIME_WAIT状态保证了两点:1. 可靠地实现 TCP 全双工连接的终止;2. 允许老的重复分节在网络中消逝。如果服务器上出现大量的TIME_WAIT状态,很可能是有大量的短连接,此时有可能的措施有这么几个,减少MSL的时长、短连接变为长连接。
CLOSE_WAIT状态时被动关闭连接的一方会处于的状态,只要收到了对端的FIN包,回复了对应的ACK包,就变成了CLOSE_WAIT状态,但是如果此时我们服务程序(异常原因)没有继续调用close()函数,导致未发送FIN包,就会造成这个连接一直处于CLOSE_WAIT状态,此时需要排查程序,弄清楚在什么异常场景下,导致程序未发送FIN包。

参考资料:
https://zhuanlan.zhihu.com/p/78540103
https://www.jianshu.com/p/eb0d3e4744f1
https://blog.csdn.net/yu616568/article/details/44677985