1. 程式人生 > >vs—socket—udp詳細通訊過程

vs—socket—udp詳細通訊過程

socket和tcp/ip協議的關係

Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織資料,以符合指定的協議。

在這裡插入圖片描述

Socket基本概念

  • 網路上的兩個程式通過一個雙向的通訊連線實現資料的交換,這個連線的一端稱為一個socket。

  • 建立網路通訊連線至少要一對埠號(socket)。socket本質是程式設計介面(API),對TCP/IP的封裝,TCP/IP也要提供可供程式設計師做網路開發所用的介面,這就是Socket程式設計介面;HTTP是轎車,提供了封裝或者顯示資料的具體形式;Socket是發動機,提供了網路通訊的能力。

  • Socket的英文原義是“孔”或“插座”。作為BSD UNIX的程序通訊機制,取後一種意思。通常也稱作"套接字",用於描述IP地址和埠,是一個通訊鏈的控制代碼,可以用來實現不同虛擬機器或不同計算機之間的通訊。在Internet上的主機一般運行了多個服務軟體,同時提供幾種服務。每種服務都開啟一個Socket,並繫結到一個埠上,不同的埠對應於不同的服務。Socket正如其英文原義那樣,像一個多孔插座。一臺主機猶如佈滿各種插座的房間,每個插座有一個編號,有的插座提供220伏交流電, 有的提供110伏交流電,有的則提供有線電視節目。 客戶軟體將插頭插到不同編號的插座,就可以得到不同的服務。

中文名 套接字
常用型別 流式socket和資料包式socket
相關模式 對等模式 、C/S模式
相關應用 c++、java、python
  • 應用程式通常通過“套接字”向網路發出請求或者應答網路請求。
  • socket實際上提供程序通訊的端點。程序通訊之前,雙方首先必須各自建立一個端點,否則是沒有辦法建立聯絡並相互通訊的。
  • 在網間網內部,每一個Socket用一個半相關模式:(協議,本地地址,本地埠)
  • 一個完整的socket有一個本地唯一的Socket號,由作業系統分配。
  • Socket是面向客戶/伺服器模型而設計的,針對客戶和伺服器程式提供不同的Socket系統呼叫。客戶隨機申請一個Socket(相當於一個想打電話的人可以在任何一臺入網電話上撥號呼叫),系統為之分配一個Socket號;伺服器擁有全域性公認的Socket,任何客戶都可以向它發出連線請求和資訊請求(相當於一個被呼叫的電話擁有一個呼叫方知道的電話號碼)。 Socket利用客戶/伺服器模式巧妙地解決了程序之間建立通訊連線的問題。伺服器Socket半相關為全域性所公認非常重要。讀者不妨考慮一下,兩個完全隨機的使用者程序之間如何建立通訊?假如通訊雙方沒有任何一方的Socket固定,就好比打電話的雙方彼此不知道對方的電話號碼,要通話是不可能的。

套接字連線的步驟

套接字之間的連線過程可以分為3個步驟:

  1. 伺服器監聽 是伺服器端套接字並不定位具體的客戶端套接字,而是處於等待連線的狀態,實時監控網路狀態。

  2. 客服端請求‘ 客戶端的套接字提出連線請求,要連線的目標是伺服器端的套接字。為此,客戶端的套接字必須先描述它要連線的伺服器的套接字,指出伺服器端套接字的地址和埠號,然後就向伺服器端套接字提出連線請求。

  3. 連線確認 當伺服器套接字監聽到或收到客戶端套接字的連線請求,它就響應客戶端套接字的請求,建立一個新的執行緒,把伺服器端套接字的描述發給客戶端,一旦客戶端確認了此描述,連線就建立好了。而伺服器端套接字繼續處於監聽狀態,繼續接受其他客戶端套接字的連線請求。

