分布式微服务架构详解,各种微服务架构图解

首页 > 经验 > 作者:YD1662022-10-28 06:26:25

图1-8

第一次握手(SYN=1, seq=x)

客户端发送一个 TCP的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。发送完毕后,客户端进入 SYN_SEND 状态。

第二次握手(SYN=1, ACK=1, seq=y, ACK num=x 1):

服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X 1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

第三次握手(ACK=1,ACK num=y 1)

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

1.4.2 TCP为什么是三次握手?

TCP是全双工,如果没有第三次的握手,服务端不能确认客户端是否ready,不知道什么时候可以往客户端发数据包。三次的握手刚好两边都互相确认对方已经ready。

我们假设网络的不可靠性,

A发起一个连接,当发起一个请求没有得到反馈的时候,会有很多可能性,比如请求包丢失,或者超时,或者B没有响应

由于A不能确认结果,于是再发,当有一个请求包到了B之后,A并不知道这个数据包已经到了B,所以可能还会重试。

所以B收到请求之后,知道了A的存在并且要和我建立连接,这个时候B会发送ack给到A,告诉A我收到了请求包。

对于B来说,这个应答包也是一个网络通信,我怎么知道能不能到达A呢?所以这个时候B不能很主观的认为连接已经建立好了,还需要等到A再次发送应答包来确认。

1.4.3 TCP的四次挥手

如图1-9所示,TCP的连接断开,会通过所谓的四次挥手完成。

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

分布式微服务架构详解,各种微服务架构图解(9)

图1-9

上述交互过程如下:

这个等待实现是2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃(此时A直接进入CLOSE状态)。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

第一次挥手(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 状态。

【问题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报文。

1.4.4 TCP协议的报文传输

连接建立好之后,就开始进行数据包的传输了。那TCP作为一个可靠的通信协议,如何保证消息传输的可靠性呢?

TCP采用了消息确认的方式来保证数据报文传输的安全性,也就是说客户端发送了数据包到服务端后,服务端会返回一个确认消息给到客户端,如果客户端没有收到确认包,则会重新再发送。

为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)

如图1-10所示,为了记录所有发送的包和接收的包,TCP协议在发送端和接收端分别拿会有发送缓冲区和接收缓冲区,TCP的全双工的工作模式及TCP的滑动窗口就是依赖于这两个独立的Buffer和该Buffer的填充状态。

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

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

分布式微服务架构详解,各种微服务架构图解(10)

图1-10

发送端/接收端的缓冲区中是按照包的 ID 一个个排列,根据处理的情况分成四个部分。

这里的第三部分和第四部分之所以做一个区分,其实是因为TCP采用做了流量控制,这里采用了滑动窗口的方式来实现流量整形,避免出现数据拥堵的情况。

分布式微服务架构详解,各种微服务架构图解(11)

图1-11

为了更好的理解数据包的通信过程,我们通过下面这个网址来演示一下

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

1.4.5 滑动窗口协议

上述地址中动画演示的部分,其实就是数据包发送和确认机制,同时还涉及到互动窗口协议。

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

发送窗口

就是发送端允许连续发送的幀的序号表。

发送端可以不等待应答而连续发送的最大幀数称为发送窗口的尺寸。

接收窗口

接收方允许接收的幀的序号表,凡落在 接收窗口内的幀,接收方都必须处理,落在接收窗口外的幀被丢弃。

接收方每次允许接收的幀数称为接收窗口的尺寸。

1.5 理解阻塞通信的本质

理解了TCP通信的原理后,在Java中我们会采用Socket套接字来实现网络通信,下面这段代码演示了Socket通信的案例。

