一、简介和应用场景

1.1、简介

  • netty是异步的,基于事件驱动的网络应用框架,用于快速开发高性能、高可用的网络IO程序
  • netty主要针对在TCP/IP协议下,面向clents短的高并发应用,或者是PeerToPeer场景下,大量数据持续持续输出应用。(netty本质是对JDK的IO模型进行重写和优化)
  • Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景

    1.2、应用场景

    分布式系统中的,远程服务调用,高性能RPC服务框架是必不可少的,Netty作为高性能的异步通信框架作为基础组件会被这RPC框架,例如阿里巴巴的dubbo底层使用的RPC,底层使用的就是netty
    游戏服务器使用

二、IO

IO可以简单的理解为用什么的通道进行进行数据的发送和接收,这决定了程序通信的性能
Java支持三种网络编程模型(IO模式):BIO,NIO,AIO

JavaBIO模型:(同步并阻塞/传统阻塞型)JDK1.4之前

JavaBIO(bocking io)它是java的原生IO,服务器实现模式是一个连接一个线程,即客户端有连接请求时,服务器端就需要启动一个线程进行处理,如果连接不做任何事,就会造成不必要的性能开销
image.png (BIO)
每有一个客户端请求,服务端都要开启一个线程,当并发过大时,服务端压力也会增大,而且在进行读写时,如果没有获取到数据,会一直等着,知道读写完成才返回

JavaNIO模型:(同步非阻塞)JDK1.4之后出现

服务器实现模式为一个线程处理多个请求(连接),即客户端发送的请求都会被注册到多路复用器(选择器Selector)上,多路复用器会一直轮询,多路复用器轮询到连接有IO请求就进行处理
image.png

JavaAIO模型:(异步非阻塞NIO.2)JDK1.7出现

这个模型并没有得到广泛的应用,AIOO引入异步通道的概念,采用了Proactor模式,简化了程序的编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端启动线程去处理,一般是用于连接数多且连接时间长的应用

三、IO模型选择

  • BIO方式适用于连接数目比较小且固定的架构,这样方式对服务器资源要求比较高,并发局限于应用中,JDK1.4之前唯一的选择,但程序易理解
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,Netty是基于NIO的对其功能进行增强,可以支持长链接,比如聊天服务器,弹幕系统,服务间通信等,编程比较复杂,JDK1.4开始支持
  • AIO方式使用连接数目多且连接时间比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK1.7开始支持

    四、JavaBIO(Java网络编程)

    JavaBIO(bocking io):同步阻塞IO,他是传统的javaIO编程,其功能类和接口在java.io包下,处理模式是一个连接一个线程,即客户端有连接请求时,服务端就需要开启一个线程去处理,通过线程池可以实现多个客户连接服务器

    4.1、网络编程三要素:

    IP:在网络上用来唯一标识一台计算机
    端口:用来标识计算机上运行的那个程序(应用程序的唯一标识),它的取值是0-65536,其中0-1023被一些知名网络和应用程序使用,
    协议:通信的规则

    4.2、UDP协议

    UDP协议:他是面向无连接的协议,即在数据传输时发送端不会和接收端建立连接,发送端也不会确认接收端是否存在,就会发出数据,接收端接收到数据也不会向发送端响应反馈,优点:消耗资源少,通信效率高,通常用于视频音频普通数据的传输,使用UDP协议由于是面向无连接的,传输过程中偶尔会丢一些数据包,所以UDP协议适用于丢一些数据也不会对结果有较大影响的场景(即适用于要求数据传入快,偶尔掉包无所谓的场景)例如直播,视频会议等

    4.3、TCP协议

    它是面向有连接的协议,它提供了两台计算机可靠无差别的数据传输,TPC协议中要明确客户端和服务器端,客户端向服务器端发送请求,每次连接的创建都需要3次握手,断开连接需要四次挥手
    三次握手:

  • 第一次:客户端向服务器端发送请求申请连接

  • 第二次:服务器端接收到请求,向客户端给予响应,接受连接
  • 第三次:客户端向服务器端再次发送确认信息,确保信息无误(第三次连接防止客户端已经挂了)

    4.4、UDP通信实现

    原理:UDP协议是一种不可靠的通信协议,他在通信的两端各建立一个Socket,但是这两个Socket只是发送和接收的对象,没有客户端和服务器的概念,Java通过DatagramSocket类来实现UDP协议的Socket
    发送端: ```java package com.study.udp;

import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress;

//使用UDP协议发送数据报包 public class SendUDP { public static void main(String[] args) throws Exception { //创建DatagramSocket发送UDP协议的数据报包 DatagramSocket ds = new DatagramSocket(); byte[] bytes = “testUDP”.getBytes(); InetAddress inetAddress = InetAddress.getByName(“10.10.4.160”); //创建数据报包 DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, inetAddress, 10086); //调用send发送数据 ds.send(datagramPacket);

  1. //关闭发送端
  2. ds.close();
  3. }

}

**接收端:**
```java
package com.study.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

//接收UDP数据
public class ReceiveUDP {
    public static void main(String[] args) throws Exception {
        //创建DatagramSocket发送UDP协议的数据报包
        DatagramSocket ds = new DatagramSocket(10086);
        byte[] bytes = new byte[10000];

        //创建数据报包
        DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
        ds.receive(datagramPacket);
        System.out.println(new String(datagramPacket.getData()));
        //关闭接收端
        ds.close();

    }
}

4.5、TCP协议通信实现

TCP是一种可靠的通信协议,它在通信的两端各建立一个Socket对象,从而在通信的两端形成网络虚拟链路,Java对TCP协议进行良好的封装,使用Socket代表两端的通信端口,并通过Socket产生IO流来进行网络通信,Java为客户端提供了Socket类,为服务端提供了ServerSocket类
接收端:

package com.study.tcp;

import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class TcpServer {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(10068, 20, InetAddress.getByName("10.10.4.160"));
        //监听客户端连接,一旦有一个连接就会生成一个socket对象
        Socket socket = serverSocket.accept();
        InputStream inputStream = socket.getInputStream();
        byte[] bytes = new byte[1000];
        inputStream.read(bytes);
        System.out.println(new String(bytes));
        socket.close();
    }
}

发送端:

package com.study.tcp;

import java.io.OutputStream;
import java.net.Socket;

public class TcpClient {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("10.10.4.160", 10068);

        OutputStream outputStream = socket.getOutputStream();
        String str = "testTCP";
        outputStream.write(str.getBytes());

        outputStream.close();
        socket.close();
    }
}

通过原生的TCP客户端服务代码实现可以发现,我们使用java服务端只能监听一个端口,当这个端口请求过来时,才会建立一个连接,这样是能实现一个服务端处理一个请求,这是不现实的,可以通过线程池,来优化,每次请求过来,从线程池中获取一个线程连接去处理,这样就可以实现处理多个请求,这样如果连接过来什么都没做,就会出现资源浪费情况,
在通过ServerSocketS调用accept()方法时它会堵塞,在没有接收到连接时会一直阻塞在哪里等待连接,通过socket获取输入流read的时候也会阻塞,连接过后如果客户端没有输入数据,那么read方法也会阻塞,在哪里等着读取,这是客户端发送数据,read会读到数据,读到以后还是会继续阻塞在哪里
线程池优化:

package com.study.bio;


