分布式之远程通信协议

HTTP协议通信原理

说到通信,就一定会提起tcp和upd这两种通信协议,以及建立连接的握手过程。而http协议的通信时基于tcp/ip协议之上的一个应用层协议,应用层协议除了http还是有FTP,DNS,SMTP,Telnet等。

涉及到网络协议,我们一定需要知道OSI七层网络模型和TCP/IP四层概念模型,OSI七层网络模型包含(应用层,表示层,会话层,传输层,网络层,数据链路层,物理层)。
TCP/IP四层概念模型包含(应用层,传输层,网络层,数据链路层)。

在这里插入图片描述

请求发起过程,在tcp/ip四层网络模型中所做的事情

当应用程序用TCP传输数据时,数据被送入协议栈中,然后追逐个通过每一层直到被当作一串比特流送入网络。其中每一层对收到的数据都要增加一些首部信息,有时还要增加尾部信息。
分布式之远程通信协议_第1张图片

客户端如何找到目标服务

在客户端发情请求的时候,我们会在数据链路层去组装目标机器的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 地址缓存过一段时间就会过期

接收端收到数据包以后的处理过程

当目标主机收到一个以太网数据帧时,数据就开始从协议栈中由底向上升,同时去掉各层协议加上的报文首部。每层协议都要去检查报文首部中的协议标识,以确定接收数据的上层协议。
分布式之远程通信协议_第2张图片
为什么有了mac层还要走IP层呢?
之前提到,mac地址是唯一的,那理论上,在任何两个设备之间,我应该都可以通过mac地址发送数据,为什么还需要IP地址?
mac地址就好像个人的身份证号,人的身份证号和人户口所在的城市,出生日期有关,但是和人所在的位置没有关系,人是会移动的,知道一个人的身份证号,并不能找到它这个人,mac地址类似,它是和设备的生产者,批次,日期之类的关联起来,知道一个设备的mac,并不能在网络中将输出发送给它,除非两者是同一个局域网。
所以要实现机器之间的通信,我们还需要有IP地址的概念,IP地址表达的是当前机器在网络中的位置,类似于城市名+道路号+门牌号的概念。通过IP层的寻址,我们能知道按何种路径在全世界任意两台Internet上的机器间传输数据。

TCP/IP的分层管理

TCP/IP协议按照层次分为4层:应用层,传输层,网络层,数据链路层。对于分层这个概念,大家一定不陌生,比如我们的分布式架构体系中,会分为业务层,服务层,基础支撑层。比如docker,也是基于分层来实现。所以我们会发现,复杂的程序都需要分层,这个是软件设计的要求,每一层专注于当前领域的事情。如果某些地方需要修改,我们只需要把变动的层替换掉就行,一方面改动影响较少,另一方面整个架构的灵活性也更高。最后,分层之后整个架构的设计也变得相对简单了。

分层负载

了解了分层的概念以后,我们再去理解所谓的二层负载,三层负载,汽车负载就容易多了。
一次http请求过来,一定会从应用层到传输层,完成整个交互。只要是在网络上跑的数据包,都是完整的。可以有下层没上层,绝对不可能有上层没下层。

二层负载

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

三层负载均衡

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

四层负载均衡

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

七层负载均衡

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

TCP/IP协议的深入分析

我们如果需要深入学习网络协议,就要先把一些基本的协议的作用和工作过程搞清楚,网络设备还没智能到人脑的程度,它是由人类创造出来的,它的工作过程肯定是符合人类的交流习惯并按照人类的交流习惯来设计的,所以要以人类的思维方式去理解这些协议。

例如,你给别人打电话,不可能电话一接通你就啪啦啪啦地说一大通,万一对方接通电话后因为有事还没来得及倾听呢?这不太符合正常人类的交流习惯。一般是电话接通后,双方会有个交互的过程,会先说一声“你好”,然后对方也回复一声“你好”,双方通过各自一句“你好”明确对方的注意力都放在了电话沟通上,然后你们双方就可以开始交流了,这才是正常的人类交流方式,这个过程体现在计算机网络里就是网络协议!
我们通过TCP协议在两台电脑建立网络连接之前要先发数据包进行沟通,沟通后再建立连接,然后才是信息的传输。而UDP协议就类似于我们的校园广播,广播内容已经通过广播站播放出去了,你能不能听到,那就与广播站无关了,正常情况下,不可能你说没注意听然后再让广播站再播放一次广播内容。基于这些思路,我们先去了解下TCP里面关注比较多的握手协议

