初識Socket通信編程(一)

1、什麼是socket?
  當兩臺計算機須要通訊的時候,每每咱們使用的都是TCP去實現的,可是並不會直接去操做TCP協議,一般是經過Socket進行tcp通訊。Socket是操做系統提供給開發者的一個接口,經過它,就能夠實現設備之間的通訊。
 
2、TCP是如何通訊的?
  TCP鏈接和斷開分別會存在3次握手/4此握手的過程,而且在此過程當中包含了發送數據的長度(接受數據的長度),無容置疑,這個過程是複雜的,這裏咱們不須要作深刻的探討。若是有興趣,能夠參考此文章,這裏詳細的解釋了TCP通訊的過程:
 
3、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會愈來愈高,解決的辦法是在循環中對程序進行線程休眠必定時間。
相關文章
相關標籤/搜索