计算机网络常见知识
本文最后更新于 2025-03-24,文章超过7天没更新,应该是已完结了~
网络分层结构
计算机网络体系大致分为三种,OSI七层模型,TCP/IP四层模型和五层模型。OSI是一种理论下的模型,而TCP/IP已被广泛使用,成为网络互联事实上的标准。
需求1:
科学家要解决的第一个问题是,两个硬件之间怎么通信。具体就是一台发些比特流,然后另一台能收到。于是,科学家发明了物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的 数模转换与模数转换--->数字信号转称模拟信号)。这一层的数据单位叫做比特。
需求2:
现在通过电线我能发数据流了,但是,我还希望通过无线电波,通过其它介质来传输。然后我还要保证传输过去的比特流是正确的,要有纠错功能。于是,发明了数据链路层:定义了如何将格式化数据进行传输,以及如何控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。
需求3:
现在我能发正确的发比特流数据到另一台计算机了,但是当我发大量数据时候,可能需要好长时间,例如一个视频格式的,网络会中断好多次(事实上,即使有了物理层和数据链路层,网络还是经常中断,只是中断的时间是毫秒级别的),那么我还得要保证传输大量文件时的准确性。于是,我要对发出去的数据进行封装。就像发快递一样,一个个地发。于是,先发明了传输层。
例如TCP,是用于发大量数据的,我发了1万个包出去,另一台电脑就要告诉我是否接受到了1万个包,如果缺了3个包,就告诉我是第1001,234,8888个包丢了,那我再发一次。这样,就能保证对方把这个视频完整接收了。
例如UDP,是用于发送少量数据的。我发20个包出去,一般不会丢包,所以,我不管你收到多少个。在多人互动游戏,也经常用UDP协议,因为一般都是简单的信息,而且有广播的需求。如果用TCP,效率就很低,因为它会不停地告诉主机我收到了20个包,或者我收到了18个包,再发我两个!如果同时有1万台计算机都这样做,那么用TCP反而会降低效率,还不如用UDP,主机发出去就不管了。
需求4:
传输层只是解决了打包的问题。但是如果我有多台计算机,怎么找到我要发的那台?或者,A要给F发信息,中间要经过B,C,D,E,但是中间还有好多节点如K.J.Z.Y。我怎么选择最佳路径?这就是路由要做的事。
于是,发明了网络层。即路由器,交换机那些具有寻址功能的设备所实现的功能。这一层定义的是IP地址,通过IP地址寻址。所以产生了IP协议。
需求5:
现在我们已经保证给正确的计算机,发送正确的封装过后的信息了。但是用户级别的体验好不好?难道我每次都要调用TCP去打包,然后调用IP协议去找路由,自己去发?当然不行,所以我们要建立一个自动收发包,自动寻址的功能。
于是,发明了会话层。会话层的作用就是建立和管理应用程序之间的通信(可以实现断点重续)。
需求6:
现在我能保证应用程序自动收发包和寻址了。但是我要用Linux给window发包,两个系统语法不一致,就像安装包一样,exe是不能在linux下用的,shell在window下也是不能直接运行的。于是需要表示层(presentation),帮我们解决不同系统之间的通信语法问题。
TCP报文段结构
序列号seq: 占32位,本报文段发送的数据组的第一个字节的序号。一个报文段的序号为300,此报文段数据部分共有100字节,则下一个报文段的序号为400。所以序号确保了TCP传输的有序性。
确认号ack: 占32位,期待收到对方发送下一个报文段的第一个字节的序号,表明该序号之前的所有数据已经正确无误的收到。因此当前报文段最后一个字节的编号+1为确认号。
数据偏移: 占4位,并以4个字节为单位。用来指出TCP报文段的数据载荷部分的起始处距离TCP报文段的起始处多远,即指出TCP报文段首部长度。(1111)2=(15)10,即是15个32位,一个32位是4个字节,因此数据偏移的最大值是154=60个字节,这也是TCP首部的最大字节。因为固定首部的存在,数据偏移的值最小为20个字节,因此选项长度不能超过40字节(减去20个字节的固定首部)。
紧急标志位URG:取值为1时紧急指针字段有效;取值为0则无效。
确认ACK: 占1位,仅当ACK=1时,确认号字段才有效。ACK=0时确认号无效
推送标志位PSH:接收方的TCP收到该标志位为1的报文段会尽快上交给应用程序,而不必等待接收缓存都填满后再向上交付。
复位标志位RST:用来复位TCP连接。当RST=1时,表明TCP连接出现了异常,必须释放连接,然后再重新建立连接;还用来拒绝打开TCP连接。
同步SYN: 连接建立时用于同步序号。当SYN=1,ACK=0时表示:这是一个连接请求报文段,若同意连接,则在响应报文段中SYN=1,AKC=1。因此,SYN=1表示这是一个连接请求。SYN这个标志位只有在TCP建立连接才会置为1,握手完成后SYN标志位被置为0。
终止FIN: 用来释放一个连接。FIN=1表示:此报文段发送方的数据已经发送完毕,并要求释放运输链接。
窗口: 占16位,以字节为单位。窗口指的是发送本报文段的一方的接受窗口。例如,A发送了一个报文段,其确认号是3000,窗口字段是1000.这就是告诉对方B:“从3000算起,A接收缓存空间还可接受1000个字节数据,字节序号是3000-3999”,可以想象到河道的阀门。
校验和:由发送发填充,接收端对TCP报文段执行CRC算法以校验TCP报文段在传输过程是否损坏。这个校验不仅校验TCP头部,也包括数据部分,这也是TCP可靠传输的一个重要保障。
紧急指针:占16位,以字节为单位,用来指明紧急数据的长度。当发送方有紧急数据时,可将紧急数据插队到发送缓存的最前面,并立刻封装到一个TCP报文段中进行发送。紧急执政会指出本报文段数据载荷部分包含了多长的紧急数据,紧急数据之后是普通数据。
三次握手详细过程
第一次握手:客户端向服务端发起建立连接请求,客户端会随机生成一个起始序列号x,客户端向服务端发送的字段中包含标志位SYN=1,序列号seq=x。第一次握手前客户端状态为CLOSE,第一次握手后客户端状态为SYN-SENT,此时服务端状态为LISTEN。
第二次握手:服务端在收到客户端发过来的报文后,会随机生成一个服务端的起始序列号y,然后给客户端回复一段报文,其中包括标志位SYN=1,ACK=1,序列号seq=y,确认号ack=x+1。第二次握手前服务端状态为LISTEN,第二次握手后服务端的状态为SYN-RCVD,此时客户端的状态还是为SYN-SENT。(其中SYN=1表示要和客户端建立连接,ACK=1表示确认序列号有效)。
第三次握手:客户端接收到服务端发过来的报文后,会再次向服务端发送报文,其中包括标志位ACK=1,序列号seq=x+1(无内容也会消耗一个序列号),确认号ack=y+1。第三次握手前客户端状态为SYN-SEND,第三次握手后客户端和服务端状态都为ESTABLISHED。此时建立完成。
为什么不能用两次握手进行连接?
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的确认应答分组在传输中被丢失的情况下,C将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
四次挥手详细过程
客户端进程发出连接释放报文,FIN=1,seq=u(等于前面已经传送过来的数据的最后一个字节的序号+1),并且停止发送数据。此时,客户端进入了FIN-WAIT-1(终止等待1)状态。TCP规定,FIN报文段即使不携带数据,也要消耗一个序列号。
服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时服务端就进入了CLOSE-WAIT(关闭等待)状态。A到B的连接就释放了,这时候处于半关闭状态,即客户端没有数据发送了,服务器还发送数据,客户端依然要接受。这个状态还要持续一段事件,也就是整个CLOSE-WAIT状态持续的时间。
客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接收服务器发送的最后数据)。
服务器将最后的数据发送完毕后,就向客户端发送连接释放报文(还需要再发送确认请求),FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时服务器进入了LAST-ACK(最后确认)状态,等待客户端的确认。
客户端收到服务器的连接释放报文后,必须发出确认,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)时间后,才进入CLOSED状态。
服务器只要收到了客户端发出的确认,立刻进入CLOSED状态。可以看到,服务器结束TCP连接的时间要比客户端早一些。
第四次挥手为什么要等待2MSL?
假如网络是不可靠的,有可以最后一个ACK丢失,Server如果没有收到ACK,将不断重复发送FIN片段,所以Client不能立即关闭。Client会设置一个计时器,等待2MSL的时间(2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接)。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。
如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
TCP连接异常终止
TCP的异常终止是相对于正常释放TCP连接的过程而言的,我们都知道,TCP连接的建立是通过三次握手完成的,而TCP正常释放连接是通过四次挥手来完成,但是有些情况下,TCP在交互的过程中会出现一些意想不到的情况,导致TCP无法按照正常的四次挥手来释放连接,如果此时不通过其他的方式来释放TCP连接的话,这个TCP连接将会一直存在,占用系统的部分资源。在这种情况下,我们就需要有一种能够释放TCP连接的机制,这种机制就是TCP的reset报文。reset报文是指TCP报头的标志字段中的reset位置一的报文,如下图所示:
TCP和UDP的区别
TCP连接时需要三次握手,有时间延迟,而UDP无连接,时间上不存在建立连接需要的延迟。
空间上,TCP需要在端系统(因特网上所有主机)中维护连接状态,需要一定的开销。此连接装入包括接收和发送缓存(滑动窗口),拥塞控制参数(拥塞控制)和序号与确认号的参数(握手)。UDP不维护连接状态,也不跟踪参数,开销小。
UDP提供尽最大努力的交付,不保证可靠交付。所有维护传输的可靠性工作需要用户在应用层来完成。没有TCP的确认机制、重传机制。如果因为网络原因没传送到对端,UDP也不会给应用层返回错误信息。
UDP分组首部开销小,TCP首部20字节,UDP首部8字节。
UDP面向报文的,TCP面向字节流的,UDP对应用层交下来的报文,添加首部后直接向下交付给IP层,既不合并也不拆分。对IP层交上来的UDP用户数据报,去除首部后就原封不动地交付给上层应用程序,报文不可分割,于是报文是UDP数据报处理的最小单位。正是因为这样,UDP显得不够灵活,不能控制读写数据的次数和数量。比如我们要发送100个字节的报文,我们调用一次sendto函数就会发送100字节,对端也需要用recvfrom函数一次性接收100字节,不能使用循环每次获取10个字节,获取十次这样的做法。
TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的通信方式。
TCP 和 UDP 的常见应用
基于TCP的应用层协议有:
HTTP : 超文本传输协议,默认端口80
FTP:文件传输协议,默认端口 20(用于传输数据) 21(用于传输控制信息)
SMTP:简单邮件传输协议,默认端口25
TELNET:网络电传,默认端口23
SSH:安全外壳协议,默认端口22
基于UDP的应用层协议有:
DNS:域名服务,默认端口53
TFTP:简单文件传输协议,默认端口69
SNMP:简单网络管理协议,通过UDP端口161接收
TCP的粘包和拆包
TCP是面向流,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大数据包发送,这就是所谓的粘包和拆包。
为什么会产生粘包和拆包?
要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
接收数据端的应用层没有及时读取接收缓冲区的数据,将发生粘包
要发送的数据大于TCP发送缓冲区剩余空间大小,将发生拆包
解决方案
发送端将每个包都封装成固定的长度。比如100字节大小,如果不足100字节可以通过补0填充到指定长度。
在数据尾部增加特殊字符进行分割
将消息分为头部和消息体,头部中保存整个信息的长度,只有读取到足够长度的消息之后才算是读到一个完整的信息。
TCP是如何保证可靠性的?
首先,TCP的连接是基于三次握手,而断开则是基于四次挥手。确保连接和断开的可靠性。
其次,TCP的可靠性还体现在有状态;TCP会记录有哪些数据发送了,哪些数据接收了,哪些没有被接收,并且保证数据包按序到达,保证数据传输不会出错。
再次,TCP的可靠性,还体现在可控制。它有数据包校验、ACK应答、超时重传(发送方)、失序数据重传(接收方)、丢弃重复数据、流量控制(滑动窗口)和拥塞控制等机制。
TCP流量控制的方式
1.拥塞控制
在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏,这种情况就叫做网络拥塞。在计算机网络中数位链路容量(即带宽)、交换结点中的缓存和处理机等,都是网络的资源。若出现拥塞而不进行控制,整个网络的吞吐量将随输入负荷的增大而下降。当输入的负载到达一定程度 吞吐量不会增加,即一部分网络资源会丢失掉,网络的吞吐量维持在其所能控制的最大值。
TCP的四种拥塞控制算法
慢开始
拥塞避免
快重传
快恢复
慢开始
把拥塞窗口cwnd设置为一个最大报文段MSS的数值。而在每收到一个
2.滑动窗口
1.发送方根据接收方缓存区大小,动态设置自己的可发送窗口大小,处于窗口内的数据表示可发送,之外的数据不可发送。
2.当发送方窗口内的数据接收到确认回复时,整个窗口会向前移动,直到发送完成所有数据,并且接收方除了确认回复还会说自己现在的接收窗口是多少。
可以用下面的一个例子来具体说明:设A向B发送数据。在连接建立时,B告诉了A:“我的接收窗口是rwnd = 400 ”(这里的rwnd表示receiver window) 。因此,发送方的发送窗口不能超过接收方给出的接收窗口的数值。请注意,TCP的窗口单位是字节,不是报文段。假设每一个报文段为100字节长,而数据报文段序号的初始值设为1。
从图中可以看出,B进行了三次流量控制。第一次把窗口减少到rwnd = 300 ,第二次又减到了rwnd = 100 ,最后减到rwnd = 0 ,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机B重新发出一个新的窗口值为止。B向A发送的三个报文段都设置了 ACK = 1 ,只有在ACK=1时确认号字段才有意义。
如果B向A发送了0窗口的报文段不久后,B的接收缓存又有了一些存储空间,与时B向A发送了rwnd=400的报文段,然而这个报文段在传送过程丢失了,A一直等待收到B发送的非0窗口的通知,而B也一直等待A发送的数据,如果没有其它措施,这种互相等待的死锁局面将会一直延续下去。
所以TCP为每一个连接设有一个持续计时器(persistence timer)。只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口控测报文段(携1字节的数据),而对方就在确认这个探测报文段时+给出了现在的窗口值。如果窗口仍然为0,那么收到这个报文段的一方就重新设置持续计时器。
什么是跨域?
跨域是指从一个域名的网页去请求另一个域名的资源,由于有同源策略关系,不允许这么访问。但是,有很多场景经常有跨域访问的需求,比如,在前后端分离的模式下,前后端的域名是不一致的,此时就会发生跨域问题。
什么是同源策略?
同源是指“协议+域名+端口”三者相同,即使不同域名指向同一个ip地址,也不算同源。
同源策略限以下几种行为:
Cookie,LocalStorage 和IndexDB无法读取。
DOM和Js对象无法获取
AJAX请求不能发送
为什么要有同源策略?
加入你刚刚在网银输入账号密码,查看了自己的余额,然后去访问黄色网站,这个网站可以访问刚刚的网银站点,并且获取账号密码,这样很危险,所以同源策略是为了保护网站信息的。
跨域问题怎么解决?
CORS,跨域资源共享
CORS(Cross-origin resource sharing) ,跨域资源共享。CORS其实是浏览器制定的一个规范,浏览器会自动进行CORS通信,它的实现主要在服务端,通过一些HTTP Header来限制可以访问的域,例如页面A需要访问B服务器上的资源,如果B服务器上声明了允许A的域名访问,那么从A到B的跨域请求就可以完成。
服务器已经设置允许跨域请求,当用户想修改服务器数据时,比如想删除服务器数据,浏览器会自动发出一个预检请求,预检请求就是查看服务器是否支持当前跨域请求的方式,因为预检请求没有具体哪个方法,所以用OPTIONS代替,服务端一般都会设置允许的请求方式,如果服务器允许该请求方式,则正常访问处理。
@CrossOrigin注解
如果项目使用的是SpringBoot,可以在Controller类上添加一个@CrossOrigin(origins="*")注解就可以实现对当前controller的跨域访问了,当然也可以加到方法上,或者对所有接口进行跨域处理。
nginx反向代理接口跨域
nginx反向代理跨域原理如下:首先同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也不会存在跨域问题。反向代理nginx与服务器是属于服务器之间的沟通,不涉及浏览器的同源策略
nginx反向代理接口跨域实现思路如下:通过nginx配置一个代理服务器(域名与domain1相同,端口不同),反向代理访问domain2接口,并且顺便兼容转化以下cookie的domain信息,方便当前域cookied的写入,实现跨域登陆。
// proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
add_header Access-Control-Allow-Origin http://www.domain1.com;
#本身nginx代理服务器与服务器可以直接沟通,但是为了不出问题还是在请求头设置允许跨域请求的域名
}
}
这样我们的前端只要访问http:www.domain1.com:81/*就可以访问。
补充 : 我们知道response在写set-cookie的时候,domain是一个可选项,所以有时候可以看到cookie中没有domain的内容,proxy_cookie_domain也就不必要了,但是对于有些设置了domain的项目,用nginx做反向代理的时候,就必须转换一下了。浏览器在发送请求的时候,回在request header中带上cookie项,其中不包含任何域名信息,因为浏览器在设置cookie选项时,选择的肯定是缓存中属于当前域名下的cookie,请求发送出去的cookie只是一个普通的字符串没有其它信息,所以前端到后端请求过程中proxy_cookie_domain没用的,不过服务端在响应时,通过set-cookie的domain属性,控制cookie生效的域名,当前端收到set-cookie的domain和当前域名不一致,那么该cookie是无效的。所以需要去做domain的转换,将当前cookied生效域名为当前域名,才能使用cookied。
通过jsonp跨域
通常是为了减轻web服务器的负载,我们把js,css,img等静态资源分离到另一台独立域名的服务器上,在html页面中在通过标签从不同域名下加载静态资源,这种行为浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。
服务器设置永久重定向的思考服务器设置永久重定向的思考
为什么服务器一定要重定向而不是直接发送给用户想看的网页内容呢?
1.其中一个原因跟搜索引擎排名有关,如果一个页面有两个地址,搜索引擎会认为它们是两个网址,从而造成每个搜索链接都减少而降低排名,但搜索引擎知道301永久重定向是什么意思,这样就把这个两个网址归到同一网址排名下。
2.还有就是当一个页面有好几个名字时,它可能会出现在缓存好几次。
3.网站调整(如改变网页目录结构)
4.网页被移到一个新地址
5.网页扩展名改变
Content-Type和POST提交数据方式的关系
- 感谢你赐予我前进的力量