远程通信协议

1 HTTP请求流程

在分布式架构中,有一个很重要的环节,就是分布式网络中的计算机节点彼此之间需要通信。这个通信的过程一定会涉及到通信协议相关的知识点。用浏览器访问各种网站,作为用户来说,只需要输入一个网址并且正确跳转就行。但是作为程序员,看到的可能就是这个响应背后的整体流程。所以通过一个http请求的整体流程来进行整理通信的知识。

1.1 DNS服务(域名解析)

首先,访问一个域名,会经过DNS解析。DNS(Domain Name System),它和http协议一样是位于应用层的协议,主要提供域名到ip的解析服务。其实不用域名也可以访问目标主机的服务,但是ip本身不是那么容易记,所以使用域名进行替换使得用户更容易记住。

在很多大型网站,会引来CDN来加速静态内容的访问,这里简单介绍一下什么是CDN(Content Delivery Network),表示的是内容分发网络。CDN其实就是一种网络缓存技术,能够把一些相对稳定的资源放到距离最终用户较近的地方,一方面可以节省整个广域网的带宽消耗,另一方面可以提升用户的访问速度,改进用户体验。一般会把静态的文件(图片,脚本,静态页面)放到CDN中。如果引入CDN,解析的流程也会稍微复杂一点。

1.2 HTTP协议通信原理

说到通信,就得说起TCP和UDP这两种通信协议,以及建立连接得握手过程。而http协议的通信是基于tcp/ip协议之上的一个应用层协议,应用层协议除了http,还有ftp、dns、smtp、telnet等。涉及到网络协议,一定需要知道OSI七层网络模型和TCP/IP四层概念模型,

OSI七层网络模型包含:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。

tcp/ip四层概念模型包含:应用层、传输层、网络层、数据链路层。

当应用程序用TCP传送数据时,数据被送入协议栈中,然后逐个通过每一层知道被当作一串比特流送入网络。其中每一层对收到的数据都要增加一些首部信息(有时还要增加尾部信息)

客户端如何找到目标服务:

​ 在客户端发起请求的时候,会在数据链路层去组装目标机器的MAC地址,目标机器的MAC地址怎么得到?这里就涉及到一个ARP协议,这个协议简单来说就是已知目标机器的ip,需要获得目标机器得MAC地址。(发送一个广播消息,这个ip是谁的,请来认领。认领ip的机器会发送一个MAC地址的响应)。

​ 有了这个目标MAC地址,数据包在链路上广播,MAC的网卡才能发现,这个包是给它的。MAC的网卡把包收进来,然后打开IP包,发现IP地址也是自己的,再打开TCP包,发现端口是自己,也就是80端口,而这个时候这台机器上有一个nginx是监听80端口。于是将请求提交给nginx,nginx返回一个网页。然后将网页需要发回请求的机器。然后层层封装,最后到MAC层。因为来的时候有源MAC地址,返回的时候,源MAC就变成了目标MAC,再返回给请求的机器。

​ 为了避免每次都用ARP请求,机器本地也会进行ARP缓存。当然机器会不断地上线下线,IP也可能会变,所以ARP地MAC地址缓存过一段时间就会过期。

接收到数据包以后地处理过程:

​ 当目的主机收到一个以太网数据帧时,数据就开始从协议栈中由底向上升,同时去掉各层协议加上地报文首部。每层协议都要去检查报文首部中的协议标识,以确定接收数据的上层协议。

为什么有了MAC层还要走IP层?

​ MAC地址是唯一的,那理论上,在任何两个设备之间,应该都可以通过mac地址发送数据,为什么还需要ip地址?

​ MAC地址就好像个人的身份证号,人的身份证号和人户口所在的城市,出生的日期有关,但是和人所在的位置没有关系,人是会移动的,知道一个人的身份证号,并不能找到它这个人,MAC地址类似,它是和设备的生产者,批次,日期之类的关联起来,知道一个设备的MAC,并不能在网络中将数据发送给它,除非它和发送方的在同一个网络内。

​ 所以要实现机器之间的通信,还需要有ip地址的概念,ip地址表达的是当前机器在网络中的位置,类似于城市名+道路号+门牌号的概念。通过IP层的寻址,能知道按何种路径在全世界任意两台Internet上的机器间传输数据。

1.3 分层负载

1.3.1 二层负载

