局域网实时聊天系统.docx
精选优质文档-倾情为你奉上局域网实时聊天系统1、 图形界面的设置:使用MFC应用程序框架设计局域网聊天系统的图形界面(对话框),使用的Windows标准控件包括:按钮、静态文本、IP地址控件、分组框、编辑框、列表框等控件。本系统集服务器和客户端为一体,通过网络设置按键弹出具体设置页面,通过单选按钮进行单项设置:服务器端:本地监听端口,用于监听客户端的连接信息; 开启按键和停止按键,用于强制服务器的开启和停止。客户端:服务器的IP设置和服务器的端口设置,用于连接服务器;连接服务器按键和停止按键,用于主动加入和退出聊天系统。聊天记录编辑框:默认只读,用于显示聊天系统中各客户端和服务器的聊天记录;聊天记录输入框:输入信息之后,可点击Enter或者发送信息按键发送信息;关闭窗口:点击按键,可关闭正在执行的对话框;更多功能:可用于聊天系统的其他功能扩展。2、 初始化状态:CheckRadioButton:选择单选按钮中的一个;SetDlgItemText:设置编辑框中显示的字符串;EnableWindow:重载函数,设置控件的启用与关闭;ExtendDiaog:设置四个静态变量m_DlgRectLarge、m_GroupRectLarge:用于保存主对话框和分组框的临时变量;m_DlgRectSmall、m_GroupRectSmall:用于保存主对话框和分组框的改变变量。SetWindowPos:有ID获得主对话框和分组框的句柄,设置界面的伸缩。3、 开启服务器:(1) 创建监听线程:m_hListenThread = CreateThread(NULL, 0, ListenThreadFunc, this, 0, NULL);l NULL:返回的句柄不能被继承;l 0:新线程堆栈的大小与进程主线程堆栈相同;l ListenThreadFunc:线程开始运行的地址,一般为线程入口函数名;l This:传递给线程启动函数的32位参数;l 0:线程创建后立即执行;若为CREAT_SUSPEND,则挂起不执行;l NULL:存放返回的线程ID。(2) 创建监听线程入口函数:DWORD WINAPI ListenThreadFunc(LPVOID pParam)CChatRoomDlg *pChatRoom = (CChatRoomDlg *)pParam;/创建监听套接字( IPv4网络协议、流式套接字、TCP协议)pChatRoom->m_ListenSock = socket(AF_INET , SOCK_STREAM , IPPROTO_TCP);/创建sockaddr_in结构存储IP地址和端口sockaddr_in service;/绑定IP地址和端口到监听套接字bind(pChatRoom->m_ListenSock, (sockaddr*)&service, sizeof(sockaddr_in);/监听申请的连接,等待客户端连接 其中两个参数为:S:用于标识一个已捆绑未连接套接口的描述字。backlog:等待连接队列的最大长度。listen(pChatRoom->m_ListenSock, 5);(3) 使用异步I/O模型防止阻塞connect、accept、recieve或recievefrom这些都是阻塞程序,所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生,则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况读写或是异常。int select(nfds, readfds, writefds, exceptfds, timeout);返回值:准备就绪的描述符数,若超时则返回0,若出错则返回-1。nfds:select监视的数,视进程中打开的文件数而定,一般设为你要监视各文件中的最大文件号加一。(注:nfds并非一定表示监视的文件句柄数。)readfds:select监视的可读文件句柄集合。writefds: select监视的可写文件句柄集合。exceptfds:select监视的异常文件句柄集合。timeout:本次select()的超时结束时间。当readfds或writefds中映象的文件可读或可写或超时,本次select()就结束返回。程序员利用一组系统提供的宏在select()结束时便可判断哪一文件可读或可写,对Socket编程特别有用的就是readfds。几行相关的宏解释如下:FD_ZERO(fd_set *fdset):清空fdset与所有的联系。FD_SET(int fd, fd_set *fdset):建立文件句柄fd与fdset的联系。FD_CLR(int fd, fd_set *fdset):清除文件句柄fd与fdset的联系。(int fd, fd_set *fdset):检查fdset联系的文件句柄fd是否可读写,当>0表示可读写。(4) 非阻塞情况下,在一个套接口接受一个连接SOCKET accept(SOCKET , struct sockaddr FAR *, int FAR *);accept函数主要用于服务器端,一般位于listen函数之后,默认会阻塞进程,直到有一个客户请求连接,建立好连接后,它返回的一个新的套接字 socketfd_new ,此后,服务器端即可使用这个新的套接字socketfd_new与该客户端进行通信,而sockfd 则继续用于监听其他客户端的连接请求。第一个参数:用来标识服务端套接字(也就是listen函数中设置为监听状态的套接字)第二个参数:用来保存客户端套接字对应的“地方”(包括客户端IP和端口信息等), 第三个参数是“地方”的占地大小。返回值:对应客户端套接字标识。实际上是这样的: accept函数指定服务端去接受客户端的连接,接收后,返回了客户端套接字的标识,且获得了客户端套接字的“地方”(包括客户端IP和端口信息等)。这个新的套接字 socketfd_new 与监听套接字sockfd 是什么关系?它所代表的socket对象包含了哪些信息?socketfd_new 是否占用了新的端口与客户端通信? 先简单分析一番,由于网站的服务器也是一种TCP服务器,使用的是80端口,并不会因客户端的连接而产生新的端口给客户端服务,该客户端依然是向服务器端的80端口发送数据,其他客户端依然向80端口申请连接。因此,可以判断,socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd_new一样的端口号。 那这么说,难道一个端口可以被两个socket对象绑定?当客户端发送数据过来的时候,究竟是与哪一个socket对象通信呢? 首先,一个端口肯定只能绑定一个socket。我认为,服务器端的端口在bind的时候已经绑定到了监听套接字socetfd所描述的对象上,accept函数新创建的socket对象其实并没有进行端口的占有,而是复制了socetfd的本地IP和端口号,并且记录了连接过来的客户端的IP和端口号。 那么,当客户端发送数据过来的时候,究竟是与哪一个socket对象通信呢? 客户端发送过来的数据可以分为2种,一种是连接请求,一种是已经建立好连接后的数据传输。 由于TCP/IP协议栈是维护着一个接收和发送缓冲区的。在接收到来自客户端的数据包后,服务器端的TCP/IP协议栈应该会做如下处理:如果收到的是请求连接的数据包,则传给监听着连接请求端口的socetfd套接字,进行accept处理;如果是已经建立过连接后的客户端数据包,则将数据放入接收缓冲区。这样,当服务器端需要读取指定客户端的数据时,则可以利用socketfd_new 套接字通过recv或者read函数到缓冲区里面去取指定的数据(因为socketfd_new代表的socket对象记录了客户端IP和端口,因此可以鉴别)。(5) 监听到的客户端连接加入消息队列在监听过程中,每接收一个客户端连接便产生一个通信套接字,为保存套接字、IP地址、端口号、套接字句柄等信息,此时设计一个类如下:class CClientItem public:CString m_strIp;SOCKET m_Socket;HANDLE hThread;CChatRoomDlg *m_pMainWnd;CClientItem()m_pMainWnd = NULL;m_Socket = INVALID_SOCKET;hThread = NULL;CClientItem tItem;tItem.m_Socket = accSock;tItem.m_pMainWnd = pChatRoom;tItem.m_strIp = inet_ntoa(clientAddr.sin_addr);INT_PTR idx = pChatRoom->m_ClientArray.Add(tItem);/创建连接的客户端的工作线程,默认挂起状态tItem.hThread = CreateThread(NULL, 0, ClientThreadProc, &(pChatRoom->m_ClientArray.GetAt(idx), CREATE_SUSPENDED, NULL);pChatRoom->m_ClientArray.GetAt(idx).hThread = tItem.hThread;ResumeThread(tItem.hThread);4、 开启客户端:(1)点击开启客户端,创建连接线程m_hConnectThred = CreateThread(NULL, 0, ConnectThreadFunc, this, 0, NULL);(2)创建连接线程入口函数DWORD WINAPI ConnectThreadFunc(LPVOID pParam)CChatRoomDlg *pChatRoom = (CChatRoomDlg *)pParam;/创建连接套接字pChatRoom->m_ConnectSock = socket(AF_INET , SOCK_STREAM , IPPROTO_TCP);/获取对话框中的IP和端口信息CString strServIp;pChatRoom->GetDlgItemText(IDC_IP_ADDR, strServIp);int iPort = pChatRoom->GetDlgItemInt(IDC_CONNECT_PORT);/IP地址的转化char szIpAddr16 = 0;USES_CONVERSION;strcpy_s(szIpAddr, 16, T2A(strServIp);/服务器相关IP和端口信息结构体sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(iPort);server.sin_addr.s_addr = inet_addr(szIpAddr);/连接函数connect(pChatRoom->m_ConnectSock, (struct sockaddr *)&server, sizeof(struct sockaddr)5、 发送消息:/获取对话框消息,存到strMsgGetDlgItemText(IDC_INPUT_MSG, strMsg);/将消息在消息记录编辑框显示ShowMsg(strMsg);/如果是服务器端将消息发送给每一个客户端SendClientsMsg(strMsg);void CChatRoomDlg:SendClientsMsg(CString strMsg, CClientItem *pNotSend)TCHAR szBufMAX_BUF_SIZE = 0;/定义缓存器_tcscpy_s(szBuf, MAX_BUF_SIZE, strMsg);/把需要传送的信息拷贝到缓存器 /遍历消息队列中每一个客户端for( INT_PTR idx = 0; idx < m_ClientArray.GetCount(); idx+ ) if ( !pNotSend | pNotSend->m_Socket != m_ClientArray.GetAt(idx).m_Socket | pNotSend->hThread != m_ClientArray.GetAt(idx).hThread | pNotSend->m_strIp != m_ClientArray.GetAt(idx).m_strIp) /将缓存器中的内容传给客户端 第一个参数为客户端对应的套接字标识 /由accept()函数生成send(m_ClientArray.GetAt(idx).m_Socket, (char *)szBuf, _tcslen(szBuf)*sizeof(TCHAR), 0); /如果是客户端将消息发送给服务器 m_ConnectSock为客户端套接字send(m_ConnectSock, (char *)strMsg.GetBuffer(), strMsg.GetLength()*sizeof(TCHAR), 0);send 函数int send( SOCKET s, const char FAR *buf, int len, int flags ); 不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。 该函数的第一个参数指定发送端套接字描述符; 第二个参数指明一个存放应用程序要发送数据的缓冲区; 第三个参数指明实际要发送的数据的字节数; 第四个参数一般置0。 这里只描述同步Socket的send函数的执行流程。当调用该函数时, (1)send先比较待发送数据的长度len和套接字s的发送缓冲的长度, 如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR; (2)如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len (3)如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完 (4)如果len小于剩余 空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。 如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。 要注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执 行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR)注意:在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。通过测试发现,异步socket的send函数在网络刚刚断开时还能发送返回相应的字节数,同时使用select检测也是可写的,但是过几秒钟之后,再send就会出错了,返回-1。select也不能检测出可写了。6、 接收消息:/int iRet = recv(pChatRoom->m_ConnectSock, (char *)szBuf, MAX_BUF_SIZE, 0);/int iRet = recv(m_ClientItem.m_Socket, (char *)szBuf, MAX_BUF_SIZE, 0);recv函数int recv( SOCKET s, char FAR *buf, int len, int flags); 不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。该函数的第一个参数指定接收端套接字描述符; 第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据; 第三个参数指明buf的长度; 第四个参数一般置0。 这里只描述同步Socket的recv函数的执行流程。当应用程序调用recv函数时, (1)recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR, (2)如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的), recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。注意:在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。7、 显示消息:void CChatRoomDlg:ShowMsg(CString strMsg)/任何当前选定内容被去掉选定状态m_MsgEdit.SetSel(-1, -1);m_MsgEdit.ReplaceSel(strMsg+_T("rn");void SetSel( int nStartChar, int nEndChar, BOOL bNoScroll = FALSE );功能:调用此函数能在EDIT Control中选择某范围的字符串。NoScroll 指示是否显示脱字符是滚动可见的。如果值为FALSE,则显示,TRUE不显示。nStartChar 指出当前选中部分的开始位置。如果nStartChar=0且nEndChar=-1,则编辑控件的文本被全选;如果nStartChar=-1,则任何当前选定内容被去掉选定状态。nEndChar 指出结束位置。8、 客户端退出:void CChatRoomDlg:RemoveClientFromArray(CClientItem in_Item)for( int idx = 0; idx <m_ClientArray.GetCount(); idx+ ) CClientItem tItem = m_ClientArray.GetAt(idx);if ( tItem.m_Socket = in_Item.m_Socket &&tItem.hThread = in_Item.hThread &&tItem.m_strIp = in_Item.m_strIp ) m_ClientArray.RemoveAt(idx);9、 扩展知识:(1)UDP套接口函数Socket函数:SOCKET WSAAPI socket(int af,int type,int protocol);Af: AF_INETipv4 AF_INETipv6Type和protocol:SOCK_STREAM 0或者IPPROTO_TCP TCP协议面向连接的全双工字节流传输机制SOCK_DGRAM 0或者IPPROTO_UDP UDP协议无连接、不可靠的数据报服务SOCK_RAW JUTI1DE10或者IPPROTO_* 原始套接字,程序自己填写协议的首部和数据(2)TCP套接口函数从程序员角度,TCP协议是不可见的,需要通过socket提供的接口使用TCP协议、控制它的行为。服务器端会有两个socket:监听socket:等待客户端啦建立连接srv接收到客户端的连接请求而创建的新的socket:用于与客户端交互clt服务器启动时首先创建监听socket srv,绑定本地地址,并在srv上监听,调用accept后,服务器就阻塞在这个函数,一直等待,直到有客户建立连接,accept会返回一个新的socket clt,与客户之间的交互,如接收请求,发送应答,全部都是有clt完成的,当有一方关闭了连接,clt的任务就完成了,需要closesocket释放占用的资源。服务器srv可以继续为其他的客户端提供服务,通常srv并不关闭,只要启动了服务器,就一直监听客户端的连接。一旦closesocket(srv)关闭服务器之后就无法为客户端提供服务了。客户端只有一个socket:应用程序需要服务器资源时,先创建一个socket,调用connect与服务器建立连接,在connect的参数中指明要连接的服务器地址和端口,调用connect导致TCP协议的三次握手,向服务器发送SYN,SYN到达服务器,服务器的TCP检查客户要求的服务,创建一个新的socket为客户服务,并发送SYN和ACK确认客户端的SYN,客户端接收到SYN和ACK之后,发送ACK完后三次握手。服务器端接收到ACK表明成功建立了连接,通知服务器程序连接完成,阻塞的accept会返回新创建的socket clt,客户端发送的请求都有服务器的clt处理,应答也有clt发送。Shutdown函数:让套接口不能再某个方向上传输数据。Int WSAAPI shutdown(SOCKET s,int how)How:SD_SEND:不允许之后的发送操作,但套接口仍可以从对方接收数据半关闭SD_RECEIVE:关闭连接上的读操作,不再允许套接口上的读操作。SD_BOTH:发送和接收都关闭。专心-专注-专业