多线程的网络通信程序

起因是学校课要求写一个双机网络通信,并且可以发送文件的程序。

使用的是传统的socket (recv(),send() …..)等。而且是阻塞模式,使用图形界面MFC编写 (VC 6.0)。但为了能让程序不出现假死现象( recv,accept 这样的函数都会出现这样的事情),所以采用了多线程技术,其实也就是用了AfxBeginThread ,TerminateThread等等。这样对于阻塞函数都让他们在新建立的线程里运行就好了。

另外解决的一个大问题就是,创建的新线程无法对窗口进行操作,比如要自在编辑框显示一句话等等。如果直接取得窗口类的句柄操作,会出现wincore的错误,也就是跨线程错误。主要原因也就是——-MFC的线程被自己外部创建的线程调用就会有这个错误。解决的方法就是在线程里用SendMessage给窗口发送一个自定义的消息,比如我这里用的就是。

要实现这样的方法

在BEGIN_MESSAGE_MAP下面要添加

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

//定义全局变量,方便各个工作线程和窗口线程的通信 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

#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一样也要自定义消息

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,"对方已经同意,开始发送文件");