[精通WindowsSocket网络开发-基于VC++实现]第六章——Select模式开发[阻塞]

来源:互联网 发布:伏尼契手稿知乎 编辑:程序博客网 时间:2024/06/05 15:54
套接字Select模型是比较常用的一种I/O模型。利用该模型使得WindowsSockets应用程序可以在同一时间内管理和控制多个套接字。该模型的核心是select()函数。在使用该函数是,还需要用到FD_SET,FD_ZERO,FD_ISSET和FD_CLR四个宏。开发WindowsSocket程序时,应用程序需要这样的能力:当执行操作的套接字满足可读可写条件时,需要给应用程序发送通知,接收到这个通知后,应用程序再调用相应的WindowsSocketsAPI去执行函数调用。套接字的Select模型,就是能够使得应用程序具有这样能力的一种方式。套接字的Select模型,能够使WindowsSockets应用程序同时对多个套接字进行管理。调用select()检查当前各个套接字的当前状态。并且根据该函数的返回值,判断套接字的可读可写性。然后调用相应的WindwosSocketsAPI,完成数据的发送,接收。以一个远程文件下载程序为实例。

套接字Select模型 

阻塞模式的套接字执行I/O操作时,如果执行操作的条件没有得到满足,线程就会被阻塞在该调用的函数上。程序不得不处于等待状态。该调用函数什么时候返回,不得而知。

非阻塞模式套接字执行I/O操作时,在某种情况下,调用函数都会立即返回。软件开发人员必须编写更多的代码,对该函数返回的错误进行处理,这无疑增加了开发WindowsSockets应用程序的难度。另外,应用程序中往往需要在一个循环体内反复调用该函数,直到返回成功指示为止,这不是一种很好的做法。 

select模型

Select模式是windowsSocket中最常见的I/O模型。所以称其为Select模型,是因为它的核心是利用select()函数实现I/O管理。利用select()函数,WindowsSockets应用程序可以判断套接字上是否存在数据,或者能否向该套接字写入数据。

如上图:在调用recv()接收数据之前,先调用select()。如果此时系统没有可读的数据,那么select()会阻塞在这里。当系统存在可读的数据时,该函数返回。此时应用程序就可以调用recv()接收数据了。 应该可以看到Select模式为开发WindowsSockets应用程序,提供了调用某个函数前的通知机制。

select()函数

/*1.select()函数 int select(IN int nfds,_inout_opt fd_set FAR * readfds,_inout_opt fd_set FAR * writefds,_inout_opt fd_set FAR * exceptfds,IN const struct timeval FAR * timeout);//通过该函数,WindowsSockets应用程序可以判断套接字是否存在数据,或者能否向其写入数据。参数:int nfsd:被忽略,之所以仍然要提供这个参数,是为了保持与早期的Berkeley套接字应用程序兼容。fd_set FAR* readfds:具有可读性套接字集合的指针fd_set FAR* writefds:具有可写性套接字集合的指针fd_set FAR* exceptfds:检查错误套接字集合的指针timeval FAR* timeout:用于设置调用select()函数时的等待时间返回值:失败返回SOCKET_ERROR,否则,返回。调用select()时,readfds,writefds和sexceptfds 3个参数中至少有一个不能设置为NULL。并且,在该非空的参数中,必须至少包含一个套接字。否则select()将没有任何套接字可以等待。select()返回后,会修改每个fd_set结构,删除那些不存在的没有完成I/O操作的套接字。2.struct fd_set#define FD_SETSIZE      64typedef struct fd_set //是一个管理多个套接字的结构体{u_int fd_count;               //套接字数量SOCKET  fd_array[FD_SETSIZE];   //套接字数组} fd_set; //fd_xxx:(file descriptor)文件描述符号,在这是socket句柄在程序中使用该结构表示一系列特定套接字的集合。eg:准备接收数据的套接字集合,又称为可读性集合。准备发送数据的套接字集合,又称为可写性集合。当select()成功返回后,会在fs_set结构中,返回刚好未完成I/O操作的所有套接字句柄的总量。A:readfds参数将包含符号下面任何一个条件的套接字1.有数据可以读入,此时在该套接字上调用recv()等输入函数,立即接收到对方的数据。2.连接已经关闭,重设或中止3.假如已经调用listen(),而且一个连接正在建立。那么此时调用accept()会成功B:writefds参数将包含符号下面任何一个条件的套接字1.有数据可以发生。此时在该套接字上可以调用send()等输出函数,向对方发送数据2.如果已经在一个非锁定套接字上调用了connect(),此时连接成功C:exceptfds参数将包含符号下面任何一个条件的套接字1.如果已经在一个非锁定套接字上调用了connect(),此时连接失败2.有带外(Out-of-band,OOB)数据可供读取3. struct  timevalstruct timeval  //用于定义select()函数的等待时间{    long    tv_sec;       //秒     long    tv_usec;     //毫秒};调用select()时,timeout参数可以分为3种情况A:空指针。select()调用会无限期,等到至少有一个套接字符合设置的条件后,该函数返回B:0。无论是否有套接字符号设置的条件,select()都立即返回。允许应用程序对该函数进行“轮询”。出于对性能方面的考虑,应避免这样的设置。C:非0值。如果在等待的时间内,有套接字满足设置的条件,则该函数返回。如果在等待时间内没有套接字满足设置的条件,则该函数在到达设定的时间后返回,并且该返回返回值为0.4.宏FD_CLR(s,*set):从set集合中删除s套接字FD_ISSET(s,*set):检查s是否为set集合的一名成员。如果s是set集合的一名成员,则返回TRUEFD_SET(s,*set):将套接字s加入set集合。FD_ZERO(*set):将set集合初始化为空集合。5.调用select()时时有宏A.使用FD_ZERO宏,初始化自己感兴趣的套接字集合fd_set.eg:FD_ZERO(readfd)B.使用FD_SET宏,将套接字分配给参与操作的fd_set集合。eg:FD_SET(s,readfd);C.以该fd_set为参数调用select().等待在指定的fd_set集合中,I/O活动设置好这个套接字。select()完成后会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。D.select()函数成功返回后,使用FD_ISSSET宏,对每个fd_set集合进行检查。eg:FD_ISSET(s,readfd);如果该宏为TRUE,则说明该套接字可读 */

Select模型的优势和不足

select模式优势在于可以同时对多个建立起来的套接字进行有序的管理。可以防止应用程序一次I/O调用过程中,使阻塞模式套接字被迫进入阻塞状态;使非阻塞套接字产生WSAEWOULDBLOCK错误。

select()函数就好像是一个消息中心,当消息到来时,通知应用程序接收和发送数据。这使得WindowsSockets应用程序开发人员可以把精力更多的集中在如何处理数据的发送和接收上。

应该看到完成一次I/O操作经历了两次WindowsSockets函数调用。例如,当接受对方的数据时,第一步,调用select()等待该套接字的满足条件,第二步:调用recv()接收数据。这种结果与在一个阻塞模式的套接字上调用recv()函数是一样的。

因此,使用select()WindowsSockets程序,其效率可能受损。因为,每一个WindowsSockets I/O调用都会经过该函数,因而会导致严重的CPU额外负担。在CPU的使用率不是关键因素时,这种效率可以接受。但是,当需要高效率时,肯定会产生问题。

 

远程文件下载程序

服务器端关键代码

