基于select模型的TCP服务器

实验工程文件下载

https://gryffinbit.lanzous.com/iSR7Gjhg68b

实验目的

  1. 熟悉非阻塞套接字的工作模式
  2. 理解select模型的基本思路

实验环境

Windows10 x64,Visual Studio 2017

实验要求

将第三章的点对点的C/S模型改用select模型。为了方便理解,我们只是关注可读套接字,也就是select函数的第三个和第四个参数均为NULL。

实验原理(模型)

select的函数

int select(
        int nfds,
        fd_set FAR* readfds,
        fd_set FAR* writefds,
        fd_set FAR* exceptfds,
        const struct timeval FAR* timeout
);
  • fd_set数据类型,代表着一系列特定套接字的集合。
  • nfds : 为保持与早期套接字应用程序兼容。
  • readfds: 用于检查可读性。该集合包括想要检查是否符合下述任何一个条件的套接字。
    • 有数据到达,可以读入
    • 连接已关闭、重设或中止
    • 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。
  • wirtefds:用于检查可写性。该集合包括想要检查是否符合下述任何一个条件的套接字。
    • 发送缓冲区已空,可以发送数据。
    • 如果已完成了对一个非锁定连接调用的处理,连接就会成功。
  • exceptfds:用于检查带外数据。该集合包括想要检查是否符合下述任何一个条件的套接字。
    • 假如已完成了对一个非锁定连接调用的处理,连接尝试就会失败。
    • 有带外(out-of-band,OOB)数据可供读取
  • timeout:是一个指向一个timeval结构的指针,用于决定select等待I/O操作完成的最长时间。如果timeout是一个空指针,那么select调用会无限制地等待下去,直到至少有一个套接字符合制定的条件后才结束。

timeval结构的定义

strcut timeval{
    long tv_sec;    //以秒为单位指定等待时间
    long tv_usec;   //以毫秒为单位指定等待时间
};

select模型的操作步骤

用select操作一个或多个套接字句柄,一般采用下述步骤。

  1. 使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set集合。
  2. 使用FD_SET宏,将要检查的套接字句柄添加到自己感兴趣的每哥fd_set集合中,相当于在指定的fd_set集合中,设置好要检查的I/O活动。
  3. 调用select函数,然后等待。select完成返回后,会修改每个fd_set结构,删除那些不存在待决I/O操作的套接字句柄,在各个fd_set集合中返回符合条件的套接字。
  4. 根据select的返回值,使用FD_ISSET宏,对每个fd_set集合进行检查,判断一个待定的套接字是否仍在集合中,便可判断出哪些套接字存在着尚未完成的I/O操作。
  5. 知道了每个集合中未完成的I/O操作之后,对相应的套接字的I/O进行处理,然后返回步骤1,继续进行select处理。

实验过程

  1. 将客户端中调用recv函数的部分都删掉,也就是客户端只能给服务器发消息,不能接收消息。

  2. select模型也需要引用头文件库文件,打开网络库,校验版本,创建监听套接字,绑定地址,开始监听,然后创建一个fd_set结构体,并把监听套接字放这个集合中。

  3. 在一个死循环中调用select函数,并判断select的返回值。

    若返回值为SOCKET_ERROR,则报错并做相应处理;

    若返回值为0, 则继续;

    若返回值大于0,说明select函数有反应。有反应就要区分是监听套接字还是响应套接字导致的。若是监听套接字导致的,就调用accept函数,创建一个新的响应套接字,并把这个响应套接字放到第2步定义的fd_set结构当中。如果是响应套接字,就调用recv函数接收客户机发来的消息。

  4. 最后释放掉所有的套接字,并关闭网络库

  5. 测试。先开始运行select模型,然后在客户端的Debug/Release文件夹中双击exe文件即可打开多个客户端,这些客户端均可给服务器发消息。

  6. 测试完成后,可以在accept函数之后调用send函数,发消息给客户端提示连接成功。相应的,就需要先在客户端调用recv函数接收此消息。

👉创建服务器端

#include <stdio.h>        
#include <stdlib.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
//winsock2.h转到文档,可以查看当且编译器环境支持的最高版本

//引用库文件
#pragma comment(lib,"Ws2_32.lib")

int main(void)
{
    //打开网络库
    WORD wdVersion = MAKEWORD(2, 2);
    WSADATA wsaData;

    if (0 != WSAStartup(wdVersion, &wsaData))
    {
        printf("WSAStartup fail!");
        return -1;
    }

    //校验版本
    if (2 != HIBYTE(wsaData.wVersion) || 2 != LOBYTE(wsaData.wVersion))
    {
        printf("Version fail!");
        //关闭库
        WSACleanup();
        return -1;
    }

    //创建监听套接字
    SOCKET socketListen = socket(AF_INET, SOCK_STREAM, 0);
    if (SOCKET_ERROR == socketListen)
    {
        printf("socket fail!");
        //关闭库
        WSACleanup();
        return -1;
    }

    //绑定地址
    SOCKADDR_IN sockAddress;
    sockAddress.sin_family = AF_INET;//通信域为IPV4
    //sockAddress.sin_addr.s_addr = inet_addr("127.0.0.1");//回送地址,主要用于网络软件测试以及本地机进程间通信
    inet_pton(AF_INET, "127.0.0.1", (void*)&sockAddress.sin_addr);
    sockAddress.sin_port = htons(51314);

    if (SOCKET_ERROR == bind(socketListen, (struct sockaddr*)&sockAddress, sizeof(sockAddress)))
    {
        printf("bind fail!");
        //关闭监听套接字
        closesocket(socketListen);
        //关闭库
        WSACleanup();
        return -1;
    }

    //开始监听
    if (SOCKET_ERROR == listen(socketListen, 5))
    {
        printf("listen fail!");
        //关闭监听套接字
        closesocket(socketListen);
        //关闭库
        WSACleanup();
        return -1;
    }

    //申明一个fd_set结构体存放套接字,可以右键转定义查看它的结构
    fd_set allSockets;
    //初始化
    FD_ZERO(&allSockets);
    //把监听套接字放到fd_set这个集合中
    FD_SET(socketListen, &allSockets);

    while (true)
    {
        //由于给select传入一个fd_set结构体,它只返回fd_set中有响应的套接字,所以需要定义一个中转变量
        fd_set TempSockets = allSockets;

        //最大等待时间
        struct timeval st;
        st.tv_sec = 10;
        st.tv_usec = 0;

        //调用select函数,这里我们将第三个和第四个参数设为了NULL,
        //即只是关心可读的套接字,也就是调用accept和recv的情况
        int ret = select(0, &TempSockets, NULL, NULL, &st);
        //判断select的返回值分为SOCKET_ERROR,0, 和大于0三种情况
        if (ret == SOCKET_ERROR)
        {
            printf("select fail!");
            break;
        }
        
        if (0 == ret) //没有反应,继续
        {
            continue;
        }

        if (ret > 0)
        {
            //select函数有反应,有反应就要区分是监听套接字还是响应套接字导致的
            for (u_int i = 0; i < TempSockets.fd_count; i++)
            {
                //是监听套接字导致的
                if (TempSockets.fd_array[i] == socketListen)
                {
                    //需要创建响应套接字
                    SOCKET SockAccept = accept(socketListen, NULL, NULL);
                    //判断accept函数是否调用失败,失败的返回值是INVALID_SOCKET,不是SOCKET_ERROR
                    if (SockAccept == INVALID_SOCKET)
                    {
                        //创建响应套接字失败
                        printf("创建响应套接字失败!");
                        continue;
                    }
                    //把新创建的响应套接字也放到fd_set集合中
                    FD_SET(SockAccept, &allSockets);
                    //给客户端发消息
                    if (SOCKET_ERROR == send(SockAccept, "我是服务器,您已成功连接", strlen("我是服务器,您已成功连接"), 0))
                    {
                        printf("send fail!");
                        //关闭响应套接字
                        closesocket(SockAccept);
                     //关闭库
                        WSACleanup();
                        return -1;
                    }
                }
                else
                {
                    //是响应套接字导致的,那就需要调用recv函数
                    //判断客户端连接的集合中是否有需要接收的数据
                    char szRecvBuffer[1024] = { 0 };

                    int nReturnValue = recv(TempSockets.fd_array[i], szRecvBuffer, sizeof(szRecvBuffer) - 1, 0);

                    if (0 == nReturnValue)
                    {
                        //客户端正常关闭   
                        printf("客户端已下线");
                        //从集合中删掉Socket,还得调用closesocket关闭这个socket
                        SOCKET SockTemp = TempSockets.fd_array[i];
                        FD_CLR(TempSockets.fd_array[i], &allSockets);
                        closesocket(SockTemp);
                    }
                    else if (SOCKET_ERROR == nReturnValue)
                    {
                        //网络中断 
                        int nRes = WSAGetLastError();
                        printf("响应套接字%d所对应的客户端中断连接\n",  TempSockets.fd_array[i]);
                        //同样需要从集合删掉套接字和关闭它。
                    }
                    else
                    {
                        //接收到客户端消息 
                        printf("Data from Socket %d : %s \n", TempSockets.fd_array[i],szRecvBuffer);
                    }
                }
            }
        }
    }
    //释放所有socket
    for (u_int i = 0; i < allSockets.fd_count; i++)
    {
        closesocket(allSockets.fd_array[i]);
    }
    //关闭网络库
    WSACleanup();

    system("pause");
    return 0;
}

👉创建客户端

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#include <WS2tcpip.h>

#pragma comment(lib,"Ws2_32.lib")

int main()
{
    //打开网络库
    WORD wdVersion = MAKEWORD(2, 2);
    WSADATA wsaData;

    if (0 != WSAStartup(wdVersion, &wsaData))
    {

        printf("WSAStartup fail!");
        return -1;
    }

    //校验版本
    if (2 != HIBYTE(wsaData.wHighVersion) || 2 != LOBYTE(wsaData.wVersion))
    {
        printf("Version fail!");
        //关闭库
        WSACleanup();
        return -1;
    }

    //创建一个SOCKET 监听
    //套接字的类型,使用的协议一定要和服务器的类型和协议匹配上
    SOCKET socketUser = socket(AF_INET, SOCK_STREAM, 0);
    if (SOCKET_ERROR == socketUser)
    {
        printf("socket fail!");
        //关闭库
        WSACleanup();
        return -1;
    }

    //初始化服务器端的地址,IP和端口一定要和想要连接的服务器一样,不然就连接不上
    SOCKADDR_IN sockAddress;
    sockAddress.sin_family = AF_INET;  //IPv4的协议
    //sockAddress.sin_addr.s_addr = inet_addr("127.0.0.1");
    inet_pton(AF_INET, "127.0.0.1", (void*)&sockAddress.sin_addr); //服务器地址
    sockAddress.sin_port = htons(51314);  //服务器的端口

    //客户机端主动连接服务器端 
    if (SOCKET_ERROR == connect(socketUser, (struct sockaddr*)&sockAddress, sizeof(sockAddress)))
    {

        printf("connect fail!\n");
        //int a=WSAGetLastError();
        //printf("error code : %d\n", a);
        //关闭套接字
        closesocket(socketUser);
        //关闭库
        WSACleanup();
        return -1;
    }

    //接收服务器返回的信息
    char szBuffer[1024] = { 0 };
    //接收数据, 跟服务器端一样
    int nReturnValue = recv(socketUser, szBuffer, sizeof(szBuffer), 0);

    if (0 == nReturnValue)
    {
        //客户端正常关闭   释放Socket
        closesocket(socketUser);
    }
    else if (SOCKET_ERROR == nReturnValue)
    {
        //网络中断  
        printf("连接中断");
    }
    else
    {
        //接收到客户端消息 
        printf("server data : %s\n", szBuffer);

        

    //发送消息给服务器
    while (1)
    {
        
            //发送数据, 跟服务器端一样
            char szSendData[1024] = { 0 };
            printf("Input Something : ");
            scanf_s("%s", szSendData, 1024);
            if (SOCKET_ERROR == send(socketUser, szSendData, strlen(szSendData), 0))
            {

            printf("send fail!");
            //关闭客户机Socket
            closesocket(socketUser);
            //关闭库
            WSACleanup();
            return -1;
            }
        }
    }

    //关闭socket
    closesocket(socketUser);
    //关闭网络库
    WSACleanup();

    system("pause");
    return 0;
}

👉最后运行客户端,生成解决方案之后,到工程文件夹里找到exe,开启多个客户端。

评论