通信编程:Select 模型通信

目录

非阻塞模式

Winsock 可以在阻塞和非阻塞模式下执行 I/O 操作,套接字创建时默认工作在阻塞模式下。也就是说当某个操作不能执行时,程序会先阻塞,等待操作可以被执行时才继续程序。例如对 recv 函数的调用会使程序进入等待状态,直到接收到数据才返回。
阻塞套接字的好处是使用简单,但是当需要处理多个套接字连接时,就必须创建多个线程,给编程带来了许多不便。所以实际开发中使用最多的还是非阻塞模式,它使用起来比较复杂,但是处理发送和接收数据或者管理连接的 Winsock 调用将会立即返回,效率很高。
不过如果系统输入缓冲区中没有待处理的数据,那么对 recv 的调用将返回 WSAEWOULDBLOCK 错误。关键的问题在于如何确定套接字什么时候可读/可写,如果需要不断调用函数去测试的话,程序的性能势必会受到影响,解决的办法就是使用 Windows 提供的不同的 I/O 模型。

Select 模型

select 模型的设计源于 UNIX 系统,主要实现的原理是 IO 多路复用。select 模型的优势是程序能够在单个线程内同时处理多个套接字连接,这避免了阻塞模式下的线程膨胀问题。但是添加到 fd_set 结构的套接字数量是有限制的,如果能能添加的 socket 太多的话,服务器性能就会受到影响。
通信编程:Select 模型通信

select 函数

模型通过使用 select 函数来管理 I/O,函数可以确定一个或者多个套接字的状态。如果套接字上没有网络事件发生,便进入等待状态,以便执行同步 I/O。

int
WSAAPI
select(
    _In_ int nfds,
    _Inout_opt_ fd_set FAR * readfds,
    _Inout_opt_ fd_set FAR * writefds,
    _Inout_opt_ fd_set FAR * exceptfds,
    _In_opt_ const struct timeval FAR * timeout
    );

函数调用成功返回发生网络事件的所有 socket 数量的综合,超过时间限制就返回 0.

参数 说明
nfds 忽略,为了与 Berkeley 套接字兼容
readfds 指向一个套接字集合,用来检查其可读性
writefds 指向一个套接字集合,用来检查其可写性
exceptfds 指向一个套接字集合,用来检查错误
timeout 指定此函数等待的最长时间,为 NULL 时最长时间为无限大

套接字集合

fd_set 结构是 socket 集合,它可以把多个套接字连在一起,select 函数可以测试这个集合中哪些套接字有事件发生。

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

WINSOCK 定义了 4 个操作 fd_set 的宏。

功能
FD_ZERO(*set) 初始化 set 为空集合,集合在使用前应该总是清空
FD_CLR(s, *set) 从 set 移除套接字 s
FD_ISSET(s, *set) 检查 s 是不是 set 的成员,如果是返回 TRUE
FD_SET(s, *set) 添加套接字到集合

网络事件

传递给 select 函数的 3 个 fd_set 结构分别用于为了检查可读性(readfds)检查可写性(writefds)检查错误(exceptfds)。当我们想要测试某个 socket 的某种状态是,就把它放入对应的 fd_set 中,等待 select 函数返回。select 函数调用完成后,若 socket 还在 fd_set 中,就说明该 socket 满足可读、可写或者出错了。

设置超时

timeout 是 timeval 结构的指针,它指定了 select 函数等待的最长时间。

/*
 * Structure used in select() call, taken from the BSD file sys/time.h.
 */
struct timeval {
        long    tv_sec;         /* seconds */
        long    tv_usec;        /* and microseconds */
};
参数 说明
tv_sec 等待多少秒
tv_usec 等待多少毫秒

如果 timeout 设为 NULL,select 将会无限阻塞。

Select 模型样例

注意无论是客户端还是服务器,都需要包含头文件 initsock.h 来载入 Winsock。

功能设计

模拟实现 TCP 协议通信过程,要求编程实现服务器端与客户端之间双向数据传递。也就是在一条 TCP 连接中,客户端和服务器相互发送一条数据即可。

服务器

使用 Select 模型实现的服务器需要按照如图所示的步骤进行编程,具体编码如下所示。
通信编程:Select 模型通信

#include "initsock.h"
#include <iostream>
using namespace std;

CInitSock theSock;      // 初始化Winsock库
int main()
{
    // 创建监听套接字
    SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(4567);
    sin.sin_addr.S_un.S_addr = INADDR_ANY;
    // 绑定套接字到本地机器
    if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
        cout << " Failed bind()" << endl;
        return -1;
    }
    // 进入监听模式
    if (::listen(sListen, 5) == SOCKET_ERROR)
    {
        cout << " Failed listen()" << endl;
        return 0;
    }
    cout << "服务器已启动监听,可以接收连接!" << endl;

    // select模型处理过程
    // 1)初始化一个套接字集合fdSocket,添加监听套接字句柄到这个集合
    fd_set fdSocket;        // 所有可用套接字集合
    FD_ZERO(&fdSocket);
    FD_SET(sListen, &fdSocket);
    while (TRUE)
    {
        // 2)将fdSocket集合的一个拷贝fdRead传递给select函数,
        // 当有事件发生时,select函数移除fdRead集合中没有未决I/O操作的套接字句柄,然后返回。
        fd_set fdRead = fdSocket;
        int nRet = ::select(0, &fdRead, NULL, NULL, NULL);
        if (nRet > 0)
        {
            // 3)通过将原来fdSocket集合与select处理过的fdRead集合比较,
            // 确定都有哪些套接字有未决I/O,并进一步处理这些I/O。
            for (int i = 0; i < (int)fdSocket.fd_count; i++)
            {
                if (FD_ISSET(fdSocket.fd_array[i], &fdRead))
                {
                    if (fdSocket.fd_array[i] == sListen)    // (1)监听套接字接收到新连接
                    {
                        if (fdSocket.fd_count < FD_SETSIZE)
                        {
                            sockaddr_in addrRemote;
                            int nAddrLen = sizeof(addrRemote);
                            //接收客户端的连接请求
                            SOCKET sNew = ::accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
                            FD_SET(sNew, &fdSocket);
                            cout << "\n与主机" << ::inet_ntoa(addrRemote.sin_addr) << "建立连接" << endl;
                        }
                        else
                        {
                            cout << " Too much connections!" << endl;
                            continue;
                        }
                    }
                    else
                    {
                        char szText[256];
                        int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
                        if (nRecv > 0)    // (2)可读
                        {
                            //接收数据
                            szText[nRecv] = '\0';
                            cout << "  接收到数据:" << szText << endl;
                            //发送数据
                            char result[20];
                            char sendText[] = "你好,客户端!";
                            if(::send(fdSocket.fd_array[i], sendText, strlen(sendText), 0) > 0)
                            {
                                cout << "  向客户端发送数据:" << sendText << endl;
                            }
                        }
                        else    // (3)连接关闭、重启或者中断
                        {
                            ::closesocket(fdSocket.fd_array[i]);
                            FD_CLR(fdSocket.fd_array[i], &fdSocket);
                        }
                    }
                }
            }
        }
        else
        {
            cout << " Failed select()" << endl;
            break;
        }
    }
    return 0;
}

客户端

#include "InitSock.h"
#include <iostream>
using namespace std;

CInitSock initSock;     // 初始化Winsock库

int main()
{
    // 创建套节字
    SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (s == INVALID_SOCKET)
    {
        cout << " Failed socket()" << endl;
        return 0;
    }

    // 也可以在这里调用bind函数绑定一个本地地址
    // 否则系统将会自动安排
    char address[20] = "127.0.0.1";
    // 填写远程地址信息
    sockaddr_in servAddr;
    servAddr.sin_family = AF_INET;
    servAddr.sin_port = htons(4567);
    // 注意,这里要填写服务器程序(TCPServer程序)所在机器的IP地址
    // 如果你的计算机没有联网,直接使用127.0.0.1即可
    servAddr.sin_addr.S_un.S_addr = inet_addr(address);

    if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
    {
        cout << " Failed connect() " << endl;
        return 0;
    }
    else 
    {
        cout << "与服务器 " << address << "建立连接" << endl;
    }

    char szText[] = "你好,服务器!";
    if (::send(s, szText, strlen(szText), 0) > 0)
    {
        cout << "  发送数据:" << szText << endl;
    }

    // 接收数据
    char buff[256];
    int nRecv = ::recv(s, buff, 256, 0);
    if (nRecv > 0)
    {
        buff[nRecv] = '\0';
        cout << "  接收到数据:" << buff << endl;
    }
    
    // 关闭套节字
    ::closesocket(s);
    return 0;
}

运行效果

通信编程:Select 模型通信

参考资料

《Windows 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社
UNIX再学习 -- 函数 select、poll、epoll

上一篇:JavaSE:单例设计模式


下一篇:经纬度转换 C#类