select 模型

  1. select模型的意义是在单线程中处理 多个socket链接
  2. 由于accept ,recv 会阻塞线程,因此出现了select
  3. select可用于判断是否有 accept ,recv事件,有的话再处理
  4. 避免了长时间的线程阻塞
  5. 理论上一个线程可以处理 64个socket链接

    1. 这里 注意下 windows平台下 可以自定义突破 64的限制
    2. 官方文档 自定义sockst数量
  6. 这里有一点要注意,select->recv 收到的数据可能是不完整的

    1. 解决办法,每个socket都配一个缓冲区,用于缓冲所有数据
    2. 每次接受数据后都判断 这个socket有没有收到一个完整的数据包
    3. 这里还要小心点,缓冲区可能有多个数据包的实体
    4. 重点是 如何在 缓冲区中 分离提取数据

函数原型

int select(
        int nfds,   //忽略,仅为了兼容
        fd_set FAR *readfds, //指向一个套接字集合,用来检查其可读性
        fd_set FAR *writefds,//指向一个套接字集合,用来检查其可写性
        fd_set FAR *exceptfds,//指向一个套接字集合,用来检查错误
        const struct timeval FAR *timeout  //指定此函数等待的最长时间//  //如果为NULL,则最长时间为无限大。
        );
        
//关键函数
fd_set 结构:套接字集合。Select函数可以测试这个集合中哪些套接字有事件发生。
FD_ZERO(*set)  初始化set为空,set在使用前需要清空
FD_SET(s, *set)  添加套接字到集合
FD_ISSET(s, *set)  检查s是不是set的成员,如果是则返回TRUE
FD_CLR(s, *set)  从set移除套接字s

单线程服务多socket实例

#include "pch.h"
#include <thread>
#include <iostream>
#include <Winsock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

/*
windows专属的 初始化api
使用Winsock 之前一定要运行这段代码
*/
void initial_win_socket()
{
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD(2, 2);

    err = WSAStartup(wVersionRequested, &wsaData);
    if(err != 0)
    {
        /* Tell the user that we could not find a usable */
        /* WinSock DLL.                                  */
        return;
    }

}


//回掉函数的函数指针
typedef int(*p_socket_callback)(SOCKET, sockaddr_in);

//每当建立一个链接就调用这个函数
//建议在线程中条用

/*
关于 recv 的 返回值
0 表示 通道正常关闭(close/closesocket)
-1 表示 通道异常关闭(没有 close/closesocket 程序直接关闭了,有操作系统发送这数据)
*/
int socket_callback(SOCKET in_sock, sockaddr_in recvName)
{

    printf("开始处理 socket: %ld \r\n", in_sock);
    int nRet = 0;

    //while(1)
    //{
        //接收数据
    char data_rec[128] = { 0 };
    char ip_sender[128] = { 0 };

    inet_ntop(AF_INET, &(recvName.sin_addr), ip_sender, 16); //获取客户端的ip
    nRet = recv(in_sock, data_rec, sizeof(data_rec), 0);     //获取接收到的数据

    if(nRet == SOCKET_ERROR || nRet == 0)
    {
        printf("数据接收 异常 错误码:%d\r\n", nRet);
        return nRet;
        //break;
    }
    printf("receive from %s:%d : %s\r\n", ip_sender, ntohs(recvName.sin_port), data_rec);

    //向客户端发送数据
    char str_to_clinet[] = "这是一条服务器回复的数据";
    nRet = send(in_sock, str_to_clinet, sizeof(str_to_clinet), 0);
    if(nRet == SOCKET_ERROR || nRet == 0)
    {
        printf("发送数据 异常 错误码:%d\r\n", nRet);
        return nRet;
        //break;
    }
    //}

    printf("结束处理 socket: %ld \r\n", in_sock);
    /*closesocket(in_sock);*/
}


/*
输入  ip端口 开始绑定
输入  回掉函数,每建立一个socks链接就会调用
最后  别忘记 在回掉函数中关闭 socket接口
closesocket(s);

*/
void bind_port(const char * bind_ip, int bind_port, p_socket_callback fx_cb = NULL)
{
    //1 建立SOCKET(套接字)
    SOCKET s = socket(AF_INET,
                      SOCK_STREAM,//数据流形式
                      IPPROTO_TCP);
    if(s == INVALID_SOCKET)
    {
        return;
    }
    int nRet;

    //2 配置监听端口的结构体
    sockaddr_in name;
    name.sin_family = AF_INET;
    name.sin_port = htons(bind_port);
    inet_pton(AF_INET, bind_ip, &(name.sin_addr.S_un.S_addr));//将ip地址转化为 api使用的比特流形式

    // 3 将socket 绑定到端口
    nRet = bind(s,
        (struct sockaddr*)&name,
                sizeof(name));

    if(nRet == SOCKET_ERROR)
    {
        return;
    }
    //4 开始监听
    nRet = listen(s, SOMAXCONN/* 设置socket队列最大数量*/);
    if(nRet == SOCKET_ERROR)
    {
        return;
    }


    //5 配置 select模型
    fd_set fdReadTotal;//我们把所有要监听的socket链接放在这里

    fd_set fdRead;// 由于每次检查后都会改变队列.我们那这个作为 fdReadTotal 的马甲去select

    FD_ZERO(&fdRead);
    FD_ZERO(&fdReadTotal);

    SOCKET sClient;
    FD_SET(s, &fdReadTotal);//将监听 连接建立的socket先放进去

    // 配置 select 的延时 
    timeval timeout;
    timeout.tv_sec = 5;
    timeout.tv_usec = 0;

    while(1)
    {


        // 配置接受链接信息的结构体
        sockaddr_in recvName = { 0 };
        recvName.sin_family = AF_INET;
        int nLength = sizeof(sockaddr_in);


        //1 配置马甲
        fdRead = fdReadTotal;

        //2 select选择
        nRet = select(0, &fdRead, NULL, NULL, &timeout);
        if(nRet == SOCKET_ERROR)
        {
            //表示出错了
            return;
        }
        else if(nRet == 0)
        {
            //表示等待超时
            printf("wait for select!");
        }
        else//表示成功的等到了对应的socket事件触发
        {

            /*
            如果是 listen的socket句柄在里面
            说明有新的链接要建立
            */
            if(FD_ISSET(s, &fdRead))
            {
                //表示可以接收连接请求了
                sClient = accept(s, (sockaddr*)&recvName, &nLength);
                FD_SET(sClient, &fdReadTotal);
            }
            else//否则我们就循环检查所有其他socket通信句柄
            {

                for(int i = 0; i < fdReadTotal.fd_count; i++)
                {
                    if(FD_ISSET(fdReadTotal.fd_array[i], &fdRead))
                    {
                        //表示可以收包了
                        SOCKET curSocket = fdReadTotal.fd_array[i];

                        /*
                        在正常逻辑中 我们这里需要使用多线程处理 链接
                        但是由于使用的是select模型直接处理即可
                        */
                        //std::thread t(fx_cb, sClient, recvName);
                        //t.detach();

                        int res = fx_cb(curSocket, recvName);

                        if(res <= 0)//如果socket句柄异常那么就删除这个句柄
                        {
                            closesocket(curSocket);
                            FD_CLR(curSocket, &fdReadTotal);
                        }
                    }
                }

            }

        }
    }

    closesocket(s);
}
int main()
{
    initial_win_socket();
    bind_port("0.0.0.0", 10086, socket_callback);
}

Last modification:November 13, 2018
如果觉得我的文章对你有用,请随意赞赏