套接字的非阻塞模式是指套接字在执行操作时,调用的函数不管操作是否完成都会立即返回的工作模式。非阻塞套接字在处理同时建立的多个连接,发送和接收的数据量不均,时间不定等方面具有明显的优势。但这种套接字在使用上存在一定难度。本章讲述套接字的非阻塞模式及其一个远程算数运算套接字程序。
套接字的非阻塞模式
所有windows平台都支持套接字以阻塞模式和非阻塞模式的方式工作。
非阻塞模式
把套接字设置为非阻塞模式,即通知系统内核:在调用WindowsSocketsAPI时,不要让线程随眠,而应该让函数理解返回。在返回时,该函数返回一个错误代码。一个非阻塞模式套接字多次调用recv()的过程如上。前3次调用recv()函数时,内核函数还没有准备好。因此,该函数理解返回WSAEWOULDBLOCK错误代码。第4次调用recv()函数时,数据已经准备好,被负责到应用程序的缓冲区中,recv()返回成功指示,应用程序开始处理数据。
设置套接字的非阻塞模式
当使用socket(),WSASocket()创建套接字时,默认都是阻塞的。在创建套接字之后,通过调用ioctlsocket(),将该套接字设置为非阻塞模式。
套接字设置为非阻塞模式后,在调用WindowsSocketsAPI函数时,调用函数会立即返回。大多数情况下,这些函数调用都会调用“失败”,并返回WSAEWOULDBLOCK错误代码。说明请求的操作在调用期间内没有时间完成。通常,应用程序需要重复调用该函数,直到获得成功返回代码。
WSAEWOULDBLOCK的含义 |
|
函数名 |
说明 |
accept(),WSAAcept() |
应用程序没有接收到连接请求 |
recv(),WSARecv(),recvfrom(),WSARecvfrom() |
接收缓冲区没有收到数据 |
send(),WSASend(),sendto(),WSASendto() |
发送缓冲区此时不可用 |
connect(),WSAConnect() |
连接未能立即完成 |
closesocket() |
通常情况下意味着应用程序使用SO_LINGER选项并且设置一个非零的超时值,调用了setsocketopt() |
需要说明的是并非所有的WindowsSocketsAPI在非阻塞模式下调用,都会返回WSEWOULDBLOCK错误。eg:bind(),listen()。
要将套接字设置为非阻塞模式,除了使用ioctlsocket()函数之外,还可以使用WSAAsyncSelect()(第7章讲解),WSAEventselect()(第8章讲解)。当调用该函数时,套接字会自动地设置为非阻塞方式。
非阻塞模式套接字的优势和不足
由于使用非阻塞套接字在调用函数时,会经常返回WSAEWOULDBLOCK错误。所以在任何时候,都应仔细检查返回代码,并做好应对“失败”的准备。应用程序连续不断的调用这个函数,直到它返回成功指示为止。本章程序清单中,在While循环体内不断的调用recv(),以读入1024个字节的数据。这种做法很浪费系统资源。
有人使用MSG_PEEK标志调用recv()查看缓冲区中是否有数据可读。同样,这种方法也不好,因为该做法对系统造成的开销是很大的,并且应用程序至少要调用recv()两次,才能实际地读入数据。较好的做法是,使用套接字的“I/O模型”(第6,7,8,9,10章来讲解)来判断非阻塞套接字是否可读可写。
非阻塞模式套接字与阻塞套接字相比,使用较复杂。使用非阻塞模式套接字,需要编写更多的代码,以便在每个WindowsSocketAPI函数调用中,对收到的WSAEWOULDBLOCK错误进行处理。因此,非阻塞套接字便显得有些难于使用。
但是,非阻塞套接字在控制建立的多个连接,在数据的收发量不均,时间不定等方面,明显具有优势。通常情况下,可考虑使用套接字的“I/O模型”,它有助于应用程序通过异步方法,同时对一个或多个套接字的通信加以管理。
远程算数运算程序
本程序实现一个可以同时为多个客户端提供服务的远程算数运算程序。客户端和服务器端都使用非阻塞套接字。
NoBlockServer
client.h
#pragma once #include <WinSock2.h> #define TIMEFOR_THREAD_CLIENT 500//线程睡眠时间 #define MAX_NUM_CLIENT 10//接受的客户端连接最多数量 #define MAX_NUM_BUF 48//缓冲区的最大长度 #define INVALID_OPERATOR 1 //无效的操作符 #define INVALID_NUM 2 //分母为0 typedef struct _head//数据包包头结构,该结构在win32下位4byte { char type;//类型 unsigned short len;//数据包长度(包括头的长度) }hdr,*phdr; #define HEADERLEN (sizeof(hdr))//头长度 typedef struct _data//数据包中数据结构 { char buf[MAX_NUM_BUF];//数据 }DATABUF,*pDataBuf; //数据包:包头(类型[2byte]+长度[2byte])+数据; class Client { public: Client(const SOCKET sClient,const sockaddr_in &addrClient); virtual ~Client(void); public: BOOL StartRunning();//创建和接收数据线程 void HandleData(const char* pExpr);//计算表达式 BOOL IsConning()//是否连接存在 { return m_bConning; } void DisConning()//断开与客户端的连接 { m_bConning=FALSE; } BOOL IsExit()//发送和接收线程是否已经退出 { return m_bExit; } public: static DWORD _stdcall RecvDataThread(void* pParam);//接收客户端数据 static DWORD _stdcall SendDataThread(void* pParam);//向客户端发送数据 public: SOCKET m_socket;//套接字 sockaddr_in m_addr;//接收客户端的地址 private: DATABUF m_data;//客户端数据;接收线程和发送线程公用该数据变量 HANDLE m_hEvent;//事件对象。当接收线程完成算数表达式的计数后使用该事件对象,通知发送线程发送数据 HANDLE m_hThreadSend;//发送数据线程句柄 HANDLE m_hThreadRecv;//接收数据线程句柄 CRITICAL_SECTION m_cs;//临界区对象。确保接收数据线程和发送数据线程对m_data数据成员的互斥访问 BOOL m_bConning;//客户端连接状态 BOOL m_bExit;//线程退出 };
client.cpp
#include "StdAfx.h" #include <process.h> #include "Client.h" Client::Client(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);//初始化临界区 } Client::~Client(void) { closesocket(m_socket); m_socket=INVALID_SOCKET; DeleteCriticalSection(&m_cs);//释放临界区 CloseHandle(m_hEvent);//释放事件对象 } BOOL Client::StartRunning()//创建和接收数据线程 { m_bConning=TRUE; if ((m_hThreadRecv=CreateThread(NULL,0,RecvDataThread,this,0,NULL)) == NULL) { return FALSE; } else { CloseHandle(m_hThreadRecv);//成功创建线程后,调用closeHandle()将该线程句柄的引用记数减1 } if ((m_hThreadSend = CreateThread(NULL,0,SendDataThread,this,0,NULL)) == NULL) { return FALSE; } else { CloseHandle(m_hThreadSend); } return TRUE; } DWORD Client::RecvDataThread(void* pParam)//接收客户端数据线程 { Client* pClient=(Client*)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 (reVal == SOCKET_ERROR) { int nErrCode=WSAGetLastError(); if (WSAEWOULDBLOCK == nErrCode)//接受数据缓冲区不可用 { continue;//继续循环 } else if( WSAENETDOWN == nErrCode || WSAETIMEDOUT == nErrCode ||WSAECONNRESET == nErrCode)//客户端关闭了连接 { break;//线程退出 } } if( reVal == 0)//客户端关闭了连接 { break; } //接收数据 if (reVal > HEADERLEN) { printf("server recv:%s\n",temp); 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;//线程退出 } DWORD Client::SendDataThread(void* pParam)//向客户端发送数据线程 { Client* pClient=(Client*)pParam; for (;pClient->m_bConning;)//连接状态 { //收到事件通知 if (WAIT_OBJECT_0 == WaitForSingleObject(pClient->m_hEvent,INFINITE))//该函数只有当接收到发送数据线程的m_hEvent事件通知时,才会返回。当该函数返回时,说明接收数据线程已经完成了数据的计算。 { //当客户端的连接断开时,接收数据线程先退出,然后该线程后退出,并设置退出标志 if (!pClient->m_bConning) { pClient->m_bExit=TRUE; break; } //进入临界区 EnterCriticalSection(&pClient->m_cs); //发送数据 phdr pHeander=(phdr)pClient->m_data.buf; printf("server send client:[%d]%s\n",pHeander->len,pClient->m_data.buf); int val = send(pClient->m_socket,pClient->m_data.buf,pHeander->len,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; } void Client::HandleData(const char* pExpr)//接收表达式,打包数据 { memset(m_data.buf,0,MAX_NUM_BUF); //如果是“byebye”或者“Byebye” if ('B' == ((phdr)pExpr)->type) { EnterCriticalSection(&m_cs); phdr pHeaderSend=(phdr)m_data.buf;//发送的数据 pHeaderSend->type ='B';//单词类型 pHeaderSend->len=HEADERLEN+strlen("OK");//数据包长度 memcpy(m_data.buf+HEADERLEN,"OK",strlen("OK"));//复制数据到m_data LeaveCriticalSection(&m_cs); } else//算数表达式 { int nFirNum,nSecNum,nResult; char cOper; //格式化读入数据 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(nSecNum == 0) nResult=INVALID_NUM; else nResult=nFirNum/nSecNum; break; } default: nResult=INVALID_OPERATOR; } //将算数表达式和计算的结果写入字符数组中 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='E'; //数据类型为算数表达式 pHeaderSend->len=HEADERLEN+strlen(temp);//数据包的长度 memcpy(m_data.buf+HEADERLEN,temp,strlen(temp));//复制数据到m_data LeaveCriticalSection(&m_cs); } }
NoBlockServer.cpp
// NoBlockServer.cpp : 定义控制台应用程序的入口点。 //服务器同时为多个客户端提供服务,实现算术运算和向客户端发送数据的功能 /* 服务器设计以多线程方式工作: 1.建立一个用于接受客户端请求的线程,等待客户端的连接 2.为每一个接受的客户端都建立一个接收数据线程和一个发送数据线程。当接收线程收到数据后,先计算算数表达式的结果,然后使用事件通知发送数据线程。发送数据线程接收到事件后,将计算结果发送给客户端 3.建立一个资源清理线程,用于及时对服务器的资源进行清理。当客户端的套接字关闭之后,需要清理为该客户端创建的线程和分配的内存空间 4.服务器的主线程用于接收用户的输入和显示信息 5.服务器仅显示启动和退出的界面。在子线程中完成对客户端数据的处理,不在服务器界面上显示客户端的数据。 6.所有子线程都必须自动退出,不调用线程结束函数。在所有子线程都退出后,主线程最后退出 */ #include "stdafx.h" #include <WinSock2.h> #include <iostream> #include <list> #include "Client.h" using namespace std; #pragma comment(lib,"ws2_32.lib") #define TIMEFOR_THREAD_EXIT 5000 //主线程睡眠时间 #define TIMEFOR_THREAD_HELP 1500 //清理资源线程退出时间 #define TIMEFOR_THREAD_SLEEP 500 //等待客户端请求线程睡眠时间 typedef list<Client*> CLIENTLIST;//链表;管理客户端连接的链表。把每个连接的客户端作为链表的一个节点。使用该链表对客户端的接入和退出进行管理。 CLIENTLIST clientList;//管理连接的链表 HANDLE hThreadAccept;//接受客户端连接线程句柄 HANDLE hThreadHelp;//释放资源线程句柄 SOCKET sServer;//监听套接字 BOOL bServerRunning;//服务器的工作状态 HANDLE hServerEvent;//关闭服务器事件对象 CRITICAL_SECTION csClientList;//保护链表的临界区对象 BOOL InitServer();//初始化 BOOL StartService();//启动服务 void StopService();//停止服务 BOOL CreateHelperAndAcceptThread();//创建客户端连接线程 void ExitServer();//服务器退出 void InitMember();//初始化全局变量 BOOL InitSocket();//初始化SOCKET DWORD _stdcall HelperThread(void *pParam);//释放资源 DWORD _stdcall AcceptThread(void *pParam);//接受客户端连接线程 void main() { if (!InitServer()) { ExitServer(); return; } if (!StartService()) { ExitServer(); return; } StopService(); ExitServer(); system("pause"); return; } BOOL InitServer()//初始化 { InitMember(); if (!InitSocket()) { return FALSE; } return TRUE; } void InitMember()//初始化全局变量 { InitializeCriticalSection(&csClientList);//初始化临界区 //为了便于控制服务器退出事件对象的状态,将该事件对象设置为手动模式。 hServerEvent = CreateEvent(NULL,TRUE,FALSE,NULL);//手动设置事件,初始化为无信号状态 hThreadAccept=NULL; hThreadHelp=NULL; sServer=INVALID_SOCKET; bServerRunning=FALSE; clientList.clear();//清空链表 } BOOL InitSocket()//初始化SOCKET { WSAData wsData; if (WSAStartup(MAKEWORD(2,2),&wsData) != 0) { return FALSE; } if ((sServer = socket(AF_INET,SOCK_STREAM,0)) == INVALID_SOCKET) { return FALSE; } //设置套接字为非阻塞模式 unsigned long ul=1; if (ioctlsocket(sServer,FIONBIO,&ul) == SOCKET_ERROR) { return FALSE; } SOCKADDR_IN addrServ; addrServ.sin_family=AF_INET; addrServ.sin_addr.s_addr=INADDR_ANY; addrServ.sin_port=htons(5000); if (bind(sServer,(SOCKADDR*)&addrServ,sizeof(addrServ)) == SOCKET_ERROR) { return FALSE; } if (listen(sServer,SOMAXCONN) == SOCKET_ERROR) { return FALSE; } printf("start listen...\n"); return TRUE; } BOOL StartService()//启动服务 { return CreateHelperAndAcceptThread();//创建清理资源和接收客户端连接请求的线程 } /* 服务器退出 当关闭服务器时,服务器向清理资源线程和接收客户端请求连接线程发送退出信号。接收客户端线程将自动退出。 清理资源线程向所有正在于客户端进行通讯的接收和发送数据线程,发送服务器退出消息,使这些线程自动退出。当这些线程退出后,清理资源线程再向主线程发送消息,然后改线程退出。主线程在接收到清理资源线程发送的消息后,最后退出。 */ void StopService()//停止服务,等待用户输入退出服务器命令。当用户确定退出服务器时,有清理资源线程停止客户端的服务,所有子线程退出。 { printf("tip--->e(E):Exit server. please input:\n"); char cInput; for (;bServerRunning;) { cin >> cInput; if (cInput == 'E' || cInput == 'e') { if (IDOK == MessageBox(NULL,L"Are you sure?",L"Server",MB_OKCANCEL)) //等待用户确认退出的消息框 { break;//跳出循环体 } else { Sleep(TIMEFOR_THREAD_EXIT);//线程睡眠 } } else { Sleep(TIMEFOR_THREAD_EXIT);//线程睡眠 } } bServerRunning=FALSE; cout<<"Server exit ..."<<endl;//显示服务器退出信息 Sleep(TIMEFOR_THREAD_EXIT);//给其他线程时间退出 WaitForSingleObject(hServerEvent,INFINITE);//等待清理资源线程发送事件;该函数直到收到清理资源线程发送的服务器退出事件后才返回 } BOOL CreateHelperAndAcceptThread()//创建客户端连接线程 { bServerRunning=TRUE; //创建释放资源线程 if ((hThreadHelp=CreateThread(NULL,0,HelperThread,NULL,0,NULL)) == NULL)//第5个参数:0表示新创建的线程将立即运行各种的线程函数 { bServerRunning =FALSE; cout<<"Server failed!"<<endl; return FALSE; } else { CloseHandle(hThreadHelp); } //创建接收客户端请求线程 if ((hThreadAccept=CreateThread(NULL,0,AcceptThread,NULL,0,NULL)) == NULL) { bServerRunning =FALSE; cout<<"Server failed!"<<endl; return FALSE; } else { CloseHandle(hThreadAccept); } cout<<"Server succeded!"<<endl; return TRUE; } void ExitServer()//服务器退出 { DeleteCriticalSection(&csClientList);//释放临界区对象 CloseHandle(hServerEvent);//释放事件对象句柄 closesocket(sServer);//关闭SOCKET WSACleanup();//卸载WindowsSocketDLL } DWORD _stdcall HelperThread(void *pParam)//释放资源 { /* 客户端退出 当接收数据线程获知客户端请求结束时,向发送数据线程的线程发送消息使其退出,然后,该线程退出线程。同时清理资源线程清理该线程的占用资源。 */ for (;bServerRunning;)//服务器正在运行 { EnterCriticalSection(&csClientList);//进入临界区 //清理已经断开的连接客户端内存空间 CLIENTLIST::iterator iter=clientList.begin(); for (;iter != clientList.end();) { Client* pClient=(Client*)*iter; if (pClient->IsExit())//客户端线程已经退出 { printf("first server delete a client[%s],id=%d\n",inet_ntoa(pClient->m_addr.sin_addr),pClient->m_socket); clientList.erase(iter++);//删除节点 delete pClient;//释放内存 pClient=NULL; } else { iter++; } } LeaveCriticalSection(&csClientList);//离开临界区 Sleep(TIMEFOR_THREAD_HELP); } //服务器停止工作 if (!bServerRunning) { //断开每个连接,线程退出 EnterCriticalSection(&csClientList); CLIENTLIST::iterator iter=clientList.begin(); for (;iter != clientList.end();) { Client* pClient=(Client*)*iter; //如果客户端的连接还存在,则断开连接,线程退出 if (pClient->IsConning()) { pClient->DisConning(); } ++iter; } LeaveCriticalSection(&csClientList);//离开临界区 Sleep(TIMEFOR_THREAD_SLEEP);//给连接客户端线程时间,使其自动退出 EnterCriticalSection(&csClientList);//进入临界区 //确保为每个客户端分配的内存空间都回收。如果不加入while()这层循环,可能存在这样的情况,当pClient->IsExit()时,该线程还没有退出。那么就需要从链表的开始部分重新判断 while(clientList.size() != 0) { iter = clientList.begin(); for (;iter != clientList.end();iter++) { Client* pClient=(Client*)*iter; if (pClient->IsExit())//客户端线程已经退出 { printf("second server delete a client[%s],id=%d\n",inet_ntoa(pClient->m_addr.sin_addr),pClient->m_socket); clientList.erase(iter++);//删除节点 delete pClient;//释放内存空间 pClient=NULL; } } Sleep(TIMEFOR_THREAD_SLEEP);//给连接客户端线程时间,使其自动退出 } LeaveCriticalSection(&csClientList);//离开临界区 } clientList.clear();//清空线程 SetEvent(hServerEvent);//通知主线程退出 return 0; } DWORD _stdcall AcceptThread(void *pParam)//接受客户端连接线程 { SOCKET sAccept; sockaddr_in addrClient; while(bServerRunning) { memset(&addrClient,0,sizeof(sockaddr_in)); int lenClient=sizeof(sockaddr_in); sAccept=accept(sServer,(sockaddr*)&addrClient,&lenClient); if (INVALID_SOCKET == sAccept) { int nErrCode=WSAGetLastError(); if (nErrCode == WSAEWOULDBLOCK)//无法立即完成一个非阻挡性套接字操作 { Sleep(TIMEFOR_THREAD_SLEEP); continue;//继续等待 } else { return 0;//线程退出 } } else//接受客户端的请求 { Client* pClient=new Client(sAccept,addrClient);//创建客户端对象 printf("server accept a client[%s]:%d;id=%d\n",inet_ntoa(addrClient.sin_addr),ntohs(addrClient.sin_port),sAccept); EnterCriticalSection(&csClientList);//进入临界区 clientList.push_back(pClient);//加入链表 LeaveCriticalSection(&csClientList);//离开临界区 pClient->StartRunning();//为接受的客户端建立接受数据和发送数据线程 } } return 0;//线程退出 }
NoBlockClient
NoBlockClient.cpp
// NoBlockClient.cpp : 定义控制台应用程序的入口点。 /* 客户端为多线程工作方式,主线程显示用户的输入和数据的计算结果,子线程接收和发送数据 1.主线程用于所有的界面显示和接收用户的输入 2.建立一个数据发送线程,当数据打包后,通知该线程向服务器发送数据 3.建立一个数据接收线程。当该线程接收到服务器发送的数据后,由该线程完成对数据的解包,并通知主线程显示结果 4.所有的子线程都必须自动退出。在所有字线程都退出后,主线程最后退出 客户端退出: 客户端首先向服务器发送"byebye"或"Byebye"消息,然后等待服务器的返回消息。当客户端的接收数据线程收到服务器返回的"OK"消息后,通知主线程显示该消息。然后,主线程通知发送数据线程和接收数据线程退出。在确定这两个线程都退出后,主线程最后退出。 */ #include <winsock2.h> #include <stdio.h> #include <iostream> using namespace std; #pragma comment(lib,"ws2_32.lib") #define TIMEFOR_THREAD_EXIT 1000 #define TIMEFOR_THREAD_SLEEP 500 #define BUF_MAX_NUM 64 #define HEADERLEN (sizeof(hdr)) typedef struct _head//数据包包头结构该结构在win32xp下为4byte { char type;//类型 unsigned short len;//数据包的长度(包括头的长度) }hdr,*phdr; typedef struct _data//数据包中的数据结构 { char buf[BUF_MAX_NUM]; }DATABUF,*pDataBuf; //数据包:包头(类型[2byte]+长度[2byte])+数据; SOCKET sClient;//套接字 HANDLE hThreadSend;//发送数据线程 HANDLE hThreadRecv;//接收数据线程 DATABUF bufSend;//发送数据缓冲区 DATABUF bufRecv;//接收数据缓冲区 CRITICAL_SECTION csSend;//当读写 bufSend变量时,要在之前进入临界区,之后退出临界区;//临界区对象,锁定bufSend CRITICAL_SECTION csRecv;//当读写 bufRecv变量时,要在之前进入临界区,之后退出临界区;//临界区对象,锁定bufRecv BOOL bSendData;//通知发送数据线程发送数据 HANDLE hEventShowDataResult;//显示计算结果的事件 BOOL bConnected;//与服务器的连接状态 HANDLE arrThread[2];//子线程数组 BOOL InitClient();//初始化 BOOL ConnectServer();//连接服务器 BOOL CreateSendAndRecvThread();//创建发送和接收数据线程 void InputAndOutput();//用户输入数据和显示结果 void ExitClient();//退出 void InitMember();//初始化全局变量 BOOL InitSocket();//创建SOCKET DWORD _stdcall RecvDataThread(LPVOID pParam);//接收数据线程 DWORD _stdcall SendDataThread(LPVOID pParam);//发送数据线程 BOOL PackByebye(const char* pExpr);//将输入的"Byebye","byebye"字符串打包 BOOL PackExpression(const char* pExpr);//将输入的算数表达式打包 void ShowConnectMsg(BOOL bConnected);//显示连接服务器消息 void ShowDataResultMsg();//显示计算结果 void ShowTipMsg(BOOL bFirstInput);//显示提示信息 int main() { if (!InitClient()) { ExitClient(); return 0; } if (ConnectServer()) { ShowConnectMsg(TRUE); } else { ShowConnectMsg(FALSE); ExitClient(); return 0; } if (!CreateSendAndRecvThread()) //创建发送和接收数据线程 { ExitClient(); return 0; } InputAndOutput();//用户输入数据和显示结果 ExitClient();//退出 system("pause"); return 0; } BOOL InitClient()//初始化 { InitMember(); if (!InitSocket()) { return FALSE; } return TRUE; } void InitMember()//初始化全局变量 { InitializeCriticalSection(&csSend); InitializeCriticalSection(&csRecv); sClient=INVALID_SOCKET; hThreadRecv=NULL; hThreadSend=NULL; bConnected=FALSE; bSendData=FALSE; memset(bufRecv.buf,0,BUF_MAX_NUM); memset(bufSend.buf,0,BUF_MAX_NUM); //手动设置事件,初始化为无信号状态 hEventShowDataResult=(HANDLE)CreateEvent(NULL,TRUE,FALSE,NULL); } BOOL InitSocket()//创建SOCKET { WSADATA wsd; if (WSAStartup(MAKEWORD(2,2),&wsd) != 0) { return FALSE; } if ((sClient=socket(AF_INET,SOCK_STREAM,0)) == INVALID_SOCKET) { return FALSE; } //设置套接字为非阻塞模式 unsigned long ul=1; if (ioctlsocket(sClient,FIONBIO,&ul) == SOCKET_ERROR) { return FALSE; } return TRUE; } BOOL ConnectServer()//连接服务器 { SOCKADDR_IN addrServ; addrServ.sin_family=AF_INET; addrServ.sin_addr.s_addr=inet_addr("127.0.0.1"); addrServ.sin_port=htons(5000); int reVal; while (1) { reVal=connect(sClient,(SOCKADDR*)&addrServ,sizeof(addrServ)); //处理连接错误。当使用非阻塞套接字时,建议最好不要使用这种方法来判断客户端是否成功连接服务器。推荐使用select(),WSAAsyncSelect()或者WSAEventselect(),来判断连接服务器是否成功。在后面的章节中会讲解到。 if (reVal == SOCKET_ERROR) { int nErrCode=WSAGetLastError(); if (nErrCode == WSAEWOULDBLOCK || nErrCode == WSAEINVAL)//连接未能够立即完成;监听状态 { continue; } else if (nErrCode == WSAEISCONN)//连接已经完成 { break; } else//其他原因,连接失败 { return FALSE; } } if(reVal == 0)//连接成功 { break; } } bConnected=TRUE; return TRUE; } BOOL CreateSendAndRecvThread()//创建发送和接收数据线程 { unsigned long ulThreadId; if ((hThreadRecv=CreateThread(NULL,0,RecvDataThread,NULL,0,&ulThreadId)) == NULL) { return FALSE; } if ((hThreadSend=CreateThread(NULL,0,SendDataThread,NULL,0,&ulThreadId)) == NULL) { return FALSE; } arrThread[0]=hThreadRecv; arrThread[1]=hThreadSend; return TRUE; } /* 客户端退出 客户端首先向服务器发送"Byebye","byebye"消息,然后等待服务器的返回消息。当客户端的接收数据线程收到服务器返回的"OK"消息后,通知主线程显示该消息。然后,主线程通知发送数据线程和接收数据线程退出。在确认这两个线程都退出后,主线程最后退出。 */ void InputAndOutput()//用户输入数据和显示结果 { char cInput[BUF_MAX_NUM];//用户输入缓冲区 BOOL bFirstInput=TRUE;//第一次只能输入算术表达式 while(bConnected)//连接状态 { memset(cInput,0,BUF_MAX_NUM); 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 (!bConnected)//客户端被动退出,此时接收和发送数据线程已经退出。 { break; } ShowDataResultMsg();//显示数据 if (0 == strcmp(bufRecv.buf,"OK"))//客户端主动退出 { bConnected=FALSE; Sleep(TIMEFOR_THREAD_EXIT);//给数据接收和发送线程退出时间 } } } if (!bConnected)//与服务器连接已经断开 { ShowConnectMsg(FALSE);//显示信息 } //等待数据发送和接收线程退出;为了做到主线程最后退出 if (WAIT_ABANDONED_0 == WaitForMultipleObjects(2,arrThread,TRUE,INFINITE)) { int nErrCode=GetLastError(); } } void ExitClient()//退出 { DeleteCriticalSection(&csSend); DeleteCriticalSection(&csRecv); CloseHandle(hThreadRecv); CloseHandle(hThreadSend); closesocket(sClient); WSACleanup(); } DWORD _stdcall RecvDataThread(LPVOID pParam)//接收数据线程 { int reVal; char temp[BUF_MAX_NUM]; memset(temp,0,BUF_MAX_NUM); while(bConnected)//连接状态 { reVal = recv( sClient,temp,BUF_MAX_NUM,0);//接收数据 if ( SOCKET_ERROR == reVal)//接受数据缓冲区不可用 { int nErrCode = WSAGetLastError(); if (WSAEWOULDBLOCK == nErrCode) { Sleep(TIMEFOR_THREAD_SLEEP);//线程睡眠 continue;//继续接收数据 } else { bConnected = FALSE; SetEvent(hEventShowDataResult);//通知主线程,防止在无限期的等待 return 0;//线程退出 } } if ( 0 == reVal)//服务器关闭了连接 { bConnected = FALSE;//线程退出 SetEvent(hEventShowDataResult);//通知主线程,防止在无限期的等待 return 0;//线程退出 } if ( reVal > HEADERLEN && reVal != -1)//收到数据 { //对数据解包 phdr header =(phdr)temp; EnterCriticalSection(&csRecv); memset(bufRecv.buf,0,BUF_MAX_NUM); memcpy(bufRecv.buf,temp + HEADERLEN,header->len - HEADERLEN);//将数据结果复制到接收数据缓冲区 LeaveCriticalSection(&csRecv); SetEvent(hEventShowDataResult);//通知主线程显示计算结果 memset(temp,0,BUF_MAX_NUM); } Sleep(TIMEFOR_THREAD_SLEEP);//线程睡眠 } return 0; } DWORD _stdcall SendDataThread(LPVOID pParam)//发送数据线程 { while(bConnected)//连接状态 { if (bSendData)//发送数据 { EnterCriticalSection(&csSend);//进入临界区 while(TRUE) { int nBuflen=((phdr)bufSend.buf)->len; int val = send(sClient,bufSend.buf,nBuflen,0); //处理返回错误 if (SOCKET_ERROR == val) { int nErrCode = WSAGetLastError(); if (WSAEWOULDBLOCK == nErrCode)//发送缓冲区不可用 { continue;//继续循环 } else { LeaveCriticalSection(&csSend);//离开临界区 bConnected = FALSE;//断开状态 SetEvent(hEventShowDataResult);//通知主线程,防止在无限期的等待返回结果。 return 0; } } bSendData = FALSE;//发送状态 break;//跳出for } LeaveCriticalSection(&csSend);//离开临界区 } Sleep(TIMEFOR_THREAD_SLEEP);//线程睡眠 } return 0; } BOOL PackByebye(const char* pExpr)//将输入的"Byebye","byebye"字符串打包 { BOOL reVal = FALSE; if (!strcmp("Byebye",pExpr) || !strcmp("byebye",pExpr)) { EnterCriticalSection(&csSend); phdr pHeader =(phdr)bufSend.buf;//强制转换 pHeader->type='B';//类型 pHeader->len = HEADERLEN + strlen("Byebye");//数据包长度 memcpy(bufSend.buf + HEADERLEN,pExpr,strlen(pExpr));//复制数据 LeaveCriticalSection(&csSend); pHeader = NULL; bSendData = TRUE;//通知发送数据线程 reVal = TRUE; } return reVal; } BOOL PackExpression(const char* pExpr)//将输入的算数表达式打包 { char* pTemp = (char*)pExpr;//算数表达式数字开始的位置 while (!*pTemp)//第一个数字位置 { pTemp++; } char* pos1=pTemp; char* pos2=NULL; char* pos3=NULL; int len1=0,len2=0,len3=0; if ((*pTemp != '+') && (*pTemp != '-') && ((*pTemp < '0') || (*pTemp > '9')))//第一个字符是+ - 或者是数字 { return FALSE; } 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 (('+' != *pTemp) && ('-' != *pTemp) && ('*' != *pTemp) && ('/' != *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 ('=' != *pTemp) //最后是等于号 return FALSE; //重新输入 len3 = pTemp - pos3;//数字长度 int nExprlen = len1 + len2 + len3; //算数表示长度 //表达式读入发送数据缓冲区 EnterCriticalSection(&csSend); //进入临界区 //数据包头 phdr pHeader = (phdr)(bufSend.buf); pHeader->type = 'E'; //类型 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; } void ShowConnectMsg(BOOL bSuc)//显示连接服务器失败消息 { if (bSuc) { cout << "* Succeed to connect server! *" << endl; } else { cout << "* Fail to connect server! *" << endl; } } void ShowDataResultMsg()//显示计算结果 { EnterCriticalSection(&csRecv); cout << "***Result: "<<bufRecv.buf <<endl; LeaveCriticalSection(&csRecv); } void ShowTipMsg(BOOL bFirstInput)//显示提示信息 { if (bFirstInput)//首次显示 { cout << "**********************************" << endl; cout << "* *" << endl; cout << "* Please input expression. *" << endl; cout << "* Usage:NumberOperatorNumber= *" << endl; cout << "* *" << endl; cout << "**********************************" << endl; }else{ cout << "**********************************" << endl; cout << "* *" << endl; cout << "* Please input: expression *" << endl; cout << "* Usage:NumberOperatorNumber= *" << endl; cout << "* *" << endl; cout << "* If you want to exit. *" << endl; cout << "* Usage: Byebye or byebye *" << endl; cout << "* *" << endl; cout << "**********************************" << endl; } }
运行结果: