BIO的步驟流程說明:
BIO代碼實現:
// 用於存儲讀取到的數據
// 用於存儲讀取到的數據
byte[] bytes = new byte[1024];
ServerSocket serverSocket= null;
try {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080)) ; // 綁定監聽的端口
for(;;){
//線程阻塞,等待連接
Socket socket = serverSocket.accept();
// 如果有客戶端連接進來就往下走,如果沒有讀取到數據會堵塞
InputStream input = socket.getInputStream();
try{
int read = input.read(bytes);
String content = new String(bytes);
System.out.println(content);
}} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
客戶端:
try {
Socket socket = new Socket("127.0.0.1",8080);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
總結:
其中BIO可以通過採用多線程完成併發請求處理,accept方法處阻塞,不變,如果監聽到有客戶端連接進來,則開啓一個線程,並且將獲取到的和客戶端長連接交互的socket 作爲對象傳入子線程中,然後讓read方法的阻塞在子線程中出現,而主線程則繼續執行,然後循環到accept()方法中等待第二個客戶端線程的鏈接。
其中負責監聽的socket對象在創建之後會返回一個文件描述符,用於bind一個端口,這個bind調用的是操作系統的內核函數,調用listen 方法用於監聽,而accept方法則阻塞住不往下走,直到客戶端通過操作系統完成三次握手的過程之後accept會接受到這個客戶端,返回一個文件描述符(簡單理解就是和客戶端交互的socket對象),然後進入read方法,進行阻塞。可以通過開闢線程來執行read,而accept則循環監控連接;
缺點:線程頻繁創建和銷燬(線程棧消耗),上線文切換頻繁,不安全。無法解決單線程問題
NIO non-blocking IO(非阻塞IO)關鍵組成:
- channel 通道
- buffer 緩衝區
- selector 選擇器
1、channel通道:
Java NIO的通道類似流,但又有些不同,channel既可以從通道中讀取數據,又可以寫數據到通道。但流的讀寫通常是單向的。通道可以異步地讀寫。通道中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入。正如上面所說,從通道讀取數據到緩衝區,從緩衝區寫入數據到通道。
通道的類型(參考右邊鏈接):http://ifeve.com/channels/
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel 從文件中讀寫數據。
DatagramChannel 能通過UDP讀寫網絡中的數據。
SocketChannel 能通過TCP讀寫網絡中的數據。
ServerSocketChannel可以監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。
以ServerSocketChannel爲例:首先先開啓ServerSocketChannel,調用open()方法,並且可以非阻塞的監聽新進連接,如果不設置爲非阻塞也會和BIO 一樣會放棄cpu的執行權,直到有新連接被監聽到纔會往下執行;該對象可產生一個用於監聽的ServerSocket對像,ServerSocket可以綁定監聽指定的端口;注意:FileChannel不能切換到非阻塞模式
// 開啓通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 獲取服務端監聽套接字
ServerSocket serverSocket = serverChannel.socket();
// 綁定監聽的端口 也可以直接用ServerSocketChannel 來監聽
// serverChannel.bind(new InetSocketAddress(8080));
serverSocket.bind(new InetSocketAddress(8080));
// 設置通道爲非阻塞 accept()方法會立即返回,如果沒有監聽到新連接返回null,監聽到
//新連接則返回一個與新連接交互的SocketChannel通道
serverChannel.configureBlocking(false);
通道和緩衝區的數據交互
//將 Buffer 中數據寫入 Channel
channel.write(buff)
//從 Channel 讀取數據到 Buffer
channel.read(buff)
channel 註冊到選擇器上,register()如果已經註冊過的,更新對象和更新channel對什麼事件感興趣,註冊之後返回一個selectorKey,用於標識channel 和channel感興趣的事件,如果是有客戶端連接這個channel,並且事件是這個channel感興趣的 ,那麼操作系統會將這個連接分配給這個channel。關於底層應用的的共享內存啊、內核輪詢之類的可以自己去看馬士兵的NIO裏面介紹的挺多的。
2、buffer 緩衝區:
緩衝區本質上是一個內存塊,您可以在其中寫入數據,然後可以在以後再次讀取。該內存塊包裝在NIO Buffer對象中,數據從通道讀取到緩衝區,然後從緩衝區寫入通道,該對象提供了一組方法,可以更輕鬆地使用該內存塊。Java NIO 有以下Buffer類型:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
要獲取Buffer對象,您必須首先分配它。每個Buffer類都有一個allocate()執行此操作的方法
ByteBuffer buf = ByteBuffer.allocate(1024);
2.1、使用Buffer來讀取和寫入數據通常遵循以下四個步驟:
- 將數據寫入緩衝區
- 呼叫 buffer.flip()
- 從緩衝區讀取數據
- 調用buffer.clear()或buffer.compact()清除數據
2.1.1 將數據寫入緩衝區
數據寫入緩衝區有兩種方式:
int bytes = channel.read(buf); // 從通道讀取數據寫入緩衝池
buf.put("hello world".getByte()); // 在程序中向緩衝池添加數據
2.1.2 呼叫 buffer.flip()
buffer中的flip方法涉及到bufer中的Capacity,Position和Limit三個概念
- 容量 (capacity)
- 位置 (position)
- 限制 (limit)
容量是在調用allocate(1024)時定義的容量,有興趣可以看看源碼:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
// 傳入的都是容量所以說開始的時候cap = lim
HeapByteBuffer(int cap, int lim) {
super(-1, 0, lim, cap, new byte[cap], 0);
}
// 調用上級
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
// 容量賦值position的開始位置賦值爲0,標記設爲-1,mark是起輔助判斷作用。
Buffer(int mark, int pos, int lim, int cap) { // package-private
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
// 最後會到這裏來
public final Buffer limit(int newLimit) {
if ((newLimit > capacity) || (newLimit < 0))
throw new IllegalArgumentException();
limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;
return this;
}
所以初始化之後的值:limit = cap = 1024(我傳入的值) 而mark = -1 , position = 0 ;
讀取過程(下面鏈接講的挺清楚的):
https://blog.csdn.net/u013096088/article/details/78638245
讀取結束(消息長15):position 位於15;
調用flip()方法之後:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
limit = 15 ; positon 重置;也就是說調用flip之後,讀寫指針指到緩存頭部,並且設置了最多隻能讀出之前寫入的數據長度(而不是整個緩存的容量大小)
簡答點描述就是:你將文件按一定的順序疊起來,然後領導突然說把已經疊好的按順序交給他,你只能把現在到了第幾個文件記住,然後把已經整理的交給領導。大概就這個意思吧,可能例子不夠恰當
2.1.3 從緩衝區讀取數據
int bytes = channel.write(buf); // 從緩衝池中向通道寫入數據
byte b = buffer.get(0); // 從緩衝池中獲取數據
2.1.4 調用buffer.clear()或buffer.compact()清除數據
讀取所有數據後,需要清除緩衝區,以使其可以再次寫入。您可以通過兩種方式執行此操作:通過調用clear()或通過 compact()。該clear()方法清除整個緩衝區。該compact() 方法僅清除您已讀取的數據。任何未讀的數據都將移至緩衝區的開頭,並且現在將在未讀的數據之後將數據寫入緩衝區。
3、selector 選擇器
一個selector對象可以通過調用Selector.open()來創建,這個工廠方法會使用系統默認的selector provider來創建一個新的selector對象。或者我們還可以通過實現抽象類SelectorProvider自定義一個selector provider,然後調用它的openSelector()來創建,
例如:new SelectorProviderImpl().openSelector()
除非調用selector.close(),否則該selector將會一直保持打開狀態。
通過channel的register方法,將channel註冊到給定的selector中,並返回一個表示註冊關係的SelectionKey 對象。
SelectionKey key = channel.register(selector,
SelectionKey.OP_ACCEPT|SelectionKey.OP_READ|SelectionKey.OP_WRITE|SelectionKey.OP_CONNECT);
// 爲了確保selector捕捉到信息(也就是有客戶端的行爲才響應)需要調用select方法
// 該方法屬於一個阻塞方法,即沒有內容不會往下執行,表示監聽的端口沒有人連接也沒有人讀寫
selector.select();
第一個表示選擇器,參數二表示註冊到選擇器之後這個channel對什麼事件感興趣,SelectionKey抽象類裏定義了4中,分別:是:
- OP_ACCEPT: 接收連接進行事件,表示服務器監聽到了客戶連接
- OP_READ: 讀就緒事件,表示通道中已經有了可讀的數據
- OP_WRITE: 寫就緒事件,表示已經可以向通道寫數據了
- OP_CONNECT:表示客戶與服務器的連接已經建立成功
你可以獲取selector中的所有selector中的selectionKeys的集合:
selectionKeys = selector.selectedKeys();
// 獲取遍歷selectionKeys的迭代器,你通過這個key值的遍歷找出這個選擇器中你感興趣的事件進行業務操作。
iterator = selectionKeys.iterator();
// 這個key你捕捉到了並且你處理了,那麼就應該將這個key移除這個集合,避免重複處理相同事件
iterator.remove(selectionKey);
太深的內容我自己理解也不到位,希望將自己能理順的知識做個總結,方便有需要的人蔘考;下面是我開發中用到的NIO例子;
完整的NIO 的Demo :
public class ServerCrawImpl implements IServerCraw {
public ServerCrawImpl() {
}
@Override
public void startup() {
ServerSocketChannel serverChannel;
Selector selector;
try {
// 開啓管道
serverChannel = ServerSocketChannel.open();
// 獲取服務端監聽socket
ServerSocket serverSocket = serverChannel.socket();
//綁定端口
serverSocket.bind(new InetSocketAddress(8603));
// 設置socket非阻塞
serverChannel.configureBlocking(false);
//創建Selector複用器,選擇器
selector = Selector.open();
//將多個Channel類型事件註冊到多路複用器 實現Selector管理Channel,其中的事件可以多個
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
log.debug(ExceptionUtil.getStackMsg(e));
return;
}
while (true) {
try {
//如果selector管理的管道沒有客戶端請求,客戶端的讀,客戶端的寫的交互,則阻塞,返回0
int eventCount = selector.select();
if (eventCount == 0) {
continue;
}
} catch (Exception e) {
log.debug(ExceptionUtil.getStackMsg(e));
break;
}
// 獲取所有管道相關信息的keys集合
Set<SelectionKey> keys = selector.selectedKeys();
// 獲取遍歷keys集合的迭代器
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
// 獲取當前的channel對應 key
SelectionKey key = iterator.next();
// 移除,防止重複處理
iterator.remove();
//處理業務
try {
// key存在並且得到的是連接請求
if (key.isValid() && key.isAcceptable()) {
// 獲取與客戶端交互的channel
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//接受客戶端的請求
SocketChannel client = server.accept();
//客戶端 服務端 全都設置爲 非阻塞
client.configureBlocking(false);
client.register(key.selector(), SelectionKey.OP_READ | SelectionKey.OP_WRITE, ByteBuffer.allocate(2048));
}
if (key.isValid() && key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(3);
int res = 0;
try {
// 從緩存中讀取數據到通道中返回給客戶端,需要判斷一下客戶端收到的消息長度和自己發送的長度是否一致
res = client.read(buffer);
} catch (Exception e) {
client.close();
log.debug(ExceptionUtil.getStackMsg(e));
break;
}
//如果read()方法返回-1,說明客戶端關閉了連接,那麼客戶端已經接收到了與自己發送字節數相等的數據,可以安全地關閉
if (res == -1) {
client.close();
} else if (res == 0) {
continue;
} else if (res > 0) {
continue;
}
}
if (key.isValid() && key.isWritable()) {
try {
// 從緩衝池中獲取數據
// 業務操作
} catch (Exception e) {
log.debug("異常:" + ExceptionUtil.getStackMsg(e));
}
}
}
} catch (Exception e) {
log.debug(ExceptionUtil.getStackMsg(e));
}
}
}
}
}
NIO 的弊端:客戶端連接就緒之後都會有文件描述符的read阻塞監聽,NIO 會重複調用read(),會曾加內核的壓力;
我也僅限會用,太深的東西也沒辦法理解透徹,有不對的歡迎指正。