主机结构
-
端口
- 端口号:区分同一个主机上不同应用程序的数据包,TCP和UDP在内核中是完全独立的两个软件模块,所以TCP和UDP的端口号是相互独立的,可以绑定同一个端口号
- 多个TCP服务无法绑定同一个ip下的同一个端口号(显示Address in use),除非对socket设置
SO_REUSEPORT - 客户端是可以使用已建立连接的端口进行连接的:一个连接由四元组组成,只要有一个变量变化,就是不同的连接
- 客户端通常使用系统自动选择的临时端口号,服务器则使用知名服务端口号,定义在
etc/services文件中
-
文件描述符
-
两个队列
-
每一个socket执行
listen方法时自动创建,这两个队列是挂在某一个socket下面的 -
SYN队列(半连接队列):收到一个SYN时插入队列发出SYN+ACK,收到ACK后将该队列取出,放入全连接队列
-
底层实现:哈希表;原因:当收到一个客户端ACK时就要取出对应的连接放到全连接队列,用哈希表提高查询效率
-
当半连接队列满了,接收到的SYN报文将被丢弃(SYN攻击)
-
应对方法:开启
syncookies:服务器根据当前状态计算一个cookies值放在TCP包头的seq里随着SYN+ACK报文发出,客户端返回ack报文时取出该值验证,若合法则建立成功,设置syncookies=1代表仅在半连接队列放不下时启用- 为什么不用syncookies取代半连接队列:防止恶意发送大量ACK报文让服务端解码耗尽CPU(ACK攻击)
-
通过增大
tcp_max_syn_backlog的值,并且增大全连接队列来增大半连接队列
-
-
Accept队列(全连接队列):已完成三次握手的连接放进全连接队列,队列长度=
min(backlog,somaxconn),在全连接队列中的连接会等待进程使用accept函数进行调用- 底层实现:链表
- 当全连接队列满了又接收已三次握手的连接,可通过设置
tcp_abort_on_overflow设定服务端的应对,=0:直接丢弃客户端发送的ack报文并开启定时器重传第二次握手的SYN+ACK,=1:发送一个RST报文(只有确定全连接队列会长期溢出时才设置1) - 通过修改配置文件设置
listen(int sockfd,int backlog)里的backlog参数大小 - 通过调用
accept()来从全连接队列里取出一条连接,而accept()该命令跟建立连接是毫无关系的
-
-
IP模块
-
-
从输入队列取数据,进行CRC校验,无误则处理IP头部选项
-
如果是给本机的则派送给上层应用,如果不是或设置了松散源路由或严格源路由,则交给数据包转发子模块处理
- 主机一般只能发送和接收数据而不能转发,通过将
/proc/sys/net/ipv4/ip_forward内核参数置1来实现转发
- 主机一般只能发送和接收数据而不能转发,通过将
-
通过路由表计算出下一跳的路由,然后把所有等待发送的IP数据报放到输出队列
-
静态路由更新方式
route或netstat命令查看路由表sudo route add-host xxx xx:添加主机xxx对应的路由项 ,实现从本机发送给xxx的数据包将从网卡xx直接发送给目标sudo route del-net xxxx netmask 255.255.255.0:删除网络xxxx对应的路由项sudo route del default:删除默认路由项,导致无法访问因特网sudo route add default gw xxxx xx:重新设置默认路由项
-
动态路由更新方式:BGP、RIP、OSPF协议来发现路径并自我更新路由表
-
ICMP重定向报文
- 给接收方提供两个信息:引起重定向的IP数据包的源端IP地址和应该使用的路由器IP地址
/proc/sys/net/ipv4/conf/all/send_redirects指定是否允许发送ICMP重定向报文:一般来说路由器只能发送/proc/sys/net/ipv4/conf/all/accept_redirects指定是否允许接收ICMP重定向报文:一般来说主机只能接收8、88、
-
-
TCP机制
-
超时重传机制
- 对于ACK报文不会重传,而是由对方发现未收到回应后由它重传
- 在发生数据包丢失或确认应答丢失时重传,超时时间RTO,RTO略大于RTT
- 每次超时时间为上一次的两倍,如第一次为1s,第二次超时时间为2s,以此类推
- 最大重传次数:由主机上的某个参数控制
-
快速重传机制:当接收方没有收到seqx时,即使它收到了seqx+1,seqx+2,seqx+3也都会回ackx,当发送方接收到了三个相同的ACK报文时,会在定时器过期之间重传丢失的报文段,但是不好确定是重传一个报文,还是重传那个报文后的所有报文,可以使用SACK解决
-
SACK:在TCP头部选项中添加SACK的东西
-
用来通知那些数据已经收到了
比如接收方发送消息 ACK 200 SACK 300~400,说明200前的数据都已经收到了,300到400的数据也都已经收到,发送方就可以判断出200 ~ 300的数据丢失,重传这些数据后,接收方即可发送消息ACK 400
-
D-SACK,用来通知有哪些数据重复接收了
比如接收方发送消息 ACK 4000 SACK 3000~3500,说明4000前的数据都已经接收到了,3000 ~ 3500的数据被重复接收了
-
-
-
滑动窗口
-
解决问题:一个一个地发数据收到ack后才发下一个效率太低,使用滑动窗口,可用窗口内的数据可以批量发送,窗口大小:无需等待ack而可以继续发送数据的最大值
-
操作系统开辟的缓存空间,在等到ACK前,已发送的数据保留在缓存区内,直到收到了ACK,且某个ACK丢了也没关系,因为采用的是累计确认(收到了ack=x,意味着x之前的数据已经全部收到)
-
TCP头部的window字段接收方告知发送方自己的窗口大小(发送方发送的数据必须小于这个大小)
-
- 发送窗口为一个滑动窗口,其中有一部分是可用窗口,当可用窗口内的数据全部发送(即窗口耗尽),那么必须等到收到新的一轮ACK之后,滑动窗口向右移动给可用窗口让出空间才能继续发送
SND.WND:发送窗口的大小(由接收方指定)SND.UNA:绝对指针,指向发送窗口的第一个字节的序列号SND.NXT:绝对指针,指向可用窗口的第一个字节的序列号SND.UNA+SND.WND:相对指针,指向#4的第一个字节SND.UNA+SND.WND-SND.NXT:可用窗口大小
-
RCV.WND:接收窗口大小RCV.NXT:接收窗口第一个字节的序列号,即期望从发送方发来的下一个数据字节RCV.NXT+RCV.WND:相对指针,指向#4的第一个字节- 接收窗口是约等于发送窗口的,因为接收方告知发送方自己窗口大小的传输过程存在时延
-
-
流量控制
-
目的:发送方根据接收方的接受能力控制发送的数据量
-
TCP规定必须先收缩窗口,过段时间再减少缓存
- 如果同时减少(减少缓存即减少窗口大小),当发送方发送完自己窗口内的数据时,再接收到收缩窗口的通知,就会有一部分数据包接收方接收不到,而发送方的发送窗口大小会变成一个负值
-
TCP为每个连接设一个持续计时器在发送方收到零窗口通知时启动,一旦计时器超时就会发送窗口探测报文,以探测对方当前的窗口大小,一般探测次数为3次
- 解决场景:当发送方收到零窗口通知后,接收方接收窗口重新调整,但他告知发送方的窗口通知ack报文丢失了,就会出现发送方在等窗口通知,接收方在等数据的死锁局面
-
TCP规定
-
接收方不能通告小窗口:当窗口小于min(MSS,缓存空间/2)时,直接告知发送方窗口为0;
-
发送方不能发送小数据:Nagle算法,满足条件才能发送数据:窗口以及数据 大于MSS或收到之前发送数据的ACK,发送方在等待的时候囤积数据,直到满足以上条件
-
解决场景:接收方的窗口越来越小,导致发送方单词发送的数据也越来越小,性价比太低,糊涂窗口综合征
-
要同时满足接收方不发小窗口+开启Nagle算法
因为如果只有Nagle算法,接收方回复ack的速度很快,一样会让发送方发送数据
-
telnet 或 ssh 这样的交互性比较强的程序,需要关闭 Nagle 算法
-
在Socket设置
TCP_NODELAY来关闭该算法
-
-
-
-
拥塞控制
-
目的:避免发送方发送的数据阻塞整个网络
-
拥塞窗口:cwnd:由发送方维护的状态,当网络顺畅则cwnd增大,当网络堵塞则缩小
-
慢启动算法:当发送方每收到一个ACK,cwnd的大小就会+1,直到cwnd==ssthresh为止
-
拥塞避免算法:当cwnd >= ssthresh之后,每个轮次不管发送方收到了几个ACK,cwnd的大小只会+1,呈线性增长
-
拥塞发生算法:触发了超时重传机制,ssthresh设为当前cwnd的一半,然后将cwnd置为初始值,重新开始慢启动(所以可能导致网络卡顿)
-
快速恢复算法:
- 接收方丢了一个中间包,发送三个重复的ACK给发送方,发送方即进行快速重传
- 将cwnd置为当前的一半后,将ssthresh置为cwnd
- 然后cwnd=ssthresh+3
- 重传丢失的数据包
- 如果再次收到重复ACK,cwnd+1
- 如果收到新数据ACK,将cwnd设置为ssthresh的值,然后开始拥塞避免
-
-
-
处于TIME_WAIT状态的服务端如果收到了合法SYN报文则会直接进入SYN_RECV状态,如果收到了非法的SYN报文则会再发送一个四次挥手的ACK报文,客户端发现该ACK报文不符合自己预期则回复RST
- SYN合法的判断:客户端的SYN序列号大于服务端的确认应答号,且时间戳大于服务端的时间戳
-
接收到对方数据时,若序列号不符合自己预期或自己处于close状态(客户端)或未监听对方所要求的端口(服务端)将回RST
-
处于Established状态的服务端如果接收到客户端针的SYN报文,则会回复Challenge ACK:包含了原连接的正确序列号和确认应答号
- 利用该机制的工具:killcx:伪造发送一个SYN报文后获取正确的确认应答号,然后发送RST报文,将连接关闭(可关闭活跃或非活跃的TCP连接)
-
处于FIN_WAIT_2状态的客户端,收到乱序的FIN报文会将FIN报文放进一个红黑树实现的乱序队列,当后续读取到了正确顺序的数据包时会检索这个乱序队列有没有接下去按序的报文,如果有则读取,若该数据报文是FIN报文,则进入TIME_WAIT状态
-
TCP接收端只有在接收到URG标志时才检查紧急指针,并将带外数据读入到只有一个字节的带外缓存,若没有及时读出它将会被下一个带外数据覆盖
- 设置
SO_OOBINLINE选项:带外数据将和普通数据一起放在TCP接收缓冲区,通过紧急指针以及socket提供的系统调用来识别带外数据
- 设置
-
TCP keepalive机制:TCP保活机制
- 在一个时间段内
net.ipv4.tcp_keepalive_time若一个连接没有数据交互会启动TCP alive机制:每间隔一个时间net.ipv4.tcp_keepalive_intvl发送给对端一个探测报文,若连续几个net.ipv4.tcp_keepalive_probes探测报文都没有得到响应,则内核告知连接死亡,并将错误信息发送给上层应用程序 - 如果有一个探测报文得到相应,则会重置TCP保活时间
- 需要通过socket接口设置
SO_KEEPALIVE才能生效 - 在应用层自我实现心跳机制:通过
keepalive_timeout参数指定HTTP长连接的超时时间,由web服务软件启动定时器,如果超时没有接受新请求即释放连接
- 在一个时间段内
-
TCP fast open机制:减少TCP连接建立的时延
- 客户端和服务端第一次连接时,服务端在SYNACK中放上一个cookie
- 客户端第二次跟这个服务端连接时,可以在第一次握手时放上SYN+cookie+请求报文,跳过三次握手阶段,服务端接收后可以直接回复SYN+ACK+Data,实现HTTP请求只需要1RTT
-
TCP 延迟确认机制:减少小报文的传输
-
服务端并不马上确认上次受到的数据,而是延迟一段时间,将需要发送的数据跟确认信息一起发出,减少发送TCP报文段的数量;
-
如果在延迟等待响应数据的期间收到了对端第二个数据报文,才立刻ACK
但客户端由于用户输入慢于计算机处理速度,所以确认信息立刻发出,length都为0
-
-
TCP异常断开连接机制
- 客户端主机崩溃:服务端无法感知,会一直处于establish状态,直到发送数据(然后超时重传)或触发TCP keepalive机制或服务端重启进程
- 有一方的进程崩溃后,其操作系统在回收崩溃进程资源的时候,会发送FIN报文给对方进行四次挥手,因为TCP连接由内核维护所以该过程不需要进程的参与
- 客户端主机重启后接收到数据:若主机上没有进程绑定TCP报文的目的端口,则回复RST;若有进程绑定目的端口号,但之前连接的数据结构已丢失,故内核栈找不到socket结构体,也回复RST报文,重置连接
-
服务器推送技术
- HTTP不断请求,客户端不断进行隐式的HTTP请求,服务端接收到登录请求后给出回应,但可能会造成卡顿
- 长轮询机制:HTTP请求的超时时间设置大一点,在服务端进行轮询,如果有登陆请求就立刻返回,如果超时就重发请求,应用于如RocetMQ