TCP三次握手协议

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

三次握手的流程

  1. 起初两端都处于CLOSED关闭状态,由客户端发起连接并且将标志位SYN置为1,随机产生一个值seq=x,并将该数据包发送给服务端,客户端进入SYN-SENT状态,等待服务端确认
  2. Server收到数据包后由标志位SYN=1得知Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=x+1,随机产生一个值seq=y,并将该数据包发送给Client以确认连接请求,Server进入SYN-RCVD状态,此时操作系统为该TCP连接分配TCP缓存和变量
  3. Client收到确认后,检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1,并且此时操作系统为该TCP连接分配TCP缓存和变量,并将该数据包发送给Server,Server检查ack是否为y+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client和Server就可以开始传输数据

起初A和B都处于CLOSED状态——B创建TCB,处于LISTEN状态,等待A请求——A创建TCB,发送连接请求(SYN=1,seq=x),进入SYN-SENT状态——B收到连接请求,向A发送确认(SYN=ACK=1,确认号ack=x+1,初始序号seq=y),进入SYN-RCVD状态——A收到B的确认后,给B发出确认(ACK=1,ack=y+1,seq=x+1),A进入ESTABLISHED状态——B收到A的确认后,进入ESTABLISHED状态

TCP四次挥手协议

四次挥手表示TCP断开连接的时候,需要客户端和服务端总共发送4个包以确认连接的断开。客户端或服务端均可主动发起挥手动作,因为TCP时一个全双工协议,也就是双向协议。在socket编程中,任意一方执行close()操作即可产生挥手操作
分布式之远程通信协议_第4张图片

单工:数据传输只支持数据在一个方向上传输,例如UDP

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

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

四次挥手

  • A的应用进程先向其TCP发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN-WAIT-1(终止等待1)状态,等待B的确认
  • B收到连接释放报文段后即发出确认报文段,(ACK=1,确认号ack=u+1,序号seq=v),B进入CLOSE-WAIT(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放
  • A收到B的确认后,进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段
  • B没有要向A发出的数据,B发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),B进入LAST-ACK(最后确认)状态,等待A的确认
  • A收到B的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A进入TIME-WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,A才进入CLOSED状态

起初A和B处于ESTABLISHED状态——A发出连接释放报文段并处于FIN-WAIT-1状态——B发出确认报文段且进入CLOSE-WAIT状态——A收到确认后,进入FIN-WAIT-2状态,等待B的连接释放报文段——B没有要向A发出的数据,B发出连接释放报文段且进入LAST-ACK状态——A发出确认报文段且进入TIME-WAIT状态——B收到确认报文段后进入CLOSED状态——A经过等待计时器时间2MSL后,进入CLOSED状态

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

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

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

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

使用协议进行通信

tcp连接建立以后,就可以基于这个连接通道来发送和接受消息了,TCP、UDP都是在基于Socket概念上为某类应用场景而扩展出的传输协议,那么什么是socket呢?socket是一种抽象层,应用程序通过它来发送和接收数据,就像应用程序打开一个文件句柄,把数据读写到磁盘上一样。使用socket可以把应用程序添加到网络中,并与处于同一个网络中的其他应用程序进行通信。不同类型的Socket与不同类型的底层协议簇有关联。主要的socket类型为流套接字(stream socket)和数据报文套接字(datagram socket)。
stream socket把TCP作为端对端协议(底层使用IP协议),提供一个可信赖的字节流服务。数据报文套接字(datagram socket)使用UDP协议(底层同样使用IP协议)提供了一种“尽力而为”的数据报文服务。接下来,我们使用Java提供的API来展示TCP协议的客户端和服务端通信的案例和UDP协议的客户端和服务端通信的案例,然后更进一步了解底层的原理基于TCP协议实现通信实现一个简单的从客户端发送一个消息到服务端的功能
分布式之远程通信协议_第5张图片