public class ServerSocketExample { public static void main(String[] args) throws IOException { final int DEFAULT_PORT = 8080; ServerSocket serverSocket = null; serverSocket = new ServerSocket(DEFAULT_PORT); System.out.println("启动服务,监听端口:" DEFAULT_PORT); while (true) { Socket socket = serverSocket.accept(); System.out.println("客户端:" socket.getPort() "已连接"); new Thread(new Runnable() { Socket socket; public Runnable setSocket(Socket s){ this.socket=s; return this; } @Override public void run() { try { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String clientStr = null; //读取一行信息 clientStr = bufferedReader.readLine(); System.out.println("客户端发了一段消息:" clientStr); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bufferedWriter.write("我已经收到你的消息了"); bufferedWriter.flush(); //清空缓冲区触发消息发送 } catch (IOException e) { e.printStackTrace(); } } }.setSocket(socket)).start(); } } }

在我们讲Redis的专题中详细讲到过,上述通信是BIO模型,也就是阻塞通信模型,阻塞主要体现的点是

相信大家和我一样有一些以后,这个阻塞和唤醒到底是怎么回事,下面我们简单来了解一下。

1.5.1 阻塞操作的本质

阻塞是指进程在等待某个事件发生之前的等待状态,它是属于操作系统层面的调度,我们通过下面操作来追踪Java程序中有多少程序,每一个线程对内核产生了哪些操作。

strace,Linux操作系统中的指令

  1. 把ServerSocketExample.java,去掉package导入头,拷贝到linux服务器的 /data/app目录下。
  2. 使用javac ServerSocketExample.java进行编译,得到.class文件
  3. 使用下面这个命令来追踪(打开一个新窗口) 按照strace官网的描述, strace是一个可用于诊断、调试和教学的Linux用户空间跟踪器。我们用它来监控用户空间进程和内核的交互,比如系统调用、信号传递、进程状态变更等。 strace -ff -o out java ServerSocketExample -f 跟踪目标进程,以及目标进程创建的所有子进程 -o 把strace的输出单独写到指定的文件
  4. 上述指令执行完成后,会在/data/app目录下得到很多out.*的文件,每个文件代表一个线程。因为Java本身是多线程的。 [root@localhost app]# ll total 748 -rw-r--r--. 1 root root 14808 Aug 23 12:51 out.33320 //最小的表示主线程 -rw-r--r--. 1 root root 186893 Aug 23 12:51 out.33321 -rw-r--r--. 1 root root 961 Aug 23 12:51 out.33322 -rw-r--r--. 1 root root 917 Aug 23 12:51 out.33323 -rw-r--r--. 1 root root 833 Aug 23 12:51 out.33324 -rw-r--r--. 1 root root 819 Aug 23 12:51 out.33325 -rw-r--r--. 1 root root 23627 Aug 23 12:53 out.33326 -rw-r--r--. 1 root root 1326 Aug 23 12:51 out.33327 -rw-r--r--. 1 root root 1144 Aug 23 12:51 out.33328 -rw-r--r--. 1 root root 1270 Aug 23 12:51 out.33329 -rw-r--r--. 1 root root 8136 Aug 23 12:53 out.33330 -rw-r--r--. 1 root root 8158 Aug 23 12:53 out.33331 -rw-r--r--. 1 root root 6966 Aug 23 12:53 out.33332 -rw-r--r--. 1 root root 1040 Aug 23 12:51 out.33333 -rw-r--r--. 1 root root 445489 Aug 23 12:53 out.33334
  5. 打开out.33321这个文件(主线程后面的一个文件),shift g到该文件的尾部,可以看到如下内容。 下面这些方法,都是属于系统调用,也就是调用操作系统提供的内核指令触发相关的操作。 # 创建socket fd socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 5 .... # 绑定8888端口 bind(5, {sa_family=AF_INET6, sin6_port=htons(8888), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0 # 创建一个socket并监听申请的连接, 5表示sockfd,50表示等待队列的最大长度 listen(5, 50) = 0 mprotect(0x7f21d00df000, 4096, PROT_READ|PROT_WRITE) = 0 write(1, "\345\220\257\345\212\250\346\234\215\345\212\241\357\274\214\347\233\221\345\220\254\347\253\257\345\217\243\357\274\23288"..., 34) = 34 write(1, "\n", 1) = 1 lseek(3, 58916778, SEEK_SET) = 58916778 read(3, "PK\3\4\n\0\0\10\0\0U\23\213O\336\274\205\24X8\0\0X8\0\0\25\0\0\0", 30) = 30 lseek(3, 58916829, SEEK_SET) = 58916829 read(3, "\312\376\272\276\0\0\0004\1\367\n\0\6\1\37\t\0\237\1 \t\0\237\1!\t\0\237\1\"\t\0"..., 14424) = 14424 # poll, 把当前的文件指针挂到等待队列,文件指针指的是fd=5,简单来说就是让当前进程阻塞,直到有事件触发唤醒 * events: 表示请求事件,POLLIN(普通或优先级带数据可读)、POLLERR,发生错误。 poll([{fd=5, events=POLLIN|POLLERR}], 1, -1

从这个代码中可以看到,Socket的accept方法最终是调用系统的poll函数来实现线程阻塞的。

通过在linux服务器输入 man 2 poll

man: 帮助手册

2: 表示系统调用相关的函数

DESCRIPTION poll() performs a similar task to select(2): it waits for one of a set of file descriptors to become ready to perform I/O.

poll类似于select函数,它可以等待一组文件描述符中的IO就绪事件

  1. 通过下面命令访问socket server。 telnet 192.168.221.128 8888 这个时候通过tail -f out.33321这个文件,发现被阻塞的poll()方法,被POLLIN事件唤醒了,表示监听到了一次连接。 poll([{fd=5, events=POLLIN|POLLERR}], 1, -1) = 1 ([{fd=5, revents=POLLIN}]) accept(5, {sa_family=AF_INET6, sin6_port=htons(53778), inet_pton(AF_INET6, "::ffff:192.168.221.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 6
1.5.2 阻塞被唤醒的过程

如图1-12所示,网络数据包通过网线传输到目标服务器的网卡,再通过2所示的硬件电路传输,最终把数据写入到内存中的某个地址上,接着网卡通过中断信号通知CPU有数据到达,操作系统就知道当前有新的数据包传递过来,于是CPU开始执行中断程序,中断程序的主要逻辑是

分布式微服务架构详解,各种微服务架构图解(12)

上一页12345下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.