基于P2P的局域网即时通信系统
二、实验环境及工具
1.计算机:PC机,PC虚拟机,
2.操作系统:Windows2000,WindowsXP
3.程序设计语言: VC 6.0
三、设计要求
1.实现一个图形用户界面局域网内的消息系统。
2.功能:建立一个局域网内的简单的P2P消息系统,程序既是服务器又是客户,服务器端口使用3333。
a)用户注册及对等方列表的获取:对等方A启动后,用户设置自己的信息(用户名,所在组);扫描网段中在线的对等方(3333端口打开),向所有在线对等方的服务端口发送消息,接收方接收到消息后,把对等方A加入到自己的用户列表中,并发应答消息;对等方A把回应消息的其它对等方加入用户列表。双方交换的消息格式自己根据需要定义,至少包括用户名、IP地址。
b)发送消息和文件:用户在列表中选择用户,与用户建立TCP连接,发送文件或消息。
3.用户界面:界面上包括对等方列表;消息显示列表;消息输入框;文件传输进程显示及操作按钮或菜单。
四、设计内容与步骤
1.学习Socket和TCP的基本原理和通信机制;
2.功能设计和界面设计
3.服务器功能的设计和实现
4.客户功能的设计和实现
5.课程设计任务说明书
五、方案设计
1.消息格式
本系统采用的消息格式是,文件头+消息内容
文件头为 ‘1’-‘9’,消息格式分配如下:
‘1’+本机名:登陆,发送给所有在线对等方的服务端口
‘2’+本机名:对登陆消息的回馈
‘3’+本机名:退出
‘4’+本机名:对话请求
“51”或”52”:对话请求的回应(是否同意)
‘6’+本机名+”退出对话”:退出对话
‘7’+对话内容:对话
‘8’+文件名长度+文件名+文件长度(转换成CString):请求传送
“91”同意传输
“92”拒绝
“93”磁盘已满
2.该软件分别开了3个监听端口:3333、3334、3335。之所以分开3个端口是因为各种传送的不同,在设计实验的过程中我发现对于登陆消息,退出消息,应该用的socket是即用即断,即比如我收到登陆消息,并发送回馈消息后就断开连接,这样就不用一个用户同时连接很多用户,如果用完不断,就是全连接了。而文件传输应该跟对话传输分开,因此应该再开一个端口。
3.在线用户的扫描:
本软件是通过扫描局域网内的在线用户(不一定打开软件),然后一一发送登陆信息,如果收到登陆信息就在列表上增加用户并发送回馈,如果收到回馈就在列表上增加用户,如果收到退出消息就删除用户。
4.文件传输
原本打算使用多线程文件传输,及发送端开多个线程同时读一个文件并发送,接收端在磁盘开辟一个与接收文件大小一致的一个文件,然后接收端开多个线程接收并各自负责写进特定文件位置,不过由于Socket匹配问题,因此还是使用单线程传输比较简单一点。
六、方案实现及主要程序
1.工程中的类
(1).本软件中分别有三个CAsyncSocket的派生类,分别是CCtrlSocket,CTalkSocket,CFileSocket
a)CCtrlSocket:用于接收及发送控制信息,包括文件头为‘1’(登陆); ‘2’(回馈);‘3’(退出); ‘4’(对话请求);’5’(对话请求的回应)的消息,对应监听端口是CTRLPORT——3333
b)CTalkSocket:用于接收及发送对话信息,及部分文件控制信息。包括文件头为‘6’(退出对话); ‘7’(对话); ‘8’(请求传送); ‘9’(传送回应)的消息,对应监听端口是TALKPORT——3334
c)CFileSocket:用于发送及接收文件,对应监听端口是FILEPORT——3335
其它类如CPathDialog,CFileDlg与本设计的主要部分无紧要联系,故不一一说明了
2.类的具体实现
(1).CCtrlSocket类:主要部分有FD_READ及 FD_CONNECT触发的事件,OnConnect在建立连接后发送出相应的消息,而OnReceive在有消息到来的情况下处理消息
void CCtrlSocket::OnReceive(int nErrorCode)
{
// TODO: Add your specialized code here and/or call the base class
char q[50];
char t;
unsigned int j;
CString tempaddr;
CString Ctemp;
UINT tempport;
this->Receive(q,strlen(q)+1,0);
t=q[0];
for(j=0;j q[j]=q[j+1]; } CChatApp *pApp=(CChatApp *) AfxGetApp(); CChatDlg *pDlg = (CChatDlg *) pApp->m_pMainWnd; pDlg->UpdateData(true); switch(t)//对控制信息的判断 { case '1'://登陆 pDlg->m_listonline.InsertItem(0,q); this->GetPeerName(tempaddr,tempport); pDlg->m_listonline.SetItemText(0,1,tempaddr); Ctemp="2"+pDlg->m_hostname; this->Send(Ctemp,strlen(Ctemp)+1,0); break; case '2'://回馈 pDlg->m_listonline.InsertItem(0,q); this->GetPeerName(tempaddr,tempport); pDlg->m_listonline.SetItemText(0,1,tempaddr); break; case '3'://退出 for(j=0;j { if(pDlg->m_listonline.GetItemText(j,0)==q) { pDlg->m_listonline.DeleteItem(j); } } break; case '4'://请求对话 Ctemp.Format("%s",q); Ctemp="是否接受"+Ctemp+"的对话请求?"; if(AfxMessageBox(Ctemp, MB_YESNO|MB_ICONQUESTION) != IDYES) { Ctemp="52";//拒绝 this->Send(Ctemp,strlen(Ctemp)+1,0); break; } else if(TalkSocket.m_hSocket!=INVALID_SOCKET) { Ctemp="6"+pDlg->m_hostname+"退出对话";//断开原来对话 TalkSocket.Send(Ctemp,strlen(Ctemp)+1,0); } Ctemp="51";//同意 this->Send(Ctemp,strlen(Ctemp)+1,0); this->GetPeerName(tempaddr,tempport); pDlg->GetDlgItem(IDC_CUT_OFF)->EnableWindow(true); pDlg->GetDlgItem(IDC_SEND_MSS)->EnableWindow(true); pDlg->GetDlgItem(IDC_SEND_FILE)->EnableWindow(true); _tcpSocketClose(TalkSocket); _tcpSocketConnect(TalkSocket,tempaddr,TALKPORT); pDlg->m_linkip=tempaddr; pDlg->m_linkname.Format("%s",q); break; case '5'://请求对话的回应 if(q[0]=='1') { pDlg->m_editrec+="完成连接\\r\\n"; pDlg->GetDlgItem(IDC_CUT_OFF)->EnableWindow(true); pDlg->GetDlgItem(IDC_SEND_MSS)->EnableWindow(true); pDlg->GetDlgItem(IDC_SEND_FILE)->EnableWindow(true); } else if(q[0]=='2') AfxMessageBox("对方不想与你对话或者对方正忙!"); else AfxMessageBox("Error!"); break; default: break; } pDlg->UpdateData(false); CAsyncSocket::OnReceive(nErrorCode); } void CCtrlSocket::OnConnect(int nErrorCode) { // TODO: Add your specialized code here and/or call the base class if(nErrorCode==0) { this->AsyncSelect(FD_READ); CChatApp *pApp=(CChatApp *) AfxGetApp(); CChatDlg *pDlg = (CChatDlg *) pApp->m_pMainWnd; pDlg->UpdateData(true); CString Ctemp; switch(SendMssKind) { case 1: Ctemp="1"+pDlg->m_hostname; this->Send(Ctemp,strlen(Ctemp)+1,0); break; case 3: Ctemp="3"+pDlg->m_hostname; this->Send(Ctemp,strlen(Ctemp)+1,0); break; case 4: Ctemp="4"+pDlg->m_hostname; this->Send(Ctemp,strlen(Ctemp)+1,0); break; default: break; } } CAsyncSocket::OnConnect(nErrorCode); } (2).CTalkSocket类:主要部分有FD_READ及 FD_CLOSE触发的事件,OnClose对方关掉软件后响应,而OnReceive在有消息到来的情况下处理消息 void CTalkSocket::OnReceive(int nErrorCode) { // TODO: Add your specialized code here and/or call the base class char q[150]; unsigned int j; CString tempaddr; CString Ctemp; CString filename; CString filelen; long file_length; char RootPathName[4]; // root path DWORD SectorsPerCluster; // sectors per cluster DWORD BytesPerSector; // bytes per sector DWORD NumberOfFreeClusters; // free clusters DWORD TotalNumberOfClusters; // total clusters long DiskFree; this->Receive(q,strlen(q)+1,0); CChatApp *pApp=(CChatApp *) AfxGetApp(); CChatDlg *pDlg = (CChatDlg *) pApp->m_pMainWnd; pDlg->UpdateData(true); char t=q[0]; for(j=0;j q[j]=q[j+1]; } Ctemp.Format("%s",q); switch(t) { case '6'://结束对话 pDlg->m_editrec=pDlg->m_editrec+Ctemp+"\\r\\n"; _tcpSocketClose(TalkSocket); pDlg->GetDlgItem(IDC_CUT_OFF)->EnableWindow(false); pDlg->GetDlgItem(IDC_SEND_MSS)->EnableWindow(false); pDlg->GetDlgItem(IDC_SEND_FILE)->EnableWindow(false); break; case '7'://对话信息 pDlg->m_editrec=pDlg->m_editrec+Ctemp+"\\r\\n"; break; case '8'://请求文件传输 //q[0]=q[0]-48; filename=Ctemp.Mid(1,q[0]); file_length=atol(Ctemp.Right(Ctemp.GetLength()-q[0]-1)); if(file_length<1024) filelen.Format("%ld字节",file_length); else if(file_length<1048576) filelen.Format("%.2fK",file_length/(float)1024); else if(file_length<1073741824) filelen.Format("%.2fM",file_length/(float)1048576); else filelen.Format("%.2fG",file_length/(float)1073741824); Ctemp="是否接受对方的文件["+filename+"]?[约"+filelen+"]"; if(AfxMessageBox(Ctemp, MB_YESNO|MB_ICONQUESTION) != IDYES) { Ctemp="92";//拒绝 this->Send(Ctemp,strlen(Ctemp)+1,0); } else { RootPathName[0]=pDlg->m_editdir[0]; RootPathName[1]=pDlg->m_editdir[1]; RootPathName[2]=pDlg->m_editdir[2]; RootPathName[3]=0; GetDiskFreeSpace(RootPathName,&SectorsPerCluster,&BytesPerSector,&NumberOfFreeClusters,&TotalNumberOfClusters); DiskFree=(long)SectorsPerCluster*BytesPerSector*NumberOfFreeClusters;//大于一定数目会变成负数,不过只要小于2G,即1073741824*2就不会了 if(DiskFree<0||DiskFree>file_length) { pDlg->m_editfile=pDlg->m_editdir+"\\\\"+filename; pDlg->m_filelen=filelen; Ctemp="91";//同意 this->Send(Ctemp,strlen(Ctemp)+1,0); CFile file; if(!file.Open(pDlg->m_editfile,CFile::modeCreate)) AfxMessageBox("文件建立失败"); file.Close(); pDlg->file_length=file_length; pDlg->GetDlgItem(IDC_SEND_FILE)->EnableWindow(false); } else { AfxMessageBox("磁盘空间不足,自动放弃接收文件!"); Ctemp="93";//磁盘空间不足 this->Send(Ctemp,strlen(Ctemp)+1,0); } } break; case '9'://请求文件传输的回应 if(q[0]=='1') { pDlg->m_editrec+="准备传输……(请不要使用或移动传输的文件)\\r\\n"; _tcpSocketClose(FileConn); _tcpSocketConnect(FileConn,pDlg->m_linkip,FILEPORT); pDlg->GetDlgItem(IDC_SEND_FILE)->EnableWindow(false); } else if(q[0]=='2') { AfxMessageBox("对方不想接收你的文件!"); } else if(q[0]=='3') { AfxMessageBox("对方磁盘已满,不能接收!"); } else { AfxMessageBox("Error!"); } break; case 'A'://结束文件传输 break; default: break; } pDlg->UpdateData(false); CAsyncSocket::OnReceive(nErrorCode); } void CTalkSocket::OnClose(int nErrorCode) { // TODO: Add your specialized code here and/or call the base class CChatApp *pApp=(CChatApp *) AfxGetApp(); CChatDlg *pDlg = (CChatDlg *) pApp->m_pMainWnd; pDlg->UpdateData(true); pDlg->m_editrec+="对方下线\\r\\n"; _tcpSocketClose(TalkSocket); pDlg->GetDlgItem(IDC_CUT_OFF)->EnableWindow(false); pDlg->GetDlgItem(IDC_SEND_MSS)->EnableWindow(false); pDlg->GetDlgItem(IDC_SEND_FILE)->EnableWindow(false); pDlg->UpdateData(false); CAsyncSocket::OnClose(nErrorCode); } (3).CFileSocket类:主要部分有FD_READ及 FD_WRITE触发的事件,OnSend是在Connect建立连接后或缓存为空,可以准备发送,而OnReceive在有消息到来的情况下处理消息,不过由于其它响应也比较重要,便也附上了 void CFileSocket::OnAccept(int nErrorCode) { // TODO: Add your specialized code here and/or call the base class _tcpSocketClose(FileSocket); if(!FileListen.Accept(FileSocket)) { AfxMessageBox("接收连接失败!"); return; } TotalRecv=0; TotalSend=0; FileSocket.AsyncSelect(FD_READ); CAsyncSocket::OnAccept(nErrorCode); } void CFileSocket::OnConnect(int nErrorCode) { // TODO: Add your specialized code here and/or call the base class TotalRecv=0; TotalSend=0; FileConn.AsyncSelect(FD_WRITE); CAsyncSocket::OnConnect(nErrorCode); } void CFileSocket::OnReceive(int nErrorCode) { // TODO: Add your specialized code here and/or call the base class FileSocket.AsyncSelect(FD_CLOSE); CChatApp *pApp=(CChatApp *) AfxGetApp(); CChatDlg *pDlg = (CChatDlg *) pApp->m_pMainWnd; pDlg->UpdateData(true); char recvbuf[4096]; CString Ctemp; CFile file; int dwRecv; int per;//文件进度 if(!file.Open(pDlg->m_editfile,CFile::modeWrite|CFile::shareDenyNone)) { } else { dwRecv=0; memset(recvbuf,0,4096); dwRecv=this->Receive(recvbuf,4096,0); if(dwRecv!=0) { file.SeekToEnd(); file.Write(recvbuf,dwRecv); TotalRecv+=dwRecv; per=(int)((float)TotalRecv/(float)pDlg->file_length*100); pDlg->m_prog.SetPos(per); pDlg->m_per.Format("%d",per); } if(TotalRecv==pDlg->file_length) { pDlg->m_editrec+="接收完毕……\\r\\n"; TotalRecv=0; pDlg->GetDlgItem(IDC_SEND_FILE)->EnableWindow(true); } pDlg->UpdateData(false); file.Close(); FileSocket.AsyncSelect(FD_READ|FD_CLOSE); } CAsyncSocket::OnReceive(nErrorCode); } void CFileSocket::OnSend(int nErrorCode) { // TODO: Add your specialized code here and/or call the base class this->AsyncSelect(FD_CLOSE); CChatApp *pApp=(CChatApp *) AfxGetApp(); CChatDlg *pDlg = (CChatDlg *) pApp->m_pMainWnd; pDlg->UpdateData(true); CString Ctemp; CFile file; char buf[4096]; UINT dwread; int per;//文件进度 if(!file.Open(pDlg->m_editfile,CFile::modeRead)) { } else { memset(buf,0,4096); file.Seek(TotalSend,CFile::begin); dwread=file.Read(buf,4096); if(dwread!=0) { TotalSend+=(long)dwread; FileConn.Send(buf,dwread,0); } per=(int)((float)TotalSend/(float)pDlg->file_length*100); pDlg->m_prog.SetPos(per); pDlg->m_per.Format("%d",per); if(dwread<4096) { pDlg->m_editrec+="发送完毕……\\r\\n"; file.Close(); TotalSend=0; pDlg->GetDlgItem(IDC_SEND_FILE)->EnableWindow(true); } else { file.Close(); this->AsyncSelect(FD_WRITE|FD_CLOSE); } pDlg->UpdateData(false); } CAsyncSocket::OnSend(nErrorCode); } void CFileSocket::OnClose(int nErrorCode) { // TODO: Add your specialized code here and/or call the base class this->Close(); TotalRecv=0; TotalSend=0; CAsyncSocket::OnClose(nErrorCode); } 七、调试 1.调试发现的问题:用虚拟机调试发现,虚拟机内看到宿机的IP跟宿机自己看到的IP是不同的(图表1 及图表 2) 图表 1 虚拟机 1,TRAVIS3 图表 2 宿机 TRAVIS 2.功能调试 a)对话请求:A方选择B方IP,并请求对话,如果请求成功,相应按钮激活,B方接收到对话请求后可以选择是否对话,同意则相应按钮激活,对话是的界面如(图表1 及图表 2) b)文件传输:A方建立对话之后,按发送文件按钮,弹出文件选择框,然后选择文件(如图表3),B方接收请求之后决定是否接收(图表4),同意后A方发送,B方接收(图表5) 图表 3 文件选择框 图表 4 文件传输请求 图表 5 宿机与虚拟机1传输文件 八、分析与总结 这次课程设计完成了所有的功能,所有功能调试通过,不过感觉外观有待美化。刚开始由于实验做过C/S模式的聊天室,因此对这个P2P模式十分轻视,但是真正做起来就十分困难,单单扫描在线对等方就困难重重,首先C/S模式的IP是确定的,而P2P的想要连接的IP是未知的,因此我上网搜索了许多才找到了搜索局域网在线用户的方法,然后我用Connect();的方法进行端口扫描,不过由于当时我用的是一个Socket进行Connect及发送登陆信息,发送完了再发送,这样效率十分的低,因此后期我用几个Socket同时connect并发送登陆信息,这样的效率增大了,也可靠了许多,然后是的难点是FileSocket,原先在OnRecive触发时没有屏蔽FD_READ,导致信息接收不完全,最后发现只要在开始屏蔽FD_READ,在接收完了在打开FD_READ就行了,因此也迎刃而解。 总结:这次课程设计陪伴着我度过了整个寒假。我学习到了许多,包括文件读写,Socket使用,窗口操作,字符处理等等,受益匪浅,感觉在学习网络,复习网络的过程中得到了许多乐趣,如挑战困难成功的乐趣,也学习了许多VC的应用,如BreakPoint看参数的变化 参考书目: [1]《电脑编程技巧与维护》杂志社,《Visual C++编程技巧典型案例解析——网络与通信级计算机安全与维护篇》,中国电力出版社,2005 自我评价:优下载本文