BIO、NIO、AIO

视频链接

博客链接

IO

同步与异步(一个任务依赖另外一个任务)

  • 同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
  • 异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。

同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。

阻塞和非阻塞

  • 阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。
  • 非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。

BIO(Bloking input-output)

服务端多线程的话,每来一个客户端请求,就会创建一个线程。因为如果总共是一个线程的话,如果有一个服务端等待客户端输入数据等情况,会造成服务端的阻塞,比如read方法等待客户端输入。

在BIO的时候,Server中的通道是单向的,而不是双向的,socket.getOutputStream(),socket.getIntputStream()

主要讲的是网络上的输入输出
你给server发消息,客户端就叫做输出,服务端给客户端发消息对客户端来说就叫做输入。

传统的BIO的几个地方有阻塞

  • accept
  • read
  • write

BIO服务端单线程时候的阻塞情况(不支持并发)

下面的服务端代码会在read的时候进行阻塞。

客户端
package SocketTest;

import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

public class IOClient
{
    public static void main(String[] args) throws IOException 
    {
        Socket socket = new Socket("127.0.0.1",8888);
        Scanner scanner = new Scanner(System.in);
        String next = scanner.next(); //客户端不发消息
        socket.getOutputStream().write(next.getBytes());
        socket.getOutputStream().flush();

        System.out.println("waiting for msg back");
        byte[] bytes = new byte[1024];
        int len = socket.getInputStream().read(bytes);
        System.out.println(new String(bytes,0,len));
        socket.close();
    }
}

服务端
package SocketTest;

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class IOServer
{
    public static void main(String[] args) throws IOException 
    {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress("127.0.0.1",8888));
        while (true)
        {
            Socket socket = serverSocket.accept();
            handle(socket);
        }
    }
    static void handle(Socket socket)
    {
        try
        {
            byte[] bytes = new byte[1024];
            int len = socket.getInputStream().read(bytes);
            System.out.println(new String(bytes,0,len));
            socket.getOutputStream().write("hello client".getBytes());
            socket.getOutputStream().flush();

        }
        catch (IOException e)
        {
            e.printStackTrace();
        }


    }
}

服务端多线程解决阻塞

多线程会造成服务器端很浪费。

客户端
package SocketTest;

import java.io.IOException;
import java.net.Socket;

public class IOClient
{
    public static void main(String[] args) throws IOException 
    {
        Socket socket = new Socket("127.0.0.1",8888);
        socket.getOutputStream().write("Hello Server".getBytes());
        socket.getOutputStream().flush();
        System.out.println("waiting for msg back");
        byte[] bytes = new byte[1024];
        int len = socket.getInputStream().read(bytes);
        System.out.println(new String(bytes,0,len));
        socket.close();
    }
}

服务端

ServerSocket对象只会监听有没有人连,Socket才是真正进行传输的对象。

package SocketTest;

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class IOServer
{
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress("127.0.0.1",8888));
        while (true)
        {
            Socket socket = serverSocket.accept();  //阻塞
            new Thread(()->{
                handle(socket);
            }).start();

        }
    }
    static void handle(Socket socket)
    {
        try
        {
            byte[] bytes = new byte[1024];
            int len = socket.getInputStream().read(bytes);
            System.out.println(new String(bytes,0,len));
            socket.getOutputStream().write("hello client".getBytes());
            socket.getOutputStream().flush();

        }
        catch (IOException e)
        {
            e.printStackTrace();
        }


    }
}

存在的问题

client连到server的话,如果server没收到连接请求,accept()可能会阻塞。
输入流的read和输出流的write方法也是会阻塞的。

在这里插入图片描述

NIO

NIO设计初衷就是使用单线程来处理并发。

上面的BIO的单线程的代码中,服务端的read方法会阻塞。read方法不解阻塞(因为客户端先write-》服务端accept-》服务端read-》服务端write-》客户端read),那么在单线程中accept就不会再被调用,那我们就想让read解阻塞。

在单线程中,如果read不阻塞就会出现其他问题

  • 1、比如serverSocket.accept()获得到的socket丢掉了,因为如果新接收请求会得到新的socket,所以下面的例子中会想到使用List进行存放。
  • 2、accept()阻塞,需要设置成非阻塞。

