《计算机网络系统实践》报告
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
计算机与信息学院
《计算机网络系统实践》报告
2008 年 9 月
设计题目:网络文件传输
学生姓名:高明
学 号:20052361
专业班级:计算机05-1班
一、设计要求
1.实现单线程文件传输功能;
2.在以上基础上,掌握多线程技术,在文件网络传输时,可选择单线程或多
线程
3.加入异常控制依据,增强程序的鲁棒性;
4.了解如何提高套接字传输的速率,以及如何加强传输的稳定性。
二、开发环境与工具
Windows XP和VC6.0
三、设计原理
首先,网络应用程序是一种在不同系统的新进程间通过网络通信协议进行的进程间的通信问题。
在网络中为了标识通信的进程,首先要标识网络中进程所在的主机,其次要标识主机用IP地址来标识不同的主机,主机上不同的进程要用使用端口号来标识。
即:本地协议、本地地址、本地端口号、远地协议、远地地址、远地端口号。
其次,在Windows中编程是通过套接口SOCKET来编程的,套接口可以看成是两个网络应用程序进行的通信时,各自通信连接种的一个端点。
通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机的Socket中,该Socket通过网络接口卡NIC的传输介质将这段信息发送到另一台主机的Socket 种,使这段信息能穿传送到其他程序中。
套接口分为流式和数据报套接口。
网络传输文件的时候还需要用到多线程和线程间访问互斥文件的问题。
创建线程后,一个应用程序可以同时有多个线程一起运用CPU的资源,大大的加强对CPU的利用率。
同时也从另一个方面加快了传输的速度。
但是要遇到一个问题就是各个贤臣对文件的共享问题。
所以建立好的信号量来实现他们之间的访问规则是很重要的。
否则,传输的文件将会出现很多问题。
四、系统功能描述及软件模块划分
我设计实现的文件传输系统主要包括两个大的部分:服务器端和客户端。
其中服务器端开启服务,监听客户端的连接,然后如果有客户端开启,建立了连接以后,就可以由服务器端选择文件来进行发送传输。
在服务器端,用户可以选择
是采用单线程还是多线程,多线程传输的时候可以选择线程数目。
客户端同时也是和服务器端一样用多线程进行连接和接收。
所以,模块的设计主要有:服务器的启动、客户端的连接、线程类型的选择和告知、文件的分块划分、文件的接收保存和合并等等。
五、设计步骤
首先服务器和客户端要创建两种套接字的Socket,一个是基于流式一个是数据报。
流式套接字是用来进行文件的传输用,而数据报是用来实现通讯用的服务。
当然这两个类都是自己新建的而且是继承自CasyncSocket类。
class CXXSock : public CasyncSocket
class CServerSock : public CasyncSocket
在两个类中要自己重写void CServerSock::OnAccept(int nErrorCode)和void CXXSock::OnReceive(int nErrorCode)两个函数。
分别用来接收信息和接收连接。
//在主对话框种OnInitDialog的时候要对其进行初始化
m_sockServ = new CXXSock();
if ( !m_sockServ->Create(1314, SOCK_DGRAM) )
{
delete m_sockServ;
m_sockServ = NULL;
MessageBox("UDP-Socket创建失败!");
}
可以注意到,m_sockServ是创建的基于数据报式的套接字,用来通信用。
客户端也是同样的实现。
在服务器端,启动服务的时候会运行下面的代码:
listenSocket.Create(7000);
listenSocket.Listen();
这个listenSocket在创建的时候默认就是基于流式的套接字,用来传输文件用。
在客户端那边也有一个用来通信的socket。
在刚开始启动的时候,会向服务器索要线程数来创建Socket数量:
if ( m_sockClient->SendTo( "&*&", 4, 1314, m_strIP ) == SOCKET_ERROR )// 握手服务器索要线程数。
服务器端接到“&*&”的字符以后就会向接收Socket发送线程数。
客户端接收到线程数后就可以开始连接了,点击连接按钮控件后运行如下代码:for ( int i=0; i<numThreadM; i++ )
{
CString str;
str.Format("%d Connect False!", i);
receiveSockM[i].Create();
receiveSockM[i].Connect(strServerIP,7000);
}
即逐个创建连接接收文件线程。
在服务器端,刚刚提到一个OnAccept虚函数,这个函数会接收客户端的每个连接,然后计数达到numThread个数后就提示“连接成功!”。
连接成功后,服务器端选择文件进行发送:
CFileDialog openFile(TRUE);
if ( IDOK == openFile.DoModal() )
{
filename=openFile.GetFileName();
strcpy( fileNameM,filename.GetBuffer(0) );
}
当然应该把文件名和文件的大小也同时保存下来。
还有就是路径,我也实现了在界面上显示文件的相关属性的功能,待会的设计结果里面会有说明。
在服务器选择文件并点选发送以后,会用通信Socket向客户端发送文件大小、文件名之类的信息,然后客户端也可以显示,下面是数据的编码:
CString strFileMess;
strFileMess.Format("&$&%ld", m_fileSize);
strFileMess += '#';
strFileMess += fileNameM;
strFileMess += '#';
m_sockServ->SendTo((LPCTSTR)strFileMess,strFileMess.GetLeng th(),&m_sockServ->m_saToSend, sizeof(SOCKADDR) );
客户端如果确认接收,那么服务器端就相应的开始发送数据了。
在发送数据的过程中是用了多线程的技术。
创建numThread个nEnvent事件用来实现互斥,然后就相应的创建线程:
for ( int i=0; i<numThread; i++ )
{
if((hMessageSend[i]=CreateEvent(NULL,false,false,NULL))==NU LL)
AfxMessageBox("Create hE-Event Handle Fail");
ResetEvent( hMessageSend[i] );
}
for(i=0;i<numThread;i++)
_beginthread( SendThreadFuncM, 0, (void *)i );
在sendThreadFuncM中:
void SendThreadFuncM(void * pParam)
{
int idx=(int)pParam;
SendThreadM(idx+1);//start from 1
_endthread();
}
调用线程执行函数SendThreadM来运行相应的文件发送代码。
关于线程分割文件的问题在下面的一节中再进行介绍。
客户端的接收其实也是大同小异,代码几乎相同。
while(LeftToRead>0)//leftToRead是还有多少要发送
{
if ( m_sockServ->m_stopRecved )return;
ReadOnce = ( LeftToRead>ReadSize ? ReadSize : LeftToRead );
count = file.Read( data,ReadOnce );//读取相应大小文件到data
sendCount += ReadOnce;//已发文件计数;
WaitForSingleObject( hMessageSend[idx-1],20 );
while(SOCKET_ERROR==sendSockets[idx-1].Send(data,count) )//发送LeftToRead=LeftToRead-count;//设置剩余文件大小
pMainDlg->m_ctrlProgress.SetPos(sendCount);
CString str;
str.Format("%ld (B)", sendCount);
pMainDlg->GetDlgItem(IDC_STACOPYED)->SetWindowText(str);
}
file.Close();
上面这段代码就是发送文件的核心代码,就是在这里实现了文件的分块发送,主要是在循环里面来进行的。
代码很清晰这里不做过多的解释。
在客户端也是相同的机制,不过在分块接收过后要多一道程序就是合并文件。
其实合并文件也是相当简单,就是逐个打开刚刚保存的文件,然后依次写到另一个新建的文件里面去。
具体情况见void CombineFilesM()函数。
六、关键问题及其解决方法
在设计的过程中有几个关键性的问题如下:
1.首先是服务器端的线程数目如何让客户端知道。
如果说客户端不知道服务器端开了几个线程,那么就没有办法建立连接。
因为在服务器端比如有5个socket在监听连接,也就是说一会要
创建5个发送线程。
但是如果在客户端你创建了6个连接甚至更多,那
么就会导致失败,因为有一个无法连接,数据会出问题。
如果只建立了4
个连接甚至更少,那服务器端就不会连接成功,因为他在等待5个连接。
所以就要在启动的时候各个建立一个基于数据报的socket单独的连接。
在客户端连接的时候,会先向服务器握手,其实就是向服务器索要线程
数的。
在内部就发送相应字符串到服务器,服务器接收到后也发送特殊
的字符串到客户端来实现交互。
当客户端知道服务器开通了多找线程后
就可以正常的Connect了。
其中客户端的OnBtnGood函数就是握手服务器的时候调用的,可以
看一下里面的实现,正是向服务器发送了一个字符串来索要线程数。
在服务器端的CXXSock::OnReceive(int nErrorCode)类方法里面,你会看到对相应字符串的解析和处理,其中第一个if里面的处理就是发
送了线程数—num过去。
在客户端的CXXSock::OnReceive(int nErrorCode)函数里面,也会看到相应的处理函数,就是得到numThread。
这样就实现了这个基本功能。
2.文件的大小分割。
因为创建了多个线程来传输文件,当然要对文件首先进行分割达到传送的目的。
但是各个线程的创建怎么来区分我到底是要传送文件的那
一部分呢?这就引入了GetBeginPosM这个函数。
因为每个线程都设置
了一个线程标识idex,根据每个线程的这个数字来区分自己是哪个线
程。
在分割文件的时候,主要思想是把文件分成nunThread等份,然后
每个人调用GetBeginPosM函数后都会根据传进去的idex来计算我是
分管哪一块的,然后返回文件的起点。
在得到起点以后,传输的话可以
先把文件指针跳到那个位,然后从该位置开始传输一定大小的文件过去。
七、设计结果
图1
——图1是客户端的界面。
其中服务器IP是输入服务器端的IP地址,然后首先点击“握手服务器”,就会连接到服务器上得到线程数如图2:
图2
下图3是服务器端的界面,可以选择是单线程还是多线程以及线程数:
图3
图4
客户端点击连接后,双方建立连接,然后会提示成功。
图5
图5所示是在选取文件进行发送
图6
如图6所示点击发送后,在服务器和客户端都显示了文件的相关属性!这个时候客户端可以选择是否要接收,如果点击接收选择另存为的路径位置,则服务器端就开始了发送。
并且用进度条来显示传输进度以及实时显示已传大小:
图7
图8
这样直到传输结束。
八、软件使用说明
1、在一台PC上启动服务器端,于另一台机器上启动客户端。
两机器之间要有网络连接。
且于同一个IP段内。
2、点击服务器端的“启动”按钮启动服务器。
3、服务器启动后,在客户端的IP地址栏里输入服务器的主机IP地址,完成后点击“握手服务器”获得线程数。
4、客户端点击“连接”,连接服务器
5、连接成功后,服务器端点击“选取文件”选取欲发送文件,完成后点击“发送文件”。
6、客户端获取到发送请求后,得到了文件的相关信息,确认要接收后点击“接收”按钮,选择保存文件的路径位置。
7、开始接收,服务器端开始发送。
8、Combine发送完毕!成功,请点击“退出”进行退出操作。
9、发送过程中如想要终止发送,可点击“终止发送”来实现。
九、参考资料
[1] 任泰明 TCP/IP协议与网络编程西安电子科技大学出版社 2004
[2] 罗斌 Visual C++编程技巧精选500例中国水利水电出版社 2004
[3] 孙鑫 VC++深入详解电子工业出版社 2006
十、验收时间及验收情况
2008年9月12日星期五于逸夫科技楼五楼网络实验室成功验收。
十一、设计体会
这次的课程设计收获很多。
首先是学习和领会了多线程的应用以及它能带来的好处。
再就是对网络传输有了进一步的了解。
学会了MFC中对网络编程所封装的几个类的使用方法。
其次在设计的过程中也是遇到了很多的问题,比如说多线程的创建,互斥问题、文件的分割问题、发送和接收文件的混乱问题等等。
都在不断的调试中得到了修改。
我觉得软件的整体设计是相当重要的。
如果刚开始不从一个全局的观念去设计好软件,那么在实现的过程中就会出现动拼一句西凑一条的问题,到最后代码连自己也无法看得懂。