主线程包括初始化、连接服务器、创建数据接收和发送线程、输入数据和显示结果,以及退出5个部分。其程序流程图,如图所示。
客户端的主线程函数清单如下。InitClient()函数完成初始化全局变量和创建非阻塞套接字。如果InitClient()函数失败,则main()函数退出并返回CLIENT_SETUP_FAIL错误代码。
ConnectServer()函数实现接服务器的功能。CreateSendAndRecvThread()函数实现创建数据接收和发送线程,如果该函数失败返回CLIENT_CREATETHREAD_FAIL错误代码。当CreateSendAndRecvThread()函数成功返回后,客户端存在三个线程:主线程、数据发送和数据接收线程。此时用户就可以输入数据进行远程计算了。
InputAndOutput()函数用于提示信息、接收用户输入和显示数据结果。
ExitClient()函数在客户端退出时清理主线程资源。
#define CLIENT_SETUP_FAIL 1 //启动客户端失败
#define CLIENT_CREATETHREAD_FAIL 2 //创建线程失败
int main(int argc, char* argv[])
{
//初始化
if (!InitClient())
{
ExitClient();
return CLIENT_SETUP_FAIL;
}
//连接服务器
if (ConnectServer())
{
ShowConnectMsg(TRUE);
}else{
ShowConnectMsg(FALSE);
ExitClient();
return CLIENT_SETUP_FAIL;
}
//创建发送和接收数据线程
if (!CreateSendAndRecvThread())
{
ExitClient();
return CLIENT_CREATETHREAD_FAIL;
}
//用户输入数据和显示结果
InputAndOutput();
//退出
ExitClient();
return 0;
1.初始化
客户端是一个Win32 Console Application程序,在程序中使用一些全局变量。其中包括以下几方面。
q sClient,客户端套接字。
q hThreadSend,发送数据线程句柄。
q hThreadRecv,接收数据线程句柄。
q bufSend,发送数据缓冲区。
q bufRecv,接收数据缓冲区。
q csSend,临界区对象,确保主线程和发送数据线程对bufSend变量的互斥访问。
q csRecv,临界区对象,确保主线程与接收数据线程对bufRecv变量的互斥访问。
q bSendData,通知发送数据线程发送数据的布尔变量。
q hEventShowDataResult,显示计算结果的事件对象。
q bConnecting,与服务器的连接状态变量。
q rrThread[2],子线程数组。
客户端全局变量声明如下。
SOCKET sClient; //套接字
HANDLE hThreadSend; //发送数据线程
HANDLE hThreadRecv; //接收数据线程
DATABUF bufSend; //发送数据缓冲区
DATABUF bufRecv; //接收数据缓冲区
CRITICAL_SECTION csSend; //临界区对象 锁定bufSend
CRITICAL_SECTION csRecv; //临界区对象 锁定bufRecv
BOOL bSendData; //通知发送数据线程
HANDLE hEventShowDataResult; //显示计算结果的事件
BOOL bConnecting; //与服务器的连接状态
HANDLE arrThread[2]; //子线程数组
客户端的初始化包括全局变量的初始化和创建非阻塞套接字。
InitMember()函数完成全局变量的初始化。调用InitializeCriticalSection函数初始化csSend和csRecv临界区对象。设置bufSend和bufRecv数据缓冲区为零。以TURE和FALSE为CreateEvent()函数的第二个和第三个参数,创建hEventShowDataResult事件对象。将该事件对象设置为手动方式且初始为无信号状态。InitMember()函数的程序清单如下。
/**
* 初始化全局变量
*/
void InitMember(void)
{
//初始化临界区
InitializeCriticalSection(&csSend);
InitializeCriticalSection(&csRecv);
sClient = INVALID_SOCKET; //套接字
hThreadRecv = NULL; //接收数据线程句柄
hThreadSend = NULL; //发送数据线程句柄
bConnecting = FALSE; //为连接状态
bSendData = FALSE; //不发送数据状态
//初始化数据缓冲区
memset(bufSend.buf, 0, MAX_NUM_BUF);
memset(bufRecv.buf, 0, MAX_NUM_BUF);
memset(arrThread, 0, 2);
//手动设置事件,初始化为无信号状态
hEventShowDataResult = (HANDLE)CreateEvent(NULL, TRUE, FALSE, NULL);
}
InitSockt()函数实现创建非阻塞套接字。函数中在创建套接字之后,调用ioctlsocket()函数创建非阻塞套接字。在数据接收和发送线程中,使用该套接字作为参数,调用recv()和send()函数接收和发送数据。该函数程序清单如下。
/**
* 创建非阻塞套接字
*/
BOOL InitSockt(void)
{
int reVal; //返回值
WSADATA wsData; //WSADATA变量
reVal = WSAStartup(MAKEWORD(2,2),&wsData); //初始化Windows Sockets Dll
//创建套接字
sClient = socket(AF_INET, SOCK_STREAM, 0);
if(INVALID_SOCKET == sClient)
return FALSE;
//设置套接字非阻塞模式
unsigned long ul = 1;
reVal = ioctlsocket(sClient, FIONBIO, (unsigned long*)&ul);
if (reVal == SOCKET_ERROR)
return FALSE;
return TRUE;
}
2.连接服务器
ConnectServer()函数实现连接服务器的功能。该函数的程序清单如下。在使用非阻塞套接字调用connect()函数时,除了返回WSAEWOULDBLOCK错误外,还返回WSAEINVAL和WSAEISCONN错误。其含义和返回的顺序如表所示。当第三次connect()函数时,返回WSAEISCONN错误代码。此代码说明客户端已经完成与服务器的连接。
表 非阻塞套接字调用connect()函数,返回错误的代码、顺序和含义
顺序 | 代码 | 说明 |
1 | WSAEWOULDBLOCK | 连接未能立即完成 |
2 | WSAEINVAL | 监听状态 |
3 | WSAEISCONN | 连接已经完成 |
注意:使用非阻塞套接字调用connect()函数,返回的错误代码依Windows Sockets的实现而有所不同。当使用非阻塞套接字时,建议最好不要使用这种方法来判断客户端是否成功连接服务器。推荐使用select()函数,或者WSAAsyncselect()函数,或者WSAEventselect()函数,来判断连接服务器是否成功。在后面的章节中,讲解这些函数。
/**
* 连接服务器
*/
BOOL ConnectServer(void)
{
int reVal; //返回值
sockaddr_in serAddr; //服务器地址
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(SERVERPORT);
serAddr.sin_addr.S_un.S_addr = inet_addr(SERVERIP);
for (;;)
{
//连接服务器
reVal = connect(sClient, (struct sockaddr*)&serAddr, sizeof(serAddr));
//处理连接错误
if(SOCKET_ERROR == reVal)
{
int nErrCode = WSAGetLastError();
if( WSAEWOULDBLOCK == nErrCode || //连接还没有完成
WSAEINVAL == nErrCode)
{
continue;
}else if (WSAEISCONN == nErrCode) //连接已经完成
{
break;
}else //其他原因,连接失败
{
return FALSE;
}
}
if ( reVal == 0 ) //连接成功
break;
}
bConnecting = TRUE;
return TRUE;
}
3.创建数据接收和发送线程
CreateSendAndRecvThread()函数实现创建数据接收和发送线程。与服务器创建线程后对线程句柄的处理不同。在这个函数中,没有立即调用CloseHandle()函数将线程句柄的引用计数减1。当这两个线程都成功创建后,将其保存在arrThread数组中。在后面将看到这两个线程句柄的作用。该函数的程序清单如下。
/**
* 创建发送和接收数据线程
*/
BOOL CreateSendAndRecvThread(void)
{
//创建接收数据的线程
unsigned long ulThreadId;
hThreadRecv = CreateThread(NULL, 0, RecvDataThread, NULL, 0, &ulThreadId);
if (NULL == hThreadRecv)
return FALSE;
//创建发送数据的线程
hThreadSend = CreateThread(NULL, 0, SendDataThread, NULL, 0, &ulThreadId);
if (NULL == hThreadSend)
return FALSE;
//添加到线程数组
arrThread[0] = hThreadRecv;
arrThread[1] = hThreadSend;
return TRUE;
}
4.用户输入数据和显示结果
InputAndOutput()函数实现用户输入数据和显示计算结果的功能。虽然可以在接收数据线程显示数据结果,但是为了避免与主线程竞争,还是在主线程显示数据结果。这符合将所有界面显示安排在主线程的原则。
该函数是一个for()循环体。只要与服务器的连接bConnecting为TRUE,就执行图所示的循环过程。用户输入数据,然后对数据打包,最后显示数据。
因为用户输入的第一个数据必须是算数表达式,所以使用bFirstInput布尔变量作为判断是否成功第一次输入算数表达式的条件。并且依据此条件,为用户显示不同输入提示信息。请参考客户端界面设计插图。当用户成功输入第一个算数表达式后,该变量值为TRUE。后续的数据输入既可是数据也可以是“Byebye”或者“byebye”消息。
ShowTipMsg()函数为用户显示不同提示信息的功能。PackExpression()函数和PackByebye()函数实现对数据的打包。ShowDataResultMsg()函数显示数据结果。
数据打包后,通知发送数据线程将数据包发送出去,然后调用WaitForSingleObject()函数等待数据结果。当接收数据线程收到数据结果,设置hEventShowDataResult事件对象为有信号状态,该函数返回,显示数据结果。
当客户端主动退出时,将收到“OK”消息。设置bConnecting变量为FALSE,然后线程睡眠。数据接收函数和数据发送函数以bConnecting变量为退出条件。当该变量值为FALSE时,该函数退出。
为了做到主线程最后退出,在InputAndOutput()函数中,使用arrThread数组,作为参数调用WaitForMultipleObjects()函数。只由当数据接收和发送线程都退出时,该函数才返回。这就确保了主线程的最后退出。
使用CreateThread()函数创建的线程,在运行时是处于无信号状态的。当线程退出时,该线程变为有信号状态。使用该线程句柄作为WaitForSingleObject()或者WaitForMultipleObjects()函数的参数,当线程退出时,该函数返回。
InputAndOutput()函数的程序清单如下。
/**
* 输入数据和显示结果
*/
void InputAndOutput(void)
{
char cInput[MAX_NUM_BUF]; //用户输入缓冲区
BOOL bFirstInput = TRUE; //第一次只能输入算数表达式
for (;bConnecting;) //连接状态
{
memset(cInput, 0, MAX_NUM_BUF);
ShowTipMsg(bFirstInput); //提示输入信息
cin >> cInput; //输入表达式
char *pTemp = cInput;
if (bFirstInput) //第一次输入
{
if (!PackExpression(pTemp)) //算数表达式打包
{
continue; //重新输入
}
bFirstInput = FALSE; //成功输入第一个算数表达式
}else if (!PackByebye(pTemp)) //“Byebye”“byebye”打包
{
if (!PackExpression(pTemp)) //算数表达式打包
{
continue; //重新输入
}
}
//等待显示计算结果
if (WAIT_OBJECT_0 == WaitForSingleObject(hEventShowDataResult, INFINITE))
{
ResetEvent(hEventShowDataResult); //设置为无信号状态
if (!bConnecting) //客户端被动退出,此时接收和发送数据线程已经退出
{
break;
}
ShowDataResultMsg(); //显示数据结果
if (0 == strcmp(bufRecv.buf, "OK")) //客户端主动退出
{
bConnecting = FALSE;
Sleep(TIMEFOR_THREAD_EXIT); //给数据接收和发送线程退出时间
}
}
}
if (!bConnecting) //与服务器连接已经断开
{
ShowConnectMsg(FALSE); //显示信息
}
//等待数据发送和接收线程退出
DWORD reVal = WaitForMultipleObjects(2, arrThread, TRUE, INFINITE);
if (WAIT_ABANDONED_0 == reVal)
{
int nErrCode = GetLastError();
}
}
PackExpression()函数对用户输入算数表达的有效性进行判断,完成对数据的打包功能。算数表达式的第一个数字可以带有正负号,所以在该函数开始,对此进行了判断。通过移动pTemp指针遍历整个表达式,依次找到第一个数字位置,运算符位置,第二个数字位置,并计算他们各自的长度。如图所示,当用户输入“12+34=”算数表达式时,pos1、pos2和pos3指针的位置。
如果用户输入一个符合要求的算术表达式,那么对该表达式打包。将发送数据bufSend变量的地址强制转换为phdr类型,定义该表达式数据类型为EXPRESSION,包的长度为包头与表达式长度之和。使用memcpy()函数将算数表达式复制到包头的后面,没有复制“=”符号。如图所示,将“12+34=”算数表达式打包后的数据包。
bSendData变量值为TRUE,通知发送数据线程发送数据。PackExpression()函数清单如下。
数据打包后,修改
//数据包类型和包头长度
#define EXPRESSION 'E' //算数表达式
#define BYEBYE 'B' //消息byebye
#define HEADERLEN (sizeof(hdr)) //头长度
/**
* 打包计算表达式的数据
*/
BOOL PackExpression(const char *pExpr)
{
char* pTemp = (char*)pExpr; //算数表达式数字开始的位置
while (!*pTemp) //第一个数字位置
pTemp++;
char* pos1 = pTemp; //第一个数字位置
char* pos2 = NULL; //运算符位置
char* pos3 = NULL; //第二个数字位置
int len1 = 0; //第一个数字长度
int len2 = 0; //运算符长度
int len3 = 0; //第二个数字长度
//第一个字符是+ - 或者是数字
if ((*pTemp != '+') &&
(*pTemp != '-') &&
((*pTemp < '0') || (*pTemp > '9')))
{
return FALSE;
}
if ((*pTemp++ == '+')&&(*pTemp < '0' || *pTemp > '9')) //第一个字符是'+',第二个是数字
return FALSE; //重新输入
--pTemp; //上移指针
if ((*pTemp++ == '-')&&(*pTemp < '0' || *pTemp > '9')) //第一个字符是'-',第二个是数字
return FALSE; //重新输入
--pTemp; //上移指针
char* pNum = pTemp; //数字开始的位置
if (*pTemp == '+'||*pTemp == '-') //+ -
pTemp++;
while (*pTemp >= '0' && *pTemp <= '9') //数字
pTemp++;
len1 = pTemp - pNum; //数字长度
//可能有空格
while(!*pTemp)
pTemp++;
//算数运算符
if ((ADD != *pTemp)&&
(SUB != *pTemp)&&
(MUT != *pTemp)&&
(DIV != *pTemp))
return FALSE;
pos2 = pTemp;
len2 = 1;
//下移指针
pTemp++;
//可能有空格
while(!*pTemp)
pTemp++;
//第2个数字位置
pos3 = pTemp;
if (*pTemp < '0' || *pTemp > '9')
return FALSE; //重新输入
while (*pTemp >= '0' && *pTemp <= '9') //数字
pTemp++;
if (EQU != *pTemp) //最后是等于号
return FALSE; //重新输入
len3 = pTemp - pos3; //数字长度
int nExprlen = len1 + len2 + len3; //算数表示长度
//表达式读入发送数据缓冲区
EnterCriticalSection(&csSend); //进入临界区
//数据包头
phdr pHeader = (phdr)(bufSend.buf);
pHeader->type = EXPRESSION; //类型
pHeader->len = nExprlen + HEADERLEN; //数据包长度
//拷贝数据
memcpy(bufSend.buf + HEADERLEN, pos1, len1);
memcpy(bufSend.buf + HEADERLEN + len1, pos2, len2);
memcpy(bufSend.buf + HEADERLEN + len1 + len2 , pos3,len3);
LeaveCriticalSection(&csSend); //离开临界区
pHeader = NULL;
bSendData = TRUE; //通知发送数据线程发送数据
return TRUE;
}
提示用户输入ShowTipMsg()函数、显示数据结果ShowDataResultMsg()函数和对“Byebye”消息打包PackExpression()函数的实现比较简单,其代码请看该书的配套光盘。
4.退出
客户端退出时,ExitClient()函数释放在初始化时申请的资源,释放数据接收和发送线程句柄。该函数实现简单,其代码请看该书的配套光盘。