/*服务器实现以下功能1.使用select模型管理与客户端建立的套接字2.同时为多个客户端提供服务器目录信息和下载文件服务3.显示客户端的相关请求信息,如:a.客户端连接数量 b.客户端请求的目录 c.客户端请求下载的文件*///初始化Windows Sockets,设置发送数据缓冲区。void CServerDlg::InitSocket(void){WORDwVersionRequested;//请求的Windows Sockets 实现版本WSADATAwsaData;//返回协商结果intnErrCode;//调用API函数的返回值wVersionRequested = MAKEWORD(2, 2);nErrCode = WSAStartup( wVersionRequested, &wsaData );if ( 0 != nErrCode ) {return;}//创建套接字m_sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (INVALID_SOCKET == m_sServer){return;}//获取系统默认的发送数据缓冲区大小unsigned int uiRcvBuf;int uiRcvBufLen = sizeof(uiRcvBuf);nErrCode= getsockopt(m_sServer, SOL_SOCKET, SO_SNDBUF,(char*)&uiRcvBuf, &uiRcvBufLen);if (SOCKET_ERROR == nErrCode){return;}//设置系统发送数据缓冲区为默认值的BUF_TIMES倍uiRcvBuf *= BUF_TIMES;nErrCode = setsockopt(m_sServer, SOL_SOCKET, SO_SNDBUF,(char*)&uiRcvBuf, uiRcvBufLen);if (SOCKET_ERROR == nErrCode){AfxMessageBox(_T("修改系统发送数据缓冲区失败!"));}//检查设置系统发送数据缓冲区是否成功unsigned int uiNewRcvBuf;getsockopt(m_sServer, SOL_SOCKET, SO_SNDBUF,(char*)&uiNewRcvBuf, &uiRcvBufLen);if (SOCKET_ERROR == nErrCode || uiNewRcvBuf != uiRcvBuf){AfxMessageBox(_T("修改系统发送数据缓冲区失败!"));;}}//释放套接字占用的资源void CServerDlg::UnInitSocke(void){ closesocket(m_sServer);WSACleanup();}//启动服务 按钮void CServerDlg::OnButtonStartup() {UpdateData(TRUE);//更新对话框intreVal;//返回值DWORDdwServIP;m_ctlIP.GetAddress(dwServIP);//得到服务器IP//服务器套接字地址SOCKADDR_IN servAddr;servAddr.sin_family = AF_INET;servAddr.sin_addr.S_un.S_addr = htonl(dwServIP);servAddr.sin_port = htons(SERVERPORT);//绑定服务器reVal = bind(m_sServer,(sockaddr*)&servAddr,sizeof(SOCKADDR_IN));if (SOCKET_ERROR == reVal){AfxMessageBox(_T("服务器绑定失败"), MB_OK, 0);closesocket(m_sServer);WSACleanup();return;}else {m_ctlTip.SetWindowText(_T("服务器绑定成功!"));UpdateData(false);}//监听reVal = listen(m_sServer,SOMAXCONN);if (SOCKET_ERROR == reVal){AfxMessageBox(_T("服务器监听失败!"), MB_OK, 0);closesocket(m_sServer);WSACleanup();return;}m_bConning = TRUE;//修改服务器状态//创建接受和处理客户端请求线程DWORD dwThread;m_hReqAndData = CreateThread(NULL, 0, DirAndFileSizeServcieThread, this, 0, &dwThread);CloseHandle(m_hReqAndData);//更新界面(CButton*)GetDlgItem(IDC_BUTTON_STARTUP)->EnableWindow(FALSE);//启动按钮无效m_ctlIP.EnableWindow(FALSE);//服务器地址控件无效m_ctlTip.SetWindowText(_T("服务器启动成功!")); //显示成功信息UpdateData(FALSE);//初始化对话框}/*接受和处理客户端请求线程 该函数以select()函数为核心,实现对服务器所有套接字的管理。主要功能: 1.接收客户端连接请求 2.调用CClient类的RecvData()函数向接收客户端数据 3.调用CClient类的SendData()函数向客户端发送数据 4.调用AddClient()和DeleteClient()实现对客户端的管理 5.调用ShowClientNumberInfor()更新 服务器节目信息 客户端启动后向服务器发送连接请求。服务器接收该请求,新建套接字。客户端后续的目录请求和文件长度请求,都在此套接字上完成。当客户端退出时,该套接字关闭。 当客户端需要下载文件时,首先通过前面建立的套接字获取文件的长度。然后,客户端创建套接字,并向服务器发送连接请求。服务器接收该请求,新建套接字。然后在此套接字上向客户端上传文件。当文件上传  完毕后,关闭该套接字。 总之,服务器管理客户端的套接字分两种,一种用于处理目录结构和文件长度,另一种用于上传文件。 */ DWORD WINAPI CServerDlg::DirAndFileSizeServcieThread(void *pParam) { CServerDlg* pServer = (CServerDlg*)pParam; SOCKET sListen = pServer->GetSocket();//获得服务器监听套接字  FD_SETallSockfd;//服务器所有套接字集合 FD_ZERO(&allSockfd);//清空集合 FD_SET(sListen, &allSockfd);//将监听套接字加入该集合  FD_SET readfd;//定义满足可读套接字集合 FD_SET writefd;//定义满足可写套接字集合 while (pServer->IsConnenting())//服务器运行状态 { FD_ZERO(&readfd);//清空可读集合 FD_ZERO(&writefd);//清空可写集合 readfd = allSockfd;//赋值 writefd = allSockfd;//赋值  //更新界面信息 pServer->ShowClientNumberInfor(allSockfd.fd_count); //无限期等待套接字满足条件 int nRet = select(0, &readfd, &writefd, NULL, NULL); if (pServer->IsConnenting() && nRet > 0)//添加了pServer->IsConnecting();调试时发现一个问题:当程序执行到上一行代码时,这时单击关闭按钮,资源释放时有一个服务器接收到了一个客户端的连接,其实客户端没有连接,为此出现了异常。 { //遍历所有套接字集合 for (int i = 0; i < allSockfd.fd_count; i++) { //存在可读的套接字(包括接收客户端的连接,和接收客户端的数据) if (FD_ISSET(allSockfd.fd_array[i], &readfd)) {  if (allSockfd.fd_array[i] == sListen)//接受客户端连接请求  { SOCKADDR_INaddrClient; int nAddrLen = sizeof(addrClient); SOCKET sClient = accept(sListen, (sockaddr*)&addrClient, &nAddrLen); //新建一个 CClient类实例 CClient *pClient = new CClient(sClient, pServer); //加入客户端管理链表中 pServer->AddClient(pClient); //加入套接字集合 FD_SET(sClient, &allSockfd); //更新界面信息 pServer->ShowClientNumberInfor(allSockfd.fd_count);  } else //接收客户端数据 { //得到CClient类的实例 CClient* pClient = pServer->GetClient(allSockfd.fd_array[i]); if (pClient != NULL) { //接收数据 BOOL bRet = pClient->RecvData(); //接收数据错误或者客户端关闭套接字 if (FALSE == bRet) { //取出套接字 SOCKET sTemp = allSockfd.fd_array[i]; //从集合中删除 FD_CLR(allSockfd.fd_array[i], &allSockfd); //从客户端管理链表中删除该客户端 pServer->DeleteClient(sTemp); //更新界面信息 pServer->ShowClientNumberInfor(allSockfd.fd_count); }  }  }  }   if (FD_ISSET(allSockfd.fd_array[i], &writefd))//存在的可写套接字 { //得到CClient类的实例 CClient* pClient = pServer->GetClient(allSockfd.fd_array[i]); if (pClient != NULL) { //发送数据 BOOL bRet = pClient->SendData();  if (FALSE == bRet)//发送数据失败 { //被删除的套接字 SOCKET sDelete = allSockfd.fd_array[i]; //从集合中删除该套接字 FD_CLR(allSockfd.fd_array[i], &allSockfd); //从客户端管理链表中删除该客户端 pServer->DeleteClient(sDelete); //更新界面信息 pServer->ShowClientNumberInfor(allSockfd.fd_count); } }   } } }  Sleep(THREAD_SLEEP);//线程睡眠 }  pServer->DeleteAllClient();//删除所有的客户端 return 0; }

 

