• c++ 网络编程(四) LINUX/windows下 socket 基于I/O复用的服务器端代码 解决多进程服务端创建进程资源浪费问题


    原文作者:aircraft

    原文链接:https://www.cnblogs.com/DOMLX/p/9613861.html

    好了,继上一篇说到多进程服务端也是有缺点的,每创建一个进程就代表大量的运算与内存空间占用,相互进程数据交换也很麻烦。

    本章的I/O模型就是可以解决这个问题的其中一种模型。。。废话不多说进入主题--

    I/O复用技术主要就是select函数的使用。

                                              

    一.I/O复用预备知识--select()函数用法与作用

    select()用来确定一个或多个套接字的状态(更为本质一点来讲是文件描述符的状态)。

    使用select()所需要包含的头文件是:#include<sys/select.h>

    函数原型为:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timeval *timeout);

    接下来根据函数原型一点点的介绍一下select()函数。

    (1),struct fd_set 这是一个集合,这个集合中存放的是文件描述符(在unix、linux系统中任何的设备、管道、FIFO等都可通过文件描述符的形式来访问)。当然一个socket也是一个文件描述符啦。相关的操作有:

    FD_ZERO(fd_set *)将某一个集合清空

    FD_SET(int, fd_set *)将一个给定的文件描述符加入到集合之中

    FD_CLR(int, fd_set *)从集合中删除指定的文件描述符。

    FD_ISSET(int, fd_set *)检查集合中指定的文件描述符是否准备好(可读或可写)

    (2),struct timeval这是常用的一个结构体,用来表示时间值,有两个结构体成员:tv_sec表示秒数和tv_usec表示毫秒数。

     

    接下来具体解释一下select的参数:

     

    nfds:一个整数值,表示的是所要监视的文件描述符的范围。即你所要监听的文件描述符的最大值+1(因为select()函数进行遍历的时候是从0-文件描述符开始遍历的)。

    readfds:是指向fd_set结构的指针,这个集合中加入我们所需要监视的文件可读操作的文件描述符。

    writefds:指向fd_set结构的指针,这个集合中加入我们所需要监视的文件可写操作的文件描述符。

    exceptfds:指向fd_set结构的指针,这个集合中加入我们所需要监视的文件错误异常的文件描述符。

    timeout:指向timeval结构体的指针,通过传入的这个timeout参数来决定select()函数的三种执行方式:

    1.传入的timeout为NULL,则表示将select()函数置为阻塞状态,直到我们所监视的文件描述符集合中某个文件描述符发生变化是,才会返回结果。

    2.传入的timeout为0秒0毫秒,则表示将select()函数置为非阻塞状态,不管文件描述符是否发生变化均立刻返回继续执行。

    3.传入的timeout为一个大于0的值,则表示这个值为select()函数的超时时间,在timeout时间内一直阻塞,超过时间即返回结果。

     

    然后该说一说select()函数的返回值了:

    返回-1:select()函数错误,并将所有描述符集合清0,具体的错误可以通过errno输出来查看(在windows下通过GetLastError获取相应的错误代码)。

    返回0:表示select()函数超时。

    返回正数:返回的正数值表示已经准备好的描述符数。

    注意在每次select()函数调用以后,都需要将集合清空,因为状态已经改变,若需要重新监视就需要重新清空后在加入需要监视的文件描述符。

    下面通过示例把select函数所有知识点进行整合,希望各位通过如下示例完全理解之前的内容。

    linux下监控键盘数据:

        #include <sys/time.h>  
        #include <stdio.h>  
        #include <sys/types.h>  
        #include <sys/stat.h>  
        #include <fcntl.h>  
        #include <assert.h>  
        int main ()  
        {  
            int keyboard;  
            int ret,i;  
            char c;  
            fd_set readfd;  
            struct timeval timeout;  
            keyboard = open("/dev/tty",O_RDONLY | O_NONBLOCK);  
            assert(keyboard>0);  
            while(1)  
            {  
                //设置select函数的超时
                timeout.tv_sec=1;  
                timeout.tv_usec=0;
              //初始化fd_set结构体变量
                FD_ZERO(&readfd);  
                FD_SET(keyboard,&readfd);  
          
                ///监控函数  
                ret=select(keyboard+1,&readfd,NULL,NULL,&timeout);  
                if(ret == -1)   //错误情况  
                    cout<<"error"<<endl ;  
                else if(ret)    //返回值大于0 有数据到来  
                    if(FD_ISSET(keyboard,&readfd))  
                    {  
                        i=read(keyboard,&c,1);  
                        if('
    '==c)  
                            continue;  
                        printf("hehethe input is %c
    ",c);  
                        if ('q'==c)  
                            break;  
                    }  
                else    //超时情况  
                {  
                    cout<<"time out"<<endl;  
                    continue;  
                }  
            }  
        }  

    好了大概对select函数有一定的认知了,下面通过select函数实现I/O复用服务端。

    二.基于I/O复用的回声服务端

      • 什么是I/O复用?通俗点讲,其实就是一个事件监听,只是这个监听的事件一般是I/O操作里的读(read)与写(write),只要发生了监听的事件它就会响应。注意与一般服务器的区别,一般服务器是连接请求先进入请求队列里,然后,服务端套接字一个个有序去受理。而I/O复用服务器是事件监听,只要对应监听事件发生就会响应,是属于并发服务器的一种。

      • I/O复用的使用
        1,I/O复用的使用其实就是对select函数的使用,说select函数是I/O复用的全部内容也不为过。但这个函数与一般函数不同,它很难使用,我们先来看看它的调用顺序,分为3步:
        步骤一:

        • 设置文件描述符,即注册要监听的文件描述符,如监听标准输入的文件描述符0 -> FD_SET(0, &reads)
        • 指定监视范围,Linux上创建文件对象生成的对应文件描述符是从0开始递增的,所以最大监视范围为最后创建的文件描述符+1。
        • 设置超时,因为select函数是一个阻塞函数,只有监视的文件描述符发生变化才会返回,设置超时就是为了防止阻塞,如果不想设置超时,则传递NULL。

        步骤二:

        • 调用select函数

        步骤三:

        • 查看调用结果,FD_ISSET(0, &reads)发生变化返回真。

    下面给出LINUX下基于I/O复用服务端实现代码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/time.h>
    #include <sys/select.h>
    
    #define BUF_SIZE 100
    void error_handling(char *message);
    
    int main(int argc, const char * argv[]) {
        int serv_sock, clnt_sock;
        struct sockaddr_in serv_adr, clnt_adr;
        struct timeval timeout;
        fd_set reads, cpy_reads;
    
        socklen_t adr_sz;
        int fd_max, str_len, fd_num;
        char buf[BUF_SIZE];
        if (argc != 2) {
            printf("Usage: %s <port> 
    ", argv[0]);
            exit(1);
        }
    
        serv_sock = socket(PF_INET, SOCK_STREAM, 0);
        memset(&serv_adr, 0, sizeof(serv_adr));
        serv_adr.sin_family = AF_INET;
        serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_adr.sin_port = htons(atoi(argv[1]));
    
        if(bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
            error_handling("bind() error");
        if(listen(serv_sock, 5) == -1)
            error_handling("listen() error");
    
        FD_ZERO(&reads);
        //向要传到select函数第二个参数的fd_set变量reads注册服务器端套接字
        FD_SET(serv_sock, &reads);
        fd_max = serv_sock;
    
        while (1)
        {
            cpy_reads = reads;
            timeout.tv_sec = 5;
            timeout.tv_usec = 5000;
    
            //监听服务端套接字和与客服端连接的服务端套接字的read事件
            if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
                break;
            if(fd_num == 0)
                continue;
    
            if (FD_ISSET(serv_sock, &cpy_reads))//受理客服端连接请求
            {
                adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                FD_SET(clnt_sock, &reads);
                if(fd_max < clnt_sock)
                    fd_max = clnt_sock;
                printf("connected client: %d 
    ", clnt_sock);
            }
            else//转发客服端数据
            {
                str_len = read(clnt_sock, buf, BUF_SIZE);
                if (str_len == 0)//客服端发送的退出EOF
                {
                    FD_CLR(clnt_sock, &reads);
                    close(clnt_sock);
                    printf("closed client: %d 
    ", clnt_sock);
                }
                else
                {
                    //接收数据为字符串时执行回声服务
                    write(clnt_sock, buf, str_len);
                }
            }
        }
    
        close(serv_sock);
        return 0;
    }
    
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('
    ', stderr);
        exit(1);
    }    

    下面给出LINUX下基于I/O复用客户端实现代码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    #define BUF_SIZE 1024
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    
    int main(int argc, const char * argv[]) {
        int sock;
        char message[BUF_SIZE];
        int str_len, recv_len, recv_cnt;
        struct sockaddr_in serv_adr;
    
        if(argc != 3)
        {
            printf("Usage: %s <IP> <port> 
    ", argv[0]);
            exit(1);
        }
    
        sock = socket(PF_INET, SOCK_STREAM, 0);
        if(sock == -1)
            error_handling("socket() error");
    
        memset(&serv_adr, 0, sizeof(serv_adr));
        serv_adr.sin_family = AF_INET;
        serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
        serv_adr.sin_port = htons(atoi(argv[2]));
    
        if (connect(sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
            error_handling("connect() error");
        else
            puts("Connected ...............");
    
        while (1) {
            fputs("Input message(Q to quit): ", stdout);
            fgets(message, BUF_SIZE, stdin);
            if (!strcmp(message, "q
    ") || !strcmp(message, "Q
    "))
                break;
    
            str_len = write(sock, message, strlen(message));
    
            /*这里需要循环读取,因为TCP没有数据边界,不循环读取可能出现一个字符串一次发送
             但分多次读取而导致输出字符串不完整*/
            recv_len = 0;
            while (recv_len < str_len) {
                recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
                if(recv_cnt == -1)
                    error_handling("read() error");
                recv_len += recv_cnt;
            }
            message[recv_len] = 0;
            printf("Message from server: %s", message);
        }
    
        close(sock);
        return 0;
    }

    下面给出windows下I/O复用socket服务端代码:

    #include<iostream>
    #include<WinSock2.h>
    #pragma comment(lib,"ws2_32.lib")
    #define bufsize 1024
    using namespace std;
    void main() {
        WSADATA wsadata;
        SOCKET serverSocket,clientSocket;
        int szClientAddr,fdnum,str_len;
        SOCKADDR_IN  serverAddr, clientAddr;
        fd_set reads, cpyReads;
        TIMEVAL timeout;
        char message[bufsize] = "";
     
        if(WSAStartup(MAKEWORD(2, 2), &wsadata)!=0)
            cout<<"WSAStartup() error"<<endl;
     
        serverSocket = socket(PF_INET, SOCK_STREAM, 0);
        if(serverSocket == INVALID_SOCKET)
            cout<<"socket()  error"<<endl;
     
        memset(&serverAddr, 0, sizeof(serverAddr));
        serverAddr.sin_family = AF_INET;
        serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
        serverAddr.sin_port = htons(9999);
     
        if (bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
            cout << "bind () error" << endl;
     
        listen(serverSocket, 5);
        cout << "服务器启动成功!" << endl;
     
        FD_ZERO(&reads);  //所有初始化为0
        FD_SET(serverSocket, &reads);  //将服务器套接字存入
     
        while (1) {
            cpyReads = reads;   
            timeout.tv_sec = 5;      //5秒
            timeout.tv_usec = 5000;   //5000毫秒
     
            //找出监听中发出请求的套接字
            if ((fdnum = select(0, &cpyReads, 0, 0, &timeout)) == SOCKET_ERROR)
                break;
            if (fdnum == 0) {
                cout << "time out!" << endl; 
                continue;
            }
            for (unsigned int i = 0; i < reads.fd_count; i++) {
                if (FD_ISSET(reads.fd_array[i], &cpyReads)) { //判断是否为发出请求的套接字
                    if (reads.fd_array[i] == serverSocket) {  //是否为服务器套接字
                        szClientAddr = sizeof(clientAddr);
                        clientSocket = accept(serverSocket, (SOCKADDR*)&clientAddr, &szClientAddr);
                        if (clientSocket == INVALID_SOCKET)  cout << "accept() error" << endl;
                        FD_SET(clientSocket, &reads);
                        cout << "连接的客户端是:" << clientSocket << endl;
                    }
                    else {//否  就是客户端
                        str_len = recv(reads.fd_array[i], message, bufsize - 1, 0);
                        if (str_len == 0) {//根据接受数据的大小 判断是否是关闭
                            FD_CLR(reads.fd_array[i], &reads);  //清除数组中该套接字
                            closesocket(cpyReads.fd_array[i]);
                            cout << "关闭的客户端是:" << cpyReads.fd_array[i] << endl;
                        }
                        else {
                            send(reads.fd_array[i], message, str_len, 0);
                        }
                    }
                }
            }    
        }
        closesocket(clientSocket);
        closesocket(serverSocket);
        WSACleanup();
    }

    下面给出windows下I/O复用socket客户端代码:

    #include<iostream>
    #include<WinSock2.h>
    #pragma comment(lib,"ws2_32.lib")
    #define bufsize 1024
    using namespace std;
    void main() {
        WSADATA wsadata;
        SOCKET clientSocket;
        SOCKADDR_IN  serverAddr;
        int  recvCnt;
     
        char message[bufsize] = "";
        if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
            cout << "WSAStartup() error" << endl;
     
        if ((clientSocket = socket(PF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
            cout << "socket()  error" << endl;
     
        serverAddr.sin_family = AF_INET;
        serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
        serverAddr.sin_port = htons(9999);
     
        if(connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr))==SOCKET_ERROR)
            cout<<"connect() error"<<endl;
     
        while (1) {
            cout << "输入Q或q退出:";
            cin >> message;
            if (!strcmp(message, "Q") || !strcmp(message, "q")) break;
            send(clientSocket, message, strlen(message), 0);
            memset(message, 0, sizeof(message));
            recv(clientSocket, message, bufsize, 0);
            cout << "服务器结果:" << message << endl;
        }
        closesocket(clientSocket);
        WSACleanup();
    }

    最后说一句啦。本网络编程入门系列博客是连载学习的,有兴趣的可以看我博客其他篇。。。。c++ 网络编程课设入门超详细教程 ---目录

    参考博客:https://blog.csdn.net/zl908760230/article/details/70257229

    参考博客:https://blog.csdn.net/hshl1214/article/details/45872243

    参考博客:https://blog.csdn.net/u010223072/article/details/48133725

    参考书籍:《TCP/IP 网络编程 --尹圣雨》

    若有兴趣交流分享技术,可关注本人公众号,里面会不定期的分享各种编程教程,和共享源码,诸如研究分享关于c/c++,python,前端,后端,opencv,halcon,opengl,机器学习深度学习之类有关于基础编程,图像处理和机器视觉开发的知识

  • 相关阅读:
    队列(顺序存储结构)
    2015计划
    iframe子窗口父窗口方法调用和元素获取
    Ajax关于重定向
    Java国际化资源文件的选择
    eclipse自动部署web应用程序到tomcat webapps
    从给定字符串结尾获取指定字节长度的字符串
    Winform的一些不知道啥东西
    C# 2008核心编程 2013-09-14
    C# 2008核心编程 2013-09-10
  • 原文地址:https://www.cnblogs.com/DOMLX/p/9613861.html
Copyright © 2020-2023  润新知