不多BB開門見山
- BIO是同步阻塞式的IO,ServerSocket.accept(),InputStream.read(),OutputStream.write()都會阻塞線程,使得一整個線程發生阻塞而無法去處理其他的工作,在這種模式下服務器要為每個客戶端的請求都要創建一個線程,所以這種模式無法滿足高并發,高性能的場景
- NIO是同步非阻塞式的IO,相對BIO優點就是不阻塞線程,客戶端連接,讀寫操作全是異步進行的,適合高性能,高負載的網絡服務器
NIO的核心概念:
Channel:在我們傳統的io中主要是使用流也就是Stream,Stream是單向的并且是阻塞的,只能是輸出或者是輸入,而Channel是雙向的,可以輸入和輸出同時進行,并且擁有兩種模式:阻塞和非阻塞。利用Channel的非阻塞這樣我們就可以使用一個線程去處理多個Channel的多個讀寫,Channel主要有以下幾種類型
1、FileChannel:從文件讀取數據的
2、DatagramChannel:讀寫 UDP 網絡協議數據
3、SocketChannel:讀寫 TCP 網絡協議數據
4、ServerSocketChannel:可以監聽 TCP 連接Selector: Selector叫做多路復用器,是用于監控多條Channel的。Selector可以輪詢注冊在其上的Channel,當Channel發生讀或者寫事件時,Channel就會被輪詢出來,可以通過SelectionKey獲取這些Channel
Buffer:Buffer是NIO的讀寫的中轉池,它提供了對數據的結構化訪問以及維護讀寫位置等信息。NIO的讀和寫都是要從Buffer中進行,所有數據都通過 Buffer 對象處理,所以,輸出操作時不會將字節直接寫入到 Channel 中,而是將數據寫入到 Buffer 中;同樣,輸入操作也不會從 Channel 中讀取字節,而是將數據從 Channel 讀入 Buffer,再從 Buffer 獲取這個字節。
Buffer
Buffer實質是一個字節數組,包含這些類型:CharBuffer、DoubleBuffer、IntBuffer、LongBuffer、ByteBuffer、ShortBuffer、FloatBuffer
buffer的結構:
- position:讀模式時是讀取數據時的起始位置,寫模式時會記錄寫入數據的位置,從寫模式切換到讀模式時,置為 0此時limit會置為剛剛position的位置幫我們記錄寫了多少數據,為什么寫模式切到讀模式position要置為0呢,原因是寫模式position會處于數據的最后一個位置,如果讀模式不置為零的話是從寫入數據的最后一個位置開始讀到limit的位置,所以會讀不到任何數據;
- limit:代表最多能寫入或者讀取多少單位的數據,寫模式下等于最大容量 capacity,代表寫的最大容量是多少;從寫模式切換到讀模式時,等于position,然后再將 position 置為 0,所以,讀模式下,limit 表示最大可讀取的數據量,position 代表從哪里讀起,這個值與實際寫入的數量相等。
- capacity:表示 buffer 容量,創建時分配。
Buffer讀寫模式的切換
Buffer利用flip() 進行讀寫模式的切換,在寫模式下position 會記錄寫入的字節的位置,limit會等于capacity表示允許最大可寫容量,讀的時候調用flip(),Buffer切換至讀模式,limit會被至為position的位置表示最大的可讀容量,而position會被至為0,一旦讀完了所有的數據,就需要清空緩沖區,讓它可以再次被寫入。有兩種方式能清空緩沖區:調用 clear() 或 compact() 方法。clear() 方法會清空整個緩沖區。
compact() 方法只會清除已經讀過的數據。任何未讀的數據都被移到緩沖區的起始處,新寫入的數據將放到緩沖區未讀數據的后面。
操作buffer時的步驟為:
1.寫入數據到 Buffer;
2.調用 flip() 方法;
3.從 Buffer 中讀取數據;
4.調用 clear() 方法或者 compact() 方法。
5.當向 Buffer 寫入數據時,Buffer 會記錄下寫了多少數據。一旦要讀取數據,需要通過 .flip() 方法將 Buffer 從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到 Buffer 的所有數據。
Selector
Selector通常將非阻塞 IO 的空閑時間用于在其他通道上執行 IO 操作,所以單獨的線程可以管理多個輸入和輸出通道。通道和緩沖區的機制,使得 Java NIO 實現了同步非阻塞 IO 模式,在此種方式下,用戶進程發起一個 IO 操作以后便可返回做其它事情,而無需阻塞地等待 IO 事件的就緒,但是用戶進程需要時不時的詢問 IO 操作是否就緒,這就要求用戶進程不停的去詢問,從而引入不必要的 CPU 資源浪費。鑒于此,需要有一個機制來監管這些 IO 事件,如果一個 Channel 不能讀寫(返回 0),我們可以把這件事記下來,然后切換到其它就緒的連接(channel)繼續進行讀寫。在 Java NIO 中,這個工作由 selector 來完成,
Selector 是一個對象,它可以接受多個 Channel 注冊,監聽各個 Channel 上發生的事件,并且能夠根據事件情況決定 Channel 讀寫。這樣,通過一個線程可以管理多個 Channel,從而避免為每個 Channel 創建一個線程,節約了系統資源。如果你的應用打開了多個連接(Channel),但每個連接的流量都很低,使用 Selector 就會很方便。
要使用 Selector,就需要向 Selector 注冊 Channel,然后調用它的 select() 方法。這個方法會一直阻塞到某個注冊的通道有事件就緒,這就是所說的輪詢。一旦這個方法返回,線程就可以處理這些事件。
Selector說白了就是能幫我們把注冊在上面的各個通道并且是我們感興趣的事件輪詢出來,這個輪詢不是普通的循環遍歷,而是一種系統級別的操作,性能比普通的輪詢高,所以輪詢通道連接最好不要自己用代碼實現,而是使用多路復用器
一個 I/O 線程可以并發處理 N 個客戶端連接和讀寫操作,這從根本上解決了傳統BIO一連接一線程模型,性能、彈性伸縮能力和可靠性都得到了極大的提升.NIO編碼
整個NIO的步驟如下:server端
public class ChatServer {
private static final int DEFAULT_PORT = 8888;
private static final String QUIT = "quit";
private static final int BUFFER = 1024;
private ServerSocketChannel server;
private Selector selector;
private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);
private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);
private Charset charset = Charset.forName("UTF-8");
private int port;
public ChatServer() {
this(DEFAULT_PORT);
}
public ChatServer(int port) {
this.port = port;
}
private void start() {
try {
//打開ServerSocketChannel,這是所有客戶端連接的父管道,所有的客戶端連接都要通過它
server = ServerSocketChannel.open();
//設置非阻塞通道
server.configureBlocking(false);
server.socket().bind(new InetSocketAddress(port));
//多路復用器
selector = Selector.open();
//將ServerSocketChannel注冊到多路復用器上,并監聽客戶端連接事件
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("啟動服務器, 監聽端口:" + port + "...");
while (true) {
//表示沒有就緒的通道,跳過本次循環
if (selector.selectNow()==0){//非阻塞監聽,也可以設置為阻塞監聽:selector.select
continue;
}
//selector.select如果設置為阻塞監聽就不需要上面的if判斷,因為設置阻塞監聽如果通道沒有事件,它會將自己阻塞住,有事件才玩往下走
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey key : selectionKeys) {
// 處理被觸發的事件
handles(key);
}
//清理通道,避免事件重復處理
selectionKeys.clear();
//另一種李大佬書上的寫法
/** selector.select();
selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
handles(iterator.next());
//清理事件,避免事件重復處理
iterator.remove();
}
**/
}
} catch (IOException e) {
e.printStackTrace();
} finally {
close(selector);
}
}
private void handles(SelectionKey key) throws IOException {
// ACCEPT事件 - 和客戶端建立了連接
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//將客戶端連接進來的通道
SocketChannel client = server.accept();
client.configureBlocking(false);
//注冊通道的讀監聽事件
client.register(selector, SelectionKey.OP_READ);
System.out.println(getClientName(client) + "已連接");
}
// READ事件 - 客戶端發送了消息
else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
//讀取客戶端消息
String fwdMsg = receive(client);
if (fwdMsg.isEmpty()) {
// 取消這個通道的監聽事件
key.cancel();
//通知多路復用器重新調整監聽,selector.wakeup()在selector是阻塞模式的時候可以用,可以喚醒阻塞
//selector.wakeup();
} else {
System.out.println(getClientName(client) + ":" + fwdMsg);
//消息轉發
forwardMessage(client, fwdMsg);
// 檢查用戶是否退出
if (readyToQuit(fwdMsg)) {
key.cancel();
//通知多路復用器重新調整監聽,selector.wakeup()在selector是阻塞模式的時候可以用,可以喚醒阻塞
//selector.wakeup();
System.out.println(getClientName(client) + "已斷開");
}
}
}
}
//將其他客戶的消息轉發給客戶端
private void forwardMessage(SocketChannel client, String fwdMsg) throws IOException {
for (SelectionKey key: selector.keys()) {
Channel connectedClient = key.channel();
if (connectedClient instanceof ServerSocketChannel) {
continue;
}
//key.isValid()判斷SelectionKey是否有效,
//client.equals(connectedClient) 判斷消息不是這個客戶端發的,避免自己給自己發消息
if (key.isValid() && !client.equals(connectedClient)) {
wBuffer.clear();
//寫消息到Buffer中
wBuffer.put(charset.encode(getClientName(client) + ":" + fwdMsg));
//切換到讀狀態
wBuffer.flip();
//判斷Buffer中是否還殘存數據
while (wBuffer.hasRemaining()) {
//將Buffer的數據寫入通道
((SocketChannel)connectedClient).write(wBuffer);
}
}
}
}
//接收客戶端消息
private String receive(SocketChannel client,SelectionKey key) throws IOException {
try {
rBuffer.clear();
//將通道的消息讀到緩沖區
while (client.read(rBuffer) > 0) ;
//切換到讀模式
rBuffer.flip();
}catch (Exception e){
key.cancel();
client.socket().close();
client.close();
System.out.println(e.toString());
return StringUtils.EMPTY;
}
//從緩沖區讀數據
return String.valueOf(charset.decode(rBuffer));
}
private String getClientName(SocketChannel client) {
return "客戶端[" + client.socket().getPort() + "]";
}
private boolean readyToQuit(String msg) {
return QUIT.equals(msg);
}
private void close(Closeable closable) {
if (closable != null) {
try {
closable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ChatServer chatServer = new ChatServer(8888);
chatServer.start();
}
}
client
public class ChatClient {
private static final String DEFAULT_SERVER_HOST = "127.0.0.1";
private static final int DEFAULT_SERVER_PORT = 8888;
private static final String QUIT = "quit";
private static final int BUFFER = 1024;
private String host;
private int port;
private SocketChannel client;
private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);
private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);
private Selector selector;
private Charset charset = Charset.forName("UTF-8");
public ChatClient() {
this(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
}
public ChatClient(String host, int port) {
this.host = host;
this.port = port;
}
public boolean readyToQuit(String msg) {
return QUIT.equals(msg);
}
private void close(Closeable closable) {
if (closable != null) {
try {
closable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void start() {
try {
client = SocketChannel.open();
client.configureBlocking(false);
selector = Selector.open();
//注冊連接監聽事件
client.register(selector, SelectionKey.OP_CONNECT);
client.connect(new InetSocketAddress(host, port));
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey key : selectionKeys) {
handles(key);
}
selectionKeys.clear();
}
} catch (IOException e) {
e.printStackTrace();
} catch (ClosedSelectorException e) {
// 用戶正常退出
} finally {
close(selector);
}
}
/*
由于我們使用的SocketChannel處于非阻塞模式,當調用connect()方法時,調用會立即返回,但是連接的過程還在進行,
需要后續調用finishConnect()方法來完成連接過程。在連接過程已經啟動,但尚未完成之前,
isConnectionPending()會返回true,這就是我們此時在檢測的狀態。如果連接未能正常創建,
調用finishConnect()則會拋出IOException異常,標志著連接失敗。
*/
private void handles(SelectionKey key) throws IOException {
// CONNECT事件 - 連接就緒事件
if (key.isConnectable()) {
SocketChannel client = (SocketChannel) key.channel();
if (client.isConnectionPending()) {
//調用finishConnect()方法來完成連接過程
client.finishConnect();
// 處理用戶的輸入
new Thread(new UserInputHandler(this)).start();
}
client.register(selector, SelectionKey.OP_READ);
}
// READ事件 - 服務器轉發消息
else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
String msg = receive(client);
if (msg.isEmpty()) {
close(selector);
} else {
System.out.println(msg);
}
}
}
public void send(String msg) throws IOException {
if (msg.isEmpty()) {
return;
}
wBuffer.clear();
wBuffer.put(charset.encode(msg));
wBuffer.flip();
while (wBuffer.hasRemaining()) {
client.write(wBuffer);
}
// 檢查用戶是否準備退出
if (readyToQuit(msg)) {
close(selector);
}
}
private String receive(SocketChannel client) throws IOException {
rBuffer.clear();
while (client.read(rBuffer) > 0);
rBuffer.flip();
return String.valueOf(charset.decode(rBuffer));
}
class UserInputHandler implements Runnable {
private ChatClient chatClient;
public UserInputHandler(ChatClient chatClient) {
this.chatClient = chatClient;
}
@Override
public void run() {
try {
// 等待用戶輸入消息
BufferedReader r =
new BufferedReader(new InputStreamReader(System.in));
while (true) {
String input = r.readLine();
// 向服務器發送消息
chatClient.send(input);
// 檢查用戶是否準備退出
if (chatClient.readyToQuit(input)) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ChatClient client = new ChatClient("127.0.0.1", 8888);
client.start();
}
}