【Linux】网络基础(2)

前言

        本篇笔记记录我在Linux系统下学习网络基础部分知识,从关于网络的各种概念和关系开始讲起,逐步架构起对网络的认识,对网络编程相关的认知。本篇继承自网络基础1,感兴趣的可以看看哦~

        这篇文章我会记录学习https协议的所思所想,然后过度到传输层,学习udp协议和tcp协议的相关细节与思想。

我的前一篇Linux笔记:

【Linux】网络基础(1)_柒海啦的博客-CSDN博客

网络套接字编程-UDP和TCP:

【Linux】网络套接字编程_柒海啦的博客-CSDN博客

目录

一、应用层:https协议

http&&https

加密方式

        1.对称加密:

        2.非对称加密:

        3.数据摘要(数据指纹)、签名:

https的工作探究:

1.方案一-只使用对称加密:

2.方案二-只使用非对称加密:

3.方案三-双方使用非对称加密:

4.方案四-对称加密和非对称加密同时使用:

中间人攻击

CA认证:

5.方案五-CA认证+非对称加密和对称加密:

如何成为中间人

二、传输层

1.再谈端口号

netstat的使用

xargs命令的使用

2.udp协议

udp协议端格式

具体的报文字段和理解

理解全双工和半双工

基于udp的应用层协议

3.tcp协议

TCP协议端格式

确认应答机制&&序号

16位窗口大小

6个标记位

16位紧急指针

TCP建立链接:三次握手

TCP断开链接:四次挥手

setsockopt

超时重传

流量控制

滑动窗口

高速重传 

拥塞控制

延迟应答

捎带应答

TCP总结


一、应用层:https协议

        当今在应用层主流的协议中便就是https协议了。它实际上就是http的一种安全版本。

        因为在http中传输信息的时候是明文传送的。如果此时存在中间人进行转发接受,那么就会出现信息泄露的问题,并且能够实时的监控,假扮另一方,导致客户端和服务器端都不清楚自己是否泄密。

        现在,先理解一下何为安全?安全对于计算机或者网络来说,就是将信息进行加密处理,传递到后利用解密将其提取出来,从而达到安全的效果。简而言之:安全 = 加密 + 解密

        但是,在现在这个社会上不存在绝对的安全。即使信息通过加密传输,但是别人还是可以破解的,所以严格的定义安全应该从本质进行考虑:即破解的人为何破解其信息?从好处去想的话,安全应该是破解的成本远远大于破解的收益

        我们可以先来看看http和https之间的关系,然后在谈https的安全方式。

http&&https

        首先,可以具体的看一下网络协议栈的内容:

         可以发现,我们给http套上一层安全层后便就成了https协议,所以我们这里只需要重点介绍安全层即可,其余的和http一致。

        由于https涉及的知识非常多。所以这里也只是对其工作方式了解即可。

        因为,tcp协议是面向字节流的,所以就http协议的明文传输很容易被中间人获取,遭到中间人攻击。https就明文传输,对其明文进行一个加密,传到对应主机根据密钥进行解密,从而获取信息,达到不是传输明文的过程,而是传输密文的过程。所以,可以看到,加密和解密需要中间数据:密钥。

        针对于加密,我们首先了解几种加密的方式。

加密方式

        此处知识点与数学相关,这里只是简单了解。

        1.对称加密

        利用同一个密钥对信息进行加密和解密。-加密算法有DE3、3DES、AES....

        特点是:算法公开、计算量小、加密速度快、加密效率高。

        明文-密钥->密文;密文-密钥->明文;

        这里可以简单的举一个对称加密的例子:

        按位异或就是一个非常简单的对称加密。

        比如我设置密钥key = 2,发送明文:6 ,将其按位异或后得到密文:100。另一台主机得到密文100后根据密钥key按位异或解密得到明文110,即6。

        实际上针对于按位异或:a^b = key; key^b = a;也就是说自己与自己异或就是0,任何数与0异或都是自己。

        2.非对称加密

        利用一个密钥对明文进行加密成密文,另一个密钥对密文进行解密解密为明文。这两个密钥一个公开,一个私有。至于怎么分配公开和私有两个密钥随意。

        常见的非对称加密算法有:RSA DSA ECDSA......

        非对称加密算法强度复杂、但是加密解密速度没有对称加密快。公钥和私钥是互相配对的,缺点就是运算速度非常慢。公钥就是明文传输在网络上的,私钥就是自己保存好。

        明文-公钥->密文;密文-私钥->明文;(当然,也可以反着来)

        3.数据摘要(数据指纹)、签名

        对于明文(原始文本),利用一个哈希函数,将原始文本转化为一串固定长度的字符串。此时这个字符串就被称为数据摘要,也叫做数据指纹。利用哈希,能够让任意文本经过此函数都是不一样的,并且生成的不可逆转。一般用于压缩文本的作用。

        对于数据摘要,经过加密后便就得到了数据签名。数据摘要的作用就是确定数据的唯一性的。

        在了解了一些加密方式后,我们来探究一下https的安全层是如何进行保护的:

https的工作探究:

        根据上面的加密方式,我们从一个个开始逐步摸索https的加密方式:

1.方案一-只使用对称加密

         可以看到,由于是对称加密,所以需要双方都有一把同样的密钥。这样依赖密钥就是明文发送了,被中间人获取后这样还不是没有任何区别。

2.方案二-只使用非对称加密

         看着挺安全的,但是注意这样的话只能单向传输,也就是说只能客户端加密信息给服务器,但是服务器给客户端信息的话就无法加密了。

3.方案三-双方使用非对称加密:

         此时貌似可以,并且双方都可以进行加密通过密文传输,似乎是安全的。但是我们要注意到,因为本身非对称加密的速度就很慢,如果此时出现两对密钥的话,效率会非常慢,所以我们应该考虑效率更加高的加密。

4.方案四-对称加密和非对称加密同时使用:

         可以发现,我们只需要综合两种加密方式的特点(对称和非对称),利用非对称其中的公钥能够在网络中明文传输,然后加密对称密钥M,让双方在密文的条件下传输对称密钥,从此之后使用对称密钥加密数据,此时不就很完美了嘛,效率问题也得到了很好的解决。

        但是就于234这三种方法也就是利用非对称传输公钥这种做法真的安全吗?我们可以简单的了解一下中间人攻击:

