糖儿飞教你学C++ Socket网络编程——25. MFC版多线程TCP通信程序

图10-3是一个多线程版的TCP一对多通信程序,该程序分为服务器端和客户端。服务器端能够同时接受多个客户端的连接,并能同时接收多个客户端发来的消息,其次,服务器端还能将消息群发给所有已连接的客户端。

 

图10-3 TCP多线程版一对多通信程序

10.3.1程序的实现原理

TCP一对多通信程序的服务器端程序的流程如图10-4所示。

服务器端接受连接和接收数据的实现思路是:每当有新的客户端连接请求时,服务器就新开一个接受连接的子线程来接受该客户端的连接,并在接受连接子线程中,创建一个接收数据子线程,接收客户端发来的数据。

服务器群发消息的实现原理是:将每个接受连接线程中accept函数返回的套接字都保存到一个套接字数组中。然后在“群发消息”的按钮中,用for循环遍历套接字数组,循环执行send函数将消息发送给套接字数组中的所有套接字。

 

socket()

bind()

listen()

accept()

recv()

PostMessage()

服务器端

创建等待线程

CreateThread()

accept()

accept()

保存套接字到数组

CreateThread()

保存套接字到数组

保存套接字到数组

CreateThread()

CreateThread()

WaitProc

WaitProc

WaitProc

RecvProc

recv()

PostMessage()

RecvProc

recv()

PostMessage()

RecvProc

线程1

线程2

线程n

……

 

 

图10-4 TCP一对多通信程序的服务器端

从图10-4可见,等待连接线程(WaitProc)和接收数据线程(RecvProc)必须分离,这样,等待连接时就不会因为阻塞而导致无法接收数据。而10.2节的控制台程序则不存在此问题,因为控制台程序没有主界面线程。

另外,在子线程中无法操纵主界面线程中的控件,解决方法是在子线程中使用PostMessage()将要传递的数据发送给主界面线程,主界面线程接收这些数据并显示。

该程序客户端是普通的TCP通信程序客户端,本例直接采用6.3.2节中制作的TCP通信客户端作为客户端程序。因此本实例只制作服务器端程序。

 

10.3.2 服务器端程序制作步骤

1)创建一个MFC工程:新建工程,选择“MFC APPWizard(exe)”,输入工程名(如Chat),单击“下一步”,在步骤1选择“基本对话框”,单击“完成”按钮。

2)在ResourceView选项卡,找到Dialog下的“IDD_CHAT_DIALOG”,将对话框的界面改为如图10-5所示,并设置各个控件的ID值。

 

图10-5 服务器端程序的界面及控件ID

3)按“Ctrl+W”键,或者在对话框界面上按右键,选择“建立类向导”,打开“MFC类向导”对话框,在“Member Variables”选项卡中为控件设置成员变量如图10-6所示。

 

图10-6 设置成员变量

4)初始对话框界面,打开*dlg.cpp文件,在OnInitDialog()函数中加入如下代码:

       BOOL CChatDlg::OnInitDialog()

       {……

       m_ip=CString("127.0.0.1");                //默认的本机ip地址

       m_port=CString("5566");                 //默认的本机端口号

       UpdateData(FALSE);                 //变量的值传到界面上

       c_send.EnableWindow(FALSE);           //群发按钮无效

       return TRUE;

}

5)在*dlg.h文件中,添加如下引用头文件和声明自定义消息的代码。

#include            

#define     WM_RECVDATA   WM_USER+1             //添加自定义消息

6)在*dlg.h文件中,声明套接字变量和线程函数,以及用于在两个线程之间传递多个参数的结构体RECVPARAM,代码如下。

struct RECVPARAM{           //结构体,用于在两个线程之间传递套接字和对话框句柄

       SOCKET sock;

       HWND hwnd;

};

class CChatDlg : public CDialog{

public:

       static DWORD WINAPI WaitProc(LPVOID lpParameter); //声明线程函数

       static DWORD WINAPI RecvProc(LPVOID lpParameter);

       CChatDlg(CWnd* pParent = NULL);    // standard constructor

       SOCKET m_socket;             //声明套接字变量

……       }

提示:

① 使用多线程时,线程函数如果是类的成员函数,它必须被定义成static。

② 线程与线程之间如果要传递多个变量值,可以把这些变量值放在一个结构体中。

7)在*dlg.h文件中,声明消息处理函数,在“//{{AFX_MSG(CChatDlg)”下添加下面一行:

afx_msg void OnRecvData(WPARAM wParam,LPARAM lParam);

8)在*dlg.cpp文件中,创建消息映射,在BEGIN_MESSAGE_MAP(CChatDlg, CDialog)下面添加一行:

ON_MESSAGE(WM_RECVDATA,OnRecvData)

9)双击“启动服务器”按钮,为该按钮编写创建套接字并监听的如下代码。

void CChatDlg::OnCreate(){                //单击启动服务器按钮

       m_socket=socket(AF_INET,SOCK_STREAM,0);

       if(INVALID_SOCKET==m_socket)     {

              MessageBox("套接字创建失败!");           }

       SOCKADDR_IN addrSock;

       addrSock.sin_family=AF_INET;

       addrSock.sin_addr.S_un.S_addr=inet_addr(m_ip);

       addrSock.sin_port=htons(atoi(m_port));     

       int retval=bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR));

       if(SOCKET_ERROR==retval)       {

              closesocket(m_socket);

              MessageBox("绑定失败!");          }

        listen(m_socket,5);     //监听连接

       RECVPARAM *pRecvParam=new RECVPARAM;

       pRecvParam->sock=m_socket;           //设置线程要传递的套接字

       pRecvParam->hwnd=m_hWnd;          //设置线程要传递的窗口句柄

       //创建等待连接线程,并传递套接字和窗口句柄两个参数给该线程

       HANDLE hThread=CreateThread(NULL,0,WaitProc,(LPVOID)pRecvParam,0,NULL);

       CloseHandle(hThread);   }

10)在*dlg.cpp文件中,在第一个函数外侧前面的位置,定义套接字数组m_Clients用于保存已连接的套接字,和变量num,用于保存数组中套接字的数量。代码如下:

SOCKET * m_Clients[10];            //保存套接字的数组

SOCKADDR_IN addrCli[10];

int num;

// CChatDlg dialog

11)编写线程函数WaitProc(),该函数主要功能是循环接受连接请求,一旦接受连接成功,则将套接字保存到套接字数组中,再创建接收消息线程RecvProc,代码如下:

DWORD WINAPI CChatDlg::WaitProc(LPVOID lpParameter) {   //接受连接线程

     SOCKET sock=((RECVPARAM*)lpParameter)->sock;      //获取参数中的套接字

       HWND hwnd=((RECVPARAM*)lpParameter)->hwnd;

       SOCKADDR_IN addrFrom;

       int len=sizeof(SOCKADDR);

       CString strNotice; /* 通知消息 */    

       for(int i=0;i<10;i++)      //初始化套接字数组

              m_Clients[i]=0;

       num=0;                               //保存套接字数组中的套接字数量

           while(1)     {           //循环接受各个客户端的连接请求

        Sleep(10);          

           SOCKET *sockConn = new SOCKET;             /* 创建通信套接字 */ 

        *sockConn = ::accept(sock, (SOCKADDR*)&addrFrom, &len);        

              m_Clients[num]=sockConn; //将套接字保存到数组中     

              addrCli[num]=addrFrom;             //将套接字地址保存到数组中

              num++;

              if(INVALID_SOCKET == *sockConn)          { 

            strNotice = "accept()失败,再次尝试 ...... "; 

            ::AfxMessageBox(strNotice); 

            continue;          }

              else  AfxMessageBox("一个客户端已成功连接"); 

        DWORD dwThreadId = 1; 

        /* 启动相应线程与客户端通信 */ 

        ::CreateThread(NULL, NULL, CChatDlg::RecvProc, ((LPVOID)sockConn), 0, &dwThreadId);      } 

    return 0;  } 

12)编写线程函数RecvProc (),该函数主要功能是接收数据。需要注意的是,线程接收到数据后,无法直接将数据显示在对话框(主线程)中,为此,必须利用PostMessage发送自定义消息将消息作为参数传递给对话框。代码如下:

