初識Socket通訊編程(一)


一、什么是socket?
  當兩台計算機需要通信的時候,往往我們使用的都是TCP去實現的,但是並不會直接去操作TCP協議,通常是通過Socket進行tcp通信。Socket是操作系統提供給開發者的一個接口,通過它,就可以實現設備之間的通信。
 
二、TCP是如何通信的?
  TCP連接和斷開分別會存在3次握手/4此握手的過程,並且在此過程中包含了發送數據的長度(接受數據的長度),無容置疑,這個過程是復雜的,這里我們不需要做深入的探討。如果有興趣,可以參考此文章,這里詳細的解釋了TCP通信的過程:
 
三、Socket消息的收發
  在Java中處理socket的方式有三種:
  1. 傳統的io流方式(BIO模式),阻塞型;
  2. NIO的方式;
  3. AIO的方式;
  這里只介紹傳統的IO流方式的tcp連接,即InputStream和OutputStream的方式讀取和寫入數據。對於長連接,通常情況可能我們如下做:
//<--------------服務端代碼-------------------->
public class SocketReadLister implements Runnable {

    private final int tcpPort=9999; private ServerSocket serverSocket; @Override public void run() { try { serverSocket = new ServerSocket(this.tcpPort); while(true){ Socket socket = serverSocket.accept(); //socket.setSoTimeout(5*1000);//設置讀取數據超時時間為5s new Thread(new SocketReadThread(socket)).start(); } }catch (Exception e){ e.printStackTrace(); } } public static void main(String[] args) throws Exception{ new Thread(new SocketReadLister()).start(); } } public class SocketReadThread implements Runnable { private Socket socket; public SocketReadThread(Socket socket) { this.socket = socket; } @Override public void run() { byte[] data = new byte[1024]; try { InputStream is=socket.getInputStream(); int length=0; int num=is.available(); while((length = is.read(data)) != -1){ String result = new String(data); System.out.println("數據available:"+num); System.out.println("數據:"+result); System.out.println("length:" + length); } System.out.print("結束數據讀取:"+length); }catch (SocketTimeoutException socketTimeoutException){ try { Thread.sleep(2*1000); }catch (Exception e) { e.printStackTrace(); } run(); } catch (Exception e){ e.printStackTrace(); try { socket.close(); }catch (IOException io){ io.printStackTrace(); } } } }
//<---------------------客戶端代碼---------------------------->
public class SocketClient implements Runnable {
    private final int tcpPort=9999; private Socket socket; @Override public void run() { String msg = "ab23567787hdhfhhfy"; byte[] byteMsg = msg.getBytes(); try { socket = new Socket("127.0.0.1", 9999); OutputStream out = socket.getOutputStream(); InputStream inputStream=socket.getInputStream(); out.write(byteMsg); Thread.sleep(10*1000); char[] chars=msg.toCharArray(); String str=""; /*out.flush();*/ for(int i=0;i<msg.length();i++) { str=chars[i]+"-"+i; out.write(str.getBytes()); Thread.sleep(1*1000); } byte[] bytes=new byte[8]; while(true) { if(inputStream.available()>0) { if(inputStream.read(bytes)!=-1) { System.out.println(new String(bytes)); } } Thread.sleep(10*1000); } } catch (Exception e) { e.printStackTrace(); try { socket.close(); } catch (IOException e2) { e2.printStackTrace(); } } } public static void main(String[] args) { new Thread(new SocketClient()).start(); } }
  正如代碼中所示,通常情況下我們在while循環中將is.read(data)) != -1作為判斷依據,判斷是否繼續讀取,這種情況下,確實可以將數據完整的讀取,但是客戶端沒有傳輸數據的時候,read()方法開始阻塞,直到有數據時才繼續執行后續代碼,使得程序掛起。
  為什么會出現這種情況呢?
  在JDK中,關於read()的說明如下:當讀取到流的末尾,沒有可讀數據的時候,read()方法將返回-1,如果沒有數據,那么read()將會發生阻塞。因此,在讀取文件流的情況下,這樣是完全正確的,但是在網絡編程的情況下,socket連接不會斷開,那么InputStream的read()將永遠不會返回-1,程序將讀完數據后,繼續循環讀取然后發生阻塞。
  在InputStream中,提供了available();此方法是非阻塞的,通過它可以初步的判定socket流中是否有數據,並返回一個預估數據長度的值,但是請注意,這里是預估,並不是准確的計算出數據的長度,所以在JDK說明文檔中,有提示使用該方法獲取的值去聲明 byte[]的長度,然后讀取數據,這是錯誤的做法。這樣在每次讀取數據之前,都可以先判斷一下流中是否存在數據,然后再讀取,這樣就可以避免阻塞造成程序的掛起。代碼如下:
while(true){
    if(is.available()>0){ is.read(data); } }
  說到read(),在InputStream中提供了3個read的重載方法:read()、read(byte[])、read(byte[],int offset,int len);后面兩種讀取方法都是基於 read()實現的,同樣存在阻塞的特性,那么我們可以思考一下,假定byte[]的長度為1024,撇開while,拿read(byte[])一次性讀取來說,當另一端發送的數據不足1024個字節時,為什么這個read(byte[])沒有發生阻塞?
  關於這個問題,網上有帖子說,這跟InputStream的flush()有關,但經過測試,我不這么認為。我更加認同 https://ketao1989.github.io/2017/03/29/java-server-in-action/中所說的那樣,TCP握手期間,會傳遞數據的長度,當讀取完數據,read()返回-1,即使此時沒有讀取到1024個字節數據,剩下的用0填充,這樣就能很好的解釋這個問題了。
  Socket既然時網絡通訊用,那么由於各種原因,必然會有網絡延遲,造成socket讀取超時;socket讀取超時時,其連接任然是有效的,因此在處理該異常時不需要關閉連接。以下是代碼片段:
if (nRecv < nRecvNeed){
    int nSize = 0; wsaBuf=new byte[nRecvNeed-nRecv]; int readCount = 0; // 已經成功讀取的字節的個數 try { while (readCount < wsaBuf.length) { //Thread.sleep(100);//讀取之前先將線程休眠,避免循環時,程序占用CPU過高 try { availableNum=inputStream.available(); if(availableNum>0){ readCount += inputStream.read(wsaBuf, readCount, (wsaBuf.length - readCount));//避免數據讀取不完整  } }catch (SocketTimeoutException timeOut){ System.out.println("讀取超時,線程執行休眠操作,2秒后再讀取"); Thread.sleep(2*1000); } } }catch (Exception e){ System.out.println("讀取數據異常"); e.printStackTrace(); close();//關閉socket連接 break; } nSize=wsaBuf.length; nRecv+=nSize; }
  另外,需要補充說明的是,socket.close()方法執行后,只能更改本端的連接狀態,不能將該狀態通知給對端,也就是說如果服務端或客戶端一方執行了close(),另一端並不知道此時連接已經斷開了。
  此外,以上代碼還存在一個很嚴重的問題亟待解決,這也是在開發中容易忽視的地方——程序能正常運行,但CPU占用過高;原因如下:
  當readCount < wsaBuf.length,即數據還未讀取完整時,線程會持續不斷的從socket流中讀取數據,由於這里使用了inputStream.available()來判斷使用需要讀取數據,當沒有數據傳輸的時候,此處就變成了一個死循環,說到此處,原因就非常明了了,在計算機運行過程中無論他是單核還是多核,系統獲取計算機資源(CPU等)都是按照時間分片的方式進行的,同一時間有且只有一個線程能獲取到系統資源,所以當遇到死循環時,系統資源一直得不到釋放,因此CPU會越來越高,解決的辦法是在循環中對程序進行線程休眠一定時間。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM