分散式架构之网络通讯协议

分散式架构之网络通讯协议

@[TOC]

1.DNS简介

1.DNS 即 Domain Name System 域名系统;

2.之所以会有域名,就是为了解决直接记忆IP不好记忆的问题;

2.CDN简介

3.HTTP协议通讯原理

域名被成功解析以后, 客户端和服务端之间,是怎么建立连线并且如何通讯的呢?

1.http是应用层协议;ftp,smtp,telnet也是应用层协议

2.http协议是建立在tcp/udp协议之上的;

3.1.网络协议概念模型

3.1.1.OSI 七层网络模型和TCP/IP 四层概念模型

1.OSI 七层网络模型包含(应用层、表示层、会话层、传输层、网络层、资料链路层、物理层)

2.TCP/IP 四层概念模型包含(应用层、传输层、网络层、资料链路层)

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

1.应用层:主要是我们访问某个连线,发起某个http请求

2.传输层:http应用协议是基于tcp/udp协议进行传输,头资讯中tcp标示协议型别

3.网络层:头资讯ip头,ip地址是一个网络卡在网络中的通讯地址,相当于门牌号;

4.资料链路层:mac头,标示资料传送到网络卡地址;全域性唯一;

5.最后一个就是资料传输的过程中,会转换为位元码的形式传输;

3.1.2.1.客户端如何找到目标服务

1.在客户端发起请求的时候, 我们会在资料链路层去组装目标机器的 MAC 地址, 目标机器的mac 地址怎么得到呢?

2.这里就涉及到一个 ARP 协议,这个协议简单来说就是已知目标机器的 ip,需要获得目标机器的mac 地址。

(传送一个广播讯息,这个 ip 是谁的,请来认领。认领 ip 的机器会发送一个 mac 地址的响应),有了这个目标 MAC 地址,

资料包在链路上广播, MAC 的网络卡才能发现,这个包是给它的。

3.MAC 的网络卡把包收进来,然后开启 IP 包,发现 IP 地址也是自己的,再开启 TCP 包,发现埠是自己,也就是 80 埠,

而这个时候这台机器上有一个 nginx 是 80 埠。

4.于是将请求提交给 nginx, nginx 返回一个网页。然后将网页需要发回请求的机器。然后层层封装,最后到 MAC 层。

因为来的时候有源 MAC 地址,返回的时候,源 MAC 就变成了目标 MAC,再返给请求的机器。

5.为了避免每次都用 ARP 请求,机器本地也会进行 ARP 快取。当然机器会不断地上线下线,IP 也可能会变,所以 ARP

的 MAC 地址快取过一段时间就会过期。

3.1.3.接收端收到资料包以后的处理过程

1.当目的主机收到一个以太网资料帧时,资料就开始从协议栈中由底向上升,

同时去掉各层协议加上的报文首部。每层协议都要去检查报文首部中的协议标识,以确定接收资料的上层协议。

2.物理层:当资料经过网络卡的时候,通过下面资料链路层判断,是否需要接收

3.资料链路层:拿到资料后,就先从资料中摘到第二层的头,检查一下MAC地址和当前网络卡的MAC地址是否匹配

4.网络层:拿到ip头,判断ip是不是自己的,若果不是就转发,如果是,继续给上一层处理

5.传输层:TCP头中会携带埠,将报文给指定的埠的程序进行处理;

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

1.MAC是全域性唯一的,理论上来讲,在任何两个装置之间,我们可以通过mac地址来发送资料,但是为啥还要IP?

2.可以这样理解,MAC地址如同人的身份证,是全域性唯一的,

但是身份证所在位置和人所在的位置实时不一定是一致的,有可能身份证在家里,人在公司;

mac一样,知道mac不一定知道ip,ip有可能变了;

3.1.4.分层负载

1.这里说的分层负载说的是服务端(接收端的)一个分层负载;

3.1.4.1.二层负载

1.如上图所示,二层负载针对的是资料链路层的负载,针对的是MAC地址;

2.负载均衡服务器对外依然提供一个 VIP(虚 IP),丛集中不同的机器采用相同 IP 地址,

但是机器的 MAC 地址不一样。当负载均衡服务器接受到请求之后,通过改写报文的目标 MAC 地址

