BIO、NIO、IO的三种机制

一、BIO-blocking IO:同步阻塞式IO

在执行accept、 connect、 read、 write这四步操作的过程中都会产生阻塞。
服务端:

public static void main(String[] args) throws Exception {
	//1.创建服务端
	ServerSocket ss = new ServerSocket();
	//2.绑定监听指定端口
	ss.bind(new InetSocketAddress(12345));
	//3.等待客户端连接 - ACCEPT产生阻塞
	ss.accept();
}

客户端:

public static void main(String[] args) throws Exception {
		//1.创建客户端
		Socket s = new Socket();
		//2.连接指定服务器指定端口
		s.connect(new InetSocketAddress("127.0.0.1", 12345));
		
		while(true){}
}

运行结果:
抛出异常,连接失败,accept,connect操作产生阻塞

服务端:

public static void main(String[] args) throws Exception {
		ServerSocket ss = new ServerSocket();
		ss.bind(new InetSocketAddress(12345));
		Socket socket = ss.accept();
		InputStream in = socket.getInputStream();
		in.read();

客户端:

public static void main(String[] args) throws Exception {
		Socket s = new Socket();
		s.connect(new InetSocketAddress("127.0.0.1",12345));
		while(true){}
}

运行结果:
异常,read阻塞。

服务端:

public static void main(String[] args) throws Exception {
		ServerSocket ss = new ServerSocket();
		ss.bind(new InetSocketAddress(12345));
		Socket socket = ss.accept();		
		while(true){}
	}

客户端:

public static void main(String[] args) throws Exception {
		Socket s = new Socket();
		s.connect(new InetSocketAddress("127.0.0.1", 12345));		
		//向服务端输出数据 
		OutputStream out = s.getOutputStream();
		int i = 0;
		while(true){
			out.write("a".getBytes());
			System.out.println(++i);
		}
	}

运行结果:
异常,write操作产生阻塞,一开始可以写进一点数据,因为缓存区可以放一些,但是一直没有读操作,一直写就会阻塞。

**

二、BIO代码理解

**
服务端:

class SRunable implements Runnable{
	private Socket s = null;
	public SRunable(Socket s) {
		this.s = s;
	}
	
	@Override
	public void run() {
		try {
			//5.从socket中读取数据
			InputStream in = s.getInputStream();
			int len = in.available();
			byte data[] = new byte[len];
			in.read(data,0,len);  
			String str = new String(data);
			System.out.println(str);
			s.close();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}	
}
public class ServerSocket1 {
	public static void main(String[] args) throws Exception {
		ServerSocket ss = new ServerSocket();
		ss.bind(new InetSocketAddress(12345));
		while(true){
			//4.循环等待客户端连接,一旦有连接成功,开启线程进行处理
			Socket s = ss.accept();
			new Thread(new SRunable(s)).start();
		}	
	}
}

服务端:

public class Socket1{
	public static void main(String[] args) throws Exception {
		Socket s = new Socket();
		s.connect(new InetSocketAddress("127.0.0.1",12345));
		OutputStream out = s.getOutputStream();
		out.write("hello".getBytes());
		out.flush();
		
		//4.关闭连接
		out.close();
	}
}

开线程的图示如下:
在这里插入图片描述

造成线程资源的浪费:
应该:连接之后不立即开线程,而是有个中转中心,谁真的使用,中心转发交给线程。达到少量线程处理多请求。
在这里插入图片描述

但是这种模型BIO不适用。所以引入NIO。

三、NIO(NonBlockingIO)

` 相比于传统的BIO最主要的特点是:在执行accept、connect、read、write操作时是非阻塞的。
` 在服务器开发中, 用少量的线程来处理多个客户端请求, 由于以上四种操作都是非阻塞的, 可以随时让线程切换所处理的客户端 ,从而可以实现高并发服务器的开发。
两者的区别:
BIO:同步阻塞式IO —面向流—操作字节或字符—单向传输数据,
NIO:同步非阻塞式IO—面向通道—操作缓冲区—双向传输数据。

四、缓冲区Buffer

1、概述:
是一段连续的内存空间,用来临时存放大量指定类型的数据
java.nio.Buffer—abstract
子类有7个,对应基本数据类型,没有布尔类:
ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer

2、重要定义:
capacity - 容量,在创建Buffer时就需要指定好,后续不可修改
position - 当前位置,初始值为0,指定Buffer进行读写操作时操作位置,每当操作过后position自动+1指向下一个位置
limit - 限制位,初始值等于capacity,position永远小于等于limit

3、创建缓冲区
没有构造方法,有静态方法。
方法一:直接创建指定大小的空缓冲区

ByteBuffer.allocate(capacity); 

方法二:通过已经有的字节数组创建缓冲区

byte [] data = “hello”.getBytes();
ByteBuffer buf = ByteBuffer.wrap(data);

4、向缓冲区写入数据
position指向写入数据数据的位置,每当写入一个数据,position自动+1指向下一个位置,position不可大于limit,如果一直写入,达到limit大小,再写入会抛出异常
通过.putXXX()写入数据,可以是不同类型的数据
(1)顺序写入数据

buffer.put("a".getBytes());

(2)手动指定position,

buffer.position(2);
buffer.put("f".getBytes());

(3)不修改position,直接覆盖,修改指定位置的值

buffer.put(2, "f".getBytes()[0]);

5、从buffer中获取数据
position指向读取数据的位置,每当读到一个数据,position自动+1指向下一个位置,position不可大于limit,如果一直读取,达到limit大小,再读取会抛出异常。
读取数据通过.getXXX()。

byte [] data = new byte[1];
buffer.get(data);
System.out.println(new String(data));

手动控制limit和position实现读取,并防止越界

		buffer.limit(buffer.position());
		buffer.position(0);
		while(buffer.position()<buffer.limit()){
			byte [] data = new byte[1];
			buffer.get(data);
			System.out.println(new String(data));
}

这样做的便捷方法:

6、反转缓冲区

buffer.flip();

反转缓冲区本质上等价于先把limit放到positon,在把position归0,这两步操作。实现读取而且不越界。

7、判断边界

.remaining()和.hasRemaining()

前者可以返回 limit - position的值,通常用来获取读写时是距离边界的距离
后者可以返回 limit-position>0 的值,通常用来判断度写时是否到达了边界
后者buffer.hasRemaining()等价于前者buffer.remaining()>0

8、重绕缓冲区
也就是重新从头读取数据
通过.rewind(); 等价于.position(0);

9、设置和重置标记的方法

.mark();

//回到标记的位置

.reset();

此处注意mark的位置:
例子:

public void test(){
		ByteBuffer buffer = ByteBuffer.allocate(5);
		buffer.put("a".getBytes());
		buffer.put("b".getBytes());
		buffer.put("c".getBytes());
		buffer.put("d".getBytes());
		buffer.put("e".getBytes());
		buffer.flip();
		byte [] data = new byte[1];
		buffer.get(data);
		System.out.println(new String(data));
		data = new byte[1];
		buffer.get(data);
		System.out.println(new String(data));
		//--打标记
		buffer.mark();
		data = new byte[1];
		buffer.get(data);
		System.out.println(new String(data));
		data = new byte[1];
		buffer.get(data);
		System.out.println(new String(data));	
		//--回到标记,buffer.position(mark的位置)
		buffer.reset();
		data = new byte[1];
		buffer.get(data);
		System.out.println(new String(data));
	}

得到的是:c。

10、清空缓冲

.clear();

等价于position=0.limit=capacity=n,mark取消。
· 清空缓冲之后,再执行.position(n);//还能得到n上的值,说明数据不会清除。虽然数据不会清除,但是后续正常的写入读取的操作不会受影响,因为没有机会读到未删除的垃圾数据。

五、通道Channel

1、概述:
通道注意是个接口,操作的是缓冲区,可以双向传输数据。

2、ServerSocketChannel:
tcp通信中的服务器端
通过.open()创建,通过.socket()获取底层的套接字对象,通过bind(InetSocketAddress(端口))绑定监听端口(JDK7才有,旧方法是:ssc.socket().bind();)。
直接运行的话,运行结果是阻塞,因为ServerSocketChannel默认情况下是阻塞模式。设置为非阻塞:

ssc.sonfigureBlocking(false);

.accept()是等待客户端连接,在阻塞模式下会一直阻塞直到客户端连接,返回一个代表链接的SockentChannel对象sc。在非阻塞模式下,此方法不会阻塞,直接执行下去,如果没有得到一个新的连接,此方法返回null。
注意:
在非阻塞模式下,accept操作没有阻塞,无论是否收到一个连接,都直接执行下去,此时即使accept方法执行成功,也无法确认连接完成.此时应该自己通过代码来控制实现连接,或者,通过选择器来实现选择操作.

SocketChannel sc = null;
   while(sc == null){
   sc = ssc.accept();
}

能走下来说明连接成功继续执行后续操作:从sc中读取数据。

3、SocketChannel:
tcp通信中的客户端
open().configureBlocking()和服务端一样,还有connect(),finishConnect()
connect()命令客户端连接指定服务器地址端口, 如果通道处于阻塞模式, 则此方法会一直阻塞, 直到 连接成功。 而如果通道处于非阻塞模式, 此方法将仅仅尝试着去连接, 如果连接成功则返回true; 如果连接一时间没有结束, 也不阻塞程序, 此方法返回false。 程序继续执行, 此时需要在后续调用finishConnection方法来完成连接。finishConnection方法也是非阻塞的 调用结束并不意味着连接完成 所以如果此方法返回false, 应该继续重复调用,直到返回true才表明连接完成。

while(!sc.isConnected()){
sc.finishConnect();
}

六、NIO案例

服务端:

package cn.nio.channel;
 
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
 
public class ServerSocketChannel1 {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(12345));
ssc.configureBlocking(false);
SocketChannel sc = null;
while(sc == null){
sc = ssc.accept();
}
sc.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(5);  
while(buf.hasRemaining()){
sc.read(buf);
}
byte[] arr = buf.array();
String str = new String(arr);
System.out.println(str);
 sc.close();
ssc.close();
}
}

客户端:

package cn.nio.channel;
 
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
 
public class SocketChannel1 {
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
boolean isConn = sc.connect(new InetSocketAddress("127.0.0.1", 12345));
if(!isConn){
while(!sc.finishConnect()){
}
}
 ByteBuffer buf = ByteBuffer.wrap("hello".getBytes());
while(buf.hasRemaining()){
sc.write(buf);
}
sc.close();
}
}

七、选择器Selector

概述:
目的是实现少量线程服务多个客户端。一方面允许多个客户端连接注册到选择器中关注对应的事件.另一方面提供了"选择"操作 来在之前注册的操作中选择已经就绪的操作 交给线程来执行. 一头连接了多个客户端连接 另一头连接了少量的线程,进行协调 。
创建唯一的选择器通过.open()
注册通道到选择器通过.regist(选择器,要关注的事件)
这里的第二个参数是int类型,可供选择的操作类型;

OP_ACCEPT 
          用于套接字接受操作的操作集位。
OP_CONNECT 
          用于套接字连接操作的操作集位。
OP_READ 
          用于读取操作的操作集位。
OP_WRITE 
          用于写入操作的操作集位。

regist方法返回的是一个SelectionKey对象。此对象是一个代表本次注册事件的对象。从这个对象上可以得到对应的是哪个通道,注册在哪个选择器上,关注的是哪个事件。
选择已经就绪的事件通过.select();如果没有时间就绪,则进入阻塞状态。返回类型是int。
获取就绪的键通过.selectedKeys();//返回的是selectionKey组成的集合,集合中的selectionKey包含通道的引用和哪类事件的信息,类似于去银行存钱的,银行给的存折,谁,什么时候,存了多少钱。

