老闆逼著讓寫一個基於UDP可靠通訊的demo以作為其網路課程的課件內容之一來給學生示範
dup是不可靠的,為了實現可靠檔案傳輸,需要加入額外的機制來確保可靠,在此選用了滑動視窗機制。
滑動視窗介紹:
滑動視窗協議的基本原理就是在任意時刻,發送方都維持了一個連續的允許發送的幀的序號,稱為發送視窗;同時,接收方也維持了一個連續的允許接收的幀的序號,稱為接收視窗。發送視窗和接收視窗的序號的上下界不一定要一樣,甚至大小也可以不同。不同的滑動視窗協議視窗大小一般不同。發送方視窗內的序號代表了那些已經被發送,但是還沒有被確認的幀,或者是那些可以被發送的幀。下面舉一個例子(假設發送視窗尺寸為2,接收視窗尺寸為1):
分析:①初始態,發送方沒有幀發出,發送視窗前後沿相重合。接收方0號視窗開啟,等待接收0號幀;②發送方開啟0號視窗,表示已發出0幀但尚確認返回資訊。此時接收視窗狀態不變;③發送方開啟0、1號視窗,表示0、1號幀均在等待確認之列。至此,發送方開啟的視窗數已達規定限度,在未收到新的確認返回幀之前,發送方將暫停發送新的資料幀。接收視窗此時狀態仍未變;④接收方已收到0號幀,0號視窗關閉,1號視窗開啟,表示準備接收1號幀。此時發送視窗狀態不變;⑤發送方收到接收方發來的0號幀確認返回資訊,關閉0號視窗,表示從重發表中刪除0號幀。此時接收視窗狀態仍不變;⑥發送方繼續發送2號幀,2號視窗開啟,表示2號幀也納入待確認之列。至此,發送方開啟的視窗又已達規定限度,在未收到新的確認返回幀之前,發送方將暫停發送新的資料幀,此時接收視窗狀態仍不變;⑦接收方已收到1號幀,1號視窗關閉,2號視窗開啟,表示準備接收2號幀。此時發送視窗狀態不變;⑧發送方收到接收方發來的1號幀收畢的確認資訊,關閉1號視窗,表示從重發表中刪除1號幀。此時接收視窗狀態仍不變。
若從滑動視窗的觀點來統一看待1位元滑動視窗、後退n及選擇重傳三種協議,它們的差別僅在於各自視窗尺寸的大小不同而已。1位元滑動視窗協議:發送視窗=1,接收視窗=1;後退n協議:發視窗>1,接收視窗>1;選擇重傳協議:發送視窗>1,接收視窗>1。
demo示範:
核心代碼:
發送方:(接收和發送線程函數)
bool CUDPTrsmtFile::ServerSend()
{
if(!s_SendHoleMsg())
return false;
// 設定檔案指標位置,指向上次已發送的大小
SetFilePointer(m_hFile, m_dwSend, NULL, FILE_BEGIN);
int ret;
int nPacketCount = 0;
DWORD dwRet;
SendBuf sendbuf;
DWORD dwRead;
DWORD dwReadSize;
SendBuf* pushbuf;
SetEvent(m_hEvent);
//若已發送大小小於檔案大小並且發送視窗前沿等於後沿,則繼續發送
//否則退出迴圈
m_sumPacket = (m_dwFileSize - m_dwSend)/(MAXBUF_SIZE);
if(m_dwSend < m_dwFileSize) // 檔案沒有傳輸完時才繼續傳輸
{
while(1)
{
dwRet = WaitForSingleObject(m_hEvent, 1000);
if(dwRet == WAIT_FAILED)
{
CString temp="等待發送號誌出錯!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SendMessage(the_hWnd,FINISHEDTRANS_MSG,0,0);
return false;
}
else if(dwRet == WAIT_TIMEOUT)
{
//重發
int ackindex=0;
::EnterCriticalSection(&m_csQueue); // 進入m_bufqueue的排斥區
ackindex=m_bufqueue.front()->index;
ret = m_hsocket.hsendto((char*)m_bufqueue.front(), sizeof(sendbuf));
::LeaveCriticalSection(&m_csQueue); // 退出m_bufqueue的排斥區
if(ret == SOCKET_ERROR)
{
CString temp="發送視窗中的資料包發送失敗,重發資料包!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
continue;
}
CString temp;
temp.Format("等待ACK確認訊息逾時,重發發送視窗的第 %d 個資料包",ackindex);
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
ResetEvent(m_hEvent);
continue;
}
//若發送視窗大小 < 預定大小 && 已讀檔案次數(nReadIndex) < 需要讀檔案的次數(nReadCount),則繼續讀取發送
//否則,要發送的內容從m_bufqueue裡提取
if(m_dwSend < m_dwFileSize)
{
dwReadSize = m_dwFileSize - m_dwSend;
dwReadSize = dwReadSize < MAXBUF_SIZE ? dwReadSize : MAXBUF_SIZE;
memset(sendbuf.buf, 0, sizeof(sendbuf.buf));
if(!ReadFile(m_hFile, sendbuf.buf, dwReadSize, &dwRead, NULL))
{
CString temp="讀取檔案失敗,請確認檔案存在或有讀取許可權!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SendMessage(the_hWnd,FINISHEDTRANS_MSG,0,0);
return false;
}
m_dwSend += dwRead;
nPacketCount++;
sendbuf.index = m_nSendIndexHead;
m_nSendIndexHead = (m_nSendIndexHead + 1) % Sliding_Window_Size; // 發送視窗前沿向前移一格
sendbuf.dwLen = dwRead;
//儲存發送過的資料,以便重發
::EnterCriticalSection(&m_csQueue); // 進入m_bufqueue的排斥區
pushbuf = GetBufFromLookaside();
memcpy(pushbuf, &sendbuf, sizeof(sendbuf));
m_bufqueue.push(pushbuf);
if(m_dwSend >= m_dwFileSize) // 檔案已讀完,在隊列中加一File_End標誌,以便判斷是否需要繼續發送
{
pushbuf = GetBufFromLookaside();
pushbuf->index = File_End;
pushbuf->dwLen = File_End;
memset(pushbuf->buf, 0, sizeof(pushbuf->buf));
m_bufqueue.push(pushbuf);
}
::LeaveCriticalSection(&m_csQueue); // 退出m_bufqueue的排斥區
}
::EnterCriticalSection(&m_csQueue); // 進入m_bufqueue的排斥區
if(m_bufqueue.front()->index == File_End) // 所有資料包已發送完畢,退出迴圈
{
::LeaveCriticalSection(&m_csQueue); // 退出m_bufqueue的排斥區
break;
}
else if(m_bufqueue.size() <= Send_Window_Size) // 發送視窗小於指定值,繼續發送
{
ret = m_hsocket.hsendto((char*)m_bufqueue.front(), sizeof(sendbuf));
CString temp;
temp.Format("發送:發送視窗第 %d 個資料包正被發送!",m_bufqueue.front()->index);
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
if(ret == SOCKET_ERROR)
{
::LeaveCriticalSection(&m_csQueue); // 退出m_bufqueue的排斥區
CString temp="發送資料包失敗,重發!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
continue;
}
//延時,防止丟包
Sleep(50);
}
else // 發送視窗大於指定值,等持接收線程接收確認訊息
{
ResetEvent(m_hEvent);
}
::LeaveCriticalSection(&m_csQueue); // 退出m_bufqueue的排斥區
}
}
CString temp;
temp.Format("共分成 %d 個資料包傳送檔案!",nPacketCount);
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
CloseHandle(m_hEvent);
CloseHandle(m_hFile);
m_hFile = NULL;
m_hsocket.hclosesocket();
while(!m_bufLookaside.empty()) // clear
{
delete m_bufLookaside.front();
m_bufLookaside.pop();
}
temp="-------檔案已經傳輸完畢-------!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SendMessage(the_hWnd,FINISHEDTRANS_MSG,0,0);
return true;
}
bool CUDPTrsmtFile::ServerRecv()
{
if(!s_RecvHoleSuccessMsg())
return false;
int ret;
RecvBuf recvbuf;
int recvSumPacket=0;
while(m_hFile != NULL)
{
ret = m_hsocket.hrecvfrom((char*)&recvbuf, sizeof(recvbuf));
if(ret == SOCKET_ERROR)
{
::EnterCriticalSection(&m_csQueue);
if(m_bufqueue.front()->index == File_End) // 檔案傳輸完畢
{
::LeaveCriticalSection(&m_csQueue);
break;
}
::LeaveCriticalSection(&m_csQueue);
CString temp="接收檔案傳輸ACK訊息出錯!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SendMessage(the_hWnd,FINISHEDTRANS_MSG,0,0);
return false;
}
if(recvbuf.flag == Flag_Ack && recvbuf.index == m_nSendIndexTail)
{
CString temp;
temp.Format("接收:已接收到發送視窗第 %d 個資料包的ACK訊息!",recvbuf.index);
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
recvSumPacket++;
int progress=(int)((1.0*recvSumPacket/m_sumPacket)*100);
SendMessage(the_hWnd,PROGRESS_MSG,0,LPARAM(&progress));
m_nSendIndexTail = (m_nSendIndexTail + 1) % Sliding_Window_Size;
//該結點已得到確認,將其加入旁視列表,以備再用
::EnterCriticalSection(&m_csQueue);
m_bufLookaside.push(m_bufqueue.front());
m_bufqueue.pop();
::LeaveCriticalSection(&m_csQueue);
SetEvent(m_hEvent);
}
}
return true;
}
bool CUDPTrsmtFile::s_SendHoleMsg(void)
{
int ret;
int count = 0;
DWORD dwRet;
char sendbuf[32];
//發送串連訊息,最多發10次,若10次都失敗,則不能通訊
while(1)
{
if(count == 10)
{
CString temp="串連對方逾時!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SendMessage(the_hWnd,FINISHEDTRANS_MSG,0,0);
return false;
}
strcpy(sendbuf, "hole");
ret = m_hsocket.hsendto(sendbuf, strlen(sendbuf));
if(ret == SOCKET_ERROR)
{
CString temp="發送串連訊息失敗!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SendMessage(the_hWnd,FINISHEDTRANS_MSG,0,0);
return false;
}
dwRet = WaitForSingleObject(m_hEvent, 2 * 1000); // 等待接收線程接收串連成功訊息
if(dwRet == WAIT_FAILED)
{
return false;
}
else if(dwRet == WAIT_TIMEOUT)
{
CString temp="重新給對方發送串連訊息!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
count++;
continue; // 重發串連訊息
}
break;
}
//傳送檔案總大小
FileInfo fi;
fi.dwLen=m_dwFileSize;
strcpy(fi.filename,m_hFileName);
ret = m_hsocket.hsendto((char*)&fi,sizeof(fi));
if(ret == SOCKET_ERROR)
{
CString temp="發送傳送檔案資訊失敗!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SendMessage(the_hWnd,FINISHEDTRANS_MSG,0,0);
return false;
}
ResetEvent(m_hEvent); //設定m_hEvent為無訊號狀態,等待接收線程接收已傳送檔案大小
dwRet = WaitForSingleObject(m_hEvent, 15 * 1000);
if(dwRet == WAIT_FAILED)
{
CString temp="等待號誌出錯!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SendMessage(the_hWnd,FINISHEDTRANS_MSG,0,0);
return false;
}
else if(dwRet == WAIT_TIMEOUT)
{
CString temp="對方沒響應或拒收發送的檔案,您可以再重發一次!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SendMessage(the_hWnd,FINISHEDTRANS_MSG,0,0);
return false;
}
SetEvent(m_hEvent); //設定m_hEvent為有訊號狀態,以便傳送檔案
return true;
}
bool CUDPTrsmtFile::s_RecvHoleSuccessMsg(void)
{
int ret;
char szbuf[64];
RecvBuf recvbuf;
while(1)
{
ret = m_hsocket.hrecvfrom(szbuf, sizeof(szbuf)); //接收串連成功訊息
if(ret == SOCKET_ERROR)
{
CString temp="接收串連訊息不成功!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
Sleep(1000);
continue;
}
else
{
szbuf[ret]='/0';
if(strcmp(szbuf,"holesuccess")==0)
{
CString temp="串連成功,開始準備傳送檔案!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
}
break;
}
}
SetEvent(m_hEvent); //接收成功,設定m_hEvent為有訊號狀態,以便發送線程退出發送串連訊息迴圈
ret = m_hsocket.hrecvfrom((char*)&recvbuf, sizeof(recvbuf)); //接收已傳輸送檔案大小
if(ret == SOCKET_ERROR)
{
CString temp="接收已傳送檔案大小資訊出錯!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SendMessage(the_hWnd,FINISHEDTRANS_MSG,0,0);
return false;
}
if(recvbuf.flag == Flag_Refuse)
{
CString temp="Sorry,對方拒收傳送的檔案!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
CloseHandle(m_hEvent);
CloseHandle(m_hFile);
m_hFile = NULL;
m_hsocket.hclosesocket();
return false;
}
m_dwSend = recvbuf.dwFileSize;
CString temp;
temp.Format("上次斷點已傳送 %ld Byte!",m_dwSend);
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
SetEvent(m_hEvent); //接收成功,設定m_hEvent為有訊號狀態,發送線程開始傳送檔案
return true;
}
接收方:(核心線程函數)
bool CUDPTrsmtFile::ClientRecv()
{
if(!c_RecvHole_SendHoleSuccessMsg())
return false;
CString bstr="開始接收檔案!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&bstr));
int ret;
DWORD dwWritten;
SendBuf recvbuf;
RecvBuf sendbuf;
int nerror = 0;
// 設定檔案指標位置,指向上次已發送的大小
SetFilePointer(m_hFile, 0, NULL, FILE_END);
m_sumPacket = (m_dwFileSize - m_dwSend)/(MAXBUF_SIZE)+1;
int recvPacket = 0;
//若已接收檔案大小小於需要接收的大小,則繼續
while(m_dwSend < m_dwFileSize)
{
//接收
memset(&recvbuf, 0, sizeof(recvbuf));
ret = m_hsocket.hrecvfrom((char*)&recvbuf, sizeof(recvbuf));
if(ret == SOCKET_ERROR)
{
CString temp="接收檔案資料包出現錯誤,終止接收!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
return false;
}
//不是希望接收的,丟棄,繼續接收
if(recvbuf.index != (m_nRecvIndex) % Sliding_Window_Size)
{
nerror++;
CString temp="接收的資料報文非當前所需,丟棄!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
continue;
}
if(!WriteFile(m_hFile, recvbuf.buf, recvbuf.dwLen, &dwWritten, NULL))
{
CString temp="寫檔案出現錯誤,檔案接收終止!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
return false;
}
//已接收檔案大小
m_dwSend += dwWritten;
//發送確認訊息
sendbuf.flag = Flag_Ack;
sendbuf.index = m_nRecvIndex;
ret = m_hsocket.hsendto((char*)&sendbuf, sizeof(sendbuf));
if(ret == SOCKET_ERROR)
{
CString temp="發送ACK確認報文出錯,檔案接收終止!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
return false;
}
/////////////
recvPacket++;
int progress=(int)((1.0*recvPacket/m_sumPacket)*100);
SendMessage(the_hWnd,PROGRESS_MSG,0,LPARAM(&progress));
//接收視窗前移一格
m_nRecvIndex = (m_nRecvIndex + 1) % Sliding_Window_Size;
}
CString temp;
temp.Format("-------檔案接收完畢!--------/n>總共接收資料包個數 %ld 個!/n>傳輸過程中出錯的資料包個數: %d 個!",m_sumPacket,nerror);
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
CloseHandle(m_hFile);
m_hFile = NULL;
m_hsocket.hclosesocket();
SendMessage(the_hWnd,LISTEN_MSG,0,0);
return true;
}
bool CUDPTrsmtFile::c_RecvHole_SendHoleSuccessMsg(void)
{
int ret;
char recvbuf[64];
while(1)
{
ret = m_hsocket.hrecvfrom(recvbuf, sizeof(recvbuf));
if(ret == SOCKET_ERROR)
{
// CString temp="接收串連訊息失敗!";
// SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
return false;
}
recvbuf[ret] = '/0';
//收到對方發來的串連訊息,結束迴圈。同時建立通訊端,向發對方發送串連成功訊息,否則繼續等待接收
if(strcmp(recvbuf, "hole") == 0)
{
//發送串連成功訊息
char szsend[] = "holesuccess";
m_hsocket.makesendsockaddr(m_hsocket.getsockaddr(), m_hsocket.getsockport());
ret = m_hsocket.hsendto(szsend, sizeof(szsend));
if(ret == SOCKET_ERROR)
{
CString temp="發送串連成功確認訊息失敗!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
return false;
}
break;
}
else
{
CString temp="再次接收串連訊息!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
continue;
}
}
Sleep(10); // 等待對方接收
CString temp="串連成功!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
FileInfo fi;
ret = m_hsocket.hrecvfrom((char*)&fi,sizeof(fi));
if(ret == SOCKET_ERROR)
{
CString temp="接收檔案資訊失敗!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
return false;
}
m_dwFileSize=fi.dwLen;
strcpy(m_hFileName,fi.filename);
CString fName=CString(m_hFileName);
fName.Replace('//','/');
int index=fName.ReverseFind('/');
CString namestr=fName.Right(fName.GetLength()-index-1);
//SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&namestr));
if(IDNO == AfxMessageBox("有檔案發送過來,是否接收?",MB_YESNO))
{
RecvBuf sendbuf;
sendbuf.index = -1;
sendbuf.flag = Flag_Refuse;
ret = m_hsocket.hsendto((char*)&sendbuf, sizeof(sendbuf));
if(ret == SOCKET_ERROR)
{
CString temp="發送拒收訊息失敗!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
return false;
}
CString temp="發送拒收訊息成功,本機不接收該檔案!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
m_hsocket.hclosesocket();
SendMessage(the_hWnd,LISTEN_MSG,0,0);
return false;
}
char szName[255];
strcpy(szName,LPCTSTR(namestr));
saveDlg.m_ofn.lpstrFile=new char[255];
memset(saveDlg.m_ofn.lpstrFile,0,255);
//strcpy(saveDlg.m_ofn.lpstrFile,(LPCTSTR)namestr);
saveDlg.m_ofn.lpstrFile=LPSTR((LPCTSTR)namestr);
if (saveDlg.DoModal() == IDOK)
{
CString fullname=saveDlg.GetPathName();
strcpy(m_hFileName,LPCTSTR(fullname));
CString temp;
temp.Format("待接收的檔案: ");
temp+=fullname;
temp+="!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
}
delete[] saveDlg.m_ofn.lpstrFile;
SendMessage(the_hWnd,UPDATEINFO_MSG,0,0);
SetFile(m_hFileName);
temp.Format("待接收檔案的總大小為 %ld Byte!",fi.dwLen);
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
//發送已傳輸檔案大小
RecvBuf sendbuf;
sendbuf.dwFileSize = m_dwSend;
sendbuf.flag = Flag_ReSend;
ret = m_hsocket.hsendto((char*)&sendbuf, sizeof(sendbuf));
if(ret == SOCKET_ERROR)
{
CString temp="發送已傳送檔案大小失敗!";
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
return false;
}
temp.Format("發送斷點資訊:上次斷點已經接收 %ld Byte!",m_dwSend);
SendMessage(the_hWnd,ECHO_MSG,0,LPARAM(&temp));
return true;
}