java必知必會之ServerSocket

ServerSocket的生命週期

一個ServerSocket的基本生命週期:
1)使用一個ServerSocket構造函數在一個特定端口建立一個新的ServerSocket
2)ServerSocket使用accept方法監聽這個端口的入站鏈接,accept方法會一直阻塞,直到一個客戶端嘗試創建鏈接,此時accept將返回一個鏈接客戶端和服務器的Socket對象
3)根據服務器類型,會調用Socket的getInputSteam或getOutputStream方法,或者兩個方法都調用,以獲取與客戶端通訊的輸入和輸出流
4)服務器和客戶端根據已協商的協議交互,直到要關閉鏈接
5)服務器或客戶端(或兩者)關閉鏈接
6)服務器返回到步驟2,等待下一次鏈接java

異常

有兩類異常,一類異常可能關閉服務器並記錄一個錯誤信息,另外一類異常只關閉活動鏈接,區分這兩類異常很是重要。某個特性鏈接範圍內的異常會關閉這個鏈接,可是不會影響其餘異常或者關閉服務器。程序員

單個請求範圍以外的異常可能會關閉服務器。服務器

結束處理時,必定要關閉Socket,客戶端不能依賴鏈接的另外一端關閉Socket,對於服務器尤爲如此。客戶端可能超時或崩潰,用戶可能取消事務,網絡可能在流量高峯期間癱瘓,黑客可能發動拒絕服務攻擊。出於諸如此類的衆多緣由,你不能依賴於客戶端關閉Socket,即便協議有這個要求也不能徹底相信客戶端必定會關閉Socket。網絡

請求隊列

操做系統把指向某個特定端口的入站鏈接請求存儲在一個先進先出的隊列中,默認地,Java將這個隊列的長度設置爲50,但不一樣的操做系統會有所不一樣。FreeBSD默認最大隊列長度爲128。在這些系統中,Java服務器socket的隊列長度將是操做系統所容許的最大值(小於等於50)。隊列中填入的未處理鏈接達到最大容量時,主機會拒絕這個端口上額外的鏈接,直到隊列騰出新的位置出來爲止。不少客戶端在首次鏈接被拒絕後還會屢次嘗試創建鏈接。socket

若是默認長度不夠大,一些ServerSocket的構造函數還容許改變這個隊列的長度,不能不能超過操做系統支持的最大值。ide

package network.serversocket;
import java.net.*;
import java.io.*;
import java.util.Date;
 
public class DaytimeServer {
 
  public final static int PORT = 13;
  public static void main(String[] args) {  
   try (ServerSocket server = new ServerSocket(PORT)) {
     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();
       } catch (IOException ex) {}       
     }
   } catch (IOException ex) {
     System.err.println(ex);
   } 
  }
}

無論怎樣,咱們都但願可以比新鏈接到來的速度更快地清空隊列。函數

每一個鏈接對應一個線程

解決方法是爲每一個鏈接提供它本身的一個線程,與接入站鏈接放入隊列的那個線程分開。生成一個線程來處理每一個入站的鏈接,這樣能夠防止一個慢客戶端阻塞全部其餘客戶端,這種是「每一個鏈接對應一個線程」的設計。測試

package network.serversocket;
import java.net.*;
import java.io.*;
import java.util.Date;
 
public class ThreadPerConnectionDemo {
 
  public final static int PORT = 13;
  public static void main(String[] args) {   
   try (ServerSocket server = new ServerSocket(PORT)) {
     while (true) {  
       try {
         Socket connection = server.accept();
         Thread task = new DaytimeThread(connection);
         task.start();
       } catch (IOException ex) {}
     } 
    } catch (IOException ex) {
      System.err.println("Couldn't start server");
    }
  }
  
  private static class DaytimeThread extends Thread {
    
    private Socket connection;
    
    DaytimeThread(Socket connection) {
      this.connection = connection;
    }
    
    @Override
    public void run() {
      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 e) {
          // ignore;
        }
      }
    }
  }
}

鏈接池的版本:

package network.serversocket;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
 
public class PooledDaytimeServer {
 
  public final static int PORT = 13;
  public static void main(String[] args) {  
   
   ExecutorService pool = Executors.newFixedThreadPool(50);
   try (ServerSocket server = new ServerSocket(PORT)) {
     while (true) {  
       try {
         Socket connection = server.accept();
         Callable<Void> task = new DaytimeTask(connection);
         pool.submit(task);
       } catch (IOException ex) {}
     } 
    } catch (IOException ex) {
      System.err.println("Couldn't start server");
    }
  }
  
  private static class DaytimeTask implements Callable<Void> {
    
    private Socket connection;
    
    DaytimeTask(Socket connection) {
      this.connection = connection;
    }
    
    @Override
    public Void 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 e) {
          // ignore;
        }
      }
      return null;
    }
  }
}

優雅地關閉socket

程序員一般會在一個try-finally塊中採用close-if-not-null模式了來關閉Socket,可使用無參構造器來稍加改進,無參構造器不會拋出任何異常,也不綁定到一個端口,所以構造放在try外邊,finally裏頭直接close。this

ServerSocket server = new ServerSocket();
try {
  SocketAddress address = new InetSocketAddress(port);
  server.bind(address);
  // ... work with the server socket
} finally {
  try {
    server.close();
  } catch (IOException ex) {
    // ignore
  }
}

或者直接採用Java7的AutoCloseable模式:操作系統

try (ServerSocket server = new ServerSocket(port)) {
  // ... work with the server socket
}catch (IOException ex) {
      logger.log(Level.SEVERE, "Could not start server", ex);
    }

若是要記錄異常信息,能夠catch下異常,不然能夠不用catch。

要測試ServerSocket是否打開:

public static boolean isOpen(ServerSocket ss) {
  return ss.isBound() && !ss.isClosed();
}

isBound方法有歧義,它是表示是否曾經綁定到某個端口,即便它目前已經關閉,isBound仍然會返回true,所以還須要判斷是不是close。

構建ServerSocket

ServerSocket httpd = new ServerSocket(80, 50);

這裏指定了綁定80端口,同時隊列一次最多可保存50個入站鏈接,若是試圖設置超過操做系統的最大隊列長度,則會使用最大隊列長度。

ServerSocket server = new ServerSocket(0);

0表示監聽未指定的端口,操做系統會爲你選擇可用的端口,稱之爲匿名端口。

若是沒有綁定任何端口,則getLocalPort返回-1。

ServerSocket選項

1)SO_TIMEOUT

爲0的話,表示永不超時,通常服務端都設置爲永不超時。客戶端則要指定。
設置該值必須在調用accppt以前。

2)SO_REUSEADDR

肯定是否容許一個新的Socket綁定到以前使用過的一個端口,而此時可能還有一些發送到原來Socket的數據正在網絡上傳輸。

3)SO_RCVBUF

設置了服務器Socket接受的客戶端Socket默認接收緩衝區的大小。
setReceiveBufferSize來設置,必須在accept以前設置。

4)服務類型

爲TCP定義了4個通用的業務流模型:
A、低成本
B、高可靠性
C、最大吞吐量
D、最小延遲

可使用setPerformancePreferences方法描述

相關文章
相關標籤/搜索