import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BioServer {
    public static void main(String[] args) throws Exception {
        //创建一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();

        //创建TCP服务器
        ServerSocket serverSocket = new ServerSocket(10086);
        //一直监听客户端连接
        while (true) {
            Socket socket = serverSocket.accept();
            //开启一个线程
            executorService.execute(() -> {
                handler(socket);
            });
        }
    }

    private static void handler(Socket socket) {
        try {
            InputStream inputStream = socket.getInputStream();
            while (true){
                byte[] bytes = new byte[10000];
                inputStream.read(bytes);
                System.out.println(new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

五、JavaNIO(non -blocking IO)

5.1、简介

JavaNIO是指java 提供新的API,从JDK1.4开始Java提供一系列改进输入输出的新特性,被统称为NIO(new IO),是同步非阻塞的,他们放在Java.nio包下,并对原有的java.io包下的类进行改写
JavaNIO有三大核心:Channel(通道),Buffer(缓冲区),Selector(选择器)
NIO是面向缓冲区,或者面向块编程的
image.png

5.2、NIO核心一:块(buffer)

在NIO中Buffer是一个顶层父类,提供了除boolean的其他七种数据类型的Buffer,用来存放对应类型的数据

package com.study.buffer;

import java.nio.IntBuffer;

public class BufferTest {
    public static void main(String[] args) {
        //创建一个buffer,分配一个长度为5的Buffer,即可以存放五个int类型的数据
        IntBuffer intBuffer = IntBuffer.allocate(5);
        //存数据intBuffer.capacity()
        for (int i = 0; i < intBuffer.capacity(); i++) {
            intBuffer.put(0);
        }
        //读数据:记住Buffer读数据需要先进行读写反转
        intBuffer.flip();
        //开始取数据
        while (intBuffer.hasRemaining()){//判断指针下一位是否有数据
            int i = intBuffer.get();//获取数据并将指针移到下一位
            System.out.println(i);
        }
    }
}

image.png

5.3、NIO三大核心原理关系

image.png

  • 每一个channel都会对应一个Buffer
  • Selector对应一个线程,一个Selector对应多个channel,所以一个线程可以对应对个channel(这里的channel就是连接),所以一个线程可以处理多个连接
  • 连线需要注册到Selector程序
  • Selector内部会一直轮询所有注册过来的channel(连接),监听channel的Event(时间),根据不同的事件来切换处理那个channel
  • 数据的读取和写入是通过Buffer,也就是程序获取数据需要通过Buffer,这和BIO有本质的区别,BIO获取和写入数据都是通过流通道,且通道是单向的,那么只能读取,要么只能写入,而NIO的Buffer是双向的,既可以读取,也可以写入,但是需要调用filp()方法,进行读写转换
  • Buffer就是一个内存块,它的底层是一个数组
  • channel是双向的,程序读取数据必须经过Buffer,不能直接从channel读取

总结:Buffer的本质是一个可以读写的内存块,可以理解为一个容器对象(包含数组),该对象提供一组方法,可以轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供了从文件、网络中读取数据的渠道,但是读取数据必须经过Buffer

5.4、Buffer类介绍

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;//标记
private int position = 0;//位置
表示下一个要读取元素的索引,每次读写缓冲区都会改变该值,为下次读写做准备
初始position为0,表示下次要读读取第一个元素,我们可以手动设置这个值来实现读取指定索引的元素
intBuffer.position(2);表示下一次开始读第三个元素
private int limit;//当前缓冲区的终点
他规定了缓冲区的读写范围(读取元素个数限制),不能对超过终点范围的缓冲区进行读写,这个极限值可以修改
intBuffer.limit(4);表示只能读到第三个元素,第四个元素不会被读取到
private int capacity;//缓冲区容量
表示缓冲区大小,在创建缓冲区时确定,在存数据时不能超过缓冲区大小(因为底层是数组存储数据)
Buffer的常用方法
image.png
ByteBuffer的常用方法
image.png

5.5、NIO核心二:通道(channel)

BIO中的流是单向,例如我们创建一个FileInputStream流,他就是输入流,只能用来读取数据,而NIO中的channel是双向的既可以用来读取也可以用来写数据。
Channel在NIO中是一个接口,常用的实现类有FileChannel,DatagarmChannel,ServerSocketChannel,SocketChannel(注意:ServerSocketChannel类似于BIO中的ServerScoket,SocketChannel类似于BIO的Socket)
FileChannel:用于文件的数据读写
DatagarmChannel:用于UDP的数据读写
ServerSocketChannel,SocketChannel:用于TCP的数据读写,他们是抽象类,他们的实现是(ServerSocketChannelImpl,SocketChannelImpl)
FileChannel的使用
image.png
下面两个复制操作效率很高,底层使用了零拷贝

5.6、FileChannel的使用

写文件到磁盘

package com.study.channel;

import java.io.FileOutputStream;
import java.nio.ByteBuffer;
//使用FileChannel来实现写文件
public class FileChannel {
    public static void main(String[] args) throws Exception{
        //原生的IO对象被增强,他内部内置了channel,IO对象中包含了channel
        String test="111212";
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\a.txt");
        java.nio.channels.FileChannel channel = fileOutputStream.getChannel();

        //创建一个缓存区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //将需要写出的数据放入缓冲区
        byteBuffer.put(test.getBytes());
        //将缓冲区数据写入通道,需要进行读写转换
        byteBuffer.flip();
        //通道的读写需要站在通道的角度,往通道里写数据使用write,读通道的数据使用read
        channel.write(byteBuffer);
        //关闭资源
        fileOutputStream.close();
    }
}

读取文件到内存:

package com.study.channel;


import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
//通过channel来读取文件
public class ReadFileChannel {
    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("d:\\a.txt");
        FileChannel channel = fileInputStream.getChannel();

        //创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //往缓冲区写数据,站在channel角度就是从channel中读数据到缓冲区
        channel.read(byteBuffer);
        byte[] array = byteBuffer.array();
        String string = new String(array);
        fileInputStream.close();
        System.out.println(string);

    }
}

总结:使用NIO进行文件的读写也是需要使用IO,我们通过原生的IO来获取通道,然后创建缓冲区,将缓冲区的数据写入或者读取到channel中,实现文件传输,原因是IO流中包含了channel,所以我们可以通过流对象获取channel,通过我们将数据写入到channel,流对象也就能获取数据
文件复制:使用同一个Buffer进行循环读写,每次循环需要重置buffer

package com.study.channel;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

//通过Buffer进行文件拷贝
public class CopyFileByChannel {
    public static void main(String[] args) throws Exception {

        //读取一个文件到缓冲区
        FileInputStream fileInputStream = new FileInputStream("d:\\a.txt");
        FileChannel inputChannel = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("d:\\b.txt");
        FileChannel outputChannel = fileOutputStream.getChannel();

        //创建一个Buffer, #1.此时position为0,limit和capacity都是10
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        //循环读写
        while (true){
            //#5.进行下一次操作时,由于上次使用flip操作和write,所以limit和position,导致读不到数据,所以要调用重置方法
            //从通道读数据到缓冲区,返回-1表示已经读完
            //重置ByteBuffer(以为读写复用了同一个buffer,所以需要重置,否者无法读取数据)
            byteBuffer.clear();
            //#2.使用read的时候,会一直读取,使得position改变成读取数据的长度(有可能等于limit)
            int num = inputChannel.read(byteBuffer);
            if(num==-1){
                break;
            }
            //读写转换#3.此操作会使limit等于position,使position等于0,因为我们读了多少数据,就写多少数据,limit就是标致读了多少数据
            byteBuffer.flip();
            //将缓冲区数据写入通道#4.使得position等于limit
            outputChannel.write(byteBuffer);
        }

    }
}

对上面进行优化:使用transferForm完成拷贝tagChannel.tarnsferForm(sourceChannel ,size)

package com.study.channel;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
//优化文件拷贝
public class CopyFileByChannelBetter {
    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("d:\\a.txt");
        FileChannel inputChannel = fileInputStream.getChannel();
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\b.txt");
        FileChannel outputChannel = fileOutputStream.getChannel();

        //将源通道数据拷贝到目标通道
        outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
        fileInputStream.close();
        outputChannel.close();
    }
}

5.7、Buffer和channel的注意事项

  • Buffer支持类型化put和get,也就是我们放入什么类型,去的时候也是用什么类型去取否者可能会报错,例如:putInt/getInt
  • 可以将普通的可读可写的Buffer变成只读Buffer

ByteBuffer onlyReadBuffer = byteBuffer.asReadOnlyBuffer();调用这个方法返回的就是一个只读的Buffer,如果这时在往里写会包ReadOnlyException异常

5.8、MappedByteBuffer

MappedByteBuffer可以让文件直接在内存中(堆外内存)进行修改,即操作系统不需要在拷贝一次,而如何同步文件由NIO完成、

5.9、channel分散和聚合 Buffer

如果数据比较多,我们可以使用多个Buffer数组完成读写操作,即Scattering和Gattering
Scattering(分散):将数据写入到Buffer时,可以写到Buffer数组中,即第一个满了,写第二个以此类推
Gattering(聚合):从Buffer中读数据时,可以读到一个Buffer数组中
channel提供了read()和write数组的方法,用来实现分散和聚合操作

5.10、NIO核心三:选择器(selector)

image.png
Selector是一个抽象类,常用方法:
public static Selector open();//创建一个选择器对象
public int select(Long timeout);//监控所有注册到这个选择器的通道,当通道中有IO操作进行时,将对应的SelectionKey加入到内部集合并返回,参数用来设置超时时间,这个方法体现了NIO的非阻塞,传入参数是等待时间,比如传入2000,即2000毫秒 后没有监听到时间就返回
select()是一个重载的方法,没有参数的select()是一个阻塞的方法,它会一直阻塞直到监听到注册过来的通道有事件发生才返回
selectNow();这个方法也是非阻塞的,他是如果通道没有事件发生,他就立即返回
public Set selectedKeys();//从内部集合获取所有的SelectionKey
selector.wakeup();唤醒selector,当selector在阻塞时,可以调用这个方法唤醒selector
注意:选择器监听通道,当通道有读写时间发生,就可以获取到对应的SelectionKey,通过select方法可以将得到额SelectionKey放入选择器的内部集合中,通过这个SelectionKey可以获得对应的channel

5.11、Selector/SelectionKey/SocketChannel/ServerSocketChannel之间的关系

image.png

  • 当有客户端连接时,ServerSocketChannel会生成一个SocketChannel,把它注册到Selector中(通过SocketChannel的register方法注册),一个Selector可以注册多个SocketChannel,注册成功后会返回一个SelectionKey,放入Selector的Set集合中(即和Selector关联)
  • Selector调用Select()方法对通道进行监听,这个方法的返回值是int,返回有事件发生的通道个数,进而得到有事件发生的SelectorKey,通过SelectKey中的channel方法,获取注册的channel

补:

5.12、SelectionKey介绍

image.png