基於JAVA Socket的底層原理分析及工具實現

前言

 在工做開始以前,咱們先來了解一下Sockethtml

  所謂Socket,又被稱做套接字,它是一個抽象層,簡單來講就是存在於不一樣平臺(os)的公共接口。學過網絡的同窗能夠把它理解爲基於傳輸TCP/IP協議的進一步封裝,封裝到以致於咱們從表面上使用就像對文件流同樣的打開、讀寫和關閉等操做。此外,它是面向應用程序的,應用程序能夠經過它發送或接收數據而不用過多的顧及網絡協議。java

 那麼,Socket是存在於不一樣平臺的公共接口又是什麼意思呢?  瀏覽器

  形象的說就是「插座」,是不一樣OS之間進行通訊的一種約定或一種方式。經過 Socket 這種約定,一臺計算機能夠接收其餘計算機的數據,也能夠向其他計算機發送數據。Socket 的典型應用就是 Web 服務器和瀏覽器,瀏覽器獲取用戶輸入的 URL,經過解析出服務器的IP地址,向服務器IP發起請求,服務器分析接收到的 URL,將對應的網頁內容返回給瀏覽器,瀏覽器再通過解析和渲染,就將文字、圖片、視頻等元素呈現給用戶。服務器

 問題又來了,不經過系統之間可否進行Socket通訊呢?

  首先,咱們瞭解一下經常使用操做系統中的Socket。網絡

  在 UNIX/Linux 系統中,爲了統一對硬件的操做,簡化接口,不一樣的硬件設備都被當作一個文件。對這些文件的操做,就等同於對磁盤上普通文件的操做。socket

  你也許聽不少高手說過,UNIX/Linux 中的一切都是文件!那個傢伙說的沒錯。tcp

  學過操做系統的同窗可能知道,當對文件進行I/O操做時,系統一般會爲文件分配一個ID,也就是文件描述符。簡單來說就是系統對文件的操做轉化爲ide

對文件描述符的操做,它的背後多是一個硬盤上的普通文件、FIFO、管道、終端、鍵盤、顯示器,甚至是一個網絡鏈接。函數

  一樣的,網絡鏈接也被定義爲是一種相似的I/O操做,相似於文件,它也有文件描述符。this

  因此當咱們能夠經過 Socket來進行一次通訊時,也能夠被稱做操做網絡文件的過程。在網絡創建時,socket() 的返回值就是文件描述符。有了這個文

件描述符,咱們就可使用普通的文件操做函數來傳輸數據了,例如:

  • 用 read() 讀取從遠程計算機傳來的數據;
  • 用 write() 向遠程計算機寫入數據。

  不難發現,除了不一樣主機之間的Socket創建過程咱們還不清楚,Socket的通訊過程就是簡單的文件流處理過程。

  在Windows系統中,也有相似「文件描述符」的概念,但一般被稱爲「文件句柄」。所以,本教程若是涉及 Windows 平臺將使用「句柄」,若是涉及Linux

平臺則使用「描述符」。與UNIX/Linux 不一樣的是,Windows 會區分 socket 和文件,Windows 就把 socket 當作一個網絡鏈接來對待,所以須要調用專們

針對 socket 而設計的數據傳輸函數,針對普通文件的輸入輸出函數就無效了。

 步入正題

  說了這麼多,到底不一樣系統間不一樣定義的Socket是怎麼通訊的呢?

  在此,咱們以JAVA Socket 與 Linux Socket的關係分析爲例進行說明。首先,拿TCP Socket通訊過程來說,就是客戶端與服務器進行TCP數據交互,它分爲一下幾個步驟:

  1. 系統分配資源,服務端開啓Socket進程,對特定端口號進行監聽
  2. 客戶端針對服務端IP進行特定端口的鏈接
  3. 鏈接創建,開始通訊
  4. 通訊完成,關閉鏈接

  以TCP的通訊過程爲例,過程以下:

  具體來講,JAVA是怎樣完成對底層Linux Socket接口的調用的呢?如下圖爲例,當咱們在JAVA建立一個TCP鏈接時,須要首先實例化JAVA的ServerSocket類,其中封裝了底層的socket()方法、bind()方法、listen()方法。

其中,socket()方法是JVM對Linux API的調用,詳細以下

    1 建立socket結構體
    2 建立tcp_sock結構體,剛建立完的tcp_sock的狀態爲:TCP_CLOSE
    3 建立文件描述符與socket綁定

  bind ()方法在Linux 的底層詳細以下:

    1.將當前網絡命名空間名和端口存到bhash()

    能夠理解爲,綁定到系統可以找到的地方。

  listen()方法在Linux 的底層詳細以下:

    1.檢查偵聽端口是否存在bhash中

    2.初始化csk_accept_queue

    3.將tcp_sock指針存放到listening_hash表

    簡單來說就是驗證鏈接請求的端口是否被開啓。

  accpet()方法在Linux 的底層詳細以下:  

1.調用accept方法

2.建立socket(建立新的準備用於鏈接客戶端的socket)

3.建立文件描述符

4.阻塞式等待(csk_accept_queue)獲取sock

咱們知道在listen階段,會爲偵聽的sock初始化csk_accept_queue,此時這個queue爲空,因此accept()方法會在此時阻塞住,直到後面有客戶端成功握手後,這個queue纔有sock.若是csk_accept_queue不爲空,則返回一個sock.後續的邏輯如accept第二個圖所示,其步驟以下:

