5.4.3 客户端类

  客户端CClient类对每个接受的客户端,为其实现接收数据、计算数据和发送计算结果的功能。

1CClient类的声明

在该类声明如下成员变量。

q        m_socket,接受客户端套接字。该套接字继承了sServer套接字的非阻塞模式属性,作为参数调用recv()send()函数,实现与客户端的数据发送和接收。

q        m_addr,接受客户端的地址。

q        m_data,客户端数据。接收线程和发送数据线程公用该数据变量。

q        m_hEvent,事件对象。当接收线程完成算数表达式的计算后使用该事件对象,通知发送线程发送数据。

q        m_hThreadSend,发送数据线程句柄。

q        m_hThreadRecv,接收数据线程句柄。

q        m_cs,临界区对象。确保接收数据线程和发送数据线程对m_data数据成员的互斥访问。

q        m_bConning,客户端连接状态。

q        m_bExit,线程退出。

该类声明如下成员函数。

q        CClient(),构造函数。以套接字和客户端地址为参数。

q        ~CClient(),析构函数。

q        CClient(),默认构造函数。

q        StartRuning(),创建发送和接收数据线程。

q        HandleData(),计算表达式结果。

q        IsExit (),接收和发送线程是否已经退出。

q        IsConning(),判断与客户端连接状态。

q        DisConning(),断开与客户端连接。

q        RecvDataThread(),接收客户端的数据。

q        SendDataThread(),向客户端发送数据。

CClient类的声明如下。

class CClient

{

public:

         CClient(const SOCKET sClient,const sockaddr_in &addrClient);

         virtual ~CClient();

 

public:

         BOOL                 StartRuning(void);                                                             //创建发送和接收数据线程

         void                     HandleData(const char* pExpr);                                    //计算表达式

         BOOL                 IsConning(void){                                                               //是否连接存在

                                     return m_bConning;

                                     }

         void                     DisConning(void){                                                            //断开与客户端的连接

                                     m_bConning = FALSE;

                                     }

         BOOL                 IsExit(void){                                                                        //接收和发送线程是否已经退出

                                     return m_bExit;

                                     }

 

public:

         static DWORD __stdcall   RecvDataThread(void* pParam);                //接收客户端数据

         static DWORD __stdcall   SendDataThread(void* pParam);               //向客户端发送数据

 

private:

         CClient();

private:

         SOCKET            m_socket;                                                                                    //套接字

         sockaddr_in     m_addr;                                                                              //地址

         DATABUF m_data;                                                                              //数据

         HANDLE            m_hEvent;                                                                          //事件对象

         HANDLE            m_hThreadSend;                                                             //发送数据线程句柄

         HANDLE            m_hThreadRecv;                                                              //接收数据线程句柄

         CRITICAL_SECTION m_cs;                                                                       //临界区对象

         BOOL                 m_bConning;                                                                    //客户端连接状态

         BOOL                 m_bExit;                                                                              //线程退出

};

2.构造函数和析构函数

该类声明了两个构造函数。默认构造函数生命为private类型,没有实现。另一个构造函数以接受的套接字和客户端地址为参数。在该构造函数中完成初始化成员变量,创建事件对象,初始化临界区的功能。构造函数程序清单如下。

CClient::CClient(const SOCKET sClient, const sockaddr_in &addrClient)

{

         //初始化变量

         m_hThreadRecv = NULL;

         m_hThreadSend = NULL;

         m_socket = sClient;

         m_addr = addrClient;

         m_bConning = FALSE;

         m_bExit = FALSE;

         memset(m_data.buf, 0, MAX_NUM_BUF);

         m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);  //手动设置信号状态,初始化为无信号状态

         InitializeCriticalSection(&m_cs);                                            //初始化临界区

}

在析构函数中,清理在占用的资源。包括关闭接受的套接字,释放保护数据的临界区对象,释放事件对象。程序清单如下。

CClient::~CClient()

{

         closesocket(m_socket);                      //关闭套接字

         m_socket = INVALID_SOCKET;        //套接字无效

         DeleteCriticalSection(&m_cs);          //释放临界区对象

         CloseHandle(m_hEvent);                            //释放事件对象

}

3.创建接收数据和发送数据线程

StartRuning()函数实现创建接收数据线程和发送数据线程。在调用CreateThread函数时,以this作为该函数的第四个参数。这样就将该实例的地址传递给RecvDataThread()函数和SendDataThread()函数。那么在这两个函数中,就可以通过该地址调用CClient类的成员函数了。在成功创建线程后,调用CloseHandle()函数将该线程句柄的引用记数减1。该函数的程序清单如下。

/*

 * 创建发送和接收数据线程

 */

BOOL CClient::StartRuning(void)

{

         m_bConning = TRUE;                                                     //设置连接状态

 

         //创建接收数据线程

         unsigned long ulThreadId;

         m_hThreadRecv = CreateThread(NULL, 0, RecvDataThread, this, 0, &ulThreadId);

         if(NULL == m_hThreadRecv)

         {

                   return FALSE;

         }else{

                   CloseHandle(m_hThreadRecv);

         }

 

         //创建接收数据线程

         m_hThreadSend =  CreateThread(NULL, 0, SendDataThread, this, 0, &ulThreadId);

         if(NULL == m_hThreadSend)

         {

                   return FALSE;

         }else{

                   CloseHandle(m_hThreadSend);

         }

        

         return TRUE;

}

4.收数据

RecvDataThread()函数实现接收客户端数据。该函数是CClient类的静态成员函数。在该函数中将pParam参数强制转换为CClient类型的指针,这样的转换是合理的。因为pParam参数,是在调用CreateThread()函数时,传递的this指针。这个this指针就是在AcceptThread()函数中调用new,创建CClient类实例的地址。在RecvDataThread()函数中,使用该指针访问CClient类的他成员变量和函数。

RecvDataThread()函数是一个for语句循环体。当pClient->m_bConning值为True时,该函数处于循环状态,不断地接收客户端的数据。当pClient->m_bConning值为FALSE时,结束循环。该客户端的接收数据线程退出。

在该函数中定义了一个临时temp字符数组,长度为MAX_NUM_BUF大小,用于接收数据客户端的数据。以m_socket套接字、temp数组、MAX_NUM_BUF0为参数,调用recv()函数接收数据。当在接收数据缓冲区内没有可读数据时,recv()函数返回WSAEWOULDBLOCK错误代码。在程序中调用continue语句,继续接收客户端的数据。

因为数据包的包头为4个字节,所以当收到客户端发送的数据时,recv()函数返回值应该为大于4字节的长度。收到客户端的数据后,调用HandleData()函数计算数据,然后调用SetEvent()函数,通知发送数据线程,发送数据。

当客户端关闭了与服务器的连接时,recv()函数返回值为0。程序跳出for循环体,接收数据线程退出。在RecvDataThread()函数返回之前,修改与客户端连接状态m_bConning变量的值为FALSE,并调用SetEvent()函数通知发送数据线程退出。RecvDataThread()函数的程序清单如下。

#define     MAX_NUM_BUF                           48                                 //缓冲区的最大长度

 

/*

 * 接收客户端数据

 */

DWORD  CClient::RecvDataThread(void* pParam)

{

         CClient *pClient = (CClient*)pParam;                                             //客户端对象指针

         int              reVal;                                                                          //返回值

         char temp[MAX_NUM_BUF];                                                    //临时变量

 

         memset(temp, 0, MAX_NUM_BUF);

        

         for (;pClient->m_bConning;)                                                    //连接状态

         {

                   reVal = recv(pClient->m_socket, temp, MAX_NUM_BUF, 0);      //接收数据

                  

                   //处理错误返回值

                   if (SOCKET_ERROR == reVal)

                   {

                            int nErrCode = WSAGetLastError();

 

                            if ( WSAEWOULDBLOCK == nErrCode )                               //接受数据缓冲区不可用

                            {

                                     continue;                                                                             //继续循环

                            }else if (WSAENETDOWN == nErrCode ||                             //客户端关闭了连接

                                                WSAETIMEDOUT == nErrCode ||

                                               WSAECONNRESET == nErrCode )

                            {

                                     break;                                                                                  //线程退出

                            }

                   }

                  

                   //客户端关闭了连接

                   if ( reVal == 0)  

                   {

                            break;

                   }

 

                   //收到数据

                   if (reVal > HEADERLEN)

                   {

                            pClient->HandleData(temp);                                                   //处理数据

 

                            SetEvent(pClient->m_hEvent);                                                //通知发送数据线程

 

                            memset(temp, 0, MAX_NUM_BUF);                                       //清空临时变量

                   }

 

                   Sleep(TIMEFOR_THREAD_CLIENT);                                             //线程睡眠

         }

        

         pClient->m_bConning = FALSE;                                                               //与客户端的连接断开

 

         SetEvent(pClient->m_hEvent);                                                                   //通知发送数据线程退出

 

         return 0;                                                                                                          //退出

}

5.计算数据

5.2节中定义了发送数据的格式。发送的数据包由包头和数据两部分组成。包头的type字段指明了该数据类型,有消息和算数表达两种。“E”代表算术表达式,“B”代表消息。包头的len字段指明了整个包的长度。客户端和服务器都使用该结构。数据包的定义如下所示。

//数据包类型

#define EXPRESSION                         'E'                        //算数表达式

#define BYEBYE                                   'B'                        //消息byebyeByebye,OK

#define HEADERLEN                          (sizeof(hdr))      //头长度

 

//数据包头结构,该结构在win32下为4byte

typedef struct _head

{

         char                    type;                                                //类型

         unsigned short          len;                                                 //数据包的长度(包括头的长度)

}hdr, *phdr;

 

//数据包中的数据结构

typedef struct _data

{

         char buf[MAX_NUM_BUF];                                     //数据

}DATABUF, *pDataBuf;

HandleData()函数实现对接收数据计算的功能。当接收的数据是消息时,修改包头的长度,并且用“OK”替换原来的“Byebye”或者“byebye”。最后将包头和消息到m_data变量中。

当接收到算数表达时,对数据的处理过程比较复杂。图为计算算数表达式的过程。

HandleData()函数中,使用sscanf()函数读取接收数据包中的数字和运算符。使用sprintf()函数将算数表达式和计算结果写入temp临时字符数组。然后,修改数据包头中len值为包头长度与temp字符串长度之和。最后,将temp字符数组中的数据复制到m_data变量中。HandleData()函数的程序清单如下。

/*

 *  计算表达式,打包数据

 */

void CClient::HandleData(const char* pExpr)  

{

         memset(m_data.buf, 0, MAX_NUM_BUF);//清空m_data

        

    //如果是“byebye”或者“Byebye

         if (BYEBYE == ((phdr)pExpr)->type)

         {

                  EnterCriticalSection(&m_cs);

                   phdr pHeaderSend = (phdr)m_data.buf;                               //发送的数据

                   pHeaderSend->type = BYEBYE;                                             //单词类型

                   pHeaderSend->len = HEADERLEN + strlen("OK");            //数据包长度

                   memcpy(m_data.buf + HEADERLEN, "OK", strlen("OK"));         //复制数据到m_data"

                   LeaveCriticalSection(&m_cs);

 

         }else{//算数表达式

 

                   int nFirNum;                                                                                //第一个数字

                   int nSecNum;                                                                              //第二个数字

                   char cOper;                                                                                  //算数运算符

                   int nResult;                                                                                  //计算结果

                   //格式化读入数据

                   sscanf(pExpr + HEADERLEN, "%d%c%d", &nFirNum, &cOper, &nSecNum);

 

                   //计算

                   switch(cOper)

                   {

                   case '+':                                                                                        //

                            {

                                     nResult = nFirNum + nSecNum;

                                     break;

                            }

                   case '-':                                                                                         //

                            {

                                     nResult = nFirNum - nSecNum;

                                     break;

                            }

                   case '*':                                                                                         //

                            {

                                     nResult = nFirNum * nSecNum;       

                                     break;

                            }

                   case '/':                                                                                         //

                            {

                                     if (ZERO == nSecNum)                                          //无效的数字

                                     {

                                               nResult = INVALID_NUM;

                                     }else

                                     {

                                               nResult = nFirNum / nSecNum;       

                                     }

                                     break;

                            }

                   default:

                            nResult = INVALID_OPERATOR;                                  //无效操作符

                            break;

                   }

 

                   //将算数表达式和计算的结果写入字符数组中

                   char temp[MAX_NUM_BUF];

                   char cEqu = '=';

                   sprintf(temp, "%d%c%d%c%d",nFirNum, cOper, nSecNum,cEqu, nResult);

 

                   //打包数据

                   EnterCriticalSection(&m_cs);

                   phdr pHeaderSend = (phdr)m_data.buf;                               //发送的数据

                   pHeaderSend->type = EXPRESSION;                                   //数据类型为算数表达式

                   pHeaderSend->len = HEADERLEN + strlen(temp);           //数据包的长度

                   memcpy(m_data.buf + HEADERLEN, temp, strlen(temp));       //复制数据到m_data

                   LeaveCriticalSection(&m_cs);

 

         }

}

