📄 15.6.3 实现接收端功能.txt
字号:
15.6.3 实现接收端功能
接下来,编写接收端程序。因为当在接收端接收数据时,如果没有数据到来, recvfrom 函数会阻
塞,从而导致程序暂停运行。所以,我们可以将接收数据的操作放置在一个单独的线程中完成,井
给这个线程传递两个参数,一个是己创建的套接字,一个是对话框控件的句柄,这样,在该线程中,
当接收到数据后,可以将该数据传回给对话框,经过处理后显示在接收编辑框控件上。我们可以回
头看看 CreateThread函数的声明,发现该函数只为我们提供了一个参数,即该函数的第四个参数,
用来向创建的线程传递参数。而现在我们需要传递两个参数,这应如何实现呢?"山穷水尽疑无路,
柳暗花明又一村",我们可以看到 CreateThread函数的第四个参数是指针类型,既然是一个指针,
那么它既可以是一个指向变量的指针,也可以是一个指向对象的指针,这样,我们可以定义一个结
构体,在该结构体中包含想要传递给线程的两个参数,然后将该结构体类型的指针变量传递给
CreateThread函数的第四个变量。
我们先在 CChatDialog类的头文件中,在该类的声明的外部定义一个阻RECVPARAM结构体,代码如例
15-11所示。
例15-11
struct RECVPARAM
{SOCKET sock; //己创建的套接字
HWND hwnd; //对话框句柄
};
在该结构体中,定义了两个成员,一个是 SOCKET类型的成员: sock,另一个是HWND类型的成员: hwnd。
提示:定义结构体时,最后一定要在其后加上分号。
然后在 CChatDialog类的 OnInitDialog函数中,在上面刚刚添加的 InitSocket函数调用(在上述如
例 15-10所示代码中加灰显示的那行代码)后面添加下述代码,以完成数据接收线程的创建,并传递
所需的参数。
例 15-12
RECVPARAM *pRecvParam=new RECVPARAM;
pRecvParam->sock=m_socket;
pRecvParam->hwnd=m_hWnd;
//创建接收线程
HANDLE hThread=CreateThread(NULL, 0, RecvProc , (LPVOID)pRecvParam, 0, NULL) ;
//关闭该接收线程句柄,释放其引用计数
CloseHandle(hThread) ;
在上述例 15-12所示代码中,首先定义了一个阻CVPARAM结构指针: pRecvParam, 并利用 new操作符
为该变量分配空间。然后,对该结构体变量中的两个成员进行初始化,将 sock成员设置为己创建的
套接字,将 hwnd成员设置为对话框的句柄。在前面章节已经讲过,所有与窗口有关的类都有一个数
据成员: m_hWnd,它保存了与该类相关的窗口的句柄。因此, CChatD ialog类的成员变量 mJ1W_hWnd
就是该对话框的句柄。
接下来,就调用 CreateThread函数创建数据接收线程,其中第四个参数就是主线程要向数据接收线
程传递的参数,因为该参数的类型是 LPVOID,而我们将要传递的却是 RECVPARAM指针类型,所以需
要进行强制转换。
下面,我们要完成数据接收线程入口函数的编写。我们可以按照本章前面介绍的方法,把线程入口
函数定义为一个全局函数,但是在实际工作中,有些单位要求在面向对象的编程中不能使用全局函
数,所有的数据成员和方法都必须封装到类中,那么是否可以将线程函数定义为类的成员函数呢?
我们可以试验一下,按照前面介绍的线程入口函数的写法为 CChatDialog类增加一个成员函数:
RecvProc,并在此函数中添加一条简单的 return语句,结果如例 15-13所示。
例 15-13
DWORD WINAPI CChatDlg : : RecvProc (LPVOID lpPararneter)
{
return 0 ;
然后编译 Chat程序,编译器将报告如下错误:
D:\VC++深入编程\Chapter15 \Chat\ChatDlg.cpp(123) : error C2664: 'CreateThread':
cannot convert pararneter 3 frorn 'unsigned long (void *)' to 'unsigned long ( stdcall
*) (void *)'
None of the functions with this name in scope match the target type
该错误是说, CreateThread函数不能将第三个参数从 unsigned long (void *)类型转换为
unsigned long Lstdcall *)(void *)类型。因为当创建线程时,系统,即运行时代码会调用线程函
数来启动线程。因为这里的线程函数是 CChatDialog类的成员函数,为了调用这个函数,必须先产
生一个 CChatDialog类的对象,然后才能调用该对象内部的成员函数。然而对于运行时代码来说,
它如何知道要产生哪一个对象呢?也就是说,运行时根本不知道如何去产生一个 CChatDialog类的对
象。对于运行时代码来说,如果要调用线程函数来启动某个线程的话,应该不需要产生某个对象就
可以调用这个线程函数。然而这里我们错误地将这个线程函数定义为类的成员函数,所以就出错了。
可以这样来解决这个问题:将线程函数声明为类的静态函数,即在 CChatDialog类的头文件中,在
RecvProc函数的声明的最前面添加关键词: static,这时该函数的声明代码如下所示 :
static DWORD WINAPI RecvProc(LPVOID lpParameter);
因为对类的静态函数而言,它不属于该类的任一个对象,它只属于类本身。所以在 CChatDialog类
的 OnlnitDialog函数中创建线程时,运行时代码就可以直接调用 CChatDialog类的静态函数,从而
启动线程。这时再次编译 Chat程序,将会看到程序成功通过。
通过上例说明,如果单位要求采用完全面向对象的思想来编程,也就是说,不能使用全局函数和全
局变量了,我们就可以采用静态成员函数和静态成员变量的方法来解决上述问题。
下面,为 RecvProc这个线程入口函数添加实现代码以完成接收数据的功能,结果如例 15-14所示。
例 15-14
DWORD WINAPI CChatDlg : : RecvProc (LPVOID lpParameter)
{
//获取主线程传递的套接字和窗口句柄
SOCKET sock=((RECVPARAM*)lpParameter)->sock;
HWND hwnd=( (RECVPARAM*)lpParameter)->hwnd ;
delete lpParameter;
SOCKADDR_IN addrFrom;
int len=sizeof(SOCKADDR) ;
char recvBuf[200] ;
char tempBuf[300] ;
int retval;
while (TRUE)
//接收数据
retval=recvfrom(sock, recvBuf , 200 , 0, (SOCKADDR*)&addrFrom, &len);
if(SOCKET_ERROR==retval)
break;
sprintf(tempBuf , "%s说: %s" , inet_ntoa (addrFrom. sin_addr) , recvBuf);
::PostMessage(hwnd,WM_RECVDATA , 0, (LPARAM}tempBuf) ; return 0 ;
在 Recv Proc这个线程函数中,首先取出主线程传递来的参数,这时应将参数 IpParameter转换为
RECVPARAM*类型,然后再访问该结构体中的成员。
接下来就可以调用 recvfrom函数接收数据了。为了让接收线程能够不断地运行,线程函数 RecvProc
进行了一个 while循环,在此循环中不断地调用 recvfrom函数接收数据,如果该函数调用失败,将
返回 SOCKET ERROR值,那么这时就调用 break语旬,终止 while循环:否则返回接收到的字节数,
这时,可以对接收到的数据进行格式化,并将格式化后的数据传递给对话框,因为我们已经将对话
框句柄作为参数传递给线程了,所以可以采用发送消息的方式将数据传递给对话框。本例调用
PostMessage函数向对话框发送一条自定义的消息: WM_RECVDATA,将参数 wParam设置为 0,而将需
要显示的数据作为lParam参数传递,并将该数据转换为参数 LParam需要的类型 : LP.ARAM。然后在
CChatDLg类的头文件中,定义 WM RECVDATA这个消息的值,即在该头文件中添加下面这条语句:
#define WM_RECVDATA WM_USER+1
并在 CChatDlg类的头文件中编写该消息响应函数原型的声明,即添加如例 15-15所示代码中加灰显
示的那条代码。读者应注意,前面章节曾介绍过,在发送消息时,如果不需要传递参数,那么在定
义消息响应函数时可以没有参数。因为本例在发送 WM RECVDATA消息时需要传递一个参数,所以在
定义该消息响应函数时应带有 wParam和 IParam参数。
例 15-15
II Generated message map functions
11{{AFX_MSG(CChatDlg}
virtual BOOL OnlnitDialog(} ;
afx_msg void OnSysCommand(U工 NT nID, LPARAM lParam} ;
afx_msg void Onpa工 nt ( ) ;
afx_msg HCURSOR OnQueryDraglcon(};
II }}AFX_MSG
afx_msg void OnRecvData(WPARAM wParam, LPARAM lparam}; DECLARE_MESSAGE_MAP(}
接下来,在 CChatDIg类的源文件中添加 WM RECVDATA消息映射,即添加如例 15-16所示代码中加灰
显示的那条代码,注意该代码后不要添加任何标点符号。这里再次提醒读者,因为 CAboutDl g类和
CChatDialog类这两个类的实现代码在同一个源文件中,所以一定要看仔细,注意消息映射的位置,
不能写错位置了。
BEGIN_MESSAGE_MAP(CChatDlg , CDialog} 11{{AFX_MSG_MAP(CChatDlg}
ON_WM_SYSCOMMAND()
ON_WM_PAINT ()
ON_WM_QUERYDRAGICON()
//}}AFX_MSG_MAP
ON_MESSAGE(WM_RECVDATA, OnRecvData)
END_MESSAGE_MAP()
最后就是该消息响应函数的实现,具体代码如例 15-17所示。
例15-17
//接收数据消息响应函数
void CChatDlg : : OnRecvData(WPARAM wParam, LPARAM lParam)
//取出接收到的数据
CString str=(char*)lParam;
CString strTemp;
//获得己有数据
GetDlgItemText(IDC_EDIT_RECV, strTemp) ;
str+="\r\n" ;
str+=strTemp;
//显示所有接收到的数据
SetDlgItemText(IDC_ EDIT_RECV, str) ;
每当接收到新的数据时,应在对话框中接收编辑框的第一行显示该数据,而以前的数据应依次向下
移动。在例 15-17所示OnRecvData函数中,首先定义了一个CString类型的变量:str,用来保存从消
息响应函数的lParam参数取出的数据,即当前接收到的新数据。接着又定义了一个 CString类型的
变量: strTemp,用来保存从编辑框控件中获得的己有文本,即以前接收到的数据。该文本的获得可
以通过调用GetDlgltemText函数来实现。接下来为新收到的数据加上换行符:"\r\n",并加上先前己
有的数据,然后调用 SetDlgItemText函数,将处理后的数据放回到接收数据的编辑框中显示出来。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -