编程技术是改变世界的力量。
本站
当前位置:网站首页 > 后端语言 > 正文

Java网络IO模型分析与实现 java io涉及的设计模式

gowuye 2024-04-04 11:53 11 浏览 0 评论

前言

本篇文章会对Java中的网络IO模型的概念进行解释,并给出具体的Java代码实现,主要涉及如下部分。

  1. BIO(同步阻塞IO模型)的概念和Java编程实现;
  2. Non Blocking IO(同步非阻塞IO模型)的概念和Java编程实现;
  3. IO多路复用的概念;
  4. NIONew IO)的概念和Java编程实现。

在开始本篇文章内容之前,有一个简单的关于Socket的知识需要说明:在进行网络通信的时候,需要一对Socket,一个运行于客户端,一个运行于服务端,同时服务端还会有一个服务端Socket,用于监听客户端的连接。下图进行一个简单示意。

那么整个通信流程如下所示。

  1. 服务端运行后,会在服务端创建listen-socketlisten-socket会绑定服务端的ipport,然后服务端进入监听状态;
  2. 客户端请求服务端时,客户端创建connect-socketconnect-socket描述了其要连接的服务端的listen-socket,然后connect-socketlisten-socket发起连接请求;
  3. connect-socketlisten-socket成功连接后(TCP三次握手成功),服务端会为已连接的客户端创建一个代表该客户端的client-socket,用于后续和客户端进行通信;
  4. 客户端与服务端通过socket进行网络IO操作,此时就实现了客户端和服务端中的不同进程的通信。

需要知道的就是,在客户端与服务端通信的过程中,出现了三种socket,分别是。

  1. listen-socket。是服务端用于监听客户端建立连接的socket
  2. connect-socket。是客户端用于连接服务端的socket
  3. client-socket。是服务端监听到客户端连接请求后,在服务端生成的与客户端连接的socket

(注:上述中的socket,可以被称为套接字,也可以被称为文件描述符。)

正文

一. BIO

BIO,即同步阻塞IO模型。用户进程调用read时发起IO操作,此时用户进程由用户态转换到内核态,只有在内核态中将IO操作执行完后,才会从内核态切换回用户态,这期间用户进程会一直阻塞。

BIO示意图如下。

简单的BIOJava编程实现如下。

服务端实现

public class BioServer {

    public static void main(String[] args) throws IOException {
        // 创建listen-socket
        ServerSocket listenSocket = new ServerSocket(8080);
        // 进入监听状态,是一个阻塞状态
        // 有客户端连接时从监听状态返回
        // 并创建代表这个客户端的client-socket
        Socket clientSocket = listenSocket.accept();
        // 获取client-socket输入流
        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(clientSocket.getInputStream()));
        // 读取客户端发送的数据
        // 如果数据没准备好,会进入阻塞状态
        String data = bufferedReader.readLine();
        System.out.println(data);

        // 获取client-socket输出流
        BufferedWriter bufferedWriter = new BufferedWriter(
                new OutputStreamWriter(clientSocket.getOutputStream()));
        // 服务端向客户端发送数据
        bufferedWriter.write("来自服务端的返回数据\n");
        // 刷新流
        bufferedWriter.flush();
    }

}

客户端实现

public class BioClient {

    public static final String SERVER_IP = "127.0.0.1";
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws IOException {
        // 客户端创建connect-socket
        Socket connectSocket = new Socket(SERVER_IP, SERVER_PORT);
        // 获取connect-socket输出流
        BufferedWriter bufferedWriter = new BufferedWriter(
                new OutputStreamWriter(connectSocket.getOutputStream()));
        // 客户端向服务端发送数据
        bufferedWriter.write("来自客户端的请求数据\n");
        // 刷新流
        bufferedWriter.flush();

        // 获取connect-socket输入流
        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(connectSocket.getInputStream()));
        // 读取服务端发送的数据
        String returnData = bufferedReader.readLine();
        System.out.println(returnData);
    }

}

BIO的问题就在于服务端在accept时是阻塞的,并且在主线程中,一次只能accept一个SocketacceptSocket后,读取客户端数据时又是阻塞的。

二. Non Blocking IO

Non Blocking IO,即同步非阻塞IO。是用户进程调用read时,用户进程由用户态转换到内核态后,此时如果没有系统资源数据能够被读取到内核缓冲区中,返回read失败,并从内核态切换回用户态。也就是用户进程发起IO操作后会立即得到一个操作结果。

Non Blocking IO示意图如下所示。

简单的Non Blocking IOJava编程实现如下。

public class NonbioServer {

    public static final List<SocketChannel> clientSocketChannels = new ArrayList<>();

