socket通信之九:使用完成端口实现的一个聊天室

来源:互联网 发布:二战江河级护卫舰数据 编辑:程序博客网 时间:2024/06/05 19:04

基本上windows平台下的几种IO模型都实现了一遍,还有两个没有实现,但是它们一个需要基于windows消息,一个和重叠IO中的事件通知模型比较类似,并且不能实现真正的异步,所以就不列出来了。


这一篇介绍如何实现一个聊天室。前面介绍的几种模型中除了基本的socket模型和阻塞版本的模型之外都可以用来实现聊天室,因为这两个模型还不能实现支持多个客户端。对于其它的几个模型,我们需要做的只是在原来模型的基本上维护一个socket连接的列表即可。这里使用完成端口来实现聊天室。使用其它的模型基本上也是这个思路,之所以使用这个是因为它不像多线程那样耗费资源,也不像selelct和重叠IO中基于事件通知的模型那样有连接限制,相比与重叠IO中的完成例程模型,不需要考虑负载均衡的问题。所以就是它了。


1.程序运行流程


先插入几张程序运行的截图。


1.1.启动服务器端的程序和客户端的程序


下图左侧是服务器端程序的启动界面,右图是客户端启动的界面。

对服务器端而言:

  1. 第一行代表的是本机的ip地址,也就是服务器端的ip地址,客户端中服务器ip地址这一栏需要填上这个地址;
  2. 端口号表示服务器端绑定的端口号,这个端口号可以自己指定。客户端中对应的端口号需要填上服务器端绑定的这个端口号,这样客户端才可以连上上服务器端。

对客户端而言:

  1. 服务器ip地址填上服务器端指定的ip地址,端口填上服务器端绑定的端口号;
  2. 如果客户端和服务器端在同一台机器上启动,此时的客户端中填写的服务器ip地址是正确的,否则需要根据服务器端的ip地址更改;
  3. 客户端需要指定一个昵称,用来标示不同的客户端,这个标示不能为空;
  4. 这里我允许有重复的昵称,如果不允许,只需要加上一定的判断就可以了。




1.2.点击服务器端的开启服务器


点击了开启服务器后,服务器才会启动,此时在下面的“信息”栏中会显示“创建启动线程成功!”,“服务器端已启动......”等字样。




1.3.客户端连接服务器


服务器端启动以后客户端就可以连接服务器了,现在“昵称”后面的编辑框中输入一个昵称,接着点击“连接服务器”按钮即可。

客户端会提示“连接服务器成功!”,“allanxia成功登陆!”这两行文字。

服务器端会提示“客户端连接:10.11.163.113:707”,“allanxia 登陆成功!”,其中第一条消息指示的是连接的客户端的ip地址和端口号,第二条信息指示的是客户端的昵称。



如果又有一个客户端连接,如下图:

会发生下面的情况:

服务端会受到Bill Gates的登陆信息;

已经连接的客户端(这里只有allanxia)也会收到他的登陆信息;

自身也会收到登陆消息。



接着再连接一个客户端:和上面同样的原理。




1.4.客户端通信


现在allanxia这个客户端发送一条消息“大家好,我是allanxia”,所有的客户端就都收到这个消息了。



现在大家就可以畅所欲言了。这就是基本的模型了,如果有客户端退出,服务器端会收到通知,但是客户端就不会收到通知了,这就是聊天室的基本模型了。




2.程序分析


这里我使用的是完成端口的模型,原理上讲其它几个支持多客户端的模型都可以用来实现这个聊天室。


这里我使用MFC+vs2008开发。


2.1客户端分析


首先看客户端的代码,因为客户端的代码比较简单,但是和控制台的相比也要做一些更改,之前没想这么多,结果调试和好一会儿。


根据上面程序运行的截图可以发现,点击“连接服务器”这个按钮后,客户端会和服务器连接。所以之前在控制台中创建客户端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.创建SocketsockClient=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;}


2.2服务端分析

服务端也是在原来控制台程序的基本上进行的更改,主要进行了下面的更改:

  1. 此时需要维护一个socket的集合;
  2. 服务器每次收到客户端的消息后,需要遍历这个socket集合并将信息发送给连接的每一个socket

并且由于服务器端需要持续监听有没有客户端连接服务器,所以还需要一个新的线程startThread,点击”启动服务器“后启动这个线程。在这个线程中执行创建socket,绑定,监听,然后在一个while循环中接收客户端的连接,并且完成与IO完成端口相关的一些操作。

这里由于我加入了一个昵称,所以需要维护socket连接和昵称之间的对应关系,这里使用map来存在socket和昵称之间的对应关系,并且由于第一次发送数据时发送的是昵称,所以我们可以根据这个map中是否存在这个socket来判断这次处理的连接请求是否是第一次连接,如果是第一次连接,将socket和昵称存储起来并将信息发送出去,如果不是,直接将信息发送出去。


总结起来就是:客户端改变的是接收和发送的模式,此时客户端的结构其实都已经有了一些变化,所以上面画了张图。但是服务器端只是一些一些数据传输上的变化,整个程序的框架还是没有发送变化,所以图就不画了。


特别需要注意的是MFC对话框和其它线程之间如何传输数据这个问题。可以将对话框的指针作为参数传递进线程中。


还有一些与ip控件处理相关的代码就不列举了。

下面是一些关键的代码:
//点击连接服务器的响应函数,启动一个新的线程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 intSOCKET 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连接的SocketsockConn=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();}

可执行程序可以在这里下载,工程文件可以在这里下载。

关于完成端口的实现,可以参考之前的文章。



0 0
原创粉丝点击