11. TCP并发网络编程

这篇具有很好参考价值的文章主要介绍了11. TCP并发网络编程。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文主要介绍TCP并发网络的编程,重点介绍io多路复用的epoll实现

一、TCP/IP 网络通信过程

要完成一个完整的 TCP/IP 网络通信过程,需要使用一系列函数来实现。这些函数包括 bind、listen、accept 和 recv/send 等。下面是它们的配合流程:

  1. 创建套接字(socket):使用 socket 函数创建一个套接字,指定协议族和套接字类型。
  2. 绑定地址(bind):将本地地址绑定到套接字上,使得客户端可以通过该地址访问服务器。
  3. 监听连接请求(listen):将套接字设置为监听状态,并指定最大等待连接数(backlog)。
  4. 接受连接请求(accept):当有客户端发起连接请求时,使用 accept 函数创建新的套接字用于与客户端进行通信。
  5. 读写数据(recv/send):使用新创建的套接字进行数据传输,包括从客户端读取数据和向客户端发送数据。
  6. 关闭连接(close):在通信结束后,需要使用 close 函数关闭套接字以释放资源。

对于第4步的请求有两种处理方式:一线程一请求和epoll方法。

二、io多路复用的epoll 实现

epoll是一种在Linux操作系统中实现高性能I/O多路复用的机制。它可以同时监听多个文件描述符,当其中任何一个文件描述符发生读写等事件时,就会触发相应的回调函数进行处理。
基于 epoll 实现的 TCP 服务端程序的流程如下:

  1. 创建一个监听套接字,使用 socket() 函数创建套接字并设置相关参数(如地址重用等)。

  2. 将监听套接字绑定到本地 IP 地址和端口号,使用 bind() 函数将套接字与指定的地址进行绑定。

  3. 开始监听连接请求,使用 listen() 函数将该套接字标记为被动监听状态,并设置可同时处理的最大连接数。

  4. 创建一个 epoll 实例,使用 epoll_create() 函数创建 epoll 实例,并设置需要监视的事件类型。

  5. 将监听套接字添加到 epoll 实例中,使用 epoll_ctl() 函数向 epoll 实例中添加需要监视的文件描述符及对应事件。其中事件类型一般为 EPOLLIN 表示可读事件或者 EPOLLERR 表示错误事件。

  6. 进入主循环处理客户端请求,使用 epoll_wait() 等待内核通知就绪事件,并获取到就绪的文件描述符列表。然后遍历这个文件描述符列表并根据每个文件描述符对应的事件类型进行相应处理。如果是新连接请求,则调用 accept() 接收该连接,并将其加入 epoll 监听队列;否则,直接读取数据或者关闭连接等操作。

  7. 关闭监听套接字以及已经建立连接的客户端套接字,清理资源并退出程序。

总之,在 epoll 实现的 TCP 服务端程序中,通过使用 epoll 实例,可以同时处理多个客户端连接请求,并且在有新数据到达时能够及时地通知程序进行相应的处理。

另外,epoll还提供了ET(边缘触发)和LT(水平触发)两种工作模式。

  1. 水平触发模式
    在水平触发模式下,如果文件描述符上的事件没有被处理完毕,epoll 会持续通知应用程序该文件描述符上仍有事件待处理。在这种情况下,如果应用程序不及时响应并读取数据,则 epoll 会一直通知应用程序该文件描述符上有数据可读取。
  2. 边沿触发模式
    在边沿触发模式下,只要文件描述符上出现新的事件(例如数据可读或连接建立),epoll 就会通知应用程序。但是,在通知之后,如果应用程序没有立即响应并读取所有数据,则 epoll 不会再次通知该文件描述符上有新的数据可读。

总体来说,边沿触发模式相比于水平触发模式更为高效,并且可以避免由于重复监听导致 CPU 占用率过高的问题。但是,在使用边沿触发模式时需要注意及时读取所有数据,并确保每个事件都得到了正确处理。

并且,与select相比,虽然两者都是Linux下的I/O多路复用机制,但是它们有一些重要的区别:

  • 监听文件描述符数量限制不同:在Linux中,select函数所支持的最大文件描述符数量默认为1024个,而epoll没有这个限制,可以监听成千上万个文件描述符。

  • 文件描述符集合拷贝方式不同:在使用select时,每次调用需要将待监视的所有文件描述符从用户空间拷贝到内核空间,在返回结果之后还需要再将结果从内核空间拷贝回用户空间。这样会带来较大的性能开销。而在使用epoll时,只需将待监视的文件描述符加入一个内核事件表中即可完成注册,当有就绪事件发生时直接通知应用程序进行处理。

  • 对于非阻塞套接字处理方式不同:在使用select时,对于非阻塞套接字我们需要手动设置为非阻塞模式并且轮询读写操作是否就绪。而epoll则通过设置EPOLLET标志位实现了边缘触发模式,并且对于非阻塞套接字只需要等待其返回EAGAIN错误码即可知道其已经处于非阻塞状态。

