基于TCP的文件传输程序.docx
《基于TCP的文件传输程序.docx》由会员分享,可在线阅读,更多相关《基于TCP的文件传输程序.docx(37页珍藏版)》请在冰豆网上搜索。
基于TCP的文件传输程序
《计算机网络系统实践》报告
2015年12月
一、设计要求
1、实现单线程文件传输功能
2、在以上的基础上,掌握多线程技术,在文件网络传输时,可选择单线程或多线程
3、加入异常控制机制,增强程序的鲁棒性(Robust)
4、了解如何提高套接字传输的速率,以及如何加强传输的稳定性
二、开发环境与工具
Windows7下MicrosoftVisualStuio
三、设计原理
1、网络传输协议
TCP/IP协议栈中的两个协议:
TCP(TransmissionControlProtocol):
传输控制协议
UDP(UserDatagrmProtocal):
用户数据报协议
TCP是面向连接的通信协议,TCP提供两台计算机之间的可靠无错的数据传输。
应用程序利用TCP进行通信时,源和目标之间会建立一个虚拟连接。
该连接一旦建立,两台计算机之间就可以把数据当做一个双向字节流进行交换。
UDP是无连接的通信协议,UDP不保证可靠数据的传输,但能够向若干个目标发送数据,接受发自若干个源的数据。
就是说,如果一个主机向另外一台主机发送数据,这个数据就会立即发出,而不管另外一台主机是否已准备接收数据。
如果另外一台主机接收到了数据,它不会确认收到与否。
本次工程目的是传输文件,显然需要用TCP建立连接,而TCP连接需要“三次握手”。
2、三次握手
三次握手具体过程:
1、客户端主动与服务器联系,TCP首部控制位中的SYN设置为1,发送带有SYN的TCP段,并把初始序号告诉对方
2、服务端收到带有SYN的报文,记录客户端的初始序号,选择自己的初始序号,设置控制位中的SYN和ACK。
因为SYN占用一个序号,所以确认序号设置为客户端的初始序号加1,对客户端的SYN进行确认
3、服务端的报文到达客户端,客户端设置ACK控制位,并把确认好设为服务器的初始序号加1,以确认服务器的SYN报文段,这个报文只是确认消息,告诉服务器已经成功建立了连接
四、系统功能描述及软件模块划分
1、系统功能描述
软件实现了点对点的文件传输。
传输前的接收提醒以及传输过程中的控制。
2、软件模块划分
本程序可以划分成以上三个模块:
传输控制模块,文件传输模块和服务连接模块。
其中:
服务连接模块用来建立客户端到服务器的连接
文件传输模块主要用两个线程:
_SendThread和_ListenThread来完成,实现发送和接收文件的功能。
传输控制模块主要通过封装好的可串行化信息类CMessage互相传递两端的文件操作消息,响应“暂停传输”,“关闭连接”等功能
五、设计步骤
1、服务连接模块
先要建立起客户端与服务器之间的连接,大致过程如下:
1服务器启动:
if(m_nServerType==SERVER)
{
//创建服务器套接字
m_psockServer=newCListenSocket(this);
if(!
m_psockServer->Create(m_wPort))//绑定端口
{
deletem_psockServer;
m_psockServer=NULL;
MessageBox(GetError(GetLastError()),_T("错误"),MB_ICONHAND);
return;
}
//开启线程,监听客户端连接
if(!
m_psockServer->Listen())
{
deletem_psockServer;
m_psockServer=NULL;
MessageBox(GetError(GetLastError()),_T("错误"),MB_ICONHAND);
return;
}
}
2客户端填入相应的IP地址,连接服务器
if(((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS))->IsBlank())
{
MessageBox("IP地址不能为空","错误",MB_ICONHAND);
return;
}
//创建客户端套接字
m_psockClient=newCClientSocket(this);
if(!
m_psockClient->Create())
{
deletem_psockClient;
m_psockClient=NULL;
MessageBox(GetError(GetLastError()),_T("错误"),MB_ICONHAND);
return;
}
//与服务器建立连接
CStringstrIPAddress;
GetDlgItem(IDC_IPADDRESS)->GetWindowText(strIPAddress);
if(!
m_psockClient->Connect(strIPAddress,m_wPort))//Connect连接服务器,服务器用OnAccept响应
{
deletem_psockClient;
m_psockClient=NULL;
MessageBox(GetError(GetLastError()),_T("错误"),MB_ICONHAND);
return;
}
//初始化客户端套接字
m_psockClient->Init();
3服务器响应
CClientSocket*pSocket=newCClientSocket(this);
//接收请求后获得新的套接字,交由新的套接字处理请求
if(m_psockServer->Accept(*pSocket))//非零则接收连接请求成功
{
//初始化Accpet返回的套接字
pSocket->Init();
CMessage*pMsg;
//检测套接字是否为空,空则还没有连接任何客户端
if(m_psockClient==NULL)
{
pMsg=newCMessage(CONNECT_BE_ACCEPT);
pSocket->SendMsg(pMsg);//发回“这边可以连接”消息回去,客户端的Receive接收
m_psockClient=pSocket;//若为服务器,则之前未初始化,可保存这个套接字,用来跟客户端交互
GetDlgItem(IDC_SELECT_FILE)->EnableWindow(TRUE);
}
else
{
pMsg=newCMessage(CONNECT_BE_REFUSE);
pSocket->SendMsg(pMsg);
}
}
4客户端收到连接成功消息
if(pMsg->m_nType==CONNECT_BE_ACCEPT)
{
GetDlgItem(IDC_SELECT_FILE)->EnableWindow(TRUE);
return;
}
2、传输控制模块
该工程用了两对套接字,一对用来接收传输控制消息,一对用来收发文件。
我们这个模块用的是第一对
在MFC中定义了一个在套接字编程中使用的CSocketFile类,可以充分发挥CSocket类的特性。
CSocketFile类是CFile的派生类,主要用来在WindowsSockets编程中发送和接收序列化数据(如结构体数据)。
通过把CSocketFile类对象、CArchive对象和CSocket创建的套接字对象联系起来,可以实现数据的加载(接收)和存储(发送)。
MFC集成CSocketFile、CSocket、CArchive可以简化网络传输。
下图可以直观地反应这三者是如何结合来在客户端和服务器之间传输消息的
先来看看服务器和客户端的套接字初始化工作:
m_pFile=newCSocketFile(this);
m_pArchiveIn=newCArchive(m_pFile,CArchive:
:
load);
m_pArchiveOut=newCArchive(m_pFile,CArchive:
:
store);
这样一来,当使用m_pArchiveIn对象来调用可串行化消息类CMessage的Serialize时,接收方就能响应OnReceive消息,来完成控制信息的传递。
本工程在主类CsendFileDlg中定义了传输的三种状态:
m_bIsWait:
已连接,正在等待传输文件
m_bIsTransmitting:
正在传输文件
m_bIsStop:
传输已终止
本工程定义的可串行化消息类CMessage,用来传递文件名m_strFileName、文件长度m_dwFileSize以及当前的传输状态m_nType,大致流程如下图:
其中,我在建工程的时候,把客户端和服务器集成在了一个界面OnReceive就用来接收两方的消息,关键代码如下:
CMessage*pMsg=newCMessage();
pSocket->ReceiveMsg(pMsg);
if(pMsg->m_nType==CONNECT_BE_ACCEPT)
{
GetDlgItem(IDC_SELECT_FILE)->EnableWindow(TRUE);
return;
}
if(pMsg->m_nType==CONNECT_BE_REFUSE)
{
MessageBox(_T("服务器正在和另外的客户端连接,稍等"),_T("错误"),MB_ICONHAND);
deletem_psockClient;
m_psockClient=NULL;
return;
}
if(pMsg->m_nType==DISCONNECT)
{
MessageBox(_T("对方已经关闭"),_T("警告"),MB_ICONHAND);
if(m_psockClient!
=NULL)
{
deletem_psockClient;
m_psockClient=NULL;
}
}
//接收请求
if(pMsg->m_nType==REQUEST)
{
m_bIsWait=TRUE;//忙状态置1
m_strFileName=pMsg->m_strFileName;
m_dwFileSize=pMsg->m_dwFileSize;
CStringstrName,strSize;
strName.Format("文件名:
%s",m_strFileName);
strSize.Format("文件大小:
%ld字节",strSize);
//这里完成了发送文件前给对方提示的功能
if(MessageBox(strName+"\r\n"+strSize,"对方请求向你发送文件",MB_OKCANCEL)==IDCANCEL)
{
CMessage*pMsg=newCMessage(REFUSE);
m_psockClient->SendMsg(pMsg);
return;
}
CFileDialogdlg(FALSE,NULL,NULL,OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT,"所有文件(*.*)|*.*||",this);
dlg.m_ofn.lpstrTitle=_T("另存为");
strcpy(dlg.m_ofn.lpstrFile,m_strFileName.GetBuffer(m_strFileName.GetLength()));
if(dlg.DoModal()==IDOK){
if(m_bIsWait==FALSE){
MessageBox(_T("对方已经取消文件发送"),_T("警告"),MB_ICONEXCLAMATION);
return;
}
m_bIsClient=FALSE;
m_strPath=dlg.GetPathName();
//启动接收文件的线程
GetDlgItem(IDC_SR)->SetWindowText("已接收:
");
pThreadListen=:
:
AfxBeginThread(_ListenThread,this);//服务器接收到RUEST,开启接收文件线程
return;
}
if(m_bIsWait==TRUE)
{
//告诉对方文件发送请求被拒绝
CMessage*pMsg=newCMessage(REFUSE);
m_psockClient->SendMsg(pMsg);
}
m_bIsWait=FALSE;
return;
}
//当对方同意,并且已经准备好接收文件的时候
if(pMsg->m_nType==ACCEPT)
{
KillTimer
(1);
m_bIsWait=FALSE;//等待状态清空
//启动发送文件线程
GetDlgItem(IDC_SR)->SetWindowText("已发送:
");
pThreadSend=:
:
AfxBeginThread(_SendThread,this);
return;
}
//当发送文件请求被拒绝
if(pMsg->m_nType==REFUSE){
m_bIsWait=FALSE;
MessageBox(_T("请求被拒绝"),_T("警告"),MB_ICONEXCLAMATION);
return;
}
//当对方取消文件传输时执行该if语句里面的内容
if(pMsg->m_nType==CANCEL)
{
m_bIsWait=FALSE;
return;
}
3、文件传输模块
两个线程:
_ListenThread():
建立新的监听套接字,准备接收文件
_SendThread():
建立客户端套接字,连接监听套接字,发送文件
当两方建立了可靠的连接,一方选择文件后,就会使用传输控制模块给对方发送文件发送请求,对方的ProcessReceive接收到后查看当前的消息状态,(上个模块的OnReceive已经写得很清楚),同意接受后,开启文件接收线程,同时给发送方返回一个准备完成状态,于是,发送方开启文件发送线程,最后完成文件的传输工作,同时置状态m_bIsStop为TRUE,便于双方传输控制
1发送线程关键函数SendFile
voidCSendFileDlg:
:
SendFile(CSocket&senSo)
{
m_bIsTransmitting=TRUE;
CFilefile;
if(!
file.Open(m_strPath,CFile:
:
modeRead|CFile:
:
typeBinary))
{
AfxMessageBox(_T("文件打开失败"));
senSo.Close();
return;
}
m_ctrlProgress.SetRange32(0,m_dwFileSize);
intnSize=0,nLen=0;
DWORDdwCount=0;
charbuf[BLOCKSIZE]={0};
file.Seek(0,CFile:
:
begin);
//开始传送
while
(1)
{
//每次读取BLOCKSIZE大小
nLen=file.Read(buf,BLOCKSIZE);
if(nLen==0)
break;
//发送文件内容
nSize=senSo.Send(buf,nLen);
dwCount+=nSize;
m_ctrlProgress.SetPos(dwCount);
CStringstrTransfersSize;
strTransfersSize.Format("%ld字节",dwCount);
//设置进度条属性
GetDlgItem(IDC_RECEIVE_SIZE)->SetWindowText(strTransfersSize);
intp=((double)dwCount)/((int)m_dwFileSize+1)*100+1;
strTransfersSize.Format("%d",p);
strTransfersSize+="%";
//判断用户是否停止发送
if(m_bIsStop)
{
m_bIsStop=FALSE;
break;
}
if(nSize==SOCKET_ERROR)
break;
}
file.Close();
senSo.Close();
if(m_dwFileSize==dwCount)
AfxMessageBox(_T("文件发送成功"));
else
AfxMessageBox(_T("文件发送失败"));
m_ctrlProgress.SetPos(0);
m_bIsTransmitting=FALSE;
}
2接收线程关键函数ReceiveFile
voidCSendFileDlg:
:
ReceiveFile(CSocket&recSo)
{
//停止等待超时计时器
KillTimer
(2);
m_bIsWait=FALSE;
m_bIsTransmitting=TRUE;//正在传输置1
m_ctrlProgress.SetRange32(0,m_dwFileSize);
intnSize=0;
DWORDdwCount=0;
charbuf[BLOCKSIZE]={0};
CFilefile(m_strPath,CFile:
:
modeCreate|CFile:
:
modeWrite);
//开始接收文件
while
(1)
{
//每次接收BLOCKSIZE大小的内容
nSize=recSo.Receive(buf,BLOCKSIZE);
if(nSize==0)
break;
//写入文件
file.Write(buf,nSize);
dwCount+=nSize;
m_ctrlProgress.SetPos(dwCount);
CStringstrTransfersSize;
strTransfersSize.Format("%ld字节",dwCount);
GetDlgItem(IDC_RECEIVE_SIZE)->SetWindowText(strTransfersSize);
intp=((double)dwCount)/((int)m_dwFileSize+1)*100+1;
strTransfersSize.Format("%d",p);
strTransfersSize+="%";
GetDlgItem(IDC_PECENT)->SetWindowText(strTransfersSize)
//用户是否停止了接收
if(m_bIsStop)
{
m_bIsStop=FALSE;
break;
}
}
file.Close();
recSo.Close();
if(m_dwFileSize==dwCount)
AfxMessageBox(_T("文件接收成功"));
else
AfxMessageBox(_T("文件接收失败"));
m_ctrlProgress.SetPos(0);
m_bIsTransmitting=FALSE;
}
六、关键问题及其解决方法
1、在传输文件前、中、后能让程序响应各类控制消息
集成后的互通消息代码很简单:
voidSerialize(CArchive&ar){
if(ar.IsStoring()){
ar<ar<ar<}
else
{
ar>>m_nType;
ar>>m_strFileName;
ar>>m_dwFileSize;
}
}
CSocket、CSocketFile、CArchive三个类的联动,完美地解决了这个问题,在传输文件的过程中,若一方有事件发生(暂停传输、关闭连接等等),则改变预先设定的状态值,SendMsg给CSocketFile,触发对方的虚函数OnReceive,从而解决通信问题。
2、解决单线程传输阻塞的非“非阻塞”式方法
在使用AfxBeginThread实现多线程的同时,也增加了诸如发送超时、接收超时等判断(使用Windows定时器SetTimer响应OnTime来实现),这一定程度上增强了程序的鲁棒性
文件的传输时阻塞式的,所以在主线程中直接传输文件必然导致界面卡死,所以这里我用到了多线程技术,为传输文件和接受文件分别开设线程。
1_ListenThread
UINT_ListenThread(LPVOIDlparam)
{
CSendFileDlg*pDlg=(CSendFileDlg*)lparam;
//创建服务器端套接字
CSocketsockSrvr;
if(!
sockSrvr.Create(pDlg->m_wPort+PORT))
{
pDlg->TransfersFailed();
:
:
MessageBox((HWND)lparam,pDlg->GetError(GetLastError()),_T("错误"),MB_ICONHAND|MB_OK);
return-1;
}
if(!
sockSrvr.Listen())//监听
{
pDlg->TransfersFailed();
:
:
MessageBox((HWND)lparam,pDlg->GetError(GetLastError()),_T("错误"),MB_ICONHAND|MB_OK);
return-1;
}
//接收套接字已经创建向主对话框发送自定义消息,该消息发送一个消息给发送方,可以开始传数据
pDlg->SendMessage(WM_ACCEPT_TRANSFERS);
CSocketrecSo;
if(!
sockSrvr.Accep