標籤:
簡單實現TCP下的大檔案高效傳輸
在TCP下進行大檔案傳輸,不像小檔案那樣直接打包個BUFFER發送出去,因為檔案比較大可能是1G,2G或更大,第一效率問題,第二TCP粘包問題。針對服務端的設計來說就更需要嚴緊些。下面介紹簡單地實現大檔案在TCP的傳輸應用。
粘包出現原因:在流傳輸中出現,UDP不會出現粘包,因為它有訊息邊界(參考Windows 網路編程)
1 發送端需要等緩衝區滿才發送出去,造成粘包
2 接收方不及時接收緩衝區的包,造成多個包接收
解決辦法:
為了避免粘包現象,可採取以下幾種措施:
一是對於發送方引起的粘包現象,使用者可通過編程設定來避免,TCP提供了強制資料立即傳送的操作指令push,TCP軟體收到該操作指令後,就立即將本段資料發送出去,而不必等待發送緩衝區滿;
二是對於接收方引起的粘包,則可通過最佳化程式設計、精簡接收進程工作量、提高接收進程優先順序等措施,使其及時接收資料,從而盡量避免出現粘包現象;
三是由接收方控制,將一包資料按結構欄位,人為控制分多次接收,然後合并,通過這種手段來避免粘包。
對於基於TCP開發的通訊程式,有個很重要的問題需要解決,就是封包和拆包.
為什麼基於TCP的通訊程式需要進行封包和拆包?
TCP是個"流"協議,所謂流,就是沒有界限的一串資料.大家可以想想河裡的流水,是連成一片的,其間是沒有分界線的.但一般通訊程式開發是需要定義一個個相互獨立的資料包的,比如用於登陸的資料包,用於登出的資料包.由於TCP"流"的特性以及網路狀況,在進行資料轉送時會出現以下幾種情況.
假設我們連續調用兩次send分別發送兩段資料data1和data2,在接收端有以下幾種接收情況(當然不止這幾種情況,這裡只列出了有代表性的情況).
A.先接收到data1,然後接收到data2.
B.先接收到data1的部分資料,然後接收到data1餘下的部分以及data2的全部.
C.先接收到了data1的全部資料和data2的部分資料,然後接收到了data2的餘下的資料.
D.一次性接收到了data1和data2的全部資料.
對於A這種情況正是我們需要的,不再做討論.對於B,C,D的情況就是大家經常說的"粘包",就需要我們把接收到的資料進行拆包,拆成一個個獨立的資料包.為了拆包就必須在發送端進行封包.
另:對於UDP來說就不存在拆包的問題,因為UDP是個"資料包"協議,也就是兩段資料間是有界限的,在接收端要麼接收不到資料要麼就是接收一個完整的一段資料,不會少接收也不會多接收.
二.為什麼會出現B.C.D的情況.
"粘包"可發生在發送端也可發生在接收端.
1.由Nagle演算法造成的發送端的粘包:Nagle演算法是一種改善網路傳輸效率的演算法.簡單的說,當我們提交一段資料給TCP發送時,TCP並不立刻發送此段資料,而是等待一小段時間,看看在等待期間是否還有要發送的資料,若有則會一次把這兩段資料發送出去.這是對Nagle演算法一個簡單的解釋,詳細的請看相關書籍.象C和D的情況就有可能是Nagle演算法造成的.
2.接收端接收不及時造成的接收端粘包:TCP會把接收到的資料存在自己的緩衝區中,然後通知應用程式層取資料.當應用程式層由於某些原因不能及時的把TCP的資料取出來,就會造成TCP緩衝區中存放了幾段資料.
三.怎樣封包和拆包.
最初遇到"粘包"的問題時,我是通過在兩次send之間調用sleep來休眠一小段時間來解決.這個解決方案的缺點是顯而易見的,使傳輸效率大大降低,而且也並不可靠.後來就是通過應答的方式來解決,儘管在大多數時候是可行的,但是不能解決象B的那種情況,而且採用應答方式增加了通訊量,加重了網路負荷. 再後來就是對資料包進行封包和拆包的操作.
封包:
封包就是給一段資料加上包頭,這樣一來資料包就分為包頭和包體兩部分內容了(以後講過濾非法包時封包會加入"包尾"內容).包頭其實上是個大小固定的結構體,其中有個結構體成員變數表示包體的長度,這是個很重要的變數,其他的結構體成員可根據需要自己定義.根據包頭長度固定以及包頭中含有包體長度的變數就能正確的拆分出一個完整的資料包.
對於拆包目前我最常用的是以下兩種方式.
1.動態緩衝區暫存方式.之所以說緩衝區是動態是因為當需要緩衝的資料長度超出緩衝區的長度時會增大緩衝區長度.
大概流程說明如下:
A,為每一個串連動態分配一個緩衝區,同時把此緩衝區和SOCKET關聯,常用的是通過結構體關聯.
B,當接收到資料時首先把此段資料存放在緩衝區中.
C,判斷緩衝區中的資料長度是否夠一個包頭的長度,如不夠,則不進行拆包操作.
D,根據包頭資料解析出裡面代表包體長度的變數.
E,判斷緩衝區中除包頭外的資料長度是否夠一個包體的長度,如不夠,則不進行拆包操作.
F,取出整個資料包.這裡的"取"的意思是不光從緩衝區中拷貝出資料包,而且要把此資料包從緩衝區中刪除掉.刪除的辦法就是把此包後面的資料移動到緩衝區的起始地址.
這種方法有兩個缺點.1.為每個串連動態分配一個緩衝區增大了記憶體的使用.2.有三個地方需要拷貝資料,一個地方是把資料存放在緩衝區,一個地方是把完整的資料包從緩衝區取出來,一個地方是把資料包從緩衝區中刪除.第二種拆包的方法會解決和完善這些缺點.
前面提到過這種方法的缺點.下面給出一個改進辦法, 即採用環形緩衝.但是這種改進方法還是不能解決第一個缺點以及第一個資料拷貝,只能解決第三個地方的資料拷貝(這個地方是拷貝資料最多的地方).第2種拆包方式會解決這兩個問題.
環形緩衝實現方案是定義兩個指標,分別指向有效資料的頭和尾.在存放資料和刪除資料時只是進行頭尾指標的移動.
2.利用底層的緩衝區來進行拆包
由於TCP也維護了一個緩衝區,所以我們完全可以利用TCP的緩衝區來緩衝我們的資料,這樣一來就不需要為每一個串連分配一個緩衝區了.另一方面我們知道recv或者wsarecv都有一個參數,用來表示我們要接收多長長度的資料.利用這兩個條件我們就可以對第一種方法進行最佳化.
對於阻塞SOCKET來說,我們可以利用一個迴圈來接收包頭長度的資料,然後解析出代表包體長度的那個變數,再用一個迴圈來接收包體長度的資料.
tcp是流,沒有界限.也就無所謂包;tcp是協議,而socket是一種介面。本文以流的形式發單個大檔案,也就無所謂封包和拆包問題,見下面代碼;但是要連續發送多個大檔案,封包和拆包就是要考慮的問題了!
#ifndef TCPRECVFILE#define TCPRECVFILE#include <stdio.h> #include <winsock2.h> #include <iostream>#include <time.h>#define DESTADDRESS "192.168.27.170"#define FILENAME "D:\\file.jpg"#define SERVER_PORT 5210 //偵聽連接埠 #define BEGIN_NUM 19900711#define DATA_NUM 20160113#define END_NUM 11700991#define BLOCK_DATA_SIZE (10 * 1024)#define FILE_HEAD 4#define BLOCK_HEAD 4class CTCPRecvfile{public: CTCPRecvfile(); ~CTCPRecvfile(); void Recvfile(); void Recv();private: void InitSocket(); void CloseSocket(); SOCKET m_Listen, m_Server; //偵聽通訊端,串連通訊端 struct sockaddr_in ServerAddr, ClientAddr; //地址資訊 };#endif1
1
#include "TCPRecvfile.h"CTCPRecvfile::CTCPRecvfile(){ InitSocket();}CTCPRecvfile::~CTCPRecvfile(){ CloseSocket();}void CTCPRecvfile::InitSocket(){ WORD wVersionRequested = MAKEWORD(2, 2); //希望使用的WinSock DLL 的版本 WSADATA wsaData; //WinSock初始化 int ret = WSAStartup(wVersionRequested, &wsaData); if (ret != 0) { printf("WSAStartup() failed!\n"); //return 0; } //建立Socket,使用TCP協議 m_Listen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (m_Listen == INVALID_SOCKET) { printf("socket() faild!\n"); //return 0; } //構建本地地址資訊 ServerAddr.sin_family = AF_INET; //地址家族 ServerAddr.sin_port = htons(SERVER_PORT); //注意轉化為網路位元組序 ServerAddr.sin_addr.S_un.S_addr = INADDR_ANY; //使用INADDR_ANY 指示任意地址 //綁定 ret = bind(m_Listen, (struct sockaddr *)&ServerAddr, sizeof(ServerAddr)); if (ret == SOCKET_ERROR) { printf("bind() faild! code:%d\n", WSAGetLastError()); //return 0; } //偵聽串連請求 ret = listen(m_Listen, 1); if (ret == SOCKET_ERROR) { printf("listen() faild! code:%d\n", WSAGetLastError()); } int length = sizeof(ServerAddr); m_Server = accept(m_Listen, (struct sockaddr *)&ServerAddr, &length); if (m_Server == INVALID_SOCKET) { printf("accept() faild! code:%d\n", WSAGetLastError()); return; } else printf("Server is Connected!\n");}void CTCPRecvfile::Recv(){ char *eachBuf = new char[BLOCK_DATA_SIZE + 2 * FILE_HEAD]; memset(eachBuf, 0, BLOCK_DATA_SIZE + 2 * FILE_HEAD); FILE *fp; UINT dwFileSize = 0; unsigned int RecvNum = 0, flag_status = 0, flag_recv = 1; fp = fopen(FILENAME, "wb"); //1、讀取第一組資料,擷取檔案大小,建立串連 recv(m_Server, eachBuf, 2 * FILE_HEAD, 0);//////----------------recv char charFileSize[4] = { 0 }; memcpy(charFileSize, eachBuf, FILE_HEAD); //拷貝前4個位元組 for (int i = 0; i < 4; i++) { flag_status += ((UCHAR)charFileSize[i]) << (8 * (4 - i - 1)); //擷取檔案起始符 } memcpy(charFileSize, eachBuf + FILE_HEAD, FILE_HEAD); //拷貝第5-8個位元組 for (int i = 0; i < 4; i++) { dwFileSize += ((UCHAR)charFileSize[i]) << (8 * (4 - i - 1)); //擷取檔案大小 } int start = clock(); { //開闢接收記憶體 int DataPos = 0; char *FileBuffer = new char[dwFileSize]; memset(FileBuffer, 0, dwFileSize); while (1) { int ret = recv(m_Server, eachBuf, BLOCK_DATA_SIZE, 0); if (ret <= 0) break; memcpy(FileBuffer + DataPos, eachBuf, ret); DataPos = DataPos + ret; } fwrite(FileBuffer, dwFileSize, 1, fp); fclose(fp); } int end = clock(); std::cout << "time:" << end - start << "ms" << RecvNum << std::endl;}void CTCPRecvfile::CloseSocket(){ closesocket(m_Server); //關閉通訊端 closesocket(m_Listen); WSACleanup();}1
1
#ifndef TCPSENDFILE#define TCPSENDFILE#include <stdio.h> #include <stdlib.h> #include <winsock2.h> #include <iostream>#include <tchar.h>#include <time.h> #define SERVER_PORT 5210 //偵聽連接埠 #define DESTADDRESS "192.168.27.170"#define FILENAME "D:\\10M.jpg"#define BEGIN_NUM 19900711#define DATA_NUM 20160113#define END_NUM 11700991#define BLOCK_DATA_SIZE (10 * 1024)#define FILE_HEAD 4#define BLOCK_HEAD 4 class CTCPSendfile{public: CTCPSendfile(); ~CTCPSendfile(); void Sendfile(); void Send();private: void InitSocket(); void CloseSocket(); SOCKET m_Client; //串連通訊端 struct sockaddr_in m_ClientAddr; //伺服器位址資訊 };#endif1
1
#include "TCPSendfile.h"CTCPSendfile::CTCPSendfile(){ InitSocket();}CTCPSendfile::~CTCPSendfile(){ CloseSocket();}void CTCPSendfile::InitSocket(){ WORD wVersionRequested = MAKEWORD(2, 2); //希望使用的WinSock DLL的版本 WSADATA wsaData; int ret = WSAStartup(wVersionRequested, &wsaData); //載入通訊端庫 if (ret != 0) { printf("WSAStartup() failed!\n"); //return 0; } //確認WinSock DLL支援版本2.2 if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { //釋放為該程式分配的資源,終止對winsock動態庫的使用 printf("Invalid WinSock version!\n"); //return 0; } //WinSock初始化 //建立Socket,使用TCP協議 m_Client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (m_Client == INVALID_SOCKET) { printf("socket() failed!\n"); //return 0; } //構建伺服器位址資訊 m_ClientAddr.sin_family = AF_INET; //地址家族 m_ClientAddr.sin_port = htons(SERVER_PORT); //注意轉化為網路節序 m_ClientAddr.sin_addr.S_un.S_addr = inet_addr(DESTADDRESS); //串連伺服器 do { ret = connect(m_Client, (struct sockaddr *)&m_ClientAddr, sizeof(m_ClientAddr)); if (ret == SOCKET_ERROR) { printf("connect() failed! Try it again!\n"); } else printf("Client is Connected\n"); Sleep(1000); } while (ret == SOCKET_ERROR);}void CTCPSendfile::Sendfile(){ HANDLE hFile; DWORD dwHighSize, dwBytesRead; DWORD dwFileSize; hFile = CreateFile(_T(FILENAME), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); dwFileSize = GetFileSize(hFile, &dwHighSize); std::cout << "dwFileSize=" << dwFileSize << std::endl; //2、讀檔案內容到 BYTE * fileData 中 BOOL bSuccess; char *fileData = new char[dwFileSize]; bSuccess = ReadFile(hFile, fileData, dwFileSize, &dwBytesRead, NULL); CloseHandle(hFile); //3、判斷檔案是否成功讀取 if (!bSuccess || (dwBytesRead != dwFileSize)) { std::cout << "讀取失敗" << std::endl;; free(fileData); return; } //發送資料幀 DWORD retval = 0; UINT DataPos = 0; char *eachBuf = new char[BLOCK_DATA_SIZE + 2 * FILE_HEAD]; memset(eachBuf, 0, BLOCK_DATA_SIZE + 2 * FILE_HEAD); eachBuf[DataPos++] = BEGIN_NUM >> 24 & 0xff;//檔案起始標識符 eachBuf[DataPos++] = BEGIN_NUM >> 16 & 0xff; eachBuf[DataPos++] = BEGIN_NUM >> 8 & 0xff; eachBuf[DataPos++] = BEGIN_NUM & 0xff; eachBuf[DataPos++] = dwFileSize >> 24 & 0xff; eachBuf[DataPos++] = dwFileSize >> 16 & 0xff; eachBuf[DataPos++] = dwFileSize >> 8 & 0xff; eachBuf[DataPos++] = dwFileSize & 0xff; retval = send(m_Client, eachBuf, 2 * FILE_HEAD, 0); int start = clock(); { retval = send(m_Client, fileData, dwFileSize, 0); if (retval == -1) std::cout << "send error!"; int end = clock(); }}void CTCPSendfile::CloseSocket(){ closesocket(m_Client); //關閉通訊端 WSACleanup();}
Windows下基於TCP協議的大檔案傳輸(流形式)