常用函式

  • 建立

    1. 函式原型: int socket(int domain,int type,int protocol);

    2. 引數說明

      1. domain:協議域,又稱協議族。 1)常用的協議族有:AF_INET,AF_INET6,AF_LOCAL(或稱AF_UNIX,Unix域Socket)、AF_ROUTE等。 2)協議族決定了socket的地址型別,在通訊中必須採用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與埠號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。 3)用於確定哪個通訊模式。
      2. type:指定socket型別。 1) 常用的socket型別有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。 2) 流式Socket(SOCK_STREAM)是一種面向連線的Socket,針對於面向連線的TCP服務應用。 3)資料報式Socket(SOCK_DGRAM)是一種無連線的Socket,對應於無連線的UDP服務應用。
      3. protocol:指定協議。 1)常用協議有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等。
      協議 對應的協議
      PROTO_TCP TCP傳輸協議
      ROTO_UDP UDP傳輸協議
      ROTO_STPC STCP傳輸協議
      PROTO_TICP TIPC
    3. 注意:

      1. type和protocol不可以隨意組合,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當第三個引數為0時,會自動選擇第二個引數型別對應的預設協議。
      2. WindowsSocket下protocol引數中不存在IPPROTO_STCP
    4. 返回值 若呼叫成功,返回新建立的套接字的描述符, 若失敗,返回INVALID_SOCKET(Linux下返回-1)。 套接字描述符是一個整數型別的值。每個程序的程序空間裡都有一個套接字描述符表,該表中存放著套接字描述符和套接字資料結構的對應關係。該表中有一個欄位存放新建立的套接字的描述符,另一個欄位存放套接字資料結構的地址,因此根據套接字描述符就可以找到其對應的套接字資料結構。每個程序在自己的程序空間裡都有一個套接字描述符表但是套接字資料結構都是在作業系統的核心緩衝裡。

  • 繫結

    1. 函式原型 int bind(SOCKET socket,const struct socketAddr*,socklen_t address_len);
    2. 引數說明:
      1. socket 一個套接字的描述符
      2. socketAddr* 是一個sockaddr結構的指標,該結構包含了要結合的地址的埠號。
      3. address_len 確定address緩衝區的長度。
    3. 返回值: 函式執行成功,返回0,否則返回SOCKET_ERROR。
  • 接收連線請求

    1. 函式原型 int accept(int fd,struct socketaddr* addr,socklen_t* len);
    2. 引數說明
    引數 引數說明
    fd 套接字描述符
    addr 返回連線者的地址
    len 接收返回地址的緩衝區長度
    1. 返回值 若成功,返回客服端的檔案描述符 若失敗,返回-1.
  • 接收

    1. 函式原型 int recv(Socket socket,char FAR* buf,int len ,int flag);

      1. 引數說明:
        1. socket 一個標識已經連線套介面的描述字。
        2. buf 用於已經接收資料的緩衝區
        3. len 緩衝區長度
        4. flag 指定呼叫方式。 取值:MSG_PEEK檢視當前資料,資料將被複制到緩衝區中,但是並不從輸入佇列中刪除。
      2. 返回值 若無錯誤,返回讀入的位元組數。 若連線已經中斷,返回0. 否則的話,返回SOCKET_ERROR錯誤 應用程式可以根據WSAGetLastError()獲取相應錯誤程式碼。
    2. 函式原型: ssize_t recvfrom(int sockfd,void buf,int len,unsigned int flags,struct socketaddr* from,socket_t* fromlen);

      1. 引數說明

        1. sockfd 標識一個已經連線套介面的描述字。
        2. buf 接收資料緩衝區
        3. len 接收資料緩衝區的長度
        4. flags 呼叫操作方式。是以下一個或者多個標誌的組合體,可以通過or操作連在一起:
        MSG_DONTWAIT 操作不會阻塞。
        MSG_ERRQUEUE 指示應該從套接字的錯誤佇列上接收錯誤值,依據不同的協議,錯誤值以某種輔佐性訊息的方式傳遞進來,使用者應該提供足夠大的緩衝區。導致錯誤的原封包通過msg_iovec作為一般的資料來傳遞。導致錯誤的資料報原目標地址作為msg_name被提供。錯誤以sock_extended_err結構形態被使用。
        MSG_PEEK 指示資料接收後,在接收佇列中保留原資料,不將其刪除,隨後的讀操作還可以接收相同的資料。
        MSG_TRUNC 返回封包的實際長度,即使它比所提供的緩衝區更長。只對Packet套接字有效。
        MSG_WAITALL 要求阻塞操作,直到請求得到完整的滿足。然而,如果捕捉到訊號,錯誤或者連線斷開發生,或者下次被接收的資料型別不同,仍會返回少於請求量的資料。
        MSG_EOR 指示記錄的結束,返回的資料完成一個記錄。
        MSG_TRUNC 指明資料報尾部資料已被丟棄,因為它比所提供的快取區需要更多的空間。
        MSG_CTRUNC 指明由於緩衝區空間不足,一些控制資料已被丟棄。
        MSG_OOB 指示接收到out-of-band資料。即需要優先處理的資料。
        MSG_ERRQUEUE 指示除了來自套接字錯誤佇列的錯誤外,沒有接受到其它資料。
        1. from(可選) 指標,指向裝有源地址的緩衝區。
        2. fromlen:(可選) 指標,指向from緩衝區長度值
  • 傳送

    1. 函式原型 int sendto(SOCKET socket,const char FAR* buf,int size,int flags,const struct sockaddr FAR* to,int tolen);
    2. 引數說明
    引數 引數說明
    socket 套接字
    buf 待發送資料的緩衝區
    size 緩衝區長度
    flags 呼叫方式標誌位,一般為0,改變Flags,將會改變sendto傳送的形式
    addr(可選) 指向目的的套接字的地址
    tolen addr所指地址的長度
    1. 返回值 若成功,返回傳送的位元組數 若失敗,返回SOCKET_ERROR