基于TCP协议实现通信

实现一个简单的从客户端发送一个消息到服务端的功能

public class ServerDemo {

    public static void main(String[] args) {
        try {
              //TCP 的服务端要先监听一个端口,一般是先调用bind 函数,给这个Socket 赋予一个IP 地址和端口。为什么需要端口呢?
            // 要知道,你写的是一个应用程序,当一个网络包来的时候,内核要通过TCP 头里面的这个端口,来找到你这个应用程序,把包给你。
            // 为什么要IP 地址呢?有时候,一台机器会有多个网卡,也就会有多个IP 地址,你可以选择监听所有的网卡,也可以选择监听一个网卡.
            // 这样,只有发给这个网卡的包,才会给你。
            ServerSocket serverSocket = new ServerSocket(8080);
			//阻塞等待客户端连接接下来,服务端调用accept 函数,拿出一个已经完成的连接进行处理。如果还没有完成,就要等着。
            Socket socket = serverSocket.accept();
			//接建立成功之后,双方开始通过read 和write 函数来读写数据,就像往一个文件流里面写东西一样。
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            System.out.println(bufferedReader.readLine());


        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class ClinetDemo {

    public static void main(String[] args) {

        Socket socket = null;
        try {
            socket = new Socket("127.0.0.1",8080);

            OutputStream os = socket.getOutputStream();

            PrintWriter printWriter = new PrintWriter(os,true);
            printWriter.println("hello server");

            os.close();
            socket.close();

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

基于TCP实现双向通信对话功能

TCP是一个全双工协议,数据通信允许数据同时在两个方向上传输,因此全双工是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。我们来做一个简单的实现

public class ServerDemo {

    public static void main(String[] args) {
        try {
            //TCP 的服务端要先监听一个端口,一般是先调用bind 函数,给这个Socket 赋予一个IP 地址和端口。为什么需要端口呢?
            // 要知道,你写的是一个应用程序,当一个网络包来的时候,内核要通过TCP 头里面的这个端口,来找到你这个应用程序,把包给你。
            // 为什么要IP 地址呢?有时候,一台机器会有多个网卡,也就会有多个IP 地址,你可以选择监听所有的网卡,也可以选择监听一个网卡.
            // 这样,只有发给这个网卡的包,才会给你。
            ServerSocket serverSocket = new ServerSocket(8080);

            Socket socket = serverSocket.accept();

            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            System.out.println(is.readLine());

            String line;

            PrintWriter out = new PrintWriter(socket.getOutputStream());
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

            line=in.readLine();

            while (!line.equals("bye")){
                out.println(in);
                out.flush();
                System.out.println("Server:"+line);
                System.out.println("Clint:"+is.readLine());
                
            }

            is.close();
            in.close();
            socket.close();
            serverSocket.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
ublic class ClinetDemo {

    public static void main(String[] args) {

        Socket socket = null;
        try {
            socket = new Socket("127.0.0.1",8080);

            OutputStream os = socket.getOutputStream();

            PrintWriter printWriter = new PrintWriter(os,true);

            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

            String readLine;


            readLine=in.readLine();

            while (!readLine.equals("bye")){
                printWriter.println(in);
                printWriter.flush();
                System.out.println("Clint:"+readLine);
                System.out.println("Server:"+is.readLine());
            }

            is.close();
            in.close();
            os.close();
            socket.close();

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

总结

通过一个图来简单描述一下socket链接建立以及通信的模型
分布式之远程通信协议_第6张图片

理解TCP的通信原理及IO阻塞

通过上面这个简单的案例,基本清楚了在Java应用程序中如何使用socket套接字来建立一个基于tcp协议的通信流程。接下来,我们在来了解一下tcp的底层通信过程是什么样的

了解TCP协议的通信过程

首先,对于TCP通信来说,每个TCPSocket的内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式及TCP的滑动窗口就是依赖于这两个独立的Buffer和该Buffer的填充状态
接收缓冲区把数据缓存到内核,若应用进程一直没有调用Socket的read方法进行读取,那么该数据会一直被缓存在接收缓冲区内。不管进程是否读取Socket,对端发来的数据都会经过内核接收并缓存到Socket的内核接收缓冲区。
read所要做的工作,就是把内核接收缓冲区中的数据复制到应用层用户的Buffer里。进程调用Socket的send发送数据的时候,一般情况下是将数据从应用层用户的Buffer里复制到Socket的内核发送缓冲区,然后send就会在上层返回。换句话说,send返回时,数据不一定会被发送到对端
分布式之远程通信协议_第7张图片
前面我们提到,Socket的接收缓冲区被TCP用来缓存网络上收到的数据,一直保存到应用进程读走为止。如果应用进程一直没有读取,那么Buffer满了以后,出现的情况是:通知对端TCP协议中的窗口关闭,保证TCP接收缓冲区不会移除,保证了TCP是可靠传输的。如果对方无视窗口大小发出了超过窗口大小的数据,那么接收方会把这些数据丢弃

滑动窗口协议

这个过程中涉及到了TCP的滑动窗口协议,滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题;发送和接受方都会维护一个数据帧的序列,这个序列被称作窗口

发送窗口

就是发送端允许连续发送的幀的序号表。发送端可以不等待应答而连续发送的最大幀数称为发送窗口的尺寸

接收窗口

接收方允许接收的幀的序号表,凡落在接收窗口内的幀,接收方都必须处理,落在接收窗口外的幀被丢弃。接收方每次允许接收的幀数称为接收窗口的尺寸。
在线滑动窗口演示功能
https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/index.html

理解阻塞到底是什么回事

了解了基本通信原理以后,我们再来思考一个问题,在前面的代码演示中,我们通过socket.accept去接收一个客户端请求,accept是一个阻塞的方法,意味着TCP服务器一次只能处理一个客户端请求,当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时,虽然在连接建立后可以向服务端发送数据,但是在服务端处理完之前的请求之前,却不会对新的客户端做出响应,这种类型的服务器称为“迭代服务器”。迭代服务器是按照顺序处理客户端请求,也就是服务端必须要处理完前一个请求才能对下一个客户端的请求进行响应。但是在实际应用中,我们不能接收这样的处理方式。所以我们需要一种方法可以独立处理每一个连接,并且他们之间不会相互干扰。而Java提供的多线程技术刚好满足这个需求,这个机制使得服务器能够方便处理多个客户端的请求。

一个客户端对应一个线程

分布式之远程通信协议_第8张图片

非阻塞模型

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

阻塞IO

前面其实已经简单讲过了阻塞IO的原理,我想在这里重申一下什么是阻塞IO呢?就是当客户端的数据从网卡缓冲区复制到内核缓冲区之前,服务端会一直阻塞。以socket接口为例,进程空间中调用recvfrom,进程从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被成为阻塞IO模型
分布式之远程通信协议_第9张图片

非阻塞IO

那大家思考一个问题,如果我们希望这台服务器能够处理更多的连接,怎么去优化呢?我们第一时间想到的应该是如何保证这个阻塞变成非阻塞吧。所以就引入了非阻塞IO模型,非阻塞IO模型的原理很简单,就是进程空间调用recvfrom,如果这个时候内核缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,然后应用程序通过不断轮询来检查这个状态状态,看内核是不是有数据过来
分布式之远程通信协议_第10张图片

I/O复用模型

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

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

常见的IO多路复用方式有【select、poll、epoll】,都是LinuxAPI提供的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,当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的异步非阻塞,应该是数据已经完全准备好了,我只需要从用户空间读就行】
分布式之远程通信协议_第11张图片

多路复用的好处

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

总结

这节课基于http请求通信到tcp,再到tcp通信的原理以及IO阻塞这块了解了通信的一些基本知识,如果对于这块有感兴趣的同学,可以去找再去找一些资料研究研究,实际上我们只需要掌握到我讲解的程度就行

你可能感兴趣的:(分布式,java)