八、利用Selector+channel+Buffer实现 少量线程处理多个客户端请求

客户端

public class SocketChannel1 {
    public static void main(String[] args) throws Exception {
	    Selector selc = Selector.open();
	    SocketChannel sc = SocketChannel.open();
	    sc.configureBlocking(false);
	    sc.connect(new InetSocketAddress("127.0.0.1", 12345));
	    sc.register(selc, SelectionKey.OP_CONNECT);
 
	    //通过选择器实行选择操作
	    while(true){
	    selc.select();//选择器尝试选择就绪的键 选不到就阻塞 选择到就返回就绪的键的数量
 
		    //得到并遍历就绪的键们
		    Set<SelectionKey> keys = selc.selectedKeys();
		    Iterator<SelectionKey> it = keys.iterator();
		    while(it.hasNext()){
		    //得到每一个就绪的键
		    SelectionKey key = it.next();
		    //获取就绪的键 对应的 操作 和 通道
		    if(key.isAcceptable()){
     
		    }else if(key.isConnectable()){
		    //--是通道的Connect操作
		    //--获取通道
		    SocketChannel scx = (SocketChannel) key.channel();
		    if(!scx.isConnected()){
		    while(!scx.finishConnect()){};
    }
	    //--将通道再次注册到selc中 关注write操作
	    scx.register(selc, SelectionKey.OP_WRITE);
	    }else if(key.isReadable()){
     
	    }else if(key.isWritable()){
	    //--发现是Write操作就绪
	    SocketChannel scx = (SocketChannel) key.channel();
	    ByteBuffer buf = ByteBuffer.wrap("hello ".getBytes());
	    while(buf.hasRemaining()){
	    scx.write(buf);
	    }
	    //取消掉当前通道 在选择器中的注册 防止重复写出
	    key.cancel();
	    //scx.close();
	    }else{
	    throw new RuntimeException("未知的键");
	    }
	    //移除就绪键
	    it.remove();
   			 }
  	 	 }
  	 }
 }

服务端

public class ServerSocket1 {
 public static void main(String[] args) throws Exception {
    Selector selc = Selector.open();
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.configureBlocking(false);
    ssc.bind(new InetSocketAddress(12345));
    //将ssc注册到选择器中关注ACCEPT操作
    ssc.register(selc, SelectionKey.OP_ACCEPT);
    //通过选择器选择就绪的键
    while(true){
    selc.select();//尝试到注册的键集中来寻找就绪的键 如果一个就绪的键都找不到 就进入阻塞 直到找到就绪的键 返回就绪的键的个数
 
    //获取就绪的键的集合
    Set<SelectionKey> keys = selc.selectedKeys();
 
	//遍历处理就绪的键 代表的操作
	 Iterator<SelectionKey> it = keys.iterator();
	  while(it.hasNext()){
	    //--获取到就绪的键 根据键代表的操作的不同 来进行不同处理
	    SelectionKey key = it.next();
 
		    if(key.isAcceptable()){
		    //--发现了Accept操作 
		    ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
		    SocketChannel sc = sscx.accept();
		    sc.configureBlocking(false);
		    sc.register(selc, SelectionKey.OP_READ);
		    }else if(key.isConnectable()){
		     
		    }else if(key.isWritable()){
		     
		    }else if(key.isReadable()){
		    //--发现了Read操作
		    SocketChannel scx = (SocketChannel) key.channel();
		    ByteBuffer buf = ByteBuffer.allocate(5);
		    while(buf.hasRemaining()){
		    scx.read(buf);
		    }
		    String msg = new String(buf.array());
		    System.out.println("[收到来自客户端的消息]:"+msg);
		    }else{
		    throw new RuntimeException("未知的键,");
		    }

		 it.remove();
		 }
	  }
   }
}

九、粘包问题

1、概述:
当通过socket发送多段数据时,底层的tcp协议会自动根据需要将数据拆分或合并 组成数据包后发送给接受者 ,接受者收到数据后 ,无法直接通过tcp协议本身判断数据的边界,这个问题就称之为粘包问题。
比如:三条数据111、22222、33经过传输之后,得到就是1112222233

2、问题本质:
粘包问题本质上是因为tcp协议是传输层的协议 本身没有对会话控制提供相应的能力 我们基于socket开发网络程序时 相当于在自己实现 会话层 表示层 和应用层的功能 所以 需要自己来相办法解决粘包问题。

3、粘包问题的解决方案
(1)只发送固定长度的数据
通信的双发约定每次发送数据的长度,每次只发送固定长度的数据,接收数据方 每次都按照固定长度获取数据
缺点:
不够灵活,只适合每次传输的数据都有固定长度的场景
(2)约定分隔符
通信双方约定一个特殊的分隔符用来表示数据的边界,接收方收到数据时,不停读取,以分隔符为标志,区分数据的边界
缺点:
如果数据本身就包含分隔符字符,则需要对数据进行预处理将数据本身包含的分隔符进行转义,相对来说比较麻烦
(3)使用协议—分头和体传输数据
在头信息中描述数据的格式和长度信息,在接收方接收数据时,先读取头信息,再根绝头信息来决定获取后续数据。
我们自己规定了规则,实现了通信,可以认为是私有的协议,在小范围内实现通信。

十 、通过自定义协议完成任意长度数据通信

服务端:

public class ServerSocketChannel1 {
	public static void main(String[] args) throws Exception {
		 Selector selc = Selector.open();
		 ServerSocketChannel ssc = ServerSocketChannel.open();
		 ssc.configureBlocking(false);
		 ssc.bind(new InetSocketAddress(12345));
		ssc.register(selc, SelectionKey.OP_ACCEPT);
 
	 while(true){
		 selc.select();
		 Set<SelectionKey> keys = selc.selectedKeys();
		 Iterator<SelectionKey> it = keys.iterator();
	   
	    while(it.hasNext()){
	    SelectionKey key = it.next();
 
	    if(key.isAcceptable()){
	    ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
	    SocketChannel sc = sscx.accept();
	    sc.configureBlocking(false);
	    sc.register(selc,SelectionKey.OP_READ);
	    }else if(key.isConnectable()){
 
	    }else if(key.isReadable()){
	    SocketChannel scx = (SocketChannel) key.channel();
	    ByteBuffer tmp = ByteBuffer.allocate(1);
	    String line = "";
	    while(!line.endsWith("\r\n")){
	    scx.read(tmp);
	    line += new String(tmp.array());
	    tmp.clear();
	    }
	    int len = Integer.parseInt(line.substring(0, line.length()-2));
	    ByteBuffer buf = ByteBuffer.allocate(len);
	    while(buf.hasRemaining()){
	    scx.read(buf);
	    }
    String msg = new String(buf.array());
    System.out.println("收到了来自客户端的消息:["+msg+"]");

    scx.register(selc, SelectionKey.OP_WRITE);
 
	    }else if(key.isWritable()){
	    SocketChannel scx = (SocketChannel) key.channel();
	    String str = "来自服务器的相应消息:[你好,客户端]";
	    String data = str.getBytes().length+"\r\n"+str;
	    ByteBuffer buf = ByteBuffer.wrap(data.getBytes());
	    while(buf.hasRemaining()){
	    scx.write(buf);
	    }
	    key.cancel();
	    }else{
	    throw new RuntimeException("未知的键");
	    }
	    it.remove();
		  }
	   }
	 }
 }

客户端:

public class SocketChannel1 {
	public static void main(String[] args) throws Exception {
		Selector selc = Selector.open();
	    SocketChannel sc = SocketChannel.open();
	    sc.configureBlocking(false);
	    sc.connect(new InetSocketAddress("127.0.0.1", 12345));
	    sc.register(selc, SelectionKey.OP_CONNECT);

	    while(true){
	    selc.select();

		Set<SelectionKey> keys = selc.selectedKeys();	
	    Iterator<SelectionKey> it = keys.iterator();
		while(it.hasNext()){
		SelectionKey key = it.next();
 
	    if(key.isAcceptable()){
 
	    }else if(key.isConnectable()){
	    SocketChannel scx = (SocketChannel) key.channel();
	    if(!scx.isConnected()){
	    while(!scx.finishConnect()){}
	    }
	    scx.register(selc, SelectionKey.OP_WRITE);
	    }else if(key.isReadable()){
	    SocketChannel scx = (SocketChannel) key.channel();
	    ByteBuffer tmp = ByteBuffer.allocate(1);
	    String line = "";
	    while(!line.endsWith("\r\n")){
	    scx.read(tmp);
	    line += new String(tmp.array());
	    tmp.clear();
	    }
	    int len = Integer.parseInt(line.substring(0, line.length()-2));
	    ByteBuffer buf = ByteBuffer.allocate(len);
	    while(buf.hasRemaining()){
	    scx.read(buf);
	    }
	    String msg = new String(buf.array());
	    System.out.println("收到了来自服务器的响应:["+msg+"]");
	     
	    }else if(key.isWritable()){
	    SocketChannel scx = (SocketChannel) key.channel();
	    String str = "hello java hello nio hello China~";
	    String data = str.getBytes().length+"\r\n"+str;
	    ByteBuffer buf = ByteBuffer.wrap(data.getBytes());
	    while(buf.hasRemaining()){
	    scx.write(buf);
	    }
	     
	    scx.register(selc, SelectionKey.OP_READ);
	    }else{
	    throw new RuntimeException("未知的键");
	    }
	    it.remove();
			}
		 }
	 }
}

十一、三种IO机制的区别

阻塞/非阻塞:
考虑的是线程的角度,当执行某些操作不能立即完成时,线程是否被挂起,失去cpu争夺权,无法继续执行,直到阻塞结束或被唤醒。
同步/异步:
考虑的是参与通信双方的工作机制,是否需要互相等待对方的执行.
同步指的是通信过程中, 一方在处理通信 ,另一方要等待对方执行不能去做其他无关的事。
异步指的是通信过程中 ,一方在处理通信, 另一方可以不用等待对方可以去做其他无关的事, 直到对方处理通信完成, 再在适合的时候继续处理通信过程。

BIOjdk1.0同步阻塞式IO面向流操作字节或字符单向传输数据
NIOjdk4.0同步非阻塞式IO面向通道操作缓冲区双向传输数据
AIOjdk7.0异步非阻塞式IO大量使用回调函数异步处理通信过程

补充:
NIO常见的框架:MINA Netty

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值