NIO和BIO对比
NIO(non blocking I/O)非阻塞I/O,jdk1.4引入的新I/O,平时接触的文件的I/O操作是BIO,即阻塞I/O
BIO API使用
具体流程:
A.测试accept()方法的阻塞
public void testAccept() throws IOException{
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(9999));
Socket sk = ss.accept();
System.out.println("有连接连入");
}
JUnit测试,“有连接接入”没有输出,说明accept()方法产生阻塞了。
B.然后添加connect()方法测试的代码:
public void testContect() throws Exception{
Socket sk = new Socket();
sk.connect(new InetSocketAddress(
"127.0.0.1", 9999));
System.out.println("连接成功");
}
先运行服务器端方法(testAccept()),再运行客户端方法,发现accept()方法阻塞释放了。另外“连接成功”正确输出。如果不先启动服务器端方法,而直接运行客户端方法,发现先是阻塞了一下,然后JUnit测试抛出异常。
总结:connect()方法会产生阻塞,指定连接成功,阻塞才释放。
accept()方法产生的阻塞,直到服务器获得到连接后,阻塞才释放。
C.测试read()方法的阻塞性C1.
再次修改testAccept()方法
InputStream in= sk.getInputStream();
byte bts[] = new byte[1024];
in.read(bts);
System.out.println("读取到了数据:"+new String(bts));
C2.为了不让连接中断,需要修改
testConnect()while(true);
总结:read()方法会产生阻塞,直到读取到内容后,阻塞才被释放。
D.测试write()方法的阻塞性
D1.修改testAccept()方法
for(int i =1;i<100000;i++){
out.write("HelloWorld".getBytes());
System.out.println(i);
}
System.out.println("数据写完了。。。");
}
先运行服务器端方法,再运行客户端方法;发现i输出值为65513,阻塞了。
for(int i =1;i<200000;i++){
out.write(“Hello”.getBytes());
System.out.println(i);
}
微调代码,输出到131026阻塞了。
总结:write()方法也会产生阻塞,write()一直往出写数据,但是没有任何一方读取数据,直到写出到一定量(我的是655130B,不同电脑可能不同)的时候,产生阻塞。向网卡设备缓冲区中写数据。
NIO 相关API
Channel查看API
ServerSocketChannel, SocketChannel基于NIO的(基于tcp实现的,安全的基于握手机制)DatagramChannel基于UDP协议,不安全
NIO-Channel API(上)
accept和connect使用
/**ServerSocketChannel.open()创建服务器端对象
* nio提供两种模式:阻塞模式和非阻塞模式
* 默认情况下是阻塞模式。
* 通过ssc.configureBlocking(false)设置为非阻塞模式
* @throws Exception
*/
@Test
public void testAccept() throws Exception{
//创建服务器端的服务通道
ServerSocketChannel ssc =
ServerSocketChannel.open();
//绑定端口号
ssc.bind(new InetSocketAddress(8888));
//设置非阻塞模式
ssc.configureBlocking(false);
//调用accpet方法获取用户请求的连接通到
SocketChannel sc = ssc.accept();
System.out.println("有连接连入");
}
运行发现,并没有输出“有连接接入”,通道提供阻塞和非阻塞两种模式,默认为阻塞模式。可以在bind port之前添加ssc.configureBlocking(false);设置通道的非阻塞模式。再次运行“有连接接入”便输出了。
public void testConnect() throws Exception{
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("127.0.0.1", 8888));
System.out.println("连接成功");
}
为加sc.configureBlocking(false);之前,运行该方法抛出异常,并没有输出“连接成功”,通道的connect()方法也是阻塞的;使用方法sc.configureBlocking(false);可以将客户端连接通道设置为非阻塞模式。
read()、write()方法测试(过度)
sc.read(ByteBuffer dst)
sc.write(ByteBuffer src)
由于这两个方法都需要ByteBuffer对象作为参数,所以我们需要先讲ByteBuffer缓冲区。
NIO-ByteBuffer缓冲区API
public class DemoByteBuffer {
/**ByteBuffer缓冲区类,有三个重要的属性
* capacity 10:容量,该缓冲区可以最多保存10
个字节
* position 0:表示位置
* limit 10:限制位(用在获取元素时限制获取的边界)
*/
@Test
public void testByteBuffer(){
ByteBuffer buf = ByteBuffer.allocate(10);
System.out.println();
}
/**put(byte bt)向缓存区中添加一个字节
* 每调用一次该方法position的值会加一。
*/
@Test
public void testPut(){
ByteBuffer buf = ByteBuffer.allocate(10);
byte b1 = 1;
byte b2 = 2;
buf.put(b1);
buf.put(b2);
buf.putInt(3);
System.out.println();
}
/**get()获取position指定位置的一个字节内容。
* 每调用一次该方法,position++;
* 如果在调用get()时,position>=limit,
* 则抛出异常BufferUnderflowException
*
* position(int pt):设置position的值为pt
* position():获取当前缓冲区的position属性的值
* limit(int):设置限制为的值
* limit():获取当前缓冲区的limit属性的值。
*/
@Test
public void testGet(){
ByteBuffer buf = ByteBuffer.allocate(10);
byte b1 = 1;
byte b2 = 2;
buf.put(b1);//1
buf.put(b2);//2
//设置position的值为0
buf.position(0);
//设置限制位(不想让用户获取无用的信息)
buf.limit(2);
System.out.println(buf.get());//
System.out.println(buf.get());
System.out.println(buf.get());
}
/**flip()方法:反转缓存区,一般用在添加完数据后。
* limit = position;将limit的值设置为当前position的值
position = 0;再将position的值设置为0
*/
@Test
public void testFlip(){
ByteBuffer buf = ByteBuffer.allocate(10);
byte b1 = 1;
byte b2 = 2;
buf.put(b1);//1
buf.put(b2);//2
/*buf.limit(buf.position());
buf.position(0);*/
buf.flip();
}
/**clear():"清除缓存区"
* 底层源代码:
* position = 0;
limit = capacity;
通过数据覆盖的方式达到清除的目的。
*/
@Test
public void testClear(){
ByteBuffer buf = ByteBuffer.allocate(10);
byte b1 = 1;
byte b2 = 2;
buf.put(b1);//1
buf.put(b2);//2
buf.clear();
byte b3=33;
buf.put(b3);
buf.flip();
for(int i = 0;i<buf.limit();i++){
System.out.println(buf.get());
}
}
/**hasRemaining()判断缓冲区中是否还有有效的数据,有返回
* true,没有返回false
* public final boolean hasRemaining() {
return position < limit;
}
*/
@Test
public void testClear12(){
ByteBuffer buf = ByteBuffer.allocate(10);
byte b1 = 1;
byte b2 = 2;
buf.put(b1);//1
buf.put(b2);//2
buf.clear();
byte b3=33;
buf.put(b3);
buf.flip();
/*for(int i = 0;i<buf.limit();i++){
System.out.println(buf.get());
}*/
/*int i =0;
while(i<buf.limit()){
System.out.println(buf.get());
i++;
}*/
while(buf.hasRemaining()){
System.out.println(buf.get());
}
}
}
NIO-Channel API(下)
1、read()方法
修改ChanelDemo类的testAccept方法:
ByteBuffer buf = ByteBuffer.allocate(10);
sc.read(buf);
System.out.println("有数据读入:"+buf.toString());
testConnect()方法不做任何修改,先运行testAccept()方法,发现在sc.read(buf)行抛出了空指针异常。buf对象不可能为null,所以sc为null.
非阻塞编程最大的问题:不知道是否真正的有客户端接入,所以容易产生空指针;所以需要人为设置阻塞。
将SocketChannel sc = ssc.accept();改为:
while(sc==null){
sc = ssc.accept();
}
再次运行testAccept()方法,空指针的问题解决了;然后再运行testConnect()方法,发现连接能够正常建立,但是“有数据读入了。。”并没有输出,说明即使ssc服务通道设置了非阻塞,也没有改变得到的通道sc默认为阻塞模式,所以sc.read(buf)阻塞了。要不想让read()方法阻塞,需要在调用read()之前加sc.configureBlocking(false);这样即使没有读到数据,“有数据读入了。。”也能打印出来。
2、write()方法
修改testContect()方法,追加以下代码:
ByteBuffer buf = ByteBuffer.wrap("HelloWorld".getBytes());
sc.write(buf);
测试bug,先不运行服务器端方法,直接运行客户端方法testConnect(),输出“连接成功”,但是sc.write(buf)行抛出NotYetConnectException异常。sc为何抛出该异常?非阻塞模式很坑的地方在于不知道连接是否真正的建立。修改testConnect():
ByteBuffer buf = ByteBuffer.wrap("HelloWorld".getBytes());
while(!sc.isConnected()){
sc.finishConnect();
}
sc.write(buf);
再次运行testConnect(),之前的异常解决了,但是有出现了新的异常:
java.net.ConnectException: Connection refused: no further information
at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
先启动服务器端(testAccept()),后启动客户端(testConnect())即可。
手写NIO非阻塞模式难度较大,代码不是重点,重要在于引出设计思想。
Selector设计思想
问题的引入
使用BIO编写代码模拟一下(编写一个服务器端和客户端程序,运行一次服务器程序,运行四次客户端程序模拟四个用户线程)
public class BIOServer {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(7777));
while(true){
Socket sk = ss.accept();
new Thread(new ServiceRunner(sk)).start();
}
}
}
class ServiceRunner implements Runnable{
private Socket sk;
public ServiceRunner(Socket sk){
this.sk = sk;
}
public void run(){
System.out.println("提供服务的线程id:"+
Thread.currentThread().getId());
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class BIOClient {
public static void main(String[] args) throws Exception {
Socket sk = new Socket();
sk.connect(new InetSocketAddress("127.0.0.1", 7777));
while(true);
}
}
服务器启动
负责为客户端提供服务,当前线程的id:9
负责为客户端提供服务,当前线程的id:10
负责为客户端提供服务,当前线程的id:11
负责为客户端提供服务,当前线程的id:12
分析该模式的缺点:
缺点1:每增加一个用户请求,就会创建一个新的线程为之提供服务。当用户请求量特别巨大,线程数量就会随之增大,继而内存的占用增大,所有不适用于高并发、高访问的场景。
缺点2:线程特别多,不仅占用内存开销,也会占用大量的cpu开销,因为cpu要做线程调度。
缺点3:如果一个用户仅仅是连入操作,并且长时间不做其他操作,会产生大量闲置线程。会使cpu做无意义的空转,降低整体性能。
缺点4:这个模型会导致真正需要被处理的线程(用户请求)不能被及时处理。
解决方法
针对缺点3和缺点4,可以将闲置的线程设置为阻塞态,cpu是不会调度阻塞态的线程,避免了cpu的空转。所以引入事件监听机制实现。
Selector多路复用选择器,起到事件监听的作用。
监听哪个用户执行操作,就唤醒对应的线程执行。那么都有哪些事件呢?
事件:1.accept事件、2.connect事件、3.read事件、4.write
事件针对缺点1和缺点2,可以利用非阻塞模型来实现,利用少量线程甚至一个线程来处理多用户请求。但是注意,这个模型是有使用场景的,适用于大量短请求场景。(比如用户访问电商网站),不适合长请求场景(比如下载大文件,这种场景,NIO不见得比BIO好)
扩展知识
惊群现象,隐患:cpu的负载会在短时间之内聚升,最严重的情况时出现短暂卡顿甚至死机。第二个问题就是性能不高。
Selector服务通道API
accept事件
编写服务器端程序:
public class NIOServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(6666));
//设置为非阻塞
ssc.configureBlocking(false);
//定义多路复用选择器
Selector sel = Selector.open();
//注册accept事件
ssc.register(sel, SelectionKey.OP_ACCEPT);
while(true){
//select()在没有收到相关事件时产生阻塞,直到
//有事件触发,阻塞才会得以释放
sel.select();
//获取所有的请求的事件
Set<SelectionKey> sks = sel.selectedKeys();
Iterator<SelectionKey> iter = sks.iterator();
while(iter.hasNext()){
SelectionKey sk = iter.next();
if(sk.isAcceptable()){
ServerSocketChannel ssc1=
(ServerSocketChannel)sk.channel();
SocketChannel sc = ssc1.accept();
while(sc==null){
sc = ssc1.accept();
}
sc.configureBlocking(false);
//为sc注册read和write事件
//0000 0001 OP_READ
//0000 0100 OP_WRITE
//0000 0101 OP_READ和OP_WRITE
sc.register(sel, SelectionKey.OP_WRITE|SelectionKey.OP_READ);
System.out.println("提供服务的线程id:"+
Thread.currentThread().getId());
}
if(sk.isWritable()){
}
if(sk.isReadable()){
}
iter.remove();
}
}
}
}
编写客户端代码:
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("127.0.0.1", 6666));
//sc.configureBlocking(false);
System.out.println("客户端有连接连入");
while(true);
}
}
服务器端启动一次,客户端启动三次,服务器端的控制台输出:
服务器端启动
有客户端连入,负责处理该请求的线程id:1
有客户端连入,负责处理该请求的线程id:1
有客户端连入,负责处理该请求的线程id:1
处理多个请求使用同一个线程。
该设计架构只适用的高并发短请求的场景中。
read事件修改
Server类
if(sk.isReadable()){
//获取连接对象
SocketChannel sc = (SocketChannel)sk.channel();
ByteBuffer buf = ByteBuffer.allocate(10);
sc.read(buf);
System.out.println("服务器端读取到:"+new String(buf.array()));
//0000 0101 sk.interestOps()获取原事件
//1111 1110 !OP_READ
//0000 0100 OP_WRITE
//sc.register(sel, SelectionKey.OP_WRITE);
sc.register(sel, sk.interestOps()&~SelectionKey.OP_READ);
}
修改Client类
System.out.println("客户端连入");
ByteBuffer buffer = ByteBuffer.wrap(
"helloworld".getBytes());
sc.write(buffer);
while(true);
write事件
修改Servet
if(sk.isWritable()){
//获取SocketChannel
SocketChannel sc = (SocketChannel)sk.channel();
ByteBuffer buf = ByteBuffer.wrap("get".getBytes());
sc.write(buf);
//去掉写事件
sc.register(sel, sk.interestOps()&~SelectionKey.OP_WRITE);
}
修改Client类
public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("127.0.0.1", 6666));
while(!sc.isConnected()){
sc.finishConnect();
}
System.out.println("客户端有连接连入");
ByteBuffer buf = ByteBuffer.wrap(
"helloworld".getBytes());
sc.write(buf);
System.out.println("客户端信息已经写出");
ByteBuffer readBuf = ByteBuffer.allocate(3);
sc.read(readBuf);
System.out.println("客户端读到服务器端传递过来的信息:"
+new String(readBuf.array()));
while(true);
}
}
public class Client2 {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress(“127.0.0.1”, 9999));
//对于客户端,最开始要注册连接监听
Selector selector = Selector.open();
sc.register(selector, SelectionKey.OP_CONNECT);
while(true){
selector.select();
Set set = selector.selectedKeys();
Iterator iter = set.iterator();
while(iter.hasNext()){
SelectionKey sk = iter.next();
if(sk.isConnectable()){
}
if(sk.isWritable()){
}
if(sk.isReadable()){
}
iter.remove();
}
}
}
}
public class Client2 {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("127.0.0.1", 9999));
//对于客户端,最开始要注册连接监听
Selector selector = Selector.open();
sc.register(selector, SelectionKey.OP_CONNECT);
while(true){
selector.select();
Set<SelectionKey> set = selector.selectedKeys();
Iterator<SelectionKey> iter = set.iterator();
while(iter.hasNext()){
SelectionKey sk = iter.next();
if(sk.isConnectable()){
}
if(sk.isWritable()){
}
if(sk.isReadable()){
}
iter.remove();
}
}
}
}
(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)