最近兩三年一直使用的網絡編程框架是Netty,今天就來聊聊爲什麼使用Netty框架,爲什麼不使用Java原生的類庫。說到這,就需要把BIO、NIO、AIO梳理一遍。
網絡編程的基本模型是Client/Server模型,也就是兩個進程之間進行相互通信,服務器端提供位置信息,包括綁定的IP地址和監聽端口,客戶端通過連接向服務器端監聽地址發起連接請求,通過三次握手建立連接,如果連接成功,雙方就可以通過網絡套接字Socket進行通信。
BIO是Blocking IO的縮寫,又稱同步阻塞IO。BIO使用的是傳統的java.io包,它是基於流模型實現的,服務器端和客戶端交互的方式是同步、阻塞。
在BIO同步阻塞模型開發中,服務器端的ServerSocket負責綁定IP地址,啓動監聽端口;客戶端Socket負責發起連接;連接成功之後,雙方通過輸入和輸出流進行同步阻塞式通信。
採用BIO通信模型的服務器端,通常由一個Acceptor線程負責監聽客戶端的連接,服務器端接收到客戶端的連接請求之後爲每個客戶端創建一個新的線程進行鏈路處理,處理完成之後,通過輸出流返回應答給客戶端。
BIO模型最大的問題,就是缺乏彈性伸縮能力,當客戶端併發訪問量增加以後,服務器端的線程池和客戶端併發訪問數呈1:1正比關係。線程是java虛擬機比較寶貴的資源,當線程數膨脹之後,虛擬機的性能就會急劇下降,隨着併發訪問量的繼續增大,系統就會發生線程堆棧溢出,創建線程失敗,最終導致系統宕機或假死,不能對外提供服務。
下面看服務器端代碼:
package com.test.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO的服務器端
* @author 程就人生
* @Date
*/
public class HelloServer {
public static void main( String[] args ){
int port = 8080;
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
Socket socket = null;
// 通過無限循環監聽客戶端連接
while(true){
// 如果沒有客戶端連接,則阻塞在Accept操作上
socket = serverSocket.accept();
// 如果有客戶端連接,則創建一個線程,使用這個線程處理這個鏈路
new Thread(new HelloHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* hello處理類
* @author 程就人生
* @Date
*/
class HelloHandler implements Runnable{
private Socket socket;
public HelloHandler(Socket socket) {
this.socket = socket;
}
public void run() {
BufferedReader reader = null;
PrintWriter writer = null;
try {
reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
// 返回給客戶端,是否刷新設置爲true
writer = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while(true){
body = reader.readLine();
if(body == null){
break;
}
System.out.println("服務器端接收到的:" + body);
writer.println("來自服務器端的響應!");
}
} catch (IOException e) {
e.printStackTrace();
// 出現異常時,對資源的釋放
if(reader != null){
try {
reader.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if(writer != null){
writer.close();
writer = null;
}
if(socket != null){
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
HelloServer在20行通過構造函數創建了一個端口號爲8080的ServerSocket。在第23行通過無限循環監聽客戶端的連接。在第25行,如果沒有客戶端連接則阻塞在accept操作上。
運行服務端代碼。在cmd控制檯,通過jps -l 獲取進程ID。
通過 jstack pid 打印堆棧信息,main方法阻塞在accept操作上。
當有新的客戶端連接時,執行27行代碼,以socket爲參數構造一個線程HelloHandler來處理新加入的socket鏈接。
在HelloHandler中,通過51行的BufferedReader讀取一行,如果已經讀到了輸入流的末尾,則退出循環。如果讀到了非空值則進行打印,並使用PrintWriter 響應客戶端。
客戶端代碼:
package com.test.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* BIO的客戶端
* @author 程就人生
* @Date
*/
public class HelloClient {
public static void main( String[] args ){
int port = 8080;
Socket socket = null;
BufferedReader reader = null;
PrintWriter writer = null;
try {
socket = new Socket("127.0.0.1", port);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
writer = new PrintWriter(socket.getOutputStream(), true);
writer.println("來自客戶端的hello!");
System.out.println(reader.readLine());
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
// 釋放資源
if(reader != null){
try {
reader.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if(writer != null){
writer.close();
writer = null;
}
if(socket != null){
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
客戶端代碼通過Socket進行創建,通過BufferedReader讀取服務器端的響應,通過PrintWriter對服務器進行響應。響應完畢後,釋放資源。
運行客戶端代碼,查看客戶端控制檯輸出。客戶端響應完畢,釋放資源,關閉連接。
查看服務器端控制檯輸出。服務器端控制檯接收到客戶端的請求,打印輸出,並等待新的客戶端鏈接。
以上便是BIO開發的簡單示例,在網上也有不少類似的demo。有面試NIO的地方,就少不了BIO。通過這個簡單的demo,也可對BIO有個深刻的瞭解。