中间人攻击

        针对于方案四,中间人攻击有如下的方式进行应对(23同理,所以说234一样的不安全)

        可以发现,即使公钥是可以公开的,但是中间人可以模拟服务器向客户端发送自己的公钥,达到让客户端以为公钥就是服务器的,从而导致数据泄露的问题。

        所以只要传递公钥,只要存在中间人拦截,中间人完全可以攻克。但是我们只要交换了密钥了,中间人来了就晚了,但是中间人最开始来了,就可以进行篡改替换。 

        所以中间人攻击能够成功的本质是:

1.本质是中间人能够对数据进行篡改。

2.客户端无法验证收到的公钥是合法的,即是不是对方服务器的公钥。

        那么我们想要安全的进行传输的话,就要破坏者两层本质,只要让中间人无法对数据进行篡改以及客户端能够验证公钥是合法性的,那么就是安全的!

        在介绍方案5之前,我们先了解一个CA认证,然后就能理解利用认证功能的方案4就可以有效的防止中间人攻击了。

CA认证:

        为了解决中间人攻击引发的问题,我们需要让客户端实现对server传入的公钥合法性进行认证。

        那么我们就需要一个权威的机构-CA机构,颁发证书-CA证书。利用CA证书对合法性进行认证。

        我们利用如下图理解一下Client认证合法性的过程:

        当我们审核通过后,CA便就会向服务端签发证书,我们先重点理解一下签发证书这个过程:

        首先是我们发送的那些认证个人信息加上公钥(利用算法生成)通过哈希函数生成一个数据摘要(数据指纹),然后利用签名者(CA机构)的私钥加密,此时就生成了一个数据签名。然后将数据签名+数据本身返回给server就是签发了一次证书。

        那么验证的时候是如何验证的呢?

        客户端接受到数字证书(数据 + 数据签名)后,首先将其分开,将数据利用哈希函数-得到一个散列值也就是数据摘要。然后利用CA的公钥,对数据签名进行解密得到数据摘要。此时两个数据摘要进行比对,如果一致说明并没有遭到中间人攻击,如果不一致那就说明遭到中间人攻击了,提示用户。

        那么这个过程为什么就能防止的了中间人攻击呢?因为这样客户端就知道发生了中间人攻击了,从而终止通信,防止数据泄露,而不像方案4那样傻乎乎的继续通信呢。

        我们让客户端能够识别公钥是否是合法的。因为首先客户端是利用CA-权威机构的公钥对数据签名进行解密的,也就是说中间人由于无法获得CA的私钥那么就无法伪造数据签名。所以即使中间人将数据证书中的数据替换了,由于哈希函数生成的数据摘要不一致便就可以让客户端进行识别出来。

        对于CA的公钥,客户端对签名做解密,只用自己内置的CA公钥。

        如果中间人将全部信息替换了,也就是说不伪造数据证书,自己去申请一个,数据也替换为自己的。别忘了,是客户端向服务器通信,在发送通信前肯定知道目标ip和端口号的啊,证书里面是存在这些的,即使你是真的,但是你不是服务器呀,所有没有作用。

        综上,加上了CA认证的方案四就是方案五如下图所示:

5.方案五-CA认证+非对称加密和对称加密:

         可以看到在https的安全层中,存在三组密钥的。第一个是CA官方的非对称密钥、第二个是服务器端的非对称密钥、第三个是对称密钥-用于加密密文的。我们做了这么多保护实际上就是保护明文是以密文进行传输的,让黑客无法破译此密文。而无法破译此密文的话就要阻止其拿到对称密钥,就要对对称密钥进行加密。但是加密过程中防止黑客伪造,加上了CA认证识别机制,从而保证了对对称密钥加密的安全性。从而https传输的安全性也就得到了保障了。

        那么中间人是如何做到在客户端和服务器中间进行转发接受消息的呢?

如何成为中间人

        ARP欺骗、ICMP攻击、假wifi&&假网站。

        可以自行去了解哦~

二、传输层

1.再谈端口号

        当我们对应用层有了一定的了解后,我们就可以继续向下学习一层和操作系统内核相关的协议-也就是传输层。

        在套接字编程中,我们需要显示的给服务器绑定对于的端口。此端口也就是对应主机中的一个进程,并且一个端口只能对应一个进程,一个进程可以对应多个端口。

        至于客户端,由于提供网络服务的客户端很多,所以不明确绑定了哪些端口,所以我们交给操作系统进行自动绑定。

        现在,我们可以认为在TCP/IP体系中,{源ip,源port,目标ip, 目标port, 协议(UDP/TCP)}这样的一个五元组来标识一个通信

        对于端口号,我们知道它是一个无符号16位整数,所以它的取值范围应该是:0~(2^16 - 1)。

0 - 1023:知名端口号。http(80)、https(443)、FTP(31)、ssh(22)这些广泛的应用层协议,其端口是固定的。

1024 - 65535:操作系统动态分配的端口号。客户端程度的端口号,就是由操作系统在这个范围里划分。

        所以我们一般在绑定端口的时候,需要避免这些知名端口。而对于内核来说,一般会将端口号和进程id利用哈希表映射到一起的,端口号找到对应进程是通过哈希表的。

        在Linux命令行的环境下,我们可以使用netstat查看传输层协议传输的进程 。

netstat的使用

查看网络状态进程的。

选项:

        -n 能显示成数字的全部显示成数字。

        -t TCP协议的通信

        -u UDP协议的通信

        -a 全部显示出来(有些默认不显示)

        -p 查看其进程状态

        -l 查看listen状态的服务

xargs命令的使用

        另外,实际上可以利用命令pidof可以查看对于服务的pid,后面跟上服务的名称即可。比如我们下面查找此守护进程。(守护进程一般结尾为d-命名规范)

         我们如果想将此数据通过管道传给另一个指令作为“命令行参数”而不是标准输入该如何使用呢?

        这个时候利用xargs就可以将标准输入的内容转化为命令行参数的内容了。

        pidof TCPServer | xargs kill -9
        可以发现此时确实将进程杀掉了。

