從BIO到NIO
我們知道,Java服務端程式設計,很重要的一塊就是IO,而我們的Java IO,經歷了由BIO到NIO再到AIO的過程。
首先,我們來看一下什麼是BIO:
1.普通BIO
我們剛開始學Java Socket的時候,可能都寫過這麼一段程式碼:
ServerSocket serverSocket = new ServerSocket(8000); while (true){ Socket socket = serverSocket.accept(); InputStream is = socket.getInputStream(); byte [] buff = new byte[1024]; int len = is.read(buff); while (len > 0){ String msg = new String(buff,0,len); System.out.println("收到" + msg); len = is.read(buff); } }
顯然,這段程式碼把對客戶端接入後的處理都放在了while迴圈中,也就是說,這段程式碼一段時間只能處理一個客戶端連線,那麼問題來了,我們想一想,能作為服務端的伺服器效能應該是不錯的,所以這種方式浪費了我們的效能,而且,由於一次處理一個客戶端連線,併發量自然而然地上不去,所以,這個方法不會在生產環境中使用。
2.多執行緒式BIO
為了提高併發量,我們可以將程式碼改成這個樣子:
ServerSocket serverSocket = new ServerSocket(8000); while (true){ Socket socket = serverSocket.accept(); new Thread(new Handler(socket)).start(); }
public class Handler implements Runnable { private Socket socket; public Handler(Socket socket) { this.socket = socket; } @Override public void run() { InputStream is = null; try { is = socket.getInputStream(); byte[] buff = new byte[1024]; int len = is.read(buff); while (len > 0) { String msg = new String(buff, 0, len); System.out.println("收到" + msg); len = is.read(buff); } } catch (IOException e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
但是這樣會有一個問題,我們來一個客戶端連線不管三七二十一就會new一個執行緒,這樣執行緒數和客戶端連線數是1:1的關係,我們知道,執行緒的建立和銷燬是很耗資源的一件事情,同樣地,這樣也會增加執行緒上下文切換的開銷。
為了降低執行緒建立和銷燬的開銷,我們可以引入執行緒池(程式碼先不貼了),但是這樣並沒有從根本上解決問題。
為了從根本上解決問題,Java引入了NIO:
3.NIO
我們可以這麼來理解,我們把web伺服器比作銀行,客戶端發起的連線比作要辦理業務的客戶,銀行處理業務的工作人員比作處理客戶端連線的執行緒,由於銀行的資源(假如說只有一臺計算機能夠處理業務)有限,那麼同一時刻只有一個人可以辦理業務。
那麼我們的BIO就相當於:進來一個客戶,就給它分配一個業務員,但是業務員只有得到資源(計算機)才能處理業務,所以客戶一進入銀行就給他分配業務員是沒什麼卵用的,還浪費資源(業務員是要領工資的)
而我們的NIO呢,就相當於只有一個業務員,但是有一個排號系統,客戶進入銀行先在排號系統登記,領一個號碼,等到輪到某個客戶處理業務了,才給他分配一個業務員,處理他的業務。
public class NIOServer {
private int port = 8000;
private Selector selector = null;
private Charset charset = Charset.forName("UTF-8");
public NIOServer(int port) throws IOException {
this.port = port;
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(this.port));
server.configureBlocking(false);
selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
}
public void listener() throws IOException {
while (true) {
int wait = selector.select();
if (wait == 0) continue;
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
process(key);
}
}
}
public void process(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
StringBuilder content = new StringBuilder();
try {
while (client.read(buff) > 0) {
buff.flip();
content.append(charset.decode(buff));
}
System.out.println(content);
key.interestOps(SelectionKey.OP_READ);
} catch (IOException io) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
public static void main(String[] args) throws IOException {
new NIOServer(8000).listener();
}
}
NIO呢,就是我們來一個客戶端連線,先不處理,先在Selector上註冊,然後不斷地輪詢註冊在Selector上面的channel,當準備好讀或者寫的時候,再對客戶端連線進行操作,這樣呢,就大大節省了服務端的資源,因此十分適合於大量長連線的場景。