select服务器(linux)

网络的重要性

两个主机之间通信离不开网络,分布式,对数据库的操作,fpc,消息队列的组件,都离不开网络。网络主要是解决机器与机器之间通信的问题。网络有很多种方式,比如tcp、udp、广播等,网络中最重要的协议是tcp。

单线程tcp

酒店(服务器)有很多门(端口),酒店门口有迎宾员(listenfd),酒店中有服务员(clientfd),酒店回来客人(客户端),迎宾员将客人送进酒店介绍给服务员(accept),后面一系列的服务就都由服务员来服务。有n个客户就有n个服务员。

下面是一段简单的tcp回声服务器代码:

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
 
#define MAXLNE  4096
 
int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
 
    //指派迎宾员去哪个门口
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    //开始迎宾
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    //接待客人
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd/*产生服务员*/ = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    printf("========waiting for client's request========\n");
    while (1) {
        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }
 
    close(listenfd);
    return 0;
}

用网络调试助手去连接改服务器的时候,可以多次连接,但是只能有一个连接发送数据,服务端可以接收到呢?

首先我们要知道,在listen的时候,客户端点击连接后,就会发生tcp的三次握手,而tcp的三次握手是在内核中的协议栈中发生的,与应用层没有关系,三次握手并不发生在任何一个api中,它是协议栈本身完成的,处于listen状态时,被动完成的。进入listen状态之后,三次握手是允许的,但是,应用层并没有把这个连接拿出来去使用。accept之后会取一个连接出来使用。

将代码改成可以接收多个客户端数据

接下来要将代码改成可以有多个客户端同时发消息的代码

想法1 将accept函数放入while循环

这样会使得可以连接多个客户端,但是每个客户端只能接收一条消息
accept拿一个连接节点–> recv阻塞 -->接收数据后send–>用同一个connfd进行下一个接收

想法2 多线程

头文件:#include <pthread.h>

线程创建函数:pthread_create

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#include <pthread.h>

#define MAXLNE 4096

void *client_routine(void *arg)
{
    int connfd = *(int *)arg;
    char buff[MAXLNE];
    while (1)
    {
        int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0)
        {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

            send(connfd, buff, n, 0);
        }
        else if (n == 0)
        {
            close(connfd);
            break;
        }
    }

    return NULL;
}

int main(int argc, char **argv)
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    //指派迎宾员去哪个门口
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    //开始迎宾
    if (listen(listenfd, 10) == -1)
    {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    printf("========waiting for client's request========\n");
    while (1)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        if ((connfd /*产生服务员*/ = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
        {
            printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }

        pthread_t threadid;
        pthread_create(&threadid, NULL, client_routine, (void*)&connfd);
    }
 
    close(listenfd);
    return 0;
}

这种服务器适合4,5个连接的场景,如会议室,但是有大量的客户端就不行了。按照posix线程标准8m来算,1G的内存,也只能连128个。内存涨到一个瓶颈的时候,系统就会重启。很难突破C10K的数量级

  1. 优点:逻辑简单
  2. 缺点:不适合大量的客户端

想法3 io多路复用组件select

当大量的客户端连进来的时候,我们并不知道要处理哪个客户端,这就需要一个组件,某个客户端发数据的时候,让我们立马可以知道这个客户端发信息来了。我们需要做的事情是,把众多的fd,放在一个组件中集中处理

相当于将一个服务员服务一个客人 变成了 让一个服务员集中处理多桌客人

fd_set rfds; //fd的集合,是一个bitset

系统提供了4个宏对描述符集进行操作:

  1. 宏FD_SET设置文件描述符集fdset中对应于文件描述符fd的位(设置为1)
  2. 宏FD_CLR清除文件描述符集fdset中对应于文件描述符fd的位(设置为0)
  3. 宏FD_ZERO清除文件描述符集fdset中的所有位(既把所有位都设置为0)。
    使用这3个宏在调用select前设置描述符屏蔽位
  4. 在调用select后使用FD_ISSET来检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。

select

参考博客:https://blog.csdn.net/lingfengtengfei/article/details/12392449

int select(int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)

参数1:最大的fd, select中会有一个循环的操作,遍历bitmap,第一个参数就是i<n的n,所以第一个参数就是填FD_SET设置的fd中最大的fd加上1(fd是从0开始的)

select函数内部有一个置0的操作,所以在**if (FD_ISSET(i, &rset))**内要用 FD_SET宏重新置1

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#include <pthread.h>

#define MAXLNE 4096

int main(int argc, char **argv)
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    //指派迎宾员去哪个门口
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    //开始迎宾
    if (listen(listenfd, 10) == -1)
    {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    // select
    fd_set rfds, rset;

    FD_ZERO(&rfds);
    FD_SET(listenfd, &rfds); //监控listenfd

    int max_fd = listenfd;
    while (1)
    {
        rset = rfds;
        int nready = select(max_fd + 1, &rset, NULL, NULL, NULL);
        if (FD_ISSET(listenfd, &rset))
        { //
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
            {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }

            FD_SET(connfd, &rfds); //将新产生的连接fd加入fd_set
            if (connfd > max_fd)
                max_fd = connfd;

            if (--nready == 0)
                continue;
        }

        int i = 0;
        for (i = listenfd + 1; i <= max_fd; i++)
        { //listenfd后面就是connfd,依次对connfd进行读写操作
            if (FD_ISSET(i, &rset)) 
            {
                n = recv(i, buff, MAXLNE, 0);
                if (n > 0)
                {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);

                    send(i, buff, n, 0);
                }
                else if (n == 0)
                {
                    FD_CLR(i, &rfds);
                    close(i);
                }

                if(--nready == 0) break;
            }
        }
    }

    close(listenfd);
    return 0;
}

标准send写法:

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#include <pthread.h>

#define MAXLNE 4096

int main(int argc, char **argv)
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    //指派迎宾员去哪个门口
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    //开始迎宾
    if (listen(listenfd, 10) == -1)
    {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    // select
    fd_set rfds, rset, wfds, wset;

    FD_ZERO(&rfds);
    FD_SET(listenfd, &rfds); //监控listenfd

    FD_ZERO(&wfds);
    

    int max_fd = listenfd;
    while (1)
    {
        rset = rfds;
        wset = wfds;
        int nready = select(max_fd + 1, &rset, &wset, NULL, NULL);
        if (FD_ISSET(listenfd, &rset))
        { //
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
            {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }

            FD_SET(connfd, &rfds); //将新产生的连接fd加入fd_set
            if (connfd > max_fd)
                max_fd = connfd;

            if (--nready == 0)
                continue;
        }

        int i = 0;
        for (i = listenfd + 1; i <= max_fd; i++)
        { //listenfd后面就是connfd,依次对connfd进行读写操作
            if (FD_ISSET(i, &rset)) 
            {
                n = recv(i, buff, MAXLNE, 0);
                if (n > 0)
                {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);

                    FD_SET(i, &wfds); 
                }
                else if (n == 0)
                {
                    FD_CLR(i, &rfds);
                    printf("disconnect\n");
                    close(i);
                }

                if(--nready == 0) break;
            } else if(FD_ISSET(i, &wset)) {
                send(i, buff, n, 0);
                FD_CLR(i, &wfds);
                FD_SET(i, &rfds);
            }
        }
    }

    close(listenfd);
    return 0;
}

一个select可以支持sizeof(fd_set) * 8个fd, 多开几个select进程就可以突破C10k数量级的限制,但是突破不了C1000k数量级的限制,因为调用select的时候,会把要监控的集合copy到内核中,多个客户端可能只有几个可以操作,然后再copy出来。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值