DWORD WINAPI CChatDlg::RecvProc(LPVOID lpParameter){

       SOCKET *sockConn = (SOCKET *)lpParameter;       //从参数中获得sockConn

       SOCKADDR_IN addrFrom;         //用于保存客户端地址

       int len=sizeof(SOCKADDR);

       char recvBuf[200];

       char tempBuf[300];

       while(TRUE)  {           

                Sleep(10); 

        int nRecv = ::recv(*sockConn, recvBuf, 200, 0);  //接收数据

        if(nRecv > 0)  { 

            recvBuf[nRecv] = '\0';  //在接收的数据末尾处加\0

                     AfxMessageBox(recvBuf);    }     //测试是否已接收到消息       

       char * strip;           //用于保存客户端ip

       int strport;            //用于保存客户端端口号

       getpeername(*sockConn, (struct sockaddr *)&addrFrom, &len);    //获取客户端的地址

      strip=inet_ntoa(addrFrom.sin_addr);           //把获取的IP转换为主机字节序

       strport=ntohs(addrFrom.sin_port);             //把获取的端口转换为主机字节序

       if(SOCKET_ERROR==nRecv)                   break;

              sprintf(tempBuf,"%s:%d说:%s", strip,strport,recvBuf);

       ::PostMessage(AfxGetMainWnd()->m_hWnd,WM_RECVDATA,0,(LPARAM)tempBuf);

       }

       return 0;  }

13)编写自定义消息的处理函数OnRecvData(),主要功能是接收PostMessage利用参数传来的消息,再把消息显示到列表框中。代码如下:

void CChatDlg::OnRecvData(WPARAM wParam,LPARAM lParam){

       CString str=(char*)lParam;

       c_recvbuf.AddString(str);     }

14)双击“群发消息”按钮,为该按钮编写群发消息给所有已连接的客户端的代码。

void CChatDlg::OnSend(){                         //群发信息给所有客户端

       char buff[200];

       char * ct;

       CTime time = CTime::GetCurrentTime();            //获取当前时间

       CString t = time.Format("        %H:%M:%S");     //设置时间显示格式

       ct=(char*)t.GetBuffer(0);                    //cstring 转 char*

       c_sendbuf.GetWindowText(buff,200);         //获取编辑框中的文本

       c_sendbuf.SetWindowText(NULL);            //清空编辑框中的文本

       CString Ser="服务器: >";

       strcat(buff,ct);

       for(int i=0;i

              send(*m_Clients[i],buff,strlen(buff)+1,0);  

       c_recvbuf.AddString(Ser+buff);   } //将已发送的消息添加到列表框中

提示:

①创建线程后可立即用CloseHandle()关闭线程句柄,这样并不会关闭线程。线程和线程句柄(Handle)是两码事。线程是在CPU上运行的程序执行流的最小单元,线程句柄是一个内核对象,用来对线程执行操作,比如改变线程的优先级,等待其他线程,强制中断线程等,这时就需要使用线程句柄,如果新开一个线程,而不需要对它进行任何干预,则CreateThread后就可直接CloseHandle了。也就是说,线程的生命周期和线程句柄的生命周期是不一样的。线程的生命周期是线程函数从开始执行到return返回,而线程句柄的生命周期是从CreateThread()返回到用CloseHandle()结束。

② getpeername()函数可获得远程连接主机的IP地址和端口等信息。

③ PostMessage()是消息发送函数,用来在不同的线程,或线程与对话框之间发送消息。

习题

1. 在Win32 API中创建线程需要使用               函数,MFC中创建线程使用的是                    函数。

2. Win32 API中线程函数的返回值类型一般声明为                      

3. 如果要在类中声明线程函数,应将线程函数的类型设置为                           

4. CreateThread()函数的第           个参数用于向线程函数传递参数。

5. 线程与线程之间如果要传递多个变量值,应使用                     传递参数。

6. 简述线程和进程的关系和异同点。

7. (实验)将9.3.2节中网络用户登录程序改写成能同时接受多个客户端登录。

你可能感兴趣的:(MFC,Socket)