网络协议:TCP/IP

Part I: TCP

OSI七层模型回顾

  • 7应用层: 该层协议包括 Socket, HTTP, HTTPS, FTP, SSH, POP3, // WebSocket属于哪一层?
  • 6表示层: 格式转换, 把数据转换为应用层能兼容的格式 或 适合传输的格式, 比如: 加密/解密, 压缩/解压
  • 5会话层: 维护和管理数据传输过程中两台计算机之间的连接, 该层协议有: SSL/TLS
  • 4传输层: 传输控制, 例如TCP协议(传输控制协议, 主要实现了传输的可靠性, 例如超时重传), 该层把「传输表头」TH加入数据形成「报文」Segment, 传输表头包括了传输协议等
  • 3网络层: 决定数据的路由, 例如IP/ICMP协议, 该层把「网络表头」NH加入数据形成「包」Packet, 网络表头包括: @TODO
  • 2链路层: 负责网络寻找和错误侦测, 该层把「数据链表头」DLH加入数据开头, 以及「数据链表尾」DLT加入数据结尾, 形成「信息框」Data Frame, 数据链表头包括, 该层协议包括WiFi, GPRS(通用分组无线服务)
  • 1物理层: 在局部局域网上传送「数据帧」Data Frame, 该层定义了网络硬件和网络数据之间的互通, 包括: 针脚/电压/集线器/网卡等

TCP报头

TCP头

  • Source Port,Destination Port:TCP的包是没有IP地址的,那是IP层上的事。但是有源端口和目标端口。
  • Sequence Number是包的序号,用来解决网络包乱序(reordering)问题。
  • Acknowledgement Number就是ACK——用于确认收到,用来解决不丢包的问题。
  • TCP Flag ,也就是包的类型,主要是用于操控TCP的状态机的,Flag共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
    • URG:紧急指针(urgent pointer)有效。
    • ACK:Acknowledgement,确认收到。
    • PSH:接收方应该尽快将这个报文交给应用层。
    • RST:RESET,重置连接;
    • SYN:Synchronize Sequence Numbers,一般在发起一个新连接的时候才会同步Seq Num;
    • FIN:FINISH,释放一个连接。
  • Window又叫Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的。

TCP状态机,以及三次握手、四次挥手

网络上的传输是没有连接的,包括TCP也是一样的。而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的。

下图是“TCP协议的状态机” 和 “TCP建链接”、“传数据” 、“TCP断链接”的对照图:
state machie

第二张图描述了TCP建立链接,收发数据,关闭链接三个步骤里TCP状态机的状态改变和发送的报文:
open and close

三次握手、四次挥手

  • 创建链接:双方交换Sequence Number的过程,服务端初始状态是LISTEN
    • 客户端 -> 服务端: SYN seq=x,自身状态变为SYN_SENT(这一步是客户端发起的,调用connect());
    • 服务端 -> 客户端: SYN seq=y, ACK=x+1,自身状态变为SYN_RECEIVED,此时TCP连接称为半连接(half-open connect)状态;
    • 客户端 -> 服务端: ACK=y+1,自身状态ESTABLISHED,服务端收到后状态也变为ESTABLISHED;
  • 收发数据:
    • 客户端发送数据: seq=j
    • 服务端收到后回复:ACK=j+1
  • 关闭链接:由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭。当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭:
    • 客户端 -> 服务端: FIN seq=m,表示客户端-服务端方向不再有数据流动,并进入FIN_WAIT_1状态,
    • 服务端 -> 客户端: ACK=m+1,进入CLOSE_WAIT状态
    • 服务端 -> 客户端: FIN seq=n,表示服务端-客户端方向不再有数据流动,进入LAST_ACK状态;
    • 客户端 -> 服务端: ACK=n+1,客户端进入TIME_WAIT状态,服务端收到这个ACK进入CLOSED状态;

SYN攻击

SYN攻击: Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包(或者发送完SYN包就立刻下线),正常情况Server回复SYN-ACK,并等待Client的ACK。

但如果Server端如果在一定时间内没有收到Client的ACK则会重发SYN-ACK。由于CLient源地址是不存在的(或者已经下线),因此Server需要不断重发SYN-ACK,这些无效的SYN-ACK包将长时间占用Server机器的SYN队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。

SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了 : netstat -nat | grep

Linux下给了一个叫tcp_syncookies的参数来应对这个问题 —— 当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使Client不在SYN队列中)。请注意,请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为,synccookies是妥协版的TCP协议,并不严谨。

一般情况下为了防范SYN攻击,有三个TCP参数可供你选择,第一个是:tcp_synack_retries来减少重试次数;第二个是:tcp_max_syn_backlog可以增大SYN连接数;第三个是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝连接了。

在Linux下,默认情况下Server重发SYN-ACK的次数为5次,重试的间隔时间从1s开始每次都翻倍,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s才能知道第5次也超时了,
所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s 才会断开这个连接。

Sequence Number

对于建链接的3次握手,主要是要初始化Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)

RFC793中说,SN会和一个假的时钟绑在一起,这个时钟会在每4微秒对SN做加一操作,直到超过2^32又从0开始。这样,一个连接的Sequence Number重复周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime。所以,只要MSL的值小于4.55小时,那么,我们就不会重用到Sequence Number。
没发送一个数据包,Seq Num就增加该次发包的长度,比如三次握手后,来了两个Len:1440的包,第二个包的SeqNum就成了1441。接收端第一个ACK回的是1441,表示第一个1440收到了。

关闭连接时发起方的TIME_WAIT状态

根据TCP状态机的图可知,主动发起断开连接的一端,收到对端的FIN+ACK后,会进入TIME_WAIT状态,从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是 两个MSL。为什么要这有TIME_WAIT?为什么不直接给转成CLOSED状态呢?主要有两个原因:

  • TIME_WAIT 确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到 ACK,就会触发被动端重发 FIN。如果主动关闭方在 TIME_WAIT 状态不等待一段时间就直接释放连接并进入 CLOSED 状态,那么主动关闭方无法收到来自被动关闭方重发的 FIN+ACK 报文段,也就不会再发送一次确认 ACK 报文段,因此被动关闭方就无法正常进入CLOSED 状态。
  • 有足够的时间让这个连接不会跟后面的连接混在一起,在连接处于 2*MSL 等待时,任何迟到的报文段将被丢弃,这样就可以使下一个新的连接中不会出现这种旧的连接之前延迟的报文段。(你要知道,有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)。你可以看看这篇文章《TIME_WAIT and its design implications for protocols and scalable client server systems

MSL = Maximum Segment Lifetime,最长段生存时间,RFC793定义了MSL为2分钟,Linux设置成了30s

TIME_WAIT会占用多少资源?

入站连接(服务器端)很少会被TIME_WAIT影响。虽然与客户端一样,服务器端主动关闭的连接会进入TIME_WAIT状态,但是服务端监听的端口并不会防止新建的入站连接请求的建立。

由于本地端口的缺乏,TIME_WAIT的存在影响的是出站连接(客户端)的建立,这些本地端口由操作系统进行自动的分配,因此,优化的方法是增加本地端口的范围。

但是服务端被占用的资源只有一个“连接描述”,由5个元素组成:(协议,本地IP,本地端口,远程IP,远程端口)所以当同一客户端向服务器建立了大量连接之后,会耗尽可用的五元组导致问题。参考: tcp - What is the cost of many TIME_WAIT on the server side? - Stack Overflow,
尽管如此, 一个连接占用的资源多少, 对于服务端来说, 还是要比客户端小很多.

重传机制

Fast Retransmit

快速重传机制: TCP引入了一种叫Fast Retransmit的算法,不以时间驱动,而以数据驱动重传。也就是说,如果包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。

比如:如果发送方发出了1,2,3,4,5份数据,
数据1先到送了,于是就接收方ack回2,但数据2因为某些原因没收到,3却到达了,于是接收方还是ack回2,
后面的4和5都收到了,接收方还是ack回2,因为数据2仍旧没有收到,于是发送端收到了三个ack=2的确认。
发送方便知道2有可能丢了,于是就发送方重发送2。然后接收端收到了2,此时因为3,4,5都收到了,于是ack回6。

Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择:是重传之前的一个还是重传所有的问题。对于上面的示例来说,因为发送端并不清楚这连续的3个ack(2)是哪次传回来的? 这样,发送端很有可能要重传从2到20的这堆数据(这就是某些TCP的实际的实现)。

SACK

另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看RFC 2018),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版。
比如发送三分数据:100-299,300-499,500-699,中间一份数据丢失,接收方收到500-699之后,会回复ACK=300,SACK=500-700
这样发送端从SACK就知道500前面的一段数据需要重发。

ack_example

Duplicate SACK

施工中


总结: KeepAlive

KeepAlive并不是TCP协议规范的一部分,但在几乎所有的TCP/IP协议栈(不管是Linux还是Windows)中,都实现了KeepAlive功能。
参考 [RFC1122 #TCP Keep-Alives] @Ref

TCP协议栈与KeepAlive相关的参数有:

  • tcp_keepalive_time: KeepAlive的空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2小时)@Uncertain
  • tcp_keepalive_intvl: KeepAlive探测包的发送间隔,默认值为75s
  • tcp_keepalive_probes: 在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次)

以上参数都可以通过sysctl命令修改内核参数实现修改: sysctl -w net.ipv4.tcp_keepalive_time = 7500

KeepAlive默认情况下是关闭的,可以被上层应用开启和关闭, 下面介绍在Java、C语言和Nginx中如何设置KeepAlive

Java(Netty)设置KeepAlive

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.INFO));

Java程序只能做到设置SO_KEEPALIVE选项,其他配置项只能依赖于sysctl配置,系统进行读取。

C语言设置KeepAlive

setsockopt函数原型:

#include <sys/socket.h>

int setsockopt(int socket, int level, int option_name,
const void *option_value, socklen_t option_len);

How to use:

int socket(int domain, int type, int protocol)
{
int (*libc_socket)(int, int, int);
int s, optval;
char *env;

*(void **)(&libc_socket) = dlsym(RTLD_NEXT, "socket");
if(dlerror()) {
errno = EACCES;
return -1;
}

if((s = (*libc_socket)(domain, type, protocol)) != -1) {
if((domain == PF_INET) && (type == SOCK_STREAM)) {
if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "off")) {
optval = 1;
} else {
optval = 0;
}
if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "skip")) {
setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
}
// ...
}
}
return s;
}

代码摘取自libkeepalive源码,C语言可以设置更为详细的TCP内核参数

Nginx配置KeepAlive

Nginx配置里有两处KeepAlive, 含义不同:

  • listen下的so_keepalive=30m::10: 设置TCP_KEEPIDLE为30分钟, TCP_KEEPINTVL默认值, TCP_KEEPCNT设为10 probes
  • upstream下的keepalive N 这里的N指的是每个Worker与upstream服务器可缓存的最大连接数,

常见的几种使用场景:

  • 检测挂掉的连接(导致连接挂掉的原因很多,如服务停止、网络波动、宕机、应用重启等)
  • 防止因为网络不活动而断连(使用NAT代理或者防火墙的时候,经常会出现这种问题)
  • TCP层面的心跳检测

通常很多应用程序也有类似KeepAlive的心跳检测, 区别在于:

  • TCP的KeepAlive发送的数据包相比应用层心跳检测包更小,仅提供检测连接功能
  • 应用层心跳包不依赖于传输层协议,无论传输层协议是TCP还是UDP都可以用
  • 应用层心跳包可以定制,可以应对更复杂的情况或传输一些额外信息
  • KeepAlive仅代表TCP层连接仍保持着,而心跳包往往还代表客户端可正常工作

比较Http协议头中Keep-Alive

在Http Response的http头可以看到下面的字段:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 40026
Connection: keep-alive

HTTP协议采用“请求-应答”模式,当使用普通模式,即非KeepAlive模式时,每个请求/应答客户和服务器都要新建一个连接,完成 之后立即断开连接(HTTP协议为无连接的协议);当使用Keep-Alive模式(又称持久连接、连接重用)时,Keep-Alive功能使客户端到服 务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。

http 1.0中默认是关闭的, http 1.1中默认启用Keep-Alive

@Ref: https://blog.biezhi.me/2017/08/talk-tcp-keepalive.html

总结: TCP中的Timeout

在TCP所有状态中大约有7个超时, 分别是:

Connection-Establishment Timer

在TCP三次握手创建一个连接时,以下两种情况会发生超时:

client发送SYN后,进入SYN_SENT状态,等待server的SYN+ACK。
server收到连接创建的SYN,回应SYN+ACK后,进入SYN_RECD状态,等待client的ACK。
当超时发生时,就会重传,一直到75s还没有收到任何回应,便会放弃,终止连接的创建。但是在Linux实现中,并不是依靠超时总时间来判断是否终止连接。而是依赖重传次数:

  • tcp_syn_retries (integer; default: 5; since Linux 2.2)
    The maximum number of times initial SYNs for an active TCP connection attempt will be retransmitted. This value should not be higher than 255. The default value is 5, which corresponds to approximately 180 seconds.
  • tcp_synack_retries (integer; default: 5; since Linux 2.2)
    The maximum number of times a SYN/ACK segment for a passive TCP connection will be retransmitted. This number should not be higher than 255.

Retransmission Timer

当三次握手成功,连接建立,发送TCP segment,等待ACK确认。如果在指定时间内,没有得到ACK,就会重传,一直重传到放弃为止。Linux中也有相关变量来设置这里的重传次数的:

  • tcp_retries1 (integer; default: 3; since Linux 2.2)
    The number of times TCP will attempt to retransmit a packet on an established connection normally, without the extra effort of getting the network layers involved. Once we exceed this number of retransmits, we first have the network layer update the route if possible before each new retransmit. The default is the RFC specified minimum of 3.
  • tcp_retries2 (integer; default: 15; since Linux 2.2)
    The maximum number of times a TCP packet is retransmitted in established state before giving up. The default value is 15, which corresponds to a duration of approxi‐mately between 13 to 30 minutes, depending on the retransmission timeout. The RFC 1122 specified minimum limit of 100 seconds is typically deemed too short.

Delayed ACK Timer

当一方接受到TCP segment,需要回应ACK。但是不需要 立即 发送,而是等上一段时间,看看是否有其他数据可以 捎带 一起发送。这段时间便是 Delayed ACK Timer ,一般为200ms。

Persist Timer

如果某一时刻,一方发现自己的 socket read buffer 满了,无法接受更多的TCP data,此时就是在接下来的发送包中指定通告窗口的大小为0,这样对方就不能接着发送TCP data了。
如果socket read buffer有了空间,可以重设通告窗口的大小在接下来的 TCP segment 中告知对方。
可是万一这个 TCP segment 不附带任何data,所以即使这个segment丢失也不会知晓(ACKs are not acknowledged, only data is acknowledged)。对方没有接受到,便不知通告窗口的大小发生了变化,也不会发送TCP data。这样双方便会一直僵持下去。

TCP协议采用这个机制避免这种问题:对方即使知道当前不能发送TCP data,当有data发送时,过一段时间后,也应该尝试发送一个字节。这段时间便是 Persist Timer 。

Keepalive Timer

TCP socket 的 SO_KEEPALIVE option,主要适用于这种场景:连接的双方一般情况下没有数据要发送,仅仅就想尝试确认对方是否依然在线。
具体实现方法:TCP每隔一段时间(tcp_keepalive_intvl)会发送一个特殊的 Probe Segment,强制对方回应,如果没有在指定的时间内回应,便会重传,一直到重传次数达到 tcp_keepalive_probes 便认为对方已经crash了。

  • tcp_keepalive_intvl (integer; default: 75; since Linux 2.4)
    The number of seconds between TCP keep-alive probes.
    tcp_keepalive_probes (integer; default: 9; since Linux 2.2)
    The maximum number of TCP keep-alive probes to send before giving up and killing the connection if no response is obtained from the other end.
  • tcp_keepalive_time (integer; default: 7200; since Linux 2.2)
    The number of seconds a connection needs to be idle before TCP begins sending out keep-alive probes. Keep-alives are only sent when the SO_KEEPALIVE socket option is enabled. The default value is 7200 seconds (2 hours). An idle connection is terminated after approximately an additional 11 minutes (9 probes an interval of 75 sec‐onds apart) when keep-alive is enabled.
    Note that underlying connection tracking mechanisms and application timeouts may be much shorter.

FIN_WAIT_2 Timer

当主动关闭方想关闭TCP connection,发送FIN并且得到相应ACK,从FIN_WAIT_1状态进入FIN_WAIT_2状态,此时不能发送任何data了,只等待对方发送FIN。可以万一对方一直不发送FIN呢?这样连接就一直处于FIN_WAIT_2状态,也是很经典的一个DoS。因此需要一个Timer,超过这个时间,就放弃这个TCP connection了。

  • tcp_fin_timeout (integer; default: 60; since Linux 2.2)
    This specifies how many seconds to wait for a final FIN packet before the socket is forcibly closed. This is strictly a violation of the TCP specification, but required to prevent denial-of-service attacks. In Linux 2.2, the default value was 180.

TIME_WAIT Timer

主动关闭连接的一方收到对方的FIN 和ACK之后会进入TIME_WAIT状态, TIME_WAIT持续两个MSL之后才会进入CLOSED

@Ref: TCP协议的那些超时 - On the road

参考 @Ref

Part II: IP

A,B,C,D类IP地址

  • A类地址: 网络地址1字节,主机地址3字节,其中网络地址最高1位必须是0
    • 网络地址范围是1 — 126(0000 0000 — 0111 1111,但第一个地址0000 0000用于表示未知地址,最后一个地址0111 111表示回环地址,两个都不能用)
    • 主机地址占3位,表示一个网络地址中最大主机数是2^24 - 2(主机地址全0表示网络地址,全1表示广播地址,所以减2)
    • A类地址范围: 1.0.0.1126.255.255.254
    • 默认子网掩码255.0.0.0
    • 10.X.X.X是私有地址,范围从10.0.0.0 — 10.255.255.255,这些地址不与外网相连。
  • B类地址: 网络地址2字节,主机地址2字节,其中网络地址最高2位必须是10
    • B类地址范围:128.0.0.1191.255.255.254,每个网络中最大主机数65534
    • 默认子网掩码255.255.0.0
    • 私有地址范围:172.16.0.0 — 172.31.255.255,这些地址不与外网相连。
  • C类地址: 网络地址3字节,主机地址1字节,其中网络地址最高3位必须是110
    • C类地址范围:192.0.0.1223.255.255.254,每个网络中最大主机数254
    • 默认子网掩码255.255.255.0
    • 私有地址:192.168.X.X,范围从192.168.0.0-192.168.255.255,这些地址不与外网相连。
  • D类地址: 不区分网络地址和主机地址,其中地址最高4位必须是1110,用于多播(multicast),即一对多通信。
    • 地址范围:224.0.0.1239.255.255.254
  • E类地址: 不区分网络地址和主机地址,其中地址最高5位必须是11110,保留
    • 地址范围:240.0.0.1255.255.255.254

127.0.0.1 和0.0.0.0

  • 127.0.0.1:回环地址。该地址指主机本身,主要预留测试本机的TCP/IP协议是否正常。只要使用这个地址发送数据,则数据包不会出现在网络传输过程中。
  • 0.0.0.0:这个IP地址在IP数据报中只能用作源IP地址,这发生在当设备启动时但又不知道自己的IP地址情况下。
  • 网络地址全为255则是广播地址

子网掩码

具有相同的前半部分地址的一组IP地址,可以利用地址的前半部分划分组。在一个IP网络中划分子网使我们能将一个单一的大型网络分成若干个较小的网络。
子网掩码(subnet mask)是用来指明一个IP地址的哪些位标识的是网络地址以,及哪些位标识的是主机地址的位掩码。

子网掩码有两种表方法:

  • 255.255.255.0 表示地址的前24bit(3字节)都是子网地址,最后8bit用作主机地址;同理255.255.255.128表示前25bit都是子网地址,最后7bit是主机地址
  • 192.168.1.0/24 可以同时表示ip范围和子网掩码,24表示的含义同255.255.255.0

IP地址范围表示方法CIDR

10.0.0.0/16表示该范围起始地址是10.0.0.110.0.0.0是网络地址),子网掩码是255.255.0.0,所以结束范围是10.0.255.255

  • 10.0.0.0/8表示 10.0.0.1 — 10.255.255.255
  • 10.0.0.0/24表示 10.0.0.1 — 10.0.0.255
  • 172.16.82.0/25表示子网掩码是前25bit都是子网地址,后7bit是主机地址,所以主机数为2^7 -2=126个,表示的范围是172.16.82.1 — 172.16.82.126