阻塞模式的套接字执行I/O操作时,如果执行操作的条件没有得到满足,线程就会被阻塞在该调用的函数上。程序不得不处于等待状态。该调用函数什么时候返回,不得而知。
非阻塞模式套接字执行I/O操作时,在某种情况下,调用函数都会立即返回。软件开发人员必须编写更多的代码,对该函数返回的错误进行处理,这无疑增加了开发WindowsSockets应用程序的难度。另外,应用程序中往往需要在一个循环体内反复调用该函数,直到返回成功指示为止,这不是一种很好的做法。
Select模式是windowsSocket中最常见的I/O模型。所以称其为Select模型,是因为它的核心是利用select()函数实现I/O管理。利用select()函数,WindowsSockets应用程序可以判断套接字上是否存在数据,或者能否向该套接字写入数据。
如上图:在调用recv()接收数据之前,先调用select()。如果此时系统没有可读的数据,那么select()会阻塞在这里。当系统存在可读的数据时,该函数返回。此时应用程序就可以调用recv()接收数据了。 应该可以看到Select模式为开发WindowsSockets应用程序,提供了调用某个函数前的通知机制。
/* 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 64 typedef 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 timeval struct 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集合的一名成员,则返回TRUE FD_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模式优势在于可以同时对多个建立起来的套接字进行有序的管理。可以防止应用程序一次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) { WORD wVersionRequested; //请求的Windows Sockets 实现版本 WSADATA wsaData; //返回协商结果 int nErrCode; //调用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); //更新对话框 int reVal; //返回值 DWORD dwServIP; 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_SET allSockfd; //服务器所有套接字集合 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_IN addrClient; 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 Sockets void CClientDlg::InitSocket(void) { WORD wVersionRequested; //请求socket版本 WSADATA wsaData; //wsaData结构 int nErrCode; //返回值 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; SOCKET sHost = 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) { BOOL reVal = TRUE; //返回值 int nErrCode; //错误值 //读取包头 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; }
源码下载