Java共支持3种网络编程的I/O模型:BIO、NIO、AIO
BIO:
同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
NIO:
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理
AIO:
异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用
BIO(Blocking I/O)就是传统的Java IO编程,其相关的类和接口在java.io
包下。BIO是同步阻塞的,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)
BIO编程流程的梳理:
服务端:
public class Server {
public static void main(String[] args) throws IOException {
System.out.println("===服务端启动===");
// 1.定义一个ServerSocket对象进行服务端的端口注册
ServerSocket ss = new ServerSocket(9999);
// 2.监听客户端的Socket连接请求
Socket socket = ss.accept();
// 3.从Socket管道中得到一个字节输入流对象
InputStream is = socket.getInputStream();
// 4.把字节输入流包装成一个缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null) {
System.out.println("服务端接收到:" + msg);
}
}
}
客户端:
public class Client {
public static void main(String[] args) throws IOException {
System.out.println("===客户端启动===");
// 1.创建Socket对象请求服务端的连接
Socket socket = new Socket("127.0.0.1", 9999);
// 2.从Socket对象中获取一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把字节输出流包装成一个打印流
PrintStream ps = new PrintStream(os);
ps.println("Hello World!服务端,你好!");
ps.flush();
}
}
小结:
在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态
伪异步I/O采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable
线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机
客户端:
public class Client {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请说:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
线程池处理类:
public class SocketServerPoolHandler {
/**
* 1.创建一个线程池的成员变量用于存储一个线程池对象
*/
private ExecutorService executorService;
/**
* 2.创建这个类的时候就需要初始化线程池对象
*/
public SocketServerPoolHandler(int maxThreadNum, int queueSize) {
executorService = new ThreadPoolExecutor(3, maxThreadNum,
120, TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueSize));
}
/**
* 3.提供一个方法来提交任务给线程池的任务队列来暂存,等着线程池来处理
*/
public void execute(Runnable target) {
executorService.execute(target);
}
}
服务端:
public class Server {
public static void main(String[] args) {
try {
// 1.注册端口
ServerSocket ss = new ServerSocket(9999);
// 2.定义一个循环接收客户端的Socket连接请求
// 初始化一个线程池对象
SocketServerPoolHandler poolHandler = new SocketServerPoolHandler(3, 10);
while (true) {
Socket socket = ss.accept();
// 3.把Socket对象交给一个线程池进行处理
// 把Socket封装成一个任务对象交给线程池处理
Runnable target = new ServerRunnableTarget(socket);
poolHandler.execute(target);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ServerRunnableTarget implements Runnable {
private Socket socket;
public ServerRunnableTarget(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
// 处理接收的客户端Socket通信需求
try {
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null) {
System.out.println("服务端接收到:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
启动服务端并启动多个客户端发送消息,由于核心线程数=最大线程数=3,当客户端数>3时,客户端的Socket任务会到线程池的阻塞队列中等待,关闭客户端,当客户端数<=3时,Socket任务将会被服务端处理
多线程BIO通信模型图:
socket.read()
时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式java.nio
包及子包下,并且对原java.io
包中的很多类进行改写NIO | BIO |
---|---|
面向缓冲区(Buffer) | 面向流(Stream) |
非阻塞(Non Blocking IO) | 阻塞IO(Blocking IO) |
选择器(Selector) |
NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
Buffer缓冲区:
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API更加容易操作和管理
Channel(通道):
Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写
Selector选择器:
Selector是一个Java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率
Buffer是一个用于特定基本数据类型的容器。由java.nio
包定义的,所有缓冲区都是Buffer抽象类的子类。Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的
Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同,有以下Buffer常用子类:
上述Buffer类都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个Buffer对象:
static XxxBuffer allocate(int capacity) //创建一个容量为capacity的XxxBuffer对象
mark()
方法指定Buffer中一个特定的position,之后可以通过调用reset()
方法恢复到这个positionBuffer clear() //清空缓冲区并返回对缓冲区的引用(缓冲区中的数据依然存在,但是处于被遗忘状态)
Buffer flip() //为将缓冲区的界限设置为当前位置,并将当前位置重置为0
int capacity() //返回Buffer的capacity大小
boolean hasRemaining() //判断缓冲区中是否还有元素
int limit() //返回Buffer的界限(limit)的位置
Buffer limit(int n) //将设置缓冲区界限为n,并返回一个具有新limit的缓冲区对象
Buffer mark() //对缓冲区设置标记
int position() //返回缓冲区的当前位置position
Buffer position(int n) //将设置缓冲区的当前位置为n,并返回修改后的Buffer对象
int remaining() //返回position和limit之间的元素个数
Buffer reset() //将位置position转到以前设置的mark所在的位置
Buffer rewind() //将位置设为0,取消设置的mark
Buffer所有子类提供了两个用于数据操作的方法:get()
和put()
方法
获取Buffer中的数据:
get() //读取单个字节
get(byte[] dst) //批量读取多个字节到dst中
get(int index) //读取指定索引位置的字节(不会移动position)
放到入数据到Buffer中:
put(byte b) //将给定单个字节写入缓冲区的当前位置
put(byte[] src) //将src中的字节写入缓冲区的当前位置
put(int index, byte b) //将指定字节写入缓冲区的索引位置(不会移动position)
使用Buffer读写数据一般遵循以下四个步骤:
flip()
方法,转换为读取模式buffer.clear()
方法或者buffer.compact()
方法清除缓冲区 @Test
public void test01() {
// 1.分配一个缓冲区,容量设置成10
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------");
// 2.put()往缓冲区中添加数据
String name = "hello";
buffer.put(name.getBytes());
System.out.println(buffer.position()); // 5
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------");
// 3.flip()为将缓冲区的界限设置为当前位置,并将当前位置重置为0 可读模式
buffer.flip();
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 5
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------");
// 4.get()数据的读取
char ch = (char) buffer.get();
System.out.println(ch);
System.out.println(buffer.position()); // 1
System.out.println(buffer.limit()); // 5
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------");
}
@Test
public void test02() {
// 1.分配一个缓冲区,容量设置成10 put()往缓冲区中添加数据
ByteBuffer buffer = ByteBuffer.allocate(10);
String name = "hello";
buffer.put(name.getBytes());
System.out.println(buffer.position()); // 5
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------");
// 2.clear()清除缓冲区中的数据 并没有真正清除数据,只是让position的位置恢复到初始位置,后续添加数据的时候才会覆盖每个位置的数据
buffer.clear();
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println((char) buffer.get()); // h
System.out.println("--------------------");
// 3.定义一个缓冲区
ByteBuffer buf = ByteBuffer.allocate(10);
String n = "hello";
buf.put(n.getBytes());
buf.flip();
// 读取数据
byte[] b = new byte[2];
buf.get(b);
System.out.println(new String(b));
System.out.println(buf.position()); // 2
System.out.println(buf.limit()); // 5
System.out.println(buf.capacity()); // 10
System.out.println("--------------------");
buf.mark(); // 标记此刻这个位置 2
byte[] b2 = new byte[3];
buf.get(b2);
System.out.println(new String(b2));
System.out.println(buf.position()); // 5
System.out.println(buf.limit()); // 5
System.out.println(buf.capacity()); // 10
System.out.println("--------------------");
buf.reset(); // 回到标记位置
if (buf.hasRemaining()) {
System.out.println(buf.remaining()); // 3
}
}
ByteBuffer可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理
从数据流的角度,非直接内存是下面这样的作用链:
本地IO-->直接内存-->非直接内存-->直接内存-->本地IO
而直接内存是:
本地IO-->直接内存-->本地IO
很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。直接内存使用allocateDirect()
创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。所以呢,当有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()
方法来确定
直接缓冲区使用场景:
通道(Channel):由java.nio.channels
包定义的。Channel表示IO源与目标打开的连接。Channel类似于传统的流。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互
1)NIO的通道类似于流,但有些区别如下:
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
通道可以从缓冲读数据,也可以写数据到缓冲
2)BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的Channel是双向的,可以读操作,也可以写操作
3)Channel在NIO中是一个接口
public interface Channel extends Closeable
获取通道的一种方式是对支持通道的对象调用getChannel()
方法。支持通道的类如下:
获取通道的其他方式是使用Files类的静态方法newByteChannel()
获取字节通道,或者通过通道的静态方法open()
打开并返回指定通道
int read(ByteBuffer dst) // 从Channel中读取数据到ByteBuffer
long read(ByteBuffer[] dsts) // 将Channel中的数据分散到ByteBuffer[]
int write(ByteBuffer src) // ByteBuffer中的数据写入到Channel
long write(ByteBuffer[] srcs) // 将ByteBuffer[]中的数据聚集到Channel
long position() // 返回此通道的文件位置
FileChannel position(long p) // 设置此通道的文件位置
long size() // 返回此通道的文件的当前大小
FileChannel truncate(long s) // 将此通道的文件截取为给定大小
void force(boolean metaData) // 强制将所有对此通道的文件更新写入到存储设备中
1)本地文件写数据
@Test
public void write() {
try {
// 1.字节输出流通向目标文件
FileOutputStream fos = new FileOutputStream("data01.txt");
// 2.得到字节输出流对应的通道Channel
FileChannel channel = fos.getChannel();
// 3.分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello World!".getBytes());
// 4.把缓冲区切换成写出模式
buffer.flip();
channel.write(buffer);
channel.close();
System.out.println("写数据到文件中!");
} catch (IOException e) {
e.printStackTrace();
}
}
2)本地文件读数据
@Test
public void read() {
try {
// 1.定义一个文件字节输入流与源文件接通
FileInputStream fis = new FileInputStream("data01.txt");
// 2.需要得到文件输入流的文件通道
FileChannel channel = fis.getChannel();
// 3.定义一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.读取数据到缓冲区
channel.read(buffer);
buffer.flip();
// 5.读取出缓冲区中的数据并输出
String rs = new String(buffer.array(), 0, buffer.remaining());
System.out.println(rs);
} catch (IOException e) {
e.printStackTrace();
}
}
3)使用Buffer完成文件复制
@Test
public void copy() {
try {
// 源文件
File srcFile = new File("data01.txt");
// 目标文件
File destFile = new File("data02.txt");
// 得到字节输入流
FileInputStream fis = new FileInputStream(srcFile);
// 得到字节输出流
FileOutputStream fos = new FileOutputStream(destFile);
// 得到文件通道
FileChannel isChannel = fis.getChannel();
FileChannel osChannel = fos.getChannel();
// 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
// 必须先清空缓冲区再写入数据到缓冲区
buffer.clear();
// 开始读取一次数据
int flag = isChannel.read(buffer);
if (flag == -1) {
break;
}
// 已经读取了数据,把缓冲区的模式切换成可读模式
buffer.flip();
// 把数据写出
osChannel.write(buffer);
}
isChannel.close();
osChannel.close();
System.out.println("复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
4)分散(Scatter)和聚集(Gather)
分散读取(Scatter):是指把Channel通道的数据读入到多个缓冲区中去
聚集写入(Gather):是指将多个Buffer中的数据聚集到Channel
@Test
public void test() {
try {
// 1.字节输入管道
FileInputStream fis = new FileInputStream("data01.txt");
FileChannel isChannel = fis.getChannel();
// 2.字节输出管道
FileOutputStream fos = new FileOutputStream("data03.txt");
FileChannel osChannel = fos.getChannel();
// 3.定义多个缓冲区做数据分散
ByteBuffer buffer1 = ByteBuffer.allocate(6);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {buffer1, buffer2};
// 4.从通道中读取数据分散到各个缓冲区
isChannel.read(buffers);
// 5.从每个缓冲区中查询是否有数据读取到
for (ByteBuffer buffer : buffers) {
// 切换到读数据模式
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.remaining()));
}
// 6.聚集写入到通道
osChannel.write(buffers);
isChannel.close();
osChannel.close();
System.out.println("复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
5)transferFrom()
从目标通道中去复制原通道数据
@Test
public void test02() {
try {
// 1.字节输入管道
FileInputStream fis = new FileInputStream("data01.txt");
FileChannel isChannel = fis.getChannel();
// 2.字节输出管道
FileOutputStream fos = new FileOutputStream("data04.txt");
FileChannel osChannel = fos.getChannel();
// 3.复制数据
osChannel.transferFrom(isChannel, isChannel.position(), isChannel.size());
isChannel.close();
osChannel.close();
System.out.println("复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
6)transferTo()
把原通道数据复制到目标通道
@Test
public void test03() {
try {
// 1.字节输入管道
FileInputStream fis = new FileInputStream("data01.txt");
FileChannel isChannel = fis.getChannel();
// 2.字节输出管道
FileOutputStream fos = new FileOutputStream("data05.txt");
FileChannel osChannel = fos.getChannel();
// 3.复制数据
isChannel.transferTo(isChannel.position(), isChannel.size(), osChannel);
isChannel.close();
osChannel.close();
System.out.println("复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
选择器(Selector)是非阻塞IO的核心,是SelectableChannel对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel
创建Selector:通过调用Selector.open()
方法创建一个Selector
Selector selector = Selector.open();
向选择器注册通道:SelectableChannel.register(Selector sel, int ops)
// 1.获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
// 2.切换非阻塞模式
ssChannel.configureBlocking(false);
// 3.绑定连接
ssChannel.bind(new InetSocketAddress(9898));
// 4.获取选择器
Selector selector = Selector.open();
// 5.将通道注册到选择器上,并且指定监听接收事件
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
当调用register(Selector sel, int ops)
将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。可以监听的事件类型(使用SelectionKey的四个常量表示):
SelectionKey.OP_READ
(1)SelectionKey.OP_WRITE
(4)SelectionKey.OP_CONNECT
(8)SelectionKey.OP_ACCEPT
(16)若注册时不止监听一个事件,则可以使用位或操作符连接
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE
Selector可以实现:一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升
1)当客户端连接服务端时,服务端会通过ServerSocketChannel得到SocketChannel:获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
2)切换非阻塞模式
ssChannel.configureBlocking(false);
3)绑定连接
ssChannel.bind(new InetSocketAddress(9999));
4)获取选择器
Selector selector = Selector.open();
5)将通道注册到选择器上,并且指定监听接收事件
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
6)轮询式的获取选择器上已经准备就绪的事件
// 轮询式的获取选择器上已经准备就绪的事件
while (selector.select() > 0) {
// 7)获取当前选择器中所有注册的选择键(已就绪的监听事件)
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
// 8)获取准备就绪的是事件
SelectionKey sk = it.next();
// 9)判断具体是什么事件准备就绪
if (sk.isAcceptable()) {
// 10)若接收就绪,获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//11)切换非阻塞模式
sChannel.configureBlocking(false);
// 12)将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
// 13)获取当前选择器上读就绪状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
// 14)读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buf)) > 0) {
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
// 15)取消选择键SelectionKey
it.remove();
}
}
1)获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
2)切换非阻塞模式
sChannel.configureBlocking(false);
3)分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
4)发送数据给服务端
Scanner scan = new Scanner(System.in);
while (scan.hasNext()) {
String str = scan.nextLine();
buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())
+ "\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
// 关闭通道
sChannel.close();
public class Server {
public static void main(String[] args) throws IOException {
// 1.获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
// 2.切换非阻塞模式
ssChannel.configureBlocking(false);
// 3.绑定连接
ssChannel.bind(new InetSocketAddress(9999));
// 4.获取选择器
Selector selector = Selector.open();
// 5.将通道注册到选择器上,并且指定监听接收事件
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6.使用Selector选择器轮询已经就绪好的事件
while (selector.select() > 0) {
// 7.获取选择器中的所有注册的通道中已经就绪好的事件
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
// 8.开始遍历这些准备好的事件
while (it.hasNext()) {
SelectionKey sk = it.next();
// 9.判断这个事件具体是什么
if (sk.isAcceptable()) {
// 10.直接获取当前接入的客户端通道
SocketChannel channel = ssChannel.accept();
// 11.切换非阻塞模式
channel.configureBlocking(false);
// 12.将该通道注册到选择器上
channel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
// 13.获取当前选择器上读就绪状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
// 14.读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buf)) > 0) {
buf.flip();
System.out.println(new String(buf.array(), 0, buf.remaining()));
buf.clear();
}
}
// 15.取消选择键SelectionKey
it.remove();
}
}
}
}
public class Client {
public static void main(String[] args) throws IOException {
// 1.获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
// 2.切换非阻塞模式
sChannel.configureBlocking(false);
// 3.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 4.发送数据给服务端
Scanner scan = new Scanner(System.in);
while (scan.hasNext()) {
String str = scan.nextLine();
buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())
+ "\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
// 5.关闭通道
sChannel.close();
}
}
NIO非阻塞通信模型图:
编写一个NIO群聊系统,实现客户端与客户端的通信需求(非阻塞)
服务器端:可以监测用户上线、离线,并实现消息转发功能
客户端:通过Channel可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息
public class Server {
private Selector selector;
private ServerSocketChannel ssChannel;
private static final int PORT = 9999;
public Server() {
try {
selector = Selector.open();
ssChannel = ServerSocketChannel.open();
ssChannel.bind(new InetSocketAddress(PORT));
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 监听
*/
public void listen() {
try {
while (selector.select() > 0) {
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey sk = it.next();
if (sk.isAcceptable()) {
SocketChannel sChannel = ssChannel.accept();
sChannel.configureBlocking(false);
System.out.println(sChannel.getRemoteAddress() + "上线");
sChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
readClientData(sk);
}
it.remove();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 读取客户端消息
*
* @param sk
*/
private void readClientData(SelectionKey sk) {
SocketChannel sChannel = null;
try {
sChannel = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = sChannel.read(buffer);
if (count > 0) {
buffer.flip();
String msg = new String(buffer.array(), 0, buffer.remaining());
System.out.println("接收到了客户端消息:" + msg);
// 向其它的客户端转发消息(去掉自己)
sendInfoToOtherClients(msg, sChannel);
}
} catch (IOException e) {
e.printStackTrace();
try {
System.out.println(sChannel.getRemoteAddress() + "离线了..");
sk.cancel();
sChannel.close();
} catch (IOException e2) {
e2.printStackTrace();
}
}
}
/**
* 转发消息给其它客户端
*
* @param msg
* @param sChannel
*/
private void sendInfoToOtherClients(String msg, SocketChannel sChannel) throws IOException {
System.out.println("服务器转发消息中...");
// 遍历所有注册到selector上的SocketChannel并排除sChannel
for (SelectionKey key : selector.keys()) {
Channel targetChannel = key.channel();
// 排除自己
if (targetChannel instanceof SocketChannel && targetChannel != sChannel) {
SocketChannel dest = (SocketChannel) targetChannel;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
dest.write(buffer);
}
}
}
public static void main(String[] args) {
Server server = new Server();
server.listen();
}
}
public class Client {
private Selector selector;
private SocketChannel socketChannel;
private String username;
private static final String IP = "127.0.0.1";
private static final int PORT = 9999;
public Client() {
try {
selector = Selector.open();
socketChannel = SocketChannel.open(new InetSocketAddress(IP, PORT));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println("当前客户端准备完成...");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取从服务器端回复的消息
*
* @throws IOException
*/
private void readInfo() throws IOException {
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
System.out.println(new String(buffer.array()).trim());
}
iterator.remove();
}
}
}
/**
* 向服务器发送消息
*
* @param s
*/
private void sendToServer(String s) {
s = username + "说:" + s;
try {
socketChannel.write(ByteBuffer.wrap(s.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Client client = new Client();
new Thread(() -> {
try {
client.readInfo();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
Scanner sc = new Scanner(System.in);
while (sc.hasNextLine()) {
String s = sc.nextLine();
client.sendToServer(s);
}
}
}
Java AIO(NIO 2.0)异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理
BIO | NIO | AIO |
---|---|---|
Socket | SocketChannel | AsynchronousSocketChannel |
ServerSocket | ServerSocketChannel | AsynchronousServerSocketChannel |
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可,这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序
即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO 2.0,主要在Java.nio.channels包下增加了下面四个异步通道:
推荐资料:
IO模式讲解(AIO&BIO&NIO)