​ 二层负载是针对MAC,负载均衡服务器对外依然提供一个VIP(虚IP),集群中不同的机器采用相同IP地址,但是机器的MAC地址不一样。当负载均衡服务器接收到请求之后,通过改写报文的目标MAC地址的方式将请求转发到目标机器实现负载均衡。二层负载均衡会通过一个虚拟MAC地址接收请求,然后再分配到真实的MAC地址。

1.3.2 三层负载

三层负载是针对IP,和二层负载均衡类似,负载均衡服务器对外依然提供一个VIP(虚IP),但是集群中不同的机器采用不同的IP地址。当负载均衡服务器接收到请求之后,根据不同的负载均衡算法,通过ip将请求转发至不同的真实服务器。三层负载均衡会通过一个IP地址接收请求,然后再分配到真实的IP地址。

1.3.3 四层负载均衡

​ 四层负载均衡工作在OSI模型的传输层,由于在传输层,只有TCP/UDP协议,这两种协议中除了包含源IP、目标IP以外,还包含源端口号及目的端口号。四层负载均衡服务器在接收到客户端请求后,以后通过修改数据包的地址信息(IP+端口号)将流量转发到应用服务器。四层通过虚拟IP+端口接收请求,然后再分配到真实的服务器。

1.3.4 七层负载均衡

​ 七层负载均衡工作在OSI模型的应用层,应用层协议较多,常用http、radius、dns等。七层负载就可以基于这些协议来负载。这些应用层协议中会包含很多有意义的内容。比如同一个Web服务器的负载均衡,除了根据IP加端口进行负载外,还可以根据七层的URL,浏览器类别来决定是否要进行负载均衡。七层通过虚拟的URL或主机名接收请求,然后再分配到真实的服务器。

2 TCP/IP协议的深入分析

2.1 TCP握手协议

TCP消息的可靠性首先来自于有效的连接建立,所以在数据进行传输前,需要通过三次握手建立一个连接,所谓的三次握手,就是在建立TCP链接时,需要客户端和服务端总共发送3个包来确认链接的建立,在socket编程中,这个过程由客户端执行connect来触发。

第一次握手(SYN=1,seq=x)客户端发送一个tcp的SYN标志位置1的包,指明客户端打算链接的服务器的端口,以及初始序号x,保存在包头的序列号(Sequence Number)字段里。发送完毕后,客户端进入SYN_SENT状态。

第二次握手(SYN=1,ACK=1,seq=y,ACKnum=x+1)服务器发会确认包(ACK)应答。即SYN标志位和ACK标志位均为1。服务器端选择自己ISN序列号,放到Seq域中,同时将确认序号(Acknowledgement Number)设置为客户的ISN加1,即X+1。发送完毕后,服务器端进入SYN_RCVD状态。

第三次握手(ACK=1,ACKnum=y+1)客户端再次发送确认包(ACK),SYN标志位为0,ACK标志位为1,并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN发送完毕后,客户端进入EXTABLISHED状态,当服务器端接收到这个包时,也进入ESTABLISHED状态,TCP握手结束。

2.2 SYN攻击

在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP链接称为半连接(half-open connect),此时server处于SYN-RCVD状态,当收到ACK后,Server转入ESTABLEISED状态。SYN攻击就是Client在短时间内伪造大量不存在的ip地址,并向Server不断发送SYN包,server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击是一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server 上有大量半连接状态且源ip地址是随机的,则可以断定遭到SYN攻击了。

2.3 TCP四次挥手协议

四次挥手表示TCP断开连接的时候,需要客户端和服务端总共发送4个包以确认连接的断开;客户端或服务器均可主动发起挥手动作(因为tcp是一个全双工协议),在socket编程中,任何一方执行close()操作即可产生挥手操作。

单工:数据传输只支持数据在一个方向上传输。

半双工:数据传输允许数据在两个方向上传输,但是在某一时刻,只允许在一个方向上传输,实际上优点像切换方向的单工通信。

全双工:数据通信允许数据同时在两个方向上传输,因此全双工是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。

第一次挥手(FIN=1,seq=x):假设客户端想要关闭连接,客户端发送一个FIN标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。发送完毕后,客户端进入FIN_WAIT_1状态。

第二次挥手(ACK=1,ACKnum=x+1):服务器端确认客户端的FIN包,发送一个确认包,表明自己接收到了客户端关闭连接的请求,但还没有准备号关闭连接。发送完毕后,服务器端进入CLOSE_WAIT状态,客户端接收到这个确认包之后,进入FIN_WAIT_2状态,等待服务器端关闭连接。

