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