一、BIO概述
BIO即同步阻塞IO,實現模型爲一個連接就需要一個線程去處理。這種方式簡單來說就是當有客戶端來請求服務器時,服務器就會開啓一個線程去處理這個請求,即使這個請求不幹任何事情,這個線程都一直處於阻塞狀態。
BIO模型有很多缺點,最大的缺點就是資源的浪費。想象一下如果QQ使用BIO模型,當有一個人上線時就需要一個線程,即使這個人不聊天,這個線程也一直被佔用,那再多的服務器資源都不管用
二、BIO代碼實踐
首先建立Server,建立一個ServerSocket對象,綁定端口,然後等待連接,如果連接成功就新建一個線程去處理連接。
public class server {
private static Socket socket=null;
public static void main(String[] args) {
try {
//綁定端口
ServerSocket serverSocket=new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080));
while (true){
//等待連接 阻塞
System.out.println("等待連接");
socket = serverSocket.accept();
System.out.println("連接成功");
//連接成功後新開一個線程去處理這個連接
new Thread(new Runnable() {
@Override
public void run() {
byte[] bytes=new byte[1024];
try {
System.out.println("等待讀取數據");
//等待讀取數據 阻塞
int length=socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,length));
System.out.println("數據讀取成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Client {
public static void main(String[] args) {
Socket socket= null;
try {
socket = new Socket("127.0.0.1",8080);
socket.getOutputStream().write("一條數據".getBytes());
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
這樣就實現了一個BIO,但是BIO的缺點實在太明顯了,因此在JDK1.4的時候,NIO出現了。
三、NIO概述
BIO是阻塞的,如果沒有多線程,BIO就需要一直佔用CPU,而NIO則是非阻塞IO,NIO在獲取連接或者請求時,即使沒有取得連接和數據,也不會阻塞程序。NIO有幾個知識點需要掌握,Channel(通道),Buffer(緩衝區), Selector(選擇器(多路複用))。
Channel既可以用來進行讀操作,又可以用來進行寫操作。NIO中常用的Channel有FileChannel
、SocketChannel、ServerSocketChannel、DatagramChannel。
Selector 一般稱爲選擇器或者多路複用器 。它是Java NIO核心組件中的一個,用於檢查一個或多個NIO Channel(通道)的狀態是否處於可讀、可寫。在javaNIO中使用Selector往往是將Channel註冊到Selector中。
四、NIO代碼實踐
在BIO的代碼中,有兩處位置發生了阻塞:1.等待請求時,2.等待數據時
socket = serverSocket.accept();
int length=socket.getInputStream().read(bytes);
而NIO的代碼就是要讓這兩處的阻塞變爲非阻塞,Socket本身並不支持NIO,因爲java創建了支持NIO的socket,SocketChannel和ServerSocketChannel。通過configureBlocking方法設置爲非阻塞。
ServerSocketChannel是NIO中的通道,ByteBuffer是獲取數據的緩衝區,多路複用機制我在這裏使用for循環代替,而多路複用機制實際上是通過select/epoll/poll實現的,這需要涉及到底層的知識,當一個socket連接成功時,就將他放入集合中,並不斷遍歷這個集合觀察是否有哪個socket發送了數據。
public class Server {
//list用來存放已經連接的socket
static List<SocketChannel> list=new ArrayList<>();
//緩衝區,用來獲取數據
static ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
public static void main(String[] args) {
try {
//創建一個socketchannel,並綁定端口
ServerSocketChannel server=ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
//設置這個通道爲非阻塞,對應於BIO的第一處阻塞
server.configureBlocking(false);
while (true){
//模擬多路複用器,不斷查詢是否有數據發過來
for (SocketChannel socketChannel1:list){
byteBuffer.clear();
int length = socketChannel1.read(byteBuffer);
if (length>0){
byteBuffer.flip();
System.out.println(new String(byteBuffer.array()));
}
}
//非阻塞的獲取連接
SocketChannel socketChannel = server.accept();
//如果沒有,則等待
if (socketChannel==null){
System.out.println("等待連接");
Thread.sleep(1000);
}else {
//如果獲取到了一個連接,則把這個連接也設置爲非阻塞,對應於BIO的第二處阻塞
socketChannel.configureBlocking(false);
//將設置連接放進list中,不斷輪詢是否有消息發出
list.add(socketChannel);
System.out.println("連接成功");
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class client {
public static void main(String[] args) {
try {
Socket socket=new Socket("127.0.0.1",8080);
Scanner scanner=new Scanner(System.in);
String str=scanner.next();
socket.getOutputStream().write(str.getBytes());
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
當啓動服務器端時,不斷輸出等待連接,當啓動客戶端後,輸出了連接成功,但此時並未阻塞程序,而是繼續等待。
當有數據發送給服務器端時,則接收到了ByteBuffer的數據,程序的所有執行過程都處於非阻塞狀態。
五、AIO概述
AIO是在JDK1.7中推出的新的IO方式--異步非阻塞IO,也被稱爲NIO2.0,AIO在進行讀寫操作時,直接調用API的read和write方法即可,這兩種均是異步的方法,且完成後會主動調用回調函數。簡單來講,當有流可該取時,操作系統會將可讀的流傳入read方法的緩衝區,並通知應用程序;對於寫操作而言,當保作系統將write方法傳遞的流寫入完畢時,操作系統主動通知應用程序。
Java提供了四個異步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。
六、AIO代碼實踐
服務器端代碼:AIO的創建方式和NIO類似,先創建通道,再綁定,再監聽。只不過這裏的監聽方式用了類似遞歸的監聽方式。在讀取數據的操作中,read方法也和NIO有區別。
public class AIOServer {
public static void main(String[] args) {
try {
//創建異步通道
AsynchronousServerSocketChannel serverSocketChannel=AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
System.out.println("等待連接中");
//在AIO中,accept有兩個參數,
// 第一個參數是一個泛型,可以用來控制想傳遞的對象
// 第二個參數CompletionHandler,用來處理監聽成功和失敗的邏輯
// 如此設置監聽的原因是因爲這裏的監聽是一個類似於遞歸的操作,每次監聽成功後要開啓下一個監聽
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
//請求成功處理邏輯
@Override
public void completed(AsynchronousSocketChannel result, Object attachment) {
System.out.println("連接成功,處理數據中");
//開啓新的監聽
serverSocketChannel.accept(null,this);
handledata(result);
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("失敗");
}
});
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handledata(AsynchronousSocketChannel result) {
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//通道的read方法也帶有三個參數
//1.目的地:處理客戶端傳遞數據的中轉緩存,可以不使用
//2.處理客戶端傳遞數據的對象
//3.處理邏輯,也有成功和不成功的兩個寫法
result.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
if (result>0){
attachment.flip();
byte[] array = attachment.array();
System.out.println(new String(array));
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("失敗");
}
});
}
}
客戶端代碼基本上沒有太多差別,可以使用同步的方式也可以使用異步方式,這裏使用異步處理的方式:
public class AIOClient {
public static void main(String[] args) {
try {
AsynchronousSocketChannel socketChannel=AsynchronousSocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
Scanner scanner=new Scanner(System.in);
String next = scanner.next();
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
byteBuffer.put(next.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}