網絡編程從BIO到NIO
標簽: Java基礎
ServerSocket是一個BIO操作,BIO,blocking I/O。即阻塞IO,為什么說是阻塞的呢?在哪里阻塞呢?阻塞了什么呢?
BIO使用:
- 單線程使用ServerSocket
- 創建服務端
package bio; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /** * BIO服務器。 * */ public class BIOServer { public static void main(String[] args) throws Exception{ // ServerSocket就是一個阻塞的IO ServerSocket serverSocket = new ServerSocket(8899); while (true) { System.out.println("waiting connection"); Socket socket = serverSocket.accept(); System.out.println("one client connection"); InputStream socketInputStream = socket.getInputStream(); System.out.println("waiting receive"); byte[] bytes = new byte[1024]; socketInputStream.read(bytes); System.out.println("receive:"+new String(bytes)); } } }
- 創建兩個客戶端,用來連接服務端
package bio; import java.io.OutputStream; import java.net.Socket; import java.util.Scanner; /** * * 客戶端1 * */ public class Client1 { public static void main(String[] args) throws Exception{ // 連接上服務端 Socket socket = new Socket("localhost", 8899); OutputStream outputStream = socket.getOutputStream(); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String nextLine = scanner.nextLine(); // 像服務端發送數據 outputStream.write(nextLine.getBytes()); } } }
package bio; import java.io.OutputStream; import java.net.Socket; import java.util.Scanner; /** * * 客戶端2 * */ public class Client2 { public static void main(String[] args) throws Exception{ // 連接上服務端 Socket socket = new Socket("localhost", 8899); OutputStream outputStream = socket.getOutputStream(); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String nextLine = scanner.nextLine(); // 像服務端發送數據 outputStream.write(nextLine.getBytes()); } } }
- 啟動服務端,觀察服務端BIOServer控制臺輸出。(控制臺打印waiting connection,并沒有one client connection,此時程序阻塞在Socket socket = serverSocket.accept();這里,等待客戶端連接。)
- 啟動一個客戶端1,觀察服務端BIOServer控制臺輸出。(控制臺繼續打印,但沒有打印出receive:,此時程序阻塞在socketInputStream.read(bytes);等待客戶端1的輸出內容。)
- 在客戶端1的控制臺中輸入幾個內容,觀察服務端控制臺輸出。(我在客戶端1的控制臺輸入了hello world,此時服務端接收到內容,并打印到控制臺,此時。一個while循環結束,繼續下一個循環,所有繼續打印了waiting connection)
- 清空服務端的控制臺內容,關閉客戶端1的服務然后再將其啟動,啟動完成之后,再啟動客戶端2,在服務端2的控制臺輸入:hello world.
- 觀察服務端的控制臺,并沒有打印出hello world。為什么呢?其實,當客戶端1連接上來之后,當前循環的代碼已經執行到了socketInputStream.read(bytes);并阻塞在這里等待客戶端1發來內容。于是,客戶端2的連接和輸出都被阻塞了。在客戶端1控制臺輸入內容,此時再觀察服務端控制臺內容:
發現問題:ServerSocket會在兩個方法阻塞,一個是accept(),一個是read()。從步驟6中可以發現,如果在單線程中使用ServerSocket,一次只能與一個客戶端完成連接,如果此時這個已經連接的客戶端沒有發送任何數據,就會一直阻塞在read方法,其他的客戶端的連接也會被阻塞了。
解決問題:為了使ServerSocket能夠在一個客戶端連接上來之后,繼續連接上其他的客戶端,可以使用多線程解決問題。一個客戶端連接上來之后,開辟一個線程去等待該客戶端的內容,主線程還能繼續處理其他客戶端的連接。
- 多線程使用ServerSocket
package bio;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO服務器。使用多線程。一個連接對應一個線程
*
*/
public class BIOServer2 {
public static void main(String[] args) throws Exception{
// ServerSocket就是一個阻塞的IO
ServerSocket serverSocket = new ServerSocket(8899);
while (true) {
System.out.println("waiting connection");
Socket socket = serverSocket.accept();
new Thread(()->{
try {
System.out.println("one client connection");
InputStream socketInputStream = socket.getInputStream();
System.out.println("waiting receive");
byte[] bytes = new byte[1024];
socketInputStream.read(bytes);
System.out.println("receive:"+new String(bytes));
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
啟動服務端,先啟動客戶端1,再啟動客戶端2,再客戶端2的控制臺輸入數據,此時觀察服務端控制臺輸出:
此時解決了使用單線程造成連接阻塞的問題。
發現問題:多線程雖然能夠解決多個連接的問題,但每一個連接都會開啟一個線程,這樣不僅會產生太多的線程資源,即使使用線程池也會造成線程資源的浪費。一般客戶端連接是有兩個步驟的,一個是連接,一個是發送數據。假設有10000個連接上來了,但只有100個連接會發送數據,這樣就會造成線程去處理9900個連接的浪費。
思考:多線程解決了連接阻塞的問題,但是增加了線程資源。單線程呢,沒有線程資源的浪費,但又會阻塞連接。要是能夠在單線程下,不產生阻塞就好了。即serverSocket.accept()和socketInputStream.read(bytes)兩個方法不會阻塞。
// 提供一個api,設置這里不要阻塞
Socket socket = serverSocket.accept();
// 提供一個api,設置這里不要阻塞
socketInputStream.read(bytes);
解決問題:ServerSocket是JDK提供的類,我們不能修改源代碼。好在對于上面的問題,JDK的開發人員早已經發現了問題,并提供了SocketChannel,實現類不阻塞的功能。可以把SocketChannel就理解成是一個ServerSocket,在其基礎上解決了阻塞的問題。(畢竟ServerSocket已經被廣泛使用,不能直接修改它的源代碼,那就新寫一個類)
NIO使用:
概念:NIO不再使用ServerSocket、Socket,引出了ServerSocketChannel、SocketChannel。ServerSocketChannel對應ServerSocket,SocketChannel對應ServerSocket。
package bio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
*
* NIO服務器,ServerSocketChannel
*
*/
public class NIOServer {
public static void main(String[] args) throws Exception{
// 下面這兩個相當于ServerSocket serverSocket = new ServerSocket(8899);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8899));
// 設置不阻塞,相當于設置serverSocket.accept()不會阻塞
// 不設置false,接收連接還是阻塞的
serverSocketChannel.configureBlocking(false);
while (true) {
// serverSocket.accept()
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("等待客戶端連接:"+socketChannel);
if (socketChannel!=null) {
System.out.println("客戶端連接成功,等待接收數據...");
// 設置不阻塞,相當于設置socketInputStream.read(bytes)不會阻塞
// 不設置false,讀取數據還是阻塞的
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffer);
if (read>0) {
byteBuffer.flip();
System.out.println("接收客戶端數據:"+byteBuffer.toString());
}
}
}
}
}
(特別注意serverSocketChannel.configureBlocking(false); socketChannel.configureBlocking(false);)
- 運行服務端,觀察控制臺輸出:
在沒有客戶端連接的時候,serverSocketChannel.accept()會返回null,不像serverSocket.accept()是一直阻塞等待。
- 注釋掉代碼:System.out.println("等待客戶端連接:"+socketChannel); (輸出太多,影響觀感),重啟服務端,然后運行客戶端1,觀察服務端控制臺輸出。
服務端接收到客戶端的請求,其實此時的代碼并沒有阻塞在等待客戶端1的內容,而是一直在一次次循環中。
- 在客戶端1輸入內容發送給服務端,觀察服務端控制臺輸出。
客戶端1發送的內容呢?服務端并沒有打印出來?這是為什么呢。其實,看看服務端代碼,就能發現,在接收到客戶端1的連接之后,代碼繼續運行,到了下一個循環里,此時serverSocketChannel.accept()==null了,相當于把客戶端1的連接信息覆蓋掉了,導致客戶端1的連接信息丟失。
-
解決問題:需要有一個集合能夠存儲每一次連接,這樣不會弄丟以前的連接信息。
package bio; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.List; /** * * NIO服務器,ServerSocketChannel * */ public class NIOServer2 { // 創建一個集合,用于記錄每一個連接信息 private static List<SocketChannel> socketChannelList = new ArrayList<>(); public static void main(String[] args) throws Exception{ // 下面這兩個相當于ServerSocket serverSocket = new ServerSocket(8899); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8899)); // 設置不阻塞,相當于設置serverSocket.accept()不會阻塞 // 不設置false,接收連接還是阻塞的 serverSocketChannel.configureBlocking(false); while (true) { // serverSocket.accept() SocketChannel socketChannel = serverSocketChannel.accept(); if (socketChannel!=null) { // 客戶端連接成功了,加入集合中 socketChannelList.add(socketChannel); } // 遍歷集合,看看各個客戶端是否發送了數據 for (SocketChannel channel : socketChannelList) { // 設置不阻塞,相當于設置socketInputStream.read(bytes)不會阻塞 // 不設置false,讀取數據還是阻塞的 channel.configureBlocking(false); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int read = channel.read(byteBuffer); if (read>0) { byteBuffer.flip(); System.out.println("接收客戶端數據:"+new String(byteBuffer.array())); } } } } }
- 依次啟動服務端、客戶端1、客戶端2;用客戶端2發送數據,客戶端1發送數據,觀察服務端控制臺輸出。
可以發現,解決了連接丟失的問題。
發現問題:ServerSocketChannel解決了ServerSocket阻塞的問題,使用單線程也能處理了多個連接的問題。可是,上面的代碼就可以了嗎?并沒有。在上面的代碼里,我使用了一個集合去記錄所有的客戶端連接,然后一遍遍循環集合看看客戶端是否有內容。想想假設有10000個客戶端連接了,集合里面就有10000個客戶端數據,然而只有100個客戶端會發送數據,這樣在遍歷集合的時候,其實只有100個是有效的數據,其余的都是無效的遍歷。這樣是不是也會造成資源的浪費了。
解決問題:這些問題,JDK的開發人員也幫我們解決了,于是就引出了“IO多路復用”的概念了,其中有包括“select”、“poll”、“epoll”,可以說多路復用解決的就是循環遍歷造成的資源浪費問題。關于多路復用,會另起一篇來寫的。
關于BIO到NIO就到這里了吧!
智能推薦
從IO-BIO-NIO-AIO-到Netty
文章目錄 IO 操作系統層面 IO的多路復用 epoll BIO NIO NIO單線程模型 NIO-reactor模式 AIO Netty 同步-異步-阻塞-非阻塞 IO 操作系統層面 一個應用程序進行IO時,需要系統內核的參與,發送syscall指令產生中斷。 發生中斷意味著需要操作系統介入,開展管理工作。由于操作系統的管理工作(比如切換線程、分配I/O設備等),需要使用特權指令,因此CPU要從...
Java 性能優化之——從BIO 到 NIO,再到 AIO
Netty 的高性能架構,是基于一個網絡編程設計模式 Reactor 進行設計的。現在,大多數與 I/O 相關的組件,都會使用 Reactor 模型,比如 Tomcat、Redis、Nginx 等,可見 Reactor 應用的廣泛性。 Reactor 是 NIO 的基礎。為什么 NIO 的性能就能夠比傳統的阻塞 I/O 性能高呢?首先來看一下傳統阻塞式 I/O 的一些特點 阻塞 I/...
BIO/NIO/AIO編程
BIO 編程 Blocking IO: 同步阻塞的編程方式。 BIO 編程方式通常是在 JDK1.4 版本之前常用的編程方式。編程實現過程為:首先在服務端啟動一個 ServerSocket 來監聽網絡請求,客戶端啟動 Socket 發起網絡請求,默認情況下ServerSocket 回建立一個線程來處理此請求,如果服務端沒有線程可用,客戶端則會阻塞等待或遭到拒絕。 且建立好的連接,在通訊過程中,是同...
freemarker + ItextRender 根據模板生成PDF文件
1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...
猜你喜歡
電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!
Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...
requests實現全自動PPT模板
http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...
Linux C系統編程-線程互斥鎖(四)
互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...
統計學習方法 - 樸素貝葉斯
引入問題:一機器在良好狀態生產合格產品幾率是 90%,在故障狀態生產合格產品幾率是 30%,機器良好的概率是 75%。若一日第一件產品是合格品,那么此日機器良好的概率是多少。 貝葉斯模型 生成模型與判別模型 判別模型,即要判斷這個東西到底是哪一類,也就是要求y,那就用給定的x去預測。 生成模型,是要生成一個模型,那就是誰根據什么生成了模型,誰就是類別y,根據的內容就是x 以上述例子,判斷一個生產出...