java網絡編程學習之路(3)

第三章、ServerSocket用法簡解

由於文章總結的內容太長,看起來很不方便,所以現在開始總結可能會常用的知識點。

ServerSocket是服務器端負責接受客戶連接請求的類。

3.1 構造ServerSocket


backlog參數是用來設定客戶連接隊列的長度,即允許幾個客戶端連接請求緩存個數。

在以下情況下會採用操作系統限定的隊列長度:
1)、backlog參數的值大於操作系統限定的隊列最大長度

2)、backlog參數的值小於或等於0

3)、在ServerSocket構造方法中沒有設置backlog參數


如果我們設立了backlog參數值爲3,當我們用客戶端進行服務器端連接請求時,沒有運行ServerSocket對象的accept()方法,這種情況下,如果連接請求到了3個以上時,程序就會拋異常,因爲連接隊列裏面已經滿了,而且我們沒有用accept方法從請求隊列中取出連接。所以我們一般把accept方法寫到while循環裏,這樣不出問題的話,會一直接受客戶端的連接請求。


3.2 接受和關閉與客戶端的連接

就如上面所訴的,ServerSocket通過accept方法來從請求隊列中取出一個客戶的連接請求,然後創建於客戶端連接的Socket對象,並且將它返回。如果隊列裏面沒有請求連接的話,那麼accept方法就會一直等下去。
當服務器端正在給一個客戶端發送數據時,該客戶端斷開連接了,這是服務器端就會拋出SocketExcepion異常,這是我們不願意看到的,因爲服務器端還和其他客戶端進行着連接,不能因爲一個客戶端的出錯導致整個通信網絡癱瘓。所以在單線程服務器中,我們一般對該異常進行捕獲。

3.3 關閉ServerSocket

ServerSocket的close方法可以使服務器釋放佔用的端口,並且斷開與所有客戶端的連接。不過當一個服務器程序運行結束時,即使沒有調用該方法,默認的還是會釋放服務器佔用的端口。
ServerSocket的isClose方法判斷ServerSocket是否關閉,只有執行了ServerSocket的close方法,isClosed纔會返回true;否則即使ServerSocket還沒有和特定的端口號綁定,isClosed方法也會返回false、
另外ServerSocket還提供了isBound方法判斷是否已經與一個端口綁定,只要綁定了,即使它已經關閉了,isBound方法也會返回true。

3.4 獲取ServerSocket信息


我們在前面的2章中得知,如果在構造函數時給其分配的端口號是0,那麼會由操作系統隨機分配端口號,例如ServerSocket serverSocket = new ServerSocket(0)。這時候,我們要想得知其端口號就可以用getLocalPort方法了。

不過多數服務器都會監聽固定端口,方便客戶端進行方法。匿名端口一般用於服務器和客戶進行臨時通信,通信結束,就斷開連接,釋放佔用的相關資源。FTP就使用了匿名端口。
FTP使用了兩個並行的TCP連接,一個是控制連接,一個是數據連接。控制連接用於在客戶和服務器之間發送控制信息,如用戶名和口令。改變遠程目錄的命令或上傳和下載文件的命令。數據連接用於傳送文件。TCP服務器在21端口上監聽控制連接,如果有客戶要求上傳或下載文件,就另外建立了一個數據連接,通過它來傳送文件。數據連接有兩種建立方式。如下圖所示:
1)、所有的連接均在特定的端口

2)、客戶端和服務器進行數據連接時先創建一個監聽匿名端口的ServerSocket,然通過getLocalPort獲取端口號發給TCP服務器,然後由TCP服務器主動建立與客戶端的連接。當然服務器端也可以使用匿名端口。

3.5 ServerSocket選項

ServerSocket有以下三個選項

1)、SO_TIMEOUT:表示等待客戶連接的超時時間
2)、SO_REUSEADDR:表示允許重用服務器所綁定的地址
3)、SO_RCVBUF:表示接受數據的緩衝區的大小

詳細用法見Socket那章,其實是類似的。

3.6 創建多線程的服務器

在單線程的情況下,服務器往往不能同時和多個客戶同學,服務器給一個客戶發送信息時,隊列中的其他客戶就必須排隊等服務器和那個客戶的通信結束。
許多實際應用要求服務器具有同時爲多個客戶提供服務的能力。HTTP服務器就是最明顯的例子。任何時刻,HTTP服務器都可能接受大量的客戶請求,每個客戶都希望快速得到HTTP服務器的響應,如果長時間讓客戶等待,那估計就沒人訪問了。
可以用併發性能來衡量一個服務器同時響應多個客戶的能力。一個具有好的併發性能的服務器,必須符合兩個條件:
1).能同時接受並處理多個客戶連接;
2).對於每個客戶,都會迅速給與響應。
爲了實現以上的併發,我們可以使用多線程。
1).爲每個客戶分配一個工作線程.
2).創建一個線程池,由其中的工作線程來爲客戶服務。
3).利用JDK的java類庫中現成的線程池,由它的工作線程來爲客戶服務。

3.6.1 爲每個客戶分配一個線程

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoServer {
	private int port = 8000;
	private ServerSocket serverSocket;
	public EchoServer() throws IOException{
		serverSocket = new ServerSocket(port);
		System.out.println("服務器已啓動");
	}
	
	public void service() throws IOException{
		while(true){
			Socket socket = null;
			socket = serverSocket.accept();
			Thread workThread = new Thread(new Handler(socket));
			workThread.start();
		}
	}
}

class Handler implements Runnable{
	private Socket socket;
	public Handler(Socket socket){
		this.socket = socket;
	}
	private PrintWriter getWriter(Socket socket) throws IOException{
		PrintWriter pw = new PrintWriter(socket.getOutputStream(),true);
		return pw;
	}
	private BufferedReader getReader(Socket socket) throws IOException{
		InputStreamReader in = new InputStreamReader(socket.getInputStream());
		BufferedReader br = new BufferedReader(in);
		return br;
	}
	public String echo(String msg){
		return "echo:"+msg;
	}
	@Override
	public void run() {
		System.out.println("new connection accepted"+socket.getLocalAddress()+";"+socket.getLocalPort());
		try {
			BufferedReader br = getReader(socket);
			PrintWriter pw = getWriter(socket);
			String msg = null;
			while((msg = br.readLine())!=null){
				System.out.println(msg);
				pw.println(echo(msg));
				if("bye".equals(msg)){
					break;
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally{
			if(socket!=null)
				try {
					socket.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
		}
		
	}
	
}



把跟客戶端進行數據接受發送的代碼寫到一個新的線程的run方法裏去,當調用線程的start方法就執行了run方法裏面的代碼。

3.6.2 創建線程池

以上的多線程的代碼多少效率上有些不高,反覆的線程創建和銷燬所造成的的資源開銷太大。所以我們引入了線程池的概念,所謂線程池就是一個池子裏事先裝入了N個線程,當你要用時就拿一個,不用時就放回來,這樣就不會返回的創建和銷燬線程。
import java.util.LinkedList;

public class ThreadPool extends ThreadGroup{
	private boolean isClosed = false;  //線程池是否關閉
	private LinkedList<Runnable> workQueue; //表示工作隊列
	private static int threadPoolID;//表示線程的ID
	private int threadID; //表示工作線程的ID
	
	public ThreadPool(int poolSize){  //指定線程池中工作線程的數目
		super("ThreadPool-"+(threadPoolID++));
		setDaemon(true);  //守護線程
		workQueue = new LinkedList<Runnable>();
		for(int i = 0;i < poolSize;i++){
			new WorkThread().start();
		}
	}
	
	/**向工作隊列中加入一個新任務,由工作線程去執行該任務*/
	public synchronized void execute(Runnable task){
		if(isClosed){      //如果線程池被關閉,則拋出異常
			throw new IllegalStateException();
		}
		if(task != null){
			workQueue.add(task);
			notify();    //喚醒正在getTask方法中等待人物的工作線程
		}
	}
	
	/**從工作隊列中取出一個任務,工作線程會調用此方法
	 * @throws InterruptedException */
	protected synchronized Runnable getTask() throws InterruptedException{
		while(workQueue.size()==0){
			if(isClosed){
				return null;
			}
			wait();        //如果工作隊列中沒有任務,就等待任務
		}
		return workQueue.removeFirst();
	}
	
	/**關閉線程池*/
	public synchronized void close(){
		if(!isClosed){
			isClosed = true;
			workQueue.clear(); //清空工作隊列
			interrupt();         //中斷所有的工作線程,該方法繼承自ThreadGroup
		}
	}
	
	/**等待工作線程把所有的任務執行完*/
	public void join(){
		synchronized (this) {
			isClosed = true;
			notifyAll();      //喚醒還在getTask方法中等待任務的工作線程
		}
		Thread[] threads = new Thread[activeCount()];
		//enumerate()方法繼承自ThreadGroup類,獲得線程中當前所有活着的工作線程
		int count = enumerate(threads);
		for(int i=0; i<count; i++){
			try {
				threads[i].join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	/** 內部類:工作線程*/
	private class WorkThread extends Thread{
		public WorkThread(){
			//加入當前的ThreadPool線程組中
			super(ThreadPool.this,"WorkThread-"+(threadID++));   //在線程組中創建線程
		}
		
		public void run(){
			while(!isInterrupted()){      //判斷線程是否被中斷
				Runnable task = null;
				try {
					task = getTask();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				if(task == null) return;
				task.run();
			}
		}
	}
}

在ThreadPool類中定義了一個workQueue用來存放線程池要執行的任務,每個任務都是Runnable的實例。ThreadPool類的客戶程只要調用ThreadPool中的execute方法就能向線程池提交任務。該方法將任務加到工作隊列中,並且喚醒正在等待任務的工作線程。即有了工作任務就通知線程去執行這個任務。工作線程執行完該任務後,再從工作隊列中取下一個任務並執行,如果沒有就wait。
線程池的join和close都可以關閉線程池,不過join在關閉前會確保工作隊列中的任務都執行完畢。join的這一功能具體實現是靠enumerate方法的。

先new一個Thread數組threads,activeCount()方法返回此線程組中活動線程的估計數。然後用enumerate方法來將線程組複製到剛new出來的數組裏。

然後再調用線程的join方法來等待線程數組中每一個線程的終止。


下面用一個例子來調用以上線程池

public class ThreadPoolTester {
	public static void main(String[] args) {
		int numTasks = 5;
		int poolSize = 3;
		ThreadPool threadPool = new ThreadPool(poolSize); //創建線程池
		
		//運行任務
		for(int i=0; i<numTasks; i++){
			threadPool.execute(createTask(i));
		}
		threadPool.join();
	}

	private static Runnable createTask(final int i) {
		
		return new Runnable() {
			
			@Override
			public void run() {
				System.out.println("Task"+i+":start");
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					
				}
				System.out.println("Task"+i+":end");
			}
		};
	}
}

運行結果:

Task1:start
Task2:start
Task0:start
Task2:end
Task3:start
Task1:end
Task4:start
Task0:end
Task3:end


一共5個任務,線程池中線程的個數爲3,通過運行其實可以發現最多一次性連續執行的任務數是3,因爲線程總數爲3,一個線程在沒有執行完一個任務時,是沒法去執行下一個任務。

JDK自帶的類庫也提供了一個線程池供我們去使用,在java.util.concurrent包裏面。具體用法後面有個例子。

3.6.3 使用線程池需要注意的事項

雖然線程池能大大提高服務器的併發性能,但是使用它會存在一定的風行。
1.死鎖
任何多線程應用程序都存在死鎖風險。所謂死鎖,典型的例子就是線程A佔用了資源1,它需要資源2才能繼續執行下去,線程B佔用了資源2,它需要資源1才能繼續執行下去,兩個線程都不釋放自己佔用的資源並且都在等着對方的資源,這樣下去就產生了死鎖。線程池會導致這種死鎖以外的另一種死鎖,假定線程池中的所有工作線程都在執行各自的任務時被阻塞,它們都在等待某一個任務A的執行結果,而任務A依然在工作隊列中,由於沒有空閒的線程去執行任務A,所以線程池中的工作線程都永遠阻塞下去了。

2.系統資源不足
如果設計的線程池中線程的數目很多,而且使用效率不高,打個比方如果有100個線程,只需要執行10個任務,這樣就嚴重浪費了資源了。

3.併發錯誤
如果沒有線程池中的wait和notify方法使用不正確,導致notify沒有喚醒一個wait線程,這樣該工作線程就會一直空閒的wait下去。

4.線程泄漏
工作線程在執行一個任務時被阻塞,如等待用戶的輸入數據,但由於用戶遲遲沒有輸入數據,導致這個線程一直處於阻塞狀態,線程池所能使用的工作線程就會減少。如果很多這樣的工作線程沒執行完任務處於阻塞狀態會導致線程池無法執行新的任務。

5.任務過載
當工作隊列中有大量任務排着隊,這些任務本身執行會消耗很多系統資源,如果我們在這些任務調度執行時沒有一個很好的安排,例如,任務A執行過程中需要任務B的執行結果,我們先把A加了進去就不太合適了。還有任務的種類不同,有些是會經常阻塞的IO操作,有些是執行一直不會阻塞的運算操作。前者時斷時續地佔用CPU資源,後者對CPU資源利用率更高。所以我們要對任務進行合理的分類,根據任務的不同設置對個工作隊列,對其進行不同的調度處理。

3.7 關閉服務器

服務器如何能在恰當的時刻關閉自己是我們所想要實現的。
以下代碼是可以關閉自己的一個服務器例子
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;

public class EchoServer2 {
	private int port = 8000;
	private ServerSocket serverSocket;
	private ExecutorService  executorService; //JDK自帶的線程池
	private final int POOL_SIZE = 4; //單個CPU時線程池中工作線程的數目
	
	private int portForShutdown = 8001;     //用於監聽關閉服務器命令的端口
	private ServerSocket serverSocketForShutdown;
	private boolean isShutdown = false;
	
	private Thread shutdownThread = new Thread(){
		public void start(){
			this.setDaemon(true);
			super.start();
		}
	public void run(){
		while(!isShutdown){
			Socket socketForShutdown = null;
			try {
				socketForShutdown = serverSocketForShutdown.accept();
				BufferedReader br = new BufferedReader(new InputStreamReader(socketForShutdown.getInputStream()));
				String command = br.readLine();
				if("shutdown".equals(command)){
					long beginTime = System.currentTimeMillis();
					socketForShutdown.getOutputStream().write("服務器正在關閉\r\n".getBytes());
					isShutdown = true;
					//請求關閉線程池
					//線程池不再接受新的任務,但是會繼續執行完工作隊列中現有的任務
					executorService.shutdown();
					//等待關閉線程池,每次等待的超時時間爲30秒
					while(!executorService.isTerminated()){
						executorService.awaitTermination(30, TimeUnit.SECONDS);
					}
					serverSocket.close();//關閉與EchoClient客戶同學的ServerSocket
					long endTime = System.currentTimeMillis();
					socketForShutdown.getOutputStream().write(("服務器已經關閉,"+"關閉服務器用了"+(endTime-beginTime)+"毫秒\r\n").getBytes());
					socketForShutdown.close();
				}else{
					socketForShutdown.getOutputStream().write("錯誤的命令\r\n".getBytes());
					socketForShutdown.close();
				}
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} 
		}
	}
};

public EchoServer2() throws IOException {
	serverSocket = new ServerSocket(port);
	serverSocket.setSoTimeout(60000); //設定等待用戶連接的超時時間爲60秒
	serverSocketForShutdown = new ServerSocket(portForShutdown);
	
	//創建線程池
	executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*POOL_SIZE);
	shutdownThread.start(); //啓動負責關閉服務器的線程
	System.out.println("服務器啓動");
}

public void service(){
	while(!isShutdown){
		Socket socket = null;
		try {
			socket = serverSocket.accept();
			socket.setSoTimeout(60000);//把等待客戶發送數據的超時時間設置爲60秒
			executorService.execute(new Handler1(socket));
		} catch (SocketTimeoutException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}catch (RejectedExecutionException e) {
			if(socket!=null){
				try {
					socket.close();
				} catch (IOException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
			}
			e.printStackTrace();
		} catch (SocketException e) {
			//如果由於在執行serverSocket的accept方法時
			//ServerSocket被ShutdownThread線程關閉而導致的異常,就退出service方法
			if(e.getMessage().indexOf("socket closed")!=-1)return;
		} catch(IOException e){
			e.printStackTrace();
		}
		}
	}

	public static void main(String[] args) throws IOException {
		new EchoServer2().service();
	}
}


class Handler1 implements Runnable{
	private Socket socket;
	public Handler1(Socket socket){
		this.socket = socket;
	}
	private PrintWriter getWriter(Socket socket) throws IOException{
		PrintWriter pw = new PrintWriter(socket.getOutputStream(),true);
		return pw;
	}
	private BufferedReader getReader(Socket socket) throws IOException{
		InputStreamReader in = new InputStreamReader(socket.getInputStream());
		BufferedReader br = new BufferedReader(in);
		return br;
	}
	public String echo(String msg){
		return "echo:"+msg;
	}
	@Override
	public void run() {
		System.out.println("new connection accepted"+socket.getLocalAddress()+";"+socket.getLocalPort());
		try {
			BufferedReader br = getReader(socket);
			PrintWriter pw = getWriter(socket);
			String msg = null;
			while((msg = br.readLine())!=null){
				System.out.println(msg);
				pw.println(echo(msg));
				if("bye".equals(msg)){
					break;
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally{
			if(socket!=null)
				try {
					socket.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
		}
		
	}
	
}


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;

public class AdminClient {
	public static void main(String[] args) {
		Socket socket = null;
		
		try {
			socket = new Socket("localhost",8001);
			//發送關閉命令
			OutputStream socketOut = socket.getOutputStream();
			socketOut.write("shutdown\r\n".getBytes());
			
			//獲取服務器反饋
			BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			
			String msg = null;
			while((msg=br.readLine())!=null){
				System.out.println(msg);
			}
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally{
			if(socket!=null)
				try {
					socket.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
		}
	}
}

主要的思路是在服務器程序中另外建立一個ServerSocket監聽那種類似於管理員權限的客戶端發送的消息,如果發送的命令爲shutdown,則關閉服務器端。

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