第七章 Java I/O演进之路

7.1 I/O的问世

7.1.1 什么是I/O

我们都知道在UNIX世界里一切皆文件,而文件是什么呢?文件就是一串二进制流而已,其实不管是Socket,还是FIFOFirst Input First Output,先进先出队列))、管道、终端。对计算机来说,一切都是文件,一切都是流。在信息交换的过程中,计算机都是对这些流进行数据的收发操作,简称为I/O操作(Input and Output),包括往流中读出数据、系统调用Read、写入数据、系统调用Write。不过计算机里有那么多流,怎么知道要操作哪个流呢?实际上是由操作系统内核创建文件描述符(File DescriptorFD)来标识的,一个FD就是一个非负整数,所以对这个整数的操作就是对这个文件(流)的操作。我们创建一个Socket,通过系统调用会返回一个FD,那么剩下的对Socket的操作就会转化为对这个描述符的操作,这又是一种分层和抽象的思想。

7.1.2 I/O交互流程

通常用户进程中的一次完整I/O交互流程分为两阶段,首先是经过内核空间,也就是由操作系统处理;紧接着就是到用户空间,也就是交由应用程序。具体交互流程如下图所示。

内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中,Linux使用两级保护机制:0级供内核(Kernel)使用,3级供用户程序使用。每个进程都有各自的私有用户空间(03G),这个空间对系统中的其他进程是不可见的。最高的1G字节虚拟内核空间则为所有进程及内核共享。

操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据。因为Linux使用的虚拟内存机制,必须通过系统调用请求Kernel来协助完成I/O操作,内核会为每个I/O设备维护一个缓冲区,用户空间的数据可能被换出,所以当内核空间使用用户空间的指针时,对应的数据可能不在内存中。

对于一个输入操作来说,进程I/O系统调用后,内核会先看缓冲区中有没有相应的缓存数据,如果没有再到设备中读取。因为设备I/O一般速度较慢,需要等待,内核缓冲区有数据则直接复制到进程空间。所以,一个网络输入操作通常包括两个不同阶段。

1)等待网络数据到达网卡,然后将数据读取到内核缓冲区。

2)从内核缓冲区复制数据,然后拷贝到用户空间。

I/O有内存I/O、网络I/O和磁盘I/O三种,通常我们说的I/O指的是后两者。如下图所示是I/O通信过程的调度示意。

7.2 五种I/O通信模型

在网络环境下,通俗地讲,将I/O分为两步:第一步是等待;第二步是数据搬迁。

如果想要提高I/O效率,需要将等待时间降低。因此发展出来五种I/O模型,分别是:阻塞I/O模型、非阻塞I/O模型、多路复用I/O模型、信号驱动I/O模型、异步I/O模型。其中,前四种被称为同步I/O,下面对每一种I/O模型进行详细分析。

7.2.1 阻塞I/O模型

阻塞I/O模型的通信过程示意如下图所示。

当用户进程调用了recvfrom这个系统调用,内核就开始了I/O的第一个阶段:准备数据。对于网络I/O来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞,当数据准备好时,它就会将数据从内核拷贝到用户内存,然后返回结果,用户进程才解除阻塞的状态,重新运行起来。几乎所有的开发者第一次接触到的网络编程都是从listen()send()recv()等接口开始的,这些接口都是阻塞型的。阻塞I/O模型的特性总结如下表所示。

7.2.2 非阻塞I/O模型

非阻塞I/O模型的通信过程示意如下图所示。

当用户进程发出read操作时,如果内核中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果,用户进程判断结果是一个error时,它就知道数据还没有准备好。于是它可以再次发送read操作,一旦内核中的数据准备好了,并且再次收到了用户进程的系统调用,那么它会马上将数据拷贝到用户内存,然后返回,非阻塞型接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回。非阻塞I/O模型的特性总结如下表所示。

非阻塞模式套接字与阻塞模式套接字相比,不容易使用。使用非阻塞模式套接字,需要编写更多的代码,但是,非阻塞模式套接字在控制建立多个连接、数据的收发量不均、时间不定时,具有明显优势。

7.7.3 多路复用I/O模型

多路复用I/O模型的通信过程示意如下图所示

多个进程的I/O可以注册到一个复用器(Selector)上,当用户进程调用该SelectorSelector会监听注册进来的所有I/O,如果Selector监听的所有I/O在内核缓冲区都没有可读数据,select调用进程会被阻塞,而当任一I/O在内核缓冲区中有可读数据时,select调用就会返回,而后select调用进程可以自己或通知另外的进程(注册进程)再次发起读取I/O,读取内核中准备好的数据,多个进程注册I/O后,只有一个select调用进程被阻塞。

多路复用I/O相对阻塞和非阻塞更难简单说明,所以额外解释一段,其实多路复用I/O模型和阻塞I/O模型并没有太大的不同,事实上,还更差一些,因为这里需要使用两个系统调用(selectrecvfrom),而阻塞I/O模型只有一次系统调用(recvfrom)。但是,用Selector的优势在于它可以同时处理多个连接,所以如果处理的连接数不是很多,使用select/epollWeb Server不一定比使用多线程加阻塞I/OWeb Server性能更好,可能延迟还更大,select/epoll的优势并不是对于单个连接能处理得更快,而是能处理更多的连接。多路复用I/O模型的特性总结如下表所示。

2.7.4 信号驱动I/O模型

信号驱动I/O模型的通信过程示意如下图所示。

信号驱动I/O是指进程预先告知内核,向内核注册一个信号处理函数,然后用户进程返回不阻塞,当内核数据就绪时会发送一个信号给进程,用户进程便在信号处理函数中调用I/O读取数据。从上图可以看出,实际上I/O内核拷贝到用户进程的过程还是阻塞的,信号驱动I/O并没有实现真正的异步,因为通知到进程之后,依然由进程来完成I/O操作。这和后面的异步I/O模型很容易混淆,需要理解I/O交互并结合五种I/O模型进行比较阅读。信号驱动I/O模型的特性总结如下表所示。

2.2.5 异步I/O模型

异步I/O模型的通信过程示意如下图所示

用户进程发起aio_read操作后,给内核传递与read相同的描述符、缓冲区指针、缓冲区大小三个参数及文件偏移,告诉内核当整个操作完成时,如何通知我们立刻就可以开始去做其他的事;而另一方面,从内核的角度,当它收到一个aio_read之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个信号,告诉它aio_read操作完成。

异步I/O的工作机制是:告知内核启动某个操作,并让内核在整个操作完成后通知我们,这种模型与信号驱动I/O模型的区别在于,信号驱动I/O模型是由内核通知我们何时可以启动一个I/O操作,这个I/O操作由用户自定义的信号函数来实现,而异步I/O模型由内核告知我们I/O操作何时完成。

异步I/O模型的特性总结如下表所示。

2.2.6 易混淆的概念澄清

在实际开发中,我们经常会听到同步、异步、阻塞、非阻塞这些概念,每次遇到的时候都会“蒙圈”,然后就查网上各种资料,结果越查越迷糊。大部分文章都千篇一律,没有说到本质上的区别,所以下次再碰到这些概念,印象还是比较模糊,尤其是在一些场景下觉得同步与阻塞、异步与非阻塞没什么区别,但其实这四个术语描述的还真不是一回事。

下面我们来慢慢探讨它们之间的区别与联系,在这之前,我们还会经常看到下面的组合术语。

1)同步阻塞。

2)同步非阻塞。

3)异步阻塞。

4)异步非阻塞。

在什么是同步和异步、阻塞和非阻塞的概念还没弄清楚之前,更别提上面这些组合术语了,只会让你更加困惑。

1.同步和异步

同步和异步其实是指CPU时间片的利用,主要看请求发起方对消息结果的获取是主动发起的,还是被动通知的,如下图所示。如果是请求方主动发起的,一直在等待应答结果(同步阻塞),或者可以先去处理其他事情,但要不断轮询查看发起的请求是否有应答结果(同步非阻塞),因为不管如何都要发起方主动获取消息结果,所以形式上还是同步操作。如果是由服务方通知的,也就是请求方发出请求后,要么一直等待通知(异步阻塞),要么先去干自己的事(异步非阻塞)。当事情处理完成后,服务方会主动通知请求方,它的请求已经完成,这就是异步。异步通知的方式一般通过状态改变、消息通知或者回调函数来完成,大多数时候采用的都是回调函数。

2.阻塞和非阻塞

阻塞和非阻塞在计算机的世界里,通常指针对I/O的操作,如网络I/O和磁盘I/O等。那么什么是阻塞和非阻塞呢?简单地说,就是我们调用了一个函数后,在等待这个函数返回结果之前,当前的线程是处于挂起状态还是运行状态。如果是挂起状态,就意味着当前线程什么都不能干,就等着获取结果,这就是同步阻塞;如果仍然是运行状态,就意味着当前线程是可以继续处理其他任务的,但要时不时地看一下是否有结果了,这就是同步非阻塞。具体如下图所示。

3.实际生活场景

同步、异步、阻塞和非阻塞可以组合成上面提到过的四种结果。

举个例子,比如我们去照相馆拍照,拍完照片之后,商家说需要30min左右才能洗出来照片。

1)这个时候,如果我们一直在店里面什么都不干,一直等待直到洗完照片,这个过程就叫同步阻塞。

2)当然,大部分人很少这么干,更多的是大家拿起手机开始看电视,看一会儿就会问老板洗完没,老板说没洗完,然后接着看,再过一会儿接着问,直到照片洗完,这个过程就叫同步非阻塞。

3)由于店里生意太好了,越来越多的人过来拍,店里面快没地方坐了,老板说你把手机号留下,我一会儿洗好了就打电话告诉你过来取,然后你去外面找了一个长凳开始躺着睡觉等待老板打电话,什么都不干,这个过程就叫异步阻塞(实际不应用)。

4)当然实际情况是,大家可能会先去逛街或者吃饭,或者做其他活动,这样一来,两不耽误,这个过程就叫异步非阻塞(效率最高)。

 

4.小结

从上面的描述中,我们能够看到阻塞和非阻塞通常是指在客户端发出请求后,在服务端处理这个请求的过程中,客户端本身是直接挂起等待结果,还是继续做其他的任务。而异步和同步则是对于请求结果的获取是客户端主动获取结果,还是由服务端来通知结果。从这一点来看,同步和阻塞其实描述的是两个不同角度的事情,阻塞和非阻塞指的是客户端等待消息处理时本身的状态,是挂起还是继续干别的。同步和异步指的是对于消息结果是客户端主动获取的,还是由服务端间接推送的。记住这两点关键的区别将有助于我们更好地区分和理解它们。

7.2.7 I/O模型的对比与总结

其实前四种I/O模型都是同步I/O操作,它们的区别在于第一阶段,而第二阶段是一样的:在数据从内核拷贝到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用。

有人可能会说,NIONon-Blocking I/O)并没有被阻塞。这里有个非常“狡猾”的地方,定义中所指的“I/O Operation”是指真实的I/O操作。NIO在执行recvfrom的时候,如果内核(Kernel)的数据没有准备好,这时候不会阻塞进程。但是,当内核(Kernel)中数据准备好的时候,recvfrom会将数据从内核(Kernel)拷贝到用户内存中,这个时候进程就被阻塞了。在这段时间内,进程是被阻塞的。下图是各I/O模型的阻塞状态对比。

从上图可以看出,阻塞程度:阻塞I/O>非阻塞I/O>多路复用I/O>信号驱动I/O>异步I/O,效率是由低到高的。最后,再看一下下表,从多维度总结了各I/O模型之间的差异,可以加深理解。

7.3 从BIONIO的演进

下表总结了Java BIOBlocking I/O)和NIONon-Blocking I/O)之间的主要差异。

7.3.1 面向流与面向缓冲

Java NIOBIO之间第一个最大的区别是,BIO是面向流的,NIO是面向缓冲区的。Java BIO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程的灵活性。但是,还需要检查该缓冲区是否包含所有需要处理的数据。而且,要确保当更多的数据读入缓冲区时,不能覆盖缓冲区里尚未处理的数据。

7.3.2 阻塞与非阻塞

Java BIO的各种流是阻塞的。这意味着,当一个线程调用read()write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情。Java NIO的非阻塞模式,是一个线程从某通道(Channel)发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用,就什么都不会获取,而不是保持线程阻塞,所以直到数据变成可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入某通道一些数据,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞I/O的空闲时间用于在其他通道上执行I/O操作,所以一个单独的线程现在可以管理多个I/O通道。

7.3.3 选择器在I/O中的应用

Java NIO的选择器(Selector)允许一个单独的线程监视多个输入通道,可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制使一个单独的线程很容易管理多个通道。

7.3.4 NIOBIO如何影响应用程序的设计

无论选择BIO还是NIO工具箱,都可能会影响应用程序设计的以下几个方面。

1)对NIOBIO类的API调用。

2)数据处理逻辑。

3)用来处理数据的线程数。

1API调用

当然,使用NIOAPI调用看起来与使用BIO时有所不同,但这并不意外,因为并不是仅从一个InputStream逐字节读取,而是数据必须先读入缓冲区再处理。

2.数据处理

使用纯粹的NIO设计相较BIO设计,数据处理也会受到影响。

BIO设计中,我们从InputStreamReader逐字节读取数据。假设你正在处理一个基于行的文本数据流,有如下一段文本。

该文本行的流可以这样处理。

请注意处理状态由程序执行多久决定。换句话说,一旦reader.readLine()方法返回,你就知道文本行肯定已读完,readline()阻塞直到整行读完,这就是原因。你也知道此行包含名称;同样,第二个readline()调用返回的时候,你知道这行包含年龄。正如你可以看到,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退数据(大多如此)。下图也说明了这条原则。

Java BIO从一个阻塞的流中读数据,而一个NIO的实现会有所不同,下面是一个简单的例子。

注意第二行,从通道读取字节到ByteBuffer。当这个方法调用返回时,你不知道你所需的所有数据是否在缓冲区内。你所知道的是,该缓冲区包含一些字节,这使得处理有点困难。

假设第一次read(buffer)调用后,读入缓冲区的数据只有半行,例如,“NameAn”,你能处理数据吗?显然不能,需要等待,直到整行数据读入缓存。在此之前,对数据的任何处理都毫无意义。

所以,你怎么知道是否该缓冲区包含足够的数据可以处理呢?好了,你不知道。发现的方法只能查看缓冲区中的数据。其结果是,在你知道所有数据都在缓冲区里之前,你必须检查几次缓冲区的数据。这不仅效率低下,而且会使程序设计方案杂乱不堪。例如:

bufferFull()方法必须跟踪有多少数据读入缓冲区,并返回真或假,这取决于缓冲区是否已满。换句话说,如果缓冲区准备好被处理,那么表示缓冲区已满。

bufferFull()方法扫描缓冲区,但必须保持与bufferFull()方法被调用之前状态相同。如果没有,下一个读入缓冲区的数据可能无法读到正确的位置。虽然这是不可能的,但却是需要注意的又一个问题。

如果缓冲区已满,它可以被处理。如果它不满,并且在实际案例中有意义,或许能处理其中的部分数据,但是许多情况下并非如此。下图展示了“缓冲区数据循环就绪”。

3.设置处理线程数

NIO可以只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。

如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。同样,如果需要维持许多打开的连接,如P2P网络中,使用一个单独的线程来管理所有出站连接,可能是一个优势。一个线程有多个连接的设计方案如下图所示。

1Java NIO:单线程管理多个连接。如果有少量的连接使用非常高的带宽,一次发送大量的数据,也许用典型的I/O服务器实现可能非常契合。下图说明了一个典型的I/O服务器设计。

2Java BIO:一个典型的I/O服务器设计。一个连接只用一个线程来处理。

7.4 Java AIO详解

JDK 1.7NIO2)才是实现真正的异步AIOAsynchronous I/O)、把I/O读写操作完全交给操作系统,学习了Linux Epoll模式,下面我们来做一些演示。

7.4.1 AIO基本原理

Java AIO处理API中,重要的三个类分别是:AsynchronousServerSocketChannel(服务端)、AsynchronousSocketChannel(客户端)及CompletionHandler(用户处理器)。CompletionHandler接口实现应用程序向操作系统发起I/O请求,当完成后处理具体逻辑,否则做自己该做的事情,“真正”的异步I/O需要操作系统更强的支持。

在多路复用I/O模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步I/O模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在I/O完成后通知用户线程直接使用即可。异步I/O模型使用Proactor设计模式实现这一机制,如下图所示。

7.4.2 AIO初体验

我们基于AIO先写一段简单的代码,来感受一下服务端和客户端的交互过程,同时也体验一下API的使用。先来看服务端代码。

package cn.edu.bbc.computer;

 

import java.net.InetSocketAddress;

import java.nio.channels.AsynchronousChannelGroup;

import java.nio.channels.AsynchronousServerSocketChannel;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

 

/**

 * @Auther: oliver

 * @Date:2021/9/25

 * @Description:

 */

public class AIOServer {

    // 线程池

    private ExecutorService executorService;

    // AIO工作的线程组

    private AsynchronousChannelGroup threadGroup;

    // 服务器通道

    public AsynchronousServerSocketChannel assc;

    public AIOServer(int port) {

        try {

            // 创建一个可伸缩的线程池

            executorService = Executors.newCachedThreadPool();

            // 创建干活的线程组,负责连接上之前的所有的琐碎的工作。

            threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);

            // 创建服务器通道,并且设置为这个通道干活的线程组

            assc = AsynchronousServerSocketChannel.open(threadGroup);

            // 绑定端口

            assc.bind(new InetSocketAddress(port));

            System.out.println("服务器启动!port:" + port);

            // 进行阻塞,实际上并没有卡在这。

            assc.accept(this, new ServerCompletionHandler());

            // 阻塞在这不让服务停止,因为accept不会阻塞。

            Thread.sleep(Integer.MAX_VALUE);

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    public static void main(String[] args) {

        AIOServer server = new AIOServer(8083);

    }

}

import java.nio.ByteBuffer;

import java.nio.channels.AsynchronousSocketChannel;

import java.nio.channels.CompletionHandler;

 

/**

 * @Auther: oliver

 * @Date:2021/9/25

 * @Description:

 */

public class ServerCompletionHandler implements CompletionHandler<AsynchronousSocketChannel,AIOServer> {

    /**

     * 功能描述: 建立连接之后才会调用,主要用来读写数据给客户端。

     * @Auther: oliver

     * @Date:2021/9/25

     * @Description:

    */

    @Override

    public void completed(AsynchronousSocketChannel result, AIOServer attachment) {

        //这个对象被使用了,accept方法被执行过了,如果不设置一个本类对象去执行任务的话,不重新监听的话,新的请求绝对进不来,所以要调用一下 server中 这句话的等效语句assc.accept(this,new ServerCompletionHandler());

        //类似递归,让这个类重新处于监听状态,处理下一个请求,没有new新对象。各个对象之间互不干扰。

        attachment.assc.accept(attachment,this);

        //读取数据

        read(result);

    }

