在工做開始以前,咱們先來了解一下Sockethtml
所謂Socket,又被稱做套接字,它是一個抽象層,簡單來講就是存在於不一樣平臺(os)的公共接口。學過網絡的同窗能夠把它理解爲基於傳輸TCP/IP協議的進一步封裝,封裝到以致於咱們從表面上使用就像對文件流同樣的打開、讀寫和關閉等操做。此外,它是面向應用程序的,應用程序能夠經過它發送或接收數據而不用過多的顧及網絡協議。java
那麼,Socket是存在於不一樣平臺的公共接口又是什麼意思呢? 瀏覽器
形象的說就是「插座」,是不一樣OS之間進行通訊的一種約定或一種方式。經過 Socket 這種約定,一臺計算機能夠接收其餘計算機的數據,也能夠向其他計算機發送數據。Socket 的典型應用就是 Web 服務器和瀏覽器,瀏覽器獲取用戶輸入的 URL,經過解析出服務器的IP地址,向服務器IP發起請求,服務器分析接收到的 URL,將對應的網頁內容返回給瀏覽器,瀏覽器再通過解析和渲染,就將文字、圖片、視頻等元素呈現給用戶。服務器
首先,咱們瞭解一下經常使用操做系統中的Socket。網絡
在 UNIX/Linux 系統中,爲了統一對硬件的操做,簡化接口,不一樣的硬件設備都被當作一個文件。對這些文件的操做,就等同於對磁盤上普通文件的操做。socket
你也許聽不少高手說過,UNIX/Linux 中的一切都是文件!那個傢伙說的沒錯。tcp
學過操做系統的同窗可能知道,當對文件進行I/O操做時,系統一般會爲文件分配一個ID,也就是文件描述符。簡單來說就是系統對文件的操做轉化爲ide
對文件描述符的操做,它的背後多是一個硬盤上的普通文件、FIFO、管道、終端、鍵盤、顯示器,甚至是一個網絡鏈接。函數
一樣的,網絡鏈接也被定義爲是一種相似的I/O操做,相似於文件,它也有文件描述符。this
因此當咱們能夠經過 Socket來進行一次通訊時,也能夠被稱做操做網絡文件的過程。在網絡創建時,socket() 的返回值就是文件描述符。有了這個文
件描述符,咱們就可使用普通的文件操做函數來傳輸數據了,例如:
不難發現,除了不一樣主機之間的Socket創建過程咱們還不清楚,Socket的通訊過程就是簡單的文件流處理過程。
在Windows系統中,也有相似「文件描述符」的概念,但一般被稱爲「文件句柄」。所以,本教程若是涉及 Windows 平臺將使用「句柄」,若是涉及Linux
平臺則使用「描述符」。與UNIX/Linux 不一樣的是,Windows 會區分 socket 和文件,Windows 就把 socket 當作一個網絡鏈接來對待,所以須要調用專們
針對 socket 而設計的數據傳輸函數,針對普通文件的輸入輸出函數就無效了。
說了這麼多,到底不一樣系統間不一樣定義的Socket是怎麼通訊的呢?
在此,咱們以JAVA Socket 與 Linux Socket的關係分析爲例進行說明。首先,拿TCP Socket通訊過程來說,就是客戶端與服務器進行TCP數據交互,它分爲一下幾個步驟:
以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