第三次挥手(FIN=1,seq=w):服务器端准备好关闭连接时,向客户端发送关闭连接请求,FIN设置为1.发送完毕后,服务器端进入LAST_ACK状态,等待来自客户端的最后一个ACK。

第四次挥手(ACK=1,ACKnum=w+1):客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入TIME_WAIT状态,等待可能出现的要求重传的Ack包。

服务器端接收到这个确认包之后,关闭连接,进入CLOSED状态。客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segement Lifetime)之后,没有收到服务器端的ACK,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入CLOSED状态。

假设Client端发起终端连接请求,也就是发送FIN报文,Server端接收到FIN报文后,意思是说“我Client端没有数据要发给你了“,但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,”告诉Client端,你的请求我收到了,但是我还准备好,请继续你等我的消息“,这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文,当Server端确定数据已发送完成,则向Client端发送FIN报文,”告诉Client端,好了,我这边数据发完了,准备好关闭连接了“。Client收到FIN报文后,”就知道可以关闭连接了,但是它还是不相信网络,怕server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重发。“Server端收到ACK后,”就知道可以断开连接了“。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那Client端也可以正常关闭了。

2.4 问题

1、为什么连接时是三次握手,关闭的时候是四次挥手?

​ 三次握手是因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭Socket(因为可能还有消息没处理完),所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了“。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步挥手。

​ 2、为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

​ 虽然按道理,四个报文都发送完毕,可以进入CLOSE状态了,但是必须假想网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

3 TCP协议的通信过程

首先,对于tcp通信来说,每个tcp socket的内核中都有一个发送缓冲区和一个接受缓冲区,tcp的全双工的工作模式及tcp的滑动窗口就是依赖于这两个独立的Buffer和该Buffer的填充状态。

接收缓冲区把数据缓存到内核,若应用进程一直没有调用Socket的read方法进行读取,那么该数据会一直被缓存在接收缓冲区内。不管进程是否读取Socket,对端发来的数据都会经过内核接收并缓存到socket的内核接收缓存区。

read所要做的工作,就是把内核接收缓冲区中的数据复制到应用层用户的Buffer中。进程调用Socket的send发送数据的时候,一般情况下是将数据从应用层用户的Buffer中复制到Socket的内核发送缓冲区,然后send就会在上层返沪。换句话说,send返回时。数据不一定会被发送到对端。

前面提到,Socket的接收缓冲区被tcp用来缓存网络上收到的数据,一直保存到应用进程读走为止。如果应用进程一直没有读取,那么Buffer满了以后,出现的情况是:通知对端tcp协议中的窗口关闭,保证tcp接收缓冲区不会移除,保证了tcp是可靠传输的。如果对方无视窗口大小发出了超过窗口大小的数据,那么接收方会把这些数据丢弃。

https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/index.html

4 IO阻塞

4.1 一个客户端对应一个线程

为每个客户端创建一个线程实际上会存在一些弊端,因为创建一个线程需要占用CPU的资源和内存资源。另外,随着线程数增加,系统资源将会称为瓶颈最终达到一个不可控的状态,所以还可以通过线程池来实现多个客户端请求的功能,因为线程池是可控的。

4.2 非阻塞模型

上面这种模型虽然优化了IO的处理方式,但是,不管是线程池还是单个线程,线程本身的处理个数是由限制的,对于操作系统来说,如果线程数太多会造成CPU上下文切换的开销。因此这种方式不能解决根本问题。所以在Java1.4之后,引入了NIO的功能,

4.2.1 阻塞IO

当客户端的数据从网卡缓冲区复制到内核缓冲区之前,服务端会一直阻塞。以socket接口为例,进程空间中调用recfrom,进程从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞IO模型。

4.2.2 非阻塞IO

非阻塞IO模型的原理很简单,就是进程空间调用recvfrom,如果这个时候内核缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,然后应用程序通过不断轮询来检查这个状态,看内核是不是有数据过来。

4.2.3 IO复用模型

