天天看点

Windows下select模型服务端编程

select模型是一种比较常用的IO模型。利用该模型可以使Windows socket应用程序可以同时管理多个套接字。 使用select模型,可以使当执行操作的套接字满足可读可写条件时,给应用程序发送通知,收到这个通知后,应用程序再去调用相应的Windows socket API去执行函数调用。

Select模型的核心是select函数。调用select函数检查当前各个套接字的状态。根据函数的返回值判断套接字的可读可写性。然后调用相应的Windows Sockets API完成数据的发送、接收等。

Select模型是Windows sockets中最常见的IO模型。它利用select函数实现IO 管理。通过对select函数的调用,应用程序可以判断套接字是否存在数据、能否向该套接字写入数据。 如:在调用recv函数之前,先调用select函数,如果系统没有可读数据那么select函数就会阻塞在这里。当系统存在可读或可写数据时,select函数返回,就可以调用recv函数接收数据了。

select 函数

int WSAAPI select(
  int           nfds,
  fd_set        *readfds,
  fd_set        *writefds,
  fd_set        *exceptfds,
  const timeval *timeout
);
           

参数说明:

  • nfds 一般设置为0,可以忽略,主要是为了兼容其他系统参数兼容。
  • readfds 准备接收数据的套接字集合,即可读性集合。
  • writefds 准备发送数据的套接字集合,即可写性集合
  • exceptfds,检查错误套接字集合指针
  • timeout,等待时间,设置为NULL时,表示永久等待,直到有事件发生返回。

函数说明:

当程序执行select函数时,程序被阻塞,直至内核检测到有可读可写等套接字时才返回,并修改fd_set集合中数据,这些的数据都是可读可写socket集合,不存在的或没有完成IO操作的套接字会被‘删除’,返回值是这些可读可写集合的数量。

若设置超时,则超时时间达到后,函数返回值为0。

需要说明的是,select函数三个套接字指针集合,至少需要传入一个集合才可以 。

例如:

//检测可读可写
select(0,&read_set,&write_set,NULL,NULL);
//只检测可写套接字
select(0,&read_set,NULL,NULL,NULL);
//都检测
select(0,&read_set,&write_set,&except_set,NULL);
           

fd_set结构体

fd_se是一个结构体,其定义如下:

#ifndef FD_SETSIZE
#define FD_SETSIZE      64
#endif /* FD_SETSIZE */

typedef struct fd_set 
{
	u_int fd_count;                 /* how many are SET? */
	SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;
           

fd_cout 表示该集合套接字数量。最大为64.

fd_array套接字数组。

readfds数组将包括满足以下条件的套接字:

1:有数据可读。此时在此套接字上调用recv,立即收到对方的数据。

 2:连接已经关闭、重设或终止。

 3:正在请求建立连接的套接字。此时调用accept函数会成功。
           

writefds数组包含满足下列条件的套接字:

1:有数据可以发出。此时在此套接字上调用send,可以向对方发送数据。

2:调用connect函数,并连接成功的套接字。
           

exceptfds数组将包括满足下列条件的套接字:

1:调用connection函数,但连接失败的套接字。

2:有带外(out of band)数据可读。
           

timeval 结构

timeval表示超时时间结构体,其定义如下:

structure timeval  
{  
    long tv_sec;//秒。  
    long tv_usec;//毫秒。  
}; 
           

当timeval为空指针时,select会一直等待,直到有符合条件的套接字时才返回。

当tv_sec和tv_usec之和为0时,无论是否有符合条件的套接字,select都会立即返回。

当tv_sec和tv_usec之和为非0时,如果在等待的时间内有套接字满足条件,则该函数将返回符合条件的套接字。如果在等待的时间内没有套接字满足设置的条件,则select会在时间用完时返回,并且返回值为0。

fd_set操作函数

为了方便使用,windows sockets提供了下列宏,用来对fd_set进行一系列操作。使用以下宏可以使编程工作简化。

FD_CLR(s,set);从set集合中删除s套接字。

FD_ISSET(s,set);检查s是否为set集合的成员。

FD_SET(s,set);将套接字加入到set集合中。

FD_ZERO(set);将set集合初始化为空集合。
           

FD宏源码如下:

//fd这个socket从set集合中‘移除’,并且set集合中的数量减一。
#define FD_CLR(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
            while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
                ((fd_set FAR *)(set))->fd_array[__i] = \
                    ((fd_set FAR *)(set))->fd_array[__i+1]; \
                __i++; \
            } \
            ((fd_set FAR *)(set))->fd_count--; \
            break; \
        } \
    } \
} while(0)

//检测fd是否已经在set中,不存在则新添加一个。
#define FD_SET(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == (fd)) { \
            break; \
        } \
    } \
    if (__i == ((fd_set FAR *)(set))->fd_count) { \
        if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) { \
            ((fd_set FAR *)(set))->fd_array[__i] = (fd); \
            ((fd_set FAR *)(set))->fd_count++; \
        } \
    } \
} while(0)

//将集合数量设置为0
#define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)
//判断fd释放在set结合中
#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))

           

select和FD宏使用

在调用select函数对套接字进行监视之前,必须将要监视的套接字分配给上述三个数组中的一个。然后调用select函数,再次判断需要监视的套接字是否还在原来的集合中。就可以知道该集合是否正在发生IO操作

在开发Windows sockets应用程序时,通过下面的步骤,可以完成对套接字的可读写判断:

  1. 使用FD_ZERO初始化套接字集合。如FD_ZERO(&readfds);
  2. 使用FD_SET将某套接字放到readfds内,用于select检测,如: FD_SET(s,&readfds);
  3. 以readfds为第二个参数调用select函数。select在返回时会返回所有fd_set集合中套接字的总个数,并对每个集合进行相应的更新。将满足条件的套接字放在相应的集合中。
  4. 使用FD_ISSET判断s是否还在某个集合中。如: FD_ISSET(s,&readfds);
  5. 调用相应的Windows socket api 对某套接字进行操作。

select返回后会修改每个fd_set结构。删除不存在的或没有完成IO操作的套接字。这也正是在第四步中可以使用FD_ISSET来判断一个套接字是否仍在集合中的原因。

#include "stdafx.h"
#include <WinSock2.h>
#pragma  comment(lib, "ws2_32.lib")

int _tmain(int argc, _TCHAR* argv[])
{
    //加载套接字库
    WORD wVersionRequested;
    WSADATA wsaData = {0};
    wVersionRequested = MAKEWORD(1,1);

    if (WSAStartup(wVersionRequested, &wsaData) != 0 ) 
    {
        return 1;
    }

    if (LOBYTE(wsaData.wVersion) != 1 ||
        HIBYTE(wsaData.wVersion) != 1) 
    {
        WSACleanup( );
        return 1; 
    }

    int nResult = 0; // used to return function results

    //创建用于监听的套接字
    SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM,0);
    if (INVALID_SOCKET == ListenSocket)
    {
        printf("create socket error (%d)\n",::WSAGetLastError());
        WSACleanup();
        return 1;
    }

    //创建socket信息
    SOCKADDR_IN addrSrv;
    addrSrv.sin_family = AF_INET;//地址族
    addrSrv.sin_port = htons(6100);//端口号
    //监听本机所有的主机地址,即不关心数据从哪个网卡过来
    addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    //将端口号/IP和套接字绑定在一起
    nResult = bind(ListenSocket,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR_IN));
    if (nResult == SOCKET_ERROR)
    {
        printf("bind socket error code = %d\n",::WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    
    //将套接字设为监听模式,准备接收客户请求
    nResult = listen(ListenSocket,SOMAXCONN);
    if (SOCKET_ERROR == nResult)
    {
        printf("listen socket error(%d)\n",::WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    fd_set allSocket;
    FD_ZERO(&allSocket);
    //监听套接字加入集合
    FD_SET(ListenSocket,&allSocket);

    printf("服务端启动监听...\n");
    
    while (TRUE)
    {
        fd_set read_set;
        FD_ZERO(&read_set);
        //可读结合
        read_set = allSocket;
        //更新
        nResult = select(0,&read_set,NULL,NULL,NULL);
        if (SOCKET_ERROR == nResult)
        {
            printf("select error...\n");
            break;
        }
        //判断监听套接字是否有连接请求
        if (FD_ISSET(ListenSocket,&read_set))
        {
            SOCKADDR_IN ClientAddr;
            int nLen = sizeof(ClientAddr);
            SOCKET socketClient = accept(ListenSocket,(sockaddr*)&ClientAddr, &nLen);
            if (INVALID_SOCKET == socketClient)
            {
                printf("accept socket is invalid...\n");
                continue;
            }
            //新建连接
            FD_SET(socketClient, &allSocket);

            char* pszClientIP = inet_ntoa(ClientAddr.sin_addr);
            if(NULL != pszClientIP)
            {
                printf("有新客户端[%s:%d]端请求连接\n",pszClientIP,ntohs(ClientAddr.sin_port));
                printf("目前客户端的数量为: %d\n",allSocket.fd_count - 1);
            }

            continue;
        }

        for (unsigned i = 0; i < allSocket.fd_count; i++)
        {
            SOCKET socket = allSocket.fd_array[i];

            SOCKADDR_IN addrClient;
            int nLen = sizeof(addrClient);
            getpeername(socket,(sockaddr*)&addrClient, &nLen);
            char* pszClientIp = inet_ntoa(addrClient.sin_addr);
            unsigned short usClientPort = ntohs(addrClient.sin_port);
             //某个socket上友数据可以接收
            if (FD_ISSET(socket,&read_set))
            {
                char szMsg[128] = {};
                int nResult = recv(socket,szMsg,sizeof(szMsg),0);
                if (nResult > 0)
                {
                    printf("--------客户端[%s:%d]--------\n",pszClientIp,usClientPort);
                    printf("消息长度%d字节\n",nResult);
                    printf("消息内容: %s\n",szMsg);
                }
                else if (0 == nResult)
                {
                    //对方关闭连接
                    printf("客户端[%s:%d]主动关闭连接...\n",pszClientIp,usClientPort);
                    closesocket(socket);
                    FD_CLR(socket,&allSocket);
                    printf("目前客户端的数量为: %d\n",allSocket.fd_count - 1);
                    continue;
                }
                else
                {
                    DWORD err = WSAGetLastError();
                    // 客户端的socket没有被正常关闭,即没有调用closesocket
                    if (err == WSAECONNRESET)
                    {
                        printf("客户端[%s:%d]被强行关闭",pszClientIp,usClientPort);
                    }
                    else
                    {
                        printf("recv data error code(%d)...\n",::WSAGetLastError());
                    }

                    closesocket(socket);
                    FD_CLR(socket, &allSocket);

                    //监听socket不算客户端端
                    printf("目前客户端的数量为: %d\n",allSocket.fd_count - 1);

                    continue;

                }

            }//read end
        }
    }

    //关闭监听套接字
    closesocket(ListenSocket); 

    //清理套接字库的使用
    WSACleanup();
    return 0;
}
           

通过select模型服务端可以管理多个客户端请求消息和请求连接,不会因为recv函数没有数据造成阻塞。

以下是服务端运行结果:

Windows下select模型服务端编程

作者:AncientCastle

来源:CSDN

原文:https://blog.csdn.net/hq354974212/article/details/76154849

版权声明:本文为博主原创文章,转载请附上博文链接!