的方式将请求转发到目标机器,实现负载均衡二层负载均衡会通过一个虚拟 MAC 地址接收请求,然后再分配到真实的 MAC 地址;

3.1.4.2.三层负载

1.三层负载即网络层的负载,针对IP,和二层负载均衡类似,负载均衡服务器对外依然提供一个 VIP(虚 IP),

但是丛集中不同的机器采用不同的 IP 地址。

2.当负载均衡服务器接受到请求之后,根据不同的负载均衡算法,通过 IP 将请求转发至不同的真实服务器

3.三层负载均衡会通过一个虚拟 IP 地址接收请求,然后再分配到真实的 IP 地址;

3.1.4.3.四层负载

1.四层负载主要是传输层负载;

2.四层负载均衡工作在 OSI 模型的传输层,由于在传输层,只有 TCP/UDP 协议,

这两种协议中除了包含源 IP、目标 IP 以外,还包含源埠号及目的埠号。

四层负载均衡服务器在接受到客户端请求后,以后通过修改资料包的地址资讯(IP+埠号)

将流量转发到应用服务器。

3.四层通过虚拟 IP + 埠接收请求,然后再分配到真实的服务器

3.1.4.4.七层负载

1.七层负载主要是应用层负载;

2.七层负载均衡工作在 OSI 模型的应用层,应用层协议较多,常用 http、 radius、 dns 等。

七层负载就可以基于这些协议来负载。这些应用层协议中会包含很多有意义的内容。比如同一个

Web 服务器的负载均衡,除了根据 IP 加埠进行负载外,还可根据七层的 URL、浏览器类别来

决定是否要进行负载均衡

3.七层通过虚拟的 URL 或主机名接收请求,然后再分配到真实的服务器

4.我们平时配置Nginx负载,主要是应用层的负载

4.TCP/IP 协议的深入分析

4.1.TCP/IP和UDP/IP简介

1.TCP/IP协议是一种可靠的协议,源于通过三次握手协议去建立连线;

2.UDP/IP协议是一种不可靠的协议,资料包有可能丢失,有可能没有被接收;

4.2.TCP三次握手协议

从上面的我们知道,http协议中,底层用你到了tcp的通讯协议;接下来就是重点讲述tcp通讯协议

1.客户端A 首先发起建立联机[SYN]请求,同时会带一个随机的顺序号码[seqnum=x];

客户端A 发起建立联机的请求之后,即进入建立联机已传送的状态[SYN_SENT];

2.客户端B 在接收到客户端A发起的建立联机[SYN]请求之后,开始确认,确认成功,客户端会发送确认[ACK]建立联机

[SYN]请求,同时传送确认号码acknum[acknum=x+1,这里的x是客户端传送请求的时候传递过来的seqnum=x,

即我们是对客户端传送的内容的确认];

客户端B确认之后,会进入已经建立联机接收的状态[SYN_RCVD];

3.客户端A 在接收成功之后,客户端A传送确认[ACK=1],同时传送随机顺序编码x+1(因为在第一次发起请求的时候是x)

同时传送对客户端B传送的seqnum=y的确认[acknum=y+1]

4.2.1.为啥还要进行第三次握手

1.这里注意下,为啥客户端A发起了请求(第一次握手),客户端B也进行了确认(第二次握手),为啥还要进行第三次握手呢? 主要是基于,第二次握手客户端B发起确认之后,客户端A已经正常接收了;

4.2.2.SYN 攻击

在三次握手过程中, Server 传送 SYN-ACK 之后,收到 Client 的 ACK 之前的 TCP 连线称为

半连线(half-open connect),

此时 Server 处于 SYN_RCVD 状态,当收到 ACK 后, Server转入 ESTABLISHED 状态。

SYN 攻击就是 Client 在短时间内伪造大量不存在的 IP 地址,并向Server 不断地传送 SYN 包,

Server 回复确认包,并等待 Client 的确认,由于源地址是不存在的,因此, Server 需要不断重发直至超时,

这些伪造的 SYN 包将产时间占用未连线伫列,导致正常的 SYN 请求因为伫列满而被丢弃,