5.取出sock

6.socket與sock互相關聯

7.socket與文件描述符關聯

8.將socket返回給線程

  到此,JAVA調用Linux API的初始步驟完成。

  讓咱們來用JAVA Socket進行簡單的編碼實現。目標:

  1. 可以實現數據通訊

  2. 可以實現客戶端和服務端多對一鏈接。

  在此,以基於TCP鏈接過程爲例,完成以上編碼。服務端較爲容易實現,它只須要開啓監聽端口,等待鏈接。同時對發送和接收模塊進行封裝,以證上面所說Socket通訊類似於IO過程。

Server端代碼:

 1 package tcp_network;  2 
 3 import java.io.DataInputStream;  4 import java.io.DataOutputStream;  5 import java.io.IOException;  6 import java.net.ServerSocket;  7 import java.net.Socket;  8 
 9 public class Tcp_server { 10     public static void main(String arg[]) throws IOException { 11         System.out.print("服務端啓動.......\n"); 12         ServerSocket server = new ServerSocket(9660); //初始化一個監聽端口,讓系統分配相關socket資源 13         boolean isRunable = true; 14         while (isRunable){//循環等待鏈接的創建 15             Socket client = server.accept(); 16             System.out.print("一個客戶端創建了鏈接.......\n"); 17             new Thread(new Channel(client)).start();//每有一個通訊鏈接,將它放到新的線程中去,實現一個服務端對多個客戶端 18  } 19  server.close(); 20  } 21     public static class Channel implements Runnable{//封裝服務的類,完成接收和發送的實現 22         private Socket client; 23         private DataInputStream in_data; 24         private DataOutputStream out_data; 25         public Channel(Socket client) throws IOException { //構造函數加載,簡單初始化相關輸入輸出流 26             this.client = client; 27             in_data = new DataInputStream(client.getInputStream());//將通訊的字節流封裝爲IO的輸入輸出流 28             out_data = new DataOutputStream(client.getOutputStream()); 29  } 30         public String receive() throws IOException {//經過對輸入流 31             String data = in_data.readUTF(); 32             return data; 33  } 34         public void send(String msg) throws IOException { 35  out_data.writeUTF(msg);//將數據寫到數據流當中 36  out_data.flush();//刷新緩衝,發送數據 37  } 38         public void release() throws IOException {//鏈接結束時,釋放系統資源 39  in_data.close(); 40  out_data.close(); 41  client.close(); 42  } 43  @Override 44         public void run() { 45             try { 46  String receive_data; 47                 while (true){ 48                     receive_data = receive(); 49                     if(!receive_data.equals("")) 50  { 51                         if(receive_data.equals("Hello")) 52  { 53                             System.out.print("IP:"+client.getInetAddress()+" 客戶端信息:"+receive_data+"\n"); 54                             send("Hi"); 55  } 56                         else { 57                             System.out.print("IP:"+client.getInetAddress()+" 客戶端信息:"+receive_data+"\n"); 58  send(receive_data.toUpperCase()); 59  } 60  } 61  } 62             } catch (IOException e) { 63                 try { 64  release(); 65                 } catch (IOException ex) { 66  ex.printStackTrace(); 67  } 68  e.printStackTrace(); 69  } 70  } 71  } 72 }

Client端代碼:

package tcp_network; import java.io.*; import java.net.Socket; public class Tcp_client { public static void main(String arg[]) throws IOException { Socket client = new Socket("localhost",9660);//新建socket資源 boolean isRuning = true; while (isRuning) { //new Send(client).send(); //new Receive(client).receive();
            new Thread(new Send(client)).start();//啓動發送線程 new Thread(new Receive(client)).start();//啓動接收線程 } } public static class Send implements Runnable { private DataOutputStream out_data; private BufferedReader console; private String msg; public Send(Socket client) throws IOException { this.console = new BufferedReader(new InputStreamReader(System.in));//接收系統輸入 this.msg = init(); try { this.out_data = new DataOutputStream(client.getOutputStream());//將字符流轉化爲數據流 }catch (Exception e){ e.printStackTrace(); } } private String init() throws IOException { String msg=console.readLine(); return msg; } @Override public void run() {//在線程體內實現發送數據 try { out_data.writeUTF(msg); out_data.flush(); System.out.println("send date !"); }catch (Exception e){ e.printStackTrace(); } } } public static class Receive implements Runnable{//將接收模塊單獨封裝,目的是避免通訊時接收一直阻塞 private DataInputStream in_data; private String msg; public Receive(Socket client){ try{ in_data = new DataInputStream(client.getInputStream());//轉換流 } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { String data = null; try {//在線程中實現接收 IO緩衝區數據並輸出 data = in_data.readUTF(); } catch (IOException e) { e.printStackTrace(); } System.out.print("服務端:"+data+"\n"); } } }

 注意:在客戶端須要把發送和接收模塊放到兩個線程中去,不然會出現客戶端一直阻塞等待接收,不能進行下次發送數據的狀況(解決辦法:放到不一樣線程中接收發送可以互不影響)。

 效果以下:

    

 

 

 

  

參考:

  https://blog.csdn.net/vipshop_fin_dev/article/details/102966081

  http://c.biancheng.net/view/2128.html

相關文章
相關標籤/搜索