BIO和NIO

BIO和NIO

  • BIO:同步阻塞式IO,服务器使用一个线程处理一个请求,当客服端有请求到达时就启动一个线程进行处理,直到请求结束才会释放这个线程,如果这个请求什么也不做就会一直占用该线程,当有很多这样的请求就会消耗大量的资源。
  • NIO:同步非阻塞式IO,多个请求注册到多路复用器(Selector)上,共用一个线程进行处理,多路复用器会轮询每个请求,当有事件发生时才会通知主线程进行处理。
    在这里插入图片描述

Socket与SocketChannel

Socket

  • Socket与ServerSocket是JDK提供的两个用于实现TCP程序的类
  • ServerSocket表示服务器端,socket表示的是客户端。通信时,服务器端要创建ServerSocket对象,对本机的指定端口进行监听;客户端创建Socket向指定IP地址的端口号发起连接请求,连接后就可以进行通信。

代码示例

public static void main(String[] args) throws IOException, InterruptedException {
        final ServerSocket serverSocket=new ServerSocket(8080);
        //服务端线程
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        Socket socket=serverSocket.accept();
                        System.out.println("连接成功");
                        System.out.println("开始接收数据...");
                        InputStream inputStream=socket.getInputStream();
                        inputStream.read();
                        System.out.println("数据接收完毕...");
                    }

                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        System.out.println("thread:"+thread.getState());
        thread.start();
        //客户端线程
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Socket socket=new Socket(InetAddress.getLocalHost(),8080);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        System.out.println("thread:"+thread.getState());
        thread1.start();
        System.out.println("thread:"+thread.getState());
    }

输出

thread:NEW
thread:RUNNABLE
thread:RUNNABLE
连接成功
开始接收数据…

分别建立了两个线程用于模拟客户端与服务端,线程创建完成处于NEW状态,调用start()方法以后变成Runnable状态。从输出可以看出服务端线程启动以后,serverSocket.accept方法后面的语句并没有执行,而是在客户端线程启动以后才开始输出;而在执行了inputStream.read方法后,后面的语句也没有执行,这是因为客户端并没有向服务端输入。
显然这是因为Socket是阻塞式的,使用这些方法都会等待对方做出响应以后程序才会继续执行。

I/O阻塞时线程的状态

在这里还有一个问题,为什么线程已经阻塞了,但是使用getState方法查看线程的状态时却仍然是Runnable呢?
这就要区分JVM层面线程的状态和操作系统层面线程的状态了,这两着有些差异,而上面提到的阻塞指的是操作系统层面的线程阻塞。

JVM线程的状态

JVM中线程的状态有六种,分别是:

NEW(初始状态)
RUNNABLE(运行状态)
BLOCKED(阻塞状态)
WATTING(等待状态)
TIMED_WAITING(限时等待状态)
TERMINATED(终止状态)

线程的状态转换关系
在这里插入图片描述

其中BLOCKEDWATTINGTIMED_WAITING都会使线程进入休眠状态,将这三种归为一类,线程的生命周期可以简化为
在这里插入图片描述

操作系统的线程状态

操作系统的状态可分为

初始状态
可运行状态
运行状态
休眠状态
终止状态

在这里插入图片描述

前面说到的阻塞指的操作系统层面上的阻塞,也就是说线程在操作系统上处于休眠状态,可为什么JVM中仍然是Runnable状态呢,因为使用JVM获取到的线程状态实际上指的是线程在JVM中的状态,线程可能在操作系统上被阻塞了,但是在JVM的角度来看这个线程仍然在运行。
JVM线程状态的改变通常只于自身显式引入的机制有关,比如显式调用wait()、sleep()等方法。而操作系统层面的线程阻塞并不会改变线程在JVM中的状态


SocketChannel

  • Socket与ServerSocket是两个通信的端点,Socket的方法都是阻塞的,属于BIO通信;SocketChannel与ServSocketChannel分别是客户端与服务端的通道,SocketChannel提供configureBlocking方法,来描述通道的阻塞状态。我们可以将SocketChannel设置为非阻塞状态
  • 两者关系:虽然每个SocketChannel通道都有一个关联的Socket对象,但并非所有socket都有一个关联的SocketChannel。如果我们使用传统的方式来new Socket,那么其不会有关联的SocketChannel。
  • 阻塞方法与非阻塞的方法对比
  1. 输入操作
    • 进程A调用阻塞socket.read方法时,若该socket的接收缓冲区没有数据可读,则该进程A被阻塞,操作系统将进程A睡眠,直到有数据到达;
    • 进程A调用非阻塞SocketChannel.read方法时,若该SocketChannel的接收缓冲区没有数据可读,则进程A收到一个EWOULDBLOCK错误提示,表示无可读数据,read方法立即返回。
  2. 输出操作
    • 进程A调用阻塞socket.write方法时,若该socket的发送缓冲区没有多余空间,则进程A被阻塞,操作系统将进程A睡眠,直到有空间为止;
    • 进程A调用非阻塞SocketChannel.write方法时,若该SocketChannel的发送缓冲区没有多余空间,则进程A收到一个EWOULDBLOCK错误提示,表示无多余空间,write方法立即返回。
  3. 连接操作
    • 对于阻塞型的socket而言,调用socket.connect方法创建连接时,会有一个三次握手的过程,每次需要等到三次握手完成之后(ESTABLISHED 状态),connect方法才会返回,这意味着其调用进程需要至少阻塞一个RTT时间。
    • 对于非阻塞的SocketChannel而言,调用connect方法创建连接时,当三次握手可以立即建立时(一般发生在客户端和服务端在一个主机上时),connect方法会立即返回;而对于握手需要阻塞RTT时间的,非阻塞的SocketChannel.connect方法也能照常发起连接,同时会立即返回一个EINPROGRESS(在处理中的错误)。

代码示例

public static void main(String[] args) throws IOException, InterruptedException {
        final ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        System.out.println("服务启动...");
        System.out.println("开始监听端口号8080...");
        final Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    SocketChannel socketChannel=serverSocketChannel.accept();
                    System.out.println("连接成功...");
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
        Thread.currentThread().sleep(2000);
        System.out.println("主线程休眠2s等待服务端准备就绪...");
        System.out.println("客户端启动...");
        //客户端
        final SocketChannel socketChannel=SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(),8080));
        System.out.println("客户端发起连接...");
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                String data="Hello, here is a piece of information, please pay attention to check!";
                try {
                    socketChannel.write(ByteBuffer.wrap(data.getBytes()));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread1.start();
    }

输出

服务启动…
开始监听端口号8080…
连接成功…
主线程休眠2s等待服务端准备就绪…
客户端启动…
客户端发起连接…

主线程中启动两个线程进行分别模拟服务端和客户端,将ServerSocketChannel设置为非阻塞状态,通过程序可以看到,执行serverSocketChannel.accept方法非没有使程序发生阻塞,而是直接输出了“连接成功…”。


多路复用器Selector

Selector选择器的概述和作用

SocketChannel经常与多路复用器Selector放在一起使用。
概述: Selector被称为:选择器,也被称为:多路复用器,可以把多个Channel注册到一个Selector选择器上, 那么就可以实现利用一个线程来处理这多个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了, 减少系统负担, 提高效率。因为线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。

作用: 一个Selector可以监听多个Channel发生的事件, 减少系统负担 , 提高程序执行效率 。
在这里插入图片描述

代码示例

public class SocketChannelTest {
    //定义任务的处理类,对SocketChannel做出处理
    private static  class ServerHandler implements Runnable{
        SocketChannel socketChannel;
        ServerHandler(SocketChannel socketChannel){
            System.out.println("开始处理READ事件...");
            this.socketChannel=socketChannel;
        }
        @Override
        public void run() {
            try {
                ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
                int len=socketChannel.read(byteBuffer);
                System.out.println("读到字节数:"+len);
                byteBuffer.flip();
                while(byteBuffer.hasRemaining()) {
                    System.out.print((char)byteBuffer.get());
                }
                byteBuffer.clear();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    public static void main(String[] args) throws IOException, InterruptedException {
        //创建一个线程池用于处理SocketChannel的任务
        final ExecutorService executorService= Executors.newFixedThreadPool(5);
        //服务端:将serverSocketChannel的ACCEPT事件注册到selector中
        //在监听到ACCEPT事件后再将SocketChannel的READ事件注册到selector中
        //在监听到READ事件后启动一个任务放到线程池中进行处理
        final ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        final Selector selector=Selector.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动...");
        System.out.println("开始监听端口号8080...");
        final Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while(true){
                        selector.select();
                        Set<SelectionKey> selectionKeys=selector.selectedKeys();
                        for(SelectionKey selectionKey:selectionKeys) {
                            selectionKeys.remove(selectionKey);
                            if (selectionKey.isAcceptable()){
                                SocketChannel socketChannel=serverSocketChannel.accept();
                                System.out.println("连接成功...");
                                String clientInfo=socketChannel.socket().getInetAddress().getHostAddress();
                                int port=socketChannel.socket().getPort();
                                System.out.println("客户端信息为"+clientInfo+":"+port);
                                socketChannel.configureBlocking(false);
                                socketChannel.register(selector,SelectionKey.OP_READ);
                                System.out.println("开始监听READ事件...");
                            }else if(selectionKey.isReadable()){
                                System.out.println("监听到READ事件...");
                                executorService.submit(new ServerHandler((SocketChannel) selectionKey.channel()));
                                selectionKey.cancel();
                            }
                        }
                    }

                } catch (IOException e) {
                    throw new RuntimeException(e);
                }

            }
        });
        thread.start();
        System.out.println("主线程休眠2s等待服务端准备就绪...");
        Thread.currentThread().sleep(2000);
        System.out.println("客户端启动...");
        //客户端
        final SocketChannel socketChannel=SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(),8080));
        System.out.println("客户端发起连接...");
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                String data="Hello, here is a piece of information, please pay attention to check!";
                try {
                    socketChannel.write(ByteBuffer.wrap(data.getBytes()));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread1.start();
    }
}

输出:

服务启动…
开始监听端口号8080…
主线程休眠2s等待服务端准备就绪…
客户端启动…
客户端发起连接…
连接成功…
客户端信息为172.19.32.1:59251
开始监听READ事件…
监听到READ事件…
开始处理READ事件…
读到字节数:69
Hello, here is a piece of information, please pay attention to check!

同样使用两个线程来模拟客户端与服务端,服务端将serverSocketChannel的ACCEPT事件注册到selector中,在监听到ACCEPT事件后再将SocketChannel的READ事件注册到selector中,在监听到READ事件后启动一个自定义的任务ServerHandler放到线程池中进行处理。


Selector选择器使用方法

  • Selector选择器的获取

Selector selector = Selector.open();

  • 注册Channel到Selector

SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
register()方法的第二个参数:是一个int值,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件,而且可以使用SelectionKey的四个常量表示:
连接就绪–常量:SelectionKey.OP_CONNECT
接收就绪–常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在注册时只能使用此项)
读就绪–常量:SelectionKey.OP_READ
写就绪–常量:SelectionKey.OP_WRITE
注意:对于ServerSocketChannel在注册时,只能使用OP_ACCEPT,否则抛出异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值