c++ 网络编程(四)TCP/IP 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(‘\n‘==c)
                        continue;
                    printf("hehethe input is %c\n",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> \n", 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 \n", 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 \n", clnt_sock);
            }
            else
            {
                //接收数据为字符串时执行回声服务
                write(clnt_sock, buf, str_len);
            }
        }
    }

    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc(‘\n‘, 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(‘\n‘, 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> \n", 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\n") || !strcmp(message, "Q\n"))
            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] = "\0";

    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] = "\0";
    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();
}

最后说一句啦。本网络编程入门系列博客是连载学习的,有兴趣的可以看我博客其他篇。。。。

参考博客: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 网络编程 --尹圣雨》

原文地址:https://www.cnblogs.com/DOMLX/p/9613861.html

时间: 2025-01-04 15:33:04

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

【网络编程】TCP/IP协议

Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,又名网络通讯协议,是Internet最基本的协议.Internet国际互联网络的基 础,由网络层的IP协议和传输层的TCP协议组成.TCP/IP 定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准.协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求.通 俗而言:TCP负责发现传输的问题,一有问题就发出信号,要求重新传输,

2015/12/14 Python网络编程,TCP/IP客户端和服务器初探

一直不是很清楚服务器的定义,对于什么是服务器/客户端架构也只有一个模糊的感觉.最近开始学习,才明白一些什么服务器和客户端的关系. 所谓的服务器,就是提供服务的东西,它是一个硬件或者软件,可以向一个或者多个客户端提供所需要的服务.它存在的目的就是等待客户的请求,然后给客户服务,再接着等待请求. 而客户端,就来连上一个服务器,提出自己的请求,然后等待获得反馈. 比如说,打印机就是一个服务器的例子,与之相连的计算机就是客户端,通过网络连接打印机后,给它提出服务需求(打印)和传输数据(传输内容),然后打

TCP/IP 在 Windows 下的实现

Windows 实现TCP/IP 协议也是建立在上一篇博客的OSI 基础之上的. 用户态是由ws2_32.dll 和一些其他服务提供者的 dll 共同实现,当中ws2_32.dll 是一个框架.能够容纳非常多的服务提供者,这些服务提供者事实上就是各种协议的实现者,如比較常见的有 TCP/IP 协议,IPX 协议.而 TCP/IP 协议的服务实现是由 msafd.dll 和 mswsock.dll 来完毕. HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\S

linux网络编程基础——TCP/IP认识

基于TCP

windows下通过bat脚本和计划任务实现设置某一服务的守护进程

通常服务器上跑的服务或者应用程序比较重要,如果无意间被关闭将造成不定程度的影响.通过为比较重要的服务设置守护进程,守护服务的进程.当服务关闭时可以自动开启,方法如下: 第一步:写守护进程的bat脚本  内容如下: 脚本内容中  set_task=RDO.exe意思为检查是否有RDO.exe进程. 要用的话就改成自己的进程名,如果进程宕了就过会自动重启(会在RDO.exe进程安装目录下生成一个start.bat) 其中 start.bat脚本内容中的start 后面的参数与set_svr后面的参数

linux网络编程之TCP/IP基础篇(一)

从今天起,将会接触到网络编程,平台是linux,实现语言C语言,最后将会实现一个简易的miniftp服务器. 主要的内容安排为:linux网络编程之TCP/IP基础篇,SOCKET编程篇,进程间通信篇,线程篇,实战ftp篇. 1.ISO/OSI参考模型:open system interconnection开放系统互联模型是由OSI(international organization for standardization )国际标准化组织定义的网络分层模型,共七层. 各层的具体含义: 物理层

c++ 网络编程(二)TCP/IP linux 下多进程socket通信 多个客户端与单个服务端交互代码实现回声服务器

原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/9612820.html LINUX下: 一.服务端代码 下面用了多个close来关闭文件描述符,可能有的小伙伴会有疑惑....我就说一句,创建进程的时候会把父进程的资源都复制 一份,而你这个子进程只需要保留自己需要处理的资源,其他的自然要关闭掉, 不然父亲一个儿子一个 待会打起来怎么办  嘿嘿 注意了:就像进程间的通信需要属于操作系统的资源管道来进行,套接字也属于操作系统,所以创建新进程也还是

linux网络编程笔记——TCP

1.TCP和UDP TCP是长连接像持续的打电话,UDP是短消息更像是发短信.TCP需要消耗相对较多的资源,但是传输质量有保障,UDP本身是不会考虑传输质量的问题. 2.网络传输内容 我习惯的做法是直接通过TCP传送结构体,当然前提是收发两端都在程序里对目标结构体有充分的定义.特别说明的一点是,要小心收发两端处理器的大小端问题!而且传输信息头里必须包含长度信息,而且通用的是大端.但是,这里的长度和结构体,我选择用小端进行传输. 3.TCPserver实现 参考了别人多线程的回调写法,看起来不错.

Linux内核分析 - 网络[十四]:IP选项

Linux内核分析 - 网络[十四]:IP选项 标签: linux内核网络structsocketdst 2012-04-25 17:14 5639人阅读 评论(1) 收藏 举报  分类: 内核协议栈(22)  版权声明:本文为博主原创文章,未经博主允许不得转载. 内核版本:2.6.34      在发送报文时,可以调用函数setsockopt()来设置相应的选项,本文主要分析IP选项的生成,发送以及接收所执行的流程,选取了LSRR为例子进行说明,主要分为选项的生成.选项的转发.选项的接收三部分