网络程序设计专题报告——socket通信(TCP)高并发之epoll模式

1. C实现socket通信(TCP)

1.1 通信流程

在这里插入图片描述

1.2 socket基本操作

socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)

1.2.1 socket()函数
int socket(int domain, int type, int protocol);

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:domain、type、protocol。

  • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
  • type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等
  • protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等。
    注意并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
1.2.2 bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind()函数把一个地址族中的特定地址赋给socket,函数的三个参数分别为:

  • sockfd:socket描述字,它是通过socket()函数创建用来唯一标识一个socket。
  • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同。
  • 对应的是地址的长度。
    通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。
1.2.3 listen()、connect()函数
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

一个服务器会在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

1.2.4 accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

1.2.5 read()、write()函数等

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
常见的网络I/O操作函数有下面几组:

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()
1.2.6 close()函数
int close(int fd);

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

epoll介绍

epoll模型的优点

  • 支持一个进程打开大数目的socket描述符
  • IO效率不随FD数目增加而线性下降
  • 使用mmap加速内核与用户空间的消息传递

epoll的两种工作模式

  • LT(level-triggered):是epoll的默认模式
    socket接收缓冲区不为空 有数据可读 读事件一直触发
    socket发送缓冲区不满 可以继续写入数据 写事件一直触发
  • ET(edge-triggered):
    socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
    socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件

epoll模型API

#include <sys/epoll.h> 

/* 创建一个epoll的句柄,size用来告诉内核需要监听的数目一共有多大。当创建好epoll句柄后,
它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。*/
int epoll_create(int size);  

/*epoll的事件注册函数*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

/*等待事件的到来,如果检测到事件,就将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  

epoll的事件注册函数epoll_ctl,第一个参数是 epoll_create() 的返回值,第二个参数表示动作,使用如下三个宏来表示:

POLL_CTL_ADD    //注册新的fd到epfd中;
EPOLL_CTL_MOD    //修改已经注册的fd的监听事件;
EPOLL_CTL_DEL    //从epfd中删除一个fd;

struct epoll_event 结构如下:

typedef union epoll_data
{
    void        *ptr;
    int          fd;
    __uint32_t   u32;
    __uint64_t   u64;
} epoll_data_t;

struct epoll_event 
{
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

epoll_event结构体中的events 可以是以下几个宏的集合:

EPOLLIN     //表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT    //表示对应的文件描述符可以写;
EPOLLPRI    //表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR    //表示对应的文件描述符发生错误;
EPOLLHUP    //表示对应的文件描述符被挂断;
EPOLLET     //将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT//只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

实验演示

编译epoll_server.c和epoll_client.c

gcc epoll_server.c -o epoll_server
gcc epoll_client.c -o epoll_client

运行截图如下:
在这里插入图片描述
核心代码:

    while (1)
    {
        if ((nCounts = epoll_wait(epfd, events, MAXSIZE, -1)) < 0)
        {
            perror("epoll_ctl");
            exit(-1);
        }
        else if (nCounts == 0)
        {
            printf("time out, No data!\n");
        }
        else
        {
            for (int i = 0; i < nCounts; i++)
            {
                int tmp_epoll_recv_fd = events[i].data.fd;
                if (tmp_epoll_recv_fd == i_listenfd) // 有客户端连接请求
                {
                    if ((i_connfd = accept(i_listenfd, (struct sockaddr *)NULL, NULL)) < 0) // 阻塞等待客户端连接
                    {
                        perror("accept");
                    }
                    else
                    {
                        printf("Client[%d], welcome!\n", i_connfd);
                    }

                    ev.events = EPOLLIN;
                    ev.data.fd = i_connfd;
                    if (epoll_ctl(epfd, EPOLL_CTL_ADD, i_connfd, &ev) < 0)
                    {
                        perror("epoll_ctl");
                        exit(-1);
                    }
                }
                else // 若是已连接的客户端发来数据请求
                {
                    // 接受客户端发来的消息并作处理(小写转大写)后回写给客户端
                    memset(msg, 0, sizeof(msg));
                    if ((nrecvSize = read(tmp_epoll_recv_fd, msg, MAXSIZE)) < 0)
                    {
                        perror("read");
                        continue;
                    }
                    else if (nrecvSize == 0) // read返回0代表对方已close断开连接。
                    {
                        printf("client has disconnected!\n");
                        epoll_ctl(epfd, EPOLL_CTL_DEL, tmp_epoll_recv_fd, NULL);
                        close(tmp_epoll_recv_fd); //

                        continue;
                    }
                    else
                    {
                        printf("recvMsg:%s", msg);
                        /*for (int i = 0; msg[i] != '\0'; i++)
                        {
                            msg[i] = toupper(msg[i]);
                        }*/
                        if (write(tmp_epoll_recv_fd, msg, strlen(msg) + 1) < 0)
                        {
                            perror("write");
                        }
                    }
                }
            }
        }
    } // while

