5.6.1主线程

主线程包括初始化、连接服务器、创建数据接收和发送线程、输入数据和显示结果,以及退出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函数初始化csSendcsRecv临界区对象。设置bufSendbufRecv数据缓冲区为零。以TUREFALSECreateEvent()函数的第二个和第三个参数,创建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错误外,还返回WSAEINVALWSAEISCONN错误。其含义和返回的顺序如表所示。当第三次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()循环体。只要与服务器的连接bConnectingTRUE,就执行图所示的循环过程。用户输入数据,然后对数据打包,最后显示数据。

因为用户输入的第一个数据必须是算数表达式,所以使用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=”算数表达式时,pos1pos2pos3指针的位置。

如果用户输入一个符合要求的算术表达式,那么对该表达式打包。将发送数据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()函数释放在初始化时申请的资源,释放数据接收和发送线程句柄。该函数实现简单,其代码请看该书的配套光盘。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值