客户端关键代码

/*A.主线程1.创建发送和接收目录线程2.接受用户界面操作并更新界面B.发送请求接收目录线程1.向服务器发送根目录请求,并接收其返回的数据2.向服务器发送子目录请求,并接收其返回的数据3.向服务器发送下载文件的长度请求,并接收其返回数据4.启动创建文件下载线程C.创建下载文件线程1.创建3个下载文件线程2.为每个下载文件线程分配下载文件任务,包括下载文件的开始位置和长度3.下载文件线程返回后,合并文件D.下载文件线程1.向服务器发送下载文件请求。请求中包括文件路径,下载文件开始位置和文件长度等信息2.接收服务器发送的文件3.保存文件*///初始化Windows Socketsvoid CClientDlg::InitSocket(void){WORDwVersionRequested;//请求socket版本WSADATAwsaData;//wsaData结构intnErrCode;//返回值wVersionRequested = MAKEWORD( 2, 2 );//请求windows Sockets 2.2版本nErrCode = WSAStartup( wVersionRequested, &wsaData );if ( 0 != nErrCode ){return;}//创建套接字m_sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (INVALID_SOCKET == m_sHost){return;}//设置系统接收数据为默认的BUF_TIMES倍unsigned int uiRcvBuf;int uiRcvBufLen = sizeof(uiRcvBuf);nErrCode= getsockopt(m_sHost, SOL_SOCKET, SO_RCVBUF,(char*)&uiRcvBuf, &uiRcvBufLen);if (SOCKET_ERROR == nErrCode){return;}uiRcvBuf *= BUF_TIMES;nErrCode = setsockopt(m_sHost, SOL_SOCKET, SO_RCVBUF,(char*)&uiRcvBuf, uiRcvBufLen);if (SOCKET_ERROR == nErrCode){AfxMessageBox(_T("修改系统发送数据缓冲区失败!"));}//检查设置系统接收数据缓冲区是否成功unsigned int uiNewRcvBuf;getsockopt(m_sHost, SOL_SOCKET, SO_RCVBUF,(char*)&uiNewRcvBuf, &uiRcvBufLen);if (SOCKET_ERROR == nErrCode || uiNewRcvBuf != uiRcvBuf){AfxMessageBox(_T("修改系统发送数据缓冲区失败!"));;}} //释放套接字占用的资源 void CClientDlg::UnInitSocket(void){closesocket(m_sHost);WSACleanup();}//连接服务器void CClientDlg::OnButtonConnect() {//获得服务器的IP地址UpdateData(TRUE);DWORD dwServIP;m_ctlIP.GetAddress(dwServIP);//服务器套结字地址SOCKADDR_IN servAddr;servAddr.sin_family = AF_INET;servAddr.sin_addr.S_un.S_addr = htonl(dwServIP);servAddr.sin_port = htons(SERVERPORT);//连接服务器int nErrCode;nErrCode = connect(m_sHost,(sockaddr*)&servAddr, sizeof(SOCKADDR_IN));if (SOCKET_ERROR == nErrCode){AfxMessageBox("连接服务器失败!",MB_OK, 0);return;}//显示连接服务器成功信息m_ctlTip.SetWindowText(_T("连接服务器成功!"));m_bConning = TRUE;//请求服务器目录m_nReqCur = REQROOT;//创建发送和接收目录线程DWORD dwThread;m_hThreadSR = CreateThread(NULL, 0, SendAndRecvDirInforThread, this, 0, &dwThread); CloseHandle(m_hThreadSR);//设置连接服务器按钮为无效状态CButton *pBt = (CButton*)this->GetDlgItem(IDC_BUTTON_CONNECT);pBt->EnableWindow(FALSE);}//发送请求接收目录信息线程DWORD CClientDlg::SendAndRecvDirInforThread(void* pParam){CClientDlg* pClient = (CClientDlg*)pParam;SOCKETsHost = pClient->GetHostSocket();//客户端套接字FD_SET writefd;//可写集合FD_SET readfd;//可读集合while (pClient->IsConning()){FD_ZERO(&writefd);//清零FD_ZERO(&readfd);//清零FD_SET(sHost, &writefd);//添加到可写集合FD_SET(sHost, &readfd);//添加到可读集合int reVal = 0;reVal = select(0, &readfd, &writefd, NULL, NULL);//等待套接字满足条件if (SOCKET_ERROR == reVal){AfxMessageBox(_T("select错误"));return 0;}else if ( reVal > 0){if (FD_ISSET(sHost, &writefd))//满足可写的条件{if (FALSE == pClient->SendReq())//发送数据{AfxMessageBox(_T("select错误"));return 0;}}if (FD_ISSET(sHost, &readfd))//满足可读的条件{if(FALSE == pClient->RecvDirInfor())//接收数据{AfxMessageBox("接收目录信息失败!");return 0;}}}Sleep(THREAD_SLEEP);//线程睡眠}return 0;}//发送请求BOOL CClientDlg::SendReq(void){int reVal;//返回值switch(m_nReqCur)//请求类型{case REQROOT://根目录{//发送数据包,只有包头hdr header;memset(&header, 0, sizeof(header));header.type = ROOT;header.len = HEADLEN;reVal = send(m_sHost, (char*)&header, HEADLEN, 0);if (SOCKET_ERROR == reVal){AfxMessageBox(_T("发送数据失败!"));return FALSE;}m_nReqCur = REQNON;break;}case REQDIRC://子目录{hdr header;memset(&header, 0, sizeof(header));header.type = DIRC;header.len = HEADLEN + m_strReqDir.size();//先发送包头reVal = send(m_sHost,(char*)&header, HEADLEN, 0);if (SOCKET_ERROR == reVal){AfxMessageBox(_T("发送数据失败!"));return FALSE;}//再发送包体reVal = send(m_sHost,m_strReqDir.c_str(), m_strReqDir.size(), 0);if (SOCKET_ERROR == reVal){AfxMessageBox(_T("发送数据失败!"));return FALSE;}m_nReqCur = REQNON;m_strReqDir.erase(m_strReqDir.begin(), m_strReqDir.end());//清空break;}case REQFSIZ:{hdr header;memset(&header, 0, sizeof(header));header.type = FSIZ;header.len = HEADLEN + m_strReqFile.size();//先发送包头reVal = send(m_sHost,(char*)&header, HEADLEN, 0);if (SOCKET_ERROR == reVal){AfxMessageBox(_T("发送数据失败!"));return FALSE;}//再发送包体reVal = send(m_sHost,m_strReqFile.c_str(), m_strReqFile.size(), 0);if (SOCKET_ERROR == reVal){AfxMessageBox(_T("发送数据失败!"));return FALSE;}m_nReqCur = REQNON;break;}case REQFDAT:{break;}default:break;}return TRUE;}//接收服务器数据BOOL CClientDlg::RecvDirInfor(void){BOOLreVal = TRUE;//返回值intnErrCode;//错误值//读取包头hdr header;nErrCode = recv(m_sHost,(char*)&header, HEADLEN,0);if (SOCKET_ERROR == nErrCode || 0 == nErrCode)//服务器关闭了{AfxMessageBox(_T("服务器关闭!"));reVal = FALSE;}//读取包体int nDataLen = header.len - HEADLEN;//包体的长度switch(header.type)//根据数据包的类型分类 再读取包体{case ROOT://根目录case DIRC://文件目录{if(FALSE == RecvDirString(nDataLen)){AfxMessageBox("接收目录信息失败!");reVal = FALSE;}break;}case FSIZ://文件大小{RecvFileSize(header.flen);break;}default:break;}return reVal;}

效果图:

源码下载

原创粉丝点击