我們先通過(guò)一段代碼來(lái)看看傳統(tǒng)IO的特點(diǎn):我們構(gòu)建一個(gè)服務(wù)端魁巩,然后使用telnet進(jìn)行客戶(hù)端的連接測(cè)試伯诬。
這里要注意如果你的window沒(méi)有安裝telnet客戶(hù)端的話(huà)咳短,使用telnet命令是會(huì)報(bào)"telnet不是內(nèi)部或外部命令"的。如下圖:
解決方法:
? ?操作過(guò)程:點(diǎn)擊"開(kāi)始"→"控制器面板"→" 查看方式:類(lèi)型"則點(diǎn)擊"程序"("查看方式:大圖標(biāo)"則點(diǎn)擊"程序和功能")→ "啟動(dòng)或關(guān)閉windows功能"→ 在"Windows功能"界面勾選Telnet服務(wù)器和客戶(hù)端 →最后點(diǎn)擊"確定"等待安裝苛预。勾選Telnet客戶(hù)端如下圖:
操作成功之后會(huì)再次進(jìn)入命令行輸入
telnet
便會(huì)出現(xiàn)如下界面:
好了孵稽,準(zhǔn)備工作就做好了许起,現(xiàn)在開(kāi)始上我們的代碼了十偶,用Java傳統(tǒng)IO寫(xiě)個(gè)服務(wù)器端:
public class OioServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
//創(chuàng)建一個(gè)Socket服務(wù),監(jiān)聽(tīng)10000端口
serverSocket = new ServerSocket(10000);
System.out.println("服務(wù)器啟動(dòng)...");
while(true){
final Socket socket = serverSocket.accept();
System.out.println("來(lái)了一個(gè)新的客戶(hù)端連接");
handler(socket);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服務(wù)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();
}
}
}
啟動(dòng)這個(gè)main我們使用telnet方式模擬客戶(hù)端來(lái)看看傳統(tǒng)的IO有哪些特征:
運(yùn)行結(jié)果:
首先很典型的就是啟動(dòng)的時(shí)候使用Dubug方式,我們發(fā)現(xiàn)線(xiàn)程堵塞在final Socket socket = serverSocket.accept()
這一行猛频,然后使用telnet連接之后狮崩,程序又堵塞在了handler方法的這一行int read = inputStream.read(buffer);
,而且我們開(kāi)了一個(gè)telnet客戶(hù)端的情況下鹿寻,如果再次開(kāi)一個(gè)會(huì)發(fā)現(xiàn)睦柴,這個(gè)時(shí)候我們是無(wú)法在第一個(gè)telnet連接還沒(méi)有斷開(kāi)的情況下開(kāi)啟第二個(gè)連接的。
所以綜合上訴我們總結(jié)出了傳統(tǒng)IO一下3個(gè)特征:
1.堵塞
2.單線(xiàn)程情況下只響應(yīng)一個(gè)客戶(hù)端連接的事件
現(xiàn)在我們?cè)谏厦娴拇a上面做一些優(yōu)化毡熏,就是解決傳統(tǒng)IO只能連接一個(gè)客戶(hù)端的問(wèn)題坦敌,因?yàn)樯厦娴拇a連接不上實(shí)際上是因?yàn)槲覀儼逊?wù)器的端口連接監(jiān)聽(tīng)和監(jiān)聽(tīng)處理都放到了一個(gè)線(xiàn)程去做,
而在第一個(gè)客戶(hù)端沒(méi)有關(guān)閉的情況下線(xiàn)程實(shí)際上堵塞在了第一個(gè)客戶(hù)端的read這一行(我去監(jiān)聽(tīng)第一個(gè)客戶(hù)端去了)痢法,這個(gè)時(shí)候你開(kāi)啟第二個(gè)telnet來(lái)連接服務(wù)器狱窘,我們的請(qǐng)求當(dāng)然是不會(huì)被搭理的咯。
所以我們將監(jiān)聽(tīng)和監(jiān)聽(tīng)處理分別交給兩個(gè)不同的線(xiàn)程來(lái)做财搁,這樣的話(huà)训柴,線(xiàn)程也不會(huì)忙不過(guò)來(lái),看下面的代碼:
我們使用線(xiàn)程池來(lái)解決單個(gè)客戶(hù)端的問(wèn)題:
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
ServerSocket serverSocket = null;
try {
//創(chuàng)建一個(gè)Socket服務(wù)妇拯,監(jiān)聽(tīng)10000端口
serverSocket = new ServerSocket(10000);
System.out.println("服務(wù)器啟動(dòng)...");
while(true){
final Socket socket = serverSocket.accept();
System.out.println("來(lái)了一個(gè)新的客戶(hù)端連接");
executorService.submit(new Runnable() {
@Override
public void run() {
handler(socket);
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服務(wù)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();
}
}
運(yùn)行結(jié)果:
思考:這就解決了我們之前的問(wèn)題但是如果我這個(gè)服務(wù)器每秒有百萬(wàn)級(jí)別的人去訪(fǎng)問(wèn)呢越锈?線(xiàn)程池就要開(kāi)百萬(wàn)個(gè)線(xiàn)程去監(jiān)聽(tīng),這顯然很不合理膘滨,至少傳統(tǒng)IO在雙十一這種高并發(fā)場(chǎng)景的瓶頸是很明顯的甘凭。
總結(jié): 傳統(tǒng)IO就好比下圖的這種模式:
一家餐廳,有一個(gè)大門(mén)(ServerSocket)然后每次來(lái)一個(gè)客人火邓,我們就要聘用一個(gè)服務(wù)員進(jìn)行服務(wù)丹弱,服務(wù)員沒(méi)有得到復(fù)用,性能消耗巨大铲咨,這顯然很不合理躲胳,如果來(lái)的客人多了,客人又沒(méi)點(diǎn)多少才纤勒,估計(jì)餐廳老板得破產(chǎn)坯苹。我們現(xiàn)實(shí)中餐廳的服務(wù)員都是一個(gè)人給好幾桌甚至好幾十桌的人服務(wù)的。所以后來(lái)在傳統(tǒng)IO的基礎(chǔ)上摇天,我們有了NIO(new IO),它和傳統(tǒng)IO最大的區(qū)別就是它更加優(yōu)雅粹湃,更加方便恐仑,更加符合生活。
NIO就是我們現(xiàn)實(shí)中飯店的模型为鳄。服務(wù)員可以為多個(gè)客戶(hù)服務(wù)裳仆。
我們通過(guò)代碼來(lái)實(shí)際驗(yàn)證這種特性:
/**
* NIO服務(wù)端
* 代碼來(lái)源網(wǎng)絡(luò),再此感謝各位前輩孤钦。
* @author -GuiRong
*/
public class NIOServer {
// 通道管理器
private Selector selector;
/**
* 獲得一個(gè)ServerSocket通道歧斟,并對(duì)該通道做一些初始化的工作
*
* @param port 綁定的端口號(hào)
* @throws IOException
*/
public void initServer(int port) throws IOException {
// 獲得一個(gè)ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 設(shè)置通道為非阻塞
serverChannel.configureBlocking(false);
// 將該通道對(duì)應(yīng)的ServerSocket綁定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 獲得一個(gè)通道管理器
this.selector = Selector.open();
// 將通道管理器和該通道綁定,并為該通道注冊(cè)SelectionKey.OP_ACCEPT事件,注冊(cè)該事件后司训,
// 當(dāng)該事件到達(dá)時(shí)构捡,selector.select()會(huì)返回,如果該事件沒(méi)到達(dá)selector.select()會(huì)一直阻塞壳猜。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 采用輪詢(xún)的方式監(jiān)聽(tīng)selector上是否有需要處理的事件勾徽,如果有,則進(jìn)行處理
*
* @throws IOException
*/
public void listen() throws IOException {
System.out.println("服務(wù)端啟動(dòng)成功统扳!");
// 輪詢(xún)?cè)L問(wèn)selector
while (true) {
// 當(dāng)注冊(cè)的事件到達(dá)時(shí)喘帚,方法返回;否則,該方法會(huì)一直阻塞
selector.select();
// 獲得selector中選中的項(xiàng)的迭代器咒钟,選中的項(xiàng)為注冊(cè)的事件
Iterator<?> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 刪除已選的key,以防重復(fù)處理
ite.remove();
handler(key);
}
}
}
/**
* 處理請(qǐng)求
*
* @param key
* @throws IOException
*/
public void handler(SelectionKey key) throws IOException {
// 客戶(hù)端請(qǐng)求連接事件
if (key.isAcceptable()) {
handlerAccept(key);
// 獲得了可讀的事件
} else if (key.isReadable()) {
handelerRead(key);
}
}
/**
* 處理連接請(qǐng)求
*
* @param key
* @throws IOException
*/
public void handlerAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 獲得和客戶(hù)端連接的通道
SocketChannel channel = server.accept();
// 設(shè)置成非阻塞
channel.configureBlocking(false);
// 在這里可以給客戶(hù)端發(fā)送信息哦
System.out.println("新的客戶(hù)端連接");
// 在和客戶(hù)端連接成功之后吹由,為了可以接收到客戶(hù)端的信息,需要給通道設(shè)置讀的權(quán)限朱嘴。
channel.register(this.selector, SelectionKey.OP_READ);
}
/**
* 處理讀的事件
*
* @param key
* @throws IOException
*/
public void handelerRead(SelectionKey key) throws IOException {
// 服務(wù)器可讀取消息:得到事件發(fā)生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 創(chuàng)建讀取的緩沖區(qū)
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("服務(wù)端收到信息:" + msg);
//回寫(xiě)數(shù)據(jù)
ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
channel.write(outBuffer);// 將消息回送給客戶(hù)端
} else {
System.out.println("客戶(hù)端關(guān)閉");
key.cancel();
}
}
/**
* 啟動(dòng)服務(wù)端測(cè)試
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(8000);
server.listen();
}
}
運(yùn)行結(jié)果:
NIO的一些疑問(wèn)
1倾鲫、客戶(hù)端關(guān)閉的時(shí)候會(huì)拋出異常,死循環(huán)解決方案
int read = channel.read(buffer);
if(read > 0){
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服務(wù)端收到信息:" + msg);
//回寫(xiě)數(shù)據(jù)
ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
channel.write(outBuffer);// 將消息回送給客戶(hù)端
}else{
System.out.println("客戶(hù)端關(guān)閉");
key.cancel();
}
2萍嬉、selector.select();阻塞乌昔,那為什么說(shuō)nio是非阻塞的IO?
selector.select()
selector.select(1000);不阻塞
selector.wakeup();也可以喚醒selector
selector.selectNow();也可以立馬返還壤追,視頻里忘了講了磕道,哈,這里補(bǔ)上
3行冰、SelectionKey.OP_WRITE是代表什么意思
OP_WRITE表示底層緩沖區(qū)是否有空間溺蕉,是則響應(yīng)返還true
附:項(xiàng)目下載地址
注:個(gè)人學(xué)習(xí)筆記,部分資源來(lái)源于網(wǎng)絡(luò)悼做。在此感謝各位前輩疯特!