JAVA IO之IO-API

409 阅读4分钟

JAVA IO之IO-API

网络IO编程步骤

NIO

  1. 创建监听,允许客户端发起请求
  2. 绑定网络Socket端口,绑定后有很重要的一步 看业务看是否需要设置OS是否阻塞
  3. 死循环接收Client请求 accept()
  4. 读取数据
public class ServerNIO {

    public static void main(String[] args) {
        ServerNIO nio = new ServerNIO();
        ServerSocketChannel socketChannel = nio.inintServer();
        while (true){

            nio.getMsg(socketChannel);

        }

    }
    //初始化Server Method
    private ServerSocketChannel inintServer(){
        ServerSocketChannel server = null;
        try {
            //NIO Channel通道创建监听
            server = ServerSocketChannel.open();
            //绑定端口9999
            server.bind(new InetSocketAddress(9999));
            //设置OS NONBLOCKING
            server.configureBlocking(false);

            System.out.println("server 启动了");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return server;
    }


    private void getMsg(ServerSocketChannel serverSocketChannel){
        try {
            //服务端接收客户端请求
            SocketChannel client = serverSocketChannel.accept();
            if (client !=null){
                //设置client为非阻塞
                client.configureBlocking(false);

                //优化性能增加buffer
                ByteBuffer buffer = ByteBuffer.allocateDirect(2048);//缓存区可设置在堆外或者堆内,堆外性能高一点点
                int read = client.read(buffer);
                if (read>0){
                    //切换读写模式
                    buffer.flip();
                    byte[] aaa = new byte[buffer.limit()];
                    buffer.get(aaa);
                    String b = new String(aaa);
                    System.out.println(client.socket().getPort() + " : " + b);
                    buffer.clear();

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

进阶版本,多路复用 多路复用的关键是引入了selector 选择器,成批处理IO请求

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * @Classname SocketMultiplexingSingleThread
 * @Description TODO
 * @Date 2020/12/23 10:34 AM
 * @Author by lixin
 */
public class SocketMultiplexingSingleThread {
    private ServerSocketChannel server =null;

    private Selector selector=null;
    private void initServer(){
        try {
            //创建channel打开监听
            server =ServerSocketChannel.open();
            //绑定端口8888
            server.bind(new InetSocketAddress(8888));
            //设置OS  NONBLOCKING
            server.configureBlocking(false);
            //创建多路复用器
            /*
                在java中把select、poll、epoll三个IO模型都抽象成selector
                底层优先使用epoll模型
                epoll模型会执行命令 epoll_create ->fd

             */
            selector = Selector.open();
            //server注册到多路复用器中并声明事件。
            /*
             select,poll:jvm里开辟一个数组 fd 放进去
             epoll:  epoll_ctl(fd3,ADD,fd4,EPOLLIN
             epoll模型在 os中会多开辟两个空间 红黑树(所有  epoll_ctl命令把FD存到红黑树中) 和有状态链表结果集
             */
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public  void getMsg() throws IOException {
        initServer();
        System.out.println("服务器启动了");

        while (true){//死循环一直循环看有没有消息进来

            /*
                1.selector.select() (可增加阻塞时间)阻塞方法,可以使用selector.wakeup()中断
                2.select,poll  其实  内核的select(fd)  poll(fd),去内核查找fd是否可以读取数据,然后在内核FDS中遍历
                3.epoll 内核调用 epoll_wait() 内核中早已经有有状态的fd链表结果集 直接拿回来使用
             */
            while (selector.select()>0){
                //从selector中取出所有事件
                /*
                    NIO 自己需要对每一个FD进行调用,浪费资源,多路复用,调用一次select()可以把所有有状态的FD拿回来,进行业务处理
                 */
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = keys.iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    // ****处理了的事件需要remove,不remove会循环处理。****
                    iterator.remove();


                    //接收事件
                    if (key.isAcceptable()){
                        //一般来讲接收一个新的连接进来会返回一个新的FD
                        //select、poll,因为他们在内核没有内存空间,那么在jvm中保存和之前的哪些fd在一个fd数组中
                        //epoll:我们通过epoll_ctl注册到内核空间中

                        acceptHandler(key);
                    }else if (key.isReadable()){
                        //注意,read 经常会因为各种各样的事情造成阻塞,可能会1分钟甚至1个小时甚至1年等,那么系统就不能被其他人所使用
                        //so,我们这里引入了一个很重要的思想,IO threads 这个思想在redis中也有体现,redis因为是单线程也会面临这阻塞问题,所以有一个 IO threads的概念
                        readHandler(key);
                    }

                    //。。。。各种事件
                }
            }
        }
    }

    private void readHandler(SelectionKey key) {
        //从客户端中读取数据
        SocketChannel client = (SocketChannel) key.channel();

        // client.register(selector,SelectionKey.OP_READ,buffer);
        //ByteBuffer buffer = (ByteBuffer) key.attachment();
        //成对出现的buffer被注册到selector,然后用attachment()读出来
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        int read= 0;
        try {
        while (true){
            read = client.read(buffer);
            if (read>0){
                buffer.flip();
                while (buffer.hasRemaining()) {
                    client.write(buffer);
                }
                buffer.clear();
            }else if (read == 0) {
                break;
            } else {
                client.close();
                break;
            }

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

    private void acceptHandler(SelectionKey key) {
        //获取服务端通道
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        try {
            //从服务端接收新的client
            SocketChannel client = serverSocketChannel.accept();
            client.configureBlocking(false);

            ByteBuffer buffer = ByteBuffer.allocate(8192);
            //测试到selector中
            client.register(selector,SelectionKey.OP_READ,buffer);
            System.out.println("-------------------------------------------");
            System.out.println("新客户端:" + client.getRemoteAddress());
            System.out.println("-------------------------------------------");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


    public static void main(String[] args) throws IOException {
        SocketMultiplexingSingleThread sevice = new SocketMultiplexingSingleThread();
        sevice.getMsg();
    }
}

究极版本多路复用精华api,给大家讲一下思路

  1. 为了充分利用多核CPU的特性,我们可以尽可能的让CPU不闲着,一般线程数设置比较理想的数是cpu核数 或者2倍的cpu核数
  2. 我们可以利用多线程,创建多个selector,每一个线程一个selector
  3. 我们可以mix处理事件,也可以指定一个selector只处理接收client
  4. 另外的selector处理W/R 事件,这个思想和netty很接近了如果我们能够自己手写出来那么为我们之后学习netty会打下很好的基础