6.发送数据

SendDataThread()函数实现向客户端发送计算表达式结果的功能。该函数在结构上与RecvDataThread()函数结构相似,也是一个for语句的循环体。

在该循环体中,以m_hEvent事件对象句柄和INFINITE为参数调用WaitForSingleObject()函数。该函数只有当接收到发送数据线程的m_hEvent事件通知时,才会返回。当该函数返回时,说明接收数据线程已经完成了对数据的计算。

接下来调用send()函数完成数据的发送。该函数的第一个参数是接收的客户端m_socket套接字,第二个参数是要发送的m_data数据,第三个参数是nSendlen数据长度。当发送缓冲区不可用时,该函数返回WSAEWOULDBLOCK错误代码。在程序中调用continue语句继续发送数据,直到成功为止。

客户端结束请求时,该线程很难发觉到。因为程序阻塞于WaitForSingleObject()函数的调用。然而,接收数据线程可以根据recv()函数的返回值为0,确定客户端已经结束请求。

为防止客户端结束请求时,发送数据线程阻塞于WaitForSingleObject()函数的调用。在接收数据线程退出时,给发送数据线程事件通知,以促使该函数返回。此时,连接状态m_bConning变量值为FALSE,表明客户端已经结束请求,发送数据线程退出。该线程退出时,设置m_bExit变量为TRUE,表明接收和发送数据线程都已经退出。

通过以上程序设计,保证了接收和发送数据线程,在客户端结束请求时安全退出。SendDataThread()函数的程序清单如下。

/*

 * //向客户端发送数据

 */

DWORD CClient::SendDataThread(void* pParam) 

{

         CClient *pClient = (CClient*)pParam;                                   //转换数据类型为CClient指针

 

         for (;pClient->m_bConning;)                                          //连接状态

         {       

                   //收到事件通知

                   if (WAIT_OBJECT_0 == WaitForSingleObject(pClient->m_hEvent, INFINITE))

                   {

                            //当客户端的连接断开时,接收数据线程先退出,然后该线程后退出,并设置退出标志

                            if (!pClient->m_bConning)

                            {

                                     pClient->m_bExit = TRUE;

                                     break ;

                            }

 

                            //进入临界区

                            EnterCriticalSection(&pClient->m_cs);

                            //发送数据

                            phdr pHeader = (phdr)pClient->m_data.buf;

                            int nSendlen = pHeader->len;

 

                            int val = send(pClient->m_socket, pClient->m_data.buf, nSendlen,0);

                            //处理返回错误

                            if (SOCKET_ERROR == val)

                            {

                                     int nErrCode = WSAGetLastError();

                                     if (nErrCode == WSAEWOULDBLOCK)                       //发送数据缓冲区不可用

                                     {

                                               continue;

                                     }else if ( WSAENETDOWN == nErrCode ||

                                                          WSAETIMEDOUT == nErrCode ||

                                                          WSAECONNRESET == nErrCode)        //客户端关闭了连接

                                     {

                                               //离开临界区

                                               LeaveCriticalSection(&pClient->m_cs);

                                               pClient->m_bConning = FALSE;                          //连接断开

                                               pClient->m_bExit = TRUE;                                    //线程退出

                                               break;

                                     }else {

                                               //离开临界区

                                               LeaveCriticalSection(&pClient->m_cs);

                                               pClient->m_bConning = FALSE;                          //连接断开

                                               pClient->m_bExit = TRUE;                                    //线程退出

                                               break;

                                     }

                            }

                            //成功发送数据

                            //离开临界区

                            LeaveCriticalSection(&pClient->m_cs);

                            //设置事件为无信号状态

                            ResetEvent(&pClient->m_hEvent);

                   }

         }

 

         return 0;

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值