2.udp协议

        用户数据报协议。

        在对端口进行了一个重新的认识后,我们在来详细的了解一下传输层udp协议的原理。

        在网络套接字编程中,我们发现我们使用TCP或者UDP协议实现的客户端与服务器模式就是为上层应用层做服务的。比如http、我们自定义的网络计算器。

        在之前网络基础1的认知中我们知道,应用层就是和计算机系统的上层一起的,而传输层和网络层则是和操作系统内核一起的,数据链路层是驱动程序。

        因为是向上层服务。所以我们要从下层收到的报文进行分离或者从上层收到的报文进行封装,然后就是如何交付给上层。也就是说,我们首先就要将报头和有效载荷进行分离。

udp协议端格式

         通过上述图片,我们可以发现,udp协议的报头是一个固定的大小的,大小为八字节。也就是16*4 = 8 * 8(1字节为8比特)。针对于源端口号也就是未来需要将其向下传送,给对于主机进行发送的报头属性,而目的端口号就是我们向上应用层交付的依据。通过提取报头属性便就可以获取其端口号,从而通过哈希找到对应进程,将有效载荷给它copy进去即可。那么如何剪裁出有效载荷的呢?可以发现,存在16为的udp长度,在这里面减去固定的八字节长度剩下的不就是有效载荷了嘛。当然,如果UDP检验和出错的话自然udp整个报文是直接被丢弃的。

        实际上,对于8字节的报头属性,是存在一个结构进行描述的。struct udp_hdr。它利用位段的形式给每个变量分配内存:

struct udp_hdr
{
    uint32_t src_port: 16;
    uint32_t dst_port: 16;
    uint32_t udp_len: 16;
    uint32_t udp_check: 16;
}

         在封装的时候,有应用层向传输层传递,根据源port和目标port进行填入,将有效载荷的长度统计,标注好检验和即可。形成报文然后向下一层发送。

        在解包的时候,首先提取报头数据:(struct udp_hdr*)start->src_port;......提取出报头的每一个字段,然后就可以分离载荷,根据port向上交付给应用层进程去。

        当然,也有可能出现很多的报文,即传输了大量的信息。此时就要进行管理起来,即内核数据结构sk_buff。进行收集管理。

具体的报文字段和理解

首先,这样的约定双方肯定是知道的,也就是采用了TCP/IP体系方可通信。

如何理解UDP的不可靠和无连接

        丢包,没有确认和重传机制,网络故障无法提示对方。(注意这里的不可靠并不是贬义词,而是形容的一种特点:正因为没有设计这些机制,从而可以让其成本低以及代码简单,有些情况下就可以进行使用)

面向数据报如何理解

        应用层发给传输层多少数据,原样发送,不会拆分,也不合并。也就是说是直接将有效载荷给缓冲区的,不会对其进行任何处理。

那么操作系统提供的比如sendto、recvfrom等接口如何理解

        首先我们要理解到诸如sendto、write、recvfrom、read等接口是拷贝到缓冲区或者从缓冲区拷贝到应用层的,而不是直接发送到对方。发送是由传输层管的。

        对于udp来说,使用sendto的话一般直接调用内核进行拷贝,不存在发送的缓冲区。

        但是udp具有接受缓冲区,但是不可保证收到的udp顺序和发送的顺序一致,缓冲区满了就会一定丢弃。

        udp的socket既能读也可以写 - 是一种全双工模式。

理解全双工和半双工

        全双工:对于一个io对象,既可以读取数据,也可以写入数据,两者互相不影响。一般这种情况是两个缓冲区(一个发送缓冲区和一个接受缓冲区)使用,互相不影响。

        半双工:要么听,要么写,两者不能同时发生。

        对于udp来说,根据字段udp_len来看,似乎只能存储16位的数据,也就是2^16字节 = 2^10 * 64  = 64k。这个大小在当今互联网环境下是一个非常小的数字了,所以一般传输多个数据的时候是传多个,分包,自己手动拼装。

基于udp的应用层协议

NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议

        当然,也包括你自定义的利用udp套接字程序的自定义的应用层协议。

3.tcp协议

        传输控制协议。

        同样的,理解此传输层协议就需要从两方面进行理解:1.如何封装(解包)传来的报文?2.如何交付

        和udp一样,我们先来看一下tcp传输层协议的报文结构。

TCP协议端格式

         可以看到,tcp就udp的报头结构复杂的多,因为报头结构一共5排,每排就是32字节,所以标准的tcp报头前5排大小位20字节。

        如果交付的话,我们首先需要根据此报头信息找到此通信的目的端口号,然后通过哈希映射到此内存将有效载荷拷贝到文件缓存区内,供上层调用。

        针对于4位首部长度,因为是4位,那么表示的范围为0 ~ 60字节。它表示的就是报头+选项的总长度。因为选项是可变的,所以报头它也是可以边长的,最多到60字节。

        我们交付的话首先是要解包的。

        解包和udp解包类似,在操作系统中同样存在一个struct tcp_hdr(位段实现)。只不过它里面应该保存的是前20字节中的数据。所以当报头读完后,剩下的就是有效载荷了。

        但是我们需要注意的是,在tcp的报头中不存在整个报文的数据长度,也就是说TCP是面向字节流的。原则上无法判断报文和报文的边界。此时该如何被解释,是应用层该操心的事情。

确认应答机制&&序号

        我们知道TCP协议是可靠的,也就是说能够保证通信是收到的。这是如何做到的呢?

        好比两个人之间交流,如果离得近,可以说这个通信十分的流畅。但是一旦距离远了,通信不流畅了,成本增加(需要重复去说),可靠性减少。 原因就是距离变长了。

        在操作系统单机内部不谈可靠性(毕竟都在整机内,距离很小),谈可靠性的属于网络-TCP/IP,也就是一个主机发送的消息,对方是否接受到了。(接受到了类似于交流,我听清了你的说话)

需要注意的是:

        1.网络中,存不存在100%可靠的协议呢?自然不存在。因为无论是双方主机,无法保证自己作为最新发送数据的一发被对方收到。但是,在局部上能做到100%可靠:当一个主机收到应答了,就能保证上一次发送数据是可靠的。 -也就是保证历史的可靠性。-本质就是我发送出的所有消息,只要有匹配的应答,能保证我刚刚发出的消息对方一定收到了。TCP协议的确认应答机制:但是只要一个报文收到了对应的应答,就能保证我发出的数据对方收到了。TCP三次握手。
        2.但是无法保证不可丢包,我只能识别出来,并且保存上次没有丢包的数据。

        所以,只要我回应了你,你就能确认上一次交流你的话我听清楚了,这也就是可靠性。

         那么在报头属性中如何标识被接受到了呢?并且如何解决这个顺序的问题呢?

引入概念:序号
    注意发送的报文可不是单纯的字符串,而每一个报文一定携带了完整报头的TCP报文。所以包含了32位序号。比如第一个报文序号是1000,第二个2000,第三个3000。发出去后给应答,server给应答后一定会给序号,并且会在基础上加1.比如收到的就是确认序号: 2001 1001 3001 此时就可以将请求报文和应答报文对应上。(确认序号表示:对应的数字之前的报文全部收到了,告诉对方下次就从确认序号指明的序号进行发送  
    1.将请求和应答一一对应。
    2.确认序号表示的含义:确认序号之前的数据已经全部收到 - 客户端不怕部分丢包了。
    3.允许部分确认丢失,或者不给应答。
    4.为什么要有两个字段数字?TCP是全双工的。任何一方即可能收,也可以发。任何通信的一方都是全双工的,在发送确认的时候也可能携带新的数据。
    5.乱序是不可靠性的存在。但是已经解决了,因为报文中的会携带序号,只需对多个序号进行排序,就可以按序到大了

        保留六位就是报文中暂时不用的部分。下面针对发送和接收数据,我们可以根据如下图进行理解:

        TCP解决的问题就是如何发送的问题,-传输控制协议-。TCP通信本质就是发送和接受缓冲区进行来回拷贝 - 拷贝的介质就是网络。TCP有发送和接受缓冲区,有client和server就有两队接受和发送缓冲区。是独立的进行发送和接受,所以TCP就支持全双工了。 

16位窗口大小

        那么双方在发送与接收消息过程中,如果一方发送消息过快,另一方向上传递过慢(发送到对方的缓存区中),我们需要让发送方变慢一点。那么这个慢的量该如何决定呢?

        好比老师上课的时候,如果在讲解过程中他讲快了,我们同学就可以举手和他说明。而这个举手和其说明的过程不就是在告诉老师我的具体情况,让他根据此情况进行修正的嘛。对于TCP协议中,报头中存在16位窗口大小就帮我们存储着类似的信息。

        因为双方通信,如果一方想要了解情况从而调节发送速度的话就是根据其缓冲区中的剩余空间。16位窗口大小,也就是剩余空间大小。并且此窗口大小是谁发的,就是被发者填写-是要发给对方的。对方根据此大小来调节对被发送方的速率。同时也是双方的流量控制。-报文交换。

6个标记位

        标准的标记位是6个bit的。当然也需要视实际情况。

        首先,我们需要确认一件事:标记位有啥用?为啥需要这么多标记位呢?

        想一下,既然叫标记位,那么自然是标记报文类型的。让接收方根据此类型从而做出不同的处理动作。

        比如client可能给对方server发送不同类型的报文:常规、建立连接、断开连接、确认等报文(反过来同理)

        需要注意,不同的报文处理的方式是不同的。比如建立连接 - 对此链接进行处理,断开就要进行释放。所以服务端可能会收到大量的不同的报文。

        各个标记位的含义:

标记位 含义
SYN 请求连接
FIN 断开连接
ACK 应答特征
RST 连接重置
PSH 催促向上交付
URG

紧急标记

        对于ACK来说,凡是报文存在应答的属性就需要置为1(比如不存在数据-没有有效载荷,只发送一个报头进行应答的,和在回复消息的同时就进行应答了)。

        对于RST,重新进行tcp建立连接。这种情况主要发生在连接后,服务器重启,客户端不清楚,仍在发此时服务器就会发送这个。

        对于PSH如果服务器向客户端发送报文的16窗口剩余数据为0,然后客户端反复等待,会发送带有PSH的报文,催促服务器尽快向上取走。

        对于URG,它是紧急标识,所以需要配合16位紧急指针来说了。

16位紧急指针

        因为tcp是具有按序到达机制的(优点)。我们发送的数据的时候,被对方上层读取到,必须有顺序。但是如果想插队呢?

        如果此时报文有URG了,那么通过机制,读取16位紧急指针的位置,尽快的向上交付。16位紧急指针指向的是:在有效载荷中的偏移量。并且只是一个字节的数据。一般用于机器-系统管理,而不是正常的业务处理。

        现在,就上述的标记位,我们来结合一些重要概念加深对其的理解。

TCP建立链接:三次握手

        对于一个server来说,在未来可能存在大量的client进行连接。那么server一定存在大量的链接。对于这些链接OS一定是要进行管理的-先描述在组织。

-如何理解链接?

        本质是内核的一种数据结构类型,建立连接成功的时候,就是在内存中创建对于的链接结构对象,在对多个对象进行某种数据结构的组织。

        并且,OS维护链接是有成本的。(内存资源 + CPU资源)

-理解三次握手

        首先,我们利用下面的图来理解三次握手的过程:

注意:

        1.图中表示的SYN和ACK都是带有此标记位的报文,实际传送的还是报文,不要被误导了。并且根据实际情况,发送到对方的时候是需要一定的时间的,所以线是倾斜的。蓝色英文单词表示此网络进程的状态。

        2.三次握手不一定要保证成功。-可靠性是建立在链接成功后的状态。注意最后一个ACK的报文发送后不一定保证其一定能收到的。

        3.对于client,最后一次一旦将ACK报文发送出去,就需要将此状态置为ESTABUSHED。对于前两次,一定保证不丢包(存在应答,如果没有,会触发后续的处理)。但是实际上,最后一次的ACK报文可能对方不一定收到。-所以可能失败。只有服务端真正的变为ESTABUSHED状态,才能建立成链接。

-为什么要三次握手?

        根据上面的三次握手图解以及注意事项。我们能理解一下为什么选择了三次握手嘛?又为何不选择一次握手,二次握手,甚至是四次握手或者更多次握手呢?

        首先,我们需要了解一个对于服务器的安全隐患,比如SYN洪水。拒绝服务攻击

        明白一个前提:OS维护链接是有成本的。一台服务器难免会有成千上万台的客户端进行链接。这么多链接服务器的操作系统是必须要管理起来的。一旦管理起来,那么就要耗费服务器的内存和cpu资源的!

        一旦有恶意分子,平白无故的对服务器端进行多次重复无意义的申请,就可能导致服务器端因为资源消耗过多,导致OS挂掉此服务器端进程,从而导致服务器端崩溃。

        就于消耗服务器端资源来说,TCP的设计应该尽量让服务器端付出的成本和客户端一样多或者更少。但是TCP的主要目的只是为了方便网络通信,并没有对安全考虑太多。所以实际上TCP的设计应该是验证其TCP服务的双端的全双工,安全只是顺带考虑了一下而已。

如果是一次握手

        那么发送一次,server就应该建立一次链接。但是server维护链接是有成本的。如果客户端发送多次,那么服务器很可能直接挂掉。(SYN洪水) ( 但是实际上三次握手也不可防止这种类似的轰炸,只不过成本变高了)

两次

        类似第三次发送,服务器无法得知客户端是否知道了或者恶意丢弃.但是此时链接已经建立了。所以类似第一次的那种攻击,一样的容易让服务器挂掉。

三次

        三次的话,可以发现,发起连接的需要完成一次应答。即完成一次应答,发送的端率先进入ESTABUSHED状态被发送端只有收到应答后才会建立ESTABUSHED状态。时间会往后推。这样的话,像之前的两种攻击客户端对于服务器的支出成本客户端偏低。而现在,客户端的成本是偏高的-如果攻击成功,那么就是同等的资源支出。不能杜绝,但是可以有效的抵御。注意之前是指单机攻击成本。

四次

        四次的话可以发现,此时任然吧最后的ACK报文交给发送端了。如果发送端是恶意的,会造成两次发送类似的事情,同样不可取。

        实际上安全保证还是应用层上进行相关措施的,三次握手的目的并不是为了安全,1.而是在进行建立连接的过程中顺便让客户端能够承担于服务器端一样的压力。2.验证全双工。验证全双工好比我们打电话的时候,我打通过去,对方说句喂,可以表示他接通了,你听得到吗?我也回应了一个喂,表示我听到了,现在我们的通话可以开始了-此时就是验证我能听能说,对方同样如此。

        其实可以得到一个简单的结论 奇数次握手:压力偏向发送方

        对于信号RST,也有可能,如果第三次发送的ACK服务器没有接收到,如果客户端瞬间发送消息 - 此时服务器接收到了,因为没有建立连接成功 - 服务器会向次客户端发送RST-连接重置。当然这里只是简单的举例,实际上出现机会很小。

TCP断开链接:四次挥手

        同样的,两端在建立链接需要三次握手,在断开链接的时候需要双方明确,所以需要四次挥手。

什么是CLOSE_WAIT?

        CLOSE_WAIT可以发现,只要客户端主动发送断开连接标记的报头(FIN),服务器端此时就会处于一个CLOSE_WAIT状态。

        那么如果此时服务器端不向对应的客户端发送FIN-即断开连接,那么此时服务器就会存在大量的CLOSE_WAIT状态的链接。

        我们用一个简单的实验证明一下上述服务器存在的CLOSE_WAIT状态。很简单就能够实现,也就是说服务器端接受对方关闭链接后不进行关闭文件描述符即可。(下面写一个测试代码,顺便回顾一下TCP原生写网络通信的流程-测试的时候只使用服务器端,客户端利用telnet代替即可)

// TCPServer.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>

// 样例测试,测试服务器端出现CLOSE_WAIT状态
// 即客户端发送断开链接,服务器不断开链接也就是不close。此时就会维护在此状态
void User(char* str)
{
    std::cout << "./" << str << " port\n";
}

int main(int arc, char* argv[])
{
    if (arc != 2)
    {
        User(argv[0]);
    }

    int serverSock = socket(AF_INET, SOCK_STREAM, 0);  // 0自动识别,使用TCP协议进行通信
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[1]));
    inet_aton("0.0.0.0", &addr.sin_addr);
    if (bind(serverSock, (sockaddr*)&addr, sizeof addr) < 0)
    {
        std::cerr << "bind error: " << errno << "-" << strerror(errno) << std::endl;
        exit(1);
    }

    // TCP初始化绑定完后需要设置监听套接字处于监听状态
    if (0 > listen(serverSock, 20))  // 限定等待数列为20个
    {
        std::cerr << "listen error: " << errno << "-" << strerror(errno) << std::endl;
        exit(1);
    }

    // 连接逻辑处理 常驻进程
    signal(SIGCHLD, SIG_IGN);  // 子进程退出返回此信号,设置为用户默认,这样就不会产生僵尸进程
    while(true)
    {
        // 首先先建立连接
        struct sockaddr_in client_addr;
        memset(&client_addr, 0, sizeof(client_addr));
        socklen_t addr_len = sizeof(client_addr);
        int sock = accept(serverSock, (sockaddr*)&client_addr, &addr_len);
        // 网络转本地字节序
        uint16_t clien_port = ntohs(client_addr.sin_port);
        char* client_ip = inet_ntoa(client_addr.sin_addr);

        if (sock < 0) continue;

        if (fork() == 0)
        {
            // 子进程
            close(serverSock);  // 关闭监听套接字,它不需要
            char buffer[1024];
            while (true)
            {
                ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);  //阻塞等待接受
                if (n > 0)
                {
                    buffer[n] = '\0';
                    std::cout << "[" << client_ip << ": " << clien_port << "]# " << buffer << std::endl; 
                }
                else if (n == 0)
                {
                    // 对方断开连接了
                    // close(sock);  // 测试CLOSE_WAIT状态
                    // break;  // 测试此状态此进程也不可退出,因为进程退出也会关闭文件描述符的
                }
                else
                {
                    // n < 0 出现错误
                    close(sock);
                    std::cerr << "recv error: " << errno << "-" << strerror(errno) << std::endl;
                    break;
                }
            }
        }
        // 父进程
        close(sock);  // 关闭服务套接字,父进程不需要
    }

    return 0;
}

        可以发现,我们本来对方关闭连接,服务器这端应该也要关闭的,但是我们不关闭并且也不退出进程,这样,我的服务器TCP连接中会不会存在 CLOSE_WAIT状态的连接呢?

        我们使用三个客户端进行连接,然后依次断开连接,就可以看到依次变成CLOSE_WAIT状态的效果:

         可以看到,实验效果确实如此,所以平时我们一定要注意编写代码时候的规范性,否则就可能让服务器崩溃退出的危险!

