一、NIO概述
1.首先介紹一下BIO(同步阻塞IO)
BIO-JDK1.0 - 同步阻塞式IO-BlockingIO
在執行ACCEPT CONNECT READ WRITE 四中操作時都會產生阻塞
Accept:客戶端未連接
Connect:連接超時(connection reset);連接拒絕connection refuse
Read:服務器端讀取數據,但是客戶端未寫入數據,產生阻塞
Write:客戶端寫入數據,服務器端未讀取,當寫入數據數量達到緩衝區極限,產生阻塞
在平常開發當中並不是問題 甚至因爲這樣的模型直觀而簡單 應用的場景非常廣泛
但是在高併發的場景下 這樣的阻塞式IO可能會造成如下問題:
在服務器開發中 需要在服務器端通過少量線程處理多個客戶端請求 這就要求 在少量的線程應該可以靈活的切換處理不同客戶端 但傳統的BIO阻塞式的工作方式 一旦阻塞了線程 線程就被掛起 無法繼續執行 無法實現這樣的功能
2.NIO - JDK4.0 - 同步非阻塞式IO - NonBlockingIO/NewIO
操作: Accepet、 Connect、 Read、Write(非阻塞的)
可以隨時讓線程切換所處理的客戶端 從而可以實現高併發服務器的開發
需求:用少量的線程來處理多個客戶端請求
騰訊QQ聊天案例
多人連接服務器,通過一箇中心選擇器進行管理,當多人需要聊天發送消息時(排隊處理),提供線程,一個客戶佔用線程沒發消息,導致其他人消息發不出去(BIO)
使用NIO用少量的線程來處理多個客戶端請求,讓線程可以隨時切換所處理的客戶端
兩者特點對比:
BIO:同步阻塞式IO 面向流 操作字節或字符 單向傳輸數據
NIO:同步非阻塞式IO 面向通道 操作緩衝區 雙向傳輸數據
二、開源的NIO結構的服務器框架(瞭解)
MINA
Netty(更好)
IO方式:
阻塞/非阻塞:討論的是線程的角度,當執行某些操作不能立即完成時,線程是否被掛起,失去cpu爭奪權無法繼續執行直到阻塞結束或被喚醒
同步/異步:討論的是參與通信雙方的工作機制,是否需要互相等待對方的執行
同步:
通信過程中 一方在處理通信,另一方 要等待對方執行不能去做其他無關的事
異步:
通信過程中 一方在處理通信,另一方 可以不用等待對方而可以去做其他無關的事 直到對方處理通信完成 再在適合的時候繼續處理通信過程
三種IO機制的區別!!!
BIO | jdk1.0 | 同步阻塞式IO | 面向流 | 操作字節或字符 | 單向傳輸數據 |
NIO | jdk4.0 | 同步非阻塞式IO | 面向通道 | 操作緩衝區 | 雙向傳輸數據 |
AIO | jdk7.0 | 異步非阻塞式IO | 大量使用回調函數 | 異步處理通信過程 |
三、粘包問題
概念:通過socket發送多段數據時 底層的tcp協議 會自動根據需要 將數據 拆分或合併 組成數據包後發送給接受者 ,接受者收到數據後 無法直接通過tcp協議本身判斷數據的邊界,這個問題就稱之爲粘包問題.
產生原因:
1.本質上是因爲tcp協議是傳輸層的協議 本身沒有對會話控制提供相應的能力
2.socket開發網絡程序時相當於在自己實現會話層、表示層和應用層的功能,所以需要自己來相辦法解決粘包問題
舉例:aaa bbbbb cccc
TCP屬於傳輸層,底層靠數據包傳輸,只負責傳輸數據,不負責分配數據格式,結果爲ccccbbbbbaaa
導致不能明確數據的結構進行分段
解決方案:
a.只發送固定長度的數據
通信的雙發約定每次發送數據的長度,每次只發送固定長度的數據,接收數據方 每次都按照固定長度獲取數據
缺點:
不夠靈活,只適合每次傳輸的數據都有固定長度的場景
b. 約定分隔符
通信雙方約定一個特殊的分隔符用來表示數據的邊界,接收方收到數據時,不停讀取,以分隔符爲標誌,區分數據的邊界
缺點:
如果數據本身就包含分隔符字符,則需要對數據進行預處理將數據本身包含的分隔符進行轉義,相對來說比較麻煩
c. 數據分頭和體,在頭信息中描述數據長度或格式
通過頭信息中傳遞數據長度或格式信息,在接收方法接收數據時,先讀取頭信息,在根據頭信息來決定如何獲取後續數據
協議:
公共協議:HTTP FTP SMTP POP3等等,需按照協議來通信,約束較大
通信問題解決:真正在開發過程中,如果需要開發底層網絡通信機制,可以根據需要選擇公有協議 或 自定義私有協議來解決通信規範問題
四、NIO通信案例
服務端:
服務端:
package cn.tedu.nio.channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerSocketChannelDemo01 {
public static void main(String[] args) throws Exception {
//1.創建ServerSockentChannel對象
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.綁定指定端口
ssc.bind(new InetSocketAddress(44444));
//3.設置非阻塞模式
ssc.configureBlocking(false);
//4.接收客戶端連接
SocketChannel sc = null;
while(sc == null){
sc = ssc.accept();
}
sc.configureBlocking(false);
//5.讀取數據
ByteBuffer buf = ByteBuffer.allocate(5);
while(buf.hasRemaining()){
sc.read(buf);
}
//6.獲取數據打印
byte[] arr = buf.array();
String str = new String(arr);
System.out.println(str);
//5.關閉通道
sc.close();
ssc.close();
}
}
客戶端:
客戶端:
package cn.tedu.nio.channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class SocketChannelDemo01 {
public static void main(String[] args) throws Exception {
//1.創建客戶端SocketChannel
SocketChannel sc = SocketChannel.open();
//2.配置啓用非阻塞模式
sc.configureBlocking(false);
//3.連接服務器
boolean isConn = sc.connect(new InetSocketAddress("127.0.0.1", 44444));
if(!isConn){
while(!sc.finishConnect()){
}
}
//4.發送數據到服務器
ByteBuffer buf = ByteBuffer.wrap("abcde".getBytes());
while(buf.hasRemaining()){
sc.write(buf);
}
//5.關閉通道
sc.close();
}
}
五、少量線程處理多客戶端請求案例
客戶端:
客戶端:
package cn.tedu.nio.channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class SocketChannelDemo01 {
public static void main(String[] args) throws Exception {
//1.創建客戶端SocketChannel
SocketChannel sc = SocketChannel.open();
//2.配置啓用非阻塞模式
sc.configureBlocking(false);
//3.連接服務器
boolean isConn = sc.connect(new InetSocketAddress("127.0.0.1", 44444));
if(!isConn){
while(!sc.finishConnect()){
}
}
//4.發送數據到服務器
ByteBuffer buf = ByteBuffer.wrap("abcde".getBytes());
while(buf.hasRemaining()){
sc.write(buf);
}
//5.關閉通道
sc.close();
}
}
服務端:
package cn.tedu.nio.selector;
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;
import java.util.Set;
public class ServerSocketDemo01 {
public static void main(String[] args) throws Exception {
//0.創建選擇器
Selector selc = Selector.open();
//1.創建代表服務器的ServerSocketChannel對象
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.設置爲非阻塞模式
ssc.configureBlocking(false);
//3.設置監聽的端口
ssc.bind(new InetSocketAddress(44444));
//4.將ssc註冊到選擇器中關注ACCEPT操作
ssc.register(selc, SelectionKey.OP_ACCEPT);
//5.通過選擇器選擇就緒的鍵
while(true){
selc.select();//嘗試到註冊的鍵集中來尋找就緒的鍵 如果一個就緒的鍵都找不到 就進入阻塞 直到找到就緒的鍵 返回就緒的鍵的個數
//6.獲取就緒的鍵的集合
Set<SelectionKey> keys = selc.selectedKeys();
//7.遍歷處理就緒的鍵 代表的操作
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()){
//--獲取到就緒的鍵 根據鍵代表的操作的不同 來進行不同處理
SelectionKey key = it.next();
if(key.isAcceptable()){
//--發現了Accept操作
//--獲取通道
ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
//--完成Accept操作
SocketChannel sc = sscx.accept();
//--在sc上註冊讀數據的操作
sc.configureBlocking(false);
sc.register(selc, SelectionKey.OP_READ);
}else if(key.isConnectable()){
}else if(key.isWritable()){
}else if(key.isReadable()){
//--發現了Read操作
//--獲取就緒的通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成讀取數據的操作
ByteBuffer buf = ByteBuffer.allocate(10);
while(buf.hasRemaining()){
scx.read(buf);
}
String msg = new String(buf.array());
System.out.println("[收到來自客戶端的消息]:"+msg);
}else{
throw new RuntimeException("未知的鍵,見了鬼了~");
}
//8.移除處理完的鍵
it.remove();
}
}
}
}