BIO
同步阻塞式IO,相信每一个学习过操作系统网络编程或者任何语言的网络编程的人都很熟悉,在while循环中服务端会调用accept方法等待接收客户端的连接请求,一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成。
具体代码如下:
server端:
ServerSocket serverSocket = new ServerSocket(8080); //启动一个server服务端 while (true) { System.out.println("accept data"); Socket accept = serverSocket.accept();//循环监听请求连接,此点会阻塞 System.out.println("get data"); InputStream inputStream = accept.getInputStream(); //得到输入流 int read = inputStream.read(bytes); //读取数据,此点也会阻塞。 System.out.println(new String(bytes)); }
client端:
Socket socket = new Socket("127.0.0.1", 8080); //连接服务端socketServer Scanner scanner = new Scanner(System.in); String next = scanner.next(); //输入传输内容 socket.getOutputStream().write(next.getBytes());//传输内容
如上代码所示,client确实能够将要传输的内容发送到server端,但是却有以下缺陷:
1.client到server是单向传输;
2.当server端在serverSocket.accept()时会产生阻塞,一直等待client请求连接;
3.当server端在inpietStream.read(bytes)时会产生阻塞,一直等待client发送数据;
面对如上问题早期的代码是这样解决的:
如果BIO要能够同时处理多个客户端请求,就必须使用多线程,即每次accept阻塞等待来自客户端请求,一旦受到连接请求就建立通信套接字同时开启一个新的线程来处理这个套接字的数据读写请求,然后立刻又继续accept等待其他客户端连接请求,即为每一个客户端连接请求都创建一个线程来单独处理,大概原理图就像这样:
虽然可以用多线程可以解决BIO的阻塞问题,也可以用线程池管理线程解决线程过多导致服务器变慢甚至崩溃的问题。但是,试想一下如果client请求连接到server,此时Socket accept = serverSocket.accept()接收到请求并且创建一个线程thread1去处理这个socket,如果此时client端发生故障或者其他问题一直没有发送数据,那么server端thread1中会一直阻塞在inputStream.read(bytes)导致thread1一直被占用,thread1得不到释放大大影响线程池的效率。
NIO
同步非阻塞式IO,关键是采用了事件驱动的思想来实现了一个多路转换器。
NIO与BIO最大的区别就是只需要开启一个线程就可以处理来自多个客户端的IO事件,这是怎么做到的呢? 就是多路复用器,可以监听来自多个客户端的IO事件:
A. 若服务端监听到客户端连接请求,便为其建立通信套接字(java中就是通道),然后返回继续监听,若同时有多个客户端连接请求到来也可以全部收到,依次为它们都建立通信套接字。
B. 若服务端监听到来自已经创建了通信套接字的客户端发送来的数据,就会调用对应接口处理接收到的数据,若同时有多个客户端发来数据也可以依次进行处理。
C. 监听多个客户端的连接请求和接收数据请求同时还能监听自己时候有数据要发送。
总之就是在一个线程中就可以调用多路复用接口(java中是select)阻塞同时监听来自多个客户端的IO请求,一旦有收到IO请求就调用对应函数处理。
server端代码:
public class ServerSocketChannels implements Runnable { //服务端通道 private ServerSocketChannel serverSocketChannel; //轮训选择器 private Selector selector; //是否停止 private volatile boolean stop; public ServerSocketChannels(int port) { try { //创建多路复用器selector,工厂方法 ① selector = Selector.open(); ① //创建ServerSocketChannel,工厂方法 ① serverSocketChannel = ServerSocketChannel.open(); ① //绑定ip和端口号,默认的IP=127.0.0.1,对连接的请求最大队列长度设置为backlog=1024,如果队列满时收到连接请求,则拒绝连接 ① serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024); ① //设置非阻塞方式 ① serverSocketChannel.configureBlocking(false); ① //注册serverSocketChannel到selector多路服用器上面,监听accrpt请求 ① serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("the time is start port = " + port); } catch (IOException e) { e.printStackTrace(); System.exit(1); } } public void stop() { this.stop = true; } /** * selector.select()会一直阻塞到有一个通道在你注册的事件上就绪了. * 所以起一个线程防止主线程堵塞 */ @Override public void run() { //如果server没有停止 while (!stop) { try { //selector.select()会一直阻塞到有一个通道在你注册的事件上就绪了 ① selector.select(1000)会阻塞到1s后然后接着执行,相当于1s轮询检查 System.out.println("this start sleep"); Thread.sleep(10000); System.out.println("this end sleep"); //找到所有准备接续的key 3 Set<SelectionKey> selectionKeys = selector.selectedKeys(); 3 Iterator<SelectionKey> it = selectionKeys.iterator(); 3 SelectionKey key = null; 3 while (it.hasNext()) { 3 key = it.next(); 3 it.remove(); 3 try { 3 //处理准备就绪的key 3 handle(key); } catch (Exception e) { if (key != null) { //请求取消此键的通道到其选择器的注册 key.cancel(); //关闭这个通道 if (key.channel() != null) { key.channel().close(); } } } } } catch (Throwable e) { e.printStackTrace(); } } if (selector != null) { try { selector.close(); } catch (IOException e) { e.printStackTrace(); } } } public void handle(SelectionKey key) throws IOException { //如果key是有效的 3 if (key.isValid()) { 3 //监听到有新客户端的接入请求 3 //完成TCP的三次握手,建立物理链路层 3 if (key.isAcceptable()) { 3 ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); 3 SocketChannel sc = (SocketChannel) ssc.accept(); 3 //设置客户端链路为非阻塞模式 3 sc.configureBlocking(false); 3 //将新接入的客户端注册到多路复用器Selector上 3 sc.register(selector, SelectionKey.OP_READ); } //监听到客户端的读请求 5 if (key.isReadable()) { 5 //获得通道对象 5 SocketChannel sc = (SocketChannel) key.channel(); 5 ByteBuffer readBuffer = ByteBuffer.allocate(1024); 5 //从channel读数据到缓冲区 5 int readBytes = sc.read(readBuffer); 5 if (readBytes > 0) { 5 //Flips this buffer. The limit is set to the current position and then 5 // the position is set to zero,就是表示要从起始位置开始读取数据 5 readBuffer.flip(); 5 //eturns the number of elements between the current position and the limit. 5 // 要读取的字节长度 5 byte[] bytes = new byte[readBuffer.remaining()]; 5 //将缓冲区的数据读到bytes数组 5 readBuffer.get(bytes); 5 String body = new String(bytes, "UTF-8"); 5 System.out.println("the time server receive order: " + body); 5 String currenttime = "我把信息还给你们"+body; 5 doWrite(sc, currenttime); } else if (readBytes < 0) { key.channel(); sc.close(); } } } } public static void doWrite(SocketChannel channel, String response) throws IOException { if (response!=null && response.length()>0 ) { byte[] bytes = response.getBytes(); //分配一个bytes的length长度的ByteBuffer ByteBuffer write = ByteBuffer.allocate(bytes.length); //将返回数据写入缓冲区 write.put(bytes); write.flip(); //将缓冲数据写入渠道,返回给客户端 channel.write(write); } } public static void main(String[] args) { new Thread(new ServerSocketChannels(8080)).start(); } }
client端代码:
public class ClientSocketChannels2 extends Thread { //服务器端的ip private String host; //服务器端的端口号 private int port; //多路服用选择器 private Selector selector; //客户端通道 private SocketChannel socketChannel; //关闭通道 private volatile boolean stop=false; public ClientSocketChannels2(String host, int port) { this.host = host == null ? "127.0.0.1" : host; this.port = port; try { //初始化一个Selector,工厂方法 selector = Selector.open(); //初始化一个SocketChannel,工厂方法 socketChannel = SocketChannel.open(); //设置非阻塞模式 socketChannel.configureBlocking(false); } catch (IOException e) { e.printStackTrace(); System.exit(1); } } /** * 首先尝试连接服务端 * @throws IOException */ public void doConnect() throws IOException { //如果连接成功,像多路复用器selector监听读请求 ② if (socketChannel.connect(new InetSocketAddress(this.host, this.port))) { socketChannel.register(selector, SelectionKey.OP_READ); //执行写操作,像服务器端发送数据 doWrite(socketChannel); } else { ② //监听连接请求 ② socketChannel.register(selector, SelectionKey.OP_CONNECT); } } public static void doWrite(SocketChannel sc) throws IOException { //构造请求消息体 byte[] bytes = "lientSocket 2 号 往通道发消息".getBytes(); //构造ByteBuffer ByteBuffer write = ByteBuffer.allocate(bytes.length); //将消息体写入发送缓冲区 write.put(bytes); write.flip(); //调用channel的发送方法异步发送 sc.write(write); //通过hasRemaining方法对发送结果进行判断,如果消息全部发送成功,则返回true if (!write.hasRemaining()) { System.out.println("lientSocket 2 号 往通道发消息"); } } @Override public void run() { try { doConnect(); } catch (IOException e) { e.printStackTrace(); System.exit(1); } while (true) { try { selector.select(1000); Set<SelectionKey> keys = selector.selectedKeys(); 4 Iterator<SelectionKey> its = keys.iterator(); 4 SelectionKey key = null; 4 while (its.hasNext()) { 4 key = its.next(); 4 its.remove(); 4 try { 4 handle(key); } catch (Exception e) { if (key != null) { key.cancel(); if (key.channel() != null) { key.channel().close(); } } } } } catch (Exception e) { e.printStackTrace(); System.exit(1); } } } public void handle(SelectionKey key) throws IOException { 4 if (key.isValid()) { 4 SocketChannel sc = (SocketChannel) key.channel(); 4 if (key.isConnectable()) { 4 //如果连接成功,监听读请求 4 if (sc.finishConnect()) { 4 sc.register(this.selector, SelectionKey.OP_READ); 4 //像服务端发送数据 4 doWrite(sc); } else { System.exit(1); } } //监听到读请求,从服务器端接受数据 6 if (key.isReadable()) { 6 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); 6 int readBytes = sc.read(byteBuffer); 6 if (readBytes > 0) { 6 byteBuffer.flip(); 6 byte[] bytes = new byte[byteBuffer.remaining()]; 6 byteBuffer.get(bytes); 6 String body = new String(bytes, "UTF-8"); 6 System.out.println("now body is " + body); 6 stop = true; } else if (readBytes < 0) { key.cancel(); sc.close(); } } } //释放所有与该多路复用器selector关联的资源 /*if(selector != null&&stop==true){ try { selector.close(); } catch (IOException e) { e.printStackTrace(); } }*/ } public static void main(String[] args) { int port = 8080; ClientSocketChannels2 client = new ClientSocketChannels2("", port); new Thread(client, "client-001").start(); } }
流程:
1.启动server,创建serverSocketChannel和selector并且绑定ip和端口号,并将serverSocketChannel注册到selector多路复用管理器上,selector.select(1000)轮询通道监听是否有可用的准备就绪的key;
2.启动client创建通道serverSocketChannel和selector并且链接socketChannel.connect(new InetSocketAddress(this.host, this.port))服务器,并将serverSocketChannel注册到selector多路复用管理器上;
3.当client请求socketChannel.connect(new InetSocketAddress(this.host, this.port))时会被server端监听到,并且完成连接,并将新接入的客户端注册到多路复用器Selector上的read事件;
4.client请求测到与服务器完成连接,将接入的通道key绑定多路复用器Selector上的read事件,并且向通道socketChannel发送信息;
5.server监听到客户端的读请求从通道中读取数据,并且向该通道中写回数据;
6.client客户端监听到读请求,从服务器端接受数据;
异步阻塞IO(Java NIO):
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(如果从UNP的角度看,select属于同步操作。因为select之后,进程还需要读写数据),从而提高系统的并发性.