图10-3是一个多线程版的TCP一对多通信程序,该程序分为服务器端和客户端。服务器端能够同时接受多个客户端的连接,并能同时接收多个客户端发来的消息,其次,服务器端还能将消息群发给所有已连接的客户端。
图10-3 TCP多线程版一对多通信程序
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通信客户端作为客户端程序。因此本实例只制作服务器端程序。
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节中网络用户登录程序改写成能同时接受多个客户端登录。习题