如何告知對方已傳送完命令

其實這個問題還是比較重要的,正常來說,客戶端開啟一個輸出流,如果不做約定,也不關閉它,那麼服務端永遠不知道客戶端是否傳送完訊息,那麼服務端會一直等待下去,直到讀取超時。所以怎麼告知服務端已經發送完訊息就顯得特別重要。

  • 通過Socket關閉 當Socket關閉的時候,服務端就會收到響應的關閉訊號,那麼服務端也就知道流已經關閉了,這個時候讀取操作完成,就可以繼續後續工作。

    但是這種方式有一些缺點

    1. 客戶端Socket關閉後,將不能接受服務端傳送的訊息,也不能再次傳送訊息
    2. 如果客戶端想再次傳送訊息,需要重現建立Socket連線
  • 通過socket關閉輸出流的方式

    		這種方式呼叫的方法是:
    		      socket.shutdownOutput();
    		而不是(outputStream為傳送訊息到服務端開啟的輸出流):
    		     outputStream.close();
    
    1. 如果關閉了輸出流,那麼相應的Socket也將關閉,和直接關閉Socket一個性質。呼叫Socket的shutdownOutput()方法,底層會告知服務端我這邊已經寫完了,那麼服務端收到訊息後,就能知道已經讀取完訊息,如果服務端有要返回給客戶的訊息那麼就可以通過服務端的輸出流傳送給客戶端,如果沒有,直接關閉Socket。
    2. 這種方式通過關閉客戶端的輸出流,告知服務端已經寫完了,雖然可以讀到服務端傳送的訊息,但是還是有一點點缺點: 不能再次傳送訊息給服務端,如果再次傳送,需要重新建立Socket連線 這個缺點,在訪問頻率比較高的情況下將是一個需要優化的地方。
  • 通過約定的符號

    1. 這種方式的用法,就是雙方約定一個字元或者一個短語,來當做訊息傳送完成的標識,通常這麼做就需要改造讀取方法。

      假如約定單端的一行為end,代表傳送完成,例如下面的訊息,end則代表訊息傳送完成: what is your name? end

      此時需要改造伺服器的讀取方式:

      Socket socket = server.accept();
      // 建立好連線後,從socket中獲取輸入流,並建立緩衝區進行讀取
      BufferedReader read=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
      String line;
      StringBuilder sb = new StringBuilder();
      while ((line = read.readLine()) != null && "end".equals(line)) {
        //注意指定編碼格式,傳送方和接收方一定要統一,建議使用UTF-8
        sb.append(line);
      }
      

      可以看見,服務端不僅判斷是否讀到了流的末尾,還判斷了是否讀到了約定的末尾。

    2. 優缺點如下:

      優點:不需要關閉流,當傳送完一條命令(訊息)後可以再次傳送新的命令(訊息) 缺點:需要額外的約定結束標誌,太簡單的容易出現在要傳送的訊息中,誤被結束,太複雜的不好處理,還佔頻寬

