初識Socket通訊編程(一)

来源:https://www.cnblogs.com/onedayinMay/archive/2020/01/16/12203599.html
-Advertisement-
Play Games

一、什麼是socket? 當兩台電腦需要通信的時候,往往我們使用的都是TCP去實現的,但是並不會直接去操作TCP協議,通常是通過Socket進行tcp通信。Socket是操作系統提供給開發者的一個介面,通過它,就可以實現設備之間的通信。 二、TCP是如何通信的? TCP連接和斷開分別會存在3次握手 ...


一、什麼是socket?   當兩台電腦需要通信的時候,往往我們使用的都是TCP去實現的,但是並不會直接去操作TCP協議,通常是通過Socket進行tcp通信。Socket是操作系統提供給開發者的一個介面,通過它,就可以實現設備之間的通信。   二、TCP是如何通信的?   TCP連接和斷開分別會存在3次握手/4此握手的過程,並且在此過程中包含了發送數據的長度(接受數據的長度),無容置疑,這個過程是複雜的,這裡我們不需要做深入的探討。如果有興趣,可以參考此文章,這裡詳細的解釋了TCP通信的過程: https://ketao1989.github.io/2017/03/29/java-server-in-action/   三、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會越來越高,解決的辦法是在迴圈中對程式進行線程休眠一定時間。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • JS 百度地圖路書 動態路線 <!DOCTYPE html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="initial-sca ...
  • 定義 在一個分散式系統(指系統中的節點互相連接並共用數據)中,當涉及讀寫操作時,只能保證一致性 (Consistency)、可用性 (Availability)、分區容錯性 (Partition Tolerance)三者中的兩個,另外一個必須被犧牲。 一致性:CAP中的C和ACID 中的C不是一個含 ...
  • 既要低頭趕路,又要抬頭望天,科技是為人服務的,任何技術背後都有更深層次的考量,在本系列的第一篇文章中我們聊了微服務的本質,它是一種可以加速分工、促進合作的新協作機制。知其然,知其所以然,在第二篇文章中我們剖析了微服務為什麼可以加速分工、促進合作,今天我們再接著來聊聊怎樣開啟微服務架構之旅。 ...
  • 二進位 0000 0000 0000 0000 0000 0000 0000 0001 // 2^0 0000 0000 0000 0000 0000 0000 0000 0010 // 2^1 0000 0000 0000 0000 0000 0000 0000 0100 // 2^2 0000 ...
  • 消息中間件 消息的可靠性傳遞 前言 消息中間件的可靠性消息傳遞,是消息中間件領域非常重要的方案落實問題(在這之前的MQ理論,MQ選型是抽象層次更高的問題,這裡不談)。 並且這個問題與日常開發是存在較大的關聯的。可以這麼說,凡是使用了MQ的,機會都要考慮這個問題。當然也有一些原始數據採集,日誌數據收集 ...
  • 一、訪問網路的兩種方法 1.get:利用參數給伺服器傳遞信息;參數為dict,然後parse解碼 2.post:一般向伺服器傳遞參數使用;post是把信息自動加密處理;如果想要使用post信息,需要使用到data參數 3.Content-Type:application/x-www.form-url ...
  • 前面我們在章節“Socket通訊探索(一)”中如何實現一個tcp連接,但是這僅僅是一個最初級的BIO實現,且沒有添加線程池,實際應用中很少採用這種方式,因為不得不考慮當大量的Tcp連接建立的時候,服務端如何安全穩定的運行?為什麼呢? 1、BIO實現方式,是阻塞式的(上一節最後面的實現方式雖然無數據的 ...
  • 引言 問題 現在我們使用通用的應用程式或庫來相互通信。例如,我們經常使用HTTP客戶機從web伺服器檢索信息,並通過web服務調用遠程過程調用。然而,通用協議或其實現有時不能很好地進行擴展。這就像我們不使用通用HTTP伺服器來交換巨大的文件、電子郵件消息和近乎實時的消息(如財務信息和多人游戲數據)一 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...