BIO 有的称之为basic(基本) IO,有的称之为block(阻塞) IO,主要应用于文件IO 和网络IO,这里不再说文件IO, 大家对此都非常熟悉,本次课程主要讲解网络IO。
在JDK1.4 之前,我们建立网络连接的时候只能采用BIO,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket 来对服务端进行通信,默认情况下服务端需要对每个请求建立一个线程等待请求,而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝,如果有的话,客户端线程会等待请求结束后才继续执行,这就是阻塞式IO。
接下来通过一个例子复习回顾一下BIO 的基本用法(基于TCP)。
package com.zdw.bio;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Create By zdw on 2019/7/19
* 该类是BIO的服务端
*/
public class BIOServer {
public static void main(String[] args) throws Exception {
//创建一个ServerSocket来监听9999端口
ServerSocket serverSocket = new ServerSocket(9999);
while (true){
//监听客户端,得到连接请求
Socket socket = serverSocket.accept();//阻塞
//从连接中取出输入流来获取客户端消息
InputStream inputStream = socket.getInputStream();//阻塞
//构建缓冲区
byte[] bytes = new byte[1024];
//把输入流中的数据读到缓冲区中
inputStream.read(bytes);
//获取客户端的IP地址
String clientIp = socket.getInetAddress().getHostAddress();
System.out.println(clientIp+":客户端发送了消息:"+new String(bytes));
//从连接中取出输出流,并给客户端回应消息
OutputStream outputStream = socket.getOutputStream();
outputStream.write("我也是啊".getBytes());
//关闭连接
socket.close();
}
}
}
上述代码编写了一个服务器端程序,绑定端口号9999,accept 方法用来监听客户端连接,如果没有客户端连接,就一直等待,程序会阻塞到这里。
package com.zdw.bio;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* Create By zdw on 2019/7/19
* BIO编程的客户端
*/
public class BIOClient {
public static void main(String[] args) throws IOException {
while(true){
//创建连接对象Socket
Socket socket = new Socket("127.0.0.1",9999);
//从连接中获取输出流,并且像服务端写消息
OutputStream outputStream = socket.getOutputStream();
System.out.println("------------------请输入你的想说的话:");
Scanner scanner = new Scanner(System.in);
String msg = scanner.nextLine();//得到输入的消息
//往连接中写入消息
outputStream.write(msg.getBytes());
//从连接中取出输入流,并接收服务端消息
InputStream inputStream = socket.getInputStream();//阻塞
//构建缓冲区
byte[] bytes = new byte[1024];
//把服务器的回应消息写入到缓冲中
inputStream.read(bytes);
System.out.println("对方的回应是:"+new String(bytes));
//关闭连接
socket.close();
}
}
}
上述代码编写了一个客户端程序,通过9999 端口连接服务器端,getInputStream 方法用来等待服务器端返回数据,如果没有返回,就一直等待,程序会阻塞到这里。
运行效果如下图所示:
从上述可以看出,BIO编程的效率是很低下,它会阻塞,一直等待服务器进行处理,如果服务端的资源没有准备好,那就会一直处于等待中,浪费了资源。
java.nio 全称java non-blocking IO,是指JDK 提供的新API。从JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为NIO(也称为New IO)。新增了许多用于处理输入输出的类,这些类都被放在java.nio 包及子包下,并且对原java.io 包中的很多类进行改写,新增了满足NIO 的功能。
NIO 和BIO 有着相同的目的和作用,但是它们的实现方式完全不同,BIO 以流的方式处理数据,而NIO 以块的方式处理数据,块I/O 的效率比流I/O 高很多。另外,NIO 是非阻塞式的,这一点跟BIO 也很不相同,使用它可以提供非阻塞式的高伸缩性网络。
NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区),,Selector(选择器)。传统的BIO基于字节流和字符流进行操作,而NIO 基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
Buffer(缓冲区):实际上一个容器,是一个特殊的数组。缓冲区对象内置了一些机制,能够记录和跟踪缓冲区的状态变化情况。Channel提供从文件、网络读取数据的通道,但是读取和写入的数据都必须经过Buffer,如下图所示:
在NIO 中,Buffer 是一个顶层父类,它是一个抽象类,常用的Buffer 子类有:
ByteBuffer, 存储字节数据到缓冲区
ShortBuffer, 存储字符串数据到缓冲区 CharBuffer, 存储字符数据到缓冲区
IntBuffer, 存储整数数据到缓冲区
LongBuffer, 存储长整型数据到缓冲区
DoubleBuffer, 存储小数到缓冲区
FloatBuffer, 存储小数到缓冲区
对于Java 中的基本数据类型,都有一个Buffer 类型与之相对应,最常用的自然是ByteBuffer 类(二进制数据),该类的主要方法如下所示:
public abstract ByteBuffer put(byte[] b); 存储字节数据到缓冲区
public abstract byte[] get(); 从缓冲区获得字节数据
public final byte[] array(); 把缓冲区数据转换成字节数组
public static ByteBuffer allocate(int capacity); 设置缓冲区的初始容量
public static ByteBuffer wrap(byte[] array); 把一个现成的字节数组放到缓冲区中使用
public final Buffer flip(); 翻转缓冲区,重置位置到初始位置
通道(Channel):类似于BIO 中的stream(流),例如FileInputStream 对象,用来建立到目标(文件,网络套接字,硬件设备等)的一个连接,但是需要注意:BIO 中的stream 是单向的,例如FileInputStream 对象只能进行读取数据的操作,而NIO 中的通道(Channel)是双向的,既可以用来进行读操作,也可以用来进行写操作。
常用的Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel。FileChannel 用于文件的数据读写,DatagramChannel 用于UDP 的数据读写,ServerSocketChannel 和SocketChannel 用于TCP 的数据读写。
这里我们先讲解FileChannel 类,该类主要用来对本地文件进行IO 操作,主要方法如下所示:
public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
案例:接下来我们通过NIO 实现几个案例,分别演示一下本地文件的读、写和复制操作,并和BIO 做个对比。
1、往本地文件中写数据
package com.zdw.nio;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* Create By zdw on 2019/7/19
*/
public class FileChannelTest {
public static void main(String[] args) {
writeToLocalFile();
}
//把数据写到本地文件中
public static void writeToLocalFile(){
String msg = "嗨,你好啊,今天是周五了";
try {
//创建本地目录
String fileFloader = "d:/word/test/";
File floader = new File(fileFloader);
if(!floader.exists()){
floader.mkdirs();
}
//创建本地文件
String filePath = fileFloader+"hello.txt";
File file = new File(filePath);
//判断本地文件是否存在
if(!file.exists()){
file.createNewFile();//不存在就创建
}
//构建本地文件的文件输出流
FileOutputStream fileOutputStream = new FileOutputStream(file);
//得到该文件输出流的通道
FileChannel fileChannel = fileOutputStream.getChannel();
//构建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//把消息存储到缓冲区中
buffer.put(msg.getBytes());
//翻转缓冲区,重置位置到初始位置
buffer.flip();
//把缓冲区的数据写到文件通道中
fileChannel.write(buffer);
//释放资源
fileOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
NIO中的通道是从输出流对象里通过getChannel 方法获取到的,该通道是双向的,既可以读,又可以写。在往通道里写数据之前,必须通过put 方法把数据存到ByteBuffer 中,然后通过通道的write 方法写数据。在write 之前,需要调用flip 方法翻转缓冲区,把内部重置到初始位置,这样在接下来写数据时才能把所有数据写到通道里。
2、从本地文件中读取数据
//把本地文件中的数据读出来
public static void readFromLocalFile(){
try {
//本地文件路径
String localFilePath = "D:/word/test/hello.txt";
//得到文件的输入流
FileInputStream fileInputStream = new FileInputStream(localFilePath);
//得到本地文件通道
FileChannel fileChannel = fileInputStream.getChannel();
//构建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//把输入流中数据读到缓冲区中
fileChannel.read(byteBuffer);
//直接输出缓冲区中的数据
System.out.println(new String(byteBuffer.array()));
//释放资源
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
3、复制文件
//复制文件
public static void copyFile() throws Exception {
//输出到本地文件路径
String localFilePath_out = "D:/word/test/out.txt";
FileOutputStream fileOutputStream = new FileOutputStream(localFilePath_out);
//要复制的文件路径
String localFilePath_in = "D:/word/test/hello.txt";
FileInputStream fileInputStream = new FileInputStream(localFilePath_in);
//从两个流中取出对应的通道
FileChannel outputStreamChannel = fileOutputStream.getChannel();//输出文件的通道
FileChannel inputStreamChannel = fileInputStream.getChannel();//输入文件的通道
//通过api直接把输入流中的 数据 复制到输出流的文件中
//outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size());//从哪个通道里面复制对应的数据到目标文件中
inputStreamChannel.transferTo(0,inputStreamChannel.size(),outputStreamChannel);//把该通道中的数据复制到对应的目录通道中
//关闭连接
outputStreamChannel.close();
inputStreamChannel.close();
}
前面在进行文件IO 时用到的FileChannel 并不支持非阻塞操作,学习NIO 主要就是进行网络IO,Java NIO 中的网络通道是非阻塞IO 的实现,基于事件驱动,非常适用于服务器需要维持大量连接,但是数据交换量不大的情况,例如一些即时通信的服务等等....
在Java 中编写Socket 服务器,通常有以下几种模式:
一个客户端连接用一个线程,优点:程序编写简单;缺点:如果连接非常多,分配的线程也会非常多,服务器可能会因为资源耗尽而崩溃。
把每一个客户端连接交给一个拥有固定数量线程的连接池,优点:程序编写相对简单,可以处理大量的连接。缺点:线程的开销非常大,连接如果非常多,排队现象会比较严重。 使用Java 的NIO,用非阻塞的IO 方式处理。这种模式可以用一个线程,处理大量的客户端连接。
NIO中的核心类和相关API:
1. Selector(选择器),能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
该类的常用方法如下所示:
public static Selector open(),得到一个选择器对象
public int select(long timeout),监控所有注册的通道,当其中有IO 操作可以进行时,将对应的SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
public Set
2. SelectionKey,代表了Selector 和网络通道的注册关系,一共四种:
int OP_ACCEPT:有新的网络连接可以accept,值为16
int OP_CONNECT:代表连接已经建立,值为8
int OP_READ 和int OP_WRITE:代表了读、写操作,值为1 和4
该类的常用方法如下所示:
public abstract Selector selector(),得到与之关联的Selector 对象
public abstract SelectableChannel channel(),得到与之关联的通道
public final Object attachment(),得到与之关联的共享数据
public abstract SelectionKey interestOps(int ops),设置或改变监听事件
public final boolean isAcceptable(),是否可以accept
public final boolean isReadable(),是否可以读
public final boolean isWritable(),是否可以写
3. ServerSocketChannel,用来在服务器端监听新的客户端Socket 连接,常用方法如下所示:
public static ServerSocketChannel open(),得到一个ServerSocketChannel 通道
public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号 public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false 表示采用非阻塞模式
public SocketChannel accept(),接受一个连接,返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件
4. SocketChannel,网络IO 通道,具体负责进行读写操作。
NIO 总是把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。常用方法如下所示:
public static SocketChannel open(),得到一个SocketChannel 通道
public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false 表示采用非阻塞模式
public boolean connect(SocketAddress remote),连接服务器
public boolean finishConnect(),如果上面的方法连接失败,接下来就要通过该方法完成连接操作
public int write(ByteBuffer src),往通道里写数据
public int read(ByteBuffer dst),从通道里读数据
public final SelectionKey register(Selector sel, int ops, Object att),注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
public final void close(),关闭通道
API 学习完毕后,接下来我们使用NIO 开发一个入门案例,实现服务器端和客户端之间的数据通信(非阻塞)。
NIO的服务端
package com.zdw.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* Create By zdw on 2019/7/22
* NIO的网络服务端
*/
public class NIOServer {
public static void main(String[] args) throws Exception{
//1\创建用来在服务器端监听新的客户端Socket 连接
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2\创建一个监听事件的选择器
Selector selector = Selector.open();
//3\绑定一个端口号
serverSocketChannel.bind(new InetSocketAddress(9999));
//4\设置为非阻塞方式
serverSocketChannel.configureBlocking(false);
//5\把ServerSocketChannel对象注册给Selector,并设置监听事件为 连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//6\业务操作
while(true){
//6.1\监控所有注册的通道,当其中有IO 操作可以进行时,将对应的SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
int select = selector.select(2000);//该方法的返回值是监控到的客户端的个数
if(select==0){//说明没有客户端连接,这个时候nio的优势体现出来了,我们可以干点别的事
System.out.println("Server:没有客户端来连接我,我没事干,就可以在这里做点想做的事情了");
continue;//继续下一次循环
}
//6.2\得到所有的SelectionKey,得到通道里面的事件
Set selectionKeys = selector.selectedKeys();
Iterator keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
//遍历每一个SelectionKey。判断通道里面的事件
SelectionKey selectionKey = keyIterator.next();
if(selectionKey.isAcceptable()){//判断是不是连接事件
System.out.println("OP_ACCEPT");
//接受客户端连接,得到通道
SocketChannel socketChannel = serverSocketChannel.accept();
//设置客户端通道为非阻塞方式
socketChannel.configureBlocking(false);
//把客户端通道注册到选择器,并设置监听事件为 读数据 事件,设置共享的数据(只是一个空的ByteBuffer)
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if(selectionKey.isReadable()){//判断是不是读取客户端数据事件
System.out.println("OP_READ");
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();//得到通道
//得到客户端的共享数据
ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
//把共享数据读取到通道中
socketChannel.read(byteBuffer);
System.out.println("客户端发来数据:"+new String(byteBuffer.array()));
}
}
//6.3\手动从当前的集合移除key,避免重复处理
keyIterator.remove();
}
}
}
上面代码用NIO 实现了一个服务器端程序,能不断接受客户端连接并读取客户端发过来的数据。
NIO客户端
package com.zdw.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* Create By zdw on 2019/7/22
* NIO的网络客户端
*/
public class NIOClient {
public static void main(String[] args) throws Exception {
//1、得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//2、设置为非阻塞方式
socketChannel.configureBlocking(false);
//3、得到服务端的IP地址和端口号
InetSocketAddress address = new InetSocketAddress("127.0.0.1",9999);
//4、连接服务器端
if(!socketChannel.connect(address)){//表示没有连接上
while(!socketChannel.finishConnect()){//finishConnect表示再次连接,如果没有连接上,那么就可以在这里处理一些其他的业务逻辑
System.out.println("Client连接服务端的同时,我还可以处理一些其他的事情");
}
}
//向缓冲区中存入要发送的数据
String msg = "Hello,NIOServer";
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
//发送数据
socketChannel.write(byteBuffer);
//这是为了客户端连接不断开
System.in.read();
}
}
上面代码通过NIO 实现了一个客户端程序,连接上服务器端后发送了一条数据。演示效果如下:
如果我们只启动客户端,也可以看到客户端的非阻塞效果,不过由于我们没有进行异常处理,所以会出现异常,效果如下:
刚才我们通过NIO 实现了一个入门案例,基本了解了NIO 的工作方式和运行流程,接下来我们用NIO 实现一个多人聊天案例,具体代码如下所示:
服务端ChatServer
package com.zdw.chat;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
/**
* Create By zdw on 2019/7/22
* 网络聊天案例的服务端
*/
public class ChatServer {
private ServerSocketChannel serverSocketChannel;//监听通道,老大
private Selector selector;//选择器对象
private static final int PORT=9999;//监听的端口号
public ChatServer(){//构造方法,得到一些必需的对象
try{
//1、得到监听通道对象
serverSocketChannel = ServerSocketChannel.open();
//2、得到选择器对象
selector = Selector.open();
//3、设置为非阻塞方式
serverSocketChannel.configureBlocking(false);
//4、绑定端口
serverSocketChannel.bind(new InetSocketAddress(PORT));
//5、将监听通道注册到选择器,并设置为监听连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//调用方法,打印日志信息
printInfo("Chat Server is Ready.........");
}catch (Exception e){
e.printStackTrace();
}
}
//6、业务逻辑处理的方法
public void start() throws Exception{
while (true){
//6.1、不停的监听客户端通道,看是否有连接,如果有连接就进行业务处理,否则就干点别的事情
int select = selector.select(2000);//2秒钟就监听一次,得到的是连接个数
if(select==0){
System.out.println("Server:没有客户端找我, 我就干别的事情");
continue;
}
//6.2、得到所有的SelectionKey,得到通道里面的事件
Iterator keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()){
//得到下一个连接关系,进行业务处理
SelectionKey selectionKey = keyIterator.next();
if(selectionKey.isAcceptable()){//如果是连接请求事件
//接收连接请求,得到通道
SocketChannel socketChannel = serverSocketChannel.accept();
//设置为非阻塞
socketChannel.configureBlocking(false);
//把通道注册到选择器中,并设置监听事件为 读数据 事件
socketChannel.register(selector,SelectionKey.OP_READ);
//打印提示信息,参照QQ的好友上线功能,提示的是客户端的IP和端口号
System.out.println(socketChannel.getRemoteAddress().toString().substring(1)+":上线了");
}
if(selectionKey.isReadable()){//如果是读数据事件
//读取客户端发送来的消息,并把接收到的消息广播到其他客户端,其他客户端都能收到消息
SocketChannel socketChannel = (SocketChannel)selectionKey.channel();//得到客户端通道
//定义缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//把客户端消息读到缓冲区中
int count = socketChannel.read(byteBuffer);//返回的是读到数据的长度
if(count>0){//读到了数据
//把数据转成字符串
String msg = new String(byteBuffer.array());
//控制台打印消息
printInfo(msg);
//广播消息到其他客户端
System.out.println("服务器端广播了消息.........");
//得到客户端所有的通道,判断客户端的通道不是发送消息给服务端的通道
for(SelectionKey key : selector.keys()){
Channel targetChannel = key.channel();//得到客户端的通道
//判断该客户端通道类型是不是SocketChannel,并且该客户端不是发送消息的客户端
if(targetChannel instanceof SocketChannel && targetChannel!=socketChannel){
SocketChannel destChannel = (SocketChannel) targetChannel;
//把消息放到缓冲区中
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//把缓冲区数据写进通道中
destChannel.write(buffer);
}
}
}
}
//6.3处理完本次的连接关系之后,一定要把key从集合中移除,避免重复处理
keyIterator.remove();
}
}
}
private void printInfo(String str) { //往控制台打印消息
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
}
//main方法用来启动服务器端
public static void main(String[] args) throws Exception{
new ChatServer().start();
}
}
上述代码通过NIO 编写了一个聊天程序的服务端,可以接受客户端发来的数据,并能把数据广播给其他客户端。
客户端ChatClient
package com.zdw.chat;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* Create By zdw on 2019/7/22
* 网络聊天客户端程序
*/
public class ChatClient {
private SocketChannel socketChannel;//客户端网络通道
private static final String ADDRESS = "127.0.0.1";//服务器端IP
private static final int PORT=9999;//服务器端端口号
private String username;//客户端的聊天用户名
public ChatClient() throws Exception {//构造方法中得到一些必须的对象
//1、得到客户端通道
socketChannel = SocketChannel.open();
//2、设置为非阻塞方法
socketChannel.configureBlocking(false);
//3、提供服务器端的IP地址和端口号
InetSocketAddress address = new InetSocketAddress(ADDRESS,PORT);
//4、连接服务器端,这里会不停的请求服务器端,是非阻塞的,当连接不上的时候可以做点其他的事情
if(!socketChannel.connect(address)){
//如果没有连接上,就需要不断的重新调用finishConnect进行连接
while(!socketChannel.finishConnect()){
System.out.println("Client客户端在连接过程中,我可以干点别的事情");
}
}
//5、得到客户端的IP地址和端口号,作为聊天用户名
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println("----------Client("+username+") is Ready-----------");
}
//向服务器端发送数据
public void sendMsgToServer(String msg) throws Exception{
//假设当客户端发送的消息是bye的时候,就关闭当前的客户端连接
if("bye".equalsIgnoreCase(msg)){
socketChannel.close();
return;
}
//向服务器端发送消息,拼装消息
msg = username+"说了:"+msg;
//发送数据
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
}
//从服务器端接收收取
public void receiveMsgFromServer() throws Exception {
//得到缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//把服务器端返回的数据读到缓冲区
int read = socketChannel.read(byteBuffer);
if(read>0){
//打印消息到控制台
System.out.println(new String(byteBuffer.array()).trim());
}
}
}
上述代码通过NIO 编写了一个聊天程序的客户端,可以向服务器端发送数据,并能接收服务器广播的数据。
客户端启动类ChatClientStart
package com.zdw.chat;
import java.util.Scanner;
/**
* Create By zdw on 2019/7/22
* 聊天程序客户端启动类,启用多线程进行接收服务端消息
*/
public class ChatClientStart {
public static void main(String[] args) throws Exception {
ChatClient chatClient = new ChatClient();
//开启线程,接收客户端消息
new Thread(){
@Override
public void run() {
while(true){
try{
chatClient.receiveMsgFromServer();
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}.start();
//控制台录入消息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String msg=scanner.nextLine();
chatClient.sendMsgToServer(msg);//发送消息到服务器端
}
}
}
上述代码运行了聊天程序的客户端,并在主线程中发送数据,在另一个线程中不断接收服务器端的广播数据,该代码运行一次就是一个聊天客户端,可以同时运行多个聊天客户端,聊天效果如下图所示:
服务端:
客户端01:
客户端02:
客户端03:
JDK 7 引入了Asynchronous I/O,即AIO。在进行I/O 编程中,常用到两种模式:Reactor和Proactor。Java 的NIO 就是Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
AIO 即NIO2.0,叫做异步不阻塞的IO。AIO 引入异步通道的概念,采用了Proactor 模式,简化了程序编写,一个有效的请求才启动一个线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。 目前AIO 还没有广泛应用,并且也不是本课程的重点内容,这里暂不做讲解。
IO 的方式通常分为几种:同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。
BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS 参与并发操作,编程比较复杂,JDK7 开始支持。
举个例子再理解一下:
同步阻塞:你到饭馆点餐,然后在那等着,啥都干不了,饭馆没做好,你就必须等着!
同步非阻塞:你在饭馆点完餐,就去玩儿了。不过玩一会儿,就回饭馆问一声:好了没啊!
异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心玩儿就可以了,类似于现在的外卖。
对比总结 | BIO | NIO | AIO |
IO方式 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
API使用难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |