登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

面包会有的

... ...

 
 
 

日志

 
 

WinSocket模型的探讨——select模型(1)  

2011-05-01 09:54:13|  分类: VC++ |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

查找了很多资料都找不到select模型的详细用法,《Windows网络编程》这本书上也只是写了一个简单的回应服务器,就连writefds的用法都没讲,也不知道什么时候利用“可写”来发文件。这些都是我的疑问,相信很多研究网络编程的同路人也碰到了我的这些问题。这些疑问在这篇文章中都解决了!耗费了偶很多的精力去猜测去思考!

感觉一些已经得道的高人都不肯把这些问题说透彻点,唉,只能靠自己去摸索了,希望这篇文章能对你有用,也希望一些高人能出来指点指点!

SOCKET模型的出现是为了解决“一个客户端一线程”的问题,为了WINDOWS的线程切换不要太频繁,我们可以使用WINDOWS的 SOCKET模型。但在论坛里又看到了一些文章说现在的计算机硬件相当发达,成万的线程切换根本不是什么问题。无论怎么样,既然这些模型被MS发明了出来,就有它的道理,我们还是好好来学习一下吧。

对于select模型,大家都知道用select函数,然后判断读集、写集。但如何使用select,最终写好的Server又是一个怎么样的结构呢?

select可以这样用,写成两个函数,SelectSend和SelectRecv,这两个函数和一般的sendrecv不同的地方在于它是有超时值的,这样就不会把程序完全阻塞了。这两个函数大概如下(参考《PC网络游戏编程》):

SelectRecv(...)


int SelectRecv(SOCKET hSocket, char *pszBuffer, int nBufferSize, 
         DWORD dwTimeout)
{
     ASSERT(hSocket != NULL);
    if(hSocket==NULL)
        return ( SOCKET_ERROR );
     FD_SET fd = {1, hSocket};
     TIMEVAL tv = {dwTimeout, 0};
    int nBytesReceived=0;
    if(select(0, &fd, NULL, NULL, &tv) == 0) 
        goto CLEAR;
    if((nBytesReceived = recv(hSocket, pszBuffer, nBufferSize, 0)) == SOCKET_ERROR)
        goto CLEAR;
    return nBytesReceived;

CLEAR:
     SetLastError(WSAGetLastError());//超时
    return(SOCKET_ERROR);
}

SelectSend(...)


int SelectSend(SOCKET hSocket,char const * pszBuffer, 
        int nBufferSize, DWORD dwTimeout)

{
    if(hSocket==NULL)
        return(SOCKET_ERROR);
    int nBytesSent = 0;
    int nBytesThisTime;
    const char* pszTemp = pszBuffer;
    do {
         nBytesThisTime = Send_Block(hSocket,pszTemp, nBufferSize-nBytesSent, dwTimeout);
        if(nBytesThisTime<0)
            return(SOCKET_ERROR);
        //如果一次没有发送成功
         nBytesSent += nBytesThisTime;
        //改变当前字符指针
         pszTemp += nBytesThisTime;
     } while(nBytesSent < nBufferSize);
    return nBytesSent;
}

这样就可以利用上面两个函数写发送和接收程序了!我们下面来看看如何使用select模型建立我们的Server,下面分了4个文件,大家可以拷贝下来实际编译一下。首先有两个公用的文件:CommonSocket.h、CommonCmd.h。他们都是放在 Common Include 目录中的!然后就是SelectServer.cpp和SocketClient.cpp这两个文件,可以分别建立两个工程把他们加进去编译!有些关键的地方我都写在文件注释里了,可以自己看看。为了方便大家拷贝,我就不用“代码插入”的方式了。直接贴到下面!

/**********************************************************************************************************************/

第一个文件

/*/
文件:SelectServer.cpp
说明:

此文件演示了如何使用select模型来建立服务器,难点是select的writefds在什么时候使用。
好好看看代码就能很明白的了,可以说我写这些代码就是为了探索这个问题的!找了很多资料都找不到!!

在这里我怀疑是否可以同时读写同一个SOCKET,结果发现是可以的,但是最好别这样做。因为会导致包的顺序不一致。

    这里说一下SELECT模型的逻辑:
我们如果不使用select模型,在调用recv或者send时候会导致程序阻塞。如果使用了select
就给我们增加了一层保护,就是说在调用了select函数之后,对处于读集合的socket进行recv操作
是一定会成功的(这是操作系统给我们保证的)。对于判断SOCKET是否可写时也一样。
而且就算不可读或不可写,使用了select也不会锁 死!因为 select 函数提供了超时!利用这个特性还可以
做异步connect,也就是可以扫描主机,看哪个主机开了服务(远程控制软件经常这样干哦!)

我们如何利用这种逻辑来设计我们的server呢?
这里用的方法是建立一个SocketInfo,这个SocketInfo包括了对Socket当前进行的操作,我把它分为:
{RecvCmd, RecvData, ExecCmd} 一开始socket是处于一个RecvCmd的状态,
然后取到了CMD(也就是取到了指令,可想象一下CPU得到了指令后干什么),然后就要取数据了,取得指令
知道要干什么,取得了数据就可以实际开始干了。实际开始干就是ExecCmd,在这个状态之后都是需要
发送数据的了,所以把他们都放在判断SOCKET可写下面<就是 if(FD_ISSET(vecSocketInfo[i].sock, &fdWrite)) >,
即当Socket可写就可以发送信息给客户端了。

发送的根本协议是这样的:先发一个SCommand的结构体过去,这个结构体说明了指令和数据的长度。
然后就根据这个长度接收数据。最后再给客户端做出相应的响应!

    根据这种代码结构,可以很方便的添加新的功能。

错误处理做得不太好,以后再补充了。

其他的如注释,结构,命名等的编码规范都用了个人比较喜欢的方式。

输出:
..BinSelectServer.exe

用法:
直接启动就可以了

Todo:
下一步首先完成各个SOCKET的模型,然后公开自己的研究代码。
功能方面就是:
1、服务器可以指定共享文件夹
2、客户端可以列出服务器共享了哪些文件
3、客户端可以列出哪些用户在线,并可以发命令和其他用户聊天
4、加上界面
/*/

#include <winsock2.h>
#pragma comment(lib, "WS2_32")

#include <windows.h>

#pragma warning(disable: 4786)
#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <algorithm>
using namespace std;

#include "..Common IncludeCommonSocket.h"
#include "..Common IncludeCommonCmd.h"

typedef struct tagSocketInfo
{
SOCKET sock;
ECurOp eCurOp;
SCommand cmd;
char *data;
}SSocketInfo;

// 登录用户的列表
map<string, SOCKET> g_LoginUsers;

// 注册用户的列表(用户名,密码)
map<string, string> g_RegUSers;

// 用于退出服务器
bool g_bExit = false;

void DoRecvCmd(vector<SSocketInfo> &vecSockInfo, int idx);
void DoRecvData(vector<SSocketInfo> &vecSockInfo, int idx);
void DoExecCmd(vector<SSocketInfo> &vecSockInfo, int idx);

bool DoAuthen(SOCKET sock, char *data, DWORD len);
bool DoGetFile(SOCKET sock, char *data, DWORD len);
bool DoRegister(SOCKET sock, char *data, DWORD len);

void GetRegUsers();

///////////////////////////////////////////////////////////////////////
//
// 函数名       : RemoveByIndex
// 功能描述     : 根据 index 来删除 VECTOR 里的元素
// 参数         : vector<T> &vec [in]
// 参数         : int nIdx   [in]
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
template<class T>
void EraseByIndex(vector<T> &vec, int nIdx)
{
vector<T>::iterator it;
it = vec.begin() + nIdx;
vec.erase(it);
}

void main()
{
InitWinsock();

vector<SSocketInfo> vecSocketInfo;

SOCKET sockListen = BindServer(PORT);
ULONG NonBlock = 1;
ioctlsocket(sockListen, FIONBIO, &NonBlock);
SOCKET sockClient;

GetRegUsers();

FD_SET fdRead;
FD_SET fdWrite;
while(!g_bExit)
{
   // 每次调用select之前都要把读集和写集清空
   FD_ZERO(&fdRead);
   FD_ZERO(&fdWrite);
  
   // 设置好读集和写集
   FD_SET(sockListen, &fdRead);
   for(int i = 0; i < vecSocketInfo.size(); i++)
   {
    FD_SET(vecSocketInfo[i].sock, &fdRead);
    FD_SET(vecSocketInfo[i].sock, &fdWrite);
   }

   // 调用select函数
   if(select(0, &fdRead, &fdWrite, NULL, NULL) == SOCKET_ERROR)
   {
    OutErr("select() Failed!");
    break;
   }

   // 说明可以接受连接了
   if(FD_ISSET(sockListen, &fdRead))
   {
    char szClientIP[50];
    sockClient = AcceptClient(sockListen, szClientIP);
    cout << szClientIP << " 连接上来" << endl;

    ioctlsocket(sockClient, FIONBIO, &NonBlock);

    SSocketInfo sockInfo;
    sockInfo.sock = sockClient;
    sockInfo.eCurOp = RecvCmd;
    // 把接收到的这个socket加入自己的队列中
    vecSocketInfo.push_back(sockInfo);
   }

   for(i = 0; i < vecSocketInfo.size(); i++)
   {
    // 如果可读
    if(FD_ISSET(vecSocketInfo[i].sock, &fdRead))
    {
     switch(vecSocketInfo[i].eCurOp)
     {
     case RecvCmd:
      DoRecvCmd(vecSocketInfo, i);
      break;

     case RecvData:
      DoRecvData(vecSocketInfo, i);
      break;
     
     default:
      break;
     }
    }

    // 如果可写
    if(FD_ISSET(vecSocketInfo[i].sock, &fdWrite))
    {
     switch(vecSocketInfo[i].eCurOp)
     {
     case ExecCmd:
      DoExecCmd(vecSocketInfo, i);
      break;
    
     default:
      break;
     }
    }
   }
}
}


