TCP/IP网络编程之优于select的epoll(二)

2024-04-12 02:18

本文主要是介绍TCP/IP网络编程之优于select的epoll(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

基于epoll的回声服务端

在TCP/IP网络编程之优于select的epoll(一)这一章中,我们介绍了epoll的相关函数,接下来给出基于epoll的回声服务端示例。

echo_epollserv.c

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <arpa/inet.h>

#include <sys/socket.h>

#include <sys/epoll.h>

 

#define BUF_SIZE 100

#define EPOLL_SIZE 50

void error_handling(char *buf);

 

int main(int argc, char *argv[])

{

    int serv_sock, clnt_sock;

    struct sockaddr_in serv_adr, clnt_adr;

    socklen_t adr_sz;

    int str_len, i;

    char buf[BUF_SIZE];

 

    struct epoll_event *ep_events;

    struct epoll_event event;

    int epfd, event_cnt;

 

    if (argc != 2) {

        printf("Usage : %s <port>\n", argv[0]);

        exit(1);

    }

 

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);

    memset(&serv_adr, 0, sizeof(serv_adr));

    serv_adr.sin_family = AF_INET;

    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);

    serv_adr.sin_port = htons(atoi(argv[1]));

 

    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)

        error_handling("bind() error");

    if (listen(serv_sock, 5) == -1)

        error_handling("listen() error");

 

    epfd = epoll_create(EPOLL_SIZE);

    ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

 

    event.events = EPOLLIN;

    event.data.fd = serv_sock;

    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

 

    while (1)

    {

        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);

        if (event_cnt == -1)

        {

            puts("epoll_wait() error");

            break;

        }

 

        for (i = 0; i < event_cnt; i++)

        {

            if (ep_events[i].data.fd == serv_sock)

            {

                adr_sz = sizeof(clnt_adr);

                clnt_sock =

                    accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);

                event.events = EPOLLIN;

                event.data.fd = clnt_sock;

                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);

                printf("connected client: %d \n", clnt_sock);

            }

            else

            {

                str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);

                if (str_len == 0) // close request!

                {

                    epoll_ctl(

                        epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);

                    close(ep_events[i].data.fd);

                    printf("closed client: %d \n", ep_events[i].data.fd);

                }

                else

                {

                    write(ep_events[i].data.fd, buf, str_len); // echo!

                }

            }

        }

    }

    close(serv_sock);

    close(epfd);

    return 0;

}

 

void error_handling(char *buf)

{

    fputs(buf, stderr);

    fputc('\n', stderr);

    exit(1);

}

  

之前解释过关键代码,而且程序结构与select方式没有区别,故省略代码说明。

条件触发和边缘触发

条件触发的方式中,只要输入缓冲有数据就会一直通知该事件。例如,服务端输入缓冲收到50字节的数据时,服务端操作系统将通知该事件(注册到发生变化的文件描述符)。但服务端读取20字节后还剩30字节的情况下,仍会注册事件。也就是说,条件触发方式中,只要输入缓冲中还剩有数据,就将以事件方式再次注册。而边缘触发中输入缓冲收到数据时仅注册一次事件,即使输入缓冲中还留有数据,也不会再进行注册

接下来通过代码了解条件触发的事件注册方式

echo_EPLTserv.c 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <fcntl.h>

#include <arpa/inet.h>

#include <sys/socket.h>

#include <sys/epoll.h>

 

#define BUF_SIZE 4

#define EPOLL_SIZE 50

void error_handling(char *buf);

 

int main(int argc, char *argv[])

{

    int serv_sock, clnt_sock;

    struct sockaddr_in serv_adr, clnt_adr;

    socklen_t adr_sz;

    int str_len, i;

    char buf[BUF_SIZE];

 

    struct epoll_event *ep_events;

    struct epoll_event event;

    int epfd, event_cnt;

 

    if (argc != 2) {

        printf("Usage : %s <port>\n", argv[0]);

        exit(1);

    }

 

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);

    memset(&serv_adr, 0, sizeof(serv_adr));

    serv_adr.sin_family = AF_INET;

    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);

    serv_adr.sin_port = htons(atoi(argv[1]));

 

    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)

        error_handling("bind() error");

    if (listen(serv_sock, 5) == -1)

        error_handling("listen() error");

 

    epfd = epoll_create(EPOLL_SIZE);

    ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

 

    event.events = EPOLLIN;

    event.data.fd = serv_sock;

    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

 

    while (1)

    {

        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);

        if (event_cnt == -1)

        {

            puts("epoll_wait() error");

            break;

        }

 

        puts("return epoll_wait");

        for (i = 0; i < event_cnt; i++)

        {

            if (ep_events[i].data.fd == serv_sock)

            {

                adr_sz = sizeof(clnt_adr);

                clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);

                event.events = EPOLLIN;

                event.data.fd = clnt_sock;

                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);

                printf("connected client: %d \n", clnt_sock);

            }

            else

            {

                str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);

                if (str_len == 0) // close request!

                {

                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);

                    close(ep_events[i].data.fd);

                    printf("closed client: %d \n", ep_events[i].data.fd);

                }

                else

                {

                    write(ep_events[i].data.fd, buf, str_len); // echo!

                }

            }

        }

    }

    close(serv_sock);

    close(epfd);

    return 0;

}

 

void error_handling(char *buf)

{

    fputs(buf, stderr);

    fputc('\n', stderr);

    exit(1);

}

  

上述示例与之前的echo_epollserv.c之间的差异如下:

  •  第10行:将调用read函数时使用的缓冲大小缩减为4字节
  • 第58行:插入验证epoll_wait函数调用次数的语句

减少缓冲大小是为了阻止服务端一次性读取接收的数据。换言之,调用read函数后,输入缓冲中仍然有数据需要读取。而且会因此注册新的事件并从epoll_wait函数返回时将循环输出“return epoll_wait”字符串。前提是条件触发的工作方式与之前描述一致。接下来观察运行结果

编译echo_EPLTserv.c 并运行

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

# gcc echo_EPLTserv.c -o echo_EPLTserv

# ./echo_EPLTserv 8500

return epoll_wait

connected client: 5

return epoll_wait

connected client: 6

return epoll_wait

return epoll_wait

return epoll_wait

return epoll_wait

return epoll_wait

closed client: 5

return epoll_wait

return epoll_wait

return epoll_wait

return epoll_wait

return epoll_wait

closed client: 6

  

运行echo_client ONE:

1

2

3

4

5

# ./echo_client 127.0.0.1 8500

Connected...........

Input message(Q to quit): It's my life

Message from server: It's my life

Input message(Q to quit): q

  

运行echo_client TWO:

1

2

3

4

5

# ./echo_client 127.0.0.1 8500

Connected...........

Input message(Q to quit): It's your life

Message from server: It's your life

Input message(Q to quit): q

  

从运行结果可以看出,每当接收到客户端数据时,都会注册该事件,并且多次调用epoll_wait函数

边缘触发的服务端实现中必知的两点:

  • 通过error变量验证错误原因
  • 为了完成非阻塞I/O,更改套接字特性

Linux套接字相关函数一般通过返回-1通知发生错误,虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误时提供额外的信息,Linux声明了全局变量:int errno;为了访问该变量,需要引入error.h头文件,因为此头文件中有上述变量的extern声明。另外,每种函数发生错误时,保存到error变量的值都不同。

下面讲解将套接字改为非阻塞方式的方法,Linux提供更改或读取文件属性的如下方法:

1

2

#include <fcntl.h>

int fcntl(int filedes, int cmd, ...); //成功时返回cmd参数相关值,失败时返回-1

  

  • filedes:属性更改目标的文件描述符
  • cmd:表示函数调用的目的

从上述声明中可以看到,fcntl具有可变参数形式。如果向第二个参数传递F_GETFL,可以获得第一个参数所指的文件描述符属性(int型)。反之,如果传递F_SETFL,可以更改文件描述符属性。若希望将文件(套接字)改为非阻塞模式,需要如下两条语句 

1

2

int flag = fcntl(fd, F_GETFL, 0);

fcntl(fd, F_SETFL, flag|O_NONBLOCK);

  

通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。调用read和write函数时,无论是否存在数据,都会形成非阻塞文件(套接字) 

echo_EPETserv.c

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <fcntl.h>

#include <errno.h>

#include <arpa/inet.h>

#include <sys/socket.h>

#include <sys/epoll.h>

 

#define BUF_SIZE 4

#define EPOLL_SIZE 50

void setnonblockingmode(int fd);

void error_handling(char *buf);

 

int main(int argc, char *argv[])