总体来说,与select相比,epoll具有更高效、更灵活、更易扩展等优点,在处理大量并发连接时具有更好的性能和可扩展性。文章来源地址https://www.toymoban.com/news/detail-465546.html


#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>

// #include <winsock2.h>
// #include <mswsock.h>
// #include <windows.h>
// #include <sys/types.h>  
// #include <unistd.h>
// #include <fcntl.h>

#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <errno.h>
#include <fcntl.h> 
#include <unistd.h> 
#include <sys/epoll.h>


#define BUFFER_LENGTH       1024
#define EPOLL_SIZE          1024

void *client_routine(void *arg){
    int clientfd=*(int *)arg;

    while (1){
        char buffer[BUFFER_LENGTH]={0};
        int len=recv(clientfd,buffer,BUFFER_LENGTH,0);

        if (len < 0){//非阻塞状态下读到空数据
            close(clientfd);
            break;
        }
        else if(len == 0) {//断开连接
            close(clientfd);
            break;
        }
        else{
            printf("Recv: %s, %d btye(s)\n",buffer,len);
        }
    }
}

int main(int argc,char *argv[]){
    if (argc < 2) {
        printf("Param Error \n");
        return -1;
    }
    int port=atoi(argv[1]);//atoi将一个字符串转换为对应的整数值

    int sockfd=socket(AF_INET,SOCK_STREAM,0);

    struct sockaddr_in addr;
    memset(&addr,0,sizeof(struct sockaddr_in));
    addr.sin_family=AF_INET;
    addr.sin_port=htons(port);
    addr.sin_addr.s_addr=INADDR_ANY;

    if (bind(sockfd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in))<0){
        perror("bind");
        return -2;
    }

    if(listen(sockfd,5)<0){
        perror("listen");
        return -3;
    }

#if 0
    // 一请求一线程
    while (1){  
        struct sockaddr_in client_addr;
        memset(&client_addr,0,sizeof(struct sockaddr_in));
        socklen_t client_len =sizeof(client_addr);

        /*调用 accept() 函数后,它会一直阻塞等待直到有新的客户端连接请求到达为止。
        当有新的连接请求到达时,它会返回一个新产生的套接字文件描述符,并且将该连接对应的客户端地址信息存储在 addr 指向的结构体中*/
        int clientfd=accept(sockfd,(struct sockaddr *)&client_addr,&client_len);

        pthread_t thread_id;
        pthread_create(&thread_id,NULL,client_routine,&clientfd);

    }

#else     
    /*使用epoll的基本流程如下:
        1,创建一个epoll实例,可以通过调用 epoll_create() 函数来创建。
        2,向 epoll 实例中添加需要监控的文件描述符及其事件类型,可以通过调用 epoll_ctl() 函数进行操作。
        3,调用 epoll_wait() 函数等待监控对象上发生事件,并处理活跃的文件描述符及其事件类型。
        4,处理完活跃文件描述符的相关操作后,返回到第三步继续等待新的事件发生。
    */
    int epfd=epoll_create(1);   
    struct epoll_event events[EPOLL_SIZE] = {0};    //创建一个结构体数组 events 用于存储 epoll_wait() 返回的事件列表。
    struct epoll_event ev;  
    ev.events=EPOLLIN;  //创建一个新的 epoll_event 结构体 ev 并设置其关注的事件类型为 EPOLLIN (表示等待读事件)
    ev.data.fd = sockfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);   //使用 epoll_ctl() 函数将 sockfd 文件描述符加入到 epfd 实例中,并关联上面创建的 ev 结构体。

    while (1){
        int nready=epoll_wait(epfd,events,EPOLL_SIZE,5); //
        if (nready == -1) continue; //表示5秒内,没有事件,继续监听

        int i=0;
        for (i=0;i<nready;i++){
            /*判断当前事件所对应的文件描述符是否为监听套接字 sockfd。如果是,则说明有新的客户端连接请求到来了,
            需要通过 accept() 函数获取新产生的客户端连接并添加到 epoll 实例中;
            否则,说明是已经建立好连接的客户端发送了数据,需要通过 recv() 函数接收数据并进行相应处理。*/
            if(events[i].data.fd == sockfd){
                /*当有新的连接请求到来时(即 sockfd 上有 EPOLLIN 事件),使用 accept() 函数接受连接,
                并将其加入 epoll 实例中关注该套接字上是否有输入事件。*/
                struct sockaddr_in client_addr;
                memset(&client_addr,0,sizeof(struct sockaddr_in));
                socklen_t client_len =sizeof(client_addr);

                int clientfd=accept(sockfd,(struct sockaddr *)&client_addr,&client_len);

                ev.events=EPOLLIN | EPOLLET;  //EPOLLET 则表示将 I/O 事件设置为边缘触发模式。
                ev.data.fd=clientfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
            }
            else{
                //当某个客户端套接字上出现可读事件时(即该文件描述符在 events 中对应的元素有 EPOLLIN 标志),则调用 recv() 函数从该套接字中读取数据
                int clientfd=events[i].data.fd;
                
                char buffer[BUFFER_LENGTH]={0};
                int len=recv(clientfd,buffer,BUFFER_LENGTH,0);

                if (len < 0){//出现了异常情况或者非阻塞状态下没有更多数据可读
                    //关闭该套接字并将其从 epoll 实例中删除
                    close(clientfd);
                    ev.events=EPOLLIN;  
                    ev.data.fd=clientfd;
                    epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,&ev); //从 epoll 实例中删除 clientfd 对应的文件描述符,并且停止监听该套接字上的事件。
                }
                else if(len == 0) {//对方已经断开连接
                    //关闭该套接字并将其从 epoll 实例中删除
                    close(clientfd);
                    ev.events=EPOLLIN;  
                    ev.data.fd=clientfd;
                    epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,&ev);
                }
                else{
                    printf("Recv: %s, %d btye(s)\n",buffer,len);
                }

            }
        }
    }