拆包和黏包

使用Socket通訊的時候,或多或少都聽過拆包和黏包,如果沒聽過而去貿然程式設計那麼偶爾就會碰到一些莫名其妙的問題,所有有這方面的知識還是比較重要的,至少知道怎麼發生,怎麼防範。   現在先簡單說明下拆包和黏包的原因:

  • 拆包:當一次傳送(Socket)的資料量過大,而底層(TCP/IP)不支援一次傳送那麼大的資料量,則會發生拆包現象。
  • 黏包:當在短時間內傳送(Socket)很多資料量小的包時,底層(TCP/IP)會根據一定的演算法(指Nagle)把一些包合作為一個包傳送。

首先可以明確的是,大部分情況下我們是不希望發生拆包和黏包的(如果希望發生,什麼都去做即可),那麼怎麼去避免呢,下面進行詳解?

  • 黏包   首先我們應該正確看待黏包,黏包實際上是對網路通訊的一種優化,假如說上層只發送一個位元組資料,而底層卻傳送了41個位元組,其中20位元組的I P首部、 20位元組的T C P首部和1個位元組的資料,而且傳送完後還需要確認,這麼做浪費了頻寬,量大時還會造成網路擁堵。當然它還是有一定的缺點的,就是因為它會合並一些包會導致資料不能立即傳送出去,會造成延遲,如果能接受(一般延遲為200ms),那麼還是不建議關閉這種優化,如果因為黏包會造成業務上的錯誤,那麼請改正你的服務端讀取演算法(協議),因為即便不發生黏包,在服務端快取區也可能會合並起來一起提交給上層,推薦使用長度+型別+資料模式。   如果不希望發生黏包,那麼通過禁用TCP_NODELAY即可,Socket中也有相應的方法: void setTcpNoDelay(boolean on)   通過設定為true即可防止在傳送的時候黏包,但是當傳送的速率大於讀取的速率時,在服務端也會發生黏包,即因服務端讀取過慢,導致它一次可能讀取多個包。

  • 拆包 最大報文段長度(MSS)表示TCP傳往另一端的最大塊資料的長度。當一個連線建立時,連線的雙方都要通告各自的 MSS。客戶端會盡量滿足服務端的要求且不能大於服務端的MSS值,當沒有協商時,會使用值536位元組。雖然看起來MSS值越大越好,但是考慮到一些其他情況,這個值還是不太好確定。   如何應對拆包,那就是如何表明傳送完一條訊息了,對於已知資料長度的模式,可以構造相同大小的陣列,迴圈讀取,示例程式碼如下:

    int length=1024;//這個是讀取的到資料長度,現假定1024
    byte[] data=new byte[1024];
    int readLength=0;
    while(readLength<length){
        int read = inputStream.read(data, readLength, length-readLength);
        readLength+=read;
    }
    
    

這樣當迴圈結束後,就能讀取到完整的一條資料,而不需要考慮拆包了。

UDP協議

  • udp是一種面向無連線,不可靠的傳輸層協議。
  • upd的連線過程

    在這裡插入圖片描述

vs 實現udp通訊

伺服器端程式設計的步驟:

  1. 建立套接字(socket)
  2. 將套接字和IP地址、埠號繫結在一起(bind)
  3. 等待客戶端發起資料通訊(recvfrom/recvto)
  4. 關閉套接字

客戶端程式設計的步驟:

  1. 建立套接字(socket)
  2. 向伺服器發起通訊(recvfrom/recvto)
  3. 關閉套接字

在這裡插入圖片描述

