關於Java中的ServerSocket類與構造服務器的解析
本文整理於網絡材料:https://www.360doc.com/content/13/0327/19/7891085_274308578.shtml
ServerSocket的構造方法有以下幾種重載形式:
ServerSocket() throws IOExceptionServerSocket(int port) throws IOException
ServerSocket(int port, int backlog) throws IOException
ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
在以上構造方法中,參數port指定服務器要綁定的端口(服務器要監聽的端口),參數backlog指定客戶連接請求隊列的長度,參數bindAddr指定服務器要綁定的IP地址。
設定客戶連接請求隊列的長度
當服務器進行運行時,可能會同時監聽到多個客戶的連接請求,管理客戶連接請求的任務是由操作係統來完成的。操作係統把這些連接請求存儲在一個先進先出的隊列中。許多操作係統限定了隊列的最大長度,一般為50.當隊列中的連接請求達到了隊列的最大容量時,服務器進程躲在的主機會拒絕新的連接請求。隻有當服務器進程通過ServerSocket的accpet()方法從隊列中取出連接請求,使隊列騰出空位時,隊列才能繼續加入新的連接請求。
對於客戶進程,如果它發出的連接請求被加入到服務器的隊列中,就意味著客戶與服務器的連接建立成功,客戶進程從Socket構造方法中正常返回。如果客戶進程發出的連接請求被服務器拒絕,Socket構造方法就會拋出ConnectionException。
ServerSocket構造方法的backlog參數用來顯式設置連接請求隊列的長度,它將覆蓋操作係統限定的隊列的最大長度。值得注意的是,在以下幾種情況中,仍然會采用操作係統限定的隊列的最大長度:
backlog參數的值大於操作係統限定的隊列的最大長度;
backlog參數的值小於或等於0;
在ServerSocket構造方法中沒有設置backlog參數。
設定綁定的IP地址
如果主機隻有一個IP地址,那麼默認情況下,服務器程序就與該IP地址綁定。ServerSocket的第4個構造方法ServerSocket(int port, int backlog, InetAddress bindAddr)有一個bindAddr參數,它顯式指定服務器要綁定的IP地址,該構造方法適用於具有多個IP地址的主機。
默認構造方法的作用
ServerSocket有一個不帶參數的默認構造方法。通過該方法創建的ServerSocket不與任何端口綁定,接下來還需要通過bind()方法與特定端口綁定。這麼默認構造方法的用途是,允許服務器在綁定到特定端口之前,先設置ServerSocket的一些選項。因為一旦服務器與特定端口綁定,有些選項就不能再改變了。
例如:先把ServerSocket的SO_REUSEADDR選項設為true,然後再把它與8000端口綁定:
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true); //設置ServerSocket的選項,該選項必須在服務器綁定端口之前設置才有效
serverSocket.bind(new InetSocketAddreaa(8000)); //與8000端口綁定
接受和關閉與客戶的連接
ServerSocket的accept()方法從連接請求隊列中取出一個客戶的連接請求然後創建與客戶連接的Socket對象,並將它返回。如果隊列中沒有連接請求,accept()方法就會一直等待,直到接收到了連接請求才返回。接下來,服務器從Socket對象中獲得輸入流和輸出流,就能與客戶交換數據。
當服務器正在進行數據的操作時,如果客戶端斷開了連接,那麼服務器端會拋出SocketException異常:java.net.SocketException:Connection reset by peer
這隻是服務器與單個客戶通信出現的異常,這種異常應該被捕獲,使得服務器能繼續與其他客戶通信。
關閉ServerSocket
ServerSocket的close()方法使服務器釋放占用的端口,並且斷開與所有客戶的連接。當一個服務器程序運行結束時,即使沒有執行ServerSocket的close()方法,操作係統也會釋放這個服務器占用的端口。因此,服務器程序並不一定在結束之前執行ServerSocket的close()方法。在某些情況下,如果希望及時釋放服務器的端口,以便讓其他程序能占用該端口,則可以顯示調用ServerSocket的close()方法。
ServerSocket的isClosed()方法判斷ServerSocket是否關閉,隻有執行了ServerSocket的close()方法,isClose()方法才回返回true;否則,即使ServerSocket還沒有和特定端口綁定,isClosed()方法也會返回false。
ServerSocket的isBound()方法判斷ServerSocket是否已經與一個端口綁定,隻要ServerSocket已經與一個端口綁定,即使它已經關閉,isBound()也會返回true。
如果需要確定一個ServerSocket已經與特定端口綁定,並且還沒有被關閉,則可以采用以下方式:
boolean isOpen = serverSocket.isBound && !serverSocket.isClosed();
獲取ServerSocket的信息
ServerSocket的以下兩個get方法可以分別獲得服務器綁定的IP地址,以及綁定的端口:
public InetAddress getInetAddress();
public int getLocalPort();
在構造ServerSocket時,如果把端口設為0,那麼將有操作係統為服務器分配一個端口(稱為匿名端口),程序隻要調用getLocalPort()方法就能獲知這個端口號。
多數服務器會監聽固定的端口,這樣才便於客戶程序訪問服務器。匿名端口一般適用於服務器與客戶之間的臨時通信,通信結束,就斷開連接,並且ServerSocket占用的臨時端口也被釋放。FTP(文件傳輸)協議就是用了匿名端口。
ServerSocket選項:
SO_TIMEOUT:表示等待客戶連接的超時時間。
SO_REUSEADDR:表示是否允許重用服務器所綁定的地址。
SO_RCVBUF:表示接受數據的緩衝區的大小。
SO_TIMEOUT選項
設置該選項:public void setSoTimeout(int timeout) throws SocketException
讀取該選項:public int getSoTimeout() throws IOException
SO_TIMEOUT表示ServerSocket的accept()方法等待客戶連接的超時時間,以毫秒為單位。如果SO_TIMEOUE的值為0,表示永遠不會超時,這是SO_TIMEOUT的默認值。
當服務器執行ServerSocket的accept()方法時,如果連接請求隊列為空,服務器就會一直等待,直到接收到了客戶連接才從accept()方法返回。如果設定了超時時間,那麼服務器等待的時間超過了超時時間,就會拋出SocketTimeOutException,它是InterruptedException的子類。
ServerSocket serverSocket = new ServerSocket(8000);
serverSocket.setSoTimeOut(6000);//等待客戶連接的時間不超過6秒
Socket socket = serverSocket.accept();
socket.close();
System.out.println("服務器關閉");
過6秒後,程序會從serverSocket.accept()方法中拋出SocketTimeOutException
SO_REUSEADDR選項
設置該選項:public void setResuseAddress(boolean on) throws SocketExceprion
讀取該選項:public boolean getReuseAddress() throws SocketException
這個選項與Socket的SO_REUSEADDR選項相同,用於決定如果網絡上仍然有數據向舊的ServerSocket傳輸數據,是否允許新的ServerSocket綁定到與舊的ServerSocket同樣的端口上。SO_REUSEADDR選項的默認值與操作係統有關,在某些操作係統中,運行重用端口,而在某些操作係統中不允許重用端口。
當ServerSocket關閉時,如果網絡上還有發送到這個ServerSocket的數據,這個ServerSocket不會立刻釋放本地端口,而是會等待一段時間,確保接收到了網絡上發送過來的延遲數據,然後在釋放端口。
許多服務器程序都是用固定的端口,當服務器程序關閉後,有可能它的端口還會被占用一段時間,如果此時立刻在同一個主機上重啟服務器程序,由於端口已經被占用,使得服務器程序無法綁定到該端口,服務器啟動失敗,並拋出BindException:Exception in thread "main" java.net.BindException:Address already in use:JVM_Bind為了確保一個進程關閉了ServerSocket後,機試ServerSocket的setReuseAddress(true)方法:
if(!serverSocket.getReuseAddress())
serverSocket.setReuseAddress(true);
值得注意的是,serverSocket.setReuseAddress(true)方法必須在ServerSocket還沒有綁定到一個本地端口之前調用,否則執行serverSocket.setReuseAddress(true)方法無效。此外,兩個共用同一個端口的進程必須都調用serverSocket.setReyseAddress(ture)方法,才能使得一個進程關閉ServerSocket後,另一個進程的ServerSocket還能後立刻重用相同端口。
SO_RCFUF選項
設置該選項:public void setReceiveBufferSize(int size) throws SocketException
讀取該選項:public int getReceiveBufferSize() throws SocketException
SO_RCFUF表示服務器端的用於接收數據的緩衝區的大小,以字節為單位。一般說來,傳輸大的連續的數據塊(基於HTTP或FTP協議的數據傳輸)可以使用較大的緩衝區,這可以減少傳輸數據的次數,從而提高傳輸數據的效率。而對於交互式的通信(Telnet和網絡遊戲),則應該采用小的緩衝區,確保能及時把小批量的數據發送給對方。
SO_RCFUF的默認值與操作係統有關。
設定連接時間、延遲和帶寬的相對重要性
public viod setPerformancePreferences(int connectionTime, int latency, int bandwidth)
該方法的作用與Socket的setPerformancePreferences()方法的作用相同,用於設定連接時間、延遲和帶寬的相對重要性
參數int connectionTime表示短連接時間的相對重要性的
參數int latency表示最小延遲的相對重要性的
參數int bandwidth表示高帶寬的相對重要性的
如果connectionTime = 2 latency = 1 bandWidth = 3就表示高帶寬最重要,其次是最少連接時間,最後是最小延遲
創建多線程的服務器
EchoServer
while(true)
{
Socket socket = null;
try{
socket = serverSocket.accept();
.....
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(socket != null)
socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
假如同時有多個客戶請求連接,這些客戶就必須排隊等候EchoServer的相應。EchoServer無法同時與多個客戶通信。
許多實際應用要求服務器具有同時為多個客戶提供服務的能力。HTTP服務器就是最明顯的例子。任何時刻,HTTP服務器都可能接收到大量的客戶請求,每個客戶都希望能快速得到HTTP服務器的響應。如果長時間讓客戶等待,會使網站失去信譽,從而降低訪問量。
可以用並發性能來衡量一個服務器同時相應多個客戶的能力。一個具有好的並發性能的服務器必須符合兩個條件:
(1)能同時接收並處理多個客戶連接。
(2)對於每個金額戶,都會迅速給予響應。
服務器同時處理的客戶連接數目越多,並且對每個客戶做出響應的速度越快,就表明並發性能越高。
用多個線程來同時為多個客戶提供服務,這是提高服務器的並發性能的最常用的手段。
為每個客戶分配一個工作線程
服務器的主線程負責接收客戶的連接,每次接收到一個客戶連接,就會創建一個工作線程,由它負責與客戶的通信
public void service()
{
while(true)
{
Socket scoket = null;
try{
socket = serverSocket.accpet();
Thead workThread = new Thread(new Handler(socket));
workThread.start();
}catch(IOException e){
e.printStackTrace();
}
}
}
創建一個線程池,由其中的工作線程為客戶服務。
對每個客戶都分配一個新的工作線程。當工作線程與客戶通信結束,這個線程就被銷毀。
這種實現方式有以下不足之處:
(1)服務器創建和銷毀工作線程的開銷(包括所花費的時間和係統資源)很大。如果服務器需要與許多客戶通信,並且與每個客戶的通行時間都很短,那麼有可能服務器為客戶創建新線程的開銷比實際與客戶通信的開銷還要大。
(2)除了創建和銷毀線程的開銷之外,活動的線程也消耗係統資源。每個線程本身都會占用一定的內存(每個線程需要大約1M內存),如果同時有大量客戶端連接服務器,就必須創建大量工作線程。它們消耗了大量內存,可能會導致係統的內存空間不足。
(3)如果線程數目固定,並且每個線程都有很長的生命周期,那麼線程切換也是相對固定的。不同操作係統有不同的切換周期,一般在20ms左右,這裏所說的線程切換是指在Java虛擬機,以及底層操作係統的調度下,因為一個線程被銷毀後,必然要把CPU轉讓給另一個已經就緒的線程,是該線程獲得運行機會。在這種情況下,線程之間的切換不在雲鬟係統的固定切換周期,切換線程的開銷甚至比創建及銷毀線程的開銷還大。
線程池為線程生命周期開銷問題和係統資源不足問題提供解決方案。線程池中預先創建了一些工作線程,他們不斷從工作隊列中取出任務,然後執行該任務。當工作線程執行完一個任務時,就會繼續執行工作隊列中的下一個任務。
線程池具有一下優點:
(1)減少了創建和銷毀線程的次數,每個工作線程都可以一直被重用,能執行多個任務。
(2)可以根據係統的承載能力,方便調整線程池中線程的數目,放置因為消耗過量係統資源而導致係統崩潰。
import java.text.SimpleDateFormat;
import java.util.Date;
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) //線程池被關閉則拋出lllegalStateException異常
{
throw new lllegalStateException();
}
if(task != null)
{
workQueue.add(task);
notify(); //喚醒正在getTask()方法中等待任務的工作線程
}
}
/**
* 從工作隊列中取出一個任務,工作線程會調用此方法
**/
protected synchronized Runnable getTask() throws
InterruptedException
{
if(workQueue.size() == 0)
{
if(isClosed)
{
System.out.println(Thread.currentThread().getName() + "等待接受任務");
}
while(workQueue.size() == 0)
{
if(isClosed)
return null;
wait(); //如果工作隊列中沒有任務,就等待任務
}
if(workQueue.size() != 0)
{
Customer c = (Customer)workQueue.set(0);
System.err.println(Thread.currentThread().getName() + "執行" + c.getName());
}
return workQueue.removeFirst();
}
}
/* 關閉線程池*/
public synchronozed 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(thrads);
for(int i = 0; i < count; i++)
{
try
{
threads[i].join(); //等待工作線程運行結束
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
}
private class WorkThread extends Thread
{
public WorkThread()
{
//加入到當前ThreadPool線程組中
super(ThreadPool.this, "WorkThread " + (threadID++));
}
public void run()
{
while(!idInterrupted()) //isInterrupted()方法繼承自Thread類,判斷線程是否被中斷
{
Runnable task = null;
try{
task = getTask();
}catch(InterruptedException ex){
}
if(task = null)
return;
try{//運行任務,異常在catch代碼塊中捕獲
task.run();
}catch(Throwable t)
{
t.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException
{
main1();
}
private static void main1() throws InterriptedException
{
ThradPool pool = new ThreadPool(3);
Customer c1 = new Customer("1111111111");
Customer c2 = new Customer("2222222222");
Customer c3 = new Customer("3333333333");
Customer c4 = new Customer("4444444444");
Customer c5 = new Customer("5555555555");
Thread.sleep(2000);
System.out.println("===========");
pool.execute(c1);
pool.execute(c2);
pool.execute(c3);
pool.execute(c4);
pool.execute(c5);
Thread.sleep(2000);
System.out.println("當前活動的線程數:" + pool.activeCount);
pool.close();
}
static class Customer implements Runnable
{
private String name;
public Customer()
{
}
public Customer(String name)
{
this.name = name;
}
public void run()
{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try{
System.out.println("start: " + sdf.format(new Date()));
Thread.slepp(5000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("end: " + sdf.format(new Date()));
}
public String getName()
{
return name;
}
}
利用JDK的Java類庫中線程的線程庫,由它的工作線程來為客戶服務
java.util.concurrent包提供了現成的線程池的實現
Executor接口表示線程池,它的execute(Runnable task)方法用來執行Runnable類型的任務,Executor的子接口ExecutorService中聲明了管理線程池的一些方法,比如用來關閉線程池的shutdown()方法等。Executor類中包含一些靜態方法,它們負責生成各種類型的線程池ExecutorService實例。
newCachedThreadPool()在有任務時才創建線程池,空閑線程保留60秒
newFixedThreadPool(int nThreads)創建含nThreads線程的線程池,空閑線程會一直保留
newSingleThreadExecutor()創建一個隻含有單個線程的線程池
newScheduledThreadPool(int corePoolSize)線程池按時間計劃來執行任務,允許用戶設置計劃任務的時間,sorePoolSize設置線程池中線程的最小數目,當任務較多時,線程池可能會創建更多的線程來執行任務。
使用線程池的注意事項:
雖然線程池能大大提高服務器的並發性能,但使用它會存在一定風險。與所有多線程應用程序一樣,用線程池勾踐的應用程序容易產生各種並發問題,如對共享資源的競爭和死鎖。此外,如果線程池本身的實現不健壯,或者沒有合理的使用線程池,還容易導致與線程池有關的死鎖、係統資源不足和線程泄露等問題。
(1)死鎖
任何多線程應用都有死鎖風險。造成死鎖的最簡單的情形是,線程A持有對象X的鎖,並且在等待對象Y的鎖,而線程B持有對象Y的鎖,並且在等待對象X的鎖。線程A與線程B都不釋放自己持有的鎖,並且等待對方的鎖,這就導致兩個線程永遠等待下去,死鎖就這樣產生了。雖然,任何多線程程序都有死鎖的風險,但線程池還會導致另外一種死鎖。在這種情形下,假定線程池中的所有工作線程都在執行各自任務時被阻塞了,他們都在等待某個任務A的執行結果。而任務A依然在工作隊列中,由於沒有空閑線程,使得任務A一直不能被執行。這使得線程池中的所有工作線程都永遠阻塞下去,死鎖就這樣產生了。
(2)係統資源不足
如果線程池中的線程數目非常多,這些線程會消耗包括內存和其他係統資源在內的大量資源,從而嚴重影響係統性能。
(3)並發錯誤
線程池的工作隊列依靠wait()和notify()來使工作線程及時取得任務,但這兩個方法都難於使用。如果編碼不正確,可能會丟失通知,導致工作線程一直保持空閑狀態,無視工作隊列中需要處理的任務。因此使用這些方法時,必須格外的小心,即便是專家也可能在這方麵出錯。最好使用現有的、比較成熟的線程池。
例如,直接使用java.util.concurrent包中的線程池類。
(4)線程泄漏
使用線程池的一個嚴重風險就是線程泄漏。對於工作線程數目固定的線程池,如果工作線程在執行任務時拋出RuntimeException或Error,並且這些異常或錯誤沒有被捕獲,那麼這個工作線程就會異常終止,使得線程池永久失去了一個工作線程,如果所有的工作線程都異常終止,線程池就最終變為空,沒有任何可用的工作線程來處理任務。
導致線程泄漏的另一種情形是,工作線程在執行一個任務時被阻塞,如等待用戶的輸入數據,但是由於用戶一直不輸入數據導致這個工作線程一直被阻塞。這樣的工作線程名存實亡,它實際上不執行任何任務了。假如線程池中所有的工作線程都處於這樣的阻塞狀態,那麼線程池就無法處理新加入的任務了。
(5)任務過載
當工作隊列中有大量排隊等候執行的任務時,這些人物本身可能會消耗太多的係統資源,而引起係統資源缺乏。
綜上所述,線程池可能會帶來種種的風險,為了盡可能的避免它們,使用線程池時需要遵循以下原則:
(1)如果任務A在執行過程中需要同步等待任務B的執行結果,那麼任務A不適合加入到線程池的工作隊列中。如果把像任務A一樣的需要等待其他任務執行結果的任務加入到工作隊列中,可能會導致線程池的死鎖。
(2)如果執行某個任務時可能會阻塞,並且是長時間的阻塞,則應該設定超時時間,避免工作線程永久的阻塞下去而導致線程泄漏。在服務器程序中,當線程等待客戶連接,或者等待客戶發送的數據時,都可能會阻塞。可以通過一下方式設定超市時間:
調用ServerSocket的setSoTimeOut(int timeout)方法,設定等待客戶連接的超時時間。
對於每個與客戶連接的Socket,調用該Socket的setSoTimeOut(int timeout)方法,設定等待客戶發送數據的超時時間。
(3)了解任務的特點,分析任務是執行經常會阻塞的I/O操作,還是執行一直不回阻塞的運算操作。前者時斷時續地占用CPU,而後者對CPU具有更高的利用率。預計完成任務大概需要多長時間?是短時間任務還是長時間任務?
(4)調整線程池的大小。線程池的最佳大小主要取決於係統的可用CPU的數目,以及工作隊列中任務的特點,假如在一個具有N個CPU的係統上隻有一個工作隊列,並且其中全部是運算性質(不會阻塞)的任務,那麼當線程池具有N或N+1個工作線程時,一般會獲得最大的CPU利用率。
(5)避免任務過載。服務器應根據係統的承載能力,限製客戶並發連接的數目。當客戶並發連接的數目超過了限製值,服務器可以拒絕連接請求,並友好地告知客戶:服務器正忙,請稍後再試。
關閉服務器
強行終止服務器程序會導致服務器中正在執行的任務被忽然中斷,服務器除了在8000端口監聽普通客戶程序EchoClient的連接外,還會在8001端口監聽管理程序AdminCLient的連接。當服務器在8001端口接收到了AdminClient發送的"shutdown"命令時,EchoServer就會開始關閉服務器,它不會再接受任何心得EchoClient進程的連接請求,對於哪些已經接收但是還沒有處理的客戶連接,則會丟棄與該客戶的通信任務,而不會把通信任務加入到線程池的工作隊列中。另外,EchoServer會等到線程池把當前工作隊列中的所有任務執行完,才結束程序。
最後更新:2017-04-03 12:54:53