我们看一个综合例子:使用MFC来实现一个网络聊天软件。看到这个例子有人可能觉得奇怪,前面网络编程时讲到过一个类似的控制台应用程序的例子,为什么要放在这里?原因在于,前面的那个例子,我们必须是一个人说完就得等另一个人说,不能自己连续说,这是由它的实现代码决定的;而我们这里想实现的是“自由”的对话,可以在任意时间发送或者接收数据。这就需要我们这一小节的知识来帮忙了:我们可以利用一个线程来实现接收消息。
下面我们一步步开始完成,先设计程序的外观:首先使用一个对话框应用程序,然后将对话框上的默认按钮“确认”和“取消”都删除。然后为它加上下面的控件:
完成了外观设计之后,我们就要开始编写网络程序了,回忆一下基于UDP的socket编写的步骤:
服务器端:
1.加载套接字库,协商版本号
2.创建套接字。
3.绑定端口
4.发送/接收数据
5.关闭套接字
6.释放资源
客户端:
1.加载套接字库,协商版本号
2.创建套接字
3.发送/接收消息
4.关闭套接字
5.释放资源
接着,为从CDialog派生下来的两个类中的那个不是CAboutDlg的类(CAboutDlg是用来产生关于对话框的这里没有用)增加一个私有SOCKET类型的成员变量m_socket和BOOL类型的成员函数InitSocket,用来初始化套接字:
下面实现接收功能。如果没有数据到来,recvfrom函数会阻塞,从而导致程序暂停运行。所以我们可将接收数据的操作放置在一个单独的线程中完成,并给这个线程传递两个参数,一个是套接字,一个是对话框的句柄,这样,当接收导数据后,可以将该数据传给对话框,经过处理之后显示在接收编辑框控件上。但是,我们知道CreateThread函数中只有第4个参数可以用来给创建的线程传递参数,这该怎么办呢?我们发现第4个参数是一个LPVOID,而他的实际类型是(void *)也就是一个空类型的指针,所以我们可以定义一个结构体,这个结构体中包含了前面说的2个参数,然后把这个结构体的地址传给函数即可。
我们先在这个类的头文件中定义一个结构体:
然后在OnInitDialog中完成对结构体的赋值并创建线程:
下面我们一步步开始完成,先设计程序的外观:首先使用一个对话框应用程序,然后将对话框上的默认按钮“确认”和“取消”都删除。然后为它加上下面的控件:
控件名称 ID 作用
组框 IDC_STATIC 标识:接收数据
接收编辑框 IDC_EDIT_RECV 显示所有聊天数据
发送组框 IDC_STATIC 标识:发送数据
IP地址空间 IDC_IPADDRESS1 允许用户输入点分十进制IP
发送编辑框 IDC_EDIT_SEND 允许用户输入发送的内容
发送按钮 IDC_BTN_SEND 点击后把信息发送到指定IP上
完成了外观设计之后,我们就要开始编写网络程序了,回忆一下基于UDP的socket编写的步骤:
服务器端:
1.加载套接字库,协商版本号
2.创建套接字。
3.绑定端口
4.发送/接收数据
5.关闭套接字
6.释放资源
客户端:
1.加载套接字库,协商版本号
2.创建套接字
3.发送/接收消息
4.关闭套接字
5.释放资源
MFC为我们提供了AfxSocketInit函数来实现加载套接字,协商版本号的功能,由MSDN可知,我们应该把它放在我的自己应用程序类的InitInstance中:
BOOL CCH_15_CHATApp::InitInstance()
{
if(!AfxSocketInit)
{
AfxMessageBox("加载套接字失败");
return FALSE;
}
//其他代码省略
}
注意如果要使用这个函数,必须包含Afxsock.h这个头文件。我们应该把这头文件包含在stdafx.h中。stdafx.h是一个预编译头文件,里面包含了MFC应用程序所需的一些必要的头文件。
接着,为从CDialog派生下来的两个类中的那个不是CAboutDlg的类(CAboutDlg是用来产生关于对话框的这里没有用)增加一个私有SOCKET类型的成员变量m_socket和BOOL类型的成员函数InitSocket,用来初始化套接字:
BOOL CCH_15_CHATDlg::InitSocket()
{
m_socket = socket(AF_INET,SOCK_DGRAM,0);
if(INVALID_SOCKET == m_socket)
{
MessageBox("创建套接字失败");
return FALSE;
}
SOCKADDR_IN addrSock;
addrSock.sin_family = AF_INET;
addrSock.sin_port = htons(6000);
addrSock.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
int retval;
retval = bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR));
if(SOCKET_ERROR == retval)
{
closesocket(m_socket);
MessageBox("绑定失败!");
return FALSE;
}
return TRUE;
}
我们这个程序既要接收又要发送,对于接收程序来说,需要指定使用那个端口接收,接收什么IP的消息。我们应该在OnInitDialog中调用这个函数,完成套接字的初始化。
下面实现接收功能。如果没有数据到来,recvfrom函数会阻塞,从而导致程序暂停运行。所以我们可将接收数据的操作放置在一个单独的线程中完成,并给这个线程传递两个参数,一个是套接字,一个是对话框的句柄,这样,当接收导数据后,可以将该数据传给对话框,经过处理之后显示在接收编辑框控件上。但是,我们知道CreateThread函数中只有第4个参数可以用来给创建的线程传递参数,这该怎么办呢?我们发现第4个参数是一个LPVOID,而他的实际类型是(void *)也就是一个空类型的指针,所以我们可以定义一个结构体,这个结构体中包含了前面说的2个参数,然后把这个结构体的地址传给函数即可。
我们先在这个类的头文件中定义一个结构体:
struct RECVPARAM
{
SOCKET sock;
HWND hwnd;
};
然后在OnInitDialog中完成对结构体的赋值并创建线程:
RECVPARAM *pRecvParam = new RECVPARAM;
pRecvParam->sock = m_socket;
pRecvParam->hwnd = m_hWnd;
HA