Linux系统编程——TCP并发模型

TCP要实现一对多需要用到TCP并发服务器,而服务器分为两种

1,单循环服务器:服务端同一时刻只能处理一个客户端任务

2,并发服务器:服务端同一时刻可以处理多个客户端的任务,例如UDP

TCP服务端并发模型

1,多进程:资源开销大,安全性高

2,多线程:资源开销小,相同资源环境下并发量大

3,线程池:为了解决多进程或者多线程,再服务器运行过程中频繁你创建和销毁带来的时间消耗问题,基于生产者和消费者编程模型,以及任务队列等,实现的一套多线程框架

4,IO多路复用

多进程

流程:

cs 复制代码
#include<stdio.h>
#include "head.h"
#define N "192.168.214.210"

int creat_TCP()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        perror("socket error");
        return -1;
    }
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(50000);
    addr.sin_addr.s_addr = inet_addr(N);
    int bin = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
    if(bin < 0)
    {
        perror("bind error");
        return -1;
    }
    return sockfd;
}
//多个客户端接入时,进行三次握手所使用的套接字文件描述符相同
void wait_handler(int signo)
{
    wait(NULL);
}

int main()
{
    signal(SIGCHLD, wait_handler);
    int sockfd = creat_TCP();
    listen(sockfd, 2);
    struct sockaddr_in addr;
    socklen_t clien = sizeof(addr);
    while(1)
    {
        int connfd = accept(sockfd, (struct sockaddr *)&addr, &clien);
        if(connfd < 0)
        {
            perror("accept error");
            return -1;
        }
        pid_t pid = fork();
        if(0 == pid)
        {
            char buff0[100] = "\0";
            while(1)
            {
                int rec = recv(connfd, buff0, sizeof(buff0), 0);
                if(0 == rec)
                {
                printf("[%s : %d] : offline\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
                    break;
                }
                if(rec < 0)
                {
                    perror("error");
                    break;
                }
                printf("[%s : %d] : %s\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), buff0);
                strcat(buff0, "---->ok");
                send(connfd, buff0, strlen(buff0), 0);
                memset(buff0, 0, sizeof(buff0));
            }
            close(connfd);
        }
    }
    
    close(sockfd);

    return 0;
}

多线程

流程:

注意:在创建之后不能使用pthread_join(),因为该函数会阻塞,无法导致服务端再次accept,如果要结束可以将线程设置成分离属性

cs 复制代码
#include<stdio.h>
#include"head.h"

#define N "192.168.110.231"

struct address
{
    int connfds;
    struct sockaddr_in  cliaddrs;
    socklen_t  clilens;
};
//定义结构体,将端口号,地址,套接字绑定,便于线程传递参数
int creat_socket()//建立套接字并绑定
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        perror("socket error");
        return -1;
    }

//允许绑定处于TIME_WAIT状态的地址,避免端口占用问题:
	int optval = 1;
	setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    struct sockaddr_in addr;//确认地址信息
    addr.sin_family = AF_INET;
    addr.sin_port = htons(50000);
    addr.sin_addr.s_addr = inet_addr(N);

    bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
    return sockfd;
}

void *task(void *arg)
{
    struct address addres= *((struct address *)arg);
    //强制类型转换传入的套接字指针
    //不能放在while里,主进程在进行多个线程任务时,arg指向的内容改变,导致connfd改变
    char buff[100] = "\0";
    while(1)
    {
        int rec = recv(addres.connfds, buff, sizeof(buff), 0);//接收客户端信息
        printf("[%s : %d] : %s\n", inet_ntoa(addres.cliaddrs.sin_addr), ntohs(addres.cliaddrs.sin_port), buff);//输出信息与地址,并进行字节序的转变

        if(0 == rec)
        {
            printf("offline\n");
            break;
        }
        if(rec < 0)
        {
            perror("connfd error\n");
            break;
        }

        strcat(buff, "----->ok");
        send(addres.connfds, buff, strlen(buff), 0);
        memset(buff, 0, sizeof(buff));//清空buff
    }
    close(addres.connfds);
}


int main()
{
    int sockfd = creat_socket();
    listen(sockfd, 3);

    struct sockaddr_in cliaddr;
    socklen_t clilen = sizeof(cliaddr);//保存客户端地址

    struct address addres;
    while(1)
    {
        int connfd = accept(sockfd, (struct sockaddr *) &cliaddr, &clilen);
        if(connfd < 0)
        {
            perror("accept error");
            continue;
        }

        addres.connfds = connfd;
        addres.cliaddrs = cliaddr;

        pthread_t thread;//保存线程地址
        pthread_create(&thread, NULL, task, &addres);
        pthread_detach(thread);
    }
    close(sockfd);
}

