TCP服务器实现高并发方法:多线程进程,select,poll,epoll,IO多路复用,最详细的概念+代码集合

写在前面

避免time_wait状态,setsockopt函数

int opt = 1;
int ret = setsockopt(套接字, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
if (ret == -1)
{
    perror("setsockopt");
    return;
}

客户端代码

# include<stdio.h>
# include<stdlib.h>
# include<string.h>
# include<assert.h>
# include<sys/socket.h>
# include<sys/types.h>
# include<netinet/in.h>
# include<unistd.h>
# include<arpa/inet.h>
int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
    {
        printf("sockfd err\n");
        return -1;
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(8000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res == -1)
    {
        printf("connect err\n");
        return -1;
    }

    char buff[128] = "";
    printf("输入:\n");
    fgets(buff,128,stdin);
    send(sockfd,buff,strlen(buff),0);
    memset(buff,0,sizeof(buff));
    recv(sockfd,buff,127,0);
    printf("recv = %s\n",buff);
    close(sockfd);
    return 0;
}

一、什么是高并发

对于网络通信方面,通俗的来讲,高并发就是一个服务器可以同时处理多个客户端请求。把服务器想象成服务员,客户端想象成客户,就意味着一个服务员可以在同一时间同时服务多名客户,而不是只能服务一名。
在我之前有一篇TCP通信的博客。这是TCP编程的基础等,要想学习会高并发,就得先学会简单的一对一。
socket网络编程之TCP编程

二、多进程实现高并发

1. 概念部分

多进程实现高并发就是给每一个连接进来的客户端都分配一个进程,当有客户端完成连接的时候(accept之后),给这个客户端分配一个进程(fork),在这个子进程中负责和客户端进行通信。
为什么可以这样做呢?
因为子进程会将父进程所有东西复制一遍,在子进程刚刚创建成功的时候,父子进程几乎一样。
父进程中的文件描述符–也就是套接字,当然也复制到了子进程中,子进程就可以使用这个已连接的套接字与客户端进行通信了。
这样做有什么缺点呢?
太占用系统资源了,每一个客户端都分配一个进程去管理,成本太高了。
如何实现?
实现步骤就是当服务端accept成功,获得了已连接的套接字后,fork()创建子进程,然后在子进程中进行数据的收发。

2. 代码部分

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

//监听套接字
int listen_sockfd()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
    {
        perror("套接字创建失败");
        return -1;
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(8000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res == -1)
    {
        perror("bind err");
        return -1;
    }
    res = listen(sockfd,5);
    if(res == -1)
    {
        perror("listen err");
        return -1;
    }
    return sockfd;
}

int main()
{
    int listen_fd = listen_sockfd();
    //消除time_wait状态
    int opt = 1;
    int ret = setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
    if (ret == -1)
    {
        perror("setsockopt");
        return -1;
    }
    //主程序循环
    while(1)
    {
        int cfd;
        struct sockaddr_in caddr;
        socklen_t clen = sizeof(caddr);
        cfd = accept(listen_fd,(struct sockaddr*)&caddr,&clen);
        if(cfd == -1)
        {
            perror("accept err");
            continue;
        }
        //创建子进程
        pid_t pid = fork();
        if(pid == -1)
        {
            perror("fork err");
            close(cfd);
            continue;
        }
        else if(pid == 0)
        {
            //子进程
            char buff[128] = "";
            recv(cfd,buff,127,0);
            printf("recv:%s\n",buff);
            send(cfd,"ok",2,0);
            close(cfd);
        }
        else
        {
            close(cfd);
        }
    }
    close(listen_fd);
    return 0;
}

三、多线程实现高并发

1. 概念部分

多线程实现并发就是每来一个客户端,就创建一个线程(accept之后),给这个客户端分配一个线程,在这线程中实现与客户端的通信。
为什么可以这样做呢?
一个进程中的多个线程可以共享进程中的一些资源,这些资源刚好就包括了文件描述符。所以可以通过多线程来实现并发。
有什么缺点呢?
虽然说系统资源消耗没有开辟进程多,但是也是不小。
如何实现?
我们需要先了解线程函数

pthread_create(参数1,参数2,参数3,参数4);
参数1:pthread_t 类型的指针,接受创建线程的ID
参数2:线程属性,设置为NULL即可
参数3:线程启动时候的函数,void* 类型
参数4:传入函数的参数void*类型
参数3和参数4void*类型可以强转成任何类型,需要在传入的时候强转为void*类型,使用的时候再强转
返回值:成功返回0,失败返回错误码
需要导入头文件
#include <pthread.h>
编译需要加上-lpthead
pthread_detach(参数1);
将这个线程设置为分离状态,结束时会自动释放。也可以选择使用pthread_join();
参数1:线程id,pthread_t类型
pthread_join(参数1,参数2);
阻塞当前线程,直到指定的线程执行完毕
参数1:线程id,pthread_t类型
参数2:可以传入NULL

更加推荐使用pthread_detach();,因为我在使用pthread_join()的时候,后创建的线程先结束的情况下,服务端不能打印出数据,因为这个先创建的线程没有结束,因为pthread_join会阻塞住。

2.代码部分

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
//创建监听套接字
int listen_sockfd()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(8000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res == -1)
    {
        perror("bind err");
        return -1;
    }
    res = listen(sockfd,5);
    if(res == -1)
    {
        perror("listen err");
        return -1;
    }
    return sockfd;
}

