二 实现网络聊天程序的方法与实践 2.1.实现方法: (1)用WINSOCK编程实现: Socket原来是UNIX的Berkeley Software Distributiion版本中的一个程序接口。他类似于C的函数库。简单地说,WinSock是定义于视窗应用程序与网络之间的标准界面。程序设计者发现在发展视窗的网络应用软件时,只要支持WinSock界面的标准规格,就不再需要顾虑所使用的网卡硬件部分,因为WinSock所提供的动态连接程序库(WS2_32.DLL)会完成网络底层沟通的工作。进而使得程序设计这能设计出更多功能或更友善的网络软件。 (2)用MFC Socket编程实现: 用WINSOCK编程实现,使用的是Windows Socket API函数编程具有实用灵活的特点,但对于缺乏基本网络知识的初学者来说可能麻烦一点,鉴于此,Microsoft公司特地编写了连个基本的Socket类即CAsyncSocket和CSocket类。同时为了建化接收和发送数据部分,在MFC中环体的提供了CsoketetFile类(以及内置了一个CSocketWnd类)。 2.2.实现算法: 现在最通用和流行的网络编程模式就是C/S模式,服务器是能过提供某种或某些功能的程序或进程;客户机使用互相使用服务器进程的某种或某些功能的程序或进程。在C/S模式中,应首先启动服务器进程,然后客户机通过网络访问服务器资源,以完成相应的操作。 通常的网络通信有两种方式,这里主要是针对传输层而言,即:面向连接方式,如采用TCP协议;面向无连接方式,如UDP协议 。但有时为了编程方便,在使用UDP协议是采用面向连接的处理方式。 另外针对如何处理网络请求网络程序又可分为可交互式与同步式。交互式的特点是编程简单,不容易出错,但执行效率不高;并发式相对复杂,不容易控制,但执行效率较高。对于并发的实现有很多种方式,其中主要的两种是: 1.创建线程为客户机服务。 2.使用事件驱动方式。在一个线程内可以通过异步I/O操作方式,当有事件发生时,就触发相应的过程来处理。 (1)用WINSOCK编程算法: 1.面向连接方式(TCP协议) 服务器端: <1>首先使用WSAStartup函数来初始化网络环境。 <2>调用socket(AF_INET,SOCK_STREAM,0)函数来创建一个套接字。 <3>调用bind函数将本地地址与刚建立的套接字关联起来。 <4>调用listen函数监听发向该套接字的连接请求。 <5>客户端的连接请求放在连接请求队列里,服务器调用accept函数从连接请求队列 中取出第一个请求,创建一个为之服务的新的套接字,该套接字处理所有与该客户交互操作的信息。而服务器进程的监听套接字这时继续处理来自其他客户的连接请求,直到因队列空而等待新的连接请求的到来。 <6>调用closesocket()关闭监听套接字,释放套接字资源。 <7>调用WSACleanup函数释放相应资源。 客户端: <1>首先使用WSAStartup函数来初始化网络环境。 <2>调用socket(AF_INET,SOCK_STREAM,0)函数来创建一个套接字。 <3>调用connect函数连接远程服务器,以请求服务。 <4>服务器相应连接请求后,此时客户端进程开始与服务器的交互操作,直到请求结束 为止。 <5>调用closesocket()关闭监听套接字,释放套接字资源。 <6>调用WSACleanup函数释放相应资源。 2.面向无连接方式(UDP协议)
否
是
面向无连接的服务器结构 (2)用MFC Socket编程算法:
1.CAsynvSocket类 这个类封装了Windows Socket API函数。CAsynvSocket式面向那些熟悉底层网络编程的程序员,使他们既可以直接获得使用Windows Socket API函数所带来的灵活性,又可以获得网络事件触发回调函数的便利性。CAsynvSocket类把与Socket相关的Windows消息转换成了回调函数。 服务器端: <1>首先使用CAsynvSocket类创建一个对象,可以使用栈方式(直接声明一个类对象实例),也可以使用堆方式(使用new,用完后显式的用delete删除该对象)。 <2>调用CAsynvSocket类的create方法来创建一个套接字,create方法支持缺省参数。 <3>调用CAsynvSocket类的listen方法来监听来自客户端的连接请求。 <4>一旦有客户端的连接进来,则调用CAsynvSocket类的accept方法。调用之前,应该先构造一个cSocket类对象。 <5>对于刚生成的套接字使用CAsynvSocket封装的API函数进行通信。
<6>一旦完成通讯,则要清除该对象,对于创建在堆栈上的对象,当函数返回时自动释放该对象,对于使用new操作符生成的对象,则使用delete操作符释放。不显式的调用close方法,因为该类的构造函数自动调用close方法。 <7>当服务器监听得套接字关闭时,按6的方法进行处理。 客户端: <1>首先使用CAsynvSocket类创建一个对象,可以使用栈方式(直接声明一个类对象实例),也可以使用堆方式(使用new,用完后显式的用delete删除该对象)。 <2>调用CAsynvSocket类的create方法来创建一个套接字,create方法支持缺省参数。 <3>调用CAsynvSocket类的connect方法来连接服务器。 <4>一旦连接完成,则调用CAsynvSocket封装的API函数进行通信。 <5>一旦完成通讯,则要清除该对象,对于创建在堆栈上的对象,当函数返回时自动释放该对象,对于使用new操作符生成的对象,则使用delete操作符释放。不显式的调用close方法,因为该类的构造函数自动调用close方法。 服务器程序图式结构:
是
客户机程序图式结构:
void CServerDoc::CloseSocket(CServiceSocket *pSocket) { pSocket->Close();
POSITION pos,temp; for(pos = m_connectionList.GetHeadPosition(); pos != NULL;) { temp = pos; CServiceSocket* pSock = (CServiceSocket *)m_connectionList.GetNext(pos); if (pSock == pSocket){ m_connectionList.RemoveAt(temp); break; } }
delete pSocket; pSocket = NULL; } 2.CSocket类 CSocket类是从CAsynvSocket类派生而来,继承了CAsynvSocket类封装的标准的Windows Socket API函数,同时与CAsynvSocket类相比,CSocket类是Windows Socket API函数的更深层次的抽象。CSocket类通常与CSocketFile类和CArchive类协同处理发送和接收数据。 CSocket对象同样也提供阻塞操作功能,该功能是Cachive同步操作的必要条件。阻塞操作函数如:Receive,send,recievefrom,sendto以及accept,在CSocket中不会返回WSAEWOULDBLOCK错误,相反,这些函数一直等到操作完成。 服务器端: <1>首先使用CSocket类创建一个对象,可以使用栈方式(直接声明一个类对象实例),也可以使用堆方式(使用new,用完后显式的用delete删除该对象)。 <2>调用CSocket类的create方法来创建一个套接字,create方法支持缺省参数。 <3>调用CSocket类的listen方法来监听来自客户端的连接请求。 <4>一旦有客户端的连接进来,则调用CSocket类的accept方法。调用之前,应该先构造一个cSocket类对象。 <5>创建一个CSocketFile对象,并将其与刚创建的处理客户请求的CSocket对象关联起来。 <6>创建CArchive对象用于套接字对象的接收和发送数据,并把他和CSocketFile对象关联起来。注意,应为发送和接收数据部分个创建一个CArchive对象,同时CArchive不能处理SOCK_DGRAM类型的数据。 <7>使用CArchive对象进行发送数据和接收数据。 <8>一旦完成通讯,则要清除该对象,对于创建在堆栈上的对象,当函数返回时自动释放该对象,对于使用new操作符生成的对象,则使用delete操作符释放。不显式的调用close方法,因为该类的构造函数自动调用close方法。 <9>当服务器监听得套接字关闭时,按6的方法进行处理。 客户端: <1>首先使用CSocket类创建一个对象,可以使用栈方式(直接声明一个类对象实例),也可以使用堆方式(使用new,用完后显式的用delete删除该对象)。 <2>调用CSocket类的create方法来创建一个套接字,create方法支持缺省参数。 <3>调用CSocket类的connect方法来连接服务器。 <4>创建一个CSocketFile对象,并将其与刚创建的处理客户请求的CSocket对象关联起来。 <5>创建CArchive对象用于套接字对象的接收和发送数据,并把他和CSocketFile对象关联起来。 <6>使用CArchive对象进行发送数据和接收数据。 <7>一旦完成通讯,则要清除该对象,对于创建在堆栈上的对象,当函数返回时自动释放该对象,对于使用new操作符生成的对象,则使用delete操作符释放。不显式的调用close方法,因为该类的构造函数自动调用close方法。 2.3服务器的四种设计方式 1.面向连接的并发式服务器:这种服务器采用面向连接协议,并发处理多个用户请求。这一般用于客户访问量多,对客户请求的响应时间要求较高的,且要求提供稳定可靠的服务。 2.面向连接的交互式服务器:这种服务器采用面向连接协议;以提供稳定,可靠的服务给客户,同时,服务器是逐个处理来到的客户请求。这种方案适用于客户访问量不多,对客户请求的响应时间要求不高,但同时对数据的稳定性和可靠性要求较高的场合。 3.面向无连接的并发式服务器:这种服务器采用面向无连接协议;可同时服务多个客户的连接请求。这种方案适用于通信频繁,可靠性和稳定性要求不高的场合。 4.面向连接的交互式服务器:这种服务器采用面向无连接协议;服务器逐个处理客户的通信请求。本方案适用于对可靠性和稳定性要求不高,通信并不频繁的场合。
2.4 实践过程 2.4.1消息结构 from :消息的发出者 to:消息的接收者 type:消息类型 shortmessage:消息内容 2.4.2消息类型取值 用户登陆 1 用户退出 2 正常通信 3 发给所有人 4 用户请求再现人员列表 5 请求指定用户IP地址 6 服务器关机 9 2.4.3实现过程 服务器端代码: <1>首先使用CAsynvSocket类创建一个对象,可以使用栈方式(直接声明一个类对象实例),也可以使用堆方式(使用new,用完后显式的用delete删除该对象)。 <2>调用CAsynvSocket类的create方法来创建一个套接字,create方法支持缺省参数。 m_pSocket->Create(m_nPort) //创建
<3>调用CAsynvSocket类的listen方法来监听来自客户端的连接请求。 m_pSocket->Listen()//监听
<4>一旦有客户端的连接进来,则调用CAsynvSocket类的accept方法。调用之前,应该先构造一个cSocket类对象。 void CServerDoc::ProcessAccept()
if (m_pSocket->Accept(*pSocket)) { CMessage *pMsg; pMsg = new CMessage(); pMsg->ShortMessage = "Come in"; pMsg->To = ""; pMsg->From = ""; pMsg->Type = 1;
POSITION posname;
for( posname=m_connectionList.GetHeadPosition();posname;) { pSock = (CServiceSocket *)m_connectionList.GetNext(posname); if(pSock->Name != pMsg->From ) { SendMsg(pSock, pMsg); } } delete pMsg; */ } else delete pSocket;
} <5>对于刚生成的套接字使用CAsynvSocket封装的API函数进行通信。 void CServerDoc::SendMsg(CServiceSocket *pSocket, CMessage *pMessage) { pSocket->SendMsg(pMessage); } CMessage* CServerDoc::ReadMsg(CServiceSocket *pSocket) { pSocket->ReceiveMsg(m_pShortMessage);
return m_pShortMessage;
} void CServiceSocket::ReceiveMsg(CMessage *pMsg) { pMsg->Serialize(*m_pArchiveIn); }
<6>一旦完成通讯,则要清除该对象,对于创建在堆栈上的对象,当函数返回时自动释放该对象,对于使用new操作符生成的对象,则使用delete操作符释放。不显式的调用close方法,因为该类的构造函数自动调用close方法。 void CServerDoc::CloseSocket(CServiceSocket *pSocket) { pSocket->Close();
POSITION pos,temp; for(pos = m_connectionList.GetHeadPosition(); pos != NULL;) { temp = pos; CServiceSocket* pSock = (CServiceSocket *)m_connectionList.GetNext(pos); if (pSock == pSocket){ m_connectionList.RemoveAt(temp); break; } }
delete pSocket; pSocket = NULL; } void CServerDoc::DeleteContents() { delete m_pSocket; m_pSocket = NULL;
CString temp; temp="Server has been shutdown!"; CMessage* pMsg = new CMessage; while(!m_connectionList.IsEmpty()) { CServiceSocket*pSocket=(CServiceSocket )m_connectionList.RemoveHead(); if(pSocket==NULL ) continue; if (pMsg == NULL) continue; pMsg->From = pSocket->Name ; pMsg->ShortMessage = _TEXT("Server has been shutdown!"); pMsg->To = _TEXT("All"); pMsg->Type = 9;
SendMsg(pSocket, pMsg); if(!pSocket->IsAborted()) { pSocket->ShutDown(); BYTE Buffer[50]; while (pSocket->Receive(Buffer,50) > 0); delete pSocket; } } delete pMsg; CDocument::DeleteContents(); }
<7>当服务器监听得套接字关闭时,按5的方法进行处理。 2.4.4 系统中定义的重要类 (1)CMessage类: 定义四个变量:CString shortmessage;CString from;CString to;ing type; 在CMessage类中声明的CMessage()函数,对变量附初值 在CMessage类中声明Serialize()函数,reset()函数 void reset();//将四个变量变为初始值。 void Serialize(CArchive& ar); 在其函数体内判断ar.IsStoring(),为真则 ar<<type;ar<<shortmessage;ar<<from;ar<<to; 否则,ar>>type;ar>>shortmessage;ar>>from;ar>>to; (2)CAccptSocket类: 该类事件听套接字处理类似以接收用户的连接请求,并创建服务于用户请求的套接字类。 CAccptSocket* m_pDoc; 创建一个接收按钮onaccept CSocket:OnAccept (nErrorcode); m_pDoc->ProcessAccept(); 重载= CAccptSocket& operator=(const CAccptSocket sSrc);//为私有变量 (3)CServiceSocket类: 此类由监听套接字创建以实现与客户端进行通信的类 定义三个成员变量: CSocketFile* m_pFile; //与该套接字关联的CSocketFile类 CArchive* m_pArchiveIn; //与CSocketFile类相关联的接收数据文档的变量 CArchive* m_pArchiveOut;//与CSocketFile类相关联的发送数据文档的变量 (4)CServerDoc类: 该类负责处理数据,套接字类负责处理收发数据,而文档类负责处理数据,下面列出几个数据收发和显示有关的函数。 m_nPort = 6666;// 通信端口设为6666 if(m_pSocket != NULL)//如果m_pSocket,m_pShortMessage不为空 delete m_pSocket; //则删除 m_pSocket = NULL; if(m_pShortMessage != NULL) delete m_pShortMessage; m_pSocket = new CAcceptSocket(this); if (m_pSocket->Create(m_nPort))//创建 if (m_pSocket->Listen())//监听 return TRUE;//成功则返回TRUE; 客户端代码: 客户端应用名为client,它是一个对话框程序。 客户端: <1>首先使用CAsynvSocket类创建一个对象,可以使用栈方式(直接声明一个类对象实例),也可以使用堆方式(使用new,用完后显式的用delete删除该对象)。 <2>调用CAsynvSocket类的create方法来创建一个套接字,create方法支持缺省参数。 int CUserDlg::OnCreate(LPCREATESTRUCT lpCreateStruct) { m_bOnline = FALSE; m_pMsg = new CMessage();
CRect rect; GetClientRect(&rect); m_ListCtrl = new CUserList(); if (!m_ListCtrl->Create(WS_CHILD | WS_VISIBLE | LVS_REPORT, rect, this, IDC_LISTBOX)) { TRACE0("Failed to create view for CMyBarLeft\n"); return -1; }
m_ListCtrl->ModifyStyleEx(0, WS_EX_CLIENTEDGE); AddExStyle(LVS_EX_FULLROWSELECT | LVS_OWNERDRAWFIXED); int i; LV_COLUMN lvc;
lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; strTemp[2] = {"名称", "IP地址"}; CString strTemp[2] = {"", ""}; int size[2] = {140,40}; for(i = 0; i < 2; i++) { lvc.iSubItem = i; lvc.pszText = (char*)(LPCTSTR)strTemp[i]; lvc.cx = size[i]; lvc.fmt = LVCFMT_LEFT; m_ListCtrl->InsertColumn(i, &lvc); }
pnid.cbSize = sizeof(NOTIFYICONDATA); pnid.hIcon = LoadIcon(AfxGetApp()->m_hInstance,MAKEINTRESOURCE(IDR_MAINFRAME)); pnid.hWnd = this->m_hWnd ; sprintf(pnid.szTip, "聊天程序\n"); pnid.uCallbackMessage = WM_SYSTRAY; pnid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; pnid.uID = ID_SYSTRAY; Shell_NotifyIcon(NIM_ADD, &pnid);
if(ConnectToServer()) { m_bOnline = TRUE; m_pMsg->From = Name; m_pMsg->To = "All"; m_pMsg->Type = 5; m_pMsg->ShortMessage = "Hello"; m_pSocket->SendMsg(m_pMsg); } else m_bOnline = FALSE;
CString m_strTemp; this->GetWindowText(m_strTemp); m_strTemp += "-"; m_strTemp += Name; this->SetWindowText(m_strTemp); return 0; }
<3>调用CAsynvSocket类的connect方法来连接服务器。 BOOL CUserDlg::ConnectToServer() { if (m_bOnline) return TRUE;
m_pSocket = new CServiceSocket(this);
if (m_pSocket == NULL) { AfxMessageBox("Couldn't allocate memory for service socket!"); return FALSE; }
if (!m_pSocket->Create()) { delete m_pSocket; m_pSocket = NULL; AfxMessageBox("Create Socket Error!"); return FALSE; } m_pSocket->Name = Name; while (!m_pSocket->Connect(m_strAddress, m_nPort)) { if (AfxMessageBox("Retry again?",MB_YESNO) == IDNO) { delete m_pSocket; m_pSocket = NULL; return FALSE; } }
m_pSocket->Init();
m_pMsg->From = Name; m_pMsg->To = "All"; m_pMsg->Type = 1; m_pMsg->ShortMessage = "Come in...."; m_pSocket->SendMsg(m_pMsg);
m_bOnline = TRUE;
return TRUE;
}
<4>一旦连接完成,则调用CAsynvSocket封装的API函数进行通信。 void CUserDlg::SendMsg() { CMsgDlg m_dlgMsg; m_dlgMsg.m_bSend = TRUE; m_dlgMsg.m_strFrom = Name; m_dlgMsg.m_strTo = m_pMsg->To; if(m_dlgMsg.DoModal() == IDOK) { m_pMsg->ShortMessage = m_dlgMsg.m_strMsg ; m_pMsg->Type = 3; m_pSocket->SendMsg(m_pMsg); } } void CUserDlg::ReceiveMsg() { CString m_strTemp; int m_nPosition ; int m_nPrePos ;
m_pMsg->Serialize(*m_pSocket->m_pArchiveIn); switch(m_pMsg->Type) { case 1: m_ListCtrl->AddItem(1, m_pMsg->From.GetBuffer(0), ""); break; case 2: m_ListCtrl->Remove(m_pMsg->From.GetBuffer(0) ); break; case 3: DisplayMsg(); break; case 4: DisplayMsg(); break; case 5: m_ListCtrl->DeleteAllItems(); m_nPosition = -1; m_nPrePos = 0; m_nPosition = m_pMsg->ShortMessage.Find("##",0); while(m_nPosition != -1) { m_strTemp = m_pMsg->ShortMessage.Mid(m_nPrePos, m_nPosition - m_nPrePos); if(m_strTemp.Compare(Name)) { m_ListCtrl->AddItem(1, m_strTemp.GetBuffer(0), ""); } m_nPrePos = m_nPosition + 2; m_nPosition = m_pMsg->ShortMessage.Find("##",m_nPrePos);
<5>一旦完成通讯,则要清除该对象,对于创建在堆栈上的对象,当函数返回时自动释放该对象,对于使用new操作符生成的对象,则使用delete操作符释放。不显式的调用close方法,因为该类的构造函数自动调用close方法。 void CUserDlg::OnClose() { if(!m_pSocket) { delete m_pSocket; m_pSocket = NULL; } if (m_pMsg != NULL) delete m_pMsg; CDialog::OnClose(); }
<7>使用CArchive对象进行发送数据和接收数据。 void CUserDlg::DisplayMsg() { CMsgDlg m_dlgMsg; CString m_strTemp;
if(m_pMsg->From == "") return; if(m_pMsg->From == Name) return; m_strTemp = m_pMsg->From; m_dlgMsg.m_bSend = FALSE; if(m_pMsg->To == "") m_dlgMsg.m_strTo = Name; else if (m_pMsg->To == "All") m_dlgMsg.m_strTo = Name; else m_dlgMsg.m_strTo = m_pMsg->To;
m_dlgMsg.m_strMsg = m_pMsg->ShortMessage ; m_dlgMsg.m_strFrom = m_pMsg->From ;
if(m_dlgMsg.DoModal() == IDOK) { m_pMsg->From = Name; //m_pMsg->To = m_dlgMsg.m_strTo ; m_pMsg->To = m_strTemp; m_pMsg->ShortMessage = m_dlgMsg.m_strMsg ; m_pMsg->Type = 3; m_pSocket->SendMsg(m_pMsg); }
} <8>一旦完成通讯,则要清除该对象,对于创建在堆栈上的对象,当函数返回时自动释放该对象,对于使用new操作符生成的对象,则使用delete操作符释放。不显式的调用close方法,因为该类的构造函数自动调用close方法。 <9>当服务器监听得套接字关闭时,按6的方法进行处理。 系统中定义的类; CMessage类:从CObject类中派生而来,目的是为了使用CObject类的序列化函数,以便将数据发从到与服务套接字相关联的文档中。 Caccept类:是一个监听套接字类,用于监听用户的连接请求。一旦有用户连接请求,这调用其成员函数来Accept创建一个服务套接字类 (3)CServiceSocket类:是一个服务于用户请求的套接字类该套接字类处理所有发出和接收数据,在类中定义三个成员变量 CSocketFile* m_pFile CArchive*m_pArchiveIn CArchive*m_pArchiveOut 第一个变量是与该套接字类相关联的CSocketFile类,第二个变量是与CsocketFile相关联的接收数据文档,第三个变量是与CsocketFile相关联的发送数据文档。该套接字对象与上述便量结合实现发送和接收数据功能。

|