傳統的io模型問題:
在傳統的IO模型中,每個連接創建成功之後都需要一個線程來維護,每個線程包含一個while死循環,那麼1w個連接對應1w個線程,繼而1w個while死循環,這就帶來如下幾個問題:
- 線程資源受限:線程是操作系統中非常寶貴的資源,同一時刻有大量的線程處於阻塞狀態是非常嚴重的資源浪費,操作系統耗不起
- 線程切換效率低下:單機cpu核數固定,線程爆炸之後操作系統頻繁進行線程切換,應用性能急劇下降。
- 除了以上兩個問題,IO編程中,我們看到數據讀寫是以字節流爲單位,效率不高。
NIO編程模型
NIO編程模型中,新來一個連接不再創建一個新的線程,而是可以把這條連接直接綁定到某個固定的線程,然後這條連接所有的讀寫都由這個線程來負責,那麼他是怎麼做到的?我們用一幅圖來對比一下IO與NIO
如上圖所示,IO模型中,一個連接來了,會創建一個線程,對應一個while死循環,死循環的目的就是不斷監測這條連接上是否有數據可以讀,大多數情況下,1w個連接裏面同一時刻只有少量的連接有數據可讀,因此,很多個while死循環都白白浪費掉了,因爲讀不出啥數據。
而在NIO模型中,他把這麼多while死循環變成一個死循環,這個死循環由一個線程控制,那麼他又是如何做到一個線程,一個while死循環就能監測1w個連接是否有數據可讀的呢?
這就是NIO模型中selector的作用,一條連接來了之後,現在不創建一個while死循環去監聽是否有數據可讀了,而是直接把這條連接註冊到selector上,然後,通過檢查這個selector,就可以批量監測出有數據可讀的連接,進而讀取數據,下面我再舉個非常簡單的生活中的例子說明IO與NIO的區別。
在一家幼兒園裏,小朋友有上廁所的需求,小朋友都太小以至於你要問他要不要上廁所,他纔會告訴你。幼兒園一共有100個小朋友,有兩種方案可以解決小朋友上廁所的問題:
- 每個小朋友配一個老師。每個老師隔段時間詢問小朋友是否要上廁所,如果要上,就領他去廁所,100個小朋友就需要100個老師來詢問,並且每個小朋友上廁所的時候都需要一個老師領着他去上,這就是IO模型,一個連接對應一個線程。
- 所有的小朋友都配同一個老師。這個老師隔段時間詢問所有的小朋友是否有人要上廁所,然後每一時刻把所有要上廁所的小朋友批量領到廁所,這就是NIO模型,所有小朋友都註冊到同一個老師,對應的就是所有的連接都註冊到一個線程,然後批量輪詢。
這就是NIO模型解決線程資源受限的方案,實際開發過程中,我們會開多個線程,每個線程都管理着一批連接,相對於IO模型中一個線程管理一條連接,消耗的線程資源大幅減少
由於NIO模型中線程數量大大降低,線程切換效率因此也大幅度提高
NIO解決這個問題的方式是數據讀寫不再以字節爲單位,而是以字節塊爲單位。IO模型中,每次都是從操作系統底層一個字節一個字節地讀取數據,而NIO維護一個緩衝區,每次可以從這個緩衝區裏面讀取一塊的數據,
這就好比一盤美味的豆子放在你面前,你用筷子一個個夾(每次一個),肯定不如要勺子挖着吃(每次一批)效率來得高。
一、Buffer demo
public class BufferTest {
public static void main(String[] args) {
//靜態方法常見 buffer
IntBuffer buf = IntBuffer.allocate(10);
int[] array = new int[]{3, 5, 1};
//put一個數組到buffer中,使用put方式將
// buf.put(array);
//使用wrap方式會直接更改原數組
buf = buf.wrap(array);
//IntBuffer.wrap(array, 0, 2);
buf.put(0, 7);
int length = buf.limit();
for (int i = 0; i < length; i++) {
System.out.print(buf.get(i));
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
}
System.out.println(buf);
/**
* limit = position;
* position = 0;
*/
buf.flip();
/**
* position = 0;
* limit = capacity;
*/
buf.clear();
System.out.println(buf);
//創建一個新的字節緩衝區,共享此緩衝區的內容
IntBuffer newBuffer = buf.duplicate();
System.out.println(newBuffer);
}
}
二、FileChannel demo
public class FileChannelTest {
public static void testFileChannel() throws IOException {
RandomAccessFile aFile = new RandomAccessFile("D:/nio-data.txt", "rw");
FileChannel channel = aFile.getChannel();
//分配一個新的緩衝區
ByteBuffer allocate = ByteBuffer.allocate(48);
int bytesRead = channel.read(allocate);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
allocate.flip();
while (allocate.hasRemaining()) {
System.out.print((char) allocate.get());
}
allocate.clear();
bytesRead = channel.read(allocate);
}
aFile.close();
}
public static void fileChannelDemo() throws IOException {
//定義一個byteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
FileChannel inputChannel = new FileInputStream("D:/nio-data.txt").getChannel();
FileChannel outputChannel = new FileOutputStream("D:/nio-data.txt", true).getChannel();
//讀取數據
byteBuffer.clear();
int len = inputChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(), "UTF-8"));
System.out.println(new String(byteBuffer.array(), 0, len, "UTF-8"));
ByteBuffer byteBuffer2 = ByteBuffer.wrap("奧會計師八度空間".getBytes());
outputChannel.write(byteBuffer2);
outputChannel.close();
inputChannel.close();
}
public static void main(String[] args) {
try {
FileChannelTest.fileChannelDemo();
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、不使用 選擇器 selector 的 ServerSocketChannel 和 SocketChannel 的demo
服務端:
public class NioChannelServer {
private ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//獲取一個intBuffer視圖,操作視圖的同時原緩衝區也會改變
private IntBuffer intBuffer = byteBuffer.asIntBuffer();
private SocketChannel socketChannel = null;
private ServerSocketChannel serverSocketChannel = null;
/**
* 打開服務端的通道
*
* @throws Exception
*/
public void openChannel() throws Exception {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8888));
System.out.println("服務端通道已經打開");
}
/**
* 等待新的連接
*
* @throws Exception
*/
public void waitReqConn() throws Exception {
while (true) {
socketChannel = serverSocketChannel.accept();
if (null != socketChannel) {
System.out.println("新的連接加入!");
}
//處理請求
processReq();
socketChannel.close();
}
}
private void processReq() throws IOException {
System.out.println("開始讀取和處理客戶端數據。。");
byteBuffer.clear();
socketChannel.read(byteBuffer);
int result = intBuffer.get(0) + intBuffer.get(1);
byteBuffer.flip();
byteBuffer.clear();
//修改視圖,byteBuffer也會變化
intBuffer.put(0, result);
socketChannel.write(byteBuffer);
System.out.println("讀取處理完成");
}
public void start() {
try {
//打開通道
openChannel();
//等待客戶端連接
waitReqConn();
socketChannel.close();
System.out.println("服務端處理完畢");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args){
new NioChannelServer().start();
}
}
客戶端:
public class NioChannelClient {
private SocketChannel socketChannel = null;
private ByteBuffer buff = ByteBuffer.allocate(8);
private IntBuffer intBuffer = buff.asIntBuffer();
public SocketChannel connect() throws IOException {
return SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
}
public int getSum(int a, int b) {
int result = 0;
try {
socketChannel = connect();
sendRequest(a, b);
result = receiveResult();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
private int receiveResult() throws IOException {
buff.clear();
socketChannel.read(buff);
return intBuffer.get(0);
}
private void sendRequest(int a, int b) throws IOException {
buff.clear();
intBuffer.put(0,a);
intBuffer.put(1,b);
socketChannel.write(buff);
System.out.println("客戶端發送請求 ("+a+"+"+b+")");
}
public static void main(String[] args){
Random random = new Random();
for (int i = 0; i <10 ; i++) {
int result = new NioChannelClient().getSum(random.nextInt(100),random.nextInt(100));
System.out.println(result);
}
}
}
四、使用 selector 方式 實現 ServerSocketChannel 和 SocketChannel
選擇器(Selector) 是 SelectableChannle 對象的多路複用器,Selector 可以同時監控多個 SelectableChannel 的 IO 狀況,也就是說,利用 Selector可使一個單獨的線程管理多個 Channel,selector 是非阻塞 IO 的核心。
選擇器(Selector)的應用:
當通道使用register(Selector sel, int ops)方法將通道註冊選擇器時,選擇器對通道事件進行監聽,通過第二個參數指定監聽的事件類型。
其中可監聽的事件類型包括以下:
讀 : SelectionKey.OP_READ (1)
寫 : SelectionKey.OP_WRITE (4)
連接 : SelectionKey.OP_CONNECT (8)
接收 : SelectionKey.OP_ACCEPT (16)
如果需要監聽多個事件是:
int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ; //表示同時監聽讀寫操作
服務端:
public class SelectorServer {
private Selector selector = null;
private ServerSocketChannel serverSocketChannel = null;
private int keys = 0;
public void initServer() throws IOException {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8888));
serverSocketChannel.configureBlocking(false);
//服務端通道註冊accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
private void listen() throws IOException {
System.out.println("服務端已經啓動");
while (true) {
//讓通道選擇器至少選擇一個通道
keys = this.selector.select();
System.out.println(keys);
Iterator<SelectionKey> itor = this.selector.selectedKeys().iterator();
if (keys > 0) {
//進行輪詢
while (itor.hasNext()) {
try{
SelectionKey key = itor.next();
if (key.isAcceptable()) {
//serverSocketChannel = (ServerSocketChannel) key.channel();
//獲取和客戶端連接的服務端渠道
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.write(ByteBuffer.wrap("hello".getBytes()));
//還需要讀取客戶端發過來的數據,所以需要註冊一個讀取數據的事件
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
read(key);
}
}finally {
//處理完一個key,就刪除,防止重複處理
itor.remove();
}
}
} else {
System.out.println("select finished without any keys");
}
}
}
private void read(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(byteBuffer);
String msg = new String(byteBuffer.array(), 0, len);
System.out.println("服務端接收到的消息是" + msg);
}
public void start() {
try {
initServer();
listen();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new SelectorServer().start();
}
}
客戶端:
public class SelectorClient {
private Selector selector;
private ByteBuffer outBuffer = ByteBuffer.allocate(1024);
private ByteBuffer inputBuffer = ByteBuffer.allocate(1024);
private int keys = 0;
private SocketChannel socketChannel = null;
public void initClient() throws IOException {
selector = Selector.open();
socketChannel = SocketChannel.open();
//客戶端通道配置爲非阻塞
socketChannel.configureBlocking(false);
//連接服務端
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
//註冊客戶端連接服務器的事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
private void listen() throws IOException {
while (true) {
keys = this.selector.select();
System.out.println(keys);
if (keys > 0) {
Iterator<SelectionKey> iter = this.selector.selectedKeys().iterator();
while (iter.hasNext()) {
try{
SelectionKey key = iter.next();
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
if (channel.isConnectionPending()) {
channel.finishConnect();
System.out.println("完成連接");
}
//連接完成之後,肯定還要做其它的事情,比如寫
channel.register(selector, SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
SocketChannel channel = (SocketChannel) key.channel();
outBuffer.clear();
System.out.println("客戶端正在寫數據。。");
//從控制檯寫消息
Scanner scanner = new Scanner(System.in);
while (true) {
String msg = scanner.next();
channel.write(ByteBuffer.wrap(msg.getBytes()));
if("end".equals(msg)) {
break;
}
}
channel.register(selector, SelectionKey.OP_READ);
System.out.println("客戶端寫數據完成。。。");
} else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
inputBuffer.clear();
int len = socketChannel.read(inputBuffer);
System.out.println("讀取服務端發送的消息:" + new String(inputBuffer.array()));
}
}finally{
iter.remove();
}
}
} else {
System.out.println("select finished without any keys");
}
}
}
public void start() {
try {
initClient();
listen();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args){
new SelectorClient().start();
}
}
nio的非阻塞是對於網絡通道來說的,需要使用Channel.configureBlocking(false)來設置通道爲非阻塞的,如果沒設置,默認是阻塞的。