【JavaWeb學習】socket通信

ServerSocket用法詳解

在B/S通信模式中,服務端需要創建監聽特定端口的ServerSocket,ServerSocket負責接收客戶的連接請求。

構造ServerSocket

serverSocket的構造函數有四種

  • ServerSocket() throws IOException
  • ServerSocket(int port) throws IOException
  • ServerSocket(int port, int backlog) throws IOException
  • ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

其中參數port用來綁定端口號(即服務器監聽的端口),

參數backlog顯示設置連接請求隊列的長度,當服務器運行時候,會監聽多個客戶的連接請求,當服務器端收到多個連接請求,操作系統會把這些連接請求存儲到一個先進先出的隊列中。許多操作系統限定了隊列的最大長度,一般爲50。

參數bindAddr,當主機有多個IP地址(多網卡)的時候,就需要顯示指定那個IP地址。

上面有一個默認構造方法,它的作用是,允許服務器在綁定到特定的端口之前,先設置ServerSocket的一些選項,因爲一旦服務器與特定的端口綁定,有些選項就不能再改變了。

例如,可以先設置ServerSocket的SO_REUSEADDR的選項爲true,然後再把它與8888端口綁定

serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(8888));

serverSocket選項

  • SO_TIMEOUT:等待客戶連接的超時時間
  • SO_REUSEADDR:表示是否允許重用服務器所綁定的地址,boolean
  • SO_RCVBUF:表示接受數據的緩衝區的大小

SO_TIMEOUT表示serverSocket的accept()方法等待客戶端連接的超時時間,以毫秒爲單位。如果SO_TIMEOUT的值爲0,表示永遠不會超時,這是SO_TIMEOUT的默認值。accept方法會一直阻塞直到有客戶端連接,方法才返回,或者超出了超時時間,那麼accept方法會拋出SocketTimeoutException.

SO_REUSEADDR=false,當serverSocket關閉時,如果網絡上還有發送到這個ServerSocket的數據,這個ServerSocket不會立刻釋放本地端口,而是會等待一段時間,確保接收到了網絡上發送過來的延遲數據,然後再釋放端口。許多服務器程序都使用固定的端口,當服務器關閉時,它的端口可能還會被佔用一段時間.如果立刻釋放端口,釋放的端口可能會立刻連上新的應用程序,這樣存活在網絡中的TCP報文會與新的TCP連接報文衝突,造成數據衝突。需要耐心等待網絡中老的TCP連接的活躍報文全部消失,2MSL時間可以滿足要求,這裏涉及到TCP的四次揮手。

單線程Socket實例

創建ServerSocket服務器

public class Server {
	
	@SuppressWarnings("resource")
	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket=new ServerSocket(8888);//監聽8888端口
		while(true) {
			System.out.println("socket已連接,等待客戶端請求...");
			Socket socket = serverSocket.accept();
			new WebAction(socket).service();//獲取請求內容並響應
		}
	}
}

WebAction類用來獲取請求的內容並響應給瀏覽器

public class WebAction {
	
	private Socket socket;
	
	private BufferedReader bufferedReader=null;
	
	private BufferedWriter bufferedWriter=null;

	public WebAction(Socket socket) {
		this.socket=socket;
	}

	public void service() {
		try {
			readRequest(socket);
			writeHtml(socket);
			close();
		} catch (IOException e) {
		}	
	}
	
	public void readRequest(Socket socket) throws IOException {
		StringBuffer sb=null;
		String temp=null;
		bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));//讀取請求的內容
		sb=new StringBuffer();
        System.out.println("---------------------");
        while((temp=bufferedReader.readLine())!=null) {//注意這裏,如果客戶端不關閉,服務器就會一直等待
        	sb.append(temp);
        	System.out.println(temp);
        }
	}
	
	public void writeHtml(Socket socket) throws IOException {
		
		bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
		StringBuffer sb=new StringBuffer();
		sb.append("http/1.1 200 ok").append("\n\n");
		sb.append("success");
		bufferedWriter.write(sb.toString());
		bufferedWriter.flush();
		bufferedWriter.close();
	}
	
	public void close() {
		try {
			bufferedReader.close();
			bufferedWriter.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

啓動服務器,並在瀏覽器中輸入 http://localhost:8888/app?name=zhangsan ,發現請求的內容服務器已經獲取到了
在這裏插入圖片描述
但是瀏覽器一直在等待,這是怎麼回事呢
在這裏插入圖片描述
原來如果客戶端打開一個輸入流,如果不做約定,也不關閉它,那麼服務端永遠不知道客戶端是否發送完消息,那麼服務端就會一直等待下去,直到讀取超時。所以怎麼告訴服務端已經發送完消息就很重要。

socket判斷髮送完成的方式

  • 方法一:Socket關閉,當socket關閉,服務端會接收到響應的關閉信號,那麼服務端就知道流已經關閉了,這個時候讀操作完成。但是這種方法太暴力,而且socket關閉後,客戶端將不能接收服務器發送的消息,也不能再次發送消息了。如果客戶端想再次發送消息,需要重新創建Socket連接。

  • 方法二:客戶端發送完成後,調用socket.shutdownOutput()而不是

outputStream.close() //如果關閉了輸出流,那麼相應的socket也會關閉,和直接調用socket.close是一樣的。

這種方法優點是發送完成之後可以接收服務端返回的數據,缺點是也不能再次發送了。

  • 方法三:客戶端與服務器約定符號

例如,客戶端在發送消息的最後加上一個“end”標誌,當服務端讀取到該標誌,就知道客戶端已經發送完成了。但這種方法容易引起誤操作。

  • 方法四:客戶端指定長度

可以採用計算機界普遍採用的,先通過前面幾個字節來說明後面跟隨的消息的長度。例如0xxxxxxx

表示第一個字節表示內容的長度,內容最大是128個字節,即128B

話說回來,通過瀏覽器訪問socket服務器,服務器如何知道發送結束了呢?因爲瀏覽器採用的是http協議,HTTP請求包括了一下內容:一個請求行、若干個請求頭、實體內容,以GET請求爲例,請求格式如下圖所示
在這裏插入圖片描述
從上圖可知,get請求的最後會有一個空行,我們可以以此作爲get請求發送結束的標誌。

只要修改一行代碼即可

	public void readRequest(Socket socket) throws IOException {
		StringBuffer sb=null;
		String temp=null;
		bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		sb=new StringBuffer();
        System.out.println("---------------------");
        while((temp=bufferedReader.readLine())!=null && !"".equals(temp)) {//判斷get請求是否結束
        	sb.append(temp);
        	System.out.println(temp);
        }
	}

但是上面的例子是單線程的,如果同時有多個請求,那隻能排隊等待,這顯然不太合適,下面我們就改造成多線程的Socket服務器,每當有新的請求,就分配一個線程給該請求。

多線程服務器

爲了讓服務器能夠同時爲多個客戶提供服務,提高服務器的併發能力,可以創建一個線程池,每次從線程池中取出工作線程爲客戶服務。

public class Server {
	
	@SuppressWarnings("resource")
	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket=new ServerSocket(8888);
		ExecutorService executorService = Executors.newCachedThreadPool();
		while(true) {
			System.out.println("socket已連接,等待客戶端請求...");
			final Socket socket = serverSocket.accept();
			// 每個請求分配一個線程
			executorService.execute(new Runnable() {
				public void run() {
					new WebAction(socket).service();//獲取請求內容並響應
				}
			});
		}
	}
}

參考文章

Java Socket變成基礎及深入講解

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