#endif

    return 0;
}



到了这里,关于11. TCP并发网络编程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux网络编程】TCP并发服务器的实现(IO多路复用select)

    服务器模型主要分为两种, 循环服务器 和 并发服务器 。 循环服务器 : 在同一时间只能处理一个客户端的请求。 并发服务器 : 在同一时间内能同时处理多个客户端的请求。 TCP的服务器默认的就是一个循环服务器,原因是有两个阻塞 accept函数 和recv函数 之间会相互影响。

    2024年02月03日
    浏览(82)
  • 多进程并发TCP服务器模型(含客户端)(网络编程 C语言实现)

    摘要 :大家都知道不同pc间的通信需要用到套接字sockte来实现,但是服务器一次只能收到一个客户端发来的消息,所以为了能让服务器可以接收多个客户端的连接与消息的传递,我们就引入了多进程并发这样一个概念。听名字就可以知道--需要用到进程,当然也有多线程并发

    2024年02月17日
    浏览(67)
  • 【Linux网络编程】高并发服务器框架 线程池介绍+线程池封装

    前言 一、线程池介绍 💻线程池基本概念 💻线程池组成部分 💻线程池工作原理  二、线程池代码封装 🌈main.cpp 🌈ThreadPool.h 🌈ThreadPool.cpp 🌈ChildTask.h  🌈ChildTask.cpp 🌈BaseTask.h 🌈BaseTask.cpp 三、测试效果 四、总结 📌创建线程池的好处 本文主要学习 Linux内核编程 ,结合

    2024年01月16日
    浏览(95)
  • [嵌入式系统-24]:RT-Thread -11- 内核组件编程接口 - 网络组件 - TCP/UDP Socket编程

    目录 一、RT-Thread网络组件 1.1 概述 1.2 RT-Thread支持的网络协议栈 1.3 RT-Thread如何选择不同的网络协议栈 二、Socket编程 2.1 概述 2.2 UDP socket编程 2.3 TCP socket编程 2.4 TCP socket收发数据 RT-Thread 是一个开源的嵌入式实时操作系统(RTOS),它提供了丰富的网络组件用于网络通信。 RT-

    2024年03月12日
    浏览(65)
  • 计算机网络套接字编程实验-TCP多进程并发服务器程序与单进程客户端程序(简单回声)

    1.实验系列 ·Linux NAP-Linux网络应用编程系列 2.实验目的 ·理解多进程(Multiprocess)相关基本概念,理解父子进程之间的关系与差异,熟练掌握基于fork()的多进程编程模式; ·理解僵尸进程产生原理,能基于|sigaction()或signal(),使用waitpid()规避僵尸进程产生; ·

    2024年02月12日
    浏览(47)
  • 【探索Linux】—— 强大的命令行工具 P.27(网络编程套接字 —— UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同)

    在上一篇文章中,我们深入探讨了Linux网络编程的基石——套接字(Socket)的概念以及相关的编程接口。我们了解到,套接字是网络通信过程中端与端之间数据交换的关键抽象概念,它提供了一套丰富的编程接口,使得开发者能够在应用层直接进行网络通信的开发。不仅如此

    2024年03月16日
    浏览(75)
  • GO语言网络编程(并发编程)Channel

    1.1.1 Channel 单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。 虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势

    2024年02月09日
    浏览(72)
  • GO语言网络编程(并发编程)select

    1.1.1 select多路复用 在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现: 这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,G

    2024年02月09日
    浏览(90)
  • GO语言网络编程(并发编程)runtime包

    1.1.1. runtime.Gosched() 让出CPU时间片,重新等待安排任务(大概意思就是本来计划的好好的周末出去烧烤,但是你妈让你去相亲,两种情况第一就是你相亲速度非常快,见面就黄不耽误你继续烧烤,第二种情况就是你相亲速度特别慢,见面就是你侬我侬的,耽误了烧烤,但是还馋就

    2024年02月09日
    浏览(71)
  • 多线程|多进程|高并发网络编程

    多进程并发服务器是一种经典的服务器架构,它通过创建多个子进程来处理客户端连接,从而实现并发处理多个客户端请求的能力。 概念: 服务器启动时,创建主进程,并绑定监听端口。 当有客户端连接请求时,主进程接受连接,并创建一个子进程来处理该客户端连接。

    2024年02月07日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包