BIO blocking input/output 阻塞式输入输出 一般值java.io下面的类,比如我们经常读写文件就用这个包里面的类,
NIO non-blocking input/output 非阻塞式输入输出, 一般值java.nio下面的类,netty其实就是对nio的封装。相对来说netty书写非阻塞式的网络请求代码更简洁。
BIO编程模式,一般一个网络请求,服务端就会分配一个线程来处理,然后服务端就会阻塞在那里,一直等待客户端将数据发送完毕,服务端收到请求后,再处理完,最后将服务端处理好的数据返回给客户端,最后再释放链接,BIO这种情况下,服务端效率相对来说会比较低。因为等待的过程服务端会阻塞在那里啥也不干。BIO两个核心类Socket,ServerSocket
NIO编程模式,多个网络请求,首先会通过一个或多个Selector来接待,selector会轮询这些请求是否有数据准备好,而且读写数据使用缓存ByteBuffer,不一定说一个链接请求,得先读写完成再去处理另外一个请求,而是可以先缓存起来,Selector轮询那个链接准备好了,再让服务器端新建一个线程来处理。对于每个链接,线程不会阻塞,更充分的使用了服务端的资源,所以通信效率更高。NIO三大核心部分:Channel(通道,SocketChannel,ServerSocketChannel),Buffer(缓冲区),Selector(选择器)
下面通过代码来感受一下,两者的区别:
1.BIO模式下:
客户端连接服务端,发送数据及接收服务端响应,网络通信主要通过Socket和ServerSocket类来完成
package com.figo.test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.util.Scanner;
/**
* 类说明:客户端
*/
public class BioClient {
public static void main(String[] args) throws InterruptedException,
IOException {
//通过构造函数创建Socket,并且连接指定地址和端口的服务端
Socket socket = new Socket("127.0.0.1",8088);
System.out.println("请输入请求消息:");
//启动读取服务端输出数据的线程
new ReadMsg(socket).start();
PrintWriter pw = null;
//允许客户端在控制台输入数据,然后送往服务器
while(true){
pw = new PrintWriter(socket.getOutputStream());
pw.println(new Scanner(System.in).next());
pw.flush();
}
}
//读取服务端输出数据的线程
private static class ReadMsg extends Thread {
Socket socket;
public ReadMsg(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//负责socket读写的输入流
try (BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()))){
String line = null;
//通过输入流读取服务端传输的数据
//如果已经读到输入流尾部,返回null,退出循环
//如果得到非空值,就将结果进行业务处理
while((line=br.readLine())!=null){
System.out.printf("%s\n",line);
}
} catch (SocketException e) {
System.out.printf("%s\n", "服务器断开了你的连接");
} catch (Exception e) {
e.printStackTrace();
} finally {
clear();
}
}
//必要的资源清理工作
private void clear() {
if (socket != null)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务端接收客户端请求,处理完成返回客户端
package com.figo.test;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 类说明:bio的服务端主程序
*/
public class BioServer {
//服务器端必须
private static ServerSocket server;
//线程池,处理每个客户端的请求
private static ExecutorService executorService
= Executors.newFixedThreadPool(5);
private static void start() throws IOException{
try{
//通过构造函数创建ServerSocket
//如果端口合法且空闲,服务端就监听成功
server = new ServerSocket(8088);
System.out.println("服务器已启动,端口号:" + DEFAULT_PORT);
while(true){
Socket socket= server.accept();
System.out.println("有新的客户端连接----" );
//当有新的客户端接入时,打包成一个任务,投入线程池
executorService.execute(new BioServerTask(socket));
}
}finally{
if(server!=null){
server.close();
}
}
}
public static void main(String[] args) throws IOException {
start();
}
}
package com.figo.test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* 新建一个线程处理客户端请求:
*/
public class BioServerTask implements Runnable{
private Socket socket;
public BioServerHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try(//负责socket读写的输出、输入流
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(),
true)){
String message;
String result;
//通过输入流读取客户端传输的数据
//如果已经读到输入流尾部,返回null,退出循环
//如果得到非空值,就将结果进行业务处理
while((message = in.readLine())!=null){
System.out.println("Server accept message:"+message);
result = "这里是否服务端,接收到消息"+message+",业务逻辑已经处理完成!";
//将业务结果通过输出流返回给客户端
out.println(result);
}
}catch(Exception e){
e.printStackTrace();
}finally{
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
2.NIO模式下
客户端连接服务端使用SocketChannel对应BIO的Socket,服务端使用ServerSocketChannel对应BIO的ServerSocket
//客户端
@Test
public void TestClient(){
SocketChannel client=null;
ByteBuffer buffer=null;
try {
//获取socket连接
client = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
//切换非阻塞状态
client.configureBlocking(false);
//指定缓冲区
buffer = ByteBuffer.allocate(1024);
//从控制台读入数据,发送数据到服务端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
buffer.put(scanner.next().getBytes());
//重置limit限制读取limit长度,起始位置position改成0,标记改成-1
/**
* public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
*/
buffer.flip();
client.write(buffer);
buffer.clear();
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
if (client!=null){
client.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//服务端
@Test
public void TestServer() throws IOException {
ServerSocketChannel server = ServerSocketChannel.open();
//1.切换非阻塞模式
server.configureBlocking(false);
//2.绑定
server.bind(new InetSocketAddress("127.0.0.1",8888));
//3.获取选择器
Selector selector = Selector.open();
//4.将服务注册到选择器上,第二个参数式选择键selectionkey表示,有四个常量
// (1)selectionkey.OP_READ 读操作 (2)selectionkey.OP_WRITE 写操作 (3)selectionkey.OP_CONNECT 连接操作 (4)selectionkey.OP_ACCEPT 接收操作
server.register(selector, SelectionKey.OP_ACCEPT); //将读事件注册到选择器上
//5.轮询式的获取选择器上已经准备就绪的事件
while (selector.select()>0){
//6.获取当前选择器中所有注册的“选择键”
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//7.判断什么事件准备就绪
if (key.isAcceptable()){
//8.如果是接收就绪,采取动作
SocketChannel accept = server.accept();
//9.切换非阻塞状态
accept.configureBlocking(false);
//10.将读事件注册到selector上
accept.register(selector,SelectionKey.OP_READ);
}else if (key.isReadable()){
//11.获取当前选择器上“读状态”的通道
SocketChannel channel =(SocketChannel) key.channel();
//12.读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len=0;
while ((len=channel.read(buffer))>0){
buffer.flip();
String s = new String(buffer.array(), 0, len);
System.out.println("接收到客户端的数据:"+s);
buffer.clear();
//数据返回给客户端
//将消息编码为字节数组
byte[] bytes = "服务端处理完成".getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//发送缓冲区的字节数组
channel.write(writeBuffer);
}
}
//执行完任务之后就要取消选择键
iterator.remove();
}
}
}
对比下来,1.发现NIO比BIO多了Selector,使用了多路复用技术,一个selector线程处理多个客户端连接,省去BIO每次都需要创建一个线程的开销。2.另外BIO是面向流Stream的,流是是阻塞式进行的,那么读写需要阻塞线程, NIO是面向缓存Buffer的,读写缓存无需阻塞,那么不会阻塞线程。
阻塞和非阻塞怎么理解?比如买奶茶这件事,阻塞就好像你点好了单,然后在排队等奶茶制作完成,非阻塞是你点好了单,然后去逛旁边的服装店了,奶茶做好了,奶茶店老板电话通知你去取。不用傻傻的在排队等了。