Java网络编程BIO和NIO

BIO、NIO

本文参考自《Netty权威指南》、《Netty实战》,主要对JDK的BIO、NIO和JDK1.7 最新提供的NIO 2.0的使用进行详细说明。

1、传统的同步阻塞式I/O编程

2、基于NIO的非阻塞编程

3、基于NIO2.0异步非阻塞(AIO)编程

4、为什么使用NIO编程

5、为什么选择Netty

​ 网络编程的基本模型是Client/Server模型(即两个进程之间进行相互通信,其中服务端提供位置信息(绑定的Ip地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。

一、传统的BIO编程

1.1BIO通信模型图

请添加图片描述

​ 采用BIO通信模型的服务器,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。

请添加图片描述

​ 该模型最大的问题缺乏弹性伸缩能力,当客户端并发量增加后,服务端的线程个数和客户端并发访问数一致,线程对于JVM是宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题。

    public final static int targetPort=9001;
    public static void main(String[] args) throws IOException {
        //实现服务器和客户端通信
        //1、创建服务器端通信的ServerSocket对象,用以监听指定端口上的连接请求
        ServerSocket serverSocket=new ServerSocket(targetPort);
        //2、对accept()用于侦听并接收此ServerSocket的连接,方法的调用将被阻塞,直到一个连接的建立
           for (; ; ) {
            //阻塞式接收客户端套接字
            final Socket socket = serverSocket.accept();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    OutputStream out = null;

                    try {
                        out = socket.getOutputStream();
                        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(out));
                        bw.write("hell0");
                        bw.newLine();
                        bw.flush();
                        socket.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    } finally {
                        try {
                            out.close();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }

                }
                }).start();

            }

    }

​ 对此我们发线,BIO主要问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。显然无法满足高性能、高并发接入的场景。

为了改进一个线程一个连接模型,后来演进了一种通过线程池或消息队列实现一个或者多个线程处理N个客户端的模型,但由于底层依然使用同步阻塞I/O,所以被称为“伪异步”

1.2 伪异步I/O编程

为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,对其模型做了优化:通过一个线程池来处理多个客户端的请求接入,线程池的最大线程数可以远大于客户端数。通过线程池来灵活调配线程资源。

伪装异步通信模型

请添加图片描述

将客户端的Socket封装成为一个Task(实现Runnable接口)提交到线程池进行处理。通过设置线程池的max Thread、阻塞队列来调控

异步任务执行逻辑

public class TimeServerHandler implements Runnable{
    private Socket socket;

