1. 程式人生 > >【網路程式設計】網路程式設計 筆記

【網路程式設計】網路程式設計 筆記

 

https://blog.csdn.net/bandaoyu/article/details/83312754

Windows下C語言的Socket程式設計例子

https://blog.csdn.net/bandaoyu/article/details/83312102

資料傳輸需要注意的問題:

網路程式設計

1. TCP與UDP的比較
TCP是面向連線的,互動雙方的程序各自建立一個流式套接字,伺服器需要等待客戶端向其提出連線申請。一旦接受客戶端申請就立刻返回一個新的套接字描述符。通過該描述符呼叫資料傳輸函式與客戶端進行資料的收發。

UDP是面向無連線的,雙方建立的是資料報套接字,伺服器和客戶端在進行傳描資料之前不需要進行連線的申請和建立,可以隨時向對方發訊息。

 

TCP

優點:可靠、穩定

缺點:速度慢,效率低、佔用系統資源高、易被攻擊。

適合場景:網路通訊質量要求高(可靠、穩定)

 

UDP

優點:速度快,比TCP稍安全

缺點:不可靠、不穩定

適用場合:網路通訊質量要求不高,速度快。

2. Socket粘包問題
什麼時候需要考慮粘包問題

1:如果利用tcp每次傳送資料,就與對方建立連線,然後雙方傳送完一段資料後,就關閉連線,這樣就不會出現粘包問題(因為只有一種包結構,類似於http協議)。關閉連線主要要雙方都發送close連線(參考tcp關閉協議)。如:A需要傳送一段字串給B,那麼A與B建立連線,然後傳送雙方都預設好的協議字元如"hello give me sth abour yourself",然後B收到報文後,就將緩衝區資料接收,然後關閉連線,這樣粘包問題不用考慮到,因為大家都知道是傳送一段字元;

2:如果傳送資料無結構,如檔案傳輸,這樣傳送方只管傳送,接收方只管接收儲存就ok,也不用考慮粘包;

3:如果雙方建立連線,需要在連線後一段時間內傳送不同結構資料,如連線後,有好幾種結構:

 1)"hellogive me sth abour yourself" 

 2)"Don'tgive me sth abour yourself" 

  那這樣的話,如果傳送方連續傳送這個兩個包出去,接收方一次接收可能會是"hello give me sth abour yourselfDon't give me sth abouryourself" 這樣接收方就傻了,到底是要幹嘛?不知道,因為協議沒有規定這麼詭異的字串,所以要處理把它分包,怎麼分也需要雙方組織一個比較好的包結構,所以一般可能會在頭加一個數據長度之類的包,以確保接收。

 

粘包出現原因:

在流傳輸中出現,UDP不會出現粘包,因為它有訊息保護邊界。

1 傳送端需要等緩衝區滿才傳送出去,造成粘包

2 接收方不及時接收緩衝區的包,造成多個包接收

 

解決辦法:

為了避免粘包現象,可採取以下幾種措施:

一是對於傳送方引起的粘包現象,使用者可通過程式設計設定來避免,TCP提供了強制資料立即傳送的操作指令push,TCP軟體收到該操作指令後,就立即將本段資料傳送出去,而不必等待發送緩衝區滿;

二是對於接收方引起的粘包,則可通過優化程式設計、精簡接收程序工作量、提高接收程序優先順序等措施,使其及時接收資料,從而儘量避免出現粘包現象;

三是由接收方控制,將一包資料按結構欄位,人為控制分多次接收,然後合併,通過這種手段來避免粘包。

以上提到的三種措施,都有其不足之處。

第一種程式設計設定方法雖然可以避免傳送方引起的粘包,但它關閉了優化演算法,降低了網路傳送效率,影響應用程式的效能,一般不建議使用。

第二種方法只能減少出現粘包的可能性,但並不能完全避免粘包,當傳送頻率較高時,或由於網路突發可能使某個時間段資料包到達接收方較快,接收方還是有可能來不及接收,從而導致粘包。

第三種方法雖然避免了粘包,但應用程式的效率較低,對實時應用的場合不適合。

 

更為簡潔的說法:

定包長

包尾加\r\n

包頭加包體長度

 

網上說法:

個人比較喜歡的一種做法是給一幀資料加幀頭幀尾,然後接收方不斷接受並快取收到的資料,根據幀頭幀尾分離出一幀完整的資料,再分離各欄位得到資料。

 

如果某個包出錯了,怎麼不斷恢復?

傳送訊息時,每個訊息長度在程式設計的時候就指定了。如果接收到的資料包有問題,我們可以通過訊息長度來不斷回覆原來的資料包。

3. TCP例子
服務端:

#include <stdio.h>

#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

 

int _tmain(int argc, _TCHAR* argv[])

{

    WSADATA wsaData;

    int port = 5099;

    char buf[] = "伺服器: 歡迎登入......\n";

 

    // 載入套接字

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

    {

         printf("載入套接字失敗:%d......\n", WSAGetLastError());

         return 1;

    }

 

    // socket()

    SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

 

    // 初始化IP和埠資訊

    SOCKADDR_IN addrSrv;

    addrSrv.sin_family = AF_INET;

    addrSrv.sin_port = htons(port); // 1024以上的埠號

    addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

 

    // bind()

    if (bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)

    {

         printf("套接字繫結失敗:%d......\n", WSAGetLastError());

         return 1;

    }

 

    // listen()

    if (listen(sockSrv, 10) == SOCKET_ERROR){

         printf("套接字監聽失敗:%d......\n", WSAGetLastError());

         return 1;

    }

 

    // 客戶端資訊

    SOCKADDR_IN addrClient;

    int len = sizeof(SOCKADDR);

 

    // 開始監聽

    printf("服務端啟動成功......開始監聽...\n");

    while (1)

    {

         // 等待客戶請求到來  

         SOCKET sockConn = accept(sockSrv, (SOCKADDR *)&addrClient, &len);

         if (sockConn == SOCKET_ERROR){

             printf("建立連線失敗:%d......\n", WSAGetLastError());

             break;

         }

 

         printf("與客戶端建立連線......IP:[%s]\n", inet_ntoa(addrClient.sin_addr));

 

         // 傳送資料

         if (send(sockConn, buf, sizeof(buf), 0) == SOCKET_ERROR){

             printf("傳送資料失敗......\n");

             break;

         }

 

         char recvBuf[100];

         memset(recvBuf, 0, sizeof(recvBuf));

         // 接收資料

         recv(sockConn, recvBuf, sizeof(recvBuf), 0);

         printf("收到資料:%s\n", recvBuf);

 

         closesocket(sockConn);

    }

 

    // 關閉套接字

    closesocket(sockSrv);

    WSACleanup();

    system("pause");

 

    return 0;

}

 

客戶端:

#include <stdio.h>

#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

 

int _tmain(int argc, _TCHAR* argv[])

{

    WSADATA wsaData;

    int port = 5099;

    char buff[1024];

    memset(buff, 0, sizeof(buff));

 

    // 載入套接字

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

    {

         printf("載入套接字失敗:%d......\n", WSAGetLastError());

         return 1;

    }

 

    // 初始化IP和埠資訊

    SOCKADDR_IN addrSrv;

    addrSrv.sin_family = AF_INET;

    addrSrv.sin_port = htons(port);

    addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

 

    // socket()

    SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

    if (SOCKET_ERROR == sockClient){

         printf("建立套接字失敗:%d......\n", WSAGetLastError());

         return 1;

    }

 

    // 向伺服器發出連線請求

    if (connect(sockClient, (struct  sockaddr*)&addrSrv, sizeof(addrSrv)) == INVALID_SOCKET)

    {

         printf("連線伺服器失敗:%d......\n", WSAGetLastError());

         return 1;

    }

    else

    {

         // 接收資料

         recv(sockClient, buff, sizeof(buff), 0);

         printf("收到資料:%s\n", buff);

 

         // 傳送資料

         char buf[] = "客戶端:請求登入......";

         send(sockClient, buf, sizeof(buf), 0);

    }

 

    // 關閉套接字

    closesocket(sockClient);

    WSACleanup();

 

    return 0;

}

 