我们我们调用epoll_wait()函数的时候,系统创建一个epoll对象,每个对象都有一个叫做eventpoll类型的结构体与之对应,该结构体中主要有两个主要的成员,一个是rbn,代表将要通过epoll_ctl向epll对象中添加的事件。这些事件都是挂载在红黑树中。一个是rdlist,里面存放的是将要发生的事件。当我们使用epoll_ctrl()函数的时候,就是向epoll对象中添加,删除,修改感兴趣的事件。通过epoll_wait()调用收集在epoll监控中已经发生的事件。当监控的事件状态发生改变的时候,我们会调用函数把epitem加入到rdlist中去。

源码地址

链接: gitee地址

总结

本文总结了 socket,epoll 的特点,简短的实现了基于epoll的socket并发通信。在网络程序设计这门课中,孟宁老师深入浅出,生动有趣的讲解让我们了解了Javascript网络编程,Socket API,网络协议设计及RPC、Linux内核网络协议栈,受益匪浅。

  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在socket epoll高并发项目中,我们可以使用以下方法来测试高并发性: 1. 压力测试:可以使用工具如Apache JMeter或wrk等进行压力测试,模拟多个并发请求发送到服务器。可以设置并发连接数和请求频率,观察服务器的响应时间和处理能力。 2. 性能测试:可以使用工具如Apache Bench或siege等进行性能测试,发送多个并发请求并记录服务器的响应时间、吞吐量等指标。可以通过调整并发连接数和请求的大小来测试服务器的性能极限。 3. 负载均衡测试:如果项目中使用了负载均衡器来分流请求,可以模拟多个并发请求发送到负载均衡器,观察负载均衡器的转发能力和服务器的响应时间。 4. 异常情况测试:可以模拟网络延迟、断连、异常数据等异常情况,观察服务器的容错能力和恢复能力。 5. 数据库测试:如果项目中有涉及数据库的操作,可以模拟并发读写请求,观察数据库的性能和并发处理能力。 6. 监控和分析:在测试过程中,可以使用监控工具来实时监测服务器的CPU、内存、网络等指标,以及检查是否有内存泄漏或资源泄漏等问题。 通过以上的测试方法和手段,我们可以评估高并发项目的性能和稳定性,找出性能瓶颈和优化空间,提高系统的并发处理能力。 ### 回答2: 在进行socket epoll高并发项目的测试时,可以采取以下几个步骤来测试高并发性: 1. 设计并发测试场景:根据项目的需求和设计,确定需要模拟的并发用户数、每个用户的请求频率和请求类型。可以使用工具如Apache JMeter或自行编写脚本来模拟并发请求。 2. 配置并发环境:在测试机器上进行并发测试,需要增加网络带宽、增加计算资源,比如使用高性能的服务器和网络设备,确保能够支持大量并发连接。 3. 编写测试程序:根据项目的需求,在测试程序中实现模拟并发请求的逻辑,通过socket epoll模型建立大量并发连接,并发送模拟请求进行测试。 4. 监控并发连接数和响应时间:使用系统工具如netstat、top等来监控服务器端的并发连接数和系统资源使用情况。同时,使用性能监控工具如zabbix、grafana等来监控服务器的吞吐量、响应时间等指标。 5. 数据验证和压力测试:在并发测试中,确保数据的一致性,对接收到的响应进行验证。并逐步增加并发连接数,直至达到系统的极限,观察系统响应时间的变化情况和可能出现的性能瓶颈。 6. 多样化的测试场景:在测试过程中,可以尝试不同的测试场景,如不同的请求类型、不同大小的数据包等,验证系统在各种情况下的高并发性能。 7. 异常处理:在测试中,需要注意处理一些异常情况,如客户端异常断开连接、网络异常等,确保系统对异常情况的处理能力。 通过以上步骤,可以对socket epoll高并发项目进行有效的测试,找出系统的性能瓶颈,及时进行优化和调整,提升系统的高并发性能。 ### 回答3: 在socket epoll高并发项目中,为了测试高并发性能,可以采取以下几种方式: 1. 压力测试工具:使用一些专业的压力测试工具,如JMeter、Apache Bench或wrk等,来模拟大量的并发请求。可以设置并发数、每秒请求数和总请求量等参数,对系统进行压力测试,观察系统在高并发情况下的性能表现。 2. 自动化测试脚本:编写自动化测试脚本,通过多线程或多进程进行模拟并发请求,向服务器发送大量的请求。可以使用Python的模块,如requests、multiprocessing等,实现并发请求的测试。 3. 网络负载生成工具:使用网络负载生成工具,比如Locust、Gatling等,来模拟真实的网络负载情况。可以设置请求频率、并发数和持续时间等参数,模拟多种场景下的高并发情况。 4. 并发性能监控工具:使用一些并发性能监控工具,如Grafana、Prometheus等,来监控系统的并发性能。通过收集CPU、内存、网络等指标数据,可以分析和评估系统在高并发场景下的性能瓶颈。 5. 随机性测试:在测试过程中,引入随机性因素,模拟真实场景下的随机请求。可以设计不同类型的请求和不同请求参数的组合,观察系统在随机请求下的并发性能表现。 需要注意的是,在进行高并发性能测试时,要合理设置测试参数,以真实场景为依据,同时监控系统各项指标,及时发现并解决性能瓶颈。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值