    public TimeServerHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        BufferedReader br=null;
        PrintWriter out=null;
        try{
            br=new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out=new PrintWriter(socket.getOutputStream(),true);
            String currentTime=null;
            String body=null;
            for(;;){
                body=br.readLine();
                if(body==null){
                    break;
                }
                System.out.println("The time server receive order:"+body);
                currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?new Date(System.currentTimeMillis())
                        .toString():"BAD ORDER";
                out.println(currentTime);
            }
        } catch (Exception e) {
            if(br!=null){
                try {
                    br.close();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
            }

            if(out!=null){
                out.close();
                out=null;
            }
            if(this.socket!=null){
                try {
                    this.socket.close();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
                this.socket=null;
            }
        }
    }
}

客户端

public class TimeClient {
    public static void main(String[] args) throws IOException {
        Socket socket=new Socket("127.0.0.1",8080);
//        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//        bw.write("QUERY TIME ORDER");
//        bw.newLine();
//        bw.flush();
        PrintWriter pw=new PrintWriter(socket.getOutputStream(),true);
        pw.println("QUERY TIME ORDER");

        BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String s = br.readLine();
        System.out.println("Now is :"+s);
    }
}

服务端接收连接,启动线程驱动

public class TimeServer {
    public static void main(String[] args) {
        int port=8080;
        ServerSocket server=null;
        try{
            server=new ServerSocket(port);
            Socket socket=null;
            //创建I/O任务线程池
            TimeServerHandlerExecutePool singleExecutor=new TimeServerHandlerExecutePool(50,10000);
            for(;;){
                socket=server.accept();
                singleExecutor.execute(new TimeServerHandler(socket));
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

线程池

public class TimeServerHandlerExecutePool {
    private ExecutorService executor;

    public TimeServerHandlerExecutePool(int maxPoolSize,int queueSize){
        executor=new ThreadPoolExecutor(Runtime.getRuntime()
                .availableProcessors(),maxPoolSize,120L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(queueSize));
    }

    public void execute(Runnable task){
        executor.execute(task);
    }
}

请添加图片描述

请添加图片描述

伪异步I/O弊端分析
  • Socket的输入流进行读取操作时,会一直阻塞,直到:
    • 有数据可读
    • 可用数据读取完毕
    • 发生控制着或I/O异常
  • 调用OutputStream的write方法写输出流时,也会被阻塞,直到所有要发送的字节全部写入完毕,或者方式一场

二、NIO编程

​ 与Socket类和ServerSocket类相对应,NIO提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种通道都支持阻塞和 非阻塞两种模式。对于阻塞模式,性能和可靠性不好。但非阻塞模式恰好相反。

一般,低负载、低开发的应用程序选择BIO降低编程复杂度;对于高负载、高并发的网络应用,使用NIO的非阻塞模式进行开发

2.1 NIO类库

​ NIO是JDK1.4引入。弥补了原来BIO的不足。

1、缓冲区Buffer

请添加图片描述

​ Buffer继承关系

​ Buffer是一个对象,包含一些要写入或者读出的数据。NIO引入了Buffer对象,为了区别于BIO。在面向流的I/O中,可以将数据直接写入或将数据读到Stream对象中。

​ NIO库中,所有数据都是用缓冲区处理的,在读取数据时,直接读到缓冲区;写数据时,写入到缓冲区。任何时候访问NIO的数据,都是通过缓冲区进行操作

​ 缓冲区本质是一个数组,通常是一个字节数组,也可使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置等信息

​ 如上图,Java的所有基本数据类型都对应一种缓冲区,每一个Buffer类都是Buffer接口的一个子实例。大多数标准I/O使用ByteBuffer,所以ByteBuffer具有一般缓冲区操作之外的特有操作,方便网络读写。

2、Channel通道

​ 网络数据通过Channel读取和写入。通道和流不同之处在于通道是双向的,而流是单向的,通道可以用于读、写或者二者同时进行(Channel具有全双工)。

​ Channel也分两大类:用于网络读写的SelectableChannel和用于文件读写的FileChannel

请添加图片描述

3、多路复用器Selector

​ 多路复用器提供选择已经就绪的任务的能力。简单来说,Selector会不断地沦陷注册在其上的Channel(一个多路复用器Selector可以同时轮询多个Channel),如果某个Channel上面发生读或写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey获取就绪Channel的集合,进行后序的I/O操作。

NIO服务端序列图

请添加图片描述

public class PlainNioServer {
    public void server(int port) throws IOException {
        //1、开启ServerSocketChannel,用于监听客户端的连接,是所有客户端连接的父管道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        //2、设置连接为非阻塞,绑定监听端口
        serverChannel.configureBlocking(false);
        ServerSocket ssocket = serverChannel.socket();
        //将服务器绑定到相应的端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress(port);
        ssocket.bind(inetSocketAddress);

        //3、打开Selector处理Channel
        Selector selector = Selector.open();
        //4、将ServerSocket注册到Selector以接受连接
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        final ByteBuffer wrap = ByteBuffer.wrap("hello".getBytes());
        for(;;){
            try{
                selector.select();
            }catch (IOException e){
                e.printStackTrace();
                break;
            }
//          5、获取所有接收事件的SelectionKey实例(即就绪的Channel集合)
            Set<SelectionKey> readyKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = readyKeys.iterator();
//          6、多路复用器 轮询 准备就绪的Key
            while(iterator.hasNext()){
                SelectionKey key=iterator.next();
                iterator.remove();
                try{
                    //检查事件是否是一个新的已经就绪可以被接受的连接
                    if(key.isAcceptable()){
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        //多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路
                        SocketChannel client = server.accept();
                        //设置客户端链路为非阻塞
                        client.configureBlocking(false);
                        //将新接入的客户端连接 注册到 多路复用器上,监听读写操作
                        client.register(selector, SelectionKey.OP_WRITE |
                                SelectionKey.OP_READ,wrap.duplicate());
                        System.out.println("Accepted connection from"+client);
                    }
//                  检查套接字是否已经准备好写数据
                    if(key.isWritable()){
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        //将数据写到已连接的客户端
                        while (buffer.hasRemaining()) {
                            if (client.write(buffer) == 0) {
                                break;
                            } }
                        client.close();
                    }
                }catch (IOException e){
                    key.cancel();
                    try{
                        key.channel().close();
                    }catch (Exception ce){
                        ce.printStackTrace();
                    }
                }
            }

        }
    }
}


版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340