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具有更高效、更灵活、更易扩展等优点,在处理大量并发连接时具有更好的性能和可扩展性。

#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;
}


目录
相关文章
|
13天前
|
网络协议 算法 网络性能优化
计算机网络常见面试题(一):TCP/IP五层模型、TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议
计算机网络常见面试题(一):TCP/IP五层模型、应用层常见的协议、TCP与UDP的区别,TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议、ARP协议
|
20天前
|
Web App开发 缓存 网络协议
不为人知的网络编程(十八):UDP比TCP高效?还真不一定!
熟悉网络编程的(尤其搞实时音视频聊天技术的)同学们都有个约定俗成的主观论调,一提起UDP和TCP,马上想到的是UDP没有TCP可靠,但UDP肯定比TCP高效。说到UDP比TCP高效,理由是什么呢?事实真是这样吗?跟着本文咱们一探究竟!
46 10
|
1月前
|
域名解析 缓存 网络协议
TCP传输层详解(计算机网络复习)
本文详细解释了TCP/IP协议族的分层模型、各层的功能、TCP报文的格式以及TCP连接建立的三次握手和断开的四次挥手过程。
83 2
TCP传输层详解(计算机网络复习)
|
1月前
|
网络协议 Java API
【网络】TCP回显服务器和客户端的构造,以及相关bug解决方法
【网络】TCP回显服务器和客户端的构造,以及相关bug解决方法
61 2
|
1月前
|
存储 网络协议 Java
【网络】UDP和TCP之间的差别和回显服务器
【网络】UDP和TCP之间的差别和回显服务器
65 1
|
1月前
|
域名解析 存储 网络协议
TCP套接字【网络】
TCP套接字【网络】
33 10
|
1月前
|
XML 网络协议 算法
【TCP】网络原理
【TCP】网络原理
31 0
|
2月前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
2月前
|
网络协议
网络协议概览:HTTP、UDP、TCP与IP
理解这些基本的网络协议对于任何网络专业人员都是至关重要的,它们不仅是网络通信的基础,也是构建更复杂网络服务和应用的基石。网络技术的不断发展可能会带来新的协议和标准,但这些基本协议的核心概念和原理将继续是理解和创新网络技术的关键。
134 0
|
2天前
|
安全 网络安全 数据安全/隐私保护
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
【10月更文挑战第38天】本文将探讨网络安全与信息安全的重要性,包括网络安全漏洞、加密技术和安全意识等方面。我们将通过代码示例和实际操作来展示如何保护网络和信息安全。无论你是个人用户还是企业,都需要了解这些知识以保护自己的网络安全和信息安全。