BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
BIO
网络编程的基本模型是C/S模型,即两个进程间的通信。
服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。 传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
简单的描述一下BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通宵模型。
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就挂了。这种通信模型在高并发的场景是没法应用的。
NIOnon-blocking IO, NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现。新增的着两种通道都支持阻塞和非阻塞两种模式。
阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。
NIO编程是面向通道的(BIO是面向流的),流分为写入/写出,是单向的,意味着通道是可以进行双向读写的。NIO所有基于channel的API对数据的操作都是间接通过操作缓冲区ByteBuffer
In : 磁盘 --通道–> ByteBuffer(内存)–>数据(内存)
Out: 数据(内存)–>ByteBuffer(内存) --> 通道 --> 磁盘
转原文链接:https://blog.csdn.net/Zhang_Pengcheng/article/details/80048134
2.不管读还是写,都要有Buffer
Buffer中的三个属性元素:capacity,limit,position
flip函数:
capacity :指的是Buffer包含的元素个数(容量)不可能为负数,由方法allocate(n)来决定大小或其他方法,分配好后,capacity不可能再变化;
limit: 无法被读或写的第一个元素索引,不可能为负数,不能超过它的capacity
position: 下一个将要读或写的元素的索引r>
/**
* Flips this buffer. The limit is set to the current position and then
* the position is set to zero. If the mark is defined then it is
* discarded.
* After a sequence of channel-read or put operations, invoke
* this method to prepare for a sequence of channel-write or relative
* get operations. For example:
*
* buf.put(magic); // Prepend header
* in.read(buf); // Read data into rest of buffer
* buf.flip(); // Flip buffer
* out.write(buf); // Write header + data to channel
* This method is often used in conjunction with the {@link
* java.nio.ByteBuffer#compact compact} method when transferring data from
* one place to another.
* @return This buffer
*/
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
当调用ByteBuffer.allocate(n)分配字节数组大小时,初始化的状态图如下,设置n=6
调用put或read函数读入4个元素时候变化如下
此时调用flip()函数,反转,position回到第一个位置,limit来到原来position的位置,状态如下
此时最多可以写入wirte4个元素,position来到limit的位置:
再次调用flip(),postion回到第一个位置,limit不变
clear函数重新初始化Buffer,恢复原来开始的状态。(数据还在,状态迁移,再次操作覆盖原来数据操作,类似初始化)
rewind函数:重新再读取一次,设置limit不变,position=0;
线程安全:Buffer在多线程并发环境下并不是安全的,如果有多个线程访问环境下,需要使用synchronization
同步操作。
通过NIO读取文件的三个步骤:
从FileInputStream获取到FileChannel对象。
创建Buffer
将数据从Channel读取到Buffer中
绝对方法与相对方法的含义:
1.相对方法: limit值与position值会在操作时考虑到
2.绝对方法: 完全忽略掉limit值与position值
我们可以随时将一个普通Buffer转换成只读Buffer,而只读Buffer不能转回原来标准Buffer
Buffer类中有一个叫address的long类型字段,标记的是java堆外native(操作系统层)要操作数据的地址,可以直接进行地址操作
1.通过HeapByteBuffer的方法是先把java堆上的数据拷贝到Native堆外的额外开辟的一个空间上,再与IO设备打交道,内存由操作系统自动维护回收;
而DirectByteBuffer则是直接把堆上的数据拷贝到Native的数据空间上。不用额外拷贝到另外一个开辟的空间为零拷贝,数据空间的内存回收是当DirectByteBuffer被回收时,通过address地址能找到这个堆外空间的地址进行回收。
可以直接在堆外内存修改数据,数据跟IO设备交互由操作系统自动完成。
public static void main(String []args)throws Exception{
RandomAccessFile randomAccessFile=new RandomAccessFile("NioTest9.txt","rw");
FileChannel fileChannel=randomAccessFile.getChannel();
//直接在内存中修改数据
//通过map拿到内存映射对象
MappedByteBuffer mappedByteBuffer=fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5);
//把第0个元素改为a
mappedByteBuffer.put(0,(byte)'a');
mappedByteBuffer.put(3,(byte)'b');
randomAccessFile.close();
}
public static void main(String []args)throws Exception{
RandomAccessFile randomAccessFile=new RandomAccessFile("NioText10.txt","rw");
FileChannel fileChannel=randomAccessFile.getChannel();
//获取文件锁. 从第3个位置锁,锁6个长度,ture为共享锁,false为排它锁
FileLock fileLock=fileChannel.lock(3,6,true);
System.out.println("valid: "+fileLock.isValid());
System.out.println("lock type: "+fileLock.isShared());
//释放锁
fileLock.release();
randomAccessFile.close();
}
是阻塞的,使用多线程new Thread技术能简单解决并发,一个连接新建一个线程,但是浪费资源严重,每台机器的线程数量,cpu有上限最大值。(适用于小型服务,用户量比较少的应用)
服务端用一个线程可以处理多个客户端的请求,定义一个Selector监听多个端口的事件event。NIO是基于事件触发的通信
不在此介绍源码,自己在本地查看
/**
* Returns the channel for which this key was created. This method will
* continue to return the channel even after the key is cancelled.
*
* @return This key's channel
*/
public abstract SelectableChannel channel();
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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;
import java.util.Set;
public class NioTest12 {
public static void main(String[]args) throws IOException {
//定义监听的5个端口号
int []ports=new int[5];
ports[0]=5000;
ports[1]=5001;
ports[2]=5002;
ports[3]=5003;
ports[4]=5004;
Selector selector=Selector.open();
for (int i=0;i selectionKeys= selector.selectedKeys();
System.out.println("selectionKeys: "+selectionKeys);
Iteratoriter= selectionKeys.iterator();
while (iter.hasNext()){
SelectionKey selectionKey=iter.next();
if (selectionKey.isAcceptable()){
//获取到ServerSocketChannel
ServerSocketChannel serverSocketChannel= (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel=serverSocketChannel.accept();
socketChannel.configureBlocking(false);
//关注读
socketChannel.register(selector, SelectionKey.OP_READ);
//一定要 iter.remove(); 表示当前事件使用完了
iter.remove();
System.out.println("获得客户端连接:"+socketChannel);
}else if (selectionKey.isReadable()){//读
SocketChannel socketChannel= (SocketChannel) selectionKey.channel();
int bytesRead=0;
while (true){
ByteBuffer byteBuffer=ByteBuffer.allocate(512);
byteBuffer.clear();
int read=socketChannel.read(byteBuffer);
if(read<=0)
break;
byteBuffer.flip();
socketChannel.write(byteBuffer);
bytesRead+=read;
}
System.out.println("读取: "+bytesRead+" , 来自于: "+socketChannel);
iter.remove();
}
}
}
}
}