理解:TIME_WAIT

        如果是主动断开链接的那一方,经过四次挥手后,在最后依次发送应答会先进入状态TIME_WAIT。此状态不会立即退出,需要维护一段时间。所以此时虽然连接已经结束了,但是ip和端口依旧是被占用着的。所以此时重复绑定是无法进行绑定的。

为什么需要TIME_WAIT这个状态

        可能存在一些报文滞留在路上,(在四次挥手期间)单向传输时间的最大时间 MSL,等待2MSL可以让网络的历史数据进行消散-双方被丢弃或者接收。因为有可能重新进行连接:历史上此时存在数据,可能会影响双方的通信。所以尽量的保证网络包的消散。当然也只是预估的时间,并不能百分百进行确定。(MSL在RFC1122中规定2分钟,centos默认配置时间为60s,cat /proc/sys/net/ipv4/tcp_fin_timeout 进行查看  MSL是报文最大生存时间)当然,也可以理论上帮助最后一个ACK被送达。(这也是概率问题)

        当然,还是和服务器端有一些原因。如果直接进入CLOSED状态,那么最后一次发的ACK没有被接收到,服务器端没有接收到会重传FIN,此时无法接收到了。虽然最后也会关闭。但是能够准确的在一定时间里进行关闭。

        但是,在实际的应用中,如果是服务器主动断开(比如出现bug),那么我们需要让服务器能够立即重启的能力!此时如果不可立即重启就可能出现很大的问题。