///////////////////////////////////////////////////////////////////////
//
// 函数名       : DoRecvCmd
// 功能描述     : 获取客户端传过来的cmd
// 参数         : vector<SSocketInfo> &vecSockInfo
// 参数         : int idx
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void DoRecvCmd(vector<SSocketInfo> &vecSockInfo, int idx)
{
SSocketInfo *sockInfo = &vecSockInfo[idx];
int nRet = RecvFix(sockInfo->sock, (char *)&(sockInfo->cmd), sizeof(sockInfo->cmd));

// 如果用户正常登录上来再用 closesocket 关闭 socket 会返回0 
// 如果用户直接关闭程序会返回 SOCKET_ERROR,强行关闭
if(nRet == SOCKET_ERROR || nRet == 0)
{
   OutMsg("客户端已退出。");
   closesocket(sockInfo->sock);
   sockInfo->sock = INVALID_SOCKET;     
   EraseByIndex(vecSockInfo, idx);
   return;
}
sockInfo->eCurOp = RecvData;
}


///////////////////////////////////////////////////////////////////////
//
// 函数名       : DoRecvData
// 功能描述     : DoRecvCmd 已经获得了指令,接下来就要获得执行指令所需要的数据
// 参数         : vector<SSocketInfo> &vecSockInfo
// 参数         : int idx
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void DoRecvData(vector<SSocketInfo> &vecSockInfo, int idx)
{
SSocketInfo *sockInfo = &vecSockInfo[idx];
// 为数据分配空间,分配多一位用来放最后的0
sockInfo->data = new char[sockInfo->cmd.DataSize + 1];
memset(sockInfo->data, 0, sockInfo->cmd.DataSize + 1);
// 接收数据
int nRet = RecvFix(sockInfo->sock, sockInfo->data, sockInfo->cmd.DataSize);
if(nRet == SOCKET_ERROR || nRet == 0)
{
   OutMsg("客户端已退出。");
   closesocket(sockInfo->sock);
   sockInfo->sock = INVALID_SOCKET;     
   EraseByIndex(vecSockInfo, idx);
   return;
}
  
sockInfo->eCurOp = ExecCmd;
}


///////////////////////////////////////////////////////////////////////
//
// 函数名       : DoExecCmd
// 功能描述     : 指令和执行指令所需数据都已经准备好了,接下来就可以执行命令
// 参数         : vector<SSocketInfo> &vecSockInfo
// 参数         : int idx
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void DoExecCmd(vector<SSocketInfo> &vecSockInfo, int idx)
{
SSocketInfo *sockInfo = &vecSockInfo[idx];
switch(sockInfo->cmd.CommandID) 
{
case CMD_AUTHEN:
   DoAuthen(sockInfo->sock, sockInfo->data, sockInfo->cmd.DataSize);
    break;
case CMD_GETFILE:
   DoGetFile(sockInfo->sock, sockInfo->data, sockInfo->cmd.DataSize);
   break;
case CMD_REGISTER:
   DoRegister(sockInfo->sock, sockInfo->data, sockInfo->cmd.DataSize);
   break;
default:
   break;
}

// 执行完命令后就设置回接收指令状态
sockInfo->eCurOp = RecvCmd;
}

///////////////////////////////////////////////////////////////////////
//
// 函数名       : DoAuthen
// 功能描述     : 对用户名和密码做验证
// 参数         : SOCKET sock
// 参数         : char *data
// 参数         : DWORD len
// 返回值       : bool 
//
///////////////////////////////////////////////////////////////////////
bool DoAuthen(SOCKET sock, char *data, DWORD len)
{
// 取得用户名和密码的字符串
// 格式为 "dyl 123"

char *pBuf = data;
int nIdx = 0;
char szName[10];
memset(szName, 0, 10);
char szPass[10];
memset(szPass, 0, 10);
while (*pBuf != ' ')
{
   szName[nIdx++] = *pBuf++;
}
szName[nIdx] = '';

*pBuf++;

nIdx = 0;
while (*pBuf != '')
{
   szPass[nIdx++] = *pBuf++;
}
szPass[nIdx] = '';


char szSend[30];
memset(szSend, 0, 30);
bool bUserExist = false;

if( g_RegUSers.find(string(szName)) != g_RegUSers.end() )
{
   if(strcmp(g_RegUSers[szName].c_str(), szPass) == 0)
   {
    strcpy(szSend, "UP OK!");
    g_LoginUsers[szName] = sock;
   }
   else
   {
    strcpy(szSend, "P Err!");
   }  
}
else
{
// 不存在这个用户
   strcpy(szSend, "U Err!");
}
int nRet = SendFix(sock, szSend, strlen(szSend));

if(nRet == SOCKET_ERROR)
   return false;

// 执行完了就释放data
delete []data;

return true;
}

  评论这张
 
阅读(917)| 评论(0)

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018