//线程函数
void * create_client(void* arg)
{
    int cfd = *(int*)arg;
    char buff[128] = "";
    recv(cfd,buff,127,0);
    printf("recv:%s\n",buff);
    send(cfd,"ok",2,0);
    close(cfd);
}
int main()
{
    int lfd = listen_sockfd();
    while(1)
    {
        int cfd;
        struct sockaddr_in caddr;
        socklen_t clen = sizeof(caddr);
        cfd = accept(lfd,(struct sockaddr*)&caddr,&clen);
        if(cfd == -1)
        {
            perror("accept err");
            continue;
        }
        pthread_t pid;
        int res = pthread_create(&pid,NULL,create_client,(void*)&cfd);
        if(res !=0)
        {
            perror("pthread create err");
            close(cfd);
            continue;
        }
        pthread_detach(pid);
    }
    close(lfd);
    return 0;
}

四、select实现高并发

1. 概念部分

select默认最多监听1024个描述符,即0-1023
如何实现可以总结为两次遍历两次拷贝。
将已经连接的文件描述符放在一个文件描述符集合中,拷贝到内核中,内核遍历查询是否有关注的事件发生,当有事件发生,标记下来,然后拷贝回用户态内核中,用户态内核再遍历找出被标记的文件描述符,然后对其操作。

需要用到的函数

1. int select(参数1,参数2,参数3,参数4,参数5);
参数1:描述符最大+1,意思只需要关注这个描述符之前的描述符
参数2:fd_set*结构体指针,读事件
参数3:fd_set*结构体指针,写事件
参数4:fd_set*结构体指针,错误事件
参数5:超时时间timeval*类型结构体指针
2. 超时时间结构体timeval
结构体内两个变量
timeval.tv_sec -- 秒
timeval.tv_usec -- 微秒
3. fd_set宏
对fd_set结构体进行操作
FD_ZERO(fd_set* fd);//清空所有位
FD_SET(int fd,fd_set* fd);//设置
FD_ISSET(int fd,fd_set* fd);//是否被设置
FD_CLR(int fd,fd_set* fd);//清除位

2. 代码部分

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<sys/socket.h>
# include<sys/types.h>
# include<sys/select.h>
# include<netinet/in.h>
# include<arpa/inet.h>
# include<errno.h>
//创建监听套接字
int create_listen_socket()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
    {
        perror("socket err");
        return -1;
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(8000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res == -1)
    {
        perror("bind err");
        return -1;
    }

    res = listen(sockfd,5);
    if(res == -1)
    {
        perror("listen err");
        return -1;
    }
    return sockfd;
}