分析:

  • 在vs中一般使用Winsock2實現網路通訊功能,所以需要引進標頭檔案winsock2.h和庫檔案"ws2_32.lib"。

       1. WinSock2 是連線系統和使用者使用的軟體之間用於交流的一個介面,這個功能就是修復軟體與系統正確的通訊的作用。
       
       2.  Winsock2 SPI(Service Provider Interface)服務提供者介面建立在Windows開放系統架構WOSA(Windows Open System Architecture)之上,是Winsock系統元件提供的面向系統底層的程式設計介面。
           Winsock系統元件向上面向使用者應用程式提供一個標準的API介面;向下在Winsock元件和Winsock服務提供者(比如TCP/IP協議棧)之間提供一個標準的SPI介面。
           各種服務提供者是Windows支援的DLL,掛載在Winsock2 的Ws2_32.dll模組下。
           對使用者應用程式使用的Winsock2 API中定義的許多內部函式來說,這些服務提供者都提供了它們的對應的運作方式(例如API函式WSAConnect有相應的SPI函式WSPConnect)。
           多數情況下,一個應用程式在呼叫Winsock2 API函式時,Ws2_32.dll會呼叫相應的Winsock2 SPI函式,利用特定的服務提供者執行所請求的服務。
           
    詳細:  https://baike.baidu.com/item/winsock2/7907481?fr=aladdin
    
  • Windows下的庫檔案目錄在哪裡吧,以便以後使用(Windows10)

    C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86

  • # pragma comment(lib,“Ws2_32.lib”) 表示連結Ws2_32.lib這個庫。 和在工程設定裡寫上鍊入Ws2_32.lib的效果一樣,不過這種方法寫的程式別人在使用你的程式碼的時候就不用再設定工程settings了。 告訴聯結器連線的時候要找ws2_32.lib,這樣你就不用在linker的lib設定裡指定這個lib了。 ws2_32.lib是winsock2的庫檔案 WinSock2就相當於連線系統和你使用的軟體之間交流的一個介面,可能這個功能就是修復軟體與系統正確的通訊的作用。

  • WASDATA WSADATA,一種資料結構。這個結構被用來儲存被WSAStartup函式呼叫後返回的Windows Sockets資料。它包含Winsock.dll執行的資料。 定義位置:Winsock.h

    結構原型: 
    typedef struct WSAData {
            WORD                    wVersion;   
            WORD                    wHighVersion;
    #ifdef _WIN64
            unsigned short          iMaxSockets;
            unsigned short          iMaxUdpDg;
            char FAR *              lpVendorInfo;
            char                    szDescription[WSADESCRIPTION_LEN+1];
            char                    szSystemStatus[WSASYS_STATUS_LEN+1];
    #else
            char                    szDescription[WSADESCRIPTION_LEN+1];
            char                    szSystemStatus[WSASYS_STATUS_LEN+1];
            unsigned short          iMaxSockets;
            unsigned short          iMaxUdpDg;
            char FAR *              lpVendorInfo;
    #endif
    } WSADATA,  FAR * LPWSADATA;
    
    各引數的含義: https://baike.baidu.com/item/WSADATA
    
  • MAKEWORD(a, b) makeword是將兩個byte型合併成一個word型,一個在高8位(b),一個在低8位(a) 返回值:一個無符號16位整形數。

    MAKEWORD(1,1)和MAKEWORD(2,2)的區別在於,前者只能一次接收一次,不能馬上傳送,而後者能。

    宣告呼叫不同的Winsock版本。 例如MAKEWORD(2,2)就是呼叫2.2版,MAKEWORD(1,1)就是呼叫1.1版。 不同版本是有區別的,例如1.1版只支援TCP/IP協議,而2.0版可以支援多協議。 2.0版有良好的向後相容性,任何使用1.1版的原始碼、二進位制檔案、應用程式都可以不加修改地在2.0規範下使用。 此外winsock 2.0支援非同步 1.1不支援非同步.

    巨集定義:#define MAKEWORD(a, b)  ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
    返回值:typedef unsigned short      WORD;
    參考: https://blog.csdn.net/happy_xiahuixiax/article/details/72637370
    
  • WSAStartup(sockVersion, &wsadata) WSAStartup,即WSA(Windows Sockets Asynchronous,Windows非同步套接字)的啟動命令。是Windows下的網路程式設計介面軟體Winsock1 或 Winsock2 裡面的一個命令。

    WSAStartup必須是應用程式或DLL呼叫的第一個Windows Sockets函式。它允許應用程式或DLL指明Windows Sockets API的版本號及獲得特定Windows Sockets實現的細節。應用程式或DLL只能在一次成功的WSAStartup()呼叫之後才能呼叫進一步的Windows Sockets API函式。

    int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData );
    ⑴ wVersionRequested:一個WORD(雙位元組)型數值,在最高版本的Windows Sockets支援呼叫者使用,高階位元組指定小版本(修訂本)號,低位位元組指定主版本號。
    ⑵lpWSAData 指向WSADATA資料結構的指標,用來接收Windows Sockets 實現的細節。
    WindowsSockets API提供的呼叫方可使用的最高版本號。高位位元組指出副版本(修正)號,低位位元組指明主版本號。
    
    參考:https://baike.baidu.com/item/WSAStartup/10237703?fr=aladdin
    
  • sockaddr結構

 truct sockaddr
 {
      unsigned short    sa_family;             /*addressfamily,AF_xxx*/
      char              sa_data[14];           /*14bytesofprotocoladdress*/
 } ;
sa_family是地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET,代表TCP/IP協議族。
sa_data是14位元組協議地址。

此資料結構用做bind、connect、recvfrom、sendto等函式的引數,指明地址資訊。但一般程式設計中並不直接針對此資料結構操作,而是使用另一個與sockaddr等價的資料結構(在WinSock2.h中定義):
struct sockaddr_in {
        short   sin_family;
        u_short sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
};
sin_family指代協議族,在socket程式設計中只能是AF_INET
sin_port儲存埠號(使用網路位元組順序),在linux下,埠號的範圍0~65535,同時0~1024範圍的埠號已經被系統使用或保留。
sin_addr儲存IP地址,使用in_addr這個資料結構
sin_zero是為了讓sockaddr與sockaddr_in兩個資料結構保持大小相同而保留的空位元組。

sockaddr_in和sockaddr是並列的結構,指向sockaddr_in的結構體的指標也可以指向sockaddr的結構體,並代替它。
也就是說,你可以使用sockaddr_in建立你所需要的資訊,
然後用memset函式初始化就可以了memset((char*)&mysock,0,sizeof(mysock));//初始化

參考:https://baike.baidu.com/item/SOCKADDR_IN
  • c_str c_str是Borland封裝的String類中的一個函式,它返回當前字串的首字元地址。當需要開啟一個由使用者自己輸入檔名的檔案時,可以這樣寫:ifstream in(st.c_str())。 在vc++2010中提示的錯誤原因:

    1. 	vc++2017應該這樣用:
    	char c[20];
        string s="1234";
        strcpy(c,s.c_str());
    這樣才不會出錯,c_str()返回的是一個臨時指標,不能對其進行操作
    c_str()返回的是一個分配給const char*的地址,其內容已設定為不可變更,如果再把此地址賦給一個可以變更內容的char*變數,就會產生衝突。但是如果放入函式呼叫,或者直接輸出,因為這些函式和輸出都是把字串指標作為 const char*引用的,所以不會有問題。
    
    2. c_str() 以const char* 型別返回 string 內含的字串
    如果一個函式要求char*引數,可以使用c_str()方法:
    string s = "Hello World!";
    printf("%s", s.c_str()); //輸出 "Hello World!"
    
    3. c_str在開啟檔案時的用處:
    當需要開啟一個由使用者自己輸入檔名的檔案時,可以這樣寫:ifstream in(st.c_str());。其中st是string型別,存放的即為使用者輸入的檔名。
    
    參考:https://baike.baidu.com/item/c_str/2622670
    
  • memset 在這裡插入圖片描述

    void *memset(void *s, int ch, size_t n);
    函式解釋:將s中當前位置後面的n個位元組 (typedef unsigned int size_t )用 ch 替換並返回 s 。
    memset:作用是在一段記憶體塊中填充某個給定的值,它是對較大的結構體或陣列進行清零操作的一種最快方法 。
    memset()函式原型是:extern void *memset(void *buffer, int c, int count) 
                       buffer:為指標或是陣列,
                       c:是賦給buffer的值,
                       count:是buffer的長度.
                       
    參考:https://baike.baidu.com/item/memset/4747579?fr=aladdin
    
  • recvfrom

    
    recvfrom(
        _In_ SOCKET s,
        _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, //接收資料的緩衝區     
        _In_ int len,                                                                 //緩衝區的大小
        _In_ int flags,                                                               //標誌位,呼叫操作方式
        _Out_writes_bytes_to_opt_(*fromlen, *fromlen) struct sockaddr FAR * from,     //sockaddr結構地址
        _Inout_opt_ int FAR * fromlen                                                 //sockaddr結構大小地址
        );
    
  • sendto

    WSAAPI
    sendto(
        _In_ SOCKET s,                                            //socket 
        _In_reads_bytes_(len) const char FAR * buf,               //傳送資料的緩衝區   
        _In_ int len,                                             //緩衝區大小      
        _In_ int flags,                                           //標誌位,呼叫操作方式
        _In_reads_bytes_(tolen) const struct sockaddr FAR * to,   //sockaddr結構地址
        _In_ int tolen                                            //sockaddr結構大小地址
        );
    