进线程区别

多进程:进程资源开销大;安全性高

多线程:相同资源环境下资源开销小,并发量大

线程池

基于生产者和消费者编程模型,以及任务队列等,实现的一套多线程框架。

目的:为了解决多线程或者多进程模型,在服务器运行过程,频繁创建和销毁线程(进程)带来的时间消耗问题。

流程:提前创建一堆线程,阻塞等待接收任务。主线程(生产者)负责接任务保存在任务队列中,空闲的次线程(消费者 )负责从队列中执行任务

IO多路复用(IO多路转接)

对多个文件描述符的读写可以复用一个进程。本质为在不创建新的进程和线程的前提下,使用一个进程实现对多个文件读写的同时监测

IO多路复用的缺点:在处理耗时任务时不占优势,例如对音视频进行处理时候,因为只有一个进程,所以其他任务来临时, 没有空闲的进程去处理

|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| 方式 | 特点 |
| select | 1. 使用位图(数组)实现对文件描述符集合的保存,最多允许同时监测1024个文件描述符; 2. 需要应用层和内核层的反复数据(文件描述符集合表)拷贝,比较费时; 3. 返回的集合表需要遍历寻找到达的事件; 4. 只能工作在水平触发模式(低速模式),不能工作在边沿触发模式(高速模式) |
| poll | 1. 使用链表实现对文件描述符集合的保存,没有了监测的文件描述符上限限制; 其余和select相同,即poll只是改良了select的第一个特点 |
| epoll | 1. 使用红黑树(二叉树)实现文件描述符集合的存储,没有文件描述符上限限制,提高查找效率; 2. 将文件描述符集合创建在内核层,避免了应用层和内核层的反复数据拷贝; 3. 返回的是到达事件,不需要遍历,只需要处理事件即可; 4. 可工作在水平触发模式(低速模式),也可工作在边沿触发模式(高速模式) |

水平触发模式:只要 IO 资源 "有数据待处理",就持续通知程序

边沿触发模式:只有 IO 资源 "状态从'无数据'变'有数据'" 时,才通知一次

原理:对于文件描述符(0,fifofd)的监测任务交给Linux内核,三种方式会将关注的描述符通知给内核,内核会在后台监测文件描述符的事件,当某个文件描述符的事件到达时,内核会通过三个函数返回给应用层,应用层会知道哪个事件以及哪个文件描述符的事件

横坐标:监测的文件描述符个数

纵坐标:花费的时间

select

步骤

1,创建文件描述符集合

2,添加关注的文件描述符到集合

3,使用select传递集合表给内核,内阁开始监测事件

4,当内核监测到事件时,应用层select将解除阻塞,并获得相关的事件结果

5,根据select返回的结果做不同的任务处理

小写名称为函数,大写名称为系统定义的带参宏即带参宏,可直接使用

fd:文件描述符

fd_set:文件描述符集合表

FD_CLR: 在set集合表内将fd文件描述符删除(clear)

FD_ISSET:判断fd是否在set内(is set?)

FD_SET: 在set内添加fd(set) 在则返回1,否则则0

FD_ZERO:将set清空(zero)

功能:传递文件描述符集合表给内核,并等待事件结果

参数:

nfds:关注的最大文件描述符+1

readfds:读事件的文件描述符集合,如果没有则传NULL,下两个同理

writefds:写事件的文件描述符集合

exceptfds:其他事件的文件描述符集合

timeout:设置select监测时的超时时间(即阻塞等待的时间)

NULL :不设置超时时间,即一直阻塞等待

返回值:

成功:内核监测的到达事件的个数

失败:-1

0:超时时间到达,但没有事件发生

集合表

对于select的集合监听表,即fd_set,本质上是一段连续的内存空间(可以理解为数组),称之为位图,因为其按照位去管理文件描述符。

位图最多允许保存1024个文件描述符,会将文件描述符对应的位置置1,如果不关注则为0,以下图为例

此时只关注0和fifofd,所以在位图中如下,因为fifofd是3(文件描述符按照最小未被分配原则去分配),所以在3的位置置为1。

通过select将左侧表传递给右侧Linux内核空间,而内核空间则会遍历该数组寻找1的位置,根据文件描述符最小为被分配原则,虽然fifofd = 3,但是需要遍历到第4个位置

更直观地解释select的第一个参数,这是由于ndfds的定义是需要检查的文件描述符数量,而由于Linux内核文件描述符从0开始,所以数量=最大描述符+1
select成功后将表传给内核去监测,内核监测到结果时,会对进行修改,并将修改过后的表返回给应用层到原来的表上,假设管道事件到达而终端事件没到达,则会把表的第0个位置置为0,表示终端事件未到达,其余不变,于是表中置1处则为到达事件的文件描述符

