快速掌握NIO和BIO的區別
阿新 • • 發佈:2019-12-31
NIO和BIO對比
NIO(non blocking I/O)非阻塞I/O,jdk1.4引入的新I/O,平時接觸的檔案的I/O操作是BIO,即阻塞I/OBIO API使用
具體流程:
A.測試accept()方法的阻塞public void testAccept() throws IOException{
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(9999));
Socket sk = ss.accept();
System.out.println("有連線連入");
}複製程式碼
JUnit測試,“有連線接入”沒有輸出,說明accept()方法產生阻塞了。
B.然後新增connect()方法測試的程式碼:
public void test Contect() throws Exception{
Socket sk = new Socket();
sk.connect(new InetSocketAddress(
"127.0.0.1",9999));
System.out.println("連線成功");
}複製程式碼
先執行伺服器端方法(testAccept()),再執行客戶端方法,發現accept()方法阻塞釋放了。另外“連線成功”正確輸出。如果不先啟動伺服器端方法,而直接執行客戶端方法,發現先是阻塞了一下,然後JUnit測試丟擲異常。
總結:connect()方法會產生阻塞,指定連線成功,阻塞才釋放。
accept()方法產生的阻塞,直到伺服器獲得到連線後,阻塞才釋放。
C.測試read()方法的阻塞性
C1. 再次修改testAccept()方法
InputStream in = sk.getInputStream();
byte bts[] = new byte[1024];
in.read(bts);
System.out.println("讀取到了資料:"+new String(bts));複製程式碼
C2.為了不讓連線中斷,需要修改testConnect()
while(true);複製程式碼
總結:read()方法會產生阻塞,直到讀取到內容後,阻塞才被釋放。 D.測試write()方法的阻塞性 D1.修改testAccept()方法
for(int i =1;i<100000;i++){
out.write("HelloWorld".getBytes());
System.out.println(i);
}
System.out.println("資料寫完了。。。" );
}複製程式碼
先執行伺服器端方法,再執行客戶端方法;發現i輸出值為65513,阻塞了。
for(int i =1;i<200000;i++){
out.write("Hello".getBytes());
System.out.println(i);
}複製程式碼
微調程式碼,輸出到131026阻塞了。
總結:write()方法也會產生阻塞,write()一直往出寫資料,但是沒有任何一方讀取資料,直到寫出到一定量(我的是655130B,不同電腦可能不同)的時候,產生阻塞。向網路卡裝置緩衝區中寫資料。
NIO 相關API
Channel檢視API ServerSocketChannel,SocketChannel基於NIO的(基於tcp實現的,安全的基於握手機制) DatagramChannel基於UDP協議,不安全NIO-Channel API(上)
accept和connect使用
/**ServerSocketChannel.open()建立伺服器端物件
* nio提供兩種模式:阻塞模式和非阻塞模式
* 預設情況下是阻塞模式。
* 通過ssc.configureBlocking(false)設定為非阻塞模式
* @throws Exception
*/
@Test
public void testAccept() throws Exception{
//建立伺服器端的服務通道
ServerSocketChannel ssc =
ServerSocketChannel.open();
//繫結埠號
ssc.bind(new InetSocketAddress(8888));
//設定非阻塞模式
ssc.configureBlocking(false);
//呼叫accpet方法獲取使用者請求的連線通到
SocketChannel sc = ssc.accept();
System.out.println("有連線連入");
}複製程式碼
執行發現,並沒有輸出“有連線接入”,通道提供阻塞和非阻塞兩種模式,預設為阻塞模式。可以在bind port之前新增ssc.configureBlocking(false);設定通道的非阻塞模式。再次執行“有連線接入”便輸出了。
public void testConnect() throws Exception{
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("127.0.0.1",8888));
System.out.println("連線成功");
}複製程式碼
為加sc.configureBlocking(false);之前,執行該方法丟擲異常,並沒有輸出“連線成功”,通道的connect()方法也是阻塞的;使用方法sc.configureBlocking(false);可以將客戶端連線通道設定為非阻塞模式。
read()、write()方法測試(過度)
sc.read(ByteBuffer dst)
sc.write(ByteBuffer src)複製程式碼
由於這兩個方法都需要ByteBuffer物件作為引數,所以我們需要先講ByteBuffer緩衝區。
NIO-ByteBuffer緩衝區API
public class DemoByteBuffer {
/**ByteBuffer緩衝區類,有三個重要的屬性
* capacity 10:容量,該緩衝區可以最多儲存10個位元組
* position 0:表示位置
* limit 10:限制位(用在獲取元素時限制獲取的邊界)
*/
@Test
public void testByteBuffer(){
ByteBuffer buf = ByteBuffer.allocate(10);
System.out.println();
}
/**put(byte bt)向快取區中新增一個位元組
* 每呼叫一次該方法position的值會加一。
*/
@Test
public void testPut(){
ByteBuffer buf = ByteBuffer.allocate(10);
byte b1 = 1;
byte b2 = 2;
buf.put(b1);
buf.put(b2);
buf.putInt(3);
System.out.println();
}
/**get()獲取position指定位置的一個位元組內容。
* 每呼叫一次該方法,position++;
* 如果在呼叫get()時,position>=limit,
* 則丟擲異常BufferUnderflowException
*
* position(int pt):設定position的值為pt
* position():獲取當前緩衝區的position屬性的值
* limit(int):設定限制為的值
* limit():獲取當前緩衝區的limit屬性的值。
*/
@Test
public void testGet(){
ByteBuffer buf = ByteBuffer.allocate(10);
byte b1 = 1;
byte b2 = 2;
buf.put(b1);//1
buf.put(b2);//2
//設定position的值為0
buf.position(0);
//設定限制位(不想讓使用者獲取無用的資訊)
buf.limit(2);
System.out.println(buf.get());//
System.out.println(buf.get());
System.out.println(buf.get());
}
/**flip()方法:反轉快取區,一般用在新增完資料後。
* limit = position;將limit的值設定為當前position的值
position = 0;再將position的值設定為0
*/
@Test
public void testFlip(){
ByteBuffer buf = ByteBuffer.allocate(10);
byte b1 = 1;
byte b2 = 2;
buf.put(b1);//1
buf.put(b2);//2
/*buf.limit(buf.position());
buf.position(0);*/
buf.flip();
}
/**clear():"清除快取區"
* 底層原始碼:
* position = 0;
limit = capacity;
通過資料覆蓋的方式達到清除的目的。
*/
@Test
public void testClear(){
ByteBuffer buf = ByteBuffer.allocate(10);
byte b1 = 1;
byte b2 = 2;
buf.put(b1);//1
buf.put(b2);//2
buf.clear();
byte b3=33;
buf.put(b3);
buf.flip();
for(int i = 0;i<buf.limit();i++){
System.out.println(buf.get());
}
}
/**hasRemaining()判斷緩衝區中是否還有有效的資料,有返回
* true,沒有返回false
* public final boolean hasRemaining() {
return position < limit;
}
*/
@Test
public void testClear12(){
ByteBuffer buf = ByteBuffer.allocate(10);
byte b1 = 1;
byte b2 = 2;
buf.put(b1);//1
buf.put(b2);//2
buf.clear();
byte b3=33;
buf.put(b3);
buf.flip();
/*for(int i = 0;i<buf.limit();i++){
System.out.println(buf.get());
}*/
/*int i =0;
while(i<buf.limit()){
System.out.println(buf.get());
i++;
}*/
while(buf.hasRemaining()){
System.out.println(buf.get());
}
}
}複製程式碼
NIO-Channel API(下)
1、read()方法 修改ChanelDemo類的testAccept方法:ByteBuffer buf = ByteBuffer.allocate(10);
sc.read(buf);
System.out.println("有資料讀入:"+buf.toString());複製程式碼
testConnect()方法不做任何修改,先執行testAccept()方法,發現在sc.read(buf)行丟擲了空指標異常。buf物件不可能為null,所以sc為null.
非阻塞程式設計最大的問題:不知道是否真正的有客戶端接入,所以容易產生空指標;所以需要人為設定阻塞。
將SocketChannel sc = ssc.accept();改為:
while(sc==null){
sc = ssc.accept();
}複製程式碼
再次執行testAccept()方法,空指標的問題解決了;然後再執行testConnect()方法,發現連線能夠正常建立,但是“有資料讀入了。。”並沒有輸出,說明即使ssc服務通道設定了非阻塞,也沒有改變得到的通道sc預設為阻塞模式,所以sc.read(buf)阻塞了。要不想讓read()方法阻塞,需要在呼叫read()之前加sc.configureBlocking(false);這樣即使沒有讀到資料,“有資料讀入了。。”也能打印出來。
2、write()方法 修改testContect()方法,追加以下程式碼:
ByteBuffer buf = ByteBuffer.wrap("HelloWorld".getBytes());
sc.write(buf);複製程式碼
測試bug,先不執行伺服器端方法,直接執行客戶端方法testConnect(),輸出“連線成功”,但是sc.write(buf)行丟擲NotYetConnectException異常。sc為何丟擲該異常?非阻塞模式很坑的地方在於不知道連線是否真正的建立。修改testConnect():
ByteBuffer buf = ByteBuffer.wrap("HelloWorld".getBytes());
while(!sc.isConnected()){
sc.finishConnect();
}
sc.write(buf);複製程式碼
再次執行testConnect(),之前的異常解決了,但是有出現了新的異常:
java.net.ConnectException: Connection refused: no further information
at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)複製程式碼
先啟動伺服器端(testAccept()),後啟動客戶端(testConnect())即可。
手寫NIO非阻塞模式難度較大,程式碼不是重點,重要在於引出設計思想。
Selector設計思想
問題的引入
使用BIO編寫程式碼模擬一下 (編寫一個伺服器端和客戶端程式,執行一次伺服器程式,執行四次客戶端程式模擬四個使用者執行緒)
public class BIOServer {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(7777));
while(true){
Socket sk = ss.accept();
new Thread(new ServiceRunner(sk)).start();
}
}
}
class ServiceRunner implements Runnable{
private Socket sk;
public ServiceRunner(Socket sk){
this.sk = sk;
}
public void run(){
System.out.println("提供服務的執行緒id:"+
Thread.currentThread().getId());
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class BIOClient {
public static void main(String[] args) throws Exception {
Socket sk = new Socket();
sk.connect(new InetSocketAddress("127.0.0.1",7777));
while(true);
}
}複製程式碼
伺服器啟動 負責為客戶端提供服務,當前執行緒的id:9 負責為客戶端提供服務,當前執行緒的id:10 負責為客戶端提供服務,當前執行緒的id:11 負責為客戶端提供服務,當前執行緒的id:12
分析該模式的缺點: 缺點1:每增加一個使用者請求,就會建立一個新的執行緒為之提供服務。當使用者請求量特別巨大,執行緒數量就會隨之增大,繼而記憶體的佔用增大,所有不適用於高併發、高訪問的場景。 缺點2:執行緒特別多,不僅佔用記憶體開銷,也會佔用大量的cpu開銷,因為cpu要做執行緒排程。 缺點3:如果一個使用者僅僅是連入操作,並且長時間不做其他操作,會產生大量閒置執行緒。會使cpu做無意義的空轉,降低整體效能。 缺點4:這個模型會導致真正需要被處理的執行緒(使用者請求)不能被及時處理。
解決方法
針對缺點3和缺點4,可以將閒置的執行緒設定為阻塞態,cpu是不會排程阻塞態的執行緒,避免了cpu的空轉。所以引入事件監聽機制實現。 Selector多路複用選擇器,起到事件監聽的作用。 監聽哪個使用者執行操作,就喚醒對應的執行緒執行。那麼都有哪些事件呢? 事件:1.accept事件、2.connect事件、3.read事件、4.write事件針對缺點1和缺點2,可以利用非阻塞模型來實現,利用少量執行緒甚至一個執行緒來處理多使用者請求。但是注意,這個模型是有使用場景的,適用於大量短請求場景。(比如使用者訪問電商網站),不適合長請求場景(比如下載大檔案,這種場景,NIO不見得比BIO好)
擴充套件知識 驚群現象,隱患:cpu的負載會在短時間之內聚升,最嚴重的情況時出現短暫卡頓甚至宕機。第二個問題就是效能不高。
Selector服務通道API
accept事件
編寫伺服器端程式:public class NIOServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(6666));
//設定為非阻塞
ssc.configureBlocking(false);
//定義多路複用選擇器
Selector sel = Selector.open();
//註冊accept事件
ssc.register(sel,SelectionKey.OP_ACCEPT);
while(true){
//select()在沒有收到相關事件時產生阻塞,直到
//有事件觸發,阻塞才會得以釋放
sel.select();
//獲取所有的請求的事件
Set<SelectionKey> sks = sel.selectedKeys();
Iterator<SelectionKey> iter = sks.iterator();
while(iter.hasNext()){
SelectionKey sk = iter.next();
if(sk.isAcceptable()){
ServerSocketChannel ssc1=
(ServerSocketChannel)sk.channel();
SocketChannel sc = ssc1.accept();
while(sc==null){
sc = ssc1.accept();
}
sc.configureBlocking(false);
//為sc註冊read和write事件
//0000 0001 OP_READ
//0000 0100 OP_WRITE
//0000 0101 OP_READ和OP_WRITE
sc.register(sel,SelectionKey.OP_WRITE|SelectionKey.OP_READ);
System.out.println("提供服務的執行緒id:"+
Thread.currentThread().getId());
}
if(sk.isWritable()){
}
if(sk.isReadable()){
}
iter.remove();
}
}
}
}
編寫客戶端程式碼:
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("127.0.0.1",6666));
//sc.configureBlocking(false);
System.out.println("客戶端有連線連入");
while(true);
}
}複製程式碼
伺服器端啟動一次,客戶端啟動三次,伺服器端的控制檯輸出:
伺服器端啟動
有客戶端連入,負責處理該請求的執行緒id:1
有客戶端連入,負責處理該請求的執行緒id:1
有客戶端連入,負責處理該請求的執行緒id:1
處理多個請求使用同一個執行緒。
該設計架構只適用的高併發短請求的場景中。
read事件
修改Server類if(sk.isReadable()){
//獲取連線物件
SocketChannel sc = (SocketChannel)sk.channel();
ByteBuffer buf = ByteBuffer.allocate(10);
sc.read(buf);
System.out.println("伺服器端讀取到:"+new String(buf.array()));
//0000 0101 sk.interestOps()獲取原事件
//1111 1110 !OP_READ
//0000 0100 OP_WRITE
//sc.register(sel,SelectionKey.OP_WRITE);
sc.register(sel,sk.interestOps()&~SelectionKey.OP_READ);
}複製程式碼
修改Client類
System.out.println("客戶端連入");
ByteBuffer buffer = ByteBuffer.wrap(
"helloworld".getBytes());
sc.write(buffer);
while(true);複製程式碼
write事件
修改Servetif(sk.isWritable()){
//獲取SocketChannel
SocketChannel sc = (SocketChannel)sk.channel();
ByteBuffer buf = ByteBuffer.wrap("get".getBytes());
sc.write(buf);
//去掉寫事件
sc.register(sel,sk.interestOps()&~SelectionKey.OP_WRITE);
}複製程式碼
修改Client類
public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("127.0.0.1",6666));
while(!sc.isConnected()){
sc.finishConnect();
}
System.out.println("客戶端有連線連入");
ByteBuffer buf = ByteBuffer.wrap(
"helloworld".getBytes());
sc.write(buf);
System.out.println("客戶端資訊已經寫出");
ByteBuffer readBuf = ByteBuffer.allocate(3);
sc.read(readBuf);
System.out.println("客戶端讀到伺服器端傳遞過來的資訊:"
+new String(readBuf.array()));
while(true);
}
}複製程式碼
public class Client2 {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("127.0.0.1",9999));
//對於客戶端,最開始要註冊連線監聽
Selector selector = Selector.open();
sc.register(selector,SelectionKey.OP_CONNECT);
while(true){
selector.select();
Set<SelectionKey> set = selector.selectedKeys();
Iterator<SelectionKey> iter = set.iterator();
while(iter.hasNext()){
SelectionKey sk = iter.next();
if(sk.isConnectable()){
}
if(sk.isWritable()){
}
if(sk.isReadable()){
}
iter.remove();
}
}
}
}複製程式碼