setsockopt

        为了能够让服务器在主动断开链接后能够立即绑定,退出TIME_WAIT状态,系统设计了此接口,方便我们进行立即绑定操作:

man 2 setsockopt

头文件

        #include <sys/types.h>
        #include <sys/socket.h>

函数原型

        int setsockopt(int sockfd, int level, int optname,
                      const void *optval, socklen_t optlen)
;

        操作文件描述符sockfd引用的套接字的选项。

函数参数

        sockfd:对应套接字描述符。

        level:指定选项所在的级别。比如SOL_SOCKET

        optname:选项的名称。SO_REUSEADDR|SO_REUSEPORT 其中就可以访问到TIME_WAIT状态的相关设定。

        optval:访问setsockopt的值,当需要打开其中立即退出TIME_WAIT状态,就可以外面设置为1即可。

        optlen:optval的大小。

返回值

        如果成功,则返回0。如果出现错误,则返回-1,并适当地设置errno。

        在没有绑定前,我们服务器立即退出就可以看到如下结果: 

        

        可以看到,在需要等待一定的时间方可继续绑定连接。

        当我们加上代码后:

main()
{
    // ......
    int serverSock = socket(AF_INET, SOCK_STREAM, 0);  // 0自动识别,使用TCP协议进行通信
    // 对监听套接字进行处理,让服务器连接时主动退出,能够立即启动
    int opt = 1;  // 表示打开
    setsockopt(serverSock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof opt);  // 在绑定前设置好
    //......
    if (bind(serverSock, (sockaddr*)&addr, sizeof addr) < 0)
    {
        std::cerr << "bind error: " << errno << "-" << strerror(errno) << std::endl;
        exit(1);
    }
    //......

}

         通过实验现象我们可以发现虽然服务器主动断开了链接为TIME_WAIT状态,但是我们服务器再次链接依旧可以立即启动,而不是必须等特定时间才行。

超时重传

        在TCP协议中,如果我们发送数据段(报文)后,,发送端如果在一定时间内没有接收到指定的应答。如果超时,就认定丢弃,就进行重传

        那么此时存在两种情况:1.发送的数据段丢了,丢包。2.被发送方发送的数据段丢了,应答丢了。(小问题:丢包重传对方可能收到重复报文的情况。收到重复也是不可靠性的一种。解决方法就是根据报文中的序号进行去重即可。)

        超时时间该如何确定呢?自然,超时时间不可太长,也不可太短。当然不可设置为固定时间-网络好的时候,超时时间应该会短一点,否则超时时间应该会长一点。(在Linux以500ms为一个单位进行控制。每次以500ms的整数倍进行重传。1*500,2*500,4*500 指数,如果到一定次数认为链接失效,client会自动断开链接。)

流量控制

        由于我们两端可能的处理数据是有限的。发送的数据必须根据接收能力进行发送,而不是发的很多直接丢弃。

        接收端发送ACK的时候,将接收缓冲区大小放入报头窗口大小字段中。
        满了,不在发送,需要定期发送一个窗口检测数据段(就是一个TCP报头),使接收端把窗口大小告诉发送端,当然也可以窗口更新了,发送窗口更新通知。(两种策略都在使用)

注意:

        1.同样的也是双向的。-全双工的-同时做。
        2.那么第一次发送消息的时候,我如何直到对方的接收区大小呢?-我们需要的是对方接受的能力即接收缓冲区的大小。但是问题所说的是第一次发送数据。并不是第一次交换报文。---三次握手的时候,前两次握手的时候一定会不会携带任何数据,但是可以在报头上携带接收缓冲区的大小。

        当然,如果想要扩大接受能力,需要调整窗口大小字段。实际上TCP40字节选项中存在一个扩大因子M,实际窗口大小字段值左移动M位。