值得注意的是,修改过后的表返回给应用层时,会覆盖原先的表,这会导致原先的数据丢失,即描述符0在表中的位置一直是0而不是1,所以需要备份,使得无论集合表还是内核更改过后的表经过备份,而不会更改原本的数据,打个比方,交换a和b的数据需要用到另一个变量c

TCP并发

cs 复制代码
#include<stdio.h>
#include"../../head.h"

#define N "192.168.115.210"
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(50000);
    addr.sin_addr.s_addr = inet_addr(N);
    bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
    listen(sockfd, 3);
    //建立套接字,绑定服务端

    fd_set TCP_select;
    fd_set temp;
    //创建集合表与临时集合表
    
    FD_ZERO(&TCP_select);
    FD_SET(sockfd, &TCP_select);
    //清空并添加套接字对应的文件描述符

    int maxfd = sockfd;
    while(1)
    {
        temp = TCP_select;
        //更新临时集合表

        char buff[100] = "\0";

        int cnt = select(maxfd + 1, &temp, NULL, NULL, NULL);
        if(cnt < 0)
        {
            perror("select error");
            return -1;
        }
        //传递文件描述符集合表

        if(FD_ISSET(sockfd, &temp))
        //判断是否为新接入的套接字
        {
            int connfds = accept(sockfd, NULL, NULL);
            //进行三次握手后产生新的套接字
            if(connfds < 0)
            {
                perror("accept error");
                return -1;
            }

            FD_SET(connfds, &TCP_select);
            //将新的套接字加入原始集合表中

            maxfd = maxfd > connfd ? maxfd : connfd;
            //更新最大关注的文件描述符
        }

        for(int i = sockfd + 1; i <= maxfd; i++)
        //遍历集合表中的文件描述符
        {
            if(FD_ISSET(i, &temp))
            {
                ssize_t cnt = recv(i, buff, sizeof(buff), 0);
                if(cnt <= 0)
                //判断连接失败或者断开连接
                {
                    if(cnt < 0)    perror("recv error");
                    FD_CLR(i, &TCP_select);
                    close(i);
                    continue;
                }

                puts(buff);
                strcat(buff, "----ok");
                cnt = send(i, buff, strlen(buff), 0);

                if(cnt < 0)
                //判断发送失败
                {
                    perror("send error");
                    FD_CLR(i, &TCP_select);
                    close(i);
                    continue;
                }
                memset(buff, 0, sizeof(buff));
                //更新buff
            }
        }
    }
    close(sockfd);
    return 0
}

epoll

步骤

1,创建文件描述符集合:epoll_create

2,添加关注的文件描述符:epoll_ctl

3,epoll通知内核开始进行事件监测:epoll_wait

4,epoll返回时,获取到达事件的结果

5,根据到达事件做任务处理

epoll_create

功能:通知内核创建文件描述符集合表

参数:监测的文件描述符个数

返回值:成功则为一个非负的文件描述符,代表了内核创建的集合表,失败为-1

epoll_ctl(control)

功能:对epoll的文件描述符集合进行操作

参数:

epfd:epoll_create创建的epoll集合

op:对文件描述符集合进行的操作

EPOLL_CTL_ADD:添加文件描述符到集合(add)

EPOLL_CTL_MOD:修改集合中的文件描述符(modify)

EPOLL_CTL_DEL:删除集合中的文件描述符(delete)

fd:要操作的文件描述符

event:文件描述符对应的事件(如果是NULL则不关注,一般用于删除)

events:关注的文件描述符事件

EPOLLIN:读事件

EPOLLOUT:写事件

data.fd:关注的文件描述符

返回值:成功则0失败-1

epoll_wait

功能:通知内核开始监测文件描述符的事件

参数:

epfd:监测的文件描述符集合

events:保存返回的到达事件的结果(数组)

传入方式:struct epoll_event evs[MAX_FD_CNT],传evs

因为到达事件的结果可能不止一个,所以要传入数组

maxevents:最大的事件个数

timeout:超时时间

-1:不设置超时,一直阻塞

返回值:

成功:到达的事件的个数

失败:-1


写端与select相同,下面为读端

cs 复制代码
//向集合中添加文件描述符
int epoll_fd_add(int epfds, int fd, uint32_t events)
{
    struct epoll_event ev;
    ev.events = events;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfds, EPOLL_CTL_ADD, fd, &ev);
    if(ret < 0)
    {
        perror("epoll_ctl error");
        return -1;
    }
}
int main()
{
	char buff[1024] = {0};
	mkfifo("./myfifo", 0664);
	
	int fifofd = open("./myfifo", O_RDONLY);
	if (fifofd < 0)
	{
		perror("open fifo error");
		return -1;
	}
	
    //创建内核中的文件描述符集合
    int epfds = epoll_create(2);
    if(epfds < 0)
    {
        perror("epoll error");
        return -1;
    }

    //添加文件描述符的集合
    epoll_fd_add(epfds, 0, EPOLLIN);
    epoll_fd_add(epfds, fifofd, EPOLLIN);

    //保存到达事件的集合
    struct epoll_event evs[2];
	while (1)
	{
        //阻塞等待事件到达
	    int cnt = epoll_wait(epfds, evs, 2, -1);
        if(cnt < 0)
        {
            return -1;
        }

        for(int i = 0; i < cnt; i++)
        {
            if(0 == evs[i].data.fd)
            {
                fgets(buff, sizeof(buff), stdin);
                printf("STDIN : %s\n", buff);
                memset(buff, 0, sizeof(buff));
            }
            else if(fifofd == evs[i].data.fd)
            {
                read(evs[i].data.fd, buff, sizeof(buff));
                printf("FIFO : %s\n", buff);
                memset(buff, 0, sizeof(buff));
            }
        }
	}

	close(fifofd);
	return 0;
}

TCP并发

cs 复制代码
#include<stdio.h>
#include"../head.h"

#define N "192.168.112.210"

int epoll_ctl_add(int epfd, int fd)
{
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    //定义结构体接入文件描述符以及要加入的集合表

    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    if(ret < 0)
    {
        perror("epoll_add error");
        return -1;
    }
    //调用epoll_ctl函数,第二个参数固定
}
//向集合表中添加文件描述符

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(50000);
    addr.sin_addr.s_addr = inet_addr(N);
    bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
    listen(sockfd, 3);
    //绑定地址和端口号

    int epfd = epoll_create(10);
    if(epfd < 0)
    {
        perror("epoll_create error");
        return -1;
    }
    //创建集合表

    epoll_ctl_add(epfd, sockfd);
    struct epoll_event evs[10];
    //调用函数加入用来接收的套接字,并创建保存客户端的文件描述符

    while(1)
    {
        char buff[100] = "\0";
        int cnt = epoll_wait(epfd, evs, 10, -1);
        if(cnt < 0)
        {
            perror("epoll_wait error");
            return -1;
        }
        //阻塞等待集合表中的事件,并保存到达的事件

        for(int i = 0; i < cnt; i++)
        {
            if(sockfd == evs[i].data.fd)
            {
                int connfd = accept(sockfd, NULL, NULL);
                epoll_ctl_add(epfd, connfd);
            }
            //判断接入新的客户端,并加入新的套接字

            else
            {
                int rec = recv(evs[i].data.fd, buff, sizeof(buff), 0);
                if(rec <= 0)
                {
                    if(rec < 0)    perror("recv error");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
                    close(evs[i].data.fd);
                    continue;
                }
                //判断是否断开

                puts(buff);
                strcat(buff, "---ok");

                int sen = send(evs[i].data.fd, buff, strlen(buff), 0);
                if(sen <= 0)
                {
                    if(sen < 0)    perror("send error");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
                    close(evs[i].data.fd);
                    continue;
                }
                memset(buff, 0, sizeof(buff));
                //清空buff
            }
        }
    }
    close(sockfd);
}

在此梳理一下,尤其是epoll_wait函数作用,以sockfd为例。先通过ADD将其添加到epfd中,然后wait阻塞等待,当sockfd事件触发时,内核监测到事件发生,便将sockfd对应的event结构体存入evs中,然后通过遍历evs,直到sockfd对应上里面的某个结构体的fd,然后执行后续程序。

在断开以及接收出错时,要先在集合表中删除该文件描述符再进行关闭,如过反过来先关闭则会报错无法进行删除操作

相关推荐
阿干tkl2 小时前
Linux Web终端连接
linux·运维·前端
chian-ocean2 小时前
网络世界的“搬运工”:深入理解数据链路层
开发语言·网络·php
oMcLin2 小时前
Linux 系统的服务器救援指南:从 Live USB 到 chroot 恢复系统
linux·服务器·php
一分生一分熟2 小时前
RK3588 编译RTL8852BE的WIFI模块驱动
linux·驱动开发
qq_254674412 小时前
人脑的工作原理——神经网络
网络
fengyehongWorld2 小时前
Linux journald与journalctl命令
linux·运维·服务器
啊吧怪不啊吧2 小时前
从单主机到多主机——分布式系统的不断推进
网络·数据库·redis·分布式·架构
米高梅狮子2 小时前
1. Cockpit 管理服务器
linux·运维·服务器
一颗青果8 小时前
HTTP协议详解
linux·网络·网络协议·http