程式碼分析

新建專案:windows控制檯應用程式 -->專案名稱:server

// server.cpp : 此檔案包含 "main" 函式。程式執行將在此處開始並結束。
//

#include "pch.h"
#include <iostream>
#include <WinSock2.h>
#include<WS2tcpip.h>
#include<string>
#pragma comment(lib,"ws2_32.lib")

using namespace std;

int main() {
    //設定版本號
	WORD sockVersion = MAKEWORD(2, 2);
    //定義一個WSADATA型別的結構體,儲存被WSAStartup函式呼叫後返回的Windows Sockets資料
	WSADATA wsadata;
	//初始化套接字,啟動構建,將“ws2_32.lib”載入到記憶體中
	if (WSAStartup(sockVersion, &wsadata)) {
		printf("WSAStartup failed \n");
		return 0;
	}
	//建立一個套接字,即建立一個核心物件
	SOCKET hServer = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (hServer == INVALID_SOCKET) {
		printf("socket failed \n");
		return 0;
	}
	//建立伺服器端地址並繫結埠號的IP地址
	sockaddr_in addrServer;
	addrServer.sin_family = AF_INET;
	addrServer.sin_port = htons(8889);
	addrServer.sin_addr.S_un.S_addr = INADDR_ANY;

	// 初始化核心物件,傳參給核心物件,此時資料可能都處於未就緒連結串列
	int nRet = bind(hServer, (sockaddr*)&addrServer, sizeof(addrServer));
	if (nRet == SOCKET_ERROR) {
		printf("socket bind failed\n");
		closesocket(hServer);
		WSACleanup();
		return 0;
	}
     //建立一個客服端地址
	sockaddr_in  addrClient;
	int nlen = sizeof(addrClient);
	//建立一箇中間變數,用於存放使用者輸入的資訊
	string str;
	//用於接受資料的緩衝區。
	char rcvdata[255];


	//可以迴圈接受資料
	while (true) {
		//接收資料:
			//初始化緩衝區,用於下一次資料的接收
			memset(rcvdata, 0, sizeof(rcvdata));
			//接受客戶端的訊息
			int ret = recvfrom(hServer, rcvdata, 255, 0, (SOCKADDR*)&addrClient, &nlen);
			if (ret > 0) {
				//緩衝區有資料,開始讀取資料
				rcvdata[ret] = 0X00;
				//接收到結束標誌,關閉伺服器。
				if (rcvdata == "byebye") {
					//關閉伺服器套接字
					closesocket(hServer);
					return 0;
				}
				printf(" ClientA:%s\n", rcvdata);
			}	
        //傳送資料:
		cout << "Server:";
		//從鍵盤獲取資料,存放在str中
		getline(cin, str);
		//建立傳送資料緩衝區
		const int len = sizeof(str);
		char senddata[len];
		strcpy_s(senddata, str.c_str());
		//傳送資料
        sendto(hServer, (char*)senddata, strlen(senddata), 0, (SOCKADDR*)&addrClient, nlen);
		str = "";
	}

	//關閉伺服器套接字
	closesocket(hServer);
	WSACleanup();
	return 0;


}

新建專案:windows控制檯應用程式 -->專案名稱:client

