我們先通過一段代碼來看看傳統IO的特點:我們構建一個服務端,然后使用telnet進行客戶端的連接測試。
這里要注意如果你的window沒有安裝telnet客戶端的話,使用telnet命令是會報"telnet不是內部或外部命令"的。如下圖:
解決方法:
? ?操作過程:點擊"開始"→"控制器面板"→" 查看方式:類型"則點擊"程序"("查看方式:大圖標"則點擊"程序和功能")→ "啟動或關閉windows功能"→ 在"Windows功能"界面勾選Telnet服務器和客戶端 →最后點擊"確定"等待安裝。勾選Telnet客戶端如下圖:
操作成功之后會再次進入命令行輸入
telnet
便會出現如下界面:
好了,準備工作就做好了,現在開始上我們的代碼了,用Java傳統IO寫個服務器端:
public class OioServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
//創建一個Socket服務,監聽10000端口
serverSocket = new ServerSocket(10000);
System.out.println("服務器啟動...");
while(true){
final Socket socket = serverSocket.accept();
System.out.println("來了一個新的客戶端連接");
handler(socket);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服務handler
*/
public static void handler(Socket socket){
byte[] buffer = new byte[1024];
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
while(true){
int read = inputStream.read(buffer);
if(read!=-1){
System.out.println(new String(buffer,0,read));
}
}
} catch (IOException e) {
System.out.println("獲取Socket的輸入流失敗!");
e.printStackTrace();
}
}
}
啟動這個main我們使用telnet方式模擬客戶端來看看傳統的IO有哪些特征:
運行結果:
首先很典型的就是啟動的時候使用Dubug方式,我們發現線程堵塞在final Socket socket = serverSocket.accept()
這一行,然后使用telnet連接之后,程序又堵塞在了handler方法的這一行int read = inputStream.read(buffer);
,而且我們開了一個telnet客戶端的情況下,如果再次開一個會發現,這個時候我們是無法在第一個telnet連接還沒有斷開的情況下開啟第二個連接的。
所以綜合上訴我們總結出了傳統IO一下3個特征:
1.堵塞
2.單線程情況下只響應一個客戶端連接的事件
現在我們在上面的代碼上面做一些優化,就是解決傳統IO只能連接一個客戶端的問題,因為上面的代碼連接不上實際上是因為我們把服務器的端口連接監聽和監聽處理都放到了一個線程去做,
而在第一個客戶端沒有關閉的情況下線程實際上堵塞在了第一個客戶端的read這一行(我去監聽第一個客戶端去了),這個時候你開啟第二個telnet來連接服務器,我們的請求當然是不會被搭理的咯。
所以我們將監聽和監聽處理分別交給兩個不同的線程來做,這樣的話,線程也不會忙不過來,看下面的代碼:
我們使用線程池來解決單個客戶端的問題:
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
ServerSocket serverSocket = null;
try {
//創建一個Socket服務,監聽10000端口
serverSocket = new ServerSocket(10000);
System.out.println("服務器啟動...");
while(true){
final Socket socket = serverSocket.accept();
System.out.println("來了一個新的客戶端連接");
executorService.submit(new Runnable() {
@Override
public void run() {
handler(socket);
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服務handler
*/
public static void handler(Socket socket){
byte[] buffer = new byte[1024];
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
while(true){
int read = inputStream.read(buffer);
if(read!=-1){
System.out.println(new String(buffer,0,read));
}
}
} catch (IOException e) {
System.out.println("獲取Socket的輸入流失敗!");
e.printStackTrace();
}
}
運行結果:
思考:這就解決了我們之前的問題但是如果我這個服務器每秒有百萬級別的人去訪問呢?線程池就要開百萬個線程去監聽,這顯然很不合理,至少傳統IO在雙十一這種高并發場景的瓶頸是很明顯的。
總結: 傳統IO就好比下圖的這種模式:
一家餐廳,有一個大門(ServerSocket)然后每次來一個客人,我們就要聘用一個服務員進行服務,服務員沒有得到復用,性能消耗巨大,這顯然很不合理,如果來的客人多了,客人又沒點多少才,估計餐廳老板得破產。我們現實中餐廳的服務員都是一個人給好幾桌甚至好幾十桌的人服務的。所以后來在傳統IO的基礎上,我們有了NIO(new IO),它和傳統IO最大的區別就是它更加優雅,更加方便,更加符合生活。
NIO就是我們現實中飯店的模型。服務員可以為多個客戶服務。
我們通過代碼來實際驗證這種特性:
/**
* NIO服務端
* 代碼來源網絡,再此感謝各位前輩。
* @author -GuiRong
*/
public class NIOServer {
// 通道管理器
private Selector selector;
/**
* 獲得一個ServerSocket通道,并對該通道做一些初始化的工作
*
* @param port 綁定的端口號
* @throws IOException
*/
public void initServer(int port) throws IOException {
// 獲得一個ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 設置通道為非阻塞
serverChannel.configureBlocking(false);
// 將該通道對應的ServerSocket綁定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 獲得一個通道管理器
this.selector = Selector.open();
// 將通道管理器和該通道綁定,并為該通道注冊SelectionKey.OP_ACCEPT事件,注冊該事件后,
// 當該事件到達時,selector.select()會返回,如果該事件沒到達selector.select()會一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 采用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理
*
* @throws IOException
*/
public void listen() throws IOException {
System.out.println("服務端啟動成功!");
// 輪詢訪問selector
while (true) {
// 當注冊的事件到達時,方法返回;否則,該方法會一直阻塞
selector.select();
// 獲得selector中選中的項的迭代器,選中的項為注冊的事件
Iterator<?> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 刪除已選的key,以防重復處理
ite.remove();
handler(key);
}
}
}
/**
* 處理請求
*
* @param key
* @throws IOException
*/
public void handler(SelectionKey key) throws IOException {
// 客戶端請求連接事件
if (key.isAcceptable()) {
handlerAccept(key);
// 獲得了可讀的事件
} else if (key.isReadable()) {
handelerRead(key);
}
}
/**
* 處理連接請求
*
* @param key
* @throws IOException
*/
public void handlerAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 獲得和客戶端連接的通道
SocketChannel channel = server.accept();
// 設置成非阻塞
channel.configureBlocking(false);
// 在這里可以給客戶端發送信息哦
System.out.println("新的客戶端連接");
// 在和客戶端連接成功之后,為了可以接收到客戶端的信息,需要給通道設置讀的權限。
channel.register(this.selector, SelectionKey.OP_READ);
}
/**
* 處理讀的事件
*
* @param key
* @throws IOException
*/
public void handelerRead(SelectionKey key) throws IOException {
// 服務器可讀取消息:得到事件發生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 創建讀取的緩沖區
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read > 0) {
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服務端收到信息:" + msg);
//回寫數據
ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
channel.write(outBuffer);// 將消息回送給客戶端
} else {
System.out.println("客戶端關閉");
key.cancel();
}
}
/**
* 啟動服務端測試
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(8000);
server.listen();
}
}
運行結果:
NIO的一些疑問
1、客戶端關閉的時候會拋出異常,死循環解決方案
int read = channel.read(buffer);
if(read > 0){
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服務端收到信息:" + msg);
//回寫數據
ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
channel.write(outBuffer);// 將消息回送給客戶端
}else{
System.out.println("客戶端關閉");
key.cancel();
}
2、selector.select();阻塞,那為什么說nio是非阻塞的IO?
selector.select()
selector.select(1000);不阻塞
selector.wakeup();也可以喚醒selector
selector.selectNow();也可以立馬返還,視頻里忘了講了,哈,這里補上
3、SelectionKey.OP_WRITE是代表什么意思
OP_WRITE表示底層緩沖區是否有空間,是則響應返還true
附:項目下載地址
注:個人學習筆記,部分資源來源于網絡。在此感謝各位前輩!