1 BIO
可以理解为Blocking IO 是同步阻塞的IO,也就是说,当有多个请求过来的时候,请求会呈现为链状结构,遵循先进先出的原则
1.1 单线程版本
1.1.1 服务端
//服务端单线程处理 public class BioServer { public static void main(String[] args) throws IOException, InterruptedException { // 1.创建服务端socket ServerSocket serverSocket = new ServerSocket(9000); System.out.println("服务端启动..."); while (true){ System.out.println("服务端接受客户端连接前"); // 2.这里会阻塞 Socket socket = serverSocket.accept(); System.out.println("服务端接受客户端连接后"); // 3.单线程处理方案 handel(socket); } } private static void handel(Socket socket) throws IOException, InterruptedException { byte[] bytes = new byte[1024]; System.out.println("服务端读取客户端传入信息前" ); // 3.1 read会阻塞 读取客户端数据,要客户端开始写才会向下执行 int read = socket.getInputStream().read(bytes); if(read != -1){ System.out.println( "服务端读取客户端传入信息,msg:"+new String(bytes,0,read) ); } System.out.println("服务端向客户端写入信息" ); Thread.sleep(200); //假设写需要200ms socket.getOutputStream().write("hello client".getBytes()); socket.getOutputStream().flush(); socket.close(); } }
1.1.2 客户端
package com.ruoyi.weixin.Test.SI_BIO; import java.io.IOException; import java.net.Socket; public class BioClient { public static void main(String[] args) throws IOException { for (int i = 0; i < 3; i++) { new Thread(()->{ try { connect(Thread.currentThread().getName()); } catch (IOException | InterruptedException e) { e.printStackTrace(); } }, "客户端"+i ).start(); } } public static void connect(String i) throws IOException, InterruptedException { String clientname = Thread.currentThread().getName(); // 1.创建连接绑定ip和端口 Socket socket = new Socket("localhost", 9000); System.out.println(clientname + "开始向服务端写入消息" ); String msg = clientname + "-hello server"; Thread.sleep(300); //假设写需要300ms socket.getOutputStream().write(msg.getBytes()); socket.getOutputStream().flush(); System.out.println(clientname + "开始向服务端写入消息完成" ); byte[] bytes = new byte[1024]; // 2.read会阻塞 读取客户端数据,要服务端开始写才会向下执行 int read = socket.getInputStream().read(bytes); // 3.接收服务端回传的数据 System.out.println(clientname + "接收到服务端的数据:" + new String(bytes,0,read) ); socket.close(); } }
1.1.3 执行
在客户端socket.getOutputStream().write(msg.getBytes());这里打个断点
在服务端socket.getOutputStream().write("hello client".getBytes());打个断点
1)启动服务端
2)启动客户端
客户端控制台,在断点处停住了
服务端控制台,由于客户端在写之前停住了,所以在read()这里阻塞了
客户端放开断点
客户端控制台:
向服务端写完数据后,执行到read,阻塞等待服务端的数据
服务端控制台:
读取完客户端的数据,向下执行
由于断点,在向客户端写之前停住了
放开服务端断点
服务端控制台:
向客户端写完数据,请求处理完成,继续等待下一个请求
客户端控制台,接收服务端数据,请求完成
通过上面的示例,Nio处理请求是一个一个处理的,也就是同步
数据交互read()是阻塞的,需要等待write的执行,也就是阻塞
1.2 多线程版本
上面是一个线程去处理所有请求,现在,一个请求来了,就开一个线程去处理。
优点:把read阻塞给优化了,这里不会阻塞其他线程了,只会在自己的线程里面阻塞,提高了并发能力,提高了效率
缺点:这里有可能会无限制创建线程,线程是稀有资源,如果请求很多,这个时候客户端一直不执行write方法,所有线程就会阻塞在read方法那里,导致线程暴涨,cpu升高,导致机器假死
再优化,采用线程池管理线程去处理请求,可以限制线程数量
public class BioServer { public static void main(String[] args) throws IOException, InterruptedException { // 1.创建服务端socket ServerSocket serverSocket = new ServerSocket(9000); System.out.println("服务端启动..."); while (true){ System.out.println("服务端接受客户端连接前"); // 2.这里会阻塞 Socket socket = serverSocket.accept(); System.out.println("服务端接受客户端连接后"); // 3.单线程处理方案 new Thread(()->{ try { handel(socket); } catch (IOException | InterruptedException e) { e.printStackTrace(); } }).start(); } } private static void handel(Socket socket) throws IOException, InterruptedException { byte[] bytes = new byte[1024]; System.out.println("服务端读取客户端传入信息前" ); // 3.1 read会阻塞 读取客户端数据,要客户端开始写才会向下执行 int read = socket.getInputStream().read(bytes); if(read != -1){ System.out.println( "服务端读取客户端传入信息,msg:"+new String(bytes,0,read) ); } System.out.println("服务端向客户端写入信息" ); Thread.sleep(200); //假设写需要200ms socket.getOutputStream().write("hello client".getBytes()); socket.getOutputStream().flush(); socket.close(); } }
2 NIO
我们上面对BIO进行了一系列的说明,证明了BIO是一个同步的阻塞的IO模型,引出我们NIO,NIO是non-blocking IOjava也称为new IO,是同步非阻塞的IO,下面我们直接上流程图吧
server:服务端服务端会产生一个ServerSocketChannel,它会注册到selector中,用于服务端和客户端通信使用
client:客户端
客户端会产生一个SocketChannel,它会注册到seletor中,用户服务端和客户端通信使用
buffer:缓冲区
用于客户端和服务端进行数据传输使用,既可以read也可以wirte
channel:通道
包括服务端的ServerSocketChannel和客户端的SocketChannel的通道,它是连接客户端和服务端的通道,是一个双向的既可以读也可以写,都是通过buffer完成的
selector:多路复用器
负责管理channel
selectedKeys:用于获取SocketChannel
2.1 服务端代码
//同步非阻塞 把整个过程分为三部分 建立连接 接收客户端数据(在客户端写之前,不会触发本事件,可以去处理其它的事件) 向客户端发送数据 类似于生产者消费者 public class NioServer { public static void main(String[] args) throws IOException { // 打开服务端通道 ServerSocketChannel ssc = ServerSocketChannel.open(); // 设置成非阻塞 ssc.configureBlocking(false); // 绑定端口 ssc.socket().bind(new InetSocketAddress(9000)); // 打开多路复用器 Selector selector = Selector.open(); // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接感兴趣 ssc.register(selector, SelectionKey.OP_ACCEPT); while (true) { System.out.println("等待事件发生。。"); // 选择多路复用器里面的通道,方法是阻塞的,只有存在客户端进行连接,才可以执行后面逻辑 int select = selector.select(); System.out.println("有事件发生了。。"); // 获取多路复用器里面的所有注册的通道key Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { // 获取某一个通道key SelectionKey key = it.next(); // 删除当前可以,防止多次处理 it.remove(); handle(key); } } } private static void handle(SelectionKey key) throws IOException { // 验证当前通道属于什么事件 if (key.isAcceptable()) { // 连接事件 System.out.println("有客户端连接事件发生了。。"); // 获取当前key所在的通道 ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); // 调用accept()方法获取客户端SocketChannel通道。 //注意这里是阻塞状态的,但是也是非阻塞状态的,这里就是NIO的精髓 //我给大家讲解一下,accept()方法本身是阻塞,它要有客户端连接进来才能向下执行 //前面已经判断是Acceptable()事件,所以一定有客户端进行连接,所以这里就不用等待了 SocketChannel sc = ssc.accept(); // 设置通道为非阻塞方式 sc.configureBlocking(false); // 将通道注册到多路复用器上,并且注册事件是OP_READ时间 sc.register(key.selector(), SelectionKey.OP_READ); } else if (key.isReadable()) { System.out.println("有客户端数据可读事件发生了。。"); SocketChannel sc = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int len = sc.read(buffer); if (len != -1){ System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len)); } ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes()); sc.write(bufferToWrite); key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); } else if (key.isWritable()) { SocketChannel sc = (SocketChannel) key.channel(); System.out.println("write事件"); key.interestOps(SelectionKey.OP_READ); } } }
2.2 客户端代码
//把整个过程分为三个事件 建立连接 向服务端发送数据 接收服务端数据(在服务端写之前,不会触发本事件,线程就可以去处理其它的事件) public class NioClient { private Selector selector; public static void main(String[] args) throws IOException { new Thread(()->{ try { co(Thread.currentThread().getName()); } catch (IOException e) { e.printStackTrace(); } }, "客户端1" ).start(); } public static void co(String clientname ) throws IOException { NioClient nioClientDemo = new NioClient(); // 初始化客户端 nioClientDemo.initClient("localhost",9000); // 客户端进行对应操作 nioClientDemo.connect( clientname); } /** * 初始化客户端 * @param ip * @param port * @throws IOException */ private void initClient(String ip,int port) throws IOException { // 获取socket通道 SocketChannel socketChannel = SocketChannel.open(); // 配置非阻塞 socketChannel.configureBlocking(false); // 打开多路复用器 this.selector = Selector.open(); // 连接服务端 其实改方法并没有实现连接, // 需要在listen()方法中调用channel.finishConnect();才能完成连接 socketChannel.connect(new InetSocketAddress(ip,port)); // 将socket注册到多路复用器,事件为SelectionKey.OP_CONNECT事件 socketChannel.register(selector, SelectionKey.OP_CONNECT); } private void connect(String clientname) throws IOException { while (true){ // 监听多路复用器里面是否存在需要处理的channel 这里是阻塞的 selector.select(); // 获取多路复用器中的channel对应的key Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ // 获取SelectionKey SelectionKey key = iterator.next(); // 获取到之后把当前可以删除,防止重复获取 iterator.remove(); // 验证SelectionKey对应的事件 if(key.isConnectable()){ // 获取通道 SocketChannel channel = (SocketChannel) key.channel(); if(channel.isConnectionPending()){ channel.finishConnect(); } // 配置成非阻塞方式 channel.configureBlocking(false); // 写入缓冲流 String msg = clientname + ":Hello server"; ByteBuffer wrap = ByteBuffer.wrap(msg.getBytes()); // 向服务端写入信息 channel.write(wrap); // 把当前通道注册到多路复用器中,并且注册事件是OP_READ事件 channel.register(selector,SelectionKey.OP_READ); }else if(key.isReadable()){ read(key); }else if(key.isWritable()){ System.out.println("客户端开始写事件"); } } } } /** * 进行读消息 * @param key * @throws IOException */ private void read(SelectionKey key) throws IOException { // 获取通道 SocketChannel channel = (SocketChannel) key.channel(); // 设置缓存流一次读取的大小 ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取消息 int len = channel.read(buffer);//channel.read()不是阻塞的 if(len != -1){ System.out.println("客户端收到消息:" + new String(buffer.array(),0,len)) ; } } }
2.3 执行
为了方便测试,上面客户端代码再复制一份,改一下客户端名称,现在就有两个客户端了
在服务端ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());打个断点
在两个客户端的ByteBuffer wrap = ByteBuffer.wrap(msg.getBytes());打个断点
启动服务端
启动客户端1
客户端1:
在断点处停住了
服务端:
注意,此时,服务端在int select = selector.select();处阻塞,等待事件的到来
我们可以启动客户端2来看效果
启动客户端2:
客户端2:
停在断点处
服务端:
发现,服务端处理了客户端2的连接事件
放开客户端1的断点
客户端1:写完后,客户端在selector.select();处阻塞,等待事件的到来
服务端:读取到客户端1的数据,并且在断点处停住了
放开服务端断点
服务端:发送完数据。再次等待下一个事件的到来
客户端1:接收完数据,等待下一个事件(一般来说到这里请求就处理完成了)
放开客户端2的断点:
客户端2:写完后,客户端在selector.select();处阻塞,等待事件的到来
服务端:读取到客户端2的数据,并且在断点处停住了
放开服务端断点
服务端:发送完数据。再次等待下一个事件的到来
服务端:
客户端2:接收完数据,等待下一个事件(一般来说到这里请求就处理完成了)
它的关键是把建立连接,发送数据,接收数据分为三个事件来处理。
这样建立完连接,服务器就空闲下来。等待处理下一个事件。
就算这个请求的客户端在write之前去处理业务需要花费很多时间,也不会阻塞服务端。服务端可以去处理其它请求的事件。所以是非阻塞。
标签:BIO,socket,read,API,key,println,客户端,服务端,NIO From: https://www.cnblogs.com/jthr/p/16963671.html