滑动窗口

        首先,在之前我们看到的TCP机制中基本都是保证数据的可靠性的。那么我们能否主要提高网络发送数据效率问题呢?

         之前,是一种串行的操作。一次发一次应答。很慢。那么现在一次发送大量的数据,效率就可以提高。

1所以,发送时并行的过去,应答也是并行的发过来。IO时间重合。
2理论上发送多少个发送应该有多少应答。
3流量控制:允许发送一批数据(同时),当然前提是总量是小于对方接收能力(窗口的大小)。

        至于滑动窗口的由来,我们可以将发送端的缓冲区看作成如下的结构:

         实际上根据窗口,我们可以分为三个部分:1.窗口前:发送收到应答部分(可以删掉),2.可以直接发送,暂时不需要应答,3.尚未发送。

        滑动窗口在自己的发送缓冲区中,属于发送的数据。窗口大小就是无需等应答数据的最大值。

        本质:发送方可以一次性向对方推送数据的上限。-滑动窗口需要遵从流量控制。==对方的接受能力决定。(既想给对方推送跟多的数据,又要保证对方来的及接受)

滑动窗口的本质:

        实际上,滑动窗口的开始和结束位置均有两个指针指向,int win_start, win_end;

        1.滑动窗口不一定整体右移动。对方没有向上传,接受能力减少
        2.可以为0,也就是说对方接受能力为0。
        3.如果没有收到开始报文的应答,而是收到中间的,此时完全没有问题。(序号的含义就是)
        4.超时重传背后的含义:就是没有收到应答,数据必须要暂时保存起来。

        一个策略:根据应答的确认序号进行更新:

        win_start = 收到应答报文中的确认序号,end = start+ 对方接受的能力(存在两个条件

        5.滑动窗口如果一直向右滑动会不会越界呢?-自然不存在。实际上,TCP的发送缓冲区是环状的。用线性模拟环状,进行模处理即可。

高速重传 

        滑动窗口是解决效率问题还是可靠性问题?自然偏重于效率问题。
        如果在滑动窗口过程中出现了丢包现象:

        1.如果数据包已经到达,ACK丢了,这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认。

        2.如果是传输过程中数据包丢了,接收方没有接受的话,根据传回来的响应通过确认序号连续的三个及其以上,此时会将对于的数据进行重传。

        上述就是高速重传机制。

-高速重传机制。中间存在丢失,ACK序号只能传丢失前的序号。此时传给的多个报文应答都是此序号。如果收到三个一样的,那么就会对此进行重发。

         那么既然有了快重传(高速重传机制)为什么还要搞一个超时重传?当然,因为快重传必须有条件的:连续三次!而且还有可能ACK应答丢。所以并不是对立的,两者是互补协作的。对于快重传来说,在超时重传的等待时间内,如果连续出现三次确认报头内的确认序号指定一个位置,那么已经确认此处为确认丢失,而不是可能丢失了,此时不必等待超时重传的时间,而直接发送节省时间从而增加效率。

拥塞控制

        前面,我们可以发现无论是超时重传、快重传、流量控制、链接管理、滑动窗口、去重&按序达到、序号机制、确认应答等TCP机制似乎就是直接针对两端来的。但是似乎有点忽略了中间存在一个重要的影响因素:网络

         如果在传输状态中,存在少量丢包,那么应该就是自己的问题,就利用双端机制进行解决即可,但是如果大量丢包,那么就是网络的问题(网络拥塞了)此时需要重传吗???

        不要忘了,这个网络不仅仅是为你们这个链接服务,而是为成千上万的链接服务的。大家都使用的一个协议,如果都识别为重传,那么就会更加加重阻塞了。

        那么此时该如何去解决呢?使用慢启动策略进行解决。

         先一个发,收到应答,逐渐增加发送,如果收到应答就会继续发送。比如:1,2,4,8......

        拥塞窗口:一个TCP / IP协议栈上的数字。单台主机一次向网络中发送大量数据时可能会引发网络拥塞的上限

        那么我们能否向1,2,4,8......无限次的增长上去吗?自然不可。首先,滑动窗口的大小实际上 = min (拥塞窗口的大小对方的接受能力);

        对于拥塞窗口来说,应该是前期指数增长,后期线性增长方合理。

        一直指数增长太快了。也就是说不能让拥塞窗口加倍。引入一个慢启动的阈值。当超过阈值后,后面进行线性方式增长。放弃指数增长。(阈值也会变化,变成上次拥塞窗口的一半-乘法减小    过阈值:拥塞避免-加法增大) 

        什么拥塞之后前期时指数增长呢?

        前期慢,后期非常快。一旦拥塞:1.前期要让网络有一个缓一缓的机会。2.中后期,网络恢复了之后,尽快恢复通信的过程。-再慢慢的发可能会影响通信效率。

        综上,我们可以总结一下拥塞控制的方式:

        因为存在网络环境因素的影响,所以在滑动窗口中的实际大小应该取决于拥塞窗口大小和对方的接受能力的较小值。当拥塞窗口主要影响的时候,拥塞窗口采取的机制是慢启动+拥塞避免算法

        网络一旦被拥塞,首先拥塞窗口被初始化为一个很小的值,然后后面每次增加就是上一次的一倍。当增加到一定的阈值的时候,停止倍数增加,进入拥塞避免状态,每次按照线性增长的方式进行增加。可以有效的控制网络发送的速率,保证网络发送的稳定和可靠性。

延迟应答

        类似于缓冲区,想一下我们为了解决内存和硬盘速度不同是采用什么解决的。也就是说为了提高速率或者减少压力我们可有采取一个延迟应答的机制。

        TCP中,因为当接收方接收到一个报文后不会立即发送回去,如果等待一会我能够给对方同步一个更大的接受能力,如果接受能力更大(暂时不考虑拥塞问题),那么对方根据滑动窗口,单次IO就会发送更多的数据,让效率得到提高。并且这样也可以减少在网络中的报文数量,减少在网络中的流量

        因为等待一会的缘故,我们可以让接收方尽快的上层将TCP层数据取走,这样在返回报文的窗口大小中就可以写更大的数据了。但是,延迟应答机制也会带来一些问题。例如,在一个长时间间隔内,如果发生了数据包的丢失或者损坏,接收方无法及时通知发送方重新发送数据。所以TCP需要一些参数来对延迟应答进行控制,限制一下读取的数量个数或者等待时间(数量一般2个,时间取200ms)。

捎带应答

        当接收方给对方发送带有应答(ACK)的报头信息的时候,也可以本身携带数据包。这样就可以减少网络的流量消耗,并且呢不用分两次发送从而提高效率。

        然而,捎带应答机制也存在一些问题。首先,如果发送方的确认信息被延迟,那么待发送的数据包也会被延迟,从而导致网络的传输效率降低。其次,如果发送方同时发送了多个数据包,但其中一个数据包丢失或者损坏,那么接收方无法仅仅对该数据包发送一个确认信息,而必须重新发送全部数据包的确认信息,从而增加网络的流量。

        简而言之,捎带应答是TCP中的一种优化效率的手段,一定程度上提高TCP传输的效率。

TCP总结

        经过这么多的TCP学习后,我们可以将上面的特性进行一个总结:

1.TCP的可靠性:校验和、序列和、确认应答、超时重发、连接管理、流量控制、拥塞控制。

2.TCP的效率:滑动窗口、快速重传、延迟应答、捎带应答。

        并且,由于是基于字节流式的传输,才有了上面的这么多机制进行管理可靠性、效率问题,那么如何理解TCP是面向字节流的呢?

1.TCP面向字节流

        我们可以利用现实生活中的例子进行对比。UDP是用户数据报协议,所以就好比发快递,我们所买的需要一个一个接受。

        而TCP是传输控制协议,好比水管,管道只需要保证水传输的可靠和快速就行了,而对于水没有明显的区分或者使用,需要我们使用不同的桶或者水杯对这些水的使用进行一个区分。

        面向字节流正是这个意思。数据没有任何边界和区分度的。TCP不关心数据到底是什么。-是应用层自主决定的。应用层完全不用考虑什么时候发,一次发多少。

2.TCP数据粘包问题

        因为数据没有边界和区分度,那么可能存在数据粘包的问题。所以采用TCP进行数据传输的时候上层必须制定协议规范化对数据的读取。也就是说上层需要明确有效载荷与有效载荷之间的边界,可以利用一些如定长、特殊字符作为换行符、设定报头-标准字段,长度和有效载荷。此时就是在应用层实现的。正因为这样,UDP数据不是粘包的,因为协议帮助应用层实现了区分,所以叫做用户数据报。

3.TCP异常问题

        那么我们有没有想过对于正在使用TCP连接的两个主机,如果其中存在异常(不是正常流程断开连接),那么TCP是如何解决的呢?

        1.其中一个主机的进程突然终止。因为进程终止时默认将该进程打开的所有文件描述符进行关闭,而我们的套接字实际上也是一个文件描述符,所以此时就会向对方发起四次挥手,属于正常流程。

        2.如果突然断电或者断网呢?首先此主机因为检测到了网络环境发生了变化,此链接一定被关闭了(断电就直接没了)。然后对方主机的话,首先对此链接存在一定性的保活。如果对方长时间不对我进行发消息,那么我会定期向对方询问是否还在。如果多次不在便就主动断开连接。(这样的策略可以保证服务器维护链接的压力减少)

4.UDP和TCP的对比

        使用TCP协议的应用层协议有:HTTP/HTTPS ssh telnet fip smtp。

        明确以下两点:

        UDP是面向用户数据包传输,不可靠,无连接的传输层协议。

        TCP是面向字节流传输,可靠,有连接的传输层协议。

        UDP虽然不可靠,无连接,但是实现简单,在想要快速开发和允许少量丢包的环境下,可以使用UDP进行网络通信,而对于那些需要数据精准传输不能丢弃任何数据的话就必须使用TCP进行网络通信了,所以两者各有自己的使用环境。

        经典的面试题:请描述一下如何使用UDP实现可靠传输?使用TCP类似的实现机制,在应用层上对UDP每个数据段进行控制即可。

5.理解listen接口的第二个参数

        我们知道listen接口在之前网络套接字编程的时候被我们用于设置服务器的监听套接字,它的第二个参数是连接队列的长度。那么如何理解此连接队列呢?

        我们知道,两个主机进行通信采用TCP协议的话会先进行三次握手。SYN、SYN+ACK、ACK(客户端发送SYN后变为SYN_SENT状态,服务器端接受到对应链接后,此连接变为SYN_RCVD状态,然后发送SYN+ACK。客户端接收到后首先变成ESTABUSHED状态,然后向服务器端发送应答,服务器端接受到后变成ESTABUSHED状态)。那么想象一下,服务器端获取链接的接口accpet参与此过程吗?

        并不参与,它的任务只是获取已经建立好的连接。也就是说:先建立好链接,然后才能accept获取对应的连接。不调用accept,能建立连接成功。如果上层来不及调用accept,对方还来了大量的连接,所以连接都应该先建立好吗?不可,否则服务器很容易挂掉。并且也需要对这些连接进行一个存储。

        也就是说,TCP在底层实际上维护了一个队列(全连接队列),用来存储已经连接好的一个连接。而accept就是对此队列的pop。而listen的第二个参数就是指定此队列的长度。当此长度满后,TCP会维护另外一个队列(半连接队列),暂时存储后面来的连接-此时服务器端不发送ACK应答,对方处于SYN_RCVD状态,服务器此连接处于SYN_RCVD状态。如果一定时间后,队列依旧是满的,那么会释放这些后来的连接拒绝服务。

int listen(int sockfd, int backlog);

        backlog参数含义:底层全连接队列的长度  = 参数 + 1;全连接用来保存est状态,但是上层没有accpet的请求。半链接队列:保存SYN_RECV和SYN_SENT状态的请求(生命周期很短)   

        此参数不可为0并且不可太长。(太长维护太多浪费资源并且很多连接都是无效连接)

        我们可以对上面的代码稍作修改,测试一下backlog参数。这里不再做过多演示。

        未完待续......

猜你喜欢

转载自blog.csdn.net/weixin_61508423/article/details/129401136