java IO模式
1.1 同步,异步,阻塞,非阻塞
首先了解一下同步,异步,阻塞,非阻塞
同步和异步关注的是**消息通信机制**
- 同步
同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。 - 异步
异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
- 阻塞调用
是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果后才会返回 - 非阻塞调用
在不能得到结果之前,该调用不会阻塞当前线程
1.2 三种 io模式
- java bio
同步阻塞型,服务器实现为一个连接一个线程,客户端有连接请求时服务器端需要启动一个线程进行处理,连接不做任何事情会造成不必要的线程开销
- java Nio
同步非阻塞,服务器实现模式为一个线程处理多个请求,即客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到连接有i/o请求就进行处理
- java aio
nio(2.0) :异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的i/o请求都是由os先完成了在通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用。
适用场景
- BIO 方式适用于连接数较小且固定的架构
- NIO 方式适用于连接数较多且连接比较短的架构,如 聊天服务器,弹幕系统,服务器间通讯
- AIO 方式适用于连接数较多且连接比较长 如相册服务器,充分调用os 参与并发操作
2 JAVA BIO
- java bio 是传统 java io 线程
服务端逻辑
*/
public class Server {
public static void main(String[] args) {
try{
//1 定义一个server socket 对象进行服务端的端口注册
ServerSocket ss = new ServerSocket(9999);
//2 监听客户端socket请求
Socket socket = ss.accept();
//3 获取字节输入流
InputStream inputStream = socket.getInputStream();
// 包装成缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String msg ;
while((msg= br.readLine())!=null){
System.out.println("服务器接受到:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
public class Client {
public static void main(String[] args) throws IOException {
//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 服务端 ");
ps.flush();
}
}
客户端挂掉,服务端也会挂
- 在上面通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态
- 同时服务端是按照行获取消息的,这意味着客户端必须按照行进行消息的发送,否则客户端将进入等待消息
2.1 BIO 传统多发和多收
客户端,服务端 多发多收模式
/**
* 客户端 多发模式
*/
public class Client {
public static void main(String[] args) throws IOException {
//1创建 socket 对象请求服务端连接
Socket socket = new Socket("127.0.0.1",9999);
//2 从SOcket 对象中获取一个字节输出流
OutputStream os = socket.getOutputStream();
// 3 把字节输出流包装成一个打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true){
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
}
}
在这里服务端只能接受一个客户端,是因为服务端只有一个线程,只能接受一个客户端
2.2 BIO 服务端接受多个客户端
服务端
/**
* 同时接受多个客户端socket通信请求
* 接受一个客户但socket请求对象后,交给一个独立
*/
ServerSocket ss;
{
try {
// 注册端口
ss = new ServerSocket(9999);
while(true){
Socket socket =ss.accept();
// 创建一个独立的线程来处理与客户端连接的请求
new ServerThreadReader(socket).start();
}
socket独立线程
public class ServerThreadReader extends Thread{
private Socket socket;
public ServerThreadReader(Socket socket){
this.socket =socket;
}
@Override
public void run() {
try {
InputStream is =socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg=null;
while((msg=br.readLine())!=null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
总结
- 每个socket 接收到,都会创建一个线程,线程的竞争,切换上下文影响性能
- 每个线程都会占用栈空间和cpu资源
- 客户端的并发访问增加时,服务端将呈现1:1的线程开销
3 伪异步io 编程
由于客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出。
我们可以采用伪异步i/o通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的socket封装成一个task,交给后端的线程池中进行处理。jdk线程池维护一个消息队列和N个活跃的线程,对消息对列中socket任务进行处理。
服务端设计 初始化socket 线程池对象,最后封装成一个任务对象交给线程处理
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(9999);
//定义循环来接受客户端的socket编程请求
//初始化一个线程池对象
HandlerSocketServerPool socketServerPool = new HandlerSocketServerPool(10,6);
while (true){
Socket socket =ss.accept();
// 把socket对象交给一个线程池进行处理
//把socket封装成一个任务对象交给线程处理
Runnable target = new ServerRunnableTarget(socket);
socketServerPool.execute(target);
}
} catch (IOException e) {
e.printStackTrace();
}
}
线程池对象:用于创建线程池
//1 创建一个线程池的成员变量用于存储线程池对象
private ExecutorService executorService;
/**
*
* @param maxThreadNum 最大线程数量
* @param queueSize 任务数量
*/
public HandlerSocketServerPool(int maxThreadNum,int queueSize){
executorService = new ThreadPoolExecutor(3,maxThreadNum,120, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(queueSize));
}
/**
* 提供一个方法来提交任务给线程池的任务队列来暂存,等待线程池来处理
*/
public void execute(Runnable target){
executorService.execute(target);
}
任务对象:将socket封装成任务 runable
public class ServerRunnableTarget implements Runnable {
private Socket socket;
public ServerRunnableTarget(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
// 处理接受到的socket
//3 获取字节输入流
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
// 包装成缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String msg ;
while((msg= br.readLine())!=null){
System.out.println("服务器接受到:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 伪异步io采用了线程池实现,避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,由于底层依然采用的是同步阻塞,无法解决问题
- 如果线程池都被阻塞,那么socket中的io消息将在队列中排队,客户端会发生大量连接超时
2.4 基于BIO实现文件上传
客户端实现将本地文件传送到socket中
public static void main(String[] args) {
try{
//请求于服务端的socket连接
Socket socket = new Socket("127.0.0.1",8888);
//将字节输出流包装成一个数据输出流
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
//上传后缀
dataOutputStream.writeUTF(".png");
//把文件数据发送给服务端进行接收
InputStream is = new FileInputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\下载.png");
byte[] bs = new byte[1024];
int len =0;
while((len= is.read(bs))>0){
dataOutputStream.write(bs,0,len);
}
dataOutputStream.flush();
is.close();
socket.shutdownOutput();//通知服务器这边数据发送完毕
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
服务端监听socket,并将其封装成线程
public static void main(String[] args) {
try{
ServerSocket ss = new ServerSocket(8888);
while(true){
Socket socket =ss.accept();
new ServerReaderThread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
服务端线程的封装
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket =socket;
}
@Override
public void run() {
try {
//1 得到一个数据输入流获取客户端发过来的数据
DataInputStream dis = new DataInputStream(socket.getInputStream());
//2 读取客户端发送过来的文件类型
String suffix =dis.readUTF();
// 定义一个字节输出管道负责把客户端发来的文件数据写出去
OutputStream os = new FileOutputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\server\\"+ UUID.randomUUID().toString()+suffix);
byte[] bytes = new byte[1024];
int len=0;
while((len=dis.read(bytes))>0){
os.write(bytes,0,len);
}
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.5 java BIO 模式下的端口转发思想
实现一个客户端发送的消息可以发送给所有客户端
3 JAVA NIO
- NIO 有三大核心部分:channel(通道),buffer(缓冲区),selector (选择器)
- java NIO 非阻塞模式,使一个线程从某通道发送请求或读取数据 ,仅能得到目前可用的数据,没有数据可用,就不会获取。
3.1 NIO 与 BIO的比较
- BIO 以流的方式处理数据,NIO以块的方式处理数据
- BIO 是阻塞的,NIO是非阻塞的
- BIO基于字节流和字符流进行操作,NIO基于Channel和Buffer进行操作,数据是从通道读取到缓冲区中,或者从缓冲区写入到通道中 Selector(选择器)用于监听多个通道事件,使用单个线程就可以监听多个客户端通道。
NIO | BIO |
面向缓冲区(Buffer) | 面向流(Stream) |
非阻塞 | 阻塞 |
选择器 selectors |
缓冲区
本质是一块可以写入数据,可以从中读取数据的内存,这块内存被包装成NIO BUFFER 对象
Channel
java NIO 的通道类似流,既可以从通道中读取数据,也可以写数据到通道,但流的读写是单向的,通道可以非阻塞读取和写入通道,通道支持读取或写入缓冲区,支持异步地读写
Selector选择器
selector 可以检查一个或多个NIO通道,确定哪些通道准备好进行读取或写入,一个单独线程可以管理多个Channel,从而管理多个网络连接,提高效率
通道表示打开到IO设备 如文件,套接字,负责传输,Buffer 负责存取数据
3.1 缓冲区
用于特定基本数据类型的容器,是Buffer抽象类的子类,Buffer 主要用于与NIO通道进行交互
Buffer类
像一个数组,保存多个相同类型的数据
如 ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer
Buffer中具有以下的概念
- 容量:buffer的固定大小,创建后不能更改
- 限制:表示缓冲区中可以操作数据的大小,不能为负,且不能大于其容量
- 位置:下一个要读取或写入的数据
- 标记:一个索引可以通过mark方法回到位置
使用Buffer读写数据一般遵循以下四个步骤:
- 1.写入数据到Buffer
- 2.调用flip()方法,转换为读取模式
- 3.从Buffer中读取数据
- 4.调用buffer.clear()方法或者buffer.compact()方法清除缓冲区
直接内存与非直接内存
- 对于直接内存,jvm将会在IO操作上具有更高的性能,因为直接作用与本地系统的IO操作,非直接内存,会先从本进程复制到直接内存,在利用本地IO处理
本地IO------>直接内存------>非直接内存--------->直接内存-------->本地IO
而直接内存是
本地IO------>直接内存------>本地Io
直接内存使用allocateDirect创建,但是需要耗费更高的性能
使用场景
- 有很大数据需要存储
- 适合频繁的IO操作
3.2 通道(Channel)
Channel表示IO源与目标打开的连接,channel类似于传统的流,不过其本身不能直接访问数据,Channel 只能与BUffer进行交互.
通道特点:
- 通道可以同时进行读写,而流只能读或写
- 通道可以实现异步读写数据
常用的Channel实现类
- FileChannel:用于读取,写入,映射和操作文件的通道
- DatagramChannel:通过UDP读写网络中的数据通道
- SocketChannel:通过TCP 读写网络中的数据
- ServerSocketChannel 可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel.
获取通道的一种方式是对支持通道的对象调用getChannel() 方法
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) 强制将所有对此通道的文件更新写入到存储设备中
3.3 通道写出文件
public void write(){
try {
// 字节输出流通向目标文件
FileOutputStream fos = new FileOutputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\server\\新建 文本文档.txt");
//2 得到字节输出流对应的通道 channel
FileChannel channel = fos.getChannel();
//3 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello".getBytes());
//4把缓冲区切换成可读模式
buffer.flip();
channel.write(buffer);
channel.close();
System.out.println("数据写出文件");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
3.4 通道读取文件
public void read() throws IOException {
//定义一个文件输入流与源文件接同
FileInputStream fis = new FileInputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\server\\新建 文本文档.txt");
//需要得到文件字节输入流的文件通道
FileChannel channel = fis.getChannel();
//定义一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//写入数据到缓冲区
channel.read(buffer);
buffer.flip();
String rs = new String(buffer.array(),0,buffer.remaining());
System.out.println(rs);
}
3.5 复制文件逻辑
public void copy() throws IOException {
// 定义文件位置
File srcFile = new File("F:\\360MoveData\\Users\\DELL\\Desktop\\下载.png");
File copyFile = new File("F:\\360MoveData\\Users\\DELL\\Desktop\\下载_copy.png");
//获取文件输入流
FileInputStream fis = new FileInputStream(srcFile);
//获取文件输出流
FileOutputStream fos = new FileOutputStream(copyFile);
//获取文件通道
FileChannel fileChannel = fis.getChannel();
FileChannel copyChannel = fos.getChannel();
// 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(true){
// 必需先清空缓冲区
buffer.clear();
//写入数据到缓冲区
int len= fileChannel.read(buffer);
if(len<0){
break;
}
//切换模式为可读模式
buffer.flip();
copyChannel.write(buffer);
}
fileChannel.close();
copyChannel.close();
System.out.println("复制文件成功!");
}
也可以使用transferFom或transferTo进行复制文件
@Test
public void copy_trans() throws IOException {
// 定义源文件位置
FileInputStream srcFile = new FileInputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\下载.png");
FileChannel channel=srcFile.getChannel();
FileOutputStream copyFile = new FileOutputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\下载_copy.png");
FileChannel copyChannel = copyFile.getChannel();
//复制文件
// copyChannel.transferFrom(channel,channel.position(),channel.size());
channel.transferTo(channel.position(),channel.size(),copyChannel);
// 关闭原通道
channel.close();
//关闭现通道
copyChannel.close();;
}
4 选择器
选择器是selectableChannel对象的多路复用器,slelector可以同时监控多个selectableChannel的io状况,select可以使单独线程管理多个Channel
- java NIO ,用非阻塞的IO方式,使用一个线程,处理多个客户端连接,就会使用选择器
- selector 可以检测到多个注册的通道上是否有事件发生(多个channel以事件的方式可以注册到同一个selector),如果有时间发生,便获取事件然后针对每个事件进行相应的处理
- 只有在来连接真正有读写事件发生时,才会进行读写
服务器流程
- 当客户端连接服务端时,服务端通过serverSocketChannel得到SocketChannel
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 =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();
//判断这个事件是什么
if(sk.isAcceptable()){
// 直接获取当前接入的客户端通道
SocketChannel socketChannel = ssChannel.accept();
// 切换为非阻塞模式
socketChannel.configureBlocking(false);
//注册到选择器中
socketChannel.register(selector,SelectionKey.OP_READ);
}else if(sk.isReadable()){
// 获取当前选择器上的读选择器就绪事件
SocketChannel socketChannel = (SocketChannel) sk.channel();
//读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len=0;
while((len=socketChannel.read(buf))>0){
buf.flip();
System.out.println(new String(buf.array(),0,len));
buf.clear();
}
}
it.remove();//移除当前事件
}
}
}
}
客户端
public class Client {
public static void main(String[] args) throws IOException {
//1 获取通道
SocketChannel socketChannel =SocketChannel.open();
//切换成非阻塞模式
socketChannel.configureBlocking(false);
//分配缓冲区大小
ByteBuffer buffer =ByteBuffer.allocate(1024);
Scanner sc = new Scanner(System.in);
while(true){
String msg = sc.nextLine();
buffer.put(("你好"+msg).getBytes());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
}
}