舊函式解決方式:

 

4. UDP例子
服務端(接收方):

// UDPReceiverTest.cpp : 定義控制檯應用程式的入口點。

//

 

#include "stdafx.h"

#include <stdio.h>

#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

 

int _tmain(int argc, _TCHAR* argv[])

{

    WSADATA wsaData;

    int port = 5099;

 

    // 載入套接字

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

    {

         printf("載入套接字失敗:%d......\n", WSAGetLastError());

         return 1;

    }

 

    // 初始化IP和埠資訊

    SOCKADDR_IN addrSrv;

    addrSrv.sin_family = AF_INET;

    addrSrv.sin_port = htons(port);

    addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

 

    // socket()

    SOCKET sockClient = socket(AF_INET,SOCK_DGRAM, 0);

    if (SOCKET_ERROR == sockClient){

         printf("建立套接字失敗:%d......\n", WSAGetLastError());

         return 1;

    }

 

    // bind()

    if (bind(sockClient, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)

    {

         printf("套接字繫結失敗:%d......\n", WSAGetLastError());

         return 1;

    }

 

    SOCKADDR_IN addrClnt;

    int nLen = sizeof(SOCKADDR);

    // 訊息

    char szMsg[1024];

    memset(szMsg, 0, sizeof(szMsg));

 

    // 等待客戶請求到來

    printf("服務端啟動成功......等待客戶傳送資料...\n");

    while (1)

    {

         // 接收資料

         if (SOCKET_ERROR != recvfrom(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrClnt, &nLen))

         {

             printf("傳送方:%s\n", szMsg);

             char szSrvMsg[] = "收到...";

             // 傳送資料

             sendto(sockClient, szSrvMsg, sizeof(szSrvMsg), 0, (SOCKADDR*)&addrClnt, nLen);

         }

    }

 

    // 上面為無線迴圈,以下程式碼不會執行

    // 關閉套接字

    closesocket(sockClient);

    WSACleanup();

 

    return 0;

}

 

客戶端:

 

// UDPSenderTest.cpp : 定義控制檯應用程式的入口點。

//

 

#include "stdafx.h"

#include <stdio.h>

#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

 

int _tmain(int argc, _TCHAR* argv[])

{

    WSADATA wsaData;

    int port = 5099;

 

    // 載入套接字

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

    {

         printf("載入套接字失敗:%d......\n", WSAGetLastError());

         return 1;

    }

 

    // socket()

    SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0);

    if (SOCKET_ERROR == sockClient){

         printf("建立套接字失敗:%d......\n", WSAGetLastError());

         return 1;

    }

 

    // 初始化IP和埠資訊

    SOCKADDR_IN addrSrv;

    addrSrv.sin_family = AF_INET;

    addrSrv.sin_port = htons(port);

    addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

 

    int nLen = sizeof(SOCKADDR);

 

    // 傳送資料

    char szMsg[1024];

    memset(szMsg, 0, sizeof(szMsg));

    sendto(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrSrv, nLen);

 

    // 傳送資料

    while (1)

    {

         // 初始化資料

         char szMsg[1024];

         memset(szMsg, 0, sizeof(szMsg));

         printf("請輸入要傳送的資料(輸入q退出):");

         scanf("%s", &szMsg);

 

         // 退出迴圈

         if (!strcmp(szMsg, "q") || !strcmp(szMsg, "Q"))

         {

             break;

         }

 

         // 傳送資料

         sendto(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrSrv, nLen);

 

         // 清空快取

         memset(szMsg, 0, sizeof(szMsg));

 

         // 接收資料

         if (SOCKET_ERROR != recvfrom(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrSrv, &nLen))

         {

             printf("接收方:%s\n", szMsg);

         }

    }

 

    // 關閉套接字

    closesocket(sockClient);

    WSACleanup();

 

    return 0;

}

 

