之前的文章主要講解了Reactor模式、多路複用器select、以及Netty框架,這篇文章主要是java Nio的代碼,但我們知道java的Nio是使用了Reactor模式和多路複用器來實現的,所以下面看看java nio的demo。
一、java Nio 客戶端
package com.test2;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class Client {
//多路複用器、選擇器(具體看使用的操作系統以及jdk版本,1.5有可能就是select而1.7就是epoll)
private Selector selector;
public Client init(String serverIp, int port) throws IOException {
// 獲取socket通道、
// 在reactor模式中的資源
// select、epoll函數中的fd(個人理解如有錯誤請求指正)
SocketChannel channel = SocketChannel.open();
// 將該通道設置爲非阻塞
channel.configureBlocking(false);
// 獲取多路複用器實例
selector = Selector.open();
// 客戶端連接服務器,需要調用channel.finishConnect();才能實際完成連接。
channel.connect(new InetSocketAddress(serverIp, port));
// 爲該通道註冊SelectionKey.OP_CONNECT事件,也就是將channel的fd和感興趣的事件添加到多路複用器中
channel.register(selector, SelectionKey.OP_CONNECT);
return this;
}
public void listen() throws IOException {
System.out.println("客戶端啓動");
// 輪詢訪問selector
while (true) {
// 選擇註冊過的io操作的事件(第一次爲SelectionKey.OP_CONNECT)
// 看過之前文章的話,就明白這是獲取註冊到該複用器中的通道的相關事件,獲取的過程也就是select()方法遍歷獲取事件不同的系統、jdk也不同,如果是epoll則是從一個fd就緒隊列獲取
// 而select和poll則是遍歷存放channel和事件的集合所以效率低下一些,還有其他的效率問題可以看看之前epoll和select的講解
selector.select();
//獲取註冊在該複用器上的channel和channelEvent
Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = ite.next();
// 刪除已選的key,防止重複處理
ite.remove();
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
// 如果正在連接,則完成連接
if (channel.isConnectionPending()) {
channel.finishConnect();
}
// channel.configureBlocking(false);
// 向服務器發送消息
channel.write(ByteBuffer.wrap(new String("send message to server.").getBytes()));
// 連接成功後,註冊接收服務器消息的事件
channel.register(selector, SelectionKey.OP_READ);
System.out.println("客戶端連接成功");
} else if (key.isReadable()) { // 判斷該channel的channelEvent事件類型,也就是reactor模式中的分發器,如果把裏面處理過程進行封裝就是處理器了
// 因爲selectionKey中存放的是channel和channelEvent所以可以通過selectionKey就能獲取channel
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
byte[] data = buffer.array();
String message = new String(data);
System.out.println("recevie message from server:, size:"
+ buffer.position() + " msg: " + message);
ByteBuffer outbuffer = ByteBuffer.wrap(("client.".concat(message)).getBytes());
channel.write(outbuffer);
}
}
}
}
public static void main(String[] args) throws IOException {
new Client().init("127.0.0.1", 9981).listen();
}
}
二、java Nio服務端
package com.test2;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* 客戶端還是服務端資源、複用器、分發器、處理器整個流程使用都是一樣的……
* @author sykj
*
*/
public class Server {
//多路複用器
private Selector selector;
public Server init(int port) throws IOException {
// 獲取一個ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
// 獲取多路複用器對象
selector = Selector.open();
// 將通道管理器與通道綁定,併爲該通道註冊SelectionKey.OP_ACCEPT事件,
// 只有當該事件到達時,Selector.select()會返回,否則一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
return this;
}
public void listen() throws IOException {
System.out.println("服務器端啓動成功");
// 使用輪詢訪問selector
while (true) {
selector.select();
Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = ite.next();
ite.remove();
// 客戶端請求連接事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 獲得客戶端連接通道
SocketChannel channel = server.accept();
channel.configureBlocking(false);
// 向客戶端發消息
channel.write(ByteBuffer.wrap(new String("send message to client").getBytes()));
// 在與客戶端連接成功後,爲客戶端通道註冊SelectionKey.OP_READ事件。
channel.register(selector, SelectionKey.OP_READ);
System.out.println("客戶端請求連接事件");
} else if (key.isReadable()) {// 有可讀數據事件
// 獲取客戶端傳輸數據可讀取消息通道。
SocketChannel channel = (SocketChannel) key.channel();
// 創建讀取數據緩衝器
ByteBuffer buffer = ByteBuffer.allocate(10);
int read = channel.read(buffer);
byte[] data = buffer.array();
String message = new String(data);
ByteBuffer outbuffer = ByteBuffer.wrap(message.getBytes());
channel.write(outbuffer);
}
}
}
}
public static void main(String[] args) throws IOException {
new Server().init(9981).listen();
}
}
OK 有代碼有註釋應該能看明白,個人感覺javaNio和Reactor模式對應起來理解Reactor 模式還是比較好,Reactor模式是資源、選擇器、分發器和處理器這幾個模塊組成.
在java Nio中個人理解的對應關係:
這就是個人理解,如果感覺不會好過於簡單的可以查看前面文章對reactor有詳細分析……
java Bio對比java Nio
Bio和Nio底層的實現模型不同,所以區別也就是從這裏開始,Bio即阻塞同步IO而Nio即非阻塞同步IO他們最大的區別就是一個是阻塞一個非阻塞的,阻塞非阻塞就是對請求響應的一種形態。在效率問題上的Nio相比Bio最大的區別就是Reactor模式的使用Reactor模式中的多路複用在多併發訪問以及高併發訪問的情況下有更少的資源消耗,因爲就複用器一個線程能管理多個或所有客戶端的連接關係,相比一個客戶端請求一個線程資源的消耗很小。
Bio的阻塞是在accept阻塞的,而Nio的阻塞是在事件上阻塞的,不是說Nio是非阻塞的嗎(那是對客戶端請求來說是非阻塞的),而服務端對獲取客戶端請求的等待是阻塞的。
這麼說吧,在服務端獲得客戶端請求不管是Bio還是Nio都是阻塞的,但他們阻塞的時間地點不同,之後說的所有情況都是在客戶端和服務端之間已經成功建立tcp鏈接。
Bio 每當有請求來的時候都是通過服務端的accept()方法來獲取客戶端的請求,服務端獲取請求後對這個請求進行後續的處理,在將處理結果返回給客戶端
而在Nio中(epoll)使用了多路複用器,所以當某個客戶端請求來的時候,會把該客戶端連接和客戶端請求事件保存到 FD事件隊列,在程序中輪詢select時是對有事件的channel和事件進行了輪詢,他的阻塞是在select()時阻塞的,而客戶端不用同步等待服務端消息,服務端獲得select()中的事件後能立馬先給客戶端返回一個成功消息,將處理後的信息之後再傳過去
Bio 和 Nio服務端阻塞的事件和時間點不同、Bio 和 Nio對客戶端的響應也是不同以及對客戶端請求事件的輪詢查詢不同。
總的來說Nio相比Bio消耗資源少,獲取客戶端請求事件循環的隊列小,也就是使用複用器帶來的好處,其他的區別……