5.4.3 客户端类
来源:互联网 发布:威新软件科技园 编辑:程序博客网 时间:2024/06/06 07:37
客户端CClient类对每个接受的客户端,为其实现接收数据、计算数据和发送计算结果的功能。
1.CClient类的声明
在该类声明如下成员变量。
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_BUF和0为参数,调用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' //消息byebye,Byebye,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;
}
- 5.4.3 客户端类
- 客户端访问WebService(3)
- 网游客户端随想---3
- javascript-3探索客户端
- 客户端网页编程3
- missian(3)同步客户端
- FTP客户端类
- 客户端通讯类
- 爬虫客户端类
- webService客户端工具类
- MongoDB客户端工具类
- Memcached客户端utils类
- zookeeper客户端工具类
- Tcp客户端接口类
- 客户端
- 客户端
- 客户端
- 客户端
- 写得蛮好的linux学习笔记 [转] 四其他
- 5.4.2接受客户端请求线程
- Linux 内核NAPI机制分析【转】
- 第一步,设置环境
- SOA服务接口设计最佳实践
- 5.4.3 客户端类
- 运算符的优先级别
- 用AJAX获取二级、三级下拉列表(例子)
- UDP 发包程序
- 如何将Exchange OWA http重定向到https
- Zen Cart的模板设计
- php 表单验证类
- Portal开源实现-Liferay的Portlet Session处理
- 5.4.4清理资源线程