NIO和BIO對比
NIO(non blocking I/O)非阻塞I/O,jdk1.4引入的新I/O,平時接觸的文件的I/O操做是BIO,即阻塞I/O
BIO API使用
具體流程:
public void testAccept() throws IOException{
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(9999));
Socket sk = ss.accept();
System.out.println("有鏈接連入");
}複製代碼
JUnit測試,「有鏈接接入」沒有輸出,說明accept()方法產生阻塞了。
public void testContect() throws Exception{
Socket sk = new Socket();
sk.connect(new InetSocketAddress(
"127.0.0.1", 9999));
System.out.println("鏈接成功");
}複製代碼
先運行服務器端方法(testAccept()),再運行客戶端方法,發現accept()方法阻塞釋放了。另外「鏈接成功」正確輸出。若是不先啓動服務器端方法,而直接運行客戶端方法,發現先是阻塞了一下,而後JUnit測試拋出異常。
總結:connect()方法會產生阻塞,指定鏈接成功,阻塞才釋放。
accept()方法產生的阻塞,直到服務器得到到鏈接後,阻塞才釋放。
InputStream in= sk.getInputStream();
byte bts[] = new byte[1024];
in.read(bts);
System.out.println("讀取到了數據:"+new String(bts));複製代碼
C2.爲了避免讓鏈接中斷,須要修改testConnect()
while(true);複製代碼
總結:read()方法會產生阻塞,直到讀取到內容後,阻塞才被釋放。
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);
}複製代碼
總結:write()方法也會產生阻塞,write()一直往出寫數據,可是沒有任何一方讀取數據,直到寫出到必定量(個人是655130B,不一樣電腦可能不一樣)的時候,產生阻塞。向網卡設備緩衝區中寫數據。
NIO 相關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(下)
修改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);這樣即便沒有讀到數據,「有數據讀入了。。」也能打印出來。
修改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設計思想
問題的引入
(編寫一個服務器端和客戶端程序,運行一次服務器程序,運行四次客戶端程序模擬四個用戶線程)
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);
}
}複製代碼
缺點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);
}
}複製代碼
服務器端啓動一次,客戶端啓動三次,服務器端的控制檯輸出:
read事件
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);
}複製代碼
System.out.println("客戶端連入");
ByteBuffer buffer = ByteBuffer.wrap(
"helloworld".getBytes());
sc.write(buffer);
while(true);複製代碼
write事件
if(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);
}複製代碼
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();
}
}
}
}複製代碼