前言
在上篇文章中對BIO網路程式設計的相關內容進行了講解,通過我們一步一步的優化,雖然我們通過多執行緒解決了併發訪問的問題,但是BIO本身的一些特性造成的問題卻沒有得到解決。
BIO是阻塞IO,我們使用執行緒來進行IO的排程,我們無法確定io是否就緒,但是每個IO操作都會建立執行緒,這個時候如果IO未就緒,那麼建立的執行緒也會處於阻塞狀態。
在之前講解NIO基本知識的時候我們提到過NIO通過通道選擇器可以實現同時對多個通道的管理,實際上就是通過管理多個IO操作,換句話說是單執行緒處理多執行緒併發,有效的防止執行緒因為IO沒有就緒而被掛起。
在使用NIO進行網路程式設計的時候需要用到的就是通道選擇器,所以我們先看一下通道選擇器的相關內容。
NIO
ServerSocketChannel
Java NIO中的 ServerSocketChannel 是一個可以監聽新進來的TCP連線的通道, 就像標準IO中的ServerSocket一樣。
//代開ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
複製程式碼
通過 ServerSocketChannel.accept() 方法監聽新進來的連線。當 accept()方法返回的時候,它返回一個包含新進來的連線的 SocketChannel。 可以設定成非阻塞模式。在非阻塞模式下,accept() 方法會立刻返回,如果還沒有新進來的連線,返回的將是null。
SocketChannel
Java NIO中的SocketChannel是一個連線到TCP網路套接字的通道。可以通過以下2種方式建立SocketChannel:
//開啟一個SocketChannel並連線到網際網路上的某臺伺服器。
socketChannel.connect(new InetSocketAddress("localhost",8888));
//一個新連線到達ServerSocketChannel時,會建立一個SocketChannel。
SocketChannel socketChannel = SocketChannel.open();
複製程式碼
通道選擇器 Selector
為什麼使用通道選擇器?
在前言中提到過,NIO通過通道選擇器可以實現同時對多個通道的管理,其實就是同時對多個IO操作的管理,也就是實現了單執行緒處理多執行緒併發問題。對於作業系統來說,執行緒之間上下文切換的開銷很大,而且每個執行緒都要佔用系統的一些資源(如記憶體)。因此太多的執行緒會耗費大量的資源,所以使用通道選擇器來對多個通道進行管理。
Selector的使用
1. 建立Selector
//通過呼叫Selector.open()方法建立一個Selector,如下:
Selector selector = Selector.open();
複製程式碼
2.將通道註冊到通道選擇器中
//為了將Channel和Selector配合使用,必須將channel註冊到selector上。通過SelectableChannel.register()方法來實現
// 建立ServerSocketChanner
ServerSocketChannel ssc = ServerSocketChannel.open();
// 繫結埠號
ssc.bind(new InetSocketAddress(8888));
// 設定通道非阻塞
ssc.configureBlocking(false);
// 建立通道選擇器
Selector selector = Selector.open();
// 將通道註冊到通道選擇器中 要求:通道都必須是非阻塞的 意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式
// 第二個引數是我們需要通道選擇器幫我們管理什麼事件型別 註冊為接受就緒
ssc.register(selector, SelectionKey.OP_ACCEPT);
複製程式碼
register()方法的第二個引數代表註冊的通道事件型別,一個通道觸發一個事件就意味著該事件準備就緒了,總共有四種事件:Connect(連線就緒 ) Accept(接受就緒) Read(有資料可讀的通道 讀就緒) Write(寫就緒)。對於選擇器而言,可以針對性的找到(監聽)的事件就緒的通道,進行相關的操作。
這四種事件用SelectionKey的四個常量來表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
可以用“位或”操作符將常量連線起來
SelectionKey物件
物件中包含很多的屬性,譬如:
- interest集合(事件集合)
- ready集合
- Channel
- Selector
- 附加的物件(可選)
3.選擇器的select ()方法
select()方法返回的int值表示有多少通道已經就緒。自上次呼叫select()方法後有多少通道變成就緒狀態。如果呼叫select()方法,因為有一個通道變成就緒狀態,返回了1,若再次呼叫select()方法,如果另一個通道就緒了,它會再次返回1。如果對第一個就緒的channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法呼叫之間,只會有一個通道就緒。
4.選擇器的selectedKeys()方法
通過呼叫selector的selectedKeys()方法,可以得到就緒通道的集合。遍歷集合可以找到自己需要的通道進行相關的操作。
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//處理nio事件
if(key.isAcceptable()){
// 獲取通道
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = channel.accept();
// 註冊讀事件型別
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
if(key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
// 讀取資料
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = -1;
while (true){
byteBuffer.clear();
len = socketChannel.read(byteBuffer);
if(len == -1){
break;
}
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
bos.write(byteBuffer.get());
}
}
// 列印讀取到的資料
System.out.println(bos.toString());
//寫資料給客戶端 註冊事件型別 寫事件
socketChannel.register(selector,SelectionKey.OP_WRITE);
}
if(key.isWritable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
String msg = "你好,我是伺服器";
ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
byteBuffer.put(msg.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
socketChannel.close();
}
iterator.remove();
}
複製程式碼
NIO網路程式設計例項
伺服器程式設計
基本步驟
- 開啟ServerSocketChanner
- 繫結埠號
- 設定通道非阻塞
- 開啟選擇器,把通道註冊到選擇器中
- 使用Selector輪詢所有的key
- 獲取socketChannel
- 設定非阻塞 註冊讀事件
- 讀取操作
- 註冊寫事件
- 寫操作
程式碼
import java.io.ByteArrayOutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws Exception{
// 建立ServerSocketChanner
ServerSocketChannel ssc = ServerSocketChannel.open();
// 繫結埠號
ssc.bind(new InetSocketAddress(8888));
// 設定通道非阻塞
ssc.configureBlocking(false);
// 建立通道選擇器
Selector selector = Selector.open();
// 將通道註冊到通道選擇器中 要求:通道都必須是非阻塞的
// 第二個引數是我們需要通道選擇器幫我們管理什麼事件型別 註冊為接受就緒
ssc.register(selector, SelectionKey.OP_ACCEPT);
// 遍歷通道選擇器
while (true){
System.out.println("我在8888等你......");
// 返回準備就緒的通道數量
int nums = selector.select();
// 如果數量小於1說明沒有通道準備就緒 跳過本次迴圈
if(nums<1) {continue;}
// 獲取所有的keys(通道 事件型別)
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//處理nio事件
if(key.isAcceptable()){
// 獲取通道
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = channel.accept();
// 註冊讀事件型別
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
if(key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
// 讀取資料
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = -1;
while (true){
byteBuffer.clear();
len = socketChannel.read(byteBuffer);
if(len == -1){
break;
}
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
bos.write(byteBuffer.get());
}
}
// 列印讀取到的資料
System.out.println(bos.toString());
//寫資料給客戶端 註冊事件型別 寫事件
socketChannel.register(selector,SelectionKey.OP_WRITE);
}
if(key.isWritable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
String msg = "你好,我是伺服器";
ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
byteBuffer.put(msg.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
socketChannel.close();
}
iterator.remove();
}
}
}
}
複製程式碼
客戶端程式設計
基本步驟
- 開啟SocketChannel
- 連線伺服器
- 寫資料給伺服器
- 讀取資料
- 關閉SocketChannel
程式碼
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOCleint {
public static void main(String[] args) throws IOException {
// 建立sc
SocketChannel socketChannel = SocketChannel.open();
// 連線伺服器
socketChannel.connect(new InetSocketAddress("localhost",8888));
// 寫資料給伺服器
String msg = "你好,我是客戶端";
ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
byteBuffer.put(msg.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
// 關閉輸出流
socketChannel.shutdownOutput();
// 讀取資料
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream bosread = new ByteArrayOutputStream();
int len = -1;
while (true){
readBuffer.clear();
len = socketChannel.read(readBuffer);
if(len == -1){
break;
}
readBuffer.flip();
while (readBuffer.hasRemaining()){
bosread.write(readBuffer.get());
}
}
System.out.println("我收到:"+bosread.toString());
socketChannel.close();
}
}
複製程式碼
我不能保證每一個地方都是對的,但是可以保證每一句話,每一行程式碼都是經過推敲和斟酌的。希望每一篇文章背後都是自己追求純粹技術人生的態度。
永遠相信美好的事情即將發生。