下面是伪代码(用List存放Socket,while (true)接收socket请求和遍历所有的Socket中接收数据。)

package SocketTest;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;

public class SolveTest
{
    public static void main(String[] args) throws IOException
    {
        List<Socket> socketList = null;

        ServerSocket serverSocket = new ServerSocket(8080);
        while (true)
        {
            serverSocket.zl(false);  //设置accept方法非阻塞,serverSocketChannel里面有一个方法可以设置成非阻塞,serverSocketChannel.configureBlocking(false);

            Socket clientSocket = serverSocket.accept();
            for(Socket socket:socketList)  //循环看有没有人发消息过来
            {
                byte[] bytes = new byte[1024];
                int read = clientSocket.getInputStream().read(bytes);
                if(read > 0)
                {
                    System.out.println();
                }

            }
        }


    }
}

下面是非伪代码

configureBlocking(false);设置非阻塞,这样accept、read就应该是非阻塞了。

package SocketTest;

import sun.tools.jstat.Jstat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;

public class SolveTest
{
    static ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    static List<SocketChannel> channelList = new ArrayList<>();
    public static void main(String[] args) {
        try
        {
            ServerSocketChannel serverSocket = ServerSocketChannel.open(); //底层会调用一个native方法即poll0方法,poll0会调用select方法 
            SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8080);
            serverSocket.bind(socketAddress);
            serverSocket.configureBlocking(false); //设置非阻塞
            while (true)
            {
                for (SocketChannel socketChannel : channelList)
                {
                    int read = socketChannel.read(byteBuffer);
                    if (read > 0) {
                        System.out.println("read------111------:" + read);
                        byteBuffer.flip();
                        byte[] bs = new byte[read];
                        byteBuffer.get(bs);
                        String content = new String(bs);
                        System.out.println(content);
                        byteBuffer.flip();
                    }
                }
                else
                {
                	channelList.remove(socketChannel);
                }
                SocketChannel accept = serverSocket.accept();  
                if (accept != null)
                {
                    System.out.println("conn success");
                    accept.configureBlocking(false);  
                    channelList.add(accept);
                    System.out.println(channelList.size()+"---list---size");
                }
            }
        }
        catch (IOException e)
        {

        }
    }
}

存在的问题

如果上面的List里面有10000个SocketChannel,只有1000个是活跃的,那么就浪费了9000次空轮询。
解决思路:

  • 假设让list循环交给内核执行,那么速度肯定快了。
  • 使用Selector

在这里插入图片描述
epoll性能好于select
linux平台下调用epoll
windows平台下调用select

Selector

为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:

  • 1、connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8)
  • 2、accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16)
  • 3、read:读事件,对应值为SelectionKey.OP_READ(1)
  • 4、write:写事件,对应值为SelectionKey.OP_WRITE(4)

这个很好理解,每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。

所以,当SocketChannel有对应的事件发生时,Selector都可以观察到,并进行相应的处理。

使用Selector的服务端代码

ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
Selector selector = Selector.open(); //通过调用Selector.open()方法创建一个Selector对象
serverChannel.register(selector, SelectionKey.OP_ACCEPT); //注册Channel到Selector
while(true)
{
    int n = selector.select(); //阻塞到至少有一个通道在你注册的时间上就绪了,可以加时间参数,设置最长阻塞时间
    if (n == 0) continue;
    Iterator ite = this.selector.selectedKeys().iterator();
    while(ite.hasNext()){
        SelectionKey key = (SelectionKey)ite.next();
        if (key.isAcceptable()) //判断是否可接收,是返回true
        {
            SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept();
            clntChan.configureBlocking(false);
            //将选择器注册到连接到的客户端信道,
            //并指定该信道key值的属性为OP_READ,
            //同时为该信道指定关联的附件
            clntChan.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
        }
        if (key.isReadable()){
            handleRead(key);
        }
        if (key.isWritable() && key.isValid()){
            handleWrite(key);
        }
        if (key.isConnectable()){
            System.out.println("isConnectable = true");
        }
      ite.remove();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值