5. TCP和 UDP 注意點
易忽略,出錯的地方:socket()

TCP:          SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

UDP:        SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0);

 

TCP不存在資料邊界:

收到資料不意味著馬上呼叫read()函式,只要不超過陣列容量,則有可能資料填充滿緩衝後通過一次read()函式呼叫讀取全部,也有可能分成多次read()函式呼叫進行讀取。如果傳輸出錯就會提供重傳服務。(套接字內部有一個由位元組陣列構成的緩衝)

6. 結構體、圖片傳輸方法
首先通訊雙方需要統一結構體,示例:

struct Massage

{

    int nID;

    char strMsg[64];

};

 

傳送方:

    // 結構體訊息

    Massage stMsg;

    memset(stMsg.strMsg, 0, sizeof(stMsg.strMsg));

    stMsg.nID = 1001;

    strcpy(stMsg.strMsg, "Struct string");

    // ...

    sendto(sockClient, (char*)&stMsg, sizeof(stMsg) + 1, 0, (SOCKADDR*)&addrClnt, nLen);

 

接收方:

   // 結構體

   Massage stMsg;

   memset(stMsg.strMsg, 0, sizeof(stMsg.strMsg));

 

   memcpy(&stMsg, szMsg, sizeof(stMsg) + 1);

 

   printf("接收方:%d\t%s\n", stMsg.nID, stMsg.strMsg);

 

特別注意: sizeof(stMsg) + 1 兩者必須保持一致。

 

拓展:傳送檔案

    // 圖片

    struct Photo

    {

         int nSize;

         char buf[256];

    };

    Photo stPhoto;

    memset(stPhoto.buf, 0, sizeof(stPhoto.buf));

 

    // 傳送檔案

    printf("正在傳送檔案......\n");

    while (fp1)

    {

         // 讀取檔案內容到buf中,每次讀256位元組,返回值表示實際讀取的位元組數

         int nCount = fread(stPhoto.buf, 1, sizeof(stPhoto.buf), fp1);

 

         stPhoto.nSize = nCount;

 

         //printf("read %d byte\n", nCount);

 

         // 如果讀取的位元組數不大於0,說明讀取出錯或檔案已經讀取完畢

         if (nCount <= 0)

         {

             sprintf(stPhoto.buf, "finish\n");

             sendto(sockClient, (char*)&stPhoto, sizeof(stPhoto), 0, (SOCKADDR*)&addrSrv, nLen);

             printf("檔案傳送完成......\n");

             break;

         }

 

         sendto(sockClient, (char*)&stPhoto, sizeof(stPhoto), 0, (SOCKADDR*)&addrSrv, nLen);

    }

 

接收檔案:

    printf("正在接收檔案......\n");

    while (1)

    {

         // 接收圖片

         if (SOCKET_ERROR != recvfrom(sockClient, szFileInfo, sizeof(szFileInfo), 0, (SOCKADDR*)&addrClnt, &nLen))

         {

             memcpy(&stPhoto, szFileInfo, sizeof(stPhoto));

 

             if (0 == strncmp(stPhoto.buf, "finish", 6))

             {

                  printf("檔案接收完成......\n");

                  break;

             }

            

             int n = fwrite(stPhoto.buf, 1, stPhoto.nSize, fp2);

             //printf("write %d byte\n", n);

         }

    }

 

7. 常見錯誤
包含<windows.h>和winsock.h後重定義問題:

[解決方案]

    由以上程式碼可以看出如果在沒有定義WIN32_LEAN_AND_MEAN巨集的大前

提下windows.h有可能包含winsock.h 標頭檔案,因此我們得出一個很簡單

的解決方法就是在包含<windows.h>之前定義WIN32_LEAN_AND_MEAN巨集,如

下所示:

#define WIN32_LEAN_AND_MEAN

#include <windows.h>
--------------------- 
作者:苦逼的IT男 
來源:CSDN 
原文:https://blog.csdn.net/daoming1112/article/details/54698466 
版權宣告:本文為博主原創文章,轉載請附上博文連結!