1.1 BIO
BIO有的称之为basic(基础)IO, 有的称之为block(阻塞)IO,主要应用于文件IO和网络IO。处理单位是字节。
在JDK1.4之前,我们建立网络连接只能使用BIO,需要在服务端先启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要对每个请求建立一个线程等待请求,而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝,如果有的话,客户端线程会等待请求结束之后才继续执行,这就是阻塞式IO。
BIO的基础用法(基于TCP)
服务端代码
package com.example.netty1.tcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws IOException {
// 1. 创建ServerSocket对象
System.out.println("服务端 启动~~~");
System.out.println("初始化端口 9999~~~");
ServerSocket ss = new ServerSocket(9999);
while (true) {
// 2. 监听客户端
Socket s = ss.accept();// 阻塞
// 3. 从连接中取输入流来接收消息
InputStream is = s.getInputStream();
byte[] b = new byte[100];
is.read(b);
String hostAddress = s.getInetAddress().getHostAddress();
System.out.println(hostAddress + "说" + new String(b).trim());
// 4. 从连接中取出输出流并回话
OutputStream os = s.getOutputStream();
os.write("没钱".getBytes());
// 5. 关闭
s.close();
}
}
}
客户端代码
package com.example.netty1.tcp;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class TCPClient {
public static void main(String[] args) throws Exception {
while (true) {
//1. 创建Socket对象
Socket socket = new Socket("127.0.0.1", 9999);
//2. 从连接中取出输出流并发送信息
OutputStream os = socket.getOutputStream();
System.out.println("请输入:");
Scanner sc = new Scanner(System.in);
String msg = sc.nextLine();
os.write(msg.getBytes());
//3. 从连接中取出输入流并接收回话
InputStream is = socket.getInputStream();
byte[] bytes = new byte[100];
is.read(bytes);
System.out.println("老板说:" + new String(bytes).trim());
// 5.关闭
socket.close();
}
}
}
编写了一个客户端程序,通过9999端口连接服务器端,getInputStream方法用来等待服务端返回数据,如果一直没有返回,就一直等待,程序就会阻塞到这里。
1.2 NIO
Java.nio 全程 Java Non-Blocking IO,是指JDK提供的新API。 从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,被统称为NIO。新增了许多用于处理输入输出的类,这些类被放在Java.nio的包及子包下,并对原Java.io包中的许多类进行了改写,新增满足NIO的功能。
NIO和BIO有着相同的目的和作用,但是他们的视线方式完全不同。
- BIO是以流的方式处理数据,而NIO是以块的方式处理数据,块IO的效率比流IO高很多。
- NIO是非阻塞式的,BIO是阻塞式的,使用NIO可以提供非阻塞式的高延伸网络。
- BIO是单向的,NIO是双向的
NIO三大核心部分:
- Channel通道
- Buffer缓存区
- Selector选择器
传统的BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读到缓存区中,或者从缓存区写入到通道里。Selector用于监听多个通道的是事件(比如:连接请求,数据到达等),因此,使用单线程就可以监听多个客户端请求(Channel)。
1.3 NIO-文件IO
Buffer(缓冲区):是一个缓冲容器(底层是数组),内置了一些机制能够跟踪和记录缓冲区的状态变化。
Channel(通道):提供从文件和网络读取数据的通道,读取和写入数据都需要经Buffer。
1.3.1 NIO-文件IO-Buffer
常用方法:
ByteBuffer put(byte[]); 存储字节数据到Buffer
byte[] get(); 从Buffer中获取数据
byte[] array(); 把Buffer数据转为字节数组
ByteBuffer allocate(int capacity); 设置缓冲区的初始容量
ByteBuffer wrap(byte[] array); 将数据放到缓冲区
Buffer flip(); 翻转缓冲区,重置位置到初始位置
1.3.2 NIO-文件IO-Channel
在NIO中,Channel是一个接口,标识通道,通道是双向的,可以用来读,也可以用来写数据。
常用的Channel实现类:FileChannel、DatagramChannel、ServerSocketChannel、SocketChannel
FileChannel用于文件数据的读写;
DatagramChannel用于UDP数据的读写;
ServerSocketChannel和SocketChannel用于TCP数据的读写;
FileChannel类常用的方法:
int read(ByteBuffer dst); 从Channel中读取数据并放到Buffer中
int write(ByteBuffer dst); 把Buffer的数据写到Channel中
long transferFrom(ReadableByteChannel src, long position, long count); 从目标channel复制数据到当前channel
long transferTo(long position, long count, WriteByteChannel target); 把数据从当前channel复制到目标channel
NIO写数据
package com.example.netty1.tcp;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 通过NIO实现文件IO
*/
public class NIOTest {
public static void main(String[] args) throws Exception {
//往本地文件写数据
// 1. 创建流
FileOutputStream fos = new FileOutputStream("basic.txt");
// 2. 往流中获取一个通道
FileChannel channel = fos.getChannel();
// 3. 提供一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4. 往缓冲区写数据
buffer.put("HelloWorld".getBytes());
// 5. 翻转缓冲区
buffer.flip();
// 6. 把缓冲区写到通道中
channel.write(buffer);
// 7. 关闭
fos.close();
}
}
NIO中的通道,是从输出流对象里通过getChannel()获取的,这个通道是双向的,既可以读,也可以写。在往通道写入数据之前,必须通过put()方法将数据放到Buffer中,然后通过channel的write()方法写入数据。在write之前,需要调用flip()方法翻转缓冲区,把内部重置到初始位置,这样才能把所有数据写到通道里。
flip()方法的作用:翻转缓冲区,在缓冲区有有一个指针从头(pos)写到尾(lim)。默认的pos是缓冲区内元素的size,lim是缓冲区大小。当从缓冲区向通道去写时,是从pos位置去写,写到lim,这样就得不到元素,需要将lim = pos, pos = 0再写。
NIO读数据
@Test
public void test1() throws Exception {
File file = new File("basic.txt");
// 1. 创建输入流
FileInputStream fis = new FileInputStream(file);
// 2. 得到一个通道
FileChannel channel = fis.getChannel();
// 3. 准备一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
// 4. 从通道中读取数据到缓冲区
channel.read(buffer);
System.out.println(new String(buffer.array()));
// 5. 关闭
fis.close();
}
BIO复制文件
@Test
public void test2() throws Exception {
// BIO复制文件 - 通过输入流和输出流来实现
FileInputStream fis = new FileInputStream("basic.txt");
FileOutputStream fos = new FileOutputStream("basic1.txt");
byte[] bytes = new byte[1024];
while (true) {
int res = fis.read(bytes);
if (res == -1) {
break;
}
fos.write(bytes, 0, res);
}
fos.close();
fis.close();
}
NIO复制文件
@Test
public void test3() throws Exception {
// NIO复制文件 - 通过通道实现
FileInputStream fis = new FileInputStream("basic.txt");
FileOutputStream fos = new FileOutputStream("basic3.txt");
FileChannel fisChannel = fis.getChannel();
FileChannel fosChannel = fos.getChannel();
fosChannel.transferFrom(fisChannel, 0, fisChannel.size());
// fisChannel.transferTo(0 , fisChannel.size(), fosChannel);
fos.close();
fis.close();
}
1.4 NIO-网络IO
JavaNIO中,网络通道是非阻塞IO,基于事件驱动,很适合需要维持大量连接,但数据交换量不大的场景, 例如:RPC、即时通讯、Web服务器等。
Java编写网络应用,通常有以下几种模式:
1、 为每个请求创建线程:一个客户端连接用一个线程,阻塞式IO。
优点:程序编码简单
缺点:如果连接非常多,分配的线程也会非常多,服务器可能会因为资源耗尽而崩溃
2、 线程池:创建固定线程数量线程的线程池,来接受客户端的请求,阻塞式IO。
优点:程序编码简单,可以处理大量连接
缺点:线程的开销非常大,连接如果非常多,排队现象比较严重
3、 JavaNIO:用非阻塞IO
优点:这种模式可以用一个线程,来处理大量的客户端请求
缺点:代码复杂度高
1.4.1 NIO-网络IO-Selector
Selector选择器也叫多路复用器;
- NIO的三大核心组件之一;
- 用于检测多个注册Channel上是否有事件发生(读、写、连接),如果有,就获取事件,并对每个事件进行处理;
- 只需要一个线程就能管理多个Channel,也就是多个连接;
- 只有连接真正有读写事件时,才会调用方法进行处理,大大降低了系统分配线程与线程上下文切换的开销;
Selector常用方法:
Selector open(); 开启一个Selector
int select(long timeout); 监控所注册的通道
Set<SelectionKey> selectedKeys(); 从selector获取所有的selectionKey
1.4.2 NIO-网络IO-SelectionKey
SelectionKey : 代表了Selector和网络SocketChannel的注册关系
- OP_ACCEPT: 有新的网络连接可以accept,值为16
- OP_CONNECT: 代表连接已经建立,值为8
- OP_READ: 读,值为1
- OP_WRITE: 写,值为4
常用方法:
Selector selector(); 得到与之关联的Selector对象
SelectableChannel channel(); 得到与之关联的通道
Object attachment(); 得到与之关联的共享数据
boolean isAcceptable(); 是否可接入
boolean isReadable(); 是否可读
boolean isWritable(); 是否可写
1.4.3 NIO-网络IO-ServerSocketChannel
ServerSocketChannel : 用来在服务器端监听新的客户端socket连接
常用方法:
ServerSocketChannel open(); 得到一个ServerSocketChannel通道
ServerSocketChannel bind(SocketAddress local); 设置服务端端口号
SelectableChannel configureBlocking(boolean block); 设置阻塞或者非阻塞模式,false代表非阻塞模式
SocketChannel accept(); 接受一个连接,返回代表这个连接的通道对象
SelectionKey register(Selector sel, int ops); 注册一个选择器并设置监听事件
1.4.4 NIO-网络IO-SocketChannel
SocketChannel: 网络IO通道,具体负责进行读和写操作。
NIO把数据写入通道,或者从通道读取数据。
常用方法:
SocketChannel open(); 得到一个SocketChannel通道
SelectableChannel configureBlocking(boolean block); 设置阻塞或者非阻塞模式,false代表非阻塞模式
boolean connect(SocketAddress remote); 连接服务器
boolean finishConnect(); 如果上面方法连接失败,接下来通过这个方法完成连接操作
int write(ByteBuffer src); 往通道里写数据
int read(ByteBuffer src); 从通道里读数据
SelectionKey register(Selector sel, int ops, Object att); 注册一个选择器并选择监听事件,最后一个参数可以设置共享数据
void close(); 关闭通道
1.4.5 NIO-网络IO-Selector、ServerSocketChannel、SocketChannel关系图
服务器端有一个选择器对象,服务端的ServerSocketChannel对象也要注册给selector,他的accept方法负责接受客户端的连接请求。有一个客户端连接过来,服务端就会建立一个通道。Selector会监控所有注册的通道,检查这些通道中是否有事件发生(连接、断开、读、写等事件),如果某个通道有事件发生,则做响应的处理。
1.4.6 NIO-网络IO-NIO实现客户端和服务器端的通信
客户端代码
package com.example.netty1.tcp;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* 网络客户端程序
*/
public class NIOClient {
public static void main(String[] args) throws IOException {
// 1. 得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
// 2. 设置非阻塞方式
socketChannel.configureBlocking(false);
// 3. 提供服务器端的IP地址的端口号
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
// 4. 连接服务器端,如果用connect方法连接不成功,就用finishConnect方法连接
if (!socketChannel.connect(address)) {
// 因为连接需要花费事件,所以用while一直循环尝试连接。在连接服务器时,还可以做一些别的事情,体现非阻塞。
while (!socketChannel.finishConnect()) {
// nio作为非阻塞的优势,如果服务器没有响应(不启动服务器),客户端不会阻塞,最后会报错,客户端尝试连接服务器,连接不上
System.out.println("Client: 连接不上,做一些别的事情");
}
}
// 5. 得到一个缓冲区并存入数据
String msg = "你好,这里是客户端,打个招呼";
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
// 6. 发送数据
socketChannel.write(buffer);
// 阻止客户端停止,否则服务端也会停止。
System.in.read();
}
}
服务端代码
package com.example.netty1.tcp;
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;
public class NIOServer {
public static void main(String[] args) throws Exception {
// 1. 开启一个ServerSocketChannel通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2. 开启一个Selector选择器
Selector selector = Selector.open();
// 3. 绑定端口9999
System.out.println("服务端 启动。。。");
System.out.println("初始化端口 9999");
serverSocketChannel.bind(new InetSocketAddress(9999));
// 4. 配置非阻塞方式
serverSocketChannel.configureBlocking(false);
// 5. Selector选择器注册ServerSocketChannel通道,绑定连接操作
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6. 循环执行:监听连接事件和读取数据操作
while (true) {
// 6.1 监控客户端连接:selector.select()方法返回的是客户端的通道数,如果为0,则说明没有客户端连接。
// nio非阻塞式的优势
if (selector.select(2000) == 0) {
System.out.println("Server: 没有客户端连接,去码头搞点薯条");
continue;
}
// 6.2 得到selectionKeys, 判断通道里的事件
Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
// 遍历所有selectionKey
while (selectionKeyIterator.hasNext()) {
SelectionKey selectionKey = selectionKeyIterator.next();
// 客户端先连接上,处理连接事件,然后客户端会向服务端发信息,再处理读取服务端数据事件。
if (selectionKey.isAcceptable()) {
// 客户端连接请求事件
System.out.println("OP_ACCOUNT");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 注册通道,将通道交给Selector进行监控
// 参数1: 选择器
// 参数2:服务器要监控的事件,客户端发send数据,服务端读read数据
// 参数3: 客户端传过来的数据要放到缓存区
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (selectionKey.isReadable()) {
// 读取客户端数据事件
// 数据在通道中,先获取通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
// 取到一个缓冲区,nio读写都是基于缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从通道中将客户端发过来的数据读到缓冲区
channel.read(buffer);
System.out.println("客户端发来数据:" + new String(buffer.array()));
}
// 手动移除当前key, 防止重复处理
selectionKeyIterator.remove();
}
}
}
}
1.4.7 NIO-网络IO-网络聊天室1.0
客户端代码:使用 NIO 编写了一个聊天程序的服务器端,可以接受客户端发来的数据,并能把数据广播给所有客户端。
package com.example.netty1.tcp;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
/**
* 使用 NIO 编写了一个聊天程序的服务器端,可以接受客户端发来的数据,并能把数据广播给所有客户端。
*/
public class ChatServer {
//监听通道
private ServerSocketChannel listenerChannel;
// 选择器
private Selector selector;
// 服务器端口
private static final int PORT = 9999;
public ChatServer() {
try {
// 开启socket监听通道
listenerChannel = ServerSocketChannel.open();
// 开启选择器
selector = Selector.open();
// 绑定端口
listenerChannel.bind(new InetSocketAddress("127.0.0.1", PORT));
// 设置为非阻塞模式
listenerChannel.configureBlocking(false);
// 将选择器绑定到监听通道并监听accept事件
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
printInfo("真人网络聊天室 启动.......");
printInfo("真人网络聊天室 初始化端口 9999.......");
printInfo("真人网络聊天室 初始化网络ip地址 127.0.0.1.......");
} catch (Exception e) {
e.printStackTrace();
}
}
public void start() throws Exception {
try {
// 不停循环
while (true) {
//
if (selector.select(2000) == 0) {
System.out.println("Server: 没有客户端连接,搞点兼职");
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 如果是连接请求事件
if (selectionKey.isAcceptable()) {
SocketChannel accept = listenerChannel.accept();
accept.configureBlocking(false);
accept.register(selector, SelectionKey.OP_READ);
printInfo(accept.getRemoteAddress().toString().substring(1) + "上线了。。。");
}
if (selectionKey.isReadable()) {
// 读取数据事件
this.readMsg(selectionKey);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取客户端的信息,并广播出去
* @param selectionKey selectionKey
*/
private void readMsg(SelectionKey selectionKey) throws Exception {
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
if (count > 0) {
String msg = new String(buffer.array());
// 打印消息
printInfo(msg);
// 广播消息
broadCast(channel, msg);
}
}
private void broadCast(SocketChannel channel, String msg) throws Exception {
System.out.println("服务器广播了消息。。。");
// 获取选择器上所有的selectionKey
for (SelectionKey key : selector.keys()) {
// 获取通道
Channel targetChannel = key.channel();
// 如果不是自身通道,发送信息
if (targetChannel instanceof SocketChannel && targetChannel != channel) {
SocketChannel destChannel = (SocketChannel) targetChannel;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
destChannel.write(buffer);
}
}
}
private void printInfo(String str) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("[" + simpleDateFormat.format(new Date()) + "] -> " + str);
}
public static void main(String[] args) throws Exception{
new ChatServer().start();
}
}
客户端代码:通过 NIO 编写了一个聊天程序的客户端,可以向服务器端发送数据,并能接收服务器广播的数据。
package com.example.netty1.tcp;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* 聊天程序客户端
*/
public class ChatClient {
// 服务器地址
private static final String HOST = "127.0.0.1";
// 服务器端口
private static final int PORT = 9999;
// 网络通道
private final SocketChannel socketChannel;
// 聊天用户名
private final String username;
public ChatClient() throws IOException {
// 1. 得到网络通道
socketChannel = SocketChannel.open();
// 2. 设置非阻塞模式
socketChannel.configureBlocking(false);
// 3. 设置服务端地址和端口号
InetSocketAddress address = new InetSocketAddress(HOST, PORT);
// 4. 连接服务端
if (!socketChannel.connect(address)) {
// NIO非阻塞的优势
while (!socketChannel.finishConnect()) {
System.out.println("Clint:连接服务端的同时,别闲着,去码头搞点薯条吃吃");
}
}
// 5. 得到服务端IP和端口信息,作为聊天用户名使用
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println("-------Clint(" + username +") is ready---------");
}
/**
* 向服务端发送信息
* @param msg 消息
* @throws Exception
*/
public void sendMsg(String msg) throws Exception {
if (msg.equalsIgnoreCase("bye")) {
socketChannel.close();
return;
}
msg = username + "说" + msg;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
socketChannel.write(buffer);
}
/**
* 从服务端接收数据
* @throws Exception
*/
public void receiveMas() throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int size = socketChannel.read(buffer);
if (size > 0) {
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
}
}
测试代码 : 运行了聊天程序的客户端,该代码运行一次就是一个聊天客户端,可以同时运行多个聊天客户端。在一个聊天客户端中发送消息,会广播给所有其他聊天客户端。客户端互相发送消息,需要提前将服务端启动。
package com.example.netty1.tcp;
import java.util.Scanner;
public class TestChat {
public static void main(String[] args) throws Exception {
ChatClient chatClient = new ChatClient();
new Thread(() -> {
// 监听服务器
while (true) {
try {
chatClient.receiveMas();
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
chatClient.sendMsg(msg);
}
}
}
1.5 AIO
JDK 7 引入了Asynchronous IO,即AIO,也叫异步不阻塞的IO,也可以叫做NIO2。在进行IO编程中,常用到两种模式: Reactor模式和Proactor模式。
- NIO采用Reactor模式,当有事件触发时,服务器端得到通知,进行相应的处理。
- AIO采用Proactor模式,采用异步通道的概念,简化了程序编写,一个有效的请求才启动一个线程,他的特点是先由操作系统完成后,才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
1.6 对比总结
IO的方式通常分为几种:同步阻塞的BIO,同步非阻塞的NIO,异步非阻塞的AIO。
BIO方式:适合连接数据较小且固定的架构
- 这种方式对服务器资源要求比较高,并发局限于应用中
- JDK1.4 以前的唯一选择,但程序直观简单易理解
- 同步阻塞:食堂排队取餐:中午去食堂吃饭,排队等着,啥都干不了,到你了选餐,付款,然后找位子吃饭
NIO方式:适合连接数目多并且连接比较短(轻操作)的架构
- 比如:聊天服务器,并发局限在应用中,编程比较复杂
- JDK1.4开始支持
- 同步非阻塞:例如下馆子:点完餐,就去商场玩。玩一会回来问一下,饭好了没
AIO方式:适用于连接数目多且连接比较长(重操作)的架构
- 比如:相册服务器,充分利用OS参与并发操作,编程比较复杂
- JDK1.7开始支持
- 异步非阻塞