前面讲的非阻塞仍然需要进程不断地轮询重试。能不能实现当数据可读了以后给程序一个通知?所以这里引入了一个IO多路复用模型,IO多路复用的本质是通过一种机制(系统内核缓冲IO数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应地读写操作。

​ 【什么是fd:在linux中,内核吧所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个fd(文件描述符),而对于一个socket的读写也会有相应的文件描述符,称为socketfd】

​ 常见的IO多路复用方式有【select、poll、epoll】,都是Linux API提供的IO复用方式,那么接下来重点说一下select和epoll这两个模型

​ select:进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这样select可以检测多个fd是否处于就绪状态。这个模式有两个缺点:

​ 1、由于它能够同时监听多个文件描述符,假如说有1000个,这个时候如果其中一个fd处于就绪状态了,那么当前进程需要线性轮询所有的fd,也就是监听的fd越多,性能开销越大。

​ 2、同时,select在单个进程中能打开的fd是有限制的,默认是1024,对于那些需要支持单机上万的tcp连接来说确实有点少。

​ epoll:linux还提供了epoll的系统调用,epoll是基于时间驱动方式来代替顺序扫描,因此性能相对来说更高,主要原理是,当被监听的fd中,有fd就绪时,会告知当前进程具体哪一个fd就绪,那么当前进程只需要去从指定的fd上读取数据即可。另外,epoll所能支持的fd上线是操作系统的最大文件句柄,这个数字要远远大于1024.

​ 【由于epoll能够通过事件告知应用进程哪个fd是可读的,所以我们也称这种IO为异步非阻塞IO,当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的异步非阻塞,应该是数据已经完全准备好了,我只需要从用户空间读就行】

4.3 多路复用的好处

IO多路复用可以通过把多个IO的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。它的最大优势是系统开销小,并且不需要创建新的进程或者线程,降低了系统的资源开销。

4.4 一台机器理论能支持的连接数

首先,在确定最大连接数之前,先了解一下系统如何标识一个tcp连接。系统用一个四元组来唯一标识要给tcp连接:(source_ip, source_port, destination_ip, destination_port)。即(源ip,源端口,目的ip,目的端口)四个元素的组合。只要四个元素的组合中有一个元素不一样,那就可以区别不同的连接。

​ 比如:你的IP地址是11.1.2.3,在8080端口监听,那么当一个来自22.4.5.6,端口为5555的连接到达后,那么建立的这条连接的四元组为:(11.1.2.3,8080,22.4.5.6,5555),这时,假设上面的那个用户(22.4.5.6)发来第二条连接请求,端口为6666,那么新连接的四元组为(11.1.2.3,8080,22.4.5.6,6666)。那么,你主机的8080端口建立了两条连接。

​ 通常来说,服务端是固定一个监听端口,比如8080,等待客户端的连接请求。在不考虑地址重用的情况下,即使server端口有多个ip,但是本地监听的端口是独立了的。所以对于tcp连接的4元组中,如果destination_ip和destination_port不变。那么只有source_ip和source_port是可变的,因此最大的tcp连接数应该为客户端的ip数乘以客户端的端口数。在IPV4中,不考虑IP分类等因素,最大的ip数为2的32次方;客户端最大的端口数为2的16次方,也就是65536,也就是服务端单机最大的tcp连接数约为2的48次方。

​ 当然,这只是一个理论值,以linux服务器为例,实际的连接数还取决于

​ 1、内存大小(因为tcp连接都要占用一定的内存)

​ 2、文件句柄限制,每一个tcp连接都需要占一个文件描述符,一旦这个文件描述符使用完了,新来的连接会返回一个“Can`t open so many files“的异常。如果大家知道对于操作系统最大可以打开的文件数限制,就知道怎么去调整这个限制

​ a)可以执行【ulimit -n】得到当前一个进程最大能打开1024个文件,所以你要采用此默认配置最多也就可以并发上千个tcp连接

​ b)可以通过【vim/etc/security/limits/conf】去修改系统最大文件打开数的限制

​ * soft nofile 2048

​ * hard nofile 2048

​ * 表示修改所有用户限制、soft/hard表示软限制还是硬限制,2048表示修改以后的值

​ c)可以通过【cat/proc/sys/fs/file-max】查看linux系统级最大打开文件数限制,表示当前这个服务器最多能同时打开多少个文件

​ 当然,这块还有很多其他的优化的点

​ 3、带宽资源的限制


远程通信协议
http://www.zivjie.cn/2023/07/23/网络通信/远程通信协议/
作者
Francis
发布于
2023年7月23日
许可协议