5.4.3 客户端类

来源:互联网 发布:威新软件科技园 编辑:程序博客网 时间:2024/06/06 07:37

  客户端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;

}

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 被刺梅的刺扎了怎么办 被刺梅的刺划手了怎么办 虎刺花叶子黄了怎么办 地栽月季叶子发黄怎么办 梅花浇水浇多了怎么办 深水井底下花管进水量小怎么办 盆景对节白腊树叶尖干枯怎么办 四季海棠茎软了怎么办 玫瑰海棠烂根了怎么办 格丽海棠花腌了怎么办 养殖场不能雨污分流怎么办 药店买的药贵了怎么办 神经损伤小便少尿不出来怎么办 手指夹伤出血了怎么办 手指夹破流血了怎么办 喂了宝宝熊胆粉怎么办 不小心擦伤了皮怎么办 吃了减肥药拉肚子怎么办 遇到他心通的人怎么办 被茅山术害了怎么办 鱼缸鱼身上烂了怎么办 鳄鱼龟皮肤烂了怎么办 墨水渗透进皮肤里了怎么办 中药渗透到皮肤里怎么办 甘露醇渗透到皮肤下怎么办 水银弄到眼睛里怎么办 水银粘到皮肤上怎么办 榴莲和虾同吃了怎么办 吃榴莲和虾中毒怎么办 榴莲和虾一起吃怎么办 狗被别人下毒了怎么办 大掌门2没存元宝怎么办 大掌门2转换阵容怎么办 率土之滨s2绝版怎么办 异界气息的装备怎么办 vivo电板没电了怎么办 门套拼接有缝隙怎么办 公司如果一直没有上税收入怎么办 赛车输了俩百万怎么办 交pk金员工不交怎么办 员工不想交pk金怎么办