记一个网络传输功能的实现过程
寫在前面的話:功能是基于C/S模型的網絡傳輸實現,要求是服務器端可以在局域網中任何機子上運行,客戶端啟動后自動尋找服務器端進行連接,之后,服務器端向已經連接的客戶端發送命令,客戶端根據命令執行相應的操作(即發送某個約定文件夾下的所有文件),并且客戶端不需要用戶操作。
1、思路
首先,對于這個功能的實現思路如下,因為服務器不確定在哪個機子上,所以為了尋找到服務器端,客戶端需要發送廣播消息,并且為了維護客戶端在線,廣播消息需要實現成心跳包(即定時發送廣播消息)。服務器監聽心跳包,如果是新加入的客戶端,則更新用戶列表,否則不做處理。這是維護在線,離線的實現還在考慮中。
第二,當服務器端發送命令給客戶端時,客戶端根據命令來決定發送哪個文件夾中的文件給服務器端,由于文件夾下可能有多個文件,所以需要有個循環,循環發送每個文件。我的處理時在發送前先發送即將發送的文件個數,之后循環發送每個文件,在每個文件的發送過程中,首先發送文件名,之后發送文件長度,最后循環發送文件內容。當然服務器端接受的時候也需要按這個順序來接收。
2、實現
客戶端:
開啟一個線程來發送心跳包,每發送一個心跳包,就睡眠3S,接著發送下一個,如此循環,心跳包采用UDP套接字來發送。
當接受命令時,客戶端轉換為C/S模型中的服務器端,因此客戶端需要開啟一個線程來監聽固定端口,接受服務器端發送來的命令,之后根據命令執行相應的操作。
服務器端:
開啟一個線程監聽UDP心跳包。根據心跳包的IP地址判斷是否做處理。
當點擊發送命令按鈕時,開啟一個線程連接選中的客戶端,發送命令并接收數據。
3、碰到的問題
(1)客戶端的監聽是使用WSAAsyncSelect()函數注冊網絡事件來實現的,注冊了FD_ACCEPT、FD_READ、FD_CLOSE三個網絡事件,代碼如下:
// TCP套接字if(InitTcpSock()){if(BindTcpSock()){if(SOCKET_ERROR == WSAAsyncSelect(m_tcpSocket, m_hWnd, WM_SOCKET, FD_ACCEPT | FD_READ | FD_CLOSE)){DWORD dwError = 0;dwError = GetLastError();ShowError(_T("注冊網絡事件失敗"), dwError);return FALSE;}listen(m_tcpSocket, 2);}}在FD_READ網絡事件發生時做處理,按接收到的命令執行相應的操作,出問題的地方就是當接受到發送文件的命令,執行相應的操作,發送指定文件夾下所有文件時,代碼如下:
1 // 發送指定文件夾所有文件 2 UINT CClientDlg::SendFolderThread(LPVOID lpParameter) 3 { 4 SendParam *pSendParam = (SendParam*)lpParameter; 5 BOOL bIsNull(TRUE); 6 DWORD dwError(0); 7 8 SOCKET socket = pSendParam->socket; 9 10 CString strFolderPath = pSendParam->szFilePath; 11 12 int iFileSum = FileSum(strFolderPath, 0); 13 if(iFileSum == 0) 14 { 15 return dwError; 16 } 17 else 18 { 19 // 發送文件總數 20 char szFileSum[10] = {0}; 21 _itoa_s(iFileSum, szFileSum, 10); 22 if(SOCKET_ERROR == send(socket, szFileSum, 10, NULL)) 23 { 24 dwError = GetLastError(); 25 ShowError(_T("發送文件總數失敗"), dwError); 26 delete pSendParam; 27 pSendParam = NULL; 28 return dwError; 29 } 30 31 strFolderPath.Append(_T("\\*")); 32 CFileFind finder; 33 BOOL isFind = finder.FindFile(strFolderPath); 34 CString strFilePath; 35 CString strFileName; 36 37 while(isFind) 38 { 39 isFind = finder.FindNextFile(); 40 if(!finder.IsDots()) 41 { 42 strFilePath = finder.GetFilePath(); 43 strFileName = finder.GetFileName(); 44 CFile file; 45 if (!file.Open(strFilePath, CFile::modeRead)) 46 { 47 dwError = GetLastError(); 48 ShowError(_T("打開文件失敗"), dwError); 49 delete pSendParam; 50 pSendParam = NULL; 51 return dwError; 52 } 53 54 char strBuf[512] = {0}; 55 char strEnd[512] = {0}; 56 57 UINT len = 0; 58 // 發送文件名 59 WideCharToMultiByte(CP_ACP, 0, 60 strFileName.GetBuffer(strFileName.GetLength() - 1), 61 strFileName.GetLength(), strBuf, 511, 0, 0); 62 strFileName.ReleaseBuffer(); 63 strBuf[strFileName.GetLength()] = 0; 64 if(SOCKET_ERROR == send(socket, strBuf, 512, NULL)) 65 { 66 dwError = GetLastError(); 67 ShowError(_T("發送文件名失敗"), dwError); 68 return dwError; 69 } 70 // 發送文件內容 71 memset(strBuf, 0, 512); 72 while(len = file.Read(strBuf, 511)) 73 { 74 if(SOCKET_ERROR == send(socket, strBuf, 512, NULL)) 75 { 76 dwError = GetLastError(); 77 ShowError(_T("發送文件失敗"), dwError); 78 return dwError; 79 } 80 memset(strBuf, 0, 512); 81 if(len < 511) 82 { 83 send(socket, strEnd, 512, NULL); 84 break; 85 } 86 } 87 // 如果文件長度剛好是511的整數倍 88 if(0 == len) 89 { 90 send(socket, strEnd, 512, NULL); 91 } 92 file.Close(); 93 } 94 } 95 //AfxMessageBox(_T("文件傳送完畢")); 96 } 97 delete pSendParam; 98 pSendParam = NULL; 99 return dwError; 100 }
?問題就出在這段代碼的74行,程序運行的時候總是提示10035錯誤代碼,根據錯誤代碼大全,這個錯誤是無法立即完成一個非阻擋性套接字操作,原因就是說緩沖區已滿,無法立即完成發送。在網上搜了一下,感覺這個帖子解釋的挺好的,摘抄幾個個人認為解釋的不錯的回復:
原因就說緩沖區滿了?所以無法立即
負責監聽和接收的socket:可以采用非阻塞;
負責發送的發送的socket要采用阻塞模式;
因為TCP協議在網絡上傳輸的兩端是阻塞傳輸的,你在代碼中設置非阻塞只是要socket把數據放到本地緩沖區上就不要管了,其實數據還在本地緩沖區,在上次發出去的數據沒有得到回應以前,數據將一直不會發出去,但這時你的socket又把下一批數據放入緩沖區,
在本機上,把發送端的包大小設置為10240,接收的包設置為5120,會立即出錯。
解決辦法:
1.TCP情況下,把負責發送的socket采用阻塞模式;
2.采用UDP
因為你采用了WSAEventSelect,這個函數會自動把socket設成了非阻塞模式。
非阻塞模式的socket在send前一定要檢查socket的當前狀態是否為可send狀態,因為你沒有合理的檢查機制,導至在socket不可send的狀態你send了。
阻塞模式的socket在send的時候不用檢查狀態的,可以直接send.
阻塞模式?寫法簡單,適合單連接。
用阻塞模式寫多連接也可以,不過有幾個連接,就得最少開幾個線程,為方便控制,書上推薦開連接數*2個線程。
非阻塞模式,相當難控制,對于現在的我來說,是個很大的工程,正在學習中...
如果有多個連接,書上建議用非阻塞模式,因為不管有多個連接,只要1-2個線程就可以搞定。
非阻塞模式有5種控制方法如下(抄書):
1.select模型
2.WSAAsyncSelect
3.WSAEventSelect
4.重疊模型
5.完成端口模型
樓主這樣發送大文件的方法根本不是正確的方法,因為這樣根本沒有考慮發送緩存是否已經滿了或網絡的異常狀況,
這樣一直發一直發,完全不考慮結果,只要有一個send沒發送成功,那么本次文件發送就會失敗,在實際應用中根本不可取;
樓主的做發是一種理想的做法;
如果非要這樣循環讀\循環send的話,可以在每次循環的時候Sleep(10)一下,這樣基本就沒有問題,但不推薦這樣做,這樣會
使程序效率極其低效;
要做好大文件的發送,要考慮以下幾個方面:
1,?發送緩存的選取
2,?文件的讀取,可以考慮使用內存文件,如果能夠一次性讀取的話,就不要讀那么多次,總之就是要減少讀取的次數
*??由于Tcp/ip協議能夠自己進行流量控制,所以即使你在發送的時候一次將整個文件一次發送,也不會有問題
*??send的時候沒必要每次只發送一點點數據,太影響效率了,一個文件要發送很久才能傳完
可以在每次循環的時候Sleep(10)一下,這樣基本就沒有問題
//Sleep(1000)該出問題還是會出問題,但通常加上Sleep(1),這樣為防止CPU?100%,而不是防出錯
我搞不明白,把文件拆開(只要不拆成1bit),有什么不妥?1040-10240之間都可以運行的很好
發整個文件,一個文件4M的話,就是4M內存,機器只運行你一個程序?
姑且不論Sleep(1)是為了防止CPU?100%,還是防出錯,這無關緊要,
一個合理高效的服務器是不會這樣發送數據的;
大文件100%是要拆的,毫無疑問
一般的小文件,又能占多少內存呢!為什么不能一次讀完?
我想說的是,每次"盡量"多從文件讀些數據進來,避免頻繁讀寫磁盤,象這樣的代碼是沒有任何問題的:
int?ret?=?fread(?bug,?sizeof(char)?,?65536,?pFile);
int?nsend?=?send(mSock,(char?*)bug,?ret,?0);
我認為第三個說的不錯,非阻塞模式的socket在send前一定要檢查socket的當前狀態是否為可send狀態,因為自己是初學網絡編程,對于select模型的使用還不太熟練,所以不會使用他說的用select來檢測套接字緩沖區是否已滿。最后不得以換成了開線程的方法來實現。其次第五個說使用sleep這種方法我試了,不行。還是會出現同樣的錯誤代碼。
?
總結
以上是生活随笔為你收集整理的记一个网络传输功能的实现过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C0304 备份最后一天所有修改的文件
- 下一篇: 【VMCloud云平台】Demo应用搭建