    public static void main(String[] args) throws Exception {
        // 客户端创建listen-socket管道
        // 管道支持非阻塞模式和同时读写
        ServerSocketChannel listenSocketChannel = ServerSocketChannel.open();
        // 设置为非阻塞模式
        listenSocketChannel.configureBlocking(false);
        // 绑定监听的端口号
        listenSocketChannel.socket().bind(new InetSocketAddress(8080));
        // 在子线程中遍历clientSocketChannels并读取客户端数据
        handleSocketChannels();

        while (true) {
            // 非阻塞方式监听客户端连接
            // 如果无客户端连接则返回空
            // 有客户端连接则创建代表这个客户端的client-socket管道
            SocketChannel clientSocketChannel = listenSocketChannel.accept();
            if (clientSocketChannel != null) {
                // 设置为非阻塞模式
                clientSocketChannel.configureBlocking(false);
                // 添加到clientSocketChannels中
                // 用于子线程遍历并读取客户端数据
                clientSocketChannels.add(clientSocketChannel);
            } else {
                LockSupport.parkNanos(1000 * 1000 * 1000);
            }
        }
    }

    public static void handleSocketChannels() {
        new Thread(() -> {
            while (true) {
                // 遍历每一个client-socket管道
                Iterator<SocketChannel> iterator = clientSocketChannels.iterator();
                while (iterator.hasNext()) {
                    SocketChannel clientSocketChannel = iterator.next();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int read = 0;
                    try {
                        // 将客户端发送的数据读取到ByteBuffer中
                        // 这一步的操作也是非阻塞的
                        read = clientSocketChannel.read(byteBuffer);
                    } catch (IOException e) {
                        // 移除发生异常的client-socket管道
                        iterator.remove();
                        e.printStackTrace();
                    }
                    if (read == 0) {
                        System.out.println("客户端数据未就绪");
                    } else {
                        System.out.println("客户端数据为:" + new String(byteBuffer.array()));
                    }
                }
                LockSupport.parkNanos(1000 * 1000 * 1000);
            }
        }).start();
    }

}

上述是Non Blocking IO的一个简单服务端的实现,相较于BIO,服务端在accept时是非阻塞的,在读取客户端数据时也是非阻塞的,但是还是存在如下问题。

  1. 一次只能accept一个Socket
  2. 需要在用户进程中遍历所有的SocketChannel并调用read() 方法获取客户端数据,此时如果客户端数据未准备就绪,那么这一次的read() 操作的开销就是浪费的。

三. IO多路复用

在上述的BIONon Blocking IO中,一次系统调用,只会获取一个IO的状态,而如果采取IO多路复用机制,则可以一次系统调用获取多个IO的状态。

也就是获取多个IO的状态可以复用一次系统调用。

最简单的IO多路复用方式是基于select模型实现,步骤如下。

  1. 在用户进程中将需要监控的IO文件描述符(Socket)注册到IO多路复用器中;
  2. 执行select操作,此时用户进程由用户态转换到内核态(一次系统调用),然后在内核态中会轮询注册到IO多路复用器中的IO是否准备就绪,并得到所有准备就绪的IO的文件描述符列表,最后返回这些文件描述符列表;
  3. 用户进程在select操作返回前会一直阻塞,直至select操作返回,此时用户进程就获得了所有就绪的IO的文件描述符列表;
  4. 用户进程获得了就绪的IO的文件描述符列表后,就可以对这些IO进行相应的操作了。

换言之,IO多路复用中,只需要一次系统调用,IO多路复用器就可以告诉用户进程,哪些IO已经准备就绪可以进行操作了,而如果不采用IO多路复用,则需要用户进程自己遍历每个IO并调用accept() 或者read() 方法去判断,且一次accept() 或者read() 方法调用只能判断一个IO

四. NIO

NIO,即New IO。关于NIO,有如下三大组件。

  1. channel(管道)。介于buffer(字节缓冲区)和Socket(套接字)之间,用于数据的读写操作;
  2. buffer(字节缓冲区)。是用户程序和channel(管道)之间进行读写数据的中间区域;
  3. selectorIO多路复用器)。服务端的listen-socketclient-socket,客户端的connect-socket,都可以注册在selector上,注册的时候还需要指定监听的事件,比如为listen-socket指定监听的事件为ACCEPT事件,该事件发生则表示客户端建立了连接,还比如为client-socket指定监听的事件为READ事件,该事件发生则表示客户端发送的数据已经可读。每次调用selectoropen() 方法,可以获取发生的事件列表,每一个事件会被封装为SelectionKey

NIO的代码实现如下所示。

服务端实现

public class NioServer {

    private static Selector selector;