从而引起网络堵塞甚至系统瘫痪。 SYN 攻击时一种典型的 DDOS 攻击,检测 SYN 攻击的方式非常简单,

即当 Server 上有大量半连线状态且源 IP 地址是随机的,则可以断定遭到 SYN 攻击了

4.3.TCP 四次挥手协议

1.第一次挥手(FIN=1, seqnum=x)

假设客户端想要关闭连线,客户端传送一个 FIN 标志位置为 1 的包,表示自己已经没有资料

可以传送了,但是仍然可以接受资料。 传送完毕后,客户端进入 FIN_WAIT_1 状态。

2.第二次挥手(ACK=1, ACKnum=x+1)

服务器端确认客户端的 FIN 包,传送一个确认包,表明自己接受到了客户端关闭连线的请求,

但还没有准备好关闭连线。 传送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这

个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连线。

3.第三次挥手(FIN=1, seqnum=w)

服务器端准备好关闭连线时,向客户端传送结束连线请求, FIN 置为 1。 传送完毕后,服务器

端进入 LAST_ACK 状态,等待来自客户端的最后一个 ACK。

4.第四次挥手(ACK=1, ACKnum=w+1)

客户端接收到来自服务器端的关闭请求,传送一个确认包,并进入 TIME_WAIT 状态,等待

可能出现的要求重传的 ACK 包。服务器端接收到这个确认包之后,关闭连线,进入 CLOSED 状态。

客户端等待了某个固定时间(两个最大段生命周期, 2MSL, 2 Maximum Segment 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 端也可以关闭连线了。 Ok, TCP 连线就这样关闭了!

4.3.1.问题

4.3.1.1.【问题 1】为什么连线的时候是三次握手,关闭的时候却是四次握手?

答: 三次握手是因为因为当 Server 端收到 Client 端的 SYN 连线请求报文后,可以直接传送SYN+ACK 报文。

其中 ACK 报文是用来应答的, SYN 报文是用来同步的。但是关闭连线时,当 Server 端收到 FIN 报文时,

很可能并不会立即关闭 SOCKET(因为可能还有讯息没处理完),所以只能先回复一个 ACK 报文,告诉 Client 端,

你发的 FIN 报文我收到了。只有等到我 Server 端所有的报文都发送完了,我才能传送 FIN 报文,因此不能一起传送。

故需要四步握手。

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

答:虽然按道理,四个报文都发送完毕,我们可以直接进入 CLOSE 状态了,但是我们必须假象网络是不可靠的,

有可以最后一个 ACK 丢失。所以 TIME_WAIT 状态就是用来重发可能丢失的 ACK 报文

5.使用协议进行通讯

5.1.概念

1.tcp连线建立以后,就可以基于这个连线通道来发送和接受讯息了,

2.TCP、 UDP 都是在基于Socket 概念上为某类应用场景而扩展出的传输协议,

5.2.socket

1.socket 是一种抽象层,应用程序通过它来发送和接收资料,就像应用程序开启一个档案控制代码,把资料读写到磁盘上一样。

2.使用 socket 可以把应用程序新增到网络中,并与处于同一个网络中的其他应用程序进行通讯。不同型别的 Socket 与

不同型别的底层协议簇有关联。

3.主要的 socket 型别为流套接字(stream socket)和资料报文套接字(datagram socket)。

stream socket把 TCP作为端对端协议(底层使用 IP 协议),提供一个可信赖的字节流服务。

datagram socket使用 UDP 协议(底层同样使用 IP 协议)提供了一种“尽力而为”的资料报文服务。

5.3.基于 TCP 协议实现通讯

1.TCP 的服务端要先一个埠,一般是先呼叫bind 函式,给这个 Socket 赋予一个 IP 地址和埠。

2.为什么需要埠呢?要知道,你写的是一个应用程序,当一个网络包来的时候,核心要通过 TCP 头里面的这个埠,

来找到你这个应用程序,把包给你。

3.为什么要 IP 地址呢?有时候,一台机器会有多个网络卡,也就会有多个 IP 地址,你可以选择有的网络卡,

也可以选择一个网络卡,这样,只有发给这个网络卡的包,才会给你

5.3.1 样例1:客户端发起请求,服务端接收

ServerSocketDemo.java

package com.gaoxinfu.demo.socket;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.net.ServerSocket;

import java.net.Socket;

public class ServerSocketDemo {

public static void main(String[] args) {

//服务端

ServerSocket serverSocket=null;

BufferedReader bufferedReader=null;

try {

serverSocket = new ServerSocket(8080);

//开始进入阻塞,等待客户端进行连线,服务端呼叫 accept 函式,拿出一个已经完成的连线进行处理。如果还没有完成,就要等著

Socket socket=serverSocket.accept();

//注意这里是服务端ServerSocket,所以获取的是客户端的输入流

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

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

} catch (IOException e) {

e.printStackTrace();

}finally {

if (bufferedReader!=null){

try {

bufferedReader.close();

} catch (IOException e) {

e.printStackTrace();

}

}

if (serverSocket!=null){

try {

serverSocket.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

}

ClientSocketDemo

package com.gaoxinfu.demo.socket;

import java.io.IOException;

import java.io.PrintWriter;

import java.net.Socket;

public class ClientSocketDemo {

public static void main(String[] args) {

Socket socket=null;

PrintWriter printWriter=null;

try {

socket=new Socket(127.0.0.1,8080);

//Socket这里是读取的客户端的输入流

printWriter=new PrintWriter(socket.getOutputStream());

printWriter.println(hello);

} catch (IOException e) {

e.printStackTrace();

}finally {

if (printWriter!=null){

printWriter.close();;

}

if (socket!=null){

try {

socket.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

}

输出

1.可以看到服务器端ServerSocketDemo启动成功之后,启动客户端ClientSocketDemo,然后

客户端输出了一个hello;服务器端已经接收并输出到控台;

5.3.2 样例2:基于TCP实现双向对话通讯功能

TCP 是一个全双工协议, 资料通讯允许资料同时在两个方向上传输,因此全双工是两个单工通讯方式的结合,

它要求传送装置和接收装置都有独立的接收和传送能力。

上面的两个类,只是实现了单向的通讯[即客户端到服务端的通讯]

我们在上面两个类的基础上进行改造;

ServerSocketDemo.java

package com.gaoxinfu.demo.socket;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.ServerSocket;

import java.net.Socket;

public class ServerSocketDemo {

public static void main(String[] args) {

//服务端

ServerSocket serverSocket=null;

BufferedReader clientBufferedReader=null;

try {

//server端 更重要的事本机的埠的动作,所以只需要埠即可

serverSocket = new ServerSocket(8080);

//开始进入阻塞,等待客户端进行连线,服务端呼叫 accept 函式,拿出一个已经完成的连线进行处理。如果还没有完成,就要等著

Socket socket=serverSocket.accept();

//注意这里是服务端ServerSocket,所以获取的是客户端的输入流

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

// System.out.println(clientBufferedReader.readLine());

PrintWriter printWriter=new PrintWriter(socket.getOutputStream());

//控制台输入资料,构造BufferedReader物件

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

String line;

line=serverBufferedReader.readLine();

while(!line.equals(bye)){

//向客户端输出该字串

printWriter.println(line);

printWriter.flush();

System.out.println(Server: +line);

System.out.println(Client: +clientBufferedReader.readLine());

line=clientBufferedReader.readLine();

}

} catch (IOException e) {

e.printStackTrace();

}finally {

if (clientBufferedReader!=null){

try {

clientBufferedReader.close();

} catch (IOException e) {

e.printStackTrace();

}

}

if (serverSocket!=null){

try {

serverSocket.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

}

ClientSocketDemo.java

package com.gaoxinfu.demo.socket;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.Socket;

public class ClientSocketDemo {

public static void main(String[] args) {

Socket socket=null;

PrintWriter printWriter=null;

BufferedReader clientBufferedReader=null;

BufferedReader serverBufferedReader=null;

try {

//客户端 侧重于传送目标,因此需要ip和埠

socket=new Socket(127.0.0.1,8080);

//构造客户端的输入

printWriter=new PrintWriter(socket.getOutputStream());

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

String readLine=;

readLine=clientBufferedReader.readLine();

//获取服务端的输入资讯

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

while(!bye.equals(readLine)){

printWriter.println(readLine);

printWriter.flush();;

System.out.println(Client 输入: +readLine);

System.out.println(Server 输入: +serverBufferedReader.readLine());

readLine=clientBufferedReader.readLine();

}

} catch (IOException e) {

e.printStackTrace();

}finally {

if (printWriter!=null){

printWriter.close();;

}

if (socket!=null){

try {

socket.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

}

5.3.3 Socket通讯模型

5.3.4 TCP协议通讯原理

1.TCP采用的是全双工的工作模式,对每一个TCP的SOCKET来说,都有一个传送缓冲区和接收缓冲区与之对应;

2.Socket将应用层的buffer资料通过send()方法传送写入到核心传送缓冲区;

3.传送缓冲区资料通过[TCP的滑动视窗协议]将资料写入到接收缓冲区;

4.Socket通过rev()方法将读取接收缓冲区的资料到应用层buffer中;

5.3.5.TCP 的滑动视窗协议

1.早期的网络通讯中,通讯双方不会考虑网络的拥挤情况直接传送资料。由于大家不知道网络拥塞状况,同时传送资料,导致中间节点

阻塞掉包,谁也发不了资料,所以就有了滑动视窗机制来解决此问题;

2.TCP 的滑动视窗协议, 滑动视窗(Sliding window)是一种流量控制技术。

3.传送和接受方都会维护一个数据帧的序列,这个序列被称作视窗;

5.3.5.1.传送视窗

1.传送视窗就是传送端[客户端]允许连续传送的帧的序号表。

2.传送端可以不等待应答而连续传送的最大帧数称为传送视窗的尺寸。

5.3.5.2.接收视窗

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

2.接收方每次允许接收的帧数称为接收视窗的尺寸。

5.3.5.3.滑动视窗演示

参考

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

6.阻塞

6.1.阻塞是什么鬼?

1.通过上面TCP的通讯原理,我们可以知道socket.accept()方法去接收一个客户端的请求,accept方法是一个阻塞的方法,

就是说:TCP服务器一次只能处理一个客户端请求;当一个客户端向一个已经被其他客户端占用的服务器传送连线请求时,

虽然在连线建立后可以向服务端传送资料,但是在服务端处理完之前的请求之前,却不会对新的客户端做出响应;

2.上面这种型别的服务器称为“迭代服务器”:迭代服务器是按照顺序处理客户端请求,也就是服务端必须要处理完前一个请求才能对

下一个客户端的请求进行响应。但是在实际应用中,我们不能接收这样的处理方式。

6.2.多执行绪解决阻塞问题

1.我们需要一种方法可以独立处理每一个连线,并且他们之间不会相互干扰。

2.Java 提供的多执行绪技术刚好满足这个需求,这个机制使得服务器能够方便处理多个客户端的请求

6.2.1.多执行绪解决阻塞问题:方式1 一个客户端一个执行绪

6.2.1.多执行绪解决阻塞问题:方式2 执行绪池

6.3.阻塞IO模型

1.客户端的资料从网络卡缓冲区复制到核心缓冲区之前,服务端会一直阻塞。以socket界面为例,程序空间中呼叫 rev,进

程从呼叫 rcv 开始到它返回的整段时间内都是被阻塞的,因此被成为阻塞 IO 模型

当使用 read()/rev() 读取资料时:

1.首先会检查缓冲区,如果缓冲区中有资料,那么就读取,否则函式会被阻塞,直到网络上有资料到来。

2.如果要读取的资料长度小于缓冲区中的资料长度,那么就不能一次性将缓冲区中的所有资料读出,剩余资料将不断积压,直到有 read()/recv() 函式再次读取。

3.直到读取到资料后 read()/recv() 函式才会返回,否则就一直被阻塞。

6.4.非阻塞模型

1.上面这种模型虽然优化了 IO 的处理方式,但是,不管是执行绪池还是单个执行绪,执行绪本身的处理个数是有限制的,对于操作系统来说,

如果执行绪数太多会造成 CPU 上下文切换的开销。因此这种方式不能解决根本问题

所以在 Java1.4 以后,引入了 NIO(New IO)的功能,

猜你喜欢