{

    int serv_sock, clnt_sock;

    struct sockaddr_in serv_adr, clnt_adr;

    socklen_t adr_sz;

    int str_len, i;

    char buf[BUF_SIZE];

 

    struct epoll_event *ep_events;

    struct epoll_event event;

    int epfd, event_cnt;

 

    if (argc != 2) {

        printf("Usage : %s <port>\n", argv[0]);

        exit(1);

    }

 

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);

    memset(&serv_adr, 0, sizeof(serv_adr));

    serv_adr.sin_family = AF_INET;

    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);

    serv_adr.sin_port = htons(atoi(argv[1]));

 

    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)

        error_handling("bind() error");

    if (listen(serv_sock, 5) == -1)

        error_handling("listen() error");

 

    epfd = epoll_create(EPOLL_SIZE);

    ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

 

    setnonblockingmode(serv_sock);

    event.events = EPOLLIN;

    event.data.fd = serv_sock;

    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

 

    while (1)

    {

        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);

        if (event_cnt == -1)

        {

            puts("epoll_wait() error");

            break;

        }

 

        puts("return epoll_wait");

        for (i = 0; i < event_cnt; i++)

        {

            if (ep_events[i].data.fd == serv_sock)

            {

                adr_sz = sizeof(clnt_adr);

                clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);

                setnonblockingmode(clnt_sock);

                event.events = EPOLLIN | EPOLLET;

                event.data.fd = clnt_sock;

                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);

                printf("connected client: %d \n", clnt_sock);

            }

            else

            {

                while (1)

                {

                    str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);

                    if (str_len == 0) // close request!

                    {

                        epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);

                        close(ep_events[i].data.fd);

                        printf("closed client: %d \n", ep_events[i].data.fd);

                        break;

                    }

                    else if (str_len < 0)

                    {

                        if (errno == EAGAIN)

                            break;

                    }

                    else

                    {

                        write(ep_events[i].data.fd, buf, str_len); // echo!

                    }

                }

            }

        }

    }

    close(serv_sock);

    close(epfd);

    return 0;

}

 

void setnonblockingmode(int fd)

{

    int flag = fcntl(fd, F_GETFL, 0);

    fcntl(fd, F_SETFL, flag | O_NONBLOCK);

}

 

void error_handling(char *buf)

{

    fputs(buf, stderr);

    fputc('\n', stderr);

    exit(1);

}

  

  • 第11行:为了验证边缘触发的工作方式,将缓冲设置为4个字节
  • 第61行:为观察事件发生数而添加的输出字符串的语句
  • 第68、69行:第68行将accept函数创建的套接字改为非阻塞模式,第69行向EPOLLIN添加EPOLLET标志,将套接字事件注册方式改为边缘触发
  • 第76、78行:之前的条件触发回声服务端中没有while循环,边缘触发方式中,发生事件时需要读取输入缓冲中的所有数据,因此需要循环调用read函数,如第78行所示
  • 第86行:read函数返回-1且errno值为EAGAIN时,意味着读取了输入缓冲中的全部数据,因此需要通过break语句跳出第76行的循环

 

编译echo_EPETserv.c运行

1

2

3

4

5

6

7

8

# ./echo_EPETserv 8500

return epoll_wait

connected client: 5

return epoll_wait

return epoll_wait

return epoll_wait

return epoll_wait

closed client: 5

  

运行客户端程序

1

2

3

4

5

6

7

8

9

# ./echo_client 127.0.0.1 8500

Connected...........

Input message(Q to quit): I like computer programming

Message from server: I like computer programming

Input message(Q to quit): Do you like computer programming?

Message from server: Do you like computer programming?

Input message(Q to quit): Good bye

Message from server: Good bye

Input message(Q to quit): q

  

上述运行结果中需要注意的是,客户端发送消息次数和服务端epoll_wait函数调用次数。客户端从请求连接到断开连接共发送数据5次,服务端也相应产生5个事件

条件触发和边缘触发孰优孰劣

我们从理论和代码的角度充分理解了条件触发和边缘触发,但仅凭这些还无法理解边缘触发相对于条件触发的优点。边缘触发可以做到分离接收数据和处理时间的时间点。虽然比较简单,但非常准确有力地说明了边缘触发的优点,现在我们来看图1-1

 

图1-1   理解边缘触发

图1-1的运行流程如下:

  • 服务端分别从客户端A、B、C接收数据
  • 服务端按照A、B、C的顺序重新组合收到的数据
  • 组合的数据将发送给任意主机

为了完成该过程,若能按如下流程运行程序,服务端的实现并不难

  • 客户端按照A、B、C的顺序连接服务端,并依序向服务端发送数据
  • 需要接收数据的客户端应在客户端A、B、C之前连接到服务端并等待

但现实中可能频繁出现如下这些情况,换言之,如下情况更符合实际

  • 客户端C和B向服务端发送数据,但A尚未连接到服务端
  • 客户端A、B、C乱序发送数据
  • 服务端已收到数据,但要接收数据的目标客户端还未连接到服务端

因此,即使输入缓冲收到数据(注册相应事件),服务端也能决定读取和处理这些数据的时间点,这样就给服务端的实现带来巨大的灵活性。

那么,条件触发中无法区分数据接收和处理吗?并非不可能。但在输入缓冲收到数据的情况下,如果不读取(延迟处理),则每次调用epoll_wait函数时都会产生相应事件。而且事件数也会累加,服务端能承受吗?这在现实中是不合理的

条件触发和边缘触发的区别主要应从服务端实现模型的角度讨论,从实现模型来看,边缘触发更有可能带来高性能,但不能简单地认为“只要使用边缘触发就一定能提高速度”

https://www.cnblogs.com/beiluowuzheng/p/9696641.html#top

这篇关于TCP/IP网络编程之优于select的epoll(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/895831

相关文章

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

ASIO网络调试助手之一:简介

多年前,写过几篇《Boost.Asio C++网络编程》的学习文章,一直没机会实践。最近项目中用到了Asio,于是抽空写了个网络调试助手。 开发环境: Win10 Qt5.12.6 + Asio(standalone) + spdlog 支持协议: UDP + TCP Client + TCP Server 独立的Asio(http://www.think-async.com)只包含了头文件,不依

poj 3181 网络流,建图。

题意: 农夫约翰为他的牛准备了F种食物和D种饮料。 每头牛都有各自喜欢的食物和饮料,而每种食物和饮料都只能分配给一头牛。 问最多能有多少头牛可以同时得到喜欢的食物和饮料。 解析: 由于要同时得到喜欢的食物和饮料,所以网络流建图的时候要把牛拆点了。 如下建图: s -> 食物 -> 牛1 -> 牛2 -> 饮料 -> t 所以分配一下点: s  =  0, 牛1= 1~

poj 3068 有流量限制的最小费用网络流

题意: m条有向边连接了n个仓库,每条边都有一定费用。 将两种危险品从0运到n-1,除了起点和终点外,危险品不能放在一起,也不能走相同的路径。 求最小的费用是多少。 解析: 抽象出一个源点s一个汇点t,源点与0相连,费用为0,容量为2。 汇点与n - 1相连,费用为0,容量为2。 每条边之间也相连,费用为每条边的费用,容量为1。 建图完毕之后,求一条流量为2的最小费用流就行了

poj 2112 网络流+二分

题意: k台挤奶机,c头牛,每台挤奶机可以挤m头牛。 现在给出每只牛到挤奶机的距离矩阵,求最小化牛的最大路程。 解析: 最大值最小化,最小值最大化,用二分来做。 先求出两点之间的最短距离。 然后二分匹配牛到挤奶机的最大路程,匹配中的判断是在这个最大路程下,是否牛的数量达到c只。 如何求牛的数量呢,用网络流来做。 从源点到牛引一条容量为1的边,然后挤奶机到汇点引一条容量为m的边

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

Go Playground 在线编程环境

For all examples in this and the next chapter, we will use Go Playground. Go Playground represents a web service that can run programs written in Go. It can be opened in a web browser using the follow

深入理解RxJava:响应式编程的现代方式

在当今的软件开发世界中,异步编程和事件驱动的架构变得越来越重要。RxJava,作为响应式编程(Reactive Programming)的一个流行库,为Java和Android开发者提供了一种强大的方式来处理异步任务和事件流。本文将深入探讨RxJava的核心概念、优势以及如何在实际项目中应用它。 文章目录 💯 什么是RxJava?💯 响应式编程的优势💯 RxJava的核心概念

函数式编程思想

我们经常会用到各种各样的编程思想,例如面向过程、面向对象。不过笔者在该博客简单介绍一下函数式编程思想. 如果对函数式编程思想进行概括,就是f(x) = na(x) , y=uf(x)…至于其他的编程思想,可能是y=a(x)+b(x)+c(x)…,也有可能是y=f(x)=f(x)/a + f(x)/b+f(x)/c… 面向过程的指令式编程 面向过程,简单理解就是y=a(x)+b(x)+c(x)