《Java后端体系系列》之网络通信框架Netty学习(一)

学习网络通信框架Netty总结

1、BIO

1.1、概念

BIO是同步阻塞模型,服务实现模式为一个连接一个线程。即客户端有连接请求时服务器就会启动一个线程进行处理。即使这个连接不做任何事,也会保持连接状态。这样的情况会造成不必要的线程开销。

1.2、场景

BIO方式适用于连接数较少且固定的架构,这种方式对服务器资源要求较高,并发局限于应用中,JDK1.4之前唯一的选择。

1.3、实现流程

工作原理图:
《Java后端体系系列》之网络通信框架Netty学习(一)_第1张图片

1)服务端启动一个ServerSocket
2)客户端启动启动Socket对服务器进行通信,默认情况下服务端需要对每个客户建立一个线程与之通信
3)客户端发出请求之后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
4)如果有响应,客户端线程会等待请求结束后,在继续执行

1.4、代码实现

public class BIOServer {
    public static void main(String[] args) throws Exception {

        //创建一个线程池,每连接到一个客户端,就启动一个线程和客户端进行通信
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

        @SuppressWarnings("resource")
        ServerSocket server=new ServerSocket(6666);
        System.out.println("tomcat服务器启动...");
        while(true){
            //阻塞, 等待客户端连接
            final Socket socket = server.accept();
            System.out.println("连接到一个客户端!");
            newCachedThreadPool.execute(new Runnable() {

                @Override
                public void run() {
                    //业务处理
                    handler(socket);
                }
            });
        }

    }

    /**
     * 处理
     * @param socket
     */
    public static void handler(Socket socket){
        try {
            byte[] bytes = new byte[1024];
            InputStream inputStream = socket.getInputStream();

            while(true){
                //读客户端数据 阻塞
                int read = inputStream.read(bytes);
                if(read != -1){
                    System.out.println(new String(bytes, 0, read));
                }else{
                    break;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            try {
                System.out.println("关闭和client的连接..");
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

1.5、总结

  1. 客户端的每个请求都需要服务端创建一个对应的线程,与客户端进行数据Read或Write.
  2. 当并发较大时,需要创建大量的线程来处理连接,系统资源占用较大。
  3. 连接建立之后,如果没有数据可读,那么线程就会阻塞在Read操作上,造成资源浪费。

2、NIO

2.1、概念

1)NIO是同步非阻塞,服务器实现模式为一个线程处理多个请求(连接)。即客户端发送的连接请求都会注册到多路复用器上,在多路复用器中轮询到连接有I/O请求就进行处理。

2)NIO是面向缓冲区或者面向块编程的。数据读取到一个缓冲区中,需要时在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用缓冲区可以提供非阻塞式的高伸缩性网络。

3)NIO是非阻塞的,一个线程从某个通道发送请求或者读取数据,但是它仅能得到目前可用的数据,没有目前没有数据,那么就什么都不会获取,而不是保持线程阻塞。所以直到数据可以读取之前,线程依然可以做其它事情。非阻塞写也是这种模式,一个线程请求写入一些数据到通道中,不需要等待完全写入,这个线程同时可以去做其它事情。

4)加入有1000个请求,NIO处理这1000个请求时,NIO会根据实际情况分配50或者100个线程来处理。而不会像BIO那样分配1000个。

5)HTTP2.0使用了多路复用技术,做到了同一个连接并发处理多个请求。

2.2、场景

NIO适用于连接数目多且连接比较短的架构,比如聊天服务器,弹幕系统、服务器间通讯等。JDK1.4之后开始支持。

2.3、三大核心

《Java后端体系系列》之网络通信框架Netty学习(一)_第2张图片

对上图进行分析:
1)每个Channel都会对应一个Buffer
2)Selector对应一个线程,一个线程对应多个Channel
3)该图中反应了有三个Channel注册到Selector
4)程序切换到哪个Channel是由Event(事件)决定的,Event是一个重要的概念
5)Selector会根据不同的事件,在各个Channel上切换
6)Buffer就是一个内存块,底层是一个数组。
7)数据的读取和写入都是通过Buffer,NIO中的Buffer是可以读也可以写的,通过flip方法进行切换。
8)Channel是双向的,可以返回操作系统的情况,底层的操作系统通道就是双向的。

2.3.1、Buffer(缓冲区)

1)缓冲区本质上是一个可读可写的内存块,可以理解为一个容器对象(底层数组)。缓冲区内置了一些机制,可以根据和记录缓冲区的状态变化。Channel提供了从文件、网络读取数据的渠道,但是读取或写入数据必须经过Buffer。
2)在NIO中Buffer是一个顶层父类,它是一个抽象类,它下面有很多子类,例如:ByteBuffer、ShortBuffer、CharBuffer、IntBuffer、LongBuffer、DoubleBuffer、FloatBuffer。
3)Buffer类中定义了所有的缓冲区都具有的四个属性:

  1. Capacity:容量,即可以容纳的最大数据量,在缓冲区创建时设定的并且后续不能被改变。
  2. Limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,极限时可以修改的。
  3. Position:位置,下一个要被读或者写的元素的索引,每次读写缓冲区时都会改变该值。
  4. Mark:标记
    4)Buffer类的相关方法:
2.3.2、Channel(通道)
  • 1)通道类似于流,但还是有一些区别如下:
    • 通道可以同时进行读写,而流只能读或者只能写
    • 通道可以实现异步读写数据
    • 通道可以从缓冲区中读数据,也可以写数据到缓冲区
  • 2)BIO中的流是单向的,例如FileOutputStream对象只能读取数据。而NIO中的通道是双向的,可以读操作也可以写操作。
  • 3)Channel在NIO中是一个接口
  • 4)常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel等
    写入数据代码实现:
public class NIOFileOper01 {

    public static void main(String[] args) throws Exception {

        String str="hello,write~";
        FileOutputStream fos=new FileOutputStream("d:\\file01.txt");
        //使用FileChannel 通道
        //fos.getChannle 返回的是 FileChanel 的实现子类 FileChannelImpl
        //可以追下源代码
        FileChannel fc=fos.getChannel();
        ByteBuffer buffer= ByteBuffer.allocate(1024);
  /*
   *NIO 中的通道是从输出流对象里通过 getChannel 方法获取到的,该通道是双向的,既可
以读,又可以写。在往通道里写数据之前,必须通过 put 方法把数据存到 ByteBuffer 中,然
后通过通道的 write 方法写数据。在 write 之前,需要调用 flip 方法翻转缓冲区,把内部重置
到初始位置,这样在接下来写数据时才能把所有数据写到通道里  
   */
        buffer.put(str.getBytes());
        buffer.flip();
        fc.write(buffer);
        fos.close();
    }

}
2.3.3、Selector(选择器)

介绍:
1)NIO中处理多个客户端连接就使用Selector
2)Selector能够检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector),如果事件发生,则获取事件然后根据事件进行相应的处理。这样就可以通过一个线程来管理多个通道,也就是管理多个连接和请求。
3)只有在连接/通道真正有事件发生时,才会进行读写,这样就减少了系统开销,并且不必为每个连接都建立一个线程,不用维护多个线程。
4)避免了多线程之间的上下文切换导致的开销。
Selector类介绍:
Selector类是一个抽象类,常用方法有:

public abstract class Selector implements Closeable {      
//得到一个选择器对象
public static Selector open();
//监控所有注册的通道,当其中有 IO 操作可以进行时,将
对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
public int select(long timeout);
//从内部集合中得到所有的 SelectionKey  
public Set<SelectionKey> selectedKeys();
}

对上图进行分析:
1)当客户端连接时,会通过ServerSocketChannel得到SocketChannel
2)Selector进行监听select方法,返回有事件发生的通道个数
3)将SocketChannel注册到Selector上,register(Selector,int ops);一个Selector可以注册多个SocketChannel
4)注册后返回一个SelectionKey,会和该Selector关联
5)如果有事件发生时,那么就会获取各个SocketChannel上的SelectionKey
6)通过SelectionKey反向获取SocketChannel
7)获取到SocketChannel后进行业务处理

2.4、NIO和BIO比较

1)BIO是以流的方式处理数据,而NIO以块的方式处理数据,NIO的效率比BIO效率高很多。
2)BIO是阻塞的,NIO是非阻塞。
3)BIO是基于字节流和字符流进行操作,而NIO是基于Channel和Buffer进行操作。数据总是从Channel读取到Buffer中,或者从Buffer写入到Channel中。Selector用于监听多个Channel通道的事件,因此单个线程就可以监听多个多个客户端Channel。

2.5、Buffer和Channel的注意事项

1)ByteBuffer支持类型化的put和get,也就是put放入的是什么类型,get就应该使用相应的数据类型来取出,否则可能报BufferUnderflowException异常。
2)可以将一个普通Buffer转成只读Buffer
3)NIO还提供MappedByteBuffer,可以让文件直接在内存中进行修改。
4)NIO还支持通过多个Buffer完成读写操作。

3、AIO

3.1、概念

AIO是异步非阻塞,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务器端启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

3.2、场景

AIO方式适用于连接数目较多且连接比较长的架构,比如相册服务器,充分调用OS参与并发操作,JDK7之后开始支持。

欢迎关注公众号:【陈汤姆】
哈哈哈哈哈

你可能感兴趣的:(《Java后端知识体系》系列,java,netty,多线程,nio)