    /**

     * 功能描述: 具体的读取逻辑

     * @Auther: oliver

     * @Date:2021/9/25

     * @Description:

     */

    private void read(AsynchronousSocketChannel asc) {

        //创建缓冲区

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        //拿出通道来执行读取的业务

        asc.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {

            @Override

            public void completed(Integer result, ByteBuffer attachment) {

                //进行读取之前重新设置poslimit

                attachment.flip();

                //打印获得的字节数

                System.out.println("resultSize = " + result);

                //获取读取的数据

                String body = new String(attachment.array()).trim();

                System.out.println("server accept body = " + body);

                String resp = "服务器收到你发来的数据:"+ body;

                write(asc,resp);

            }

 

            @Override

            public void failed(Throwable exc, ByteBuffer attachment) {

                exc.printStackTrace();

            }

        });

    }

    /**

     * 功能描述: 写回客户端,读写方法都是不阻塞的。

     * @Auther: oliver

     * @Date:2021/9/25

     * @Description:

     */

    private void write(AsynchronousSocketChannel asc, String resp) {

         try {

             ByteBuffer buffer = ByteBuffer.allocate(1024);

             buffer.put(resp.getBytes());

             buffer.flip();

             //get写不写都行

             asc.write(buffer).get();

         }catch (Exception e){

             e.printStackTrace();

         }

    }

 

    @Override

    public void failed(Throwable exc, AIOServer attachment) {

        exc.printStackTrace();

    }

}

 

上述代码的主要功能就是开启一个监听端口,然后在CompletionHandler中处理接收到消息以后的逻辑,将接收到的信息再输出到客户端。下面来看客户端的代码。

package cn.edu.bbc.computer;

 

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.AsynchronousSocketChannel;

 

/**

 * @Auther: oliver

 * @Date:2021/9/25

 * @Description:

 */

public class Client implements Runnable {

    private AsynchronousSocketChannel asc;

 

    public Client() throws IOException {

        asc = AsynchronousSocketChannel.open();

    }

 

    public void connect() {

        asc.connect(new InetSocketAddress("127.0.0.1", 8088));

    }

 

    public void write(String request) {

        try {

            asc.write(ByteBuffer.wrap(request.getBytes())).get();

            read();

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

 

    private void read() {

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try {

            // 把数据读入缓冲区中

            asc.read(buffer).get();

            // 切换缓冲区的读取模式

            buffer.flip();

            // 构造一个字节数组接受缓冲区中的数据

            byte[] respBytes = new byte[buffer.remaining()];

            buffer.get(respBytes);

            System.out.println("客户端接收服务器端:" + new String(respBytes, "utf-8").trim());

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

 

    /**

     * 功能描述: 简单让线程不停止。

     *

     * @Auther: oliver

     * @Date:2021/9/25

     * @Description:

     */

    @Override

    public void run() {

        while (true) {

 

        }

    }

 

    public static void main(String[] args) throws IOException, InterruptedException {

        Client c1 = new Client();

        c1.connect();

        Client c2 = new Client();

        c2.connect();

        Client c3 = new Client();

        c3.connect();

 

        new Thread(c1, "c1").start();

        new Thread(c2, "c2").start();

        new Thread(c3, "c3").start();

 

        Thread.sleep(1000);

 

        c1.write("我是Client1,请复述我传送的数据!");

        c2.write("我是Client2,请复述我传送的数据!");

        c3.write("我是Client3,请复述我传送的数据!");

    }

}

客户端的代码的主要功能是发送一串字符到服务端。同时,在CompletionHandler接口处理服务端发送过来的结果。

服务端执行结果如下图所示。

客户端执行结果如下图所示。

运行代码后,我们会发现不管是客户端还是服务端,其处理接收消息的逻辑都是异步操作,和BIONIOAPI使用有根本上的区别。

 

 


0 条 查看最新 评论

没有评论
暂时无法发表评论