    public static void main(String[] args) {

        try {
            // 开启并得到多路复用器
            selector = Selector.open();
            // 服务端创建listen-socket管道
            ServerSocketChannel listenSocketChannel = ServerSocketChannel.open();
            // 设置为非阻塞模式
            listenSocketChannel.configureBlocking(false);
            // 为管道绑定端口
            listenSocketChannel.socket().bind(new InetSocketAddress(8080));
            // 将listen-socket管道注册到多路复用器上,并指定监听ACCEPT事件
            listenSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                // 获取发生的事件,这个操作是阻塞的
                selector.select();
                // 每一个发生的事件会被封装为SelectionKey
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 遍历每个发生的事件,然后判断事件类型
                // 根据事件类型,进行不同的处理
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    if (selectionKey.isAcceptable()) {
                        // 处理客户端连接事件
                        handlerAccept(selectionKey);
                    } else if (selectionKey.isReadable()) {
                        // 处理客户端数据可读事件
                        handlerRead(selectionKey);
                    }
                }
                LockSupport.parkNanos(1000 * 1000 * 1000);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handlerAccept(SelectionKey selectionKey) {
        // 从事件中获取到listen-socket管道
        ServerSocketChannel listenSocketChannel = (ServerSocketChannel) selectionKey.channel();
        try {
            // 为连接的客户端创建client-socket管道
            SocketChannel clientSocketChannel = listenSocketChannel.accept();
            // 设置为非阻塞模式
            clientSocketChannel.configureBlocking(false);
            // 将client-socket管道注册到多路复用器上,并指定监听READ事件
            clientSocketChannel.register(selector, SelectionKey.OP_READ);
            // 给客户端发送数据
            clientSocketChannel.write(ByteBuffer.wrap("连接已建立\n".getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handlerRead(SelectionKey selectionKey) {
        // 从事件中获取到client-socket管道
        SocketChannel clientSocketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            // 读取客户端数据
            clientSocketChannel.read(byteBuffer);
            System.out.println(new String(byteBuffer.array()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

客户端实现

public class NioClient {

    private static Selector selector;

    public static final String SERVER_IP = "127.0.0.1";
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) {
        try {
            // 开启并得到多路复用器
            selector = Selector.open();
            // 创建connect-socket管道
            SocketChannel connectSocketChannel = SocketChannel.open();
            // 设置为非阻塞模式
            connectSocketChannel.configureBlocking(false);
            // 设置服务端IP和端口
            connectSocketChannel.connect(new InetSocketAddress(SERVER_IP, SERVER_PORT));
            // 将connect-socket管道注册到多路复用器上,并指定监听CONNECT事件
            connectSocketChannel.register(selector, SelectionKey.OP_CONNECT);

            while (true) {
                // 获取发生的事件,这个操作是阻塞的
                selector.select();
                // 每一个发生的事件会被封装为SelectionKey
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 遍历每个发生的事件,然后判断事件类型
                // 根据事件类型,进行不同的处理
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    if (selectionKey.isConnectable()) {
                        // 处理连接建立事件
                        handlerConnect(selectionKey);
                    } else if (selectionKey.isReadable()) {
                        // 处理服务端数据可读事件
                        handlerRead(selectionKey);
                    }
                }
                LockSupport.parkNanos(1000 * 1000 * 1000);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handlerConnect(SelectionKey selectionKey) throws IOException {
        // 拿到connect-socket管道
        SocketChannel connectSocketChannel = (SocketChannel) selectionKey.channel();
        if (connectSocketChannel.isConnectionPending()) {
            connectSocketChannel.finishConnect();
        }
        // 设置为非阻塞模式
        connectSocketChannel.configureBlocking(false);
        // 将connect-socket管道注册到多路复用器上,并指定监听READ事件
        connectSocketChannel.register(selector, SelectionKey.OP_READ);
        // 向服务端发送数据
        connectSocketChannel.write(ByteBuffer.wrap("客户端发送的数据\n".getBytes()));
    }

    private static void handlerRead(SelectionKey selectionKey) {
        // 拿到connect-socket管道
        SocketChannel connectSocketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            // 读取服务端数据
            connectSocketChannel.read(byteBuffer);
            System.out.println(new String(byteBuffer.array()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

总结

本篇文章中所讨论的IO模型,都是同步IO模型,而何谓同步异步,何谓阻塞非阻塞,可以总结如下。

  1. 同步异步同步IO表示需要在用户进程中主动的去询问操作系统数据是否准备好,如果没有准备好,还需要持续的询问,直到数据准备好为止;而异步IO则是在用户进程中只需要询问操作系统一次,后续数据准备好后操作系统会主动的将数据给到用户进程;
  2. 阻塞非阻塞阻塞IO就是发起一次系统调用后,会一直阻塞直到有结果返回;而非阻塞IO就是发起一次系统调用后,会立即得到一个返回结果。

实际上在Java中是有对异步IOAIO)做支持,但是AIO依赖操作系统的底层实现,而目前LinuxAIO的支持不成熟,所以AIO的使用并不多,像主流的网络应用框架Netty也都没有使用到AIO

相关推荐

嵌入式C语言中常量的应用实例(嵌入式c语言中常量的应用实例是什么)

常量,我们都知道,就是数值保持不变的量。在C语言中,常量一旦初始化了,它的值将在整个程序运行周期内,不允许发生任何变化。常量与变量是相对的,我们实际项目中经常会用到它。定义常量的两种方式C语言中主要有...

C语言编程基础知识汇总学习,适合初学者!更新常量知识

(二)整型常量整型常量有3种形式:十进制整型常量、八进制整型常量和十六进制整型常量。(注意:c语言中没有直接表示二进制的整型常量,在c语言源程序中不会出现二进制。)书写方式如下:十进制整型常量:123...

【C语言】第二章第六节:字符串常量

第二章第六节:字符串常量。下表C语言中的常用转义字符。·字符形式功能:ASCIl码(十进制形式)。→\t水平制表(横向跳格:跳到下一个tab位置)。→\b退格8。→\r回车(不换行,光标移到本行行首)...

「GCTT 出品」Go 系列教程——5. 常量

这是我们Golang系列教程的第五篇。定义在Go语言中,术语”常量”用于表示固定的值。比如5、-89、IloveGo、67.89等等。看看下面的代码:varaint=50v...

每日C语言-常量指针、指针常量、指向常量的指针常量

一、常量指针1)什么是常量指针?通过该指针不可以修改其所指向存储单元中的值指针本身即地址可以被修改2)定义:类型说明符const*指针变量;类型说明符表示指针所指向存储单元中的值得数据类型指针...

C语言-符号常量、常变量、变量之我见

更新内容:新增音频。音频和文章一起更配oHello,大家好,又和大家见面了~~相信很多朋友们听了C语言的“符号常量”、“常变量”、“变量”后还是对这三者一脸懵逼吧。不管老师怎么歇斯底里地讲解,同学们迷...

零基础带你学习C语言:四:探索常量与变量

前言常量与变量学习;一:分析:short、float、long类型#include<stdio.h>intmain(){shortage=18;floatweight=12...

C语言中是如何定义常量的?那定义字符串呢?

常量有整型常量、浮点型常量、字符型常量及字符串常量。‘常量定义是指定义符号常量,用一个标识符来代表一个常量,通过宏定义预处理指令来实现。常量的定义:#definecount60这就定义了一个常量...

C语言符号常量的优点,会是那几点?

符号常量是一个常量,是不变量,所以,在编译的时候,就把符号常量出现的地方,替换为符号常量对应的常量。符号常量一般用户定义一个全局使用的数据,而且要改变该数据的时候,只需要改变符号常量的值,代码中引用符...

嵌入式开发- C语言数据类型-常量(c语言嵌入式是干嘛的)

基本数据类型的常量-掌握**整型常量:**常量是指在程序运行期间其数值不发生变化的数据。整型常量通常简称为整数整数可以是十进制数、八进制数、十六进制数八进制06334十六进制0xd1...

c语言解剖课:只读变量、常量、字面量傻傻分不清?

写在前面本篇主题的缘起,是因为一个计算机专业的大学生在和我讨论c语言问题时,说const常量如何如何,我说变量被const修饰了,还是变量,不是“常量”。他给了我一个截图:他说大模型都是这样回答的,变...

C/C++编程笔记:C数组、字符串常量和指针!三分钟弄懂它

想弄懂C语言中数组和指针的关系吗?这篇文章就占据你三分钟时间,看完你肯定会有收获!数组数组声明为数据类型名称[constant-size],并将一个数据类型的一个或多个实例分组到一个可寻址的位...

C语言入门到精通【第008讲】——C语言常量

C语言常量常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。常量就像是常规的变量,只不过常...

这是C语言无法修改得东西,C语言基础教程之常量解析

常量是指程序在执行期间不会改变的固定值。这些固定值也称为文字。常量可以是任何基本数据类型,如整数常量,浮点常量,字符常量或字符串文字,还有枚举常量。常量被视为常规变量,除了它们的值在定义后无法修改。整...

C语言中的单精度、双精度、常量等都有什么意思?

刚接触C语言时,对于常量,变量,浮点,单精度,双精度等问题的理解,大都很模糊不清,其实在程序运行过程中,其值不能改变的量称为常量。如12、0、-3为整型常量,4.6、-1.23为实型常量,'a'、'...

取消回复欢迎 发表评论: