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


前言

 在工作開始之前,我們先來了解一下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數據交互,它分為一下幾個步驟:

  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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM