基本上windows平台下的几种IO模型都实现了一遍,还有两个没有实现,但是它们一个需要基于windows消息,一个和重叠IO中的事件通知模型比较类似,并且不能实现真正的异步,所以就不列出来了。
这一篇介绍如何实现一个聊天室。前面介绍的几种模型中除了基本的socket模型和阻塞版本的模型之外都可以用来实现聊天室,因为这两个模型还不能实现支持多个客户端。对于其它的几个模型,我们需要做的只是在原来模型的基本上维护一个socket连接的列表即可。这里使用完成端口来实现聊天室。使用其它的模型基本上也是这个思路,之所以使用这个是因为它不像多线程那样耗费资源,也不像selelct和重叠IO中基于事件通知的模型那样有连接限制,相比与重叠IO中的完成例程模型,不需要考虑负载均衡的问题。所以就是它了。
先插入几张程序运行的截图。
下图左侧是服务器端程序的启动界面,右图是客户端启动的界面。
对服务器端而言:
对客户端而言:
点击了开启服务器后,服务器才会启动,此时在下面的“信息”栏中会显示“创建启动线程成功!”,“服务器端已启动......”等字样。
服务器端启动以后客户端就可以连接服务器了,现在“昵称”后面的编辑框中输入一个昵称,接着点击“连接服务器”按钮即可。
客户端会提示“连接服务器成功!”,“allanxia成功登陆!”这两行文字。
服务器端会提示“客户端连接:10.11.163.113:707”,“allanxia 登陆成功!”,其中第一条消息指示的是连接的客户端的ip地址和端口号,第二条信息指示的是客户端的昵称。
如果又有一个客户端连接,如下图:
会发生下面的情况:
服务端会受到Bill Gates的登陆信息;
已经连接的客户端(这里只有allanxia)也会收到他的登陆信息;
自身也会收到登陆消息。
接着再连接一个客户端:和上面同样的原理。
现在allanxia这个客户端发送一条消息“大家好,我是allanxia”,所有的客户端就都收到这个消息了。
现在大家就可以畅所欲言了。这就是基本的模型了,如果有客户端退出,服务器端会收到通知,但是客户端就不会收到通知了,这就是聊天室的基本模型了。
这里我使用的是完成端口的模型,原理上讲其它几个支持多客户端的模型都可以用来实现这个聊天室。
这里我使用MFC+vs2008开发。
首先看客户端的代码,因为客户端的代码比较简单,但是和控制台的相比也要做一些更改,之前没想这么多,结果调试和好一会儿。
根据上面程序运行的截图可以发现,点击“连接服务器”这个按钮后,客户端会和服务器连接。所以之前在控制台中创建客户端socket的代码需要放到这个按钮的响应函数中来。
并且我们可能会在其它响应函数里操作这个代码,所以这个socket需要定义成类的成员变量,这个对话框类叫CChatRoomClientDlg。
socket连接创建后,就需要将昵称发送给服务器端了,这里使用send将消息发送过去就可以,和之前的控制台程序相同。
下面就要特别注意了!!!
客户端的接收流程和之前控制台的有很大的区别,之前控制台的是发送一条数据,再接收一条数据。但是在聊天室中发送数据是我们可以控制的,但是接收数据客户端就不了解的,它只需要从这个socket连接中不断接收数据并显示在客户端的当前信息栏中就可以了。所以这里我们需要启动一个新的线程,这个线程专门用来负责从这个socket连接中接收数据并显示。否则将循环放在主窗口函数中会出现阻塞。
在“发送按钮“的响应函数中填写向这个socket连接发送数据的代码就可以了,这个比较简单,因为一次只会发送一条数据,不会向接收数据这样会出现循环,所以需要使用一个新的线程,不然会出现阻塞。
基本的流程如下图:
下面是一些关键的代码:
点击”连接服务器“和”发送“按钮的响应函数:
//点击连接服务器时的处理 void CChatRoomClientDlg::OnBnClickedConn() { UpdateData(true); CString nickNameStr=getNickName(); if (nickNameStr.IsEmpty()||nickNameStr.GetLength()==0) { AfxMessageBox("请输入一个昵称!"); } else//输入昵称不为空时才和服务端相连 { CString info; WSADATA wsaData; int err; //1.首先执行初始化Windows Socket库 err=WSAStartup(MAKEWORD(1,1),&wsaData); if (err!=0) { info.Format("初始化socket失败!\n"); appendString(info); return ; } //2.创建Socket sockClient=socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in addrServer; DWORD dwip; m_ip.GetAddress(dwip); // addrServer.sin_addr.s_addr=inet_addr(IP_ADDRESS); addrServer.sin_addr.s_addr=htonl(dwip); addrServer.sin_family=AF_INET; addrServer.sin_port=htons(m_port); //3.连接Socket,第一个参数为客户端socket,第二个参数为服务器端地址 err=connect(sockClient,(struct sockaddr *)&addrServer,sizeof(addrServer)); if (err!=0) { info.Format("连接服务器失败!\n"); appendString(info); return ; }else { info.Format("连接服务器成功!\n"); appendString(info); } CString nick=getNickName(); char * sendBuff=nick.GetBuffer(nick.GetLength()); nick.ReleaseBuffer(); char recvBuf[MAX_PATH]; send(sockClient,sendBuff,strlen(sendBuff)+1,0); //第三个参数加上1是为了将字符串结束符'\0'也发送过去 //创建接收线程 CreateThread(NULL,0,receiveThread,this,0,NULL); } } //发送数据 void CChatRoomClientDlg::OnBnClickedOk() { UpdateData(true); CString output=m_output; output=output+"\n"; char * sendBuff=output.GetBuffer(output.GetLength()); output.ReleaseBuffer(); send(sockClient,sendBuff,strlen(sendBuff)+1,0); //第三个参数加上1是为了将字符串结束符'\0'也发送过去 m_output=""; UpdateData(false); }
//客户端的接收线程,持续从socket连接中接受数据 DWORD WINAPI receiveThread(LPVOID lpParam) { //将对话框指针作为参数传递给这个变量 CChatRoomClientDlg * chatRoom=(CChatRoomClientDlg*)lpParam; char recvBuf[MAX_PATH]; CString info; while(true) { memset(recvBuf,0,MAX_PATH); recv(chatRoom->sockClient,recvBuf,MAX_PATH,0); info.Format("%s\n",recvBuf); chatRoom->appendString(info); } delete[] recvBuf; }
服务端也是在原来控制台程序的基本上进行的更改,主要进行了下面的更改:
//点击连接服务器的响应函数,启动一个新的线程 void CChatRoomServerDlg::OnBnClickedBegin() { UpdateData(true); HANDLE hThread=CreateThread(NULL,0,startThread,this,0,NULL); if (hThread==NULL) { appendString(CString(_T("创建启动线程失败!\n"))); } else { appendString(CString(_T("创建启动线程成功!\n"))); } CloseHandle(hThread); }
struct PerSocketData { WSAOVERLAPPED overlap;//每一个socket连接需要关联一个WSAOVERLAPPED对象 WSABUF buffer;//与WSAOVERLAPPED对象绑定的缓冲区 char szMessage[MSGSIZE];//初始化buffer的缓冲区 DWORD NumberOfBytesRecvd;//指定接收到的字符的数目 DWORD flags; }; struct TransferData { CChatRoomServerDlg * _chatRoom;//对话框指针 HANDLE _completionPort;//完成端口 }; //使用这个工作线程来通过重叠IO的方式与客户端通信 DWORD WINAPI workThread(LPVOID lpParam) { TransferData * transData=(TransferData *)lpParam; HANDLE completionPort=transData->_completionPort; CChatRoomServerDlg * chatRoom=transData->_chatRoom; DWORD dwBytesTransfered; SOCKET clientSocket; PerSocketData * lpIOdata=NULL; while(true) { GetQueuedCompletionStatus( completionPort, &dwBytesTransfered, (LPDWORD)&clientSocket, (LPOVERLAPPED*)&lpIOdata, INFINITE); if (dwBytesTransfered==0xFFFFFFFF) { return 0; } if (dwBytesTransfered==0) { string message=chatRoom->socketMap[clientSocket]; message=message+string(" 退出聊天室!\n"); chatRoom->appendString(CString(message.c_str())); closesocket(clientSocket); HeapFree(GetProcessHeap(),0,lpIOdata); } else { //如果这个socket连接已经被加入map中,此时传送的是信息 if (chatRoom->socketMap.count(clientSocket)) { string message=lpIOdata->szMessage; map<SOCKET,string>::iterator iter; string nickName=chatRoom->socketMap[clientSocket]; string info=nickName+string(" 发送了一条消息:")+string(message); chatRoom->appendString(CString(info.c_str())); message=nickName+string(":")+string("\n")+message; //遍历每一个socket连接并将信息发送出去 for (iter=chatRoom->socketMap.begin();iter!=chatRoom->socketMap.end();iter++) { send(iter->first,message.c_str(),message.size()+1,0);//多发送一个字符,将字符串结束符也发送过去 } } else//第一次连接,此时收到的是用户昵称 { //将这个新的socket连接和昵称放入集合中 string message=lpIOdata->szMessage;//获取昵称 chatRoom->socketMap[clientSocket]=message;//将昵称和socket绑定到一起 //显示登陆信息在服务器端 string login=chatRoom->socketMap[clientSocket]; login=login+string(" 登陆成功!\n"); chatRoom->appendString(CString(login.c_str())); //组装好发送给客户端的数据 map<SOCKET,string>::iterator iter; string info=" 成功登陆!"; message=message+info; //遍历每一个socket连接并将信息发送出去 for (iter=chatRoom->socketMap.begin();iter!=chatRoom->socketMap.end();iter++) { send(iter->first,message.c_str(),message.size()+1,0);//多发送一个字符,将字符串结束符也发送过去 } } // cout<<lpIOdata->szMessage<<endl; // send(clientSocket,lpIOdata->szMessage,dwBytesTransfered+1,0);//多发送一个字符,将字符串结束符也发送过去 memset(lpIOdata,0,sizeof(PerSocketData)); lpIOdata->buffer.len=MSGSIZE; lpIOdata->buffer.buf=lpIOdata->szMessage; WSARecv(clientSocket, &lpIOdata->buffer, 1, &lpIOdata->NumberOfBytesRecvd, &lpIOdata->flags, &lpIOdata->overlap, NULL); } } return 0; } DWORD WINAPI startThread(LPVOID lpParam) { //将对话框指针作为参数传递给这个成员变量 CChatRoomServerDlg * chatRoom=(CChatRoomServerDlg*)lpParam; WSADATA wsaData; int err; CString str; //1.加载套接字库 err=WSAStartup(MAKEWORD(1,1),&wsaData); if (err!=0) { str.Format("Init Windows Socket Failed::%d\n",GetLastError()); chatRoom->appendString(str); return 0; } //下面执行一些使用完成端口需要进行的步骤 //创建一个完成端口 HANDLE completionPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0); PerSocketData * sockData; SYSTEM_INFO systeminfo; GetSystemInfo(&systeminfo); DWORD dwThreadId; TransferData trans; trans._chatRoom=chatRoom; trans._completionPort=completionPort; for (int i=0;i<systeminfo.dwNumberOfProcessors;i++) { CreateThread(NULL,0,workThread,&trans,0,&dwThreadId); } //2.创建socket //套接字描述符,SOCKET实际上是unsigned int SOCKET serverSocket; serverSocket=socket(AF_INET,SOCK_STREAM,0); if (serverSocket==INVALID_SOCKET) { str.Format("Create Socket Failed::%d\n",GetLastError()); chatRoom->appendString(str); return 0; } //服务器端的地址和端口号,使用本地ip地址和用户指定的端口号 struct sockaddr_in serverAddr,clientAdd; DWORD dwip; chatRoom->m_ip.GetAddress(dwip); //serverAddr.sin_addr.s_addr=inet_addr(IP_ADDRESS); serverAddr.sin_addr.s_addr=htonl(dwip); serverAddr.sin_family=AF_INET; serverAddr.sin_port=htons(chatRoom->m_port); //3.绑定Socket,将Socket与某个协议的某个地址绑定 err=bind(serverSocket,(struct sockaddr*)&serverAddr,sizeof(serverAddr)); if (err!=0) { str.Format("Bind Socket Failed::%d\n",GetLastError()); chatRoom->appendString(str); return 0; } //4.监听,将套接字由默认的主动套接字转换成被动套接字 err=listen(serverSocket,10); if (err!=0) { str.Format("listen Socket Failed::%d\n",GetLastError()); chatRoom->appendString(str); return 0; } chatRoom->appendString("服务器端已启动......\n"); int addrLen=sizeof(clientAdd); SOCKET sockConn; while(true) { //5.接收请求,当收到请求后,会将客户端的信息存入clientAdd这个结构体中,并返回描述这个TCP连接的Socket sockConn=accept(serverSocket,(struct sockaddr*)&clientAdd,&addrLen); if (sockConn==INVALID_SOCKET) { str.Format("Accpet Failed::%d\n",GetLastError()); chatRoom->appendString(str); return 0; } str.Format("客户端连接:%s : %d\n",inet_ntoa(clientAdd.sin_addr),clientAdd.sin_port); chatRoom->appendString(str); //将之前的第6步替换成了下面的操作 CreateIoCompletionPort((HANDLE)sockConn,completionPort,(DWORD)sockConn,0); sockData=(PerSocketData*)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PerSocketData)); sockData->buffer.len=MSGSIZE; sockData->buffer.buf=sockData->szMessage; WSARecv( sockConn, &sockData->buffer, 1, &sockData->NumberOfBytesRecvd, &sockData->flags, &sockData->overlap, NULL); } PostQueuedCompletionStatus(completionPort,0xFFFFFFFF,0,NULL); CloseHandle(completionPort); closesocket(serverSocket); //7.清理Windows Socket库 WSACleanup(); }