int main()
{
    int lfd = create_listen_socket();
    int opt = 1;
    int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
    if (ret == -1)
    {
        perror("setsockopt err");
        return -1;
    }
    //1.设置结构体
    fd_set m_set,r_set;
    //为什么设置两个结构体,因为在select函数中会修改传入的fd_set
    FD_ZERO(&m_set);
    FD_SET(lfd,&m_set);
    int maxfd = lfd;
    while(1)
    {
        r_set = m_set;
        int res = select(maxfd+1,&r_set,NULL,NULL,NULL);
        if(res == -1)
        {
            perror("select err");
            continue;
        }
        for(int i = 0;i<maxfd+1;i++)
        {
            if(FD_ISSET(i,&r_set))
            {
                //如果被设置了就进来
                if(i == lfd)
                {
                    //说明有客户端要连接
                    struct sockaddr_in caddr;
                    socklen_t clen = sizeof(caddr);
                    int cfd = accept(lfd,(struct sockaddr*)&caddr,&clen);
                    if(cfd == -1)
                    {
                        perror("accept err");
                        close(cfd);
                        continue;
                    }
                    FD_SET(cfd,&m_set);//添加进结构体中
                    if(cfd>maxfd)
                    {
                        //为什么要判断cfd和maxfd大小呢,因为有可能前面的文件描述符客户端关闭了,新加入的客户端文件描述符比maxfd要小,这时候如果更改maxfd那么就会出现有些后面的描述符无法被选中了
                        maxfd = cfd;
                    }
                    printf("有新的客户端连接进来了:%d\n",cfd);
                }
                else
                {
                    //说明有数据来了
                    char buff[128] = "";
                    int n = recv(i,buff,127,0);
                    if(n<0)
                    {
                        perror("recv err");
                        close(i);
                        FD_CLR(i,&m_set);
                        continue;
                    }
                    else if(n == 0)
                    {
                        printf("客户端%d断开连接\n",i);
                        close(i);
                        FD_CLR(i,&m_set);
                        continue;
                    }
                    else
                    {
                        //正常通信
                        printf("recv :%s\n",buff);
                        send(i,"ok",2,0);
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}

五、epoll实现高并发

1. 概念部分

epolllinux/unix内核特有的函数,处理大数量的并发是他的优势。
采用回调机制,仅仅返回有事件发生的文件描述符,不需要遍历所有的文件描述符,没有文件描述符数量限制。
所以说采用epoll是大规模高并发的最优解。
如何实现?
创建epoll实例(树根),然后将待检测的描述符添加到内核的红黑树中,使用红黑树跟踪所有带检测的描述符。当有事件发生的时候,通过回调函数将其添加到内核维护的一个用来记录就绪事件的链表中。
需要用到的函数

1.创建epoll实例
int epoll_create(int size);
参数:最大结点数,目前已经弃用,只需要传递一个大于0的数即可
2.对epoll事件结点的添加,删除,修改,在内核的红黑树中
int epoll_ctl(参数1,参数2,参数3,参数4);
参数1:epoll树根
参数2:操作类型 -- EPOLL_CTL_ADD(增加),EPOLL_CTL_DEL(删除),EPOLL_CTL_MOD(修改)
参数3:要操作的文件描述符
参数4:指定事件,epoll_event*类型结构体指针
epoll_event结构体两个参数events和data
event.events = EPOLLIN;EPOLLOUT;EPOLLERR;读事件,写事件,错误事件
event.data.fd = 文件描述符
3.等待和获取epoll实例中已经发生的事件,放入内核维护的链表中
int epoll_wait(参数1,参数2,参数3,参数4);
参数1:epoll描述符
参数2:指向epoll_event数组的指针,这里面存放的是已经发生的所有事件
参数3:events数组大小
参数4:timeout,-1代表无期限,0代表立即,整数代表多少毫秒
返回值是有事件发生的文件描述符个数

2. 代码部分

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <netinet/in.h>
//监听套接字
int create_listen_socket()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
    {
        perror("socket err");
        return -1;
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(8000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res == -1)
    {
        perror("bind err");
        return -1;
    }
    res = listen(sockfd,5);
    if(res == -1)
    {
        perror("listen err");
        return -1;
    }
    return sockfd;
}

int main()
{
    int lfd = create_listen_socket();
    int opt = 1;
    int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
    if (ret == -1)
    {
        perror("setsockopt");
        return -1;
    }
    int efd = epoll_create(1024);
    if(efd == -1)
    {
        perror("epoll_create err");
        close(lfd);
        return -1;
    }
    struct epoll_event event,events[1024];
    event.events = EPOLLIN;
    event.data.fd = lfd;
    int nfds;//存储epoll_wait返回值,有多少准备就绪的事件
    int res = epoll_ctl(efd,EPOLL_CTL_ADD,lfd,&event);
    if(res == -1)
    {
        perror("epoll_ctl err");
        close(efd);
        close(lfd);
        return -1;
    }
    while(1)
    {
        nfds = epoll_wait(efd,events,1024,-1);
        if(nfds == -1)
        {
            perror("epoll_wait err");
            close(lfd);
            close(efd);
            return -1;
        }
        for(int i =0;i<nfds;i++)
        {
            if(events[i].data.fd == lfd)
            {
                //有新的连接
                struct sockaddr_in caddr;
                socklen_t clen = sizeof(caddr);
                int cfd = accept(lfd,(struct sockaddr*)&caddr,&clen);
                if(cfd == -1)
                {
                    perror("accept err");
                    continue;
                }
                event.events = EPOLLIN;
                event.data.fd = cfd;
                int res = epoll_ctl(efd,EPOLL_CTL_ADD,cfd,&event);
                if(res == -1)
                {
                    perror("epoll_ctl err");
                    close(cfd);
                    continue;
                }
            }
            else
            {
                //有数据来了
                char buff[1024] = "";
                int res = recv(events[i].data.fd,buff,1023,0);
                if(res == -1)
                {
                    perror("recv err");
                    close(events[i].data.fd);
                }
                else if(res == 0)
                {
                    printf("断开连接\n");
                    close(events[i].data.fd);
                }
                else
                {
                    printf("recv:%s\n",buff);
                    send(events[i].data.fd,"ok",2,0);
                }
            }
        }
    }
    close(lfd);
    close(efd);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LiuJWHHH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值