【學習筆記】【Java網絡編程】第九章 服務器Socket


服務器Socket在服務器上運行,監聽入站TCP連接。每個服務器Socket監聽服務器機器上的一個特定端口。一旦ServerSocket建立了連接,服務器會使用一個常規的Socket對象向客戶端發送數據。

使用ServerSocket

  1. 使用一個ServerSocket()構造函數在一個特定端口創建一個新的ServerSocket。
  2. ServerSocket使用其accept()方法監聽這個端口的入站連接,accept()會一直阻塞直到一個客戶端嘗試建立連接,此時會返回一個連接客戶端和服務器的Socket對象。
  3. 根據服務器的類型,會調用Socket的getInputStream()方法或getOutputStream()方法,或者兩個方法都調用,以獲得與客戶端通信的輸入和輸出流。
  4. 服務器和客戶端根據已協商的協議交互,直到要關閉連接。
  5. 服務器或客戶端(或二者)關閉連接。
  6. 服務器返回步驟2,等待下一次連接。
public class DaytimeServer {

    private final static int PORT = 13;
    
    public static void main(String[] args) {
        try (ServerSocket server = new ServerSocket(PORT)) { // 在指定端口上構造ServerSocket
            while(true) {
                try (Socket connection = server.accept()) { // 阻塞線程直到監聽到入站信息
                    Writer out = new OutputStreamWriter(connection.getOutputStream());
                    Date now = new Date();
                    out.write(now.toString() + "\r\n");
                    out.flush();
                    connection.close(); // 結束處理時一定要關閉Socket
                } catch (IOException ignored) {} // 監視接受和處理連接時可能拋出的任何異常
            }
        } catch (IOException e) { // 捕獲在PORT端口上構造ServerSocket時可能產生的任何異常
            System.err.println(e);
        }
    }
}

提供二進制數據

  • 發送二進制非文本數據需要使用一個寫byte數據的OutputStream,而不是寫String的Writer。

多線程服務器

  • 操作系統將指向某個特定端口的入站連接請求存儲在一個先進先出的隊列中。隊列中填入的未處理連接達到其容量時,主機會拒絕這個端口上的額外的連接。
  • 每個連接對應一個線程:爲每個連接提供一個線程,與接受入站連接放入隊列的那個線程分開。大量同時入站的連接可能會導致崩潰。
  • 使用線程池:可能會拒絕連接,但不會崩潰。
public class PooledDaytimeServer {

    private final static int PORT = 13;
    
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(50); // 建立線程池,容量爲50
        try(ServerSocket server = new ServerSocket(PORT);) {
            while(true) {
                try {
                    Socket connection = server.accept();
                    Callable<Void> task = new DaytimeTask(connection); // 構造Callable
                    pool.submit(task); // 提交給線程池請求分配線程
                } catch (IOException ignored) {}
            }
        } catch (IOException ex) {
            System.err.println(ex);
        }
    }
    
    private static class DaytimeTask implements Callable<Void> {
    
        private Socket connection;
        
        DaytimeTask(Socket connection) {
            this.connection = connection;
        }
        
        public Void call() { // 實現Callable接口要重寫call()方法
            try {
                Writer out = new OutputStreamWriter(connection.getOutputStream());
                Date now = new Date();
                out.write(now.toString() + "\r\n");
                out.flush();
            } catch (IOException ex) {
                System.err.println(ex);
            } finally {
                try {
                    connection.close();
                } catch (IOException ignored) {}
            }
            return null;
        }
    }
}

用Socket寫入服務器

  • 理解協議,明確何時寫入和何時讀取。

關閉服務器Socket

  • 關閉ServerSocket會釋放本地主機的一個端口,允許另外一個服務器綁定到這個端口。還會中斷該ServerSocket已經接受的目前處於打開狀態的所有Socket。
  • public boolean isClosed():ServerSocket已經關閉則返回true。
  • public boolean isBound():ServerSocket曾經綁定到一個端口則返回true,即使現在已經關閉。
  • isBound() && !isClosed():測試ServerSocket是否已經打開。

日誌

日誌記錄內容

  • 請求:對應服務器建立的每一個連接都包含一個記錄。
  • 服務器錯誤:發生的所有NullPointException都要記錄在這個日誌文件中,因爲它指示了要修復的錯誤。

如果記錄日誌

  • 爲每個類創建一個日誌工具(線程安全類):private final static Logger auditLogger = Logger.getLogger("requests);
  • 多個Logger對象可以輸出到同一個日誌。
  • 日誌級別(從高到低):Level.SEVERE(用於錯誤日誌)、Level.WARNING(用於錯誤日誌)、Level.INFO(用於審計日誌)、Level.CONFIG、Level.FINE、Level.FINER、Level.FINEST。低級別日誌中只用於調試。
  • 每個記錄應包含一個時間戳、一個客戶端地址以及所處理的請求的任何特定消息。

構造服務器Socket

public ServerSocket(int port) throws BindException, IOException // 指定端口
public ServerSocket(int port, int queueLength) throws BindException, IOException // 指定入站連接請求所用的隊列長度
public ServerSocket(int port, int queueLength, InetAddress bindAddress) throws BindException, IOException // 要綁定的本地網絡接口
public ServerSocket() throws BindException, IOException
  • 端口號傳入0,系統會自動選擇可用的端口。

構造但不綁定端口

  • public void bind(SocketAddress endpoint) throws IOException:將ServerSocket對象綁定到某個端口,用於在綁定端口之前設置服務器Socket選項,傳入null表示由系統選擇可用的端口。
  • public void bind(SocketAddress endpoint, int queueLength) throws IOException

獲得服務器Socket的有關信息

  • public InetAddress getInetAddress():返回服務器使用的地址,沒有綁定網絡接口時返回null。
  • public int getLocalPort():返回監聽端口,沒有綁定端口時返回-1。
  • public String toString():ServerSocket[addr=0.0.0.0,port=0,localport=xxxx];

Socket選項

SO_TIMEOUT

  • accept()在拋出java.io.InterruptedIOException異常前等待入站連接的時間,以毫秒計,0代表永不超時。
  • public void setSoTimeout(int timeout) throws SocketException
  • public int getSotimeout() throws IOException

SO_REUSEADDR

  • 確定了是否允許一個新的Socket綁定到之前使用過的一個端口,即使此時可能還有數據正在傳輸。
  • public void setSoReuseAddress(boolean on) throws SocketException
  • public boolean getSoReuseAddress() throws SocketException

SO_RCVBUF

  • 設置ServerSocket返回的Socket的默認接收緩衝區大小,如果想設置大於64KB的接收緩衝區大小則必須在綁定ServerSocket之前設置。
  • public void setSoReceiveBuffSize(int size) throw SocketException
  • public int getSoReceiveBuffSize() throws SocketException

服務類型

  • 四個通用業務流類型:低成本、高可靠性、最大吞吐量、最小延遲。
  • public void setPerformancePreferences(int connectionTime, int latency, int bandwidth):設置連接時間、延遲和帶寬的相對優先級。

HTTP服務器

單文件服務器

重定向器Redirector

功能完備的HTTP服務器

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章