起因是学校课要求写一个双机网络通信,并且可以发送文件的程序。
使用的是传统的socket (recv(),send() …..)等。而且是阻塞模式,使用图形界面MFC编写 (VC 6.0)。但为了能让程序不出现假死现象( recv,accept 这样的函数都会出现这样的事情),所以采用了多线程技术,其实也就是用了AfxBeginThread ,TerminateThread等等。这样对于阻塞函数都让他们在新建立的线程里运行就好了。
另外解决的一个大问题就是,创建的新线程无法对窗口进行操作,比如要自在编辑框显示一句话等等。如果直接取得窗口类的句柄操作,会出现wincore的错误,也就是跨线程错误。主要原因也就是——-MFC的线程被自己外部创建的线程调用就会有这个错误。解决的方法就是在线程里用SendMessage给窗口发送一个自定义的消息,比如我这里用的就是。
要实现这样的方法
在BEGIN_MESSAGE_MAP下面要添加
1 2 3 4 |
ON_MESSAGE(WM_UpdateDATA, OnMyMessage) //绑定我自己的消息,这样外部的线程就可以通过SendMessage来调用窗体的函数OnMyMessage了,用来显示信息WM_UpdateDATA在stdafx.h中我自己定义 #define WM_UpdateDATA WM_USER+100 //定义一个自己的消息这样只要外部给窗口SendMessage WM_UpdateDATA 就会让窗口的onmymessage函数执行了,具体要显示的内容放在一个全局变量就好了。 |
这里写出一些关键的代码,具体程序可以到点击这里下载
我注释的相当详细了。
编译的话,debug版可以正常工作,但release版就无法正常工作了
serverdlg.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
//定义全局变量,方便各个工作线程和窗口线程的通信 SOCKET sockuse,sock; int flag; //主要是用来标志是否连接,用来控制一些循环和功能 HWND mydlg; //记录窗体的句柄 HWND stopbutton; //记录关闭连接按钮的句柄 char buff[200]; //主要的缓冲区,显示数据使用,也供线程间通信使用 CWinThread* mainthread; //记录线程的句柄 UINT FileTrans(LPVOID pParam) //发送文件的线程函数 { OPENFILENAMEA ofn; char szFile[260]; char path[260]; char filebuff[100]; int num; ZeroMemory(&ofn, sizeof(ofn)); ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = NULL; ofn.lpstrFile = szFile; ofn.lpstrFile[0] = ''; ofn.nMaxFile = sizeof(szFile); ofn.lpstrFilter = "所有文件*.*"; ofn.nFilterIndex = 1; ofn.lpstrFileTitle = NULL; ofn.nMaxFileTitle = 0; ofn.lpstrInitialDir = NULL; ofn.Flags = 0; //以上的定义是为了建立一个文件打开对话框。步骤 //上来说都是固定的,这里只是借用一下 if (GetOpenFileNameA(&ofn)==FALSE) //判断文件路径取得是否正确 { //若失败则恢复建立MainControl线程,进行数据接收处理。 mainthread=AfxBeginThread(MainControl,NULL); flag=1; //设定flag保证MainControl可以正常进行循环 return 0; } memset(filebuff,0,100); //清空filebuff strcpy(path,ofn.lpstrFile); //将路径拷贝到path中 strcpy(filebuff,"@@sendstart"); send(sockuse,filebuff,strlen(filebuff),0); //发送请求信息 recv(sockuse,filebuff,100,0); //等待接收反馈信息 if(strcmp(filebuff,"@@sendagree")) //若接收到的信息不是@@sendagree { strcpy(buff,"对方接受错误"); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消息,显示信息 mainthread=AfxBeginThread(MainControl,NULL); //恢复建立MainControl线程 flag=1; //设定flag保证MainControl循环正常 return 0; } SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消息,显示信息 while(!feof(fp)) { num=fread(filebuff,1,100,fp); //文件结束前每次读取100字节 send(sockuse,filebuff,num,0); //发送,最后一次不足100字节 //作为标志,可以让接受方知道文件结束 } strcpy(buff,"发送完毕"); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消息,显示信息 memset(buff,0,100); fclose(fp); //关闭文件 flag=1; return 0; } UINT FileRecv() //接收文件函数 { char sendbuff[100]; //发送缓冲区 char recvbuff[100]; //接受缓冲区 int num; //记录每次接受的字节数 CString szGetName; //记录保存的文件路径 CFileDialog * lpszOpenFile; //定义一个CfileDialog对象 lpszOpenFile=new CFileDialog(false,"","",OFN_FILEMUSTEXIST|OFN_HIDEREADONLY,"文件类型(*.*)|*.*||");//生成一个对话框 if(lpszOpenFile->DoModal()==IDOK)//假如点击对话框确定按钮 { szGetName = lpszOpenFile->GetPathName(); //得到打开文件的路径 //SetWindowText(szGetName); //在窗口标题上显示路径 } delete lpszOpenFile; //释放分配的对话框 memset(sendbuff,0,100); strcpy(sendbuff,"@@sendagree"); send(sockuse,sendbuff,strlen(sendbuff),0); //发送@@sendagree告知对方开始发送吧 SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消息,显//示信息 num=recv(sockuse,recvbuff,100,0); //接收数据 FILE *fp=fopen(szGetName,"wb"); //打开文件,路径为szGetName fwrite(recvbuff,num,1,fp); //写入之前的数据 { num=recv(sockuse,recvbuff,100,0); fwrite(recvbuff,num,1,fp); } fclose(fp); //结束后关闭文件。 strcpy(buff,"接收完毕"); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消息,////显示信息 return 0; } int Addrlen=sizeof(sockaddr_in); //accept要用到的数值 sockaddr_in ClientAddr; sockuse=accept(sock,(struct sockaddr FAR *)&ClientAddr,&Addrlen); //上一句的accept函数调用后会进行阻塞,造成未返回时程序假死,使用了单独的线程 //就是为了防止这样的现象发生 flag=1; //返回成功后,设定flag保证MainControl循环正常 strcpy(buff,"已经连接!"); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消//息,显示信息 mainthread=AfxBeginThread(MainControl,NULL); //建立MainControl线程 return 0; } { char recvbuff[100]; //接受缓冲区 memset(buff,0,100); while(flag) { memset(recvbuff,0,100); //每次接收前清空缓冲区 recv(sockuse,recvbuff,100,0); //进行阻塞接收数据,如果不是用单独的线程 //会造成程序假死,这也就是为什么我的程序使用单独的线程来处理 if(!strcmp(recvbuff,"@@end")) { SendMessage(stopbutton,BM_CLICK,NULL,NULL); //消息判断为@@end则调用 //窗口的OnButtonEnd函数来结束连接的清理工作。 return 0; } else if(!strcmp(recvbuff,"@@sendstart")) { //接受的消息判断为"@@sendstart"则调用FileRecv() FileRecv(); } else { strcpy(buff,"client:"); strcat(buff,recvbuff); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给//窗口消息,显示信息 } } return 0; } { m_ctrlstop.EnableWindow(true); m_ctrlfile.EnableWindow(true); m_ctrlstart.EnableWindow(false); m_ctrlsend.EnableWindow(true); //以上使各个按钮进行使能 sockaddr_in ServerAddr; //开始建立socket WSADATA WSAData; if(WSAStartup(MAKEWORD(2,2),&WSAData)!=0) { showmess("SOCKET 初始化错误rn"); return; } sock=socket(AF_INET,SOCK_STREAM,0); //采用流式套接字,ipv4 if(sock==SOCKET_ERROR) { showmess("SOCKET 创建错误!rn"); WSACleanup(); return; } ServerAddr.sin_family=AF_INET; ServerAddr.sin_addr.s_addr=htonl(INADDR_ANY); //任意ip作为本机ip ServerAddr.sin_port=htons(2006); //使用2006端口 if(bind(sock,(struct sockaddr FAR*)&ServerAddr,sizeof(ServerAddr))==SOCKET_ERROR) { //绑定socket和本地地址 showmess("绑定错误!n"); return; } //显示正在侦听 showmess("listening....."); listen(sock,1); flag=0; //未连接之前,flag=0 mydlg=this->GetSafeHwnd(); //取得窗口句柄供线程函数使用 stopbutton=::GetDlgItem(mydlg,IDC_BUTTON_STOP); //取得关闭连接按//钮的句柄供线程函数使用 AfxBeginThread(WaitForAccept,NULL); //启动WaitForAccept线程等待连接 return; } void CServerDlg::showmess(char *mess) //用来在信息窗口显示信息 { m_strmess+=mess; m_strmess+="rn"; //在每条信息后添加回车 UpdateData(false); //更新信息显示 } { flag=0; //将连接标志清零 strcpy(buff,"@@end"); send(sockuse,buff,strlen(buff),0); //给对方发送信息告知结束 TerminateThread(mainthread->m_hThread,0x01); //结束MainControl线程 closesocket(sock); //关闭套接字 closesocket(sockuse); WSACleanup(); //清理网络 m_ctrlstop.EnableWindow(false); //使能一些按钮 m_ctrlfile.EnableWindow(false); m_ctrlstart.EnableWindow(true); } void CServerDlg::OnMyMessage() { showmess(buff); //仅仅是为了线程函数调用内部的信息显示函数 } void CServerDlg::OnButtonFile() { flag=0; //设定 flag TerminateThread(mainthread->m_hThread,0x01); //中止MainControl AfxBeginThread(FileTrans,NULL); //启用FileTrans线程 } void CServerDlg::OnButtonSend() { char talkbuff[100]; memset(talkbuff,0,100); UpdateData(true); strcpy(talkbuff,m_strtalk); //取得对话框数据 send(sockuse,talkbuff,100,0); //发送给对方信息 } |
clientDlg.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
#include "winsock.h" #include "stdio.h" #include"string.h" #pragma comment(lib,"wsock32.lib") //这四句要加在本文件的开头部分。保证 //网络功能正常 在BEGIN_MESSAGE_MAP下面要添加 ON_MESSAGE(WM_UpdateDATA, OnMyMessage) /*绑定我自己的消息,这样外部的线程就可以通过SendMessage来调用窗体的函数OnMyMessage了,用来显示信息 WM_UpdateDATA在stdafx.h中我自己定义为 #define WM_UpdateDATA WM_USER+100 //定义一个自己的消息*/ //定义全局变量,方便各个工作线程和窗口线程的通信 SOCKET sock; char buff[100]; //主要的缓冲区,显示数据使用,也供线程间通信使用 HWND mydlg; //记录窗体的句柄 sockaddr_in ServerAddr; int flag=0; //主要是用来标志是否连接,用来控制一些循环和功能 CWinThread* mainthread; HWND stopbutton; //记录关闭连接按钮的句柄 UINT WaitForConnect(LPVOID pParam) //等待connect 的线程 { if(connect(sock,(struct sockaddr*)&ServerAddr,sizeof(ServerAddr))==SOCKET_ERROR) { strcpy(buff,"connect failn"); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消//息,显示信息 closesocket(sock); //失败则关闭sock return 0; } strcpy(buff,"已经连接!"); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL);//发送给窗口消////息,显示信息 flag=1; mainthread=AfxBeginThread(MainControl,NULL); //建立MainControl线程 return 0; } { char sendbuff[100]; //发送缓冲区 char recvbuff[100]; //接受缓冲区 int num; //记录每次接受的字节数 CString szGetName; CFileDialog * lpszOpenFile; //定义一个CfileDialog对象 lpszOpenFile = new CFileDialog(false,"","",OFN_FILEMUSTEXIST|OFN_HIDEREADONLY,"文件类型(*.*)|*.*||");//生成一个对话框 if(lpszOpenFile->DoModal()==IDOK)//假如点击对话框确定按钮 { szGetName = lpszOpenFile->GetPathName(); //得到打开文件的路径 //SetWindowText(szGetName); //在窗口标题上显示路径 } delete lpszOpenFile; //释放分配的对话框 memset(sendbuff,0,100); strcpy(sendbuff,"@@sendagree"); send(sock,sendbuff,strlen(sendbuff),0); //发送@@sendagree告知对方开始发送吧 SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消息,显示信息 num=recv(sock,recvbuff,100,0); //接收数据 FILE *fp=fopen(szGetName,"wb"); //打开文件,路径为szGetName fwrite(recvbuff,num,1,fp); //写入之前的数据 while(num==100) //根据接收是否为100字节判断文件是否结束 { num=recv(sock,recvbuff,100,0); fwrite(recvbuff,num,1,fp); } strcpy(buff,"接收完毕"); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消息,显示信息 return 0; } char recvbuff[100];//接受缓冲区 { memset(buff,0,100); while(flag) { memset(recvbuff,0,100); //每次接收前清空缓冲区 recv(sock,recvbuff,100,0); //进行阻塞接收数据,如果不是用单独的线程 //会造成程序假死,这也就是为什么我的程序使用单独的线程来处理 if(!strcmp(recvbuff,"@@end")) {//消息判断为@@end则调用 //窗口的OnButtonEnd函数来结束连接的清理工作。 SendMessage(stopbutton,BM_CLICK,NULL,NULL); return 0; } else if(!strcmp(recvbuff,"@@sendstart")) { //接受的消息判断为"@@sendstart"则调用FileRecv() FileRecv(); memset(recvbuff,0,100); } else { strcpy(buff,"server:"); strcat(buff,recvbuff); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消息,显示信息 } } return 0; } { OPENFILENAMEA ofn; char szFile[260]; char path[260]; char filebuff[100]; int num; ZeroMemory(&ofn, sizeof(ofn)); ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = NULL; ofn.lpstrFile = szFile; ofn.lpstrFile[0] = ''; ofn.nMaxFile = sizeof(szFile); ofn.lpstrFilter = "所有文件*.*"; ofn.nFilterIndex = 1; ofn.lpstrFileTitle = NULL; ofn.nMaxFileTitle = 0; ofn.lpstrInitialDir = NULL; ofn.Flags = 0; //以上的定义是为了建立一个文件打开对话框。步骤 //上来说都是固定的,这里只是借用一下 if (GetOpenFileNameA(&ofn)==FALSE) //判断文件路径取得是否正确 { //若失败则恢复建立MainControl线程,进行数据接收处理。 mainthread=AfxBeginThread(MainControl,NULL); flag=1; //设定flag保证MainControl可以正常进行循环 return 0; } memset(filebuff,0,100); //清空filebuff strcpy(path,ofn.lpstrFile); //将路径拷贝到path中 strcpy(filebuff,"@@sendstart"); send(sock,filebuff,strlen(filebuff),0); //发送请求信息 recv(sock,filebuff,100,0); //等待接收反馈信息 if(strcmp(filebuff,"@@sendagree")) //若接收到的信息不是@@sendagree { strcpy(buff,"对方接受错误"); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消息,显示信息 mainthread=AfxBeginThread(MainControl,NULL); //恢复建立MainControl线程 flag=1; //设定flag保证MainControl循环正常 return 0; } strcpy(buff,"对方已经同意,开始发送文件"); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); ////发送给窗口消息,显示信息 FILE *fp=fopen(path,"rb"); //打开文件 while(!feof(fp)) { num=fread(filebuff,1,100,fp); //文件结束前每次读取100字节 send(sock,filebuff,num,0); //发送,最后一次不足100字节 //作为标志,可以让接受方知道文件结束 } strcpy(buff,"发送完毕"); SendMessage(mydlg,WM_UpdateDATA,NULL,NULL); //发送给窗口消息,显示信息 memset(buff,0,100); fclose(fp); //关闭文件 mainthread=AfxBeginThread(MainControl,NULL); //恢复建立MainControl线程 flag=1; return 0; } { m_ctrlstop.EnableWindow(true); m_ctrlfile.EnableWindow(true); m_ctrlconnect.EnableWindow(false);//以上使各个按钮进行使能 WSADATA WSAData;//开始建立socket if(WSAStartup(MAKEWORD(2,2),&WSAData)!=0) { showmess("socket初始化错误"); return; } sock=socket(AF_INET,SOCK_STREAM,0);//采用流式套接字,ipv4 if(sock==SOCKET_ERROR) { showmess("SOCK Create FAIL!"); WSACleanup(); return; } ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(2006); //使用2006端口 UpdateData(true); //读取ip 值 ServerAddr.sin_addr.s_addr = inet_addr(m_strip); //使用编辑框中的ip地址,默认值127.0.0.1 //取得窗口句柄供线程函数使用 flag=0; stopbutton=::GetDlgItem(mydlg,IDC_BUTTON_STOP); //取得关闭连接按钮的句柄供线程函数使用 AfxBeginThread(WaitForConnect,NULL); //启动WaitForConnect线程进行连接 return; { m_strmess+=mess; m_strmess+="rn"; //在每条信息后添加回车 UpdateData(false); //更新信息显示 } void CClientDlg::OnMyMessage() { showmess(buff);//仅仅是为了线程函数调用内部的信息显示函数 } { TerminateThread(mainthread,0x01); //结束MainControl线程 strcpy(buff,"@@end"); send(sock,buff,strlen(buff),0); //给对方发送信息告知结束 closesocket(sock); //关闭套接字 WSACleanup(); m_ctrlstop.EnableWindow(false);//使一些按钮 m_ctrlfile.EnableWindow(false); m_ctrlconnect.EnableWindow(true); } { char talkbuff[100]; memset(talkbuff,0,100); UpdateData(true); strcpy(talkbuff,m_strsendmess); //取得对话框数据 send(sock,talkbuff,100,0); //发送给对方信息 } void CClientDlg::OnButtonFile() { flag=0; //设定 flag TerminateThread(mainthread->m_hThread,0x01); //中止MainControl AfxBeginThread(FileTrans,NULL); //启用FileTrans线程 } void CClientDlg::OnButtonSend() flag=0; //将连接标志清零 void CClientDlg::OnButtonStop() } void CClientDlg::showmess(char *mess) //用来在信息窗口显示信息 mydlg=this->GetSafeHwnd(); void CClientDlg::OnButtonConnect() UINT FileTrans(LPVOID pParam) UINT MainControl(LPVOID pParam) fclose(fp); //结束后关闭文件。 strcpy(buff,"开始接收文件"); UINT FileRecv() //接收文件函数 UINT MainControl(LPVOID pParam); //提前声明该线程函数 |
再次回到clientDlg.cpp中,接下来就是程序的主体部分了。
接下来是客户端的程序了,和server一样也要自定义消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void CServerDlg::OnButtonStop() void CServerDlg::OnButtonStart() UINT MainControl(LPVOID pParam) { UINT WaitForAccept(LPVOID pParam) //等待accept 的线程 while(num==100) //根据接收是否为100字节判断文件是否结束 strcpy(buff,"开始接收文件"); mainthread=AfxBeginThread(MainControl,NULL); //恢复建立MainControl线程 FILE *fp=fopen(path,"rb"); //打开文件 strcpy(buff,"对方已经同意,开始发送文件"); |