在分布式架构中,最基本的是:如何去完成一个远程通信。如果没有网络,就没有所谓的分布式系统。如果要涉及到系统之间的通信,就会存在我要发送消息,这个消息会被服务端所接收。消息的形式有很多种,如:字节流、字节数组、Java对象等,外部系统接收到这些数据后需要对其进行处理。在网络层中基于消息通信,有如下两种协议:基于TCP/IP协议和基于UDP/IP协议
a)网络协议:TCP 、 UDP 、 multicast(广播协议)
b)网络传输不光通信,还会存在IO流的传输,常见的IO流中的几种格式:BIO 、 NIO 、 AIO
c)基于应用层去做开发,完成通信协议开发,Java中提供了Socket套接字
d)基于IO,又有一些基于NIO的框架(Netty、Mina)
e)如果在网络层去传递对象的话,还有一个序列化 和 反序列化的过程
此处会涉及到OSI七层网络模型 和 TCP/IP的四层模型
什么是OSI
OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。
OSI七层网络模型:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
什么是TCP/IP四层概念模型
TCP/IP四层概念模型:应用层、传输层、网络层、数据链路层
OSI七层和TCP/IP四层对比
OSI七层网络模型 | TCP/IP四层概念模型 | 对应网络协议 |
应用层(Application) | 应用层 | HTTP、TFTP, FTP, NFS, WAIS、SMTP |
表示层(Presentation) | Telnet, Rlogin, SNMP, Gopher | |
会话层(Session) | SMTP, DNS | |
传输层(Transport) | 传输层 | TCP, UDP |
网络层(Network) | 网络层 | IP, ICMP, ARP, RARP, AKP, UUCP |
数据链路层(Data Link) | 数据链路层 | FDDI, Ethernet, Arpanet, PDN, SLIP, PPP |
物理层(Physical) | IEEE 802.1A, IEEE 802.2到IEEE 802.11 |
网络请求过程如下:
①访问www.baidu.com,在浏览器输入网址,会通过DNS解析,然后获取到一个IP地址,拿到IP地址,便可以去访问该IP的服务器,去发生会话连接。该连接时基于HTTP协议。
②传输层:打开网站,会去发送一些请求,这些请求俗称报文请求。数据发送请求到服务端,数据会到传输层,会对当前的数据报文添加一个TCP头,该TCP头表示当前的协议头,使用的是TCP协议传输。
③网络层:到网络层后,会对当前数据报文上再增加一个IP头
④数据链路层:在到数据链路层,数据链路层会做什么?数据链路层会对数据报文增加一个Mac头(Mac地址,Mac地址是全局唯一的,即网卡地址),标识这个数据包要发送的网卡地址
⑤物理层:在到物理层,物理层会把我们请求的数据转化成比特流进行传输。计算机只认识二进制0和1,会将其转化成为01010101的报文通过网络通信进行传输(如下图所示)
当目的主机收到一个以太网数据时,数据就开始从协议层由底向上升,同时去掉各层协议加上的报文首部。每层协议都要去检查报文首部中的协议标识,以确定接收数据的上层协议。这个过程称作分用
①到达数据链路层,拿到数据以后,就从数据中心摘掉第二层的头,检查下MAC地址和当前的网卡的MAC是否匹配,如果匹配说明当期那消息是发给我的,没错
②在上升至网络层,拿到IP头,判断IP地址是不是自己的,如果不是就转发,继续交给上一层处理
③继续上升至传输层,TCP头中会携带端口,将报文交给指定端口的进程去处理(如下图所示)
MAC地址就好像个人的身份证号,人的身份证号和人户口所在的城市,出生的日期有关,但是和人所在的位置没有关系,人是会移动的,知道一个人的身份证号,并不能找到它这个人,MAC地址类似,它是和设备的生产者,批次,日期之类的关联起来,知道一个设备的MAC,并不能在网络中将数据发送给它,除非它和发送方的在同一个网络内。所以要实现机器之间的通信,还需要有IP地址的概念,IP 地址表达的是当前机器在网络中的位置,类似于城市名+道路号+门牌号的概念。通过IP层的寻址,我们能知道按何种路径在全世界任意两台Internet上的的机器间传输数据。
IP协议只是一个协议,TCP和UDP是在IP协议之上,最为著名的两种传输层协议,他们都是使用IP作为网络层协议。
IP 协议提供了一组数据报文服务,每组分组报文都是由网络独立处理和分发,就像寄送快递包裹一样,为了实现这个功能,每个IP 报文必须包含一个目的地址的字段;就像我们寄送快递都需要写明收件人信息,但是和我们寄送快递一样,也可能会出现包裹丢失问题。IP协议不可靠,有丢失数据的可能
为了解决可靠性问题,所以在IP协议层之上的传输层,提供了两种可以选择的协议,TCP(可靠协议)、UDP(不可靠协议)。这两种协议都是建立在IP层所提供的服务基础上,根据应用程序的不同需求选择不同方式的传输;
TCP/IP协议(可靠)
TCP最重要的是:头部携带一个目标端口号,端口号是最重要的一个寻址过程。
TCP 协议能够检测和恢复IP层提供的主机到主机的通信中可能发生的报文丢失、重复及其他错误。TCP 提供了一个可信赖的字节流通道,这样应用程序就不需要考虑这些问题。同时,TCP 协议是一种面向连接的协议,在使用TCP进行通信之前,两个应用程序之间需要建立一个TCP 连接,而这个连接又涉及到两台电脑需要完成握手消息的交换。
UDP/IP协议(不可靠)
UDP协议不会对IP层产生的错误进行修复,而是简单的扩展了IP协议“尽力而为”的数据报文服务,使他能够在应用程序之间工作,而不是在主机之间工作,因此使用UDP协议必须要考虑到报文丢失,顺序混乱的问题
TCP建立可靠的连接,是基于我们熟知的三次握手、四次挥手的。
由于TCP协议是一种可信的传输协议,所以在传输之前,需要通过三次握手建立一个连接,所谓的三次握手,就是在建立TCP链接时,需要客户端和服务端总共发送3个数据包来确认连接的建立。
为什么建立连接需要三次握手?
首先非常明确的是两次握手是最基本的。第一次握手,客户端发了个连接请求消息到服务端,服务端收到信息后知道自己与客户端是可以连接成功的,但此时客户端并不知道服务端是否已经接收到了它的请求,所以服务端接收到消息后的应答,客户端得到服务端的反馈后,才确定自己与服务端是可以连接上的,这就是第二次握手。客户端只有确定了自己能与服务端连接上才能开始发数据。所以两次握手肯定是最基本的。
看到这里,你或许会问,那么为什么需要第三次握手呢?我们来看一下,假设一下如果没有第三次握手,而是两次握手后我们就认为连接成功了,那么会发生什么?第三次握手是为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误。譬如发起请求遇到类似这样的情况:客户端发出去的第一个连接请求由于某些原因在网络节点中滞留了导致延迟,直到连接释放的某个时间点才到达服务端,这是一个早已失效的报文,但是此时服务端仍然认为这是客户端的建立连接请求第一次握手,于是服务端回应了客户端,第二次握手。
如果只有两次握手,那么到这里,连接就建立了,但是此时客户端并没有任何数据要发送,而服务端还在傻傻的等候着,造成很大的资源浪费。所以需要第三次握手,只有客户端再次回应一下,就可以避免这种情况。
为什么断开连接需要四次挥手?
第1次挥手:客户端会发送一个FIN的标志,FIN=1标识我要去关闭连接。客户端发送完成后,会进入一个FIN_WAIT_1的状态
第2次挥手:服务端收到请求,会确认客户端发送过来的FIN包,并给客户端一个包,标识自己接受了客户端的请求,给客户端一个ACK响应,并不表示服务端能够做好关闭连接请求,比如说服务器端现在还有连接请求未完成,此时他不会告诉客户端,当前这个连接时可以关闭的。此时服务端进入CLOSE_WAIT状态,客户端会进入FIN_WATI_2的状态
第3次挥手:服务端连接处理完毕,再次发送消息到客户端,告诉客户端已经准备好进入断开连接状态,此时服务端进入LAST_ACK状态
第4次挥手:此时客户端,会发送一个ACK结果告诉服务端客户端会进入TIME_WAIT状态。注意,此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,客户端才会进入CLOSED状态。客户端告诉服务端可以关闭连接了,此时服务端也会进入 CLOSED 状态
握手是因为因为当Server 端收到Client 端的SYN 连接请求报文后,可以直接发送SYN+ACK 报文。其中ACK做报文是用来应答的,SYN 报文是用来同步的。故只有三次握手。
关闭连接时,当Server 端收到FIN 报文时,很可能并不会立即关闭SOCKET(因为可能还有消息没处理完),所以只能先回复一个ACK 报文,告诉Client 端,"你发的FIN 报文我收到了"。只有等到我Server 端所有的报文都发送完了,我才能发送FIN 报文,因此不能一起发送。故需要四步握手。
TCP协议/UDP协议都是基于Socket概念上为某一个应用场景而创建出来的协议。
什么是Socket?
应用程序通过它来发送和接收消息。相当于通过应用程序去读取数据磁盘的数据,或者将数据写入到磁盘中。使用Socket可以将我们的应用程序添加到网络上,并且能够跟他处于同一网络的应用进行通信,这就是Socket。
不同的类型的Socket和底层的协议是有关系的,有两种:基于stream socket/datagram socket(基于数据报文)。
①Stream Socket:把TCP作为端对端的协议,提供了一个可以信赖的字节传输服务
②Datagram Socket:提供的是UDP尽力而为的协议。
TCP协议是基于ServerSocket来编程的;UDP协议则是基于DatagramSocket来编程的
①TCP协议编程
/**
* TCP协议编程(服务端)
*/
public class TcpServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
BufferedReader bufferedReader = null;
Socket socket = null;
try{
//打开一个端口,去绑定当前的端口和主机号
serverSocket = new ServerSocket(8080);
System.out.println("tcp---服务器等待连接中");
//通过serverSocket.accept()去获取客户端连接
socket = serverSocket.accept();
System.out.println("tcp---客户端连接成功");
//连接成功,收到一个数据流(输入流)
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//直接输出
System.out.println("tcp---收到客户端发来的数据:"+bufferedReader.readLine());
} catch (IOException e){
e.printStackTrace();
} finally {
if(bufferedReader != null){
bufferedReader.close();
}
if(socket != null){
socket.close();
}
if(serverSocket != null){
serverSocket.close();
}
}
}
}
/**
* TCP协议编程(客户端)
*/
public class TcpClient {
public static void main(String[] args) throws IOException {
Socket socket = null;
PrintWriter out = null;
try {
System.out.println("tcp---启动客户端");
//创建连接
socket = new Socket("127.0.0.1", 8080);
System.out.println("tcp---客户端连接成功");
//使用PrintWriter,通过socket.getOuptStream传输
out = new PrintWriter(socket.getOutputStream(),true);
//向服务端发送数据
out.println("Hello");
//断开连接
System.out.println("tcp---客户端断开连接");
} catch (IOException e){
e.printStackTrace();
} finally {
if(socket != null){
socket.close();
}
}
}
}
②UDP协议编程
/**
* UDP协议编程(服务端)
*/
public class UdpServer {
public static void main(String[] args) {
DatagramSocket datagramSocket = null;
try {
//创建服务,并且接收一个数据包
datagramSocket = new DatagramSocket(8080);
System.out.println("udp---服务器等待连接中");
//创建一个byte类型的数组,用于存放接收到得数据
byte[] receiveData = new byte[1024];
//创建一个DatagramPacket对象,并指定DatagramPacket对象的大小
DatagramPacket packet = new DatagramPacket(receiveData, receiveData.length);
//读取客户端发来的数据
datagramSocket.receive(packet);
System.out.println("udp---客户端连接成功");
//把客户端发送的数据转换为字符串。
//使用三个参数的String方法。参数一:数据包 参数二:起始位置 参数三:数据包长
String result = new String(packet.getData(), packet.getOffset(), packet.getLength());
System.out.println("udp---收到客户端发来的数据:" + result);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (datagramSocket != null) {
datagramSocket.close();
}
}
}
}
/**
* UDP协议编程(客户端)
*/
public class UdpClient {
public static void main(String[] args) {
//创建DatagramSocket对象
DatagramSocket datagramSocket = null;
try {
System.out.println("udp---启动客户端");
datagramSocket = new DatagramSocket();
//使用InetAddress.getByName把IP地址转换为网络地址
InetAddress address = InetAddress.getByName("localhost");
System.out.println("udp---获取网络地址:"+address);
//要发送的报文数据,把字符串str字符串转换为字节数组
byte[] sendData = "Hello".getBytes();
//创建一个DatagramPacket对象,用于发送数据。
//参数一:要发送的数据 参数二:数据的长度 参数三:服务端的网络地址 参数四:服务器端端口号
DatagramPacket sendPacket = new DatagramPacket(sendData,sendData.length,address,8080);
System.out.println("udp---客户端连接成功");
//把数据发送到服务端
datagramSocket.send(sendPacket);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(datagramSocket != null){
datagramSocket.close();
}
}
}
}
END