摘要:綁定完成后允許套接字進行連接并等待連接。服務端根據報文返回響應,并關閉連接。單線程服務器多進程及多線程服務器復用服務器復用的多線程服務器單線程服務器一次只處理一個請求,直到其完成為止。
前言
本篇文章將涉及以下內容:
IO實現Java Socket通信
NIO實現Java Socket通信
閱讀本文之前最好了解過:
Java IO
Java NIO
Java Concurrency
TCP/IP協議
TCP 套接字TCP套接字是指IP號+端口號來識別一個應用程序,從而實現端到端的通訊。其實一個套接字也可以被多個應用程序使用,但是通常來說承載的是一個應用程序的流量。建立在TCP連接之上最著名的協議為HTTP,我們日常生活中使用的瀏覽器訪問網頁通常都是使用HTTP協議來實現的。
先來了解一下通過TCP套接字實現客戶端和服務器端的通信。
在TCP客戶端發出請求之前,服務器會創建新的套接字(socket),并將套接字綁定到某個端口上去(bind),默認情況下HTTP服務的端口號為80。綁定完成后允許套接字進行連接(listen)并等待連接(accept)。這里的accept方法會掛起當前的進程直到有Socket連接。
在服務器準備就緒后,客戶端就可以發起Socket連接。客戶端獲取服務器的Socket套接字(IP號:端口號),并新建一個本地的套接字。然后連同本地的套接字發送到服務器上。
服務器accept該請求并讀取該請求。這里面包括有TCP的三次連接過程。連接建立之后,客戶端發送HTTP請求并等待響應。服務端根據HTTP報文返回響應,并關閉連接。
Web Server當下的Web服務器能夠同時支持數千條連接,一個客戶端可能向服務器打開一條或多條連接,這些連接的使用狀態各不相同,使用率也差異很大。如何有效的利用服務器資源提供低延時的服務成了每個服務器都需要考慮的問題。根據服務器的處理方式,可以分為以下4種服務器,我們也將分別對其進行簡單的實現。
單線程服務器
多進程及多線程服務器
復用IO服務器
復用的多線程服務器
單線程服務器一次只處理一個請求,直到其完成為止。一個事務處理結束后,才會去處理下一條連接。實現簡單,但是性能堪憂。
多進程及多線程服務器可以根據需要創建,或預先創建一下線程/進程。可以為每條連接分配一個線程/進程。但是當強求數量過多時,過多的線程會導致內存和系統資源的浪費。
復用I/O服務器在復用結構中,會同時監視所有連接上的活動,當連接狀態發生變化時,就對那條連接進行少量的處理。處理結束后,就將連接返回到開放連接列表中,等待下一次狀態的變化。之后在有事情可做時才會對連接進行處理。在空閑連接上等待的時候不會綁定線程和進程。
復用的多線程服務器多個線程(對應多個CPU)中的每一個都在觀察打開的連接(或是打開連接中的一個子集)。并對每條連接的狀態變化時執行任務。
Socket通信基本實現根據我們上面講述的Socket通信的步驟,在Java中我們可以按照以下方式逐步建立連接:
首先開啟服務器端的SocketServer并且將其綁定到一個端口等待Socket連接:
ServerSocket serverSocket = new ServerSocket(PORT_ID:int); Socket socket = serverSocket.accept();
當沒有Socket連接時,服務器會在accept方法處阻塞。
然后我們在客戶端新建一個Socket套接字并且連接服務器:
Socket socket = new Socket(SERVER_SOCKET_IP, SERVER_SOCKET_PORT); socket.setSoTimeout(100000);
如果連接失敗的話,將會拋出異常說明服務器當前不可以使用。
連接成功給的話,客戶端就可以獲取Socket的輸入流和輸出流并發送消息。寫入Socket的輸出流的信息將會先存儲在客戶端本地的緩存隊列中,滿足一定條件后會flush到服務器的輸入流。服務器獲取輸入后可以解析輸入的數據,并且將響應內容寫入服務器的輸出流并返回客戶端。最后客戶端從輸入流讀取數據。
客戶端獲取Socket輸入輸出流,這里將字節流封裝為字符流。
//獲取Socket的輸出流,用來發送數據到服務端 PrintStream out = new PrintStream(socket.getOutputStream()); //獲取Socket的輸入流,用來接收從服務端發送過來的數據 BufferedReader buf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
客戶端發送數據并等待響應
String str = "hello world"; out.println(str); String echo = buf.readLine(); System.out.println("收到消息:" + echo);
這里需要注意的是,IO流是阻塞式IO,因此在讀取服務端響應的過程中(即buf.reaLine()這一行)會阻塞直到收到服務器響應。
客戶端發送結束之后不要忘了關閉IO和Socket通信。
out.close(); buf.close(); socket.close();
服務器對消息的處理和客戶端類似,后面會貼上完整代碼。
Java Socket通信阻塞式通信實現這里我們對上述的理論進行簡單的實現。這里我們實現一個簡單的聊天室,只不過其中一方是Server角色而另一個為Client角色。二者都通過System.in流輸入數據,并發送給對方。正如我們前面所說,IO流的通信是阻塞式的,因此在等待對方響應的過程中,進程將會掛起,我們這時候輸入的數據將要等到下一輪會話中才能被讀取。
client端
import java.io.*; import java.net.Socket; import java.net.SocketTimeoutException; public class SocketClient { public static void send(String server, int port){ try { Socket socket = new Socket(server, port); socket.setSoTimeout(100000); System.out.println("正在連接服務器"); //從控制臺讀入數據 BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); //獲取Socket的輸出流,用來發送數據到服務端 PrintStream out = new PrintStream(socket.getOutputStream()); //獲取Socket的輸入流,用來接收從服務端發送過來的數據 BufferedReader buf = new BufferedReader(new InputStreamReader(socket.getInputStream())); boolean running = true; while(running){ System.out.print("輸入信息:"); String str = input.readLine(); out.println(str); if("bye".equals(str)){ running = false; }else{ try{ //從服務器端接收數據有個時間限制(系統自設,也可以自己設置),超過了這個時間,便會拋出該異常 String echo = buf.readLine(); System.out.println("收到消息:" + echo); }catch(SocketTimeoutException e){ System.out.println("Time out, No response"); } } } input.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } finally { } } public static void main(String[] args){ send("127.0.0.1", 2048); } }
Server端
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String[] args) throws IOException { //服務端在2048端口監聽客戶端請求的TCP連接 ServerSocket server = new ServerSocket(2048); Socket client = null; boolean f = true; while(f){ //等待客戶端的連接,如果沒有獲取連接 client = server.accept(); System.out.println("與客戶端連接成功!"); //為每個客戶端連接開啟一個線程 new Thread(new ServerThread(client)).start(); } server.close(); } }
服務器處理數據
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; public class ServerThread implements Runnable{ private Socket client = null; public ServerThread(Socket client){ this.client = client; } @Override public void run() { try{ //獲取Socket的輸出流,用來向客戶端發送數據 PrintStream out = new PrintStream(client.getOutputStream()); //獲取Socket的輸入流,用來接收從客戶端發送過來的數據 BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream())); BufferedReader serverResponse = new BufferedReader(new InputStreamReader(System.in)); boolean flag =true; while(flag){ //接收從客戶端發送過來的數據 String str = buf.readLine(); System.out.println("收到消息:" + str); if(str == null || "".equals(str)){ flag = false; }else{ if("bye".equals(str)){ flag = false; }else{ //將接收到的字符串前面加上echo,發送到對應的客戶端 System.out.print("發送回復:"); String response = serverResponse.readLine(); out.println(response); } } } out.close(); client.close(); }catch(Exception e){ e.printStackTrace(); } } }
可以和小伙伴試試看,分別啟動SocketServer和SocketClient并進行通信。不過前提是你們兩個需要在一個局域網中。
Java實現單線程服務器上面的服務器其實只在主線程監聽了一個Socket連接,并在30秒之后將其自動關閉了。我們將實現一個經典的單線程服務器。原理和上面相似,這里我們可以直接通過向服務器發送HTTP請求來驗證該服務器的運行。
import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class SingleThreadServer implements Runnable{ private ServerSocket serverSocket; public SingleThreadServer(ServerSocket serverSocket){ this.serverSocket = serverSocket; } @Override public void run() { Socket socket = null; try{ while (!Thread.interrupted()){ socket = serverSocket.accept(); //谷歌瀏覽器每次會發送兩個請求 //一次用于獲取html //一次用于獲取favicon //如果獲取favicon成功就緩存,否則會一直請求獲得favicon //而火狐瀏覽器第一次也會發出這兩個請求 //在獲得favicon失敗后就不會繼續嘗試獲取favicon //因此使用谷歌瀏覽器訪問該Server的話,你會看到 連接成功 被打印兩次 System.out.println("連接成功"); process(socket); } } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } private void process(Socket socket){ try { InputStreamReader inputStreamReader = null; BufferedOutputStream bufferedOutputStream = null; try{ inputStreamReader = new InputStreamReader(socket.getInputStream()); bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream()); //這里無法正常讀取輸入流,因為在沒有遇到EOF之前,流會任務socket輸入尚未結束,將會繼續等待直到socket中斷 //所以這里我們將暫時不讀取Socket的輸入流中的內容。 //int size; //char[] buffer = new char[1024]; //StringBuilder stringBuilder = new StringBuilder(); //while ((size = inputStreamReader.read(buffer)) > 0){ // stringBuilder.append(buffer, 0, size); //} byte[] responseDocument = " Hello World ".getBytes("UTF-8"); byte[] responseHeader = ("HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: " + responseDocument.length + " ").getBytes("UTF-8"); bufferedOutputStream.write(responseHeader); bufferedOutputStream.write(responseDocument); }finally { bufferedOutputStream.close(); } } catch (IOException e) { e.printStackTrace(); } }
該服務器用單一線程處理每次請求,每個線程都將等待服務器處理完上一個請求之后才能獲得響應。這里需要注意,純HTTP請求的輸入流的讀取會遇到輸入流阻塞的問題,因為HTTP請求并沒有輸入流可識別的EOF標記。從而導致服務器一直掛起在讀取輸入流的地方。它的解決方法如下:
客戶端關閉Socket連接,強制服務器關閉該Socket連接。但是同時也丟失服務器響應
自定義協議,從而服務器可以識別數據的終點。
啟動服務器
public static void main(String[] args) throws IOException, InterruptedException { ExecutorService executorService = Executors.newSingleThreadExecutor(); ServerSocket serverSocket = new ServerSocket(2048); executorService.execute(new SingleThreadServer(serverSocket)); // TimeUnit.SECONDS.sleep(10); // System.out.println("shut down server"); // executorService.shutdownNow(); }
注意要先關閉之前占用2048端口號的服務器。
我們也可以使用代碼來測試:
import java.io.*; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class TestSingleThreadServer { public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0 ; i<10 ; i++){ final int threadId = i; executorService.execute(() ->{ try { Socket socket = new Socket("127.0.0.1", 20006); socket.setSoTimeout(5000); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String line = bufferedReader.readLine(); System.out.println(threadId + ":" + line); socket.close();; } catch (IOException e) { e.printStackTrace(); } }); } TimeUnit.SECONDS.sleep(40); executorService.shutdownNow(); } }Java實現多線程服務器
這里我們將為每一個Socket連接提供一個線程來處理。基本實現和上面差不多,只是將每一個Socket連接丟給一個額外的線程來處理。這里可以參考前面的簡易聊天室來試著自己實現以下。
Java NIO實現復用服務器NIO的出現改變了舊式Java讀取IO流的方式。首先,它支持非阻塞式讀取,其次它可以使用一個線程來管理多個信道。多線程表面上看起來可以同時處理多個Socket通信,但是多線程的管理本身也消耗相當多的資源。其次,很多信道的使用率往往并不高,一些信道往往并不是連通狀態中。如果我們可以將資源直接賦予當前活躍的Socket通信的話,可以明顯的提高資源利用率。
先附上參考資料將在后序更新。
參考書籍HTTP權威指南
Java TCP/IP Socket 編程
Java Multithread servers
Java NIO ServerSocketChannel
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注我的微信公眾號!將會不定期的發放福利哦~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68829.html
摘要:而用于主線程池的屬性都定義在中本篇只是簡單介紹了一下引導類的配置屬性,下一篇我將詳細介紹服務端引導類的過程分析。 從Java1.4開始, Java引入了non-blocking IO,簡稱NIO。NIO與傳統socket最大的不同就是引入了Channel和多路復用selector的概念。傳統的socket是基于stream的,它是單向的,有InputStream表示read和Outpu...
摘要:關閉套接字和上下文備注說明如何利用使用首先下載所需的包,解壓以后將和文件放到自己電腦中的安裝路徑中的文件夾下,最后需要將之前解壓后的包放在項目的中或者資源下載鏈接密碼項目源碼下載鏈接鏈接密碼 在講ZeroMQ前先給大家講一下什么是消息隊列。 消息隊列簡介: 消息隊列中間件是分布式系統中重要的組件,主要解決應用耦合,異步消息,流量削鋒等問題。實現高性能,高可用,可伸縮和最終一致性架構。是...
摘要:流控制通常就是在客戶端的頁面使用一個隱藏的窗口向服務端發出一個長連接的請求。和長鏈接以上幾種服務器推的技術中長輪詢和流控制其實都是基于長鏈接來實現的,也就是中所謂的。通信協議于年被定為標準,并被所補充規范。 初探WebSocket node websocket socket.io 我們平常開發的大部分web頁面都是主動‘拉’的形式,如果需要更新頁面內容,則需要刷新一個,但Slack工...
閱讀 841·2021-11-16 11:56
閱讀 1654·2021-11-16 11:45
閱讀 3109·2021-10-08 10:13
閱讀 4094·2021-09-22 15:27
閱讀 727·2019-08-30 11:03
閱讀 643·2019-08-30 10:56
閱讀 946·2019-08-29 15:18
閱讀 1737·2019-08-29 14:05