// client.cpp : 此檔案包含 "main" 函式。程式執行將在此處開始並結束。
//

#include"pch.h"
#include<iostream>
#include<WinSock2.h>
#include <string>
#pragma comment(lib,"ws2_32.lib")
using namespace std;

int main() {

	//套接字資訊結構
	WSADATA wsadata;
	//設定版本號
	WORD sockVersion = MAKEWORD(2, 2);
	//建立一個客戶端套接字;
	SOCKET sClient;
	//啟動構建,將“為ws2_32.lib”載入到記憶體中,做一些初始化工作
	if (WSAStartup(sockVersion, &wsadata) != 0) {
		//判斷是否構建成功,若失敗,則客戶端列印一句提示話。
		printf("WSAStartup failed \n");
		return 0;
	}

	//建立客戶端udp套接字
    sClient = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (SOCKET_ERROR == sClient) {
		printf("socket failed !\n");
		return 0;
	}

	//建立伺服器端地址
	sockaddr_in serverAddr;
	//建立伺服器端地址
	sockaddr_in clientAddr;
	//設定伺服器端地址,埠號,協議族
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8889);
	serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	//獲取伺服器地址和客戶端地址構造體的長度
	int slen = sizeof(serverAddr);
	int clen = sizeof(clientAddr);
	//設定接受資料緩衝區大小
	char buffer[2048] = { 0 };
	//用於記錄傳送函式和接受函式的返回值
	int iSend = 0;
	int iRcv = 0;
	string str;
	cout << "開始主動與伺服器建立通訊:" << endl;

	while (true) {
		//傳送資料
			cout << "Client: ";
			getline(cin, str) ;
			//判斷是否輸入的是結束標記“byebye”
			if (str == "byebye") {
				printf("close connection \n");
				closesocket(sClient);
				return 0;
			}
			const int len = sizeof(str);
			char senddata[len];
			strcpy_s(senddata, str.c_str());

			iSend=sendto(sClient, (char*)senddata, strlen(senddata), 0, (SOCKADDR*)&serverAddr, slen);
			if (iSend== SOCKET_ERROR) {
				printf("sendto failed \n");
				closesocket(sClient);
				WSACleanup();
				return 0;
			}
			str = "";

		//接受客戶端資料
		memset(buffer, 0, sizeof(buffer));
		iRcv= recvfrom(sClient, buffer, sizeof(buffer), 0, (SOCKADDR*)&clientAddr,&clen);
		if (iRcv == SOCKET_ERROR) {
			printf("recvFrom failed \n");
			closesocket(sClient);
			WSACleanup();
			return 0;
		}
		printf("Server: %s\n", buffer);
	}
	closesocket(sClient);
	WSACleanup();
	return 0;
}

將兩個cpp檔案都release後開啟exe檔案,進行正常通訊。

在這裡插入圖片描述

問題

  • 在VS2017中進行套接字程式設計時, sockaddr_in ClientAddr; ClientAddr.sin_addr.S_un.S_addr = inet_addr(“127.0.0.1”);

    在編譯時會彈出 error C4996: ‘inet_addr’: Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings錯誤提示。主要原因是inet_addr()函式已經過時,推薦使用inet_pton()或者InetPton()函式。

    問題解決 可以採用三種方法解決error C4996錯誤:第一種是關閉專案的SDL檢查;第二種是對_WINSOCK_DEPRECATED_NO_WARNINGS進行定義;第三種是使用推薦的新函式。如果想繼續使用舊函式,可使用前兩種方法。

    1. 關閉專案的SDL檢查 SDL叫做“安全開發宣告週期”檢查,是VS2012中新新增的功能。主要是為了能更好地監管該法著的程式碼安全。

    2. 定義_WINSOCK_DEPRECATED_NO_WARNINGS 在專案的預編譯標頭檔案中新增對_WINSOCK_DEPRECATED_NO_WARNINGS的定義 #define _WINSOCK_DEPRECATED_NO_WARNINGS 0 將其定義為0、1、2…均可。

    3. 使用推薦的新函式 inet_pton()函式或者InetPton()函式在Ws2tcpip.h中定義,在使用這些新函式之前需要包含該標頭檔案。 #include <Ws2tcpip.h>