1. IO模型
1.1 BIO 模型
特点:每建立一个连接就会创建一个线程,没有连接就会阻塞等待
package com.zhj.test.bio;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author zhj
*/
public class BIOServer {
public static void main(String[] args) throws IOException {
// 线程池机制
// 思路
// 1. 创建一个线程
// 2. 如果有客户端连接,就创建一个线程,与之通信(单独写一个方法)
ExecutorService executorService = Executors.newCachedThreadPool();
// 创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器程序启动!");
while (true) {
// 监听,等待客户端连接
System.out.println("等待连接!!!");
final Socket socket = serverSocket.accept();
System.out.println("连接一个客户端(socket)!");
// 创建一个线程与之通讯
executorService.execute(new Runnable() {
@Override
public void run() {
// 可以与客户端通讯
handler(socket);
}
});
}
}
/**
* 与客户端通讯
*/
public static void handler(Socket socket) {
byte[] bytes = new byte[1024];
try {
InputStream inputStream = socket.getInputStream();
// 循环读取客户端读取的数据
while (true) {
System.out.println("等待输入数据!!!");
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println(Thread.currentThread().getName() + " : " + Thread.currentThread().getId());
System.out.println("接收:" + new String(bytes, 0, read));
} else {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("关闭与客户端的连接!");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1.2 NIO
NIO 全称java non-blocking IO 是指JDK提供的新的API。从JDK1.4开始,Java提供了一系列改进输入输出的新特性,被统称NIO(New IO),是同步非阻塞的。
三大核心部分:Channel(通道),Buffer缓存区),Selector(选择题)
NIO是面向缓冲区,或者面向块编程的,数据读到一个它稍后处理的缓冲区,需要时可在缓冲区前后移动,这就增加了它处理过程中的灵活性,使他可以提供非阻塞式的高伸缩性网络。
特点:
- 非阻塞 不需要线程一直等待,有别的任务线程也可以去执行
- 一个线程可以处理多个连接,当大量请求到服务器,不需要每个连接开一个线程
HTTP2.0采用多路复用技术,同一个连接处理多个请求。
三大核心组件的关系
- 每个Channel都会对应一个Buffer
- Selector对应一个线程,一个线程对应多个Channel连接
- 该图反应了三个Channel 注册到改Selector 程序
- 程序切换到那个Channel是由事件决定的,Event就是一个重要的概念
- Selector会根据不同的时间再各个通道上切换
- Buffer就是一个内容块,底层是与一个数组的
- 数据的读取写入是通过Buffer,这个与BIO有本质区别,BIO要么是输入流,要么是输出流,不能是双向的,NIO的Buffer是可以读也可以写的,需要flip方法切换
- Channel是双向的,可以返回底层操作系统的情况,Linux底层的操作系统就是双向的
1.2.1 Buffer缓冲区的使用
- Capacity 容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
- Limit 表示缓冲区当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
- Position 位置,下一个要读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写操作做准备
- Mark 标记
package com.zhj.test.bio;
import java.nio.IntBuffer;
/**
* @author zhj
*/
public class BasicBuffer {
public static void main(String[] args) {
// 举例说明Buffer 的使用
// 创建一个Buffer
IntBuffer intBuffer = IntBuffer.allocate(5);
// 向Buffer 存数据
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i * 2);
}
// 从Buffer 读取数据
// 将Buffer转换,读写切换
/*
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
*/
intBuffer.flip();
// 设置读取位置
intBuffer.position(2);
// 设置读取结束位置
intBuffer.limit(4);
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
public class NIOByteBufferPutGet {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(64);
buffer.putInt(100);
buffer.putLong(9L);
buffer.putChar('强');
buffer.putShort((short) 4);
buffer.flip();
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getChar());
System.out.println(buffer.getShort());
}
}
public class ReadOnlyBuffer {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(64);
for (int i = 0; i < 64; i++) {
buffer.put((byte) i);
}
buffer.flip();
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(readOnlyBuffer.getClass());
while (readOnlyBuffer.hasRemaining()) {
System.out.println(readOnlyBuffer.get());
}
// 只读不能放数据
// readOnlyBuffer.put((byte) 1);
}
}
/**
* MappedByteBuffer 说明
* 1. 可以让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次
* @author zhj
*/
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
File file1 = new File("E:\\data_file\\log1.txt");
File file2 = new File("E:\\data_file\\log2.txt");
RandomAccessFile randomAccessFile = new RandomAccessFile(file1, "rw");
FileChannel fileChannel = randomAccessFile.getChannel();
/**
* 参数(1读写模式,2起始位置,3映射到内存大小)
*/
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5);
mappedByteBuffer.put(0, (byte) 'H');
mappedByteBuffer.put(3, (byte) '9');
randomAccessFile.close();
System.out.println("修改成功~");
}
}
1.2.2 Channel通道的使用
基本介绍
1)NIO的通道类似与流,但区别如下
- 通道可以同时进行读写,而流只能进行读或者写
- 通道可以实现异步读写数据
- 通道可以从缓冲区读取数据,也可以写数据到缓冲区
2)BIO中的stream 是单向的,如FileinputStream对象只能进行读取数据的操作,而NIO中的通道是双向的,可以读,也可以写
3)Channel 在NIO中是一个接口
4)常用的Channel类有 FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel
5)FileChannel用于文件的数据读写,DatagramChannel 用于UDP的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写
FileChannel 类
- read 将通道数据读取到缓冲区中
- write 把缓冲区的数据写到通道
- transferFrom() 从目标通道中复制数据到当前通道
- transferTo() 把数据从当前通道复制给目标通道
// 案例
// 写
public class NIOFileChannel01 {
public static void main(String[] args) throws IOException {
String str = "hello world";
// 创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("E:\\data_file\\log.txt");
// 通过fileOutputStream 获取对应fileChannel
// 这个fileChannel 真实类型是 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将str 放入
byteBuffer.put(str.getBytes());
// 读写切换
byteBuffer.flip();
// 写入Channel
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
// 读
public class NIOFileChannel02 {
public static void main(String[] args) throws IOException {
File file = new File("E:\\data_file\\log.txt");
// 创建一个输出流
FileInputStream fileInputStream = new FileInputStream(file);
// 通过fileOutputStream 获取对应fileChannel
// 这个fileChannel 真实类型是 FileChannelImpl
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
// 将文件 读入缓冲区
fileChannel.read(byteBuffer);
// 读写切换
// byteBuffer.flip();
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
// 读写
}
// 将buffer 写入到 fileChannel02
byteBuffer.flip();
fileChannel02.write(byteBuffer);
}
fileInputStream.close();
fileOutputStream.close();
}
}
// 文件拷贝
public class NIOFileChannel04 {
public static void main(String[] args) throws IOException {
File file1 = new File("E:\\data_file\\img01.jpg");
File file2 = new File("E:\\data_file\\img02.jpg");
// 创建一个输出流
FileInputStream fileInputStream = new FileInputStream(file1);
FileChannel fileChannel01 = fileInputStream.getChannel();
// 创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream(file2);
FileChannel fileChannel02 = fileOutputStream.getChannel();
fileChannel02.transferFrom(fileChannel01,0, fileChannel01.size());
fileInputStream.close();
fileOutputStream.close();
}
}
ScatteringAndGathering 分散聚集
/**
* Scattering 将数据写入到buffer,可采用buffer数组,依次写入
* Gathering 将数据读出到buffer
* @author zhj
*/
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws IOException {
// 使用ServerSocketChannel 和SocketChannel 网络
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
// 绑定端口到Socket并启动
serverSocketChannel.socket().bind(inetSocketAddress);
// 创建buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
// 等客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8; // 假定从客户端接收8个
while (true) {
int byteRead = 0;
while (byteRead < messageLength) {
long read = socketChannel.read(byteBuffers);
byteRead += read;
// System.out.println("byteRead = " + byteRead);
Arrays.asList(byteBuffers).stream().map(
buffer -> "postion = " + buffer.position() + ", limit = " + buffer.limit())
.forEach(System.out::println);
}
// buffer 反转
Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
// 将数据显示到客户端
long byteWrite = 0;
while (byteWrite < messageLength) {
long write = socketChannel.write(byteBuffers);
byteWrite += write;
}
Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());
System.out.println("byteRead = " + byteRead);
System.out.println("byteWrite = " + byteWrite);
System.out.println("messageLength = " + messageLength);
}
}
}
1.2.3 Selector 选择器的使用
特点
- Netty 的IO线程NioEventLoop 聚合了Selector (选择器,也叫多路复用器),可以同时并发处理成百上千个客户端的连接。
- 当线程从某客户端Socket通道进行读写时,若没有数据可用时,该线程可以进行其他任务。
- 线程常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
- 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致线程挂起。
- 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程的模型,架构性能、弹性伸缩能力和可靠性都得到了极大的提升。
方法:open() 获得
- selector.select() 阻塞
- selector.select(1000) 阻塞1s,返回
- selector.wakeup() 唤醒
- selector.selectNow() 不阻塞
1.2.4 NIO实现
NIO入门案例
ator.next();
// 事件驱动
if (key.isAcceptable()) {
System.out.println("有新的客户端连接");
SocketChannel socketChannel = serverSocketChannel.accept();
// 设置为非阻塞
socketChannel.configureBlocking(false);
// 注册selector 关联Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("生成非阻塞socketChannel:" + socketChannel.hashCode());
}
if (key.isReadable()) {
// 通过key反向获取对应channel
SocketChannel socketChannel = (SocketChannel) key.channel();
// 获取到该channel 关联的 Buffer
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
socketChannel.read(byteBuffer);
System.out.println("客户端:" + new String(byteBuffer.array()));
}
// 手动从集合移出当前key 防止多线程发生重复读取
iterator.remove();
}
}
}
}
public class NIOClient {
public static void main(String[] args) throws Exception {
// 1.得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
// 2.设置非阻塞
socketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
// 3.连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("客户端因为连接需要时间,客户端不会阻塞");
}
}
System.out.println("客户端连接服务器连接成功!");
// 4.设置发送内容
String str = "hello world!!!";
// 5.将数据放入缓冲区 wrap可以根据字节数组大小分配大小
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
socketChannel.write(buffer);
System.in.read();
}
}
1.3 NIO与BIO的比较
- BIO是以流的方式处理的,而NIO以块的方式处理数据,块IO的效率比流IO的高很多
- BIO是阻塞的,NIO是非阻塞的
- BIO基于字节流和字符流进行操作,而NIO基于Channel通道和Buffer缓冲区进行操作,数据总是从通道读到缓冲区中,或者从缓冲区写入到通道中。Sellector选择器用于监听多个通道的事件比如连接请求,数据到达等,因此使用单个线程就可以监听多个客户端通道
2 NIO群聊
服务端
public class GroupChatServer {
// 定义相关属性
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private static final int PORT = 8888;
//构造器
public GroupChatServer() {
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
// 读取客户端信息
private void readData(SelectionKey key) {
// 定义
SocketChannel socketChannel = null;
try {
socketChannel = (SocketChannel) key.channel();
// 创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = socketChannel.read(buffer);
if (count > 0) {
// 把缓冲区数据转字符串输出
String msg = new String(buffer.array());
// 输出该消息
System.out.println("From 客户端:" + msg);
// 转发消息
sendInfoToOtherClients(msg, socketChannel);
}
} catch (Exception e) {
try {
System.out.println(socketChannel.getRemoteAddress() + "离线了");
key.cancel();
socketChannel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
private void sendInfoToOtherClients(String msg, SocketChannel self) {
System.out.println("服务器转发消息中。。。");
// 遍历所有注册在selector 并排除自己
try {
for (SelectionKey key : selector.keys()) {
Channel targetChannel = key.channel();
if (targetChannel instanceof SocketChannel && targetChannel != self) {
// 转型
SocketChannel socketChannel = (SocketChannel) targetChannel;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
socketChannel.write(buffer);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 监听
public void listen() {
try {
while (true) {
int count = selector.select(2000);
if (count > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress() + "上线了!");
}
if (key.isReadable()) {
// 处理读方法
readData(key);
}
iterator.remove();
}
} else {
// System.out.println("等待。。。");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
GroupChatServer server = new GroupChatServer();
server.listen();
}
}
客户端
public class GroupChatClient {
private final String HOST = "127.0.0.1";
private final int PORT = 8888;
private Selector selector;
private SocketChannel socketChannel;
private String username;
public GroupChatClient() {
try {
selector = Selector.open();
socketChannel = SocketChannel.open(new InetSocketAddress(HOST,PORT));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + "准备就绪。。。");
} catch (Exception e) {
e.printStackTrace();
}
}
public void sendInfo(String info) {
info = username + " : " + info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
}
public void readInfo() {
try {
int readChannels = selector.select();
if (readChannels > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
iterator.remove();
}
} else {
// System.out.println("没有可以用的通道。。。");
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
GroupChatClient client = new GroupChatClient();
new Thread() {
public void run() {
while (true) {
client.readInfo();
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
client.sendInfo(msg);
}
}
}
3 零拷贝
- 从操作系统的角度来说,因为内核缓冲区之间,没有数据是重复的
- 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算
sendFile优化 2.1版本不是 2.4版本是零拷贝
mmap 和 sendfile 的区别
- mmap 适合小数据量读写,sendFile 适合大文件传输
- mmap 需要4次上下文切换,3次数据拷贝:sendFile 需要3次上下文切换,最少2次数据拷贝
- sendFile 可以利用DMA方式,减少CPU拷贝,mmap 则不能(必须从内核拷贝到Socket缓冲区)
案例
// 传统IO服务器端
public class OldIOServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(7001);
while (true) {
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
try {
byte[] bytes = new byte[4096];
while (true) {
int readCount = dataInputStream.read(bytes, 0, bytes.length);
if (-1 == readCount) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// 传统IO服务器端
public class OldIOClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 7001);
String fileName = "";
InputStream inputStream = new FileInputStream(fileName);
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] bytes = new byte[4096];
long readCount;
long total = 0;
long startTime = System.currentTimeMillis();
while ((readCount = inputStream.read(bytes)) > 0) {
total += readCount;
dataOutputStream.write(bytes);
}
System.out.println("发送总字节数: " + total + " 耗时:" + (System.currentTimeMillis()-startTime));
dataOutputStream.close();
socket.close();
inputStream.close();
}
}
// 新
public class NewIOServer {
public static void main(String[] args) throws Exception {
InetSocketAddress address = new InetSocketAddress(7002);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket socket = serverSocketChannel.socket();
socket.bind(address);
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
int readCount = 0;
while (-1 != readCount) {
try {
readCount = socketChannel.read(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
break;
}
byteBuffer.rewind(); // 倒带 position = 0 mark = -1(作废)
}
}
}
}
public class NewIOClient {
public static void main(String[] args) throws Exception {
InetSocketAddress address = new InetSocketAddress("127.0.0.1",7002);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(address);
String fileName = "";
FileChannel channel = new FileInputStream(fileName).getChannel();
long startTime = System.currentTimeMillis();
// linux 下一个transferTo方法就可以完成传输
// windows 下调用只能发8m,就需要分段传输文件,要注意传输位置 需要循环计算
// 使用零拷贝
long transferCount = channel.transferTo(0, channel.size(), socketChannel);
System.out.println("发送总字节数: " + transferCount + " 耗时:" + (System.currentTimeMillis()-startTime));
channel.close();
}
}
4 AIO 了解
- JDK 7 引入Asynchronous I/O ,即AIO.在进行I/O编程中,常用到两种模式;Reactor 和 Proactor。Java的NIO就是Reactor,当有事件触发时,服务器端得到通知,进行相应处理
- AIO即NIO 2.0 ,叫异步不阻塞IO.AIO引入异步通道的概念,采用了proactor模式,简化了程序的编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
- 目前AIO没有被广泛应用,Netty也是基于NIO,而不是AIO
| BIO | NIO | AIO | |
|---|---|---|---|
| IO模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
| 编程难度 | 简单 | 复杂 | 负载 |
| 可靠性 | 差 | 好 | 好 |
| 吞吐量 | 低 | 高 | 高 |