一.网络模型及协议
OSI七层网络模型分为7层,应用层、表示层、会话层、传输层、网络层、链路层、物理层。网络数据会从应用层开始,往下通过各层,再经过物理层的传输后,再往上通过各层到达目标应用层。各层有不同的作用,如图所示。
各层对数据的格式有一些通用性的规范,也就是我们常说的协议。比如应用层的HTTP协议、FTP协议等,传输层的TCP协议、UDP协议等,网络层的IP协议等。
二.TCP和UDP协议
2.1 区别
- TCP是面向连接的、可靠的流协议。TCP通信的两台主机需要先建立连接才开始传输数据;传输过程采用“带重传的确认”机制保证传输的可靠性;采用“滑动窗口”的方式进行流量控制。
- UDP(用户数据报协议)是面向无连接的、不可靠的协议。发送数据时时直接发送,不管对方是否在接收,也不需要对方进行确认,所以不可靠,可能会有丢包现象。
- TCP适用于要求可靠传输场景,例如文件传输;UDP适用于实时场景,例如视频会议、直播等。
2.2 TCP三次握手
2.2.1 详细介绍
- 客户端给服务端发送请求建立连接的数据包,其中SYN(表示请求建立连接)=1,还有序列号seq=J(客户端假定的序号);发送后客户端处于SYB_SENT状态
- 服务端收到数据报后,发送应答客户端和请求建立连接的数据报,其中应答ACK=1,应答的序列号ack=J+1,请求连接连接的SYN=1,序列号seq=,K(服务端假定的序号);发送后服务端处于SYN_RCVD状态
- 客户端收到应答后,发送应答服务端的数据包,其中应答ACK=1,应答的序列号ack=K+1;发送后客户端处于连接建立状态 ESTABLISHED
- 服务端收到数据包后,检查应答序列号是否正确,正确的话变为连接建立状态 ESTABLISHED,3次握手成功,连接建立,可以进行数据传输
2.2.2 为什么要有三次握手
- TCP是面向连接的,所以需要双方都确认连接的建立;
- 第一次握手,客户端请求建立连接
- 第二次握手,服务端应答客户端,并请求建立连接
- 第三次握手,客户端对服务端请求进行确认应答,如果不进行第三次,服务端就不知道它的建立连接请求有没有被客户端接收
2.2.3 漏洞与防范
- 可能的漏洞是SYN洪泛攻击:服务端会维护一个半开连接队列,大小有限;半开连接是在第二次握手后,会向第一次握手来源的ip发送数据包并等待对方的应答;当第一次握手的ip是伪造的假ip并且大量发出请求(第一次握手),半开连接队列会被占满,阻碍了正常的连接。
- 解决方案是无效连接监控释放,对等待了一定时间的服务器连接进行释放;延缓TCB分配;防火墙,验证来源ip的有效性
2.3 TCP四次挥手
2.3.1 详细介绍
- 第一次挥手:客户端发送关闭请求,不再向服务端传输数据
- 第二次挥手:服务端响应客户端的关闭请求,继续向客户端传输数据
- 第三次挥手:服务端发送关闭请求,不再向客户端传输数据
- 第四次挥手:客户端发送关闭请求请求,服务端收到数据报后就变为CLOSED状态,客户端发送后进入TIME-WAIT状态,经过一小段时间后再变为CLOSED状态
2.3.2 为什么要有四次挥手
TCP连接是全双工的,需要双方都停止数据传输,通知到对方,且进行单向的关闭
三.网络IO模型
3.1 同步与异步
同步与异步关注是否主动等待返回结果。
- 同步:调用方主动等待结果返回
- 异步:调用方不主动等待结果,而是通过回调函数、状态通知等方式拿到结果
3.2 阻塞与非阻塞
阻塞与非阻塞关注结果返回前调用方的状态。
- 阻塞:结果返回前,调用方线程挂起,什么都不做
- 非阻塞:结果返回前,调用方线程去做其他事,不挂起
3.3 两者组合
- 同步阻塞,最常用的模型,调用方什么都不做,等待结果返回
- 同步非阻塞,类似于轮询,调用方去做别的事,但偶尔来查看结果是否返回
- 异步阻塞,基本不用的模型,调用方确定了回调机制,但是不去做别的事
- 异步非阻塞,调用方确定了回调机制,然后去做别的事,回调触发时再去处理
3.4 数据传输流程
以部署的不同服务器的两个应用程序通讯为例:
- 应用A把消息发送到TCP发送缓冲区(写缓冲区)
- TCP把缓冲区的数据发送出去,经过网络传输后,发送到应用B的TCP接收缓冲区(读缓冲区)
- 应用B从缓冲区中读取属于自己的数据
3.5 五种网络IO模型
- 阻塞I/O:调用方一直阻塞,等待接收缓冲区的数据
- 非阻塞I/O:调用方不阻塞,经常轮询检查接收缓冲区的数据,直到数据准备就绪
- I/O复用:调用方存在一些专门轮询检查缓冲区数据的线程,当数据准备就绪时,再通知数据处理的线程去处理。
- 信号驱动I/O:调用方存在一些信号处理线程,不去轮询,而是等待数据准备就绪之后给它发信号,再由它去通知数据处理的线程去处理
- 异步I/O:调用方的线程向内核注册关注的读事件,数据准备就绪后直接触发事件的处理
四.BIO实战
4.1 服务端
- 创建ServerSocket,绑定服务端监听端口
- 调用ServerSocket的accept()方法拿到与客户端的socket连接
- 拿到与客户端连接的socket的输入流,读取客户端传过来的数据
- 拿到与客户端连接的socket的输出流,把响应数据写入输出流,并flush把缓冲区的数据刷回客户端。
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
//1.绑定服务端监听端口
serverSocket.bind(new InetSocketAddress(10001));
System.out.println("server start...");
//2.accept() 拿到与客户端的socket连接,包装成一个任务进行处理
while (true){
new Thread(new ServerTask(serverSocket.accept())).start();
}
}
private static class ServerTask implements Runnable{
public ServerTask(Socket socket) {
this.socket = socket;
}
private Socket socket = null;
@Override
public void run() {
try(ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())){
//3.从输入流中读取客户端数据
String input = inputStream.readUTF();
System.out.println("accept message:" + input);
//4.把响应数据写入输出流,并flush把缓冲池数据刷到客户端
outputStream.writeUTF("hello, " + input);
outputStream.flush();
}catch (Exception e){
e.printStackTrace();
}finally {
try {
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
}
4.2 客户端
- 创建Socket,并连接服务端
- 向服务端发送数据,并flush把缓冲区的数据刷到服务端
- 阻塞等待服务端的数据响应
public class BIOClient {
public static void main(String[] args) {
Socket socket = null;
ObjectOutputStream output = null;
ObjectInputStream input = null;
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1" ,10001);
try {
socket = new Socket();
//1.连接服务器
socket.connect(inetSocketAddress);
output = new ObjectOutputStream(socket.getOutputStream());
input = new ObjectInputStream(socket.getInputStream());
//2.向服务器输出数据
output.writeUTF("lm");
output.flush();
//3.接收服务器的响应
String s = input.readUTF();
System.out.println(s);
}catch (Exception e){
e.printStackTrace();
}finally {
try {
if (socket != null){ socket.close();}
if (input != null){ input.close();}
if (output != null){ output.close();}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
五.NIO实战
5.1 与BIO的区别
- 面向流和面向缓冲区,BIO是面向流的,NIO是面向缓冲区的,增加了处理过程的灵活性
- 阻塞与非阻塞,BIO是阻塞的,NIO是非阻塞的,从某通道读取数据,没数据时不阻塞,可以去做其他事;
- 选择器,NIO的选择器 允许一个线程监控多个输入通道
5.2 额外的概念
5.2.1 Selector,选择器
又叫事件订阅器;应用程序向Selector对象注册需要它关注的Channel,以及每个Channel对哪些IO事件感兴趣
5.2.2 Channels,通道
应用程序和操作系统交互事件、传递内容的渠道,可双向同时读写
5.2.3 buffer,缓冲区
通道中的数据总是要先读到一个Buffer,或者从一个Buffer中写入;buffer支持读写模式的切换
5.2.4 SelectionKey,事件类型
channel可以向Selector注册自己感兴趣的操作类型
- OP_ACCEPT,接受连接,仅服务端可用;当接收到一个客户端连接请求时就绪
- OP_CONNECT,请求连接,仅客户端可用;当SocketChannel.connect()请求连接成功后就绪
- OP_READ,读请求,一般都需要注册;当操作系统读缓冲区有数据可读时就绪
- OP_WRITE,写请求,一般没必要注册;当操作系统写缓冲区有空闲空间时就绪;一般情况下写缓冲区都有空闲空间,小块数据直接写入即可,没必要注册该类型,否则该条件会不断就绪浪费CPU;当如果是写密集型的任务,比如文件下载,缓冲区很可能满,注册该类型就很有必要,要注意写完后取消注册
5.3 服务端
- 创建选择器和ServerSocketChannel
- ServerSocketChannel绑定端口,并注册到选择器中,关注接受连接事件 OP_ACCEPT
- 拿到选择器中的事件(selector.select()),遍历执行
- 如果有接受连接事件,说明有客户端申请建立连接,用ServerSocketChannel的accept方法拿到与客户端对应的SocketChannel,并注册到选择器,关注读事件 OP_READ
- 如果有读事件,数据途经SocketChannel写入到Buffer,flip切换buffer为读模式,把数据读到内存处理后再放入写缓冲区buffer,发送给SocketChannel
public class NioServerHandle implements Runnable{
private Selector selector;
private ServerSocketChannel serverChannel;
private volatile boolean started;
/**
* 服务端构造方法
* @param port 指定要监听的端口号
*/
public NioServerHandle(int port) {
try{
//创建选择器
selector = Selector.open();
//打开监听通道
serverChannel = ServerSocketChannel.open();
//设置为非阻塞模式
serverChannel.configureBlocking(false);
//绑定端口
serverChannel.socket().bind(new InetSocketAddress(port));
//只关注请求连接事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
//标记服务器已开启
started = true;
System.out.println("服务器已启动,端口号:" + port);
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
}
public void stop(){
started = false;
}
@Override
public void run() {
//循环遍历selector
while(started){
try{
//阻塞,只有当至少一个注册的事件发生的时候才会继续.
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
}catch(Throwable t){
t.printStackTrace();
}
}
if(selector != null)
try{
selector.close();
}catch (Exception e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
//处理新接入的请求连接事件
if(key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
System.out.println("=======建立连接===");
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ);
}
//处理读事件
if(key.isReadable()){
System.out.println("======socket channel 数据准备完成," +
"可以去读==读取=======");
SocketChannel sc = (SocketChannel) key.channel();
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//数据写入缓冲区
int readBytes = sc.read(buffer);
if(readBytes>0){
//切换为读模式
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String message = new String(bytes,"UTF-8");
System.out.println("服务器收到消息:" + message);
//处理数据
String result = response(message) ;
//发送应答消息
doWrite(sc,result);
} else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
//发送应答消息
private void doWrite(SocketChannel channel,String response)
throws IOException {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
//切换为写模式
writeBuffer.flip();
channel.write(writeBuffer);
}
//对请求数据进行响应
private String response(String message){
return "hello, " + message;
}
}
5.4 客户端
- 创建选择器和SocketChannel
- SocketChannel异步连接服务器ip端口,注册到选择器,关注连接事件 OP_CONNECT
- 拿到选择器中的事件(selector.select()),遍历执行
- 如果有连接事件,说明连接建立成功,重新注册到选择器,关注 读事件 OP_READ
- 如果有读事件,数据途经SocketChannel写入到Buffer,flip切换buffer为读模式,把数据读到内存处理后再放入写缓冲区buffer,发送给SocketChannel
public class NioClientHandle implements Runnable{
private String host;
private int port;
private volatile boolean started;
private Selector selector;
private SocketChannel socketChannel;
/**
* 客户端构造方法
* @param ip 要连接的服务端的ip
* @param port 要连接的服务端的端口
*/
public NioClientHandle(String ip, int port) {
this.host = ip;
this.port = port;
try {
//创建选择器
this.selector = Selector.open();
//打开监听通道
socketChannel = SocketChannel.open();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
started = true;
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
public void stop(){
started = false;
}
@Override
public void run() {
//连接服务器
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
//循环遍历selector
while(started){
try {
//阻塞方法,等待有关注的事件发生
selector.select();
//获取当前有哪些事件可以处理
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
//删除处理过的事件
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if(key!=null){
key.cancel();
if(key.channel()!=null){
key.channel().close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
if(selector!=null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//具体的事件处理
private void handleInput(SelectionKey key) throws IOException {
if(key.isValid()){
//获得关心当前事件的channel
SocketChannel sc =(SocketChannel)key.channel();
//处理连接事件
if(key.isConnectable()){
//连接成功的情况
if(sc.finishConnect()){
System.out.println("=======与服务端建立连接===");
socketChannel.register(selector,SelectionKey.OP_READ);
}else System.exit(-1);
}
//处理读事件
if(key.isReadable()){
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//数据写入缓冲区
int readBytes = sc.read(buffer);
if(readBytes>0){
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String result = new String(bytes,"UTF-8");
System.out.println("客户端收到消息:"+result);
} else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
///连接服务器
private void doConnect() throws IOException {
//连接不一定是立刻就能成功的
if(socketChannel.connect(new InetSocketAddress(host,port))){
//立刻成功了,就直接关注读事件
System.out.println("=======与服务端建立连接===");
socketChannel.register(selector,SelectionKey.OP_READ);
} else{
//暂时还没成功,先关注连接事件
socketChannel.register(selector,SelectionKey.OP_CONNECT);
}
}
//向服务端发送数据
public void sendMsg(String msg) throws IOException {
doWrite(socketChannel,msg);
}
private void doWrite(SocketChannel sc,String request) throws IOException {
byte[] bytes = request.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
sc.write(writeBuffer);
}
}