阻塞和非阻塞主要指的是访问 IO 的线程是否会阻塞(或者说是等待)
线程访问资源,该资源是否准备就绪的一种处理方式
BIO是同步阻塞式的IO,以流的方式处理数据(效率低)
Socket编程就是BIO,一个socket连接处理一个线程。当多个socket请求与服务端建立连接时,服务端不能提供相应数量的处理线程,没有分配到处理线程的连接自然就会阻塞或者是被拒绝了。
创建一个服务器端Serve类
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
System.out.println("哈哈");
//接受请求(阻塞)
Socket socket = serverSocket.accept();
System.out.println("阻塞1");
//获取输入流(阻塞)
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("阻塞2");
//获取输出流
System.out.println(reader.readLine());
PrintStream printStream = new PrintStream(socket.getOutputStream());
printStream.println("找我什么事?");
socket.close();
}
}
}
创建一个客户端Clientr类
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 9999);
System.out.println("请输入:");
Scanner scanner = new Scanner(System.in);
String line = scanner.nextLine();
PrintStream printStream = new PrintStream(socket.getOutputStream());
printStream.println(line);
//获取输入流(阻塞)
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println(reader.readLine());
socket.close();
}
}
测试1:只启动服务端,控制台只输出了“哈哈”,说明serverSocket.accept();
是阻塞的
测试2:启动客户端,此时服务端输出了“阻塞1”,没有输出“阻塞2‘’。
测试3:在客户端输入“你在吗”,服务端输出“找我什么事?”说明socket.getInputStream()
也是阻塞的
NIO是对BIO的改进,它是同步非阻塞的IO,以块的方式处理数据(效率高)。NIO基于通道和缓冲区进行数据操作,数据总是从通道读取到缓冲区中,或者从缓冲区中写到通道中。Selector(选择器)用于监听多个通道的时间,(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
NIO的三大核心:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
实际就是一个容器,是一个特殊的数组,缓冲区内部内置了一些机制,能够跟踪和记录缓冲区的状态和变化。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
在NIO中Buffer是一个顶层抽象父类,常用的子类有:
ByteBuffer·····类中的一些常用方法,这里以ByteBuffer类为例
类似于 BIO 中的 stream,例如 FileInputStream 对象,用来建立到目标(文件,网络套接字,硬件设备等)的一个连接,但是需要注意:BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,既可以用来进行读操作,也可以用来进行写操作。
常用的 Channel 类有:
常用的方法,以FileChannel类为例
向文件中写入数据、从文件中读取数据、复制文件
public class TestNIO {
@Test
//向文件中写数据
public void test1() throws Exception {
//创建文件输出流
FileOutputStream fos = new FileOutputStream("basic.txt");
//获取通道
FileChannel channel = fos.getChannel();
//获取缓冲数组
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
//往缓区写入字节数组
String str = "hello nio";
byteBuffer.put(str.getBytes());
//翻转缓冲区
byteBuffer.flip();
//把缓冲区写到通道中
channel.write(byteBuffer);
//关闭流
fos.close();
}
@Test
//从文件中读数据
public void test2() throws Exception {
File file = new File("basic.txt");
//创建文件输入流
FileInputStream fis = new FileInputStream(file);
//获取通道
FileChannel channel = fis.getChannel();
//获取缓区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//从通道中读取数据到缓冲区中
channel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
//关闭流
fis.close();
}
@Test
//复制文件
public void test3() throws Exception {
//创建文件输入流
FileInputStream fis = new FileInputStream("basic.txt");
//创建文件输出流
FileOutputStream fos = new FileOutputStream("I:\\test\\test.txt");
//获取两个通道
FileChannel fisChannel = fis.getChannel();
FileChannel fosChannel = fos.getChannel();
//把fisChannel通道中的数据复制到fosChannel通道中
fosChannel.transferFrom(fisChannel, 0, fisChannel.size());
//关闭流
fis.close();
fos.close();
}
}
注意:使用文件IO时,把缓冲区中的数据写到Channel通道之前一定要调用Buffer类的flip()方法,翻转缓冲区
文件IO中的Channel并不支持非阻塞操作,NIO主要就是进行网络IO,java中网络IO是非阻塞IO。基于事件驱动,非常适用于服务器需要大量连接,但数据量不大的情况。
一个客户端连接用一个线程
把每一个客户端连接交给一个拥有固定数量线程的连接池
使用java中的NIO,用非阻塞IO的方式处理,这种模式可以用一个线程处理大量的客户端连接
作用:
能够检测多个注册服务上的通道是否有事件发生,如果有事件发生,便可以获取到事件然后针对每个事件进行相应的处理。这样就可以使用单线程管理多个客户端连接,这样使得只有真正的读写事件发生时,才会调用函数进行读写。减少了系统的开销,并且不必为每一个连接都创建一个线程,不用去维护多个线程。
该类常用的方法
作用:代表了Selector和网络通道的四种注册关系,一共有四种。
常用方法
作用:网络IO通道,负责读写操作,负责从网络中读取数据到缓冲区中,或者把数据写入到缓冲区中
常用法法:
创建一个客户端NIOClient类
public class NIOClient {
public static void main(String[] args) throws Exception {
//获取通道
SocketChannel channel = SocketChannel.open();
//设置非阻塞方式
channel.configureBlocking(false);
//提供服务器端IP和端口号
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
//尝试连接服务器
if (!channel.connect(address)) {
while (!channel.finishConnect()) {//体现了nio非阻塞的优势
System.out.println("Client连接客户端的同时,可以做别的事情");
}
}
String str = "你好啊,我是NIO客户端";
//获取缓冲区,并向其中写入数据
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
//发送数据
channel.write(byteBuffer);
System.in.read();
}
}
创建一个服务器端NIOServer类
public class NIOServer {
public static void main(String[] args) throws Exception {
//获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口号
serverSocketChannel.bind(new InetSocketAddress(9999));
//设置非阻塞方式
serverSocketChannel.configureBlocking(false);
//获取选择器
Selector selector = Selector.open();
//将ServerSocketChannel对象注册给Selector对象
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//register()方法的第二个参数是设置的监听事件
//干活
while (true) {
//监控客户端
if (selector.select(2000)==0) {//nio非阻塞的优势
System.out.println("没有客户端请求,我可以做别的");
continue;
}
//得到selectionKey
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历selectionKey,判断通道里的事件
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {//判断是否有连接请求
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {//客户端请求事件
System.out.println("OP_ACCEPT");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (selectionKey.isReadable()) {//读取客户端数据事件
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
channel.read(byteBuffer);
System.out.println("收到客户端数据:"+new String(byteBuffer.array()));
}
//手动从集合中移除当前selectionKey,防止重复处理
iterator.remove();
}
}
}
}
使用NIO完成一个简易的聊天案例。要求:客户端能够给服务器端发送消息,服务器接收到消息时候,能够将消息广播给其他所有的客户端。
创建一个聊天客户端类ChatClient
public class ChatClient {
private final String IP = "127.0.0.1";
private int port = 9999;
private SocketChannel socketChannel; //网络通道
private String userName;
public ChatClient() throws IOException {
//获取网络通道
socketChannel = SocketChannel.open();
//设置非阻塞方式
socketChannel.configureBlocking(false);
if (!socketChannel.connect(new InetSocketAddress(IP,port))) {
while (!socketChannel.finishConnect()) {//体现了NIO非阻塞的优势
System.out.println("连接服务器的同事还可以做别的事");
}
}
//得到客户端IP作为用户名
userName = socketChannel.getLocalAddress().toString().substring(1);
System.out.println("------------------"+userName+"is ready---------------");
}
//向服务端发送数据
public void sendMsg(String message) throws IOException {
//如果从键盘录入的为“bye”则关闭socketChannel,退出聊天
if (message.equalsIgnoreCase("bye")) {
socketChannel.close();
return;
}
String msg = userName + "说:" + message;
//获取缓冲区
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
socketChannel.write(byteBuffer);
}
//从服务端接收数据
public void receiveMsg() throws IOException {
//获取缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将接收到的数据写到缓冲区中
int count = socketChannel.read(byteBuffer);
if (count > 0) {
System.out.println(new String(byteBuffer.array()).trim());
}
}
}
创建一个聊天客户端类ChatServer
public class ChatServer {
private ServerSocketChannel serverSocketChannel;
private Selector selector;
//获取客户端连接
public ChatServer() throws IOException {
//获取ServerSocketChannel通道
serverSocketChannel = ServerSocketChannel.open();
//绑定端口号
serverSocketChannel.bind(new InetSocketAddress(9999));
//设置非阻塞方式
serverSocketChannel.configureBlocking(false);
//获取Selector
selector = Selector.open();
//把serverSockerChannel注册到服务器
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
//监控客户端连接
public void start() throws IOException {
//干活
while (true) {
if (selector.select(2000) == 0) {//体现了NIO非阻塞的优势
System.out.println("没有客户端连接,我可以做别的");
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
//接收请求,得到SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//设置非阻塞方式
socketChannel.configureBlocking(false);
//将socketChannel注册到selector中
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress().toString().substring(1)+"上线了...");
}
if (selectionKey.isReadable()) {
//读取客户端发来的数据
readMsg(selectionKey);
}
//一定要把当前key删掉,防止重复处理
iterator.remove();
}
}
}
//读取客户端发送来的数据并且进行广播
private void readMsg(SelectionKey selectionKey) throws IOException {
//获取通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//获取缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//读取客户端发送的数据
int count = socketChannel.read(byteBuffer);
if (count >0) {
String message = new String(byteBuffer.array());
printMsg(message);
//将客户端发来的消息进行广播
broadCast(message,socketChannel);
}
}
private void broadCast(String message,SocketChannel socketChannel) throws IOException {
//得到所有已经就绪的Channel
for (SelectionKey key : selector.keys()) {
Channel channel = key.channel();
if (channel instanceof SocketChannel && channel != socketChannel) {
SocketChannel targetChannel = (SocketChannel) channel;
//获取缓冲区
ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());
targetChannel.write(byteBuffer);
}
}
}
private void printMsg(String msg) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(dateFormat.format(new Date())+" "+msg);
}
public static void main(String[] args) throws IOException {
new ChatServer().start();
}
}
创建聊天测试类TestChat
public class TestChat {
public static void main(String[] args) throws IOException {
final ChatClient client = new ChatClient();
new Thread() {
public void run() {
while (true) {
try {
client.receiveMsg();
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
client.sendMsg(msg);
}
}
}
先由操作系统完成客户端的请求,再通知服务器去启动线程进行处理。
对比总结 | BIO | NIO | AIO |
---|---|---|---|
IO方式 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
API使用难度 | 简单 | 复杂 | 复杂 |
可靠性 | 低 | 高 | 高 |
吞吐量 | 低 | 高 | 高 |
适用于连接较少,对服务器资源消耗很大,但是编程简单。是同步阻塞的。
举例:你到餐馆点餐,然后在那儿等着,什么也做不了,只要饭还没有好,就要必须等着
使用于连接数量比较多且连接时间比较短的架构,比如聊天服务器,编程比较复杂。是同步非阻塞的
举例:你到餐馆点完餐,然后就可以去玩儿了,玩一会儿就回饭馆问一声,饭好了没。
适用于连接数量多而且连接时间长的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。是异步非阻塞的。
举例:饭馆打电话给你说,我们知道你的位置,待会儿给您送来,你安心的玩儿就可以了。类似于外卖。