socket連接是Java中進行通信的基本方式,也是效率最高的方式,雖然他有http等讓是進行http請求,但是如果是進行tcp、下載等通信,還是使用socket更好。Java中封裝了非常完美的socket機制,使用也非常簡單。主要包括socket和serversocket。
socket的使用非常簡單,主要包括的構造方法有:socket(),socket(string host,string port),socket(Inetaddress address,int port)等,非常明白了,通過傳入host和port進行socket的請求,當在創建相應的套接字實例的時候,會自動去對相應的ip和port進行連接,只有當連接成功,才表示相應的套接字建立成功,才可以進行相應的I/O操作。通過getOutputStream和getInputStream獲取相應的輸入輸出流,進行相應的I/O操作,但是有一個是比較特別的,getChannel用來獲取SocketChannel,他之所以特別是因為他屬於java.nio.channels下面的類,其繼承於java.nio.channels.SelectableChannel,就是說在進行nio非阻塞式的請求連接時,他非常有用,具體參見http://www.cnblogs.com/likwo/archive/2010/06/29/1767814.html。可能有些人會問,對於可以通過設置服務器連接的timeout來防止過多的阻塞,但是如果對於超過timeout,socket一般是拋出超時異常,這樣就算對異常進行了處理,也將會從新建立socket連接,浪費消費 重建的資源。例如QQ聊天,當你打開一個聊天面板,很久不說話的時候,並不會自動為你斷開socket連接,而是一直處於阻塞狀態,直到你發送了新的信息,再進行處理,因此nio的阻塞方式會更好些。
對於serversocket也比較簡單,常用的只有四個構造函數:
l ServerSocket()throws IOException
l ServerSocket(int port) throws IOException
l ServerSocket(int port, int backlog) throws IOException
l ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
分別對這幾個構造進行簡單的解釋:
第一個無參構造,只是創建一個serversocket實例,但是不進行任何端口的監聽,你還必須通過bind方法進行端口的綁定,好處就是在綁定之前可以進行相應屬性的設置,例如so_reuseaddress等;
第二個構造函數需要一個端口(1024-65535),一般不使用0-1023之間的,這個屬於系統占用的預留端口,但是如果你傳入的端口號為0,則會默認使用匿名端口,就是系統隨機分配一個端口,進行暫時通信,這個匿名方式,一般情況下不使用;
第三個構造函數需要一個端口,和一個backlog(監聽對列大小),serversocket進行某端口的監聽,當有多個連接請求是,每個請求默認都會放入一個請求隊列里,如果你沒有設置這個值,則默認為操作系統的值,根據不同的系統有所不同,例如40等,有幾種情況,這個值將會失效:大於操作系統默認值|小於等於0|沒有設置。如果設置了,而沒有及時對隊列進行處理,則會報ConnectException異常;
第四個構造函數除了具有端口、隊列大小外,還具有一個參數是ip地址,就是進行相應ip地址的綁定。當然,這個進行的是服務器ip地址的綁定,不會限制客戶端的ip訪問。當一台服務器存在多個網卡的時候,就需要通過這個參數來設置客戶端訪問的ip。
服務器socket的關閉,通過使用close進行關閉,使用isclosed進行判斷,還可以進行端口的綁定判斷。
對於服務器close方法,需要有一點進行說明:調用close方法之后,操作系統並不會立即進行端口的釋放,依舊會對舊端口占用一段時間,以防止客戶端發送的數據有延遲現象。因此有時候,就算你進行了close方法的調用,進行了端口的釋放,但是如果你立即進行同一個端口的連接時,依舊會包端口占用異常,這個是可以理解的。
serversocket通過使用accept方法進行客戶端請求的處理,每當請求隊列里有客戶端請求時,serversocket就會從隊列頂端取一個socket請求進行處理,生成一個socket來負責與客戶端通信。如果一個時間只能處理一個socket,當有多個客戶端請求時,則必須要排隊處理,等待所有前面的socket處理完,這是個極其痛苦,並且不合理的過程。因此,引入了線程的概念。
為了實現客戶端請求的快速相應和快速處理,據是高並發,則必須使用多線程機制。主題思想是:serversocket通過accept建立一個socket,然后起一個現成,把這個socket扔給新建的線程進行處理,而serversocket主線程,則繼續去監聽端口,以此實現多線程通信。一般有三種方式:
1、每一個socket請求就建立一個線程。這個是最簡單的方式,大致代碼如下:
- public void service() {
- while (true) {
- Socket socket=null;
- try {
- socket = serverSocket.accept(); //接收客戶連接
- Thread workThread=new Thread(new Handler(socket)); //創建一個工作線程
- workThread.start(); //啟動工作線程
- }catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
上面的方式非常簡單,能夠處理基本的多線程問題,當數據量不大時,應該沒有什么問題,但是如果數據量過大時,就會出現嚴重的性能,甚至是宕機問題。其缺點主要有如下幾個:
a:每個socket請求,建立一個連接,當每個都是進行簡短的通信時,則異常的耗費系統建立、銷毀線程資源。
b:如果建立線程太多,每個線程都會占用一定的系統內存,這樣將導致內存溢出。
c:頻繁地對線程進行建立 銷毀,會導致操作系統進行頻繁的cpu切換線程切換,這樣也會非常耗費系統資源。
2、自己實現線程池。
自己寫線程池,能夠對線程池的工作原理以及工作情況,更加的了解和控制,但是由於線程池必然涉及到多線程問題,因此為了防止出現死鎖、線程泄漏、並發錯誤、任務過載等問題,需要性能非常好的機制,一般不推薦個人現實。如果非要實現,可以通過使用linkedlist<runnable>的數據結構來實現一個多線程隊列。下面,還是主要推薦jdk已經幫你實現的線程池。
3、使用jdk自帶的線程池。jar包是:java.util.concurrent
這個jar包都是一些並發編程會經常使用到的工具類,主要有阻塞隊列,原子操作的map以及線程池等。其基本包括Executor、ExecutorService接口和Executors類,兩個接口定義了執行線程的方法,而Executors則定義了管理線程池的方法,主要可以創建的常用線程池有:
newSingleThreadScheduledExecutor() 創建一個可以延遲執行和定時執行的單線程線程池
newSingleThreadExecutor() 創建一個運行單線程的線程池
newScheduledThreadPool(int corePoolSize) 創建一個可以延遲執行和定時執行的線程池,設定線程數
newFixedThreadPool(int nThreads) 創建一個固定線程數的線程池
newCachedThreadPool() 創建一個帶有緩沖區的線程池
然后通過生成的線程池的execute方法進行線程的執行,具體可以百度下哈哈
使用線程池有以下幾點風險:
1、死鎖。任何多線程都不可避免的問題。但是對於線程池可能會存在另一種死鎖:就是線程池中的線程都在等待一個資源,而這個資源需要執行A后得到,而由於線程池沒有可用的線程,導致A無法執行,故而也會發生死鎖。
2、系統資源不足。多線程必定需要大量的內存資源,可能出現內存泄漏問題。
3、並發錯誤。
4、線程泄漏。就是所有的線程池中的線程都在等待輸入資源,或者都拋出了異常而沒有捕獲,則會導致線程池中所有的線程假死。
5、線程過載。運行線程過多,導致過載,這個可以通過設置線程池的大小來進行一定成功的避免。
至於如何避免,主要是要在使用多線程時要小心,同時不要使用destroy despause 等操作,盡量使用sleep notify wait等操作,在這里不詳細說明了。
