Windows下C語言的Socket程式設計例子資料傳輸需要注意的問題:
網絡程式設計
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());
// listen()
if (listen(sockSrv, 10) == SOCKET_ERROR){
printf("套接字監聽失敗:%d......\n", WSAGetLastError());
// 用戶端資訊
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");
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;
}
用戶端:
char buff[1024];
memset(buff, 0, sizeof(buff));
addrSrv.sin_port = htons(port);
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKET_ERROR == sockClient){
printf("建立套接字失敗:%d......\n", WSAGetLastError());
// 向伺服器發出連接配接請求
if (connect(sockClient, (struct sockaddr*)&addrSrv, sizeof(addrSrv)) == INVALID_SOCKET)
printf("連接配接伺服器失敗:%d......\n", WSAGetLastError());
else
recv(sockClient, buff, sizeof(buff), 0);
printf("收到資料:%s\n", buff);
char buf[] = "用戶端:請求登入......";
send(sockClient, buf, sizeof(buf), 0);
closesocket(sockClient);
舊函數解決方式:
4. UDP例子
服務端(接收方):
// UDPReceiverTest.cpp : 定義控制台應用程式的入口點。
//
#include "stdafx.h"
SOCKET sockClient = socket(AF_INET,SOCK_DGRAM, 0);
if (bind(sockClient, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
SOCKADDR_IN addrClnt;
int nLen = sizeof(SOCKADDR);
// 消息
char szMsg[1024];
memset(szMsg, 0, sizeof(szMsg));
// 等待客戶請求到來
printf("服務端啟動成功......等待客戶發送資料...\n");
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);
// 上面為無線循環,以下代碼不會執行
// UDPSenderTest.cpp : 定義控制台應用程式的入口點。
SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0);
// 發送資料
sendto(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrSrv, nLen);
// 初始化資料
char szMsg[1024];
memset(szMsg, 0, sizeof(szMsg));
printf("請輸入要發送的資料(輸入q退出):");
scanf("%s", &szMsg);
// 退出循環
if (!strcmp(szMsg, "q") || !strcmp(szMsg, "Q"))
sendto(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrSrv, nLen);
// 清空緩存
if (SOCKET_ERROR != recvfrom(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrSrv, &nLen))
printf("接收方:%s\n", szMsg);
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");
sendto(sockClient, (char*)&stPhoto, sizeof(stPhoto), 0, (SOCKADDR*)&addrSrv, nLen);
接收檔案:
printf("正在接收檔案......\n");
// 接收圖檔
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>
---------------------