网络通信协议
一次Web请求的网络通信历程
OSI七层模型和TCP/IP四层模型
OSI七层模型 | TCP/IP四层模型 | 功能 |
---|---|---|
应用层(Application Layer) | 应用层(Application Layer) | 为用户的应用提供服务并支持网络访问 |
表示层(Presentation Layer) | 应用层(Application Layer) | 负责转化数据格式,并处理数据加密和数据压缩 |
会话层(Session Layer) | 应用层(Application Layer) | 负责管理网络中计算之间的通信,提供传输层不具备的链接功能 |
传输层(Transport Layer) | 传输层(Transport Layer) | 提供端对端的接口 |
网络层(Network Layer) | 网络互联层(Internet Layer) | 为数据包选择路由 |
数据链路层(Data Link Layer) | 网络访问(链路)层(Network Access (Link) Layer) | 传输有地址的帧以及错误检测功能 |
物理层(Physical Layer) | 网络访问(链路)层(Network Access (Link) Layer) | 在物理媒介上传输二进制格式数据 |
数据包格式
物理层
负责数据的物理传输,计算机输入的只能是二进制数据,在通信线路中有光纤、电缆、无线各种设备。光信号、电信号以及无线电磁信号在物理上是完全不同的,如何让这些不同的设备能够理解、处理相同的二进制数据,这就是物理层要解决的问题。
链路层
链路层就是将数据进行封装后交给物理层进行传输,主要就是将数据封装成数据帧,以帧为单位通过物理层进行通信,有了帧,就可以在帧上进行数据校验,进行流量控制。
链路层会定义帧的大小,这个大小也被称为最大传输单元。像HTTP要在传输的数据上添加一个HTTP头一样,数据链路层也会将封装好的帧添加一个帧头,帧头里记录的一个重要信息就是发送者和接受者的MAC地址。MAC地址是网卡的设备标识符,是唯一的,数据帧通过这个信息确保数据送达到正确的目标机器。
数据链路层的负载均衡
应用服务器集群的IP地址都是一样的,所以每台应用服务器都会收到请求,链路层会校验MAC地址是否发给自己的,如果不是则拒收。
网络层(IP协议)
网际互连协议(IP,Internet Protocol),是TCP/IP体系中的网络层协议。根据端到端的设计原则,IP只为主机提供一种无连接、不可靠的、尽力而为的数据报传输服务。
网络层IP协议使得互联网应用根据IP地址就能访问到目标服务器,请求利凯App后,到达运营服务商的交换机,交换机会根据这个IP地址进行路由转发,可能中间会经过很多个转发节点,最后数据到达目标服务器。
网络层的数据要交给链路层进行处理,而链路层帧的大小定义了最大传输单元,网络层的IP数据包必须要小于最大传输单元才能进行网络传输,这个数据包也有一个IP头,主要包括的就是发送者和接受者的IP地址。
IP负载均衡
传输层(TCP协议)
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793 [1] 定义。
IP协议不是一个可靠的通信协议,不会建立稳定的通信链路,并不会确保数据一定送达。要保证通信的稳定可靠,需要传输层协议TCP。
TCP协议是一种面向连接的、可靠的、基于字节流的传输层协议。TCP作为一个比较基础的通讯协议,有很多重要的机制保证了TCP协议的可靠性和强壮性:
- 使用序号对收到的TCP报文段进行排序和检测重复的数据
- 无错传输,使用校验和检测报文段的错误
- 使用确认和计时器来检测和纠正丢包或者延时
- 流量控制,避免主机分组发送得过快而使接收方来不及完全手下
- 拥塞控制,发送方根据网络承载情况控制分组的发送量,以获得高性能同时避免拥塞崩溃丢失包的重传
TCP建立连接的三次握手过程
- APP先发送SYN=1,Seq=X的报文,表示请求建立连接,X是个随机数;
- 服务器收到这个报文后,应答SYN=1,ACK=X+1,Seq=Y的报文,表示同意建立连接
- APP收到这个报文后,检查ACK的值为自己发送的Seq值+1,确认建立连接。并发送ACK=Y+1的报文给服务器;服务器收到这个报文后检查ACK值为自己发送的Seq值+1,确认建立连接。至此,App和服务器建立起TCP连接,就可以进行数据传输了
TCP关闭连接4次挥手
- 客户端向服务端发送一个FIN,请求关闭数据传输
- 当服务器接收到客户端的FIN时,向客户端发送一个ACK,其中ACK的值等于FIN+SEQ
- 然后服务器向客户端发送一个FIN告诉客户端应用程序关闭
- 当客户端收到服务器端的FIN时,回复一个ACK给服务器端。其中ACK的值等于FIN+SEQ
应用层(HTTP协议)
超文本传输协议( HTTP,Hyper Text Transfer Protocol)
HTTP请求的7种方法
序号 | 方法 | 描述 |
---|---|---|
1 | GET | 【幂等】发送请求来获得服务器上的资源,请求体中不会包含请求数据,请求数据放在协议头中 |
2 | POST | 【非幂等】向服务器提交资源让服务器处理。提交的资源放在请求体中 |
3 | HEAD | 本质和GET一样,但是响应中没有呈现数据,而是http的头信息 |
4 | PUT | 【幂等】上传资源到指定位置。提交的资源放在请求体中 |
5 | DELETE | 【幂等】请求服务器删除某资源 |
6 | TRACE | 回显服务器收到的请求 |
7 | OPTIONS | 获取http服务器支持的http请求方法 |
HTTP响应的5种状态
序号 | 方法 | 描述 |
---|---|---|
1 | GET | 【幂等】发送请求来获得服务器上的资源,请求体中不会包含请求数据,请求数据放在协议头中 |
2 | POST | 【非幂等】向服务器提交资源让服务器处理。提交的资源放在请求体中 |
3 | HEAD | 本质和GET一样,但是响应中没有呈现数据,而是http的头信息 |
4 | PUT | 【幂等】上传资源到指定位置。提交的资源放在请求体中 |
5 | DELETE | 【幂等】请求服务器删除某资源 |
6 | TRACE | 回显服务器收到的请求 |
7 | OPTIONS | 获取http服务器支持的http请求方法 |
HTTP协议发展
- HTTP1.0
每次请求都要建立新的TCP连接,都要进行三次握手。
- HTTP1.1
并行使用多个TCP协议。
- HTTP2
引入流的概念,可以使浏览器复用TCP连接。
- HTTP3
使用QUIC协议,QUIC协议的传输由TCP换成UDP。UDP传输只保证数据有序发送,不关心客户端是否完全接收,需要QUIC协议来进行数据完整性校验,如果不完整则会要求重发。
非阻塞网络I/O
计算机之间如何进行网络通信
通过Socket进行连接
Socket工作原理
- 通过网络协议传输数据包达到网卡,网卡通过解析数据包的信息,确认是否接收(判断条件:mac地址是不是本机地址)
- 确认接收后,对数据包进行一层一层解包,将TCP数据部分拷贝到Socket的接收缓冲区
- 直到拷贝完成后,才会唤醒等待线程A
阻塞I/O
服务端与客户端
- 服务端监听指定端口,等待客户端连接(线程阻塞)
- 与客户端建立连接后,获取Socket接收缓冲区的内容(线程阻塞)
- 往Socket发送缓冲区写内容(线程阻塞)
多线程服务器与客户端
代码
- 服务端
package cn.hgy.week8;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author guoyu.huang
* @since 2020-08-16
*/
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
System.out.println("阻塞点:accept");
final Socket socket = serverSocket.accept();
new Thread(() -> {
try {
System.out.println("阻塞点:接收缓冲池");
InputStream inputStream = socket.getInputStream();
byte[] content = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = inputStream.read(content)) != -1) {
sb.append(new String(content, 0, len));
}
System.out.println(sb.toString());
Thread.sleep(5000L);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("接收成功".getBytes());
System.out.println("阻塞点:发送缓冲池");
outputStream.flush();
socket.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
- 客户端
package cn.hgy.week8;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
/**
* @author guoyu.huang
* @since 2020-08-16
*/
public class Client {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("127.0.0.1", 8080);
// 阻塞5秒输出
Thread.sleep(5000L);
socket.getOutputStream().write("client".getBytes());
socket.shutdownOutput();
// 输入
InputStream inputStream = socket.getInputStream();
byte[] content = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = inputStream.read(content)) != -1) {
sb.append(new String(content, 0, len));
}
System.out.println(sb.toString());
}
}
线程池服务器
总结
- 获取接收缓冲区数据时,如果没有数据则会发生阻塞
- 往发送缓冲区写数据时,如果缓冲区已经满了则会发生阻塞
非阻塞I/O
非阻塞I/O是指IO操作立即返回,发起线程不会阻塞等待。
- 非阻塞IO读的操作
- Socket接收缓冲区有数据,读N个数据(不保证数据被读完整,因此可能需要多次读取)
- Socket接收缓冲区没有数据,直接返回失败(不会等待)
- 非阻塞IO写的操作
- Socket发送缓冲区满,直接返回失败(不会等待)
- Socket发送缓冲区不满,写N个数据(不保证数据一次性被全部写入,因此可能需要多次写)
JAVA NIO (NEW I/O)
关键类信息
- Selector:不断的轮询注册在其上的Channel,如果某个Channel上面发送读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
- Buffer:在NIO中,所有的数据都是用缓冲区处理的,读取数据时,它是从通道(Channel)直接读到缓冲区中,在写入数据时,也是从缓冲区写入到通道。
- Channel:网络数据通过Channel读取和写入。通道和流的不同之处在于通道是双向的(通道可以用于读、写后者二者同时进行),流只是在一个方向上移动。
- SelectionKey:四种操作类型,可以快速获取channel,selector
工作原理
代码实例
- 服务端
package cn.hgy.week8;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* @author guoyu.huang
* @since 2020-08-16
*/
public class NewServer {
public static void main(String[] args) throws IOException, InterruptedException {
InetSocketAddress address = new InetSocketAddress(8080);
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(address);
server.configureBlocking(false);
// 监听ACCEPT
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
System.out.println("----- accept -----");
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 监听读
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
System.out.println("----- read -----");
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
socketChannel.register(selector, SelectionKey.OP_WRITE);
} else if (selectionKey.isWritable()) {
System.out.println("----- write -----");
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.wrap("接收成功".getBytes());
socketChannel.write(byteBuffer);
selectionKey.cancel();
}
iterator.remove();
}
}
}
}
- 客户端
package cn.hgy.week8;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* 客户端发起连接
*
* @author guoyu.huang
* @since 2020-08-16
*/
public class NewClient {
public static void main(String[] args) throws IOException, InterruptedException {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
Selector selector = Selector.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
channel.register(selector, SelectionKey.OP_CONNECT);
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println("----- connect -----");
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
if (channel.isConnectionPending()) {
channel.finishConnect();
}
// 监听写
socketChannel.register(selector, SelectionKey.OP_WRITE);
} else if (selectionKey.isReadable()) {
System.out.println("----- read -----");
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
socketChannel.register(selector, SelectionKey.OP_WRITE);
} else if (selectionKey.isWritable()) {
System.out.println("----- write -----");
Thread.sleep(8000);
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.wrap("new client".getBytes());
socketChannel.write(byteBuffer);
// 监听读
socketChannel.register(selector, SelectionKey.OP_READ);
}
iterator.remove();
}
}
}
}
扩展 - 操作系统
在NIO中selector.select()函数会调用操作系统函数,该函数会发生阻塞。不同操作系统实现方式也不同,有:select,poll,epoll。
I/O复用方式
select/poll 的read
所有Socket关联同一个线程A,当Socket触发事件时,唤醒线程A,线程A轮询所有的Socket,判断哪些Socket是有事件要触发。
epoll
eventpoll会关联线程A,所有Socket如果有事件触发,会添加到eventpoll中,同时唤醒线程A。
数据库架构原理
数据库架构
连接器
语法分析器
语义分析与优化器
PrepareStatement
两种SQL执行方式: