vc6局域网聊天室,支持文件传输的完成和修改

来源:互联网 发布:电子商务app软件多少钱 编辑:程序博客网 时间:2024/05/21 15:47

    最近马同学接到个任务,相关的是用windows的socket技术实现通信,部分要求如下:

(2) 采用TCP/IP Sock协议建立计算机间远程通讯;
(4) 实时记录传输数据;

(5) 采用多线程技术实现实时传输和数据记录;

    一、

     看要求后,决定参照东东,搞个聊天室,先实现发文件和聊天,实时更新数据下一步搞。参照视频如下:http://www.cctry.com/thread-6-1-1.html,这是syc做的,为图方便,将服务器与客户端为一体了,在他基础上,分离了服务端和客户端。

     本程序下载地址:

     http://www.cctry.com/forum.php?mod=viewthread&tid=39491&page=1#pid330149

     或 http://download.csdn.net/source/3528727 

   运行效果如下:

   

采用了skinsharp皮肤,使用方法如下:http://blog.csdn.net/akof1314/article/details/5038769,主要就是添加

#include "SkinH.h"#pragma comment(lib,"SkinH.lib")SkinH_Attach();
这3行,一般我喜欢添加在InitInstance中让所有对话框都是蓝色。哈哈

首先的几个框的初始化,比如ip初始化为127.0.0.1什么的,在OnInitDialog()中添加:

SetDlgItemText(IDC_IPADDR,"127.0.0.1");SetDlgItemText(IDC_PORT_CLIENT,"8001"); //初始化端口和ipGetDlgItem(IDC_DISCONNECT)->EnableWindow(FALSE);GetDlgItem(IDC_SENDMSG)->EnableWindow(FALSE);

本程序采用多线程,在启动服务器时,启动监听文件和监听数据2个线程,代码如下:

void CServerDlg::OnStartServer() {AfxBeginThread(FileThread,this);//监听文件传输线程m_hListenThread=CreateThread(NULL,0,ListenThreadFunc,this,0,NULL);GetDlgItem(IDC_STOP_SERVER)->EnableWindow(TRUE);GetDlgItem(IDC_SEND)->EnableWindow(TRUE);}
文件接收线程代码如下:(由于我们是开一个线程去监听,监听到有传送请求后才开线程接收文件,所以这里是2个线程)

structSAVEFILEPARAM{SOCKETsock;CServerDlg*        pCLAN;};#define NAMELENGTH 1024#define BUFFERSIZE 65000////////////////单线程接收文件线程////////////////UINTSaveFileSingle(LPVOIDparam){SAVEFILEPARAMsp;sp.pCLAN=((SAVEFILEPARAM*)param)->pCLAN;sp.sock=((SAVEFILEPARAM*)param)->sock;CSocket sock;sock.Attach(sp.sock);DWORDLength;charfileName[NAMELENGTH];CStringsavePathName;intmodal,err;memset(fileName,0,NAMELENGTH);err=sock.Receive(&Length,sizeof(DWORD));//接收文件长度if(0==err){AfxMessageBox("连接被关闭了。");sock.Close();return0;}if(SOCKET_ERROR==err){AfxMessageBox("出错啦,socket的bug");sock.Close();return0;}err=sock.Receive(fileName,NAMELENGTH);//接收文件名if(0==err){AfxMessageBox("连接被关闭啦");sock.Close();return0;}if(SOCKET_ERROR==err){AfxMessageBox("出错啦,fuck");sock.Close();return0;}int iret = AfxMessageBox("文件到啦,要不要",MB_OKCANCEL);if(IDCANCEL==iret){sock.Close();return0;}CFileDialogfdlg(false,NULL,fileName);//保存文件对话框。modal=fdlg.DoModal();if(IDCANCEL==modal){sock.Close();return0; //用户取消保存。}savePathName=fdlg.GetPathName();//////////////////////////////////////////////////////////////intfinish=0;DWORDstep=0;CFilefile;charbuffer[BUFFERSIZE];//每次收,清空buffer?if(0==file.Open(savePathName,CFile::modeWrite|CFile::modeCreate|CFile::typeBinary)){AfxMessageBox("打开要写入的文件出错。");sock.Close();return0;}//////////////////////////////////////////////////////////while(true){finish=sock.Receive(buffer,BUFFERSIZE);if(0==finish)break;if(SOCKET_ERROR==finish){sock.Close();sp.pCLAN->SetFocus();return0;}file.Write(buffer,finish);step+=finish;}file.Close();sock.Close();CStringstrFinish;strFinish.Format("已成功接收文件\r\n\r\n%s",fileName);AfxMessageBox(strFinish);sp.pCLAN->SetFocus();return0;}UINT FileThread(LPVOID param){//发文件我们固定用7000端口CServerDlg*pDlg=(CServerDlg*)param;CSocket   fileListen;//定义监听套接字进行监听if (!AfxSocketInit()){AfxMessageBox("AfxSocketInit()失败。");return 0;}if(0==fileListen.Create(7000))//端口是7000啊,别忘啦{AfxMessageBox("监听套接字创建失败。");fileListen.Close();return0;}if(0==fileListen.Listen()){AfxMessageBox("监听套接字监听失败。");fileListen.Close();return0;}//监听套接字创建成功。//监听套接字等待连接请求。while(true){CSocketsockClient;if(0==fileListen.Accept(sockClient)){AfxMessageBox("监听套接字接受服务失败。");fileListen.Close();break;}//用线程函数来接收文件。/////////////////////////////////////////////////////////////////////////////SAVEFILEPARAMparam;param.sock=sockClient.Detach();param.pCLAN=pDlg;AfxBeginThread(SaveFileSingle,(LPVOID)¶m);//////////////////////////////////////////////////////////////////////////////}fileListen.Close();return 0 ;}
监听数据线程代码如下:(SOCKET_Selet为syc提供的线程函数,主要是封装select方法,进行异步I/O)

BOOL SOCKET_Select(SOCKET hSocket, int nTimeOut, BOOL bRead){fd_set fdset;     //fd_set为管理多个套接字的结构体timeval tv;   //超时时间结构体FD_ZERO(&fdset);//将集合初始化为空集合FD_SET(hSocket, &fdset);//将套接字hSocket加入集合fdsetnTimeOut = nTimeOut > 1000 ? 1000 : nTimeOut;tv.tv_sec  = 0;//超时时间---秒tv.tv_usec = nTimeOut;//超时时间---毫秒int iRet = 0;if ( bRead ) {iRet = select(0, &fdset, NULL , NULL, &tv);//查看服务器套接字集合是否有可读}else{iRet = select(0, NULL , &fdset, NULL, &tv);//查看服务器套接字集合是否有可写}if(iRet <= 0) {return FALSE;} else if (FD_ISSET(hSocket, &fdset)){   //查看套接字 hSocket是否在集合fdset中return TRUE;}return FALSE;}DWORD WINAPI ListenThreadFunc(LPVOID pParam){CServerDlg *pChatRoom = (CServerDlg *)pParam;ASSERT(pChatRoom != NULL);pChatRoom->m_ListenSock = socket(AF_INET , SOCK_STREAM , IPPROTO_TCP);if ( pChatRoom->m_ListenSock == INVALID_SOCKET ) {AfxMessageBox(_T("新建Socket失败!"));return FALSE;}int iPort = pChatRoom->GetDlgItemInt(IDC_PORT_LISTEN);sockaddr_in service;service.sin_family = AF_INET;service.sin_addr.s_addr = INADDR_ANY;service.sin_port = htons(iPort);if ( bind(pChatRoom->m_ListenSock, (sockaddr*)&service, sizeof(sockaddr_in)) == SOCKET_ERROR ) {AfxMessageBox(_T("绑定端口失败!"));goto __Error_End;}if( listen(pChatRoom->m_ListenSock, 5) == SOCKET_ERROR ) {AfxMessageBox(_T("监听失败!"));goto __Error_End;}while( TRUE) {if ( SOCKET_Select(pChatRoom->m_ListenSock, 100, TRUE) ) {sockaddr_in clientAddr;int iLen = sizeof(sockaddr_in);SOCKET accSock = accept(pChatRoom->m_ListenSock, (struct sockaddr *)&clientAddr , &iLen);if (accSock == INVALID_SOCKET) {continue;}//客户端结点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.ElementAt(idx).hThread = tItem.hThread;ResumeThread(tItem.hThread);//恢复一个线程的执行Sleep(100);}}__Error_End:closesocket(pChatRoom->m_ListenSock);return TRUE;}#define MAX_BUF_SIZE 1024DWORD WINAPI ClientThreadProc(LPVOID lpParameter)//客户端线程的创建{CString strMsg;CClientItem m_ClientItem = *(CClientItem *)lpParameter;while( TRUE ){if ( SOCKET_Select(m_ClientItem.m_Socket, 100, TRUE) ) {TCHAR szBuf[MAX_BUF_SIZE] = {0};int iRet = recv(m_ClientItem.m_Socket, (char *)szBuf, MAX_BUF_SIZE, 0);if ( iRet > 0 ) {//right;strMsg=szBuf;strMsg = _T("客户端:") + m_ClientItem.m_strIp + _T(">") + strMsg;m_ClientItem.m_pMainWnd->ShowMsg(strMsg); //接收线程在这里显示m_ClientItem.m_pMainWnd->SendClientsMsg(strMsg,&m_ClientItem);//向除原信息发送客户端以外所有客户端发送信息}else{//close socket;strMsg = _T("客户端:") + m_ClientItem.m_strIp + _T(" 离开了聊天室!");m_ClientItem.m_pMainWnd->ShowMsg(strMsg);m_ClientItem.m_pMainWnd->RemoveClientFromArray(m_ClientItem);break;}}Sleep(500);}return TRUE;}

由于服务端需要保存客户端队列,所以引入类CClientItem,在Inc.h中定义,并在CCserverDlg.h中包含Inc.h,在线程中监听到客户连接,并将客户连接加入CArray中。

#pragma onceclass CServerDlg;class CClientItem {public:CString m_strIp;SOCKET m_Socket;HANDLE hThread;CServerDlg *m_pMainWnd;CClientItem(){m_pMainWnd = NULL;m_Socket = INVALID_SOCKET;hThread = NULL;}};DWORD WINAPI ListenThreadFunc(LPVOID pParam);DWORD WINAPI ConnectThreadFunc(LPVOID pParam);//客户端连接线程BOOL SOCKET_Select(SOCKET hSocket, int nTimeOut=100, BOOL bRead=FALSE);
server端增加以下变量和函数:(要使用CArray需要包含头文件afxtempl.h)

SOCKET m_ListenSock;HANDLE m_hListenThread;  //监听线程句柄CArray <CClientItem,CClientItem> m_ClientArray;//服务器接收客户端队列void ShowMsg(CString strMsg);  //保存聊天记录void RemoveClientFromArray(CClientItem in_Item);//删除客户端连接结点void SendClientsMsg(CString strMsg, CClientItem *pNotSend=NULL);//客户端发送消息void StopServer();

两个成员在CServerDlg的构造函数中的初始化如下:

m_ListenSock=INVALID_SOCKET;//初始化m_hListenThread = NULL;

另外4个函数实现如下:

void CServerDlg::ShowMsg(CString strMsg){m_MsgEdit.SetSel(MAKELONG(-1, -1));//把光标定位到所有文本最后一个位置m_MsgEdit.ReplaceSel(strMsg+_T("\r\n"));//把光标替换一下,替换存参数括号里面的字符串}//删除客户端连接结点void CServerDlg::RemoveClientFromArray(CClientItem in_Item){for( int idx = 0; idx <m_ClientArray.GetSize(); 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);}}}void CServerDlg::SendClientsMsg(CString strMsg, CClientItem *pNotSend)//向客户端发送消息{    char szBuf[1024] = {0};//此处TCHAR换成char    char* pchar = strMsg.GetBuffer(0);     strcpy(szBuf,pchar);    for( INT_PTR idx = 0; idx < m_ClientArray.GetSize(); idx++ ) {if ( !pNotSend || pNotSend->m_Socket != m_ClientArray.ElementAt(idx).m_Socket ||pNotSend->hThread != m_ClientArray.ElementAt(idx).hThread ||pNotSend->m_strIp != m_ClientArray.ElementAt(idx).m_strIp) {send(m_ClientArray.ElementAt(idx).m_Socket, (char *)szBuf, _tcslen(strMsg)*sizeof(char), 0);}}}void CServerDlg::OnSend() {CString strMsg;GetDlgItemText(IDC_SEVER_MSG, strMsg);strMsg = "服务器:>" + strMsg;ShowMsg(strMsg);SendClientsMsg(strMsg);//发送给队列中的所有客户端SetDlgItemText(IDC_SEVER_MSG, _T(""));}void CServerDlg::OnStopServer() {int iret=MessageBox("您真的想停止吗?","",MB_OKCANCEL);if (iret=IDOK){StopServer();//客户端线程的结束ShowMsg("停止客户端成功!");}}//服务端结束函数void CServerDlg::StopServer(){int nCount = m_ClientArray.GetSize();HANDLE *m_pHandles = new HANDLE[nCount+1];m_pHandles[0] = m_hListenThread;for( int idx = 0; idx < nCount; idx++ ){m_pHandles[idx+1] = m_ClientArray.ElementAt(idx).hThread;}DWORD dwRet = WaitForMultipleObjects(nCount+1, m_pHandles, TRUE, 1000);//等待多个内核对象激发的apiif ( dwRet != WAIT_OBJECT_0 ) {for( INT_PTR i = 0; i < m_ClientArray.GetSize(); i++ ) {TerminateThread(m_ClientArray.ElementAt(i).hThread, -1);closesocket(m_ClientArray.ElementAt(i).m_Socket);}TerminateThread(m_hListenThread, -1);closesocket(m_ListenSock);}delete [] m_pHandles;m_hListenThread = NULL;m_ListenSock = INVALID_SOCKET;}
客户端发送文件,固定通过ip控件中的地址,虽然我们初始化为127.0.0.1,发送文件固定用端口7000,于是发送函数如下:

void CClientDlg::OnSendfile() {intmodal,nCount;CStringfileName;CString     ip ;GetDlgItemText(IDC_IPADDR,ip);CFileDialog fdlg(true);modal=fdlg.DoModal();if(IDCANCEL==modal){return; //用户取消发送。}SENDFILEPARAM*p=newSENDFILEPARAM;fileName=fdlg.GetFileName();p->pathName=fdlg.GetPathName();nCount=fileName.GetLength();memset(p->fileName,0,NAMELENGTH);for(inti=0;i<nCount;i++){p->fileName[i]=fileName.GetAt(i);}p->pCLAN=this;p->ip = ip;////////////////////////////////////////////////AfxBeginThread(SendFileSingle,(LPVOID)p);//调用线程函数,fuck}
调用的发送线程以及其他结构如下:

structSENDFILEPARAM{CStringpathName;CStringip;charfileName[NAMELENGTH];CClientDlg*        pCLAN;};////////////////单线程发送文件线程。///////////////////////////////////UINTSendFileSingle(LPVOIDparam){if (!AfxSocketInit()){AfxMessageBox("SendFileSingleAfxSocketInit()失败");return 0;}CClientDlg*pCLAN=((SENDFILEPARAM*)param)->pCLAN;CStringip;charfileName[NAMELENGTH];charbuffer[BUFFERSIZE] = {""};DWORDLength; //文件长度CFilefile;memset(fileName,0,NAMELENGTH);strcpy(fileName,((SENDFILEPARAM*)param)->fileName);ip=((SENDFILEPARAM*)param)->ip;if(0==file.Open(((SENDFILEPARAM*)param)->pathName,CFile::modeRead|CFile::typeBinary)){return0;}Length=file.GetLength();//获取文件长度,字节//创建发送套接字准备发送CSocketsockSend;if( 0==sockSend.Create() ){AfxMessageBox("创建发送套接字出错。");return0;}if( 0==sockSend.Connect(ip,7000) ){AfxMessageBox("猪啊,服务器都没有,你发个毛线,是吧?\r\n");sockSend.Close();return0;}if(SOCKET_ERROR ==sockSend.Send(&Length,sizeof(DWORD)))//发送文件长度{AfxMessageBox("发送文件长度出错。\r\n");sockSend.Close();return0;}if(SOCKET_ERROR ==sockSend.Send(fileName,NAMELENGTH))//发送文件名{AfxMessageBox("发送文件名出错。\r\n");sockSend.Close();return0;}DWORDstep=0;intover,err;//////////////////////////////////////////////发送文件内容。do{file.Seek(step,CFile::begin);over=file.Read(buffer,BUFFERSIZE);err=sockSend.Send(buffer,over);if(BUFFERSIZE>over)break;if(SOCKET_ERROR==err){AfxMessageBox("发送文件内容出错。\r\n");sockSend.Close();pCLAN->SetFocus();return0;}step+=err; //少了句,发错了,哈哈//让你丫的不断的发,日啊。。。///////////////////////////////////////////////////////}while(true);file.Close();sockSend.Close();CStringstrFinish;strFinish.Format("已成功发送文件\r\n\r\n%s。",fileName);AfxMessageBox(strFinish);pCLAN->SetFocus();delete(SENDFILEPARAM*)param;return0;}
客户端发送,需要两个成员,一个连接进程的句柄,一个连接的socket:

HANDLE m_hConnectThread;SOCKET m_ConnectSock;
并在构造函数中初始化:

m_hConnectThread = NULL;m_ConnectSock = INVALID_SOCKET;
点击连接,启动进程

void CClientDlg::OnConnect() {m_hConnectThread=CreateThread(NULL,0,ConnectThreadFunc,this,0,NULL);GetDlgItem(IDC_DISCONNECT)->EnableWindow(TRUE);GetDlgItem(IDC_SENDMSG)->EnableWindow(TRUE);}
连接线程:

BOOL SOCKET_Select(SOCKET hSocket, int nTimeOut=100, BOOL bRead=TRUE){fd_set fdset;     //fd_set为管理多个套接字的结构体timeval tv;   //超时时间结构体FD_ZERO(&fdset);//将集合初始化为空集合FD_SET(hSocket, &fdset);//将套接字hSocket加入集合fdsetnTimeOut = nTimeOut > 1000 ? 1000 : nTimeOut;tv.tv_sec  = 0;//超时时间---秒tv.tv_usec = nTimeOut;//超时时间---毫秒int iRet = 0;if ( bRead ) {iRet = select(0, &fdset, NULL , NULL, &tv);//查看服务器套接字集合是否有可读}else{iRet = select(0, NULL , &fdset, NULL, &tv);//查看服务器套接字集合是否有可写}if(iRet <= 0) {return FALSE;} else if (FD_ISSET(hSocket, &fdset)){   //查看套接字 hSocket是否在集合fdset中return TRUE;}return FALSE;}//客户端连接线程#define MAX_BUF_SIZE 1024DWORD WINAPI ConnectThreadFunc(LPVOID pParam){CClientDlg *pChatRoom = (CClientDlg *)pParam;ASSERT(pChatRoom != NULL);pChatRoom->m_ConnectSock = socket(AF_INET , SOCK_STREAM , IPPROTO_TCP);if ( pChatRoom->m_ConnectSock == INVALID_SOCKET ) {AfxMessageBox(_T("新建Socket失败!"));return FALSE;}CString strServIp;pChatRoom->GetDlgItemText(IDC_IPADDR, strServIp);int iPort = pChatRoom->GetDlgItemInt(IDC_PORT_CLIENT);if ( iPort <= 0 || iPort > 65535 ) {AfxMessageBox(_T("请输入合适的端口:1 - 65535"));goto __Error_End;}sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(iPort);server.sin_addr.s_addr = inet_addr(strServIp);if ( connect(pChatRoom->m_ConnectSock, (struct sockaddr *)&server,  sizeof(struct sockaddr)) == SOCKET_ERROR ) {AfxMessageBox(_T("连接失败,请重试!"));goto __Error_End;}pChatRoom->GetDlgItem(IDC_DISCONNECT)->EnableWindow(TRUE);pChatRoom->ShowMsg(_T("系统信息:连接服务器成功!"));while( TRUE) {if ( SOCKET_Select(pChatRoom->m_ConnectSock) ) {char szBuf[MAX_BUF_SIZE] = {0};   //此处TCHAR改为charint iRet = recv(pChatRoom->m_ConnectSock, (char *)szBuf, MAX_BUF_SIZE, 0);if ( iRet > 0 ) {//right;pChatRoom->ShowMsg(szBuf);   //这里显示出服务器发的信息}else{//close socket;pChatRoom->ShowMsg(_T("服务器已停止,请重新进行连接!"));break;}}Sleep(500);}__Error_End:closesocket(pChatRoom->m_ConnectSock);return TRUE;}
其中ShowMsg函数与Server端定义相同。其余断开连接和发送的函数定义如下:

void CClientDlg::OnDisconnect() {int iret=MessageBox("您真的想停止吗?","",MB_OKCANCEL);if (iret=IDOK){StopClient();//客户端线程的结束ShowMsg("停止客户端成功!");}}void CClientDlg::OnSendmsg() {CString strMsg;GetDlgItemText(IDC_MSG_SEND, strMsg);CString strTmp = _T("客户端:>") + strMsg;int iSend = send(m_ConnectSock, (char *)strMsg.GetBuffer(sizeof(strMsg)), strMsg.GetLength()*sizeof(TCHAR), 0);strMsg.ReleaseBuffer();SetDlgItemText(IDC_MSG_SEND, _T(""));}void CClientDlg::StopClient(){DWORD dwRet = WaitForSingleObject(m_hConnectThread, 1000);//(WaitForSingleObject()等待某一个内核对象被激发,这个函数才返回)if ( dwRet != WAIT_OBJECT_0 ) {  //没正常结束线程TerminateThread(m_hConnectThread, -1);   //强制线程结束方法1closesocket(m_ConnectSock);              //强制线程结束方法2,两个都不建议使用}m_hConnectThread = NULL;m_ConnectSock = INVALID_SOCKET;}
     程序目前运行良好,可以完善的地方, 在于可以加上个进度条显示文件传输啊

   

    二、 

    鉴于他们任务要求是实时获取数据,这里又没有采集设备什么的,就采取了定时器思想,对前面的程序进行了如下修改:

   1、在启动按钮时,启动定时器;

   2、添加WM_TIMER响应函数,在这里进行数据发送;

   3、监听线程中,不进行其他收发处理,只将客户socket加入CArray中;

    即,在启动按钮中添加:(此处为2s刷新一次)

SetTimer(100,2000,0);
在OnTimer中:(这里只是随机生成3个数,需包含time.h并进行发送)
void CServerDlg::OnTimer(UINT nIDEvent) {srand(time(NULL));int name = rand()%1000;int age = rand()%90;int freq = rand()%80;CString str;str.Format("%d-%d*%d",name,age,freq);SendClientsMsg(str); //每5s发送一次aiCDialog::OnTimer(nIDEvent);}
客户端进行解析并显示就好了

测试界面修改如下:


还是能显示正常的,2s刷新一次,基本可以模拟实时数据更新这一要求。

问题在于收发数据,恐怕得写个协议,以方便解析,不然都是CString,也蛋疼。


   基本搞定,就是这样了,菜鸟goes on ~~~


8-22补充:发现一个问题,在启动并结束后,再次启动server,则会提示创建套接字失败,想了想,应该是在结束线程之前关闭套接字,因为用于监听的SOCKET是在线程里创建的。于是StopServer函数的几行改为:

if ( dwRet != WAIT_OBJECT_0 ) {for( INT_PTR i = 0; i < m_ClientArray.GetSize(); i++ ) {closesocket(m_ClientArray.ElementAt(i).m_Socket);TerminateThread(m_ClientArray.ElementAt(i).hThread, -1);}closesocket(m_ListenSock);TerminateThread(m_hListenThread, -1);}

问题解决,不再提示失败

原创粉丝点击