目录
2.网络IO (ServerSocketChannel、SocketChannel)
一.了解NIO
1.什么是NIO
首先我们要明白传统IO比如InputStream,OutputStream,在执行read()和write()时是会阻塞的,直到数据准备好才会被唤醒。
NIO,是一种非阻塞IO,NIO 从JDK1.4提出的,本意是New l0,它的出现为了弥补传统I0的不足,提供了更高效的方式
NIO在处理网络数据时底层用到(select/poll)模型 多路复用 (可以提升服务端处理连接的数量)jdk1.5使用了epoll代替了select/poll
2.NIO的新特性
基于通道(Channel)和缓冲区(Buffer)操作:
- 通道(Channel) : 1个新的原始I/O抽象
- 缓冲支持(Buffer) :为所有的原始类型提供缓冲支持
- 具体操作:1.数据从 通道 读取到缓冲区、2..数据从 缓冲区 写入通道
非阻塞:
提供多路非阻塞式的I/O操作
选择器(Selectors):
- 用于监听多个通道的事件,如:连接打开、数据到达
- 单个线程可监听多个数据通道
其他:
- 提供字符集编码、解码的解决方案:Java.nio.charset
- 支持锁&内存映射文件的文件访问接口
3.IO和NIO的区别
二.核心组件
通道(`Channel `) : Java NIO数据来源,可以是网络,也可以是本地磁盘
缓冲区(`Buffer `):数据读写的中转区,
选择器(`Selectors '):异步I0的核心类,可以实现异步非阻塞I0,一个selectors可以管理多个通道Channel
1.Channel
Channel的实现
FileChangel:从文件中读写数据
DatagramChannel:通过UDP协议读写网络中的数据
SocketChannel:通过TCP协议读写网络中的数据
ServerSocketChannel:监听一个TCP连接,对于每一个新的客户端连接都会创建一个SocketChannel.
2.Buffer
什么是Buffer
buffer是一个对象,它包含了需要写入或者刚读出的数据,最常用的缓冲区类型是 ByteBuffer,数据IO必须经过这个buffer
Buffer的本质
缓冲区本质上是一块可以写入数据,以及从中读取数据的内存,实际上也是一个byte[]数据,只是在NIO中被封装成了NIO Buffer对象,并提供了一组方法来访问这个内存块,要理解buffer的工作原理,需要知道几个属性
- capacity 初始化时的容量大小
- position 表示我们要操作元素的位置 他是要不断变化的
- limit 表示可以操作元素的最大的下标(读模式中他应该和我们的容量相等,写模式中他应该代表我们缓存区最后一个元素的位置)
图解
第一次读取数据
第二次读取数据
转换写的时候一定要调用flip操作,他会改变position 和 limit的位置
数据写出 position会不断移动
3.Selector
什么是Selector
Selector(选择器,多路复用器)是Java NIO中能够检测一到多个NIO通道,是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
Selector工作流程
三.代码演示
1.与本地磁盘IO (FileChannel)
实现一个文件copy功能
public class CopyDemo {
public static void main(String[] args) {
//实现文件的复制
try {
// FileInputStream fi = new FileInputStream(new File("d:/111/123.txt"));
// FileOutputStream fo = new FileOutputStream(new File("d:/111/123_cp.txt"));
// //NIO 通道Channel 缓存buffer
// //创建通道
// FileChannel fic = fi.getChannel();
// FileChannel foc = fo.getChannel();
FileChannel fic = FileChannel.open(Paths.get("d:/111/123.txt"),
StandardOpenOption.READ);
FileChannel foc = FileChannel.open(Paths.get("d:/111/123_cp.txt"),
StandardOpenOption.CREATE, StandardOpenOption.Write);
//初始缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// byte[] buffer = new byte[1024]; //两种方式效果相同
// ByteBuffer.wrap(buffer);
int i = 0;
while (true) {
//读操作
if ((i = fic.read(buffer)) == -1) {
break;
}
buffer.flip();//把读模式转换成写模式
//写操作
foc.write(buffer);
buffer.clear();//清空缓存区
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.网络IO (ServerSocketChannel、SocketChannel)
关键代码
server:
创建ServerSocketChannel管道与 ServerSocket相似
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
设置连接的方式 默认为阻塞连接 默认参数为ture ture代表阻塞连接 false非阻塞
serverSocketChannel.configureBlocking(false);
将管道绑定一个套接字地址 客户端可以根据这个端口进行连接 也就是服务端的监听端口
serverSocketChannel = serverSocketChannel.bind(new InetSocketAddress(7486));
接受与此通道的套接字建立的连接 与ServerSocket.accept相似
SocketChannel socketChannel = serverSocketChannel.accept();
client:
创建SocketChannel管道
SocketChannel socketChannel = SocketChannel.open();
创建一个连接
socketChannel.connect(new InetSocketAddress("localhost",7486));
客户端设置为非阻塞IO
socketChannel.configureBlocking(false);
改为非阻塞IO后 在IO操作的代码前要判断是否连接上 连接上了才可以做后续的操作
//判断是否已经连接上了 if (socketChannel.isConnectionPending()) { //如果建立连接失败 finishConnect() 会抛出异常 socketChannel.finishConnect(); }在读服务端数据的IO操作代码前最好睡一秒 因为是非阻塞IO 客户端可能没收到服务端发送的消息就执行结束了 会导致服务端报错,客户端接收到的数据不准确
Thread.sleep(1000);
server端
public class Server {
public static void main(String[] args) {
try {
//ServerSocketChannel 支持两种模式:阻塞 和 非阻塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//默认为ture ture代表阻塞连接 false非阻塞
serverSocketChannel.configureBlocking(false);
//相当与 ServerSocket 创建一个监听
//new InetSocketAddress 创建一个套接字地址
serverSocketChannel = serverSocketChannel.bind(new InetSocketAddress(7486));
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
//非阻塞就需要加上判断 判断是有连接 若有才能进行网络IO
if (socketChannel != null) {
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer); //读
System.out.println(new String(buffer.array()));
//sleep 5秒
// Thread.sleep(5000);
String msg = "Hello,I`m Server";
buffer.clear();
buffer.put(msg.getBytes());
buffer.flip();//读切换写模式
socketChannel.write(buffer); //写
buffer.clear();
} else {
Thread.sleep(1000);
System.out.println("等待客户端连接");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
client端
public class Client {
public static void main(String[] args) {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);//设置非阻塞IO
socketChannel.connect(new InetSocketAddress("localhost",7486));
ByteBuffer buffer = ByteBuffer.allocate(1024);
//判断是否已经连接上了
if (socketChannel.isConnectionPending()) {
//如果建立连接失败 finishConnect() 会抛出异常
socketChannel.finishConnect();
}
String msg = "hello,I`m client01!";
buffer.put(msg.getBytes());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
//睡一秒等待服务端发送消息 因为是非阻塞IO 客户端可能没收到服务端发送的消息就执行结束了
Thread.sleep(1000);
socketChannel.read(buffer);
System.out.println(new String(buffer.array()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.Selector
server
public class SelectorServer {
private static Selector selector;
public static void main(String[] args) {
try {
selector = Selector.open(); //创建一个多路复用器
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); //必须设为非阻塞连接
//绑定连接端口
serverSocketChannel.bind(new InetSocketAddress(7486));
//把监听注册到selector 每次注册会的得到一个相对的钥匙SelectionKey
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); //阻塞 直到查询要有连接准备好了数据
//获取已经准备好的selectedKeys
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//用迭代器的方式遍历
if (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();//拿到后一定要删除 避免重复处理
//如果是个连接时间
if (selectionKey.isAcceptable()) {
handleAccept(selectionKey);
} else if (selectionKey.isReadable()) {
//读事件
handleRead(selectionKey);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//TODO 关闭
}
}
private static void handleAccept(SelectionKey selectionKey) throws IOException {
//获取ServerSocketChannel管道
//每个SelectionKey相当于一个要是 他有对应的通道
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); //改为非阻塞IO
ByteBuffer buffer = ByteBuffer.wrap("Hello,I`m Server".getBytes());
socketChannel.write(buffer);
//把socketChannel的读事件注册
socketChannel.register(selector,SelectionKey.OP_READ);
}
private static void handleRead(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
System.out.println(new String(buffer.array()));
}
}
client
public class SelectorClient {
private static Selector selector;
public static void main(String[] args) {
try {
selector = Selector.open();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); //改为非阻塞IO
socketChannel.connect(new InetSocketAddress("localhost",7486));
socketChannel.register(selector, SelectionKey.OP_CONNECT);
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
if (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();//删除
if (selectionKey.isConnectable()) {
//连接事件
handleConnect(selectionKey);
} else if (selectionKey.isReadable()) {
//读事件
handleRead(selectionKey);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//TODO 关闭
}
}
private static void handleConnect(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.configureBlocking(false); //改为非阻塞IO
if (socketChannel.isConnectionPending()) {
socketChannel.finishConnect();
}
socketChannel.write(ByteBuffer.wrap("Hello,I`m Client".getBytes()));
socketChannel.register(selector,SelectionKey.OP_READ);
}
private static void handleRead(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
System.out.println(new String(buffer.array()));
}
}
4.零拷贝
transferTo方法可以实现零拷贝,我们要传输的文件么不用先放在buffer中,在进行传输,减少了从内核空间在复制到用户空间的过程。
client
public class ZeroCopyClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",7486));
FileChannel fileChannel = new FileInputStream("d:/111/test.zip").getChannel();
long position = 0;
long size = fileChannel.size();
//f 代表一次传输的大小
long f = 0;
while (size > 0) {
//因为transferto一次最多只能copy 8M大小的数据 所以我们需要多次传输
f = fileChannel.transferTo(position,size,socketChannel);
if (f > 0) {
position += f;
size -= f;
}
}
System.out.println("传输结束传输了总字节数" + fileChannel.size());
//TODO 关闭流
socketChannel.close();
fileChannel.close();
}
}
server
public class ZeroCopyServer {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(7486));
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer buffer = ByteBuffer.allocate(1024);
FileChannel fileChannel = new FileOutputStream("d:/111/test_cp.zip").getChannel();
int i = 0;
while (i != -1) {
i = socketChannel.read(buffer);
buffer.flip();
fileChannel.write(buffer);
buffer.clear();
}
System.out.println("copy完成");
//TODO 关闭流
fileChannel.close();
socketChannel.close();
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}