網路程式設計之socket

DaFanJoy發表於2023-10-03

在之前的文章《網路程式設計雜談之TCP協議》中,我們闡述了TCP協議的基本概念,TCP作為一種可靠的、面向連線的資料傳輸協議,確保了資料在傳送和接收之間的可靠性、順序性和完整性,特點可以概括如下:

1、面向連線:在進行資料傳輸之前,TCP需要客戶端和伺服器之間建立一個連線,這個連線包括一系列的握手和協商步驟,以確保通訊雙方都準備好進行資料傳輸。

2、可靠性:TCP是一種可靠的協議,它使用各種機制來確保資料的可靠傳輸,包括資料分段的確認和重傳機制,以及流量控制等多種手段。

3、順序性:TCP保證資料段的到達順序與傳送順序相同,即使資料在傳輸過程中被拆分成多個資料包,接收方也會將它們按照正確的順序重新組裝,比如說連結的一方發了ABC,那麼接收的一方收到的也一定是ABC。

4、流量控制:TCP使用滑動視窗協議來實現流量控制,確保了傳送方不會以超過接收方處理能力的速度傳送資料,從而避免了資料丟失和網路擁塞。

5、擁塞控制:TCP還具有擁塞控制機制,它可以檢測到網路中的擁塞並採取相應的措施來減輕擁塞,從而實現降低傳送速率和重新傳送丟失的資料包。

6、面向位元組流:TCP是面向位元組流的協議,它不會保留訊息邊界。這意味著接收方需要自行解析和分割接收到的位元組流,以還原原始訊息。

7、可靠的錯誤檢測和糾正:TCP具有強大的錯誤檢測和糾正機制,它可以檢測並糾正在資料傳輸過程中出現的錯誤,以確保資料的完整性。

8、全雙工通訊:TCP支援全雙工通訊,所謂全雙工是指建立連線後,通訊雙方可以同時傳送和接收資料,而不需要等待對方的響應。

9、Socket(套接字):TCP使用埠號來標識不同的應用程式或服務,通訊的兩端透過IP地址和埠號來建立連線,而套接字(Socket)就是對其中任意一端的抽象,分為伺服器端套接字(Server Socket)或客戶端套接字(Client Socket),分別用於伺服器和客戶端的通訊。

總的來說,TCP協議作用於傳輸層且適用於大多數需要可靠資料傳輸的應用程式,如檔案傳輸、上位機通訊等,並可以做為其他應用層協議的實現基礎,如HTTP、MQTT等。

在程式碼實現層面,Socket是指一種程式設計介面(API),不同開發語言基本上都圍繞Socket提供了一組用於建立、連線、傳送和接收資料的API。下面我們以Java為例,透過java.net包下提供的Socket操作API與 java.io包下提供的IO操作API, 實現一個基本的TCP服務端與客戶端的監聽、連結並進行訊息收發的示例。

服務端

在TCP服務端的實現中我們需要先定義一個ServerSocket物件,並實現對指定IP與埠號的繫結與監聽,這裡需要注意的是如果一直沒有客戶端連結,serverSocket.accept會一直處在阻塞狀態,一旦有客戶端連結事件發生才會向下執行,而服務需要滿足與多個客戶端進行連結,這也是為什麼我們需要一個while (true)去一直輪詢執行,因為我們其實是不知道客戶端什麼時候會連結上來的,下面的讀寫操作也是同一個道理, 所以為了不影響主執行緒accept新的客戶端,我們把完成連結的Socket開啟一個獨立的執行緒或者拋給執行緒池來處理後續IO讀寫操作,這是一種典型的BIO(Blocking I/O)即阻塞IO的處理模式。

public class BioServer {  
      
    public static void main(String[] args) throws IOException{  
         
       ExecutorService executor = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),  
             new ThreadPoolExecutor.CallerRunsPolicy());  
       ServerSocket serverSocket = new ServerSocket();  
       serverSocket.bind(new InetSocketAddress("127.0.0.1",9091));//繫結IP地址與埠,定義一個服務端socket,開啟監聽  
       while (true) {  
          Socket socket = serverSocket.accept();//這裡如果沒有客戶端連結,會一直阻塞等待,一旦有客戶端連結就會向下執行
          executor.execute(new BioServerHandler(socket)); //我們把連結Socket拋給執行緒池
            
       }  
    }  
}

讀寫操作是透過Socket獲取InputStream與OutputStream來完成的,這裡的Input與Output是站在你程式的視角來區分的,所以Input是收,Output是發,同理inputStream.read作為IO讀操作也是阻塞的,程式只有接受到資料時才會向下執行,由於我們不確定Socket連結的讀寫操作何時發生,也只能依靠 while (true)輪詢執行。

public class BioServerHandler implements Runnable{

    private final Socket socket;
    
    public BioServerHandler(Socket socket) {
       this.socket=socket;
    }
    
    @Override
    public void run() {
       // TODO Auto-generated method stub
       try {
          
          while (true) {
             byte[] rbytes = new byte[1024];
             InputStream inputStream =  socket.getInputStream(); //透過IO輸入流接受訊息
             int rlength=inputStream.read(rbytes, 0, 1024); //讀操作阻塞,一旦接受到資料向下執行並返回接收到的資料長度
             byte[] bytes = new byte[rlength];
             System.arraycopy(rbytes, 0, bytes, 0, rlength);
             String message = new String(bytes);
             System.out.printf("Client: %s%n", message);

             PrintStream writer = new PrintStream(socket.getOutputStream()); //透過IO輸出流傳送訊息
             writer.println("Hello BIO Client");
          }

          
       } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
       }
       
    }

}

客戶端

public class BioClient {  
    public static void main(String[] args) throws IOException, InterruptedException {  
        Socket socket = new Socket();  
        socket.connect(new InetSocketAddress("127.0.0.1", 9091));  
  
        while (true) {  
            if (!socket.isConnected()) {  
                System.out.print("connecting...");  
                continue;  
            }  
  
            PrintStream writer = new PrintStream(socket.getOutputStream());  
            writer.write("Hello BIO Server".getBytes(StandardCharsets.UTF_8));  
  
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));  
            String message = reader.readLine();  
            System.out.printf("Server: %s%n", message);  
        }  
  
  
    }  
}

透過上面程式碼大家能夠對Java下TCP網路程式設計的開發、Socket的操作、BIO(阻塞IO)模型有了基本的瞭解,當然一個完整的TCP服務或客戶端開發需要考慮的問題還有很多,如IO與執行緒模型、協議的制定、連結的管理、應用層報文粘包、半包等等,後續我們會在此基礎上進行進一步的擴充套件與完善。







關注微信公眾號,檢視更多技術文章。

相關文章