1. 程式人生 > 實用技巧 >socket簡單案例實現

socket簡單案例實現

socket簡單案例實現

終於還是吃了自己的狗糧......

關於 客戶端-服務端 網路模型

常規情況下,網路應用都會存在客戶端和伺服器端,比如平時外賣應用一樣,我們在外賣應用上的操作,都對應著客戶端應用想伺服器發起請求,並收到響應的過程。伺服器為客戶端提供業務資料支援,客戶端則為使用者提供互動介面。

在網路程式設計中,具體到客戶端 - 伺服器模型時,我們經常會考慮是使用TCP還是UDP,其實它們二者的區別也很簡單:在TCP中連線是誰發起的,在UDP中報文是誰傳送的。在TCP中,建立連線是一個非常重要的環節。區別出客戶端和伺服器,本質上是因為二者程式設計模型是的不同的。

伺服器端需要在一開始就監聽在一個確定的埠上,等待客戶端傳送請求,一旦有客戶端建立連線,伺服器端則會消耗一定的計算機資源為它服務。

客戶端相對簡單,它向伺服器的監聽埠發起請求,連線建立之後,通過連線通路和伺服器端進行通訊。

還有一點要強調的是,無論是客戶端還是伺服器端,它們執行的基本單位都是程序(Process),而不是機器。一個客戶端,可以同時建立多個到不同伺服器的連線;而伺服器更是可能在一臺機器上部署執行多個服務。

什麼是 socket

socket是一種作業系統提供的程序間通訊機制。這裡並不侷限於本地,可以是本地程序間的通訊,也可以是遠端程序間的通訊。在作業系統中,通常會為應用程式提供一組應用程式介面(API),稱為套接字介面(socket API)。應用程式可以通過套接字介面來使用套接字(socket),已進行資料交換。

這裡要注意一下,我們常說的TCPUDP只是傳輸層協議,是一種約定。TCP三次握手則是基於TCP協議建立網路通路,該通路的具體建立與實現還是socket完成。socket是我們用來建立連線、傳輸資料的唯一途徑。

如何使用 socket 建立連線

通過前面的客戶端 - 伺服器模型,我們知道至少需要一對套接字才能進行網路連線的建立,它們分別是服務端套接字和客戶端套接字,這裡我們先從服務端說起。

服務端準備連線過程

  • 建立套接字(我們這裡會使用 TCP的實現)
  • 繫結監聽地址:即為繫結需要監聽的 IP地址以及 埠號,這裡也可以使用本機 IP,但是考慮到部署環境 IP可能會發生變化,所以這裡需要進行 IP地址
    的繫結(比如進行通配地址指定,或者主機存在多張網絡卡時指定具體的 IP)。如果不顯式的指定埠號,就意味著把埠的選擇權交給作業系統核心來處理,作業系統核心會根據一定的演算法選擇一個空閒的埠,完成套接字的繫結。
  • 開啟套接字監聽模式:bind函式只是實現套接字與地址的關聯,如同登記了電話號碼,如果要讓別人打通帶年華,還需要我們把電話裝置接入電話線,讓伺服器真正處於可接聽的狀態,這個過程需要依賴 listen函式。這裡可以這麼理解,socket存在 主動被動模式,比如伺服器就是處於 被動模式下,它需要等待客戶端套接字的 主動連線。而 listen函式便是可以將套接字設定為 被動模式,即告訴核心:“我這個套接字是用來等待使用者請求的”。
  • 建立連線(accept阻塞):在客戶端連線請求到達時,服務端應答成功,便完成連線建立。
package com.zhoujian.socket;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/** 
  * 執行緒池工具類 
  * @author zhoujian 
  */
public class ExecutorServicePool {   

    /**     
      * 初始化執行緒池    
      */    
    public static ExecutorService executorService = Executors.newFixedThreadPool(10);

}

package com.zhoujian.socket.server;

import com.zhoujian.socket.ExecutorServicePool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/** 
  * Socket服務端示例 
  * @author zhoujian 
  */
public class SocketServer {    

    /**     
      * 監聽埠     
      */    
    private static int PORT = 8088;   
    private ServerSocket serverSocket;    
    private static Logger logger = LoggerFactory.getLogger(SocketServer.class);    
  
    public static void main(String[] args) {        
        try {            
            new SocketServer().startUp(PORT);       
        } catch (IOException e) {            
            e.printStackTrace();        
        }    
    }    
  
    private void startUp(int port) throws IOException {        
        /**         
          * 在初始化的過程中先後完成了監聽地址繫結和 listen 函式呼叫        
          */        
        serverSocket = new ServerSocket(port);       
        logger.info("Socket Server is online, listening at port {}", PORT);        
        while (true){            
            /**             
              * 此處阻塞,等待客戶端連線,在三次握手成功完成後,釋放阻塞            
              */            
            Socket socket = serverSocket.accept();           
            logger.info("socket port is {} connect successful", socket.getPort());           
            ExecutorServicePool.executorService.execute(new AnswerThread(socket));        
        }    
    }    
  
    /**     
      * 應答執行緒     
      */    
    static class AnswerThread implements Runnable {        
  
        private Socket socket;        
  
        public AnswerThread(Socket socket){            
            this.socket = socket;        
        }        
  
        @Override        
        public void run() {            
            String content = null;            
            BufferedReader bufferedReader = null;            
            try {                
                bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));                
                /**                 
                  * 這裡的判斷條件就是資料傳送完畢的標識                 
                  */                
                while ((content = bufferedReader.readLine()) != null){                     
                    logger.info("form client: {}", content);           
                    socket.getOutputStream().write(content.getBytes());         
                    socket.getOutputStream().write("\n".getBytes());           
                    socket.getOutputStream().flush();               
                }           
            } catch (IOException e) {              
                e.printStackTrace();            
            }        
        }    
    }
}

客戶端發起連線過程

  • 建立套接字
  • 使用 connect發起連線:呼叫 connect函式向服務端發起連線請求,這裡傳入的是服務端套接字的地址,connect函式可以看做是將套接字轉換為 主動模式。這裡值得注意的是,客戶端在呼叫 connect函式前不是非得呼叫 bind函式,因為如果需要(TCP|UDP|本地 套接字)的話,作業系統核心會確定源IP地址,並按照一定的演算法選擇一個臨時埠作為源埠(這裡是客戶端,服務端應當先完成地址繫結,因為需要一個穩定的地址進行標記,客戶端大可不必,還可以減小埠衝突的可能)。在這裡我們使用的是 TCP套接字,在呼叫 connect函式時將激發 TCP的三次握手,貼圖如下(圖片來自於 極客時間),這裡務必注意阻塞狀態的改變情況:

注意:Read方法也是阻塞的,當呼叫Read方法是,它就會一直阻塞在那裡,直到另一方告訴它資料已經發送完畢(一般情況下,都會使用長度進行控制,這裡是採用`\n`來作為資料完結髮送的標識)

package com.zhoujian.socket.client;

import com.zhoujian.socket.ExecutorServicePool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.Socket;

/** 
  * Socket客戶端示例 
  * @author zhoujian 
  */
public class SocketClient {    

    /**     
      * 服務端套接字IP地址     
      */    
    private static String HOST = "127.0.0.1";
  
    /**     
      * 服務端套接字監聽埠     
      */    
    private static int PORT = 8088;    
    private static Logger logger = LoggerFactory.getLogger(SocketClient.class);   
  
    /**     
      * 於主執行緒中初始化客戶端套接字,並完成與服務端套接字的連線     
      * @param args     
      * @throws IOException     
      */   
    public static void main(String[] args) throws IOException {        
        Socket client = new Socket(HOST, PORT);        
        ExecutorServicePool.executorService.execute(new ReceiveThread(client));        
        BufferedReader reader = new BufferedReader(new 
            InputStreamReader(System.in, "UTF-8"));        
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));        
            while (true){            
                String msg = reader.readLine();            
                writer.write(msg);            
                writer.write("\n");            
                writer.flush();       
            }    
        }   
  
        /**     
          * 用於接收服務端套接字的應答     
          */    
        static class ReceiveThread implements Runnable{        
  
            private Socket socket;       
  
            public ReceiveThread(Socket socket){           
                this.socket = socket;       
            }        
  
            @Override       
            public void run() {            
                String receive = null;            
                BufferedReader bufferedReader = null;            
                try {               
                    bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));                
                while ((receive = bufferedReader.readLine()) != null){                    
                    logger.info("from server: {}", receive);               
                }            
            } catch (IOException e) {                
                e.printStackTrace();           
            } 
        }
    }
}

執行截圖

服務端套接字啟動

客戶端A套接字啟動

客戶端B套接字啟動

客戶端A套接字與服務端套接字通訊

客戶端B套接字與服務端套接字通訊

拓展

待更新......

引用

網路程式設計模型:認識客戶端-伺服器網路模型的基本概念

套接字和地址:像電話和電話號碼一樣理解它們

TCP三次握手:怎麼使用套接字格式建立連線

使用套接字進行讀寫:開始交流吧

Java socket詳解,看這一篇就夠了

說明

本節內容涉及的完整程式碼地址:socket-example

本文內容大部分源自極客時間以及網路部落格圖文內容節選,如有冒犯,還請告知我進行處理
郵箱:[email protected]