在網上無意看到一個多線程的Socket服務器端例子,覺得非常不錯。特別是其中的線程池的思想,簡單而且高效。雖然JDK1.5開始已經自帶了線程池包,但該代碼不失為學習Socket和多線程的一個好的入門例子。
下面的代碼是對該例子的簡單整理,補充了注釋。
【代碼一】PooledConnectionHandler:后臺處理類
package server;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;
/**
* <pre>
* PooledConnectionHandler實現了Runnable接口,它的用途是處理服務器端傳來的Socket連
* 接。該類維護了一個被稱為"連接池"的全局鏈式列表(靜態),該列表在類被加載時創建。
*
* 當該類的run()方法被調用時,它首先檢查該"連接池"中是否有需要待處理的客戶端連接,如果
* 沒有(可能是請求尚未到來)則調用wait()方法等待,而另一個靜態方法processRequest則負
* 責接收客戶端請求并添加到"連接池"的末尾,然后通知所有正在等待的線程,各個等待的線程則
* 以"互斥"的方式競爭資源(請求)當某個線程率先獲取到對象鎖,拿到一個連接后,將釋放鎖,然
* 后在自己的線程中處理請求。各個線程之間彼此不會互相影響。
*
* 需要注意的是這個類的一個方法:processRequest這個方法第一個為靜態方法,因為對于這個
* 方法來說,它只是一個負責通知的角色,所以沒有必要是對象級的。將其修飾符置為static是合
* 理的。
*
* 其次我們看到在對全局資源"連接池"進行操作時,不管是往池中添加請求,還是從池中取出請求,
* 都需要在關鍵的語句塊中增加synchronized{
},來保證同一時刻不會有多個線程競爭同一個
* 資源,或者在添加資源未完成前有另一個線程試圖讀取該資源。
*
* 最后一個要注意的地方是其中的wait()和notifyAll()方法,wait()方法的調用發送在線程創建
* 完成,但請求尚未到來之前,這時線程并不持有對List的鎖,而notifyAll()方法喚起所有等待
* 的線程,所有等待線程將一起競爭鎖,此時只有一個線程可能檢測到池非空而進入池中請求資
* 源。
* </pre>
*/
public class PooledConnectionHandler implements Runnable {
/** The client connection of Socket. */
protected Socket connection;
/** The request pool. */
protected static List pool = new LinkedList();
/**
* Instantiates a new pooled connection handler.
*/
public PooledConnectionHandler() {
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
public void run() {
while (true) {
// 因為可能有多個線程同時去Pool中取Socket進行處理。
// 所以這里我們需同步,防止同一個請求被多次處理
synchronized (pool) {
while (pool.isEmpty()) {
try {
pool.wait();// 沒有請求到來則等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 從池中取出一個Socket,準備進行處理
connection = (Socket) pool.remove(0);
}
// 取到Socket后則不需要同步了,因為此時是Connection是對象
// 級屬性,在線程內部自己處理,不涉及公共資源的訪問
handleConnection();
}
}
/**
* Process request, append Socket to pool and notify all waitting thread
*
* @param requestToHandle the request to handle
*/
public static void processRequest(Socket requestToHandle) {
// 因為有可能在向池中塞請求的時候,另外一個線程
// 正在從池中取Socket,所以這里需要同步一下
synchronized (pool) {
// 將來自客戶端的請求添加到請求隊列末尾
pool.add(pool.size(), requestToHandle);
// 通知其它正在等待的線程有新請求來到,
// 此時所有處于wait狀態的線程將被喚醒
pool.notifyAll();
}
}
/**
* Handle connection.
*/
public void handleConnection() {
try {
PrintWriter streamWriter = new PrintWriter(connection
.getOutputStream());
BufferedReader streamReader = new BufferedReader(
new InputStreamReader(connection.getInputStream()));
String fileToRead = streamReader.readLine();
BufferedReader fileReader = new BufferedReader(new FileReader(
fileToRead));
String line = null;
while ((line = fileReader.readLine()) != null)
streamWriter.println(line);
fileReader.close();
streamWriter.close();
streamReader.close();
} catch (FileNotFoundException e) {
System.out.println("");
} catch (IOException e) {
System.out.println("" + e);
}
}
}
【代碼二】PooledRemoteFileServer:多線程服務器端,負責創建線程池并等待客戶端的連接請求
package server;
import java.io.IOException;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* <pre>
* PooledRemoteFileServer是一個多線程、池化的Socket服務器。它能夠在指定的端口
* 監聽來自客戶端的連接請求,同時它限定了允許同時連接的數目。
*
* 在服務器端,服務器啟動時創建指定定數量的后臺處理類實例,這些實例實際上是實現了
* Runnable接口的類,它們在創建完成后將立刻喚起,不停地監控來自客戶端的連接。
*
* 另一方面,服務器在創建線程之后,將在指定的端口監聽。一旦有客戶端連接上,立刻將
* 該連接交給后臺在等待的線程去處理,接著立刻返回繼續在端口監聽。這樣的話后臺線程
* 的處理將不會造成前端服務器監聽的阻塞。
* </pre>
*/
public class PooledRemoteFileServer {
/** The max connections. */
protected int maxConnections;
/** The listen port. */
protected int listenPort;
/** The server socket. */
protected ServerSocket serverSocket;
/**
* Instantiates a new pooled remote file server.
*
* @param aListenPort the a listen port
* @param maxConnections the max connections
*/
public PooledRemoteFileServer(int aListenPort, int maxConnections) {
listenPort = aListenPort;// 監聽端口
this.maxConnections = maxConnections;// 最大同時連接
}
/**
* 初始化池:每次創建一個Runnable實例,然后創建線程對象
*/
public void setUpHandlers() {
for (int i = 0; i < maxConnections; i++) {
PooledConnectionHandler currentHandler = new PooledConnectionHandler();
// 線程啟動后將一直監控Socket隊列,以輪詢的方式
// 監控是否有新的客戶端請求到來,如果有的話則取
// 出處理,無的話則繼續等待直至請求到來
new Thread(currentHandler, "Handler" + i).start();
}
}
/**
* 接收客戶端連接
*/
public void acceptConnections() {
try {
ServerSocket server = new ServerSocket(listenPort, 5);
Socket incomingConnection = null;
while (true) {
incomingConnection = server.accept();
handleConnection(incomingConnection);
}
} catch (BindException be) {
System.out.println("");
} catch (IOException ioe) {
System.out.println("" + listenPort);
}
}
/**
* 在池中處理Socket請求
*
* @param connectionToHandle the connection to handle
*/
protected void handleConnection(Socket connectionToHandle) {
PooledConnectionHandler.processRequest(connectionToHandle);
}
public static void main(String args[]) {
PooledRemoteFileServer server = new PooledRemoteFileServer(1001, 3);
// 初始化線程池
server.setUpHandlers();
// 開始在指定端口等待到來的請求
server.acceptConnections();
}
}
這個例子的精髓是在PooledConnectionHandler類,它首先創建一個公共的全局“線程池”(LinkList),然后啟動線程監控線程池,與此同時服務器端在接收到客戶端請求后將請求加到“線程池”中,這兩個動作是異步的,在加的時候不允許讀,在讀得到時候不允許加(通過synchronized關鍵字控制),而且多個線程之間并不會互相影響,因為其中的connection屬性是對象級的。
從這個例子中我們也可以學到在多線程的情況下,哪些變量是必須設置為全局的(static),哪些是必須設置為對象級的:即會被多個線程訪問的資源必須設置為全局的,而跟線程處理狀態,結果有關的屬性一般必須設置為對象級的,以防止互相干擾。
其次就是在多線程情況下,哪些方法是可以設置為static的而不會出現線程安全的問題,哪些方法是不能設置為靜態方法的:如果方法是屬于控制流程,通知,派發的,那么一般可以設置為靜態的。因為這些方法一般不需要多個,一個就夠了。就如同控制器只要一個就夠了。而業務邏輯實現方法一般不能設置為靜態的,因為靜態方法不能引用對象變量(非靜態變量),但業務邏輯通常是需要針對不同的用戶做出不同的處理的,所以幾乎可以肯定的說是絕對會出現對象變量的。
-------------------------------------------------------------
生活就像打牌,不是要抓一手好牌,而是要盡力打好一手爛牌。
posted on 2008-08-16 23:08
Paul Lin 閱讀(7264)
評論(0) 編輯 收藏 所屬分類:
J2SE