今天我会带你把《模块三:网络编程》中涉及的课后练习题,逐一讲解,并给出每个课时练习题的解题思路和答案。
【问题】请你找一个 epoll 的 hello world 例子,并尝试理解它。
【解析】epoll 是一个 C 语言的 API,因此使用的时候需要一点 C 的基础。不过,即便没有,其实也不影响你读懂下面的程序。
下面是是一段摘自“https://github.com/millken/c-example/blob/master/epoll-example.c”的示例程序,该程序用 epoll 模式实现了一个服务,如下所示:
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/types.h>#include <sys/socket.h>#include <netdb.h>#include <unistd.h>#include <fcntl.h>#include <sys/epoll.h>#include <errno.h>#define MAXEVENTS 64static intmake_socket_non_blocking (int sfd){int flags, s;flags = fcntl (sfd, F_GETFL, 0);if (flags == -1){perror ("fcntl");return -1;}flags |= O_NONBLOCK;s = fcntl (sfd, F_SETFL, flags);if (s == -1){perror ("fcntl");return -1;}return 0;}static intcreate_and_bind (char *port){struct addrinfo hints;struct addrinfo *result, *rp;int s, sfd;memset (&hints, 0, sizeof (struct addrinfo));hints.ai_family = AF_UNSPEC; /* Return IPv4 and IPv6 choices */hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */hints.ai_flags = AI_PASSIVE; /* All interfaces */s = getaddrinfo (NULL, port, &hints, &result);if (s != 0){fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));return -1;}for (rp = result; rp != NULL; rp = rp->ai_next){sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);if (sfd == -1)continue;s = bind (sfd, rp->ai_addr, rp->ai_addrlen);if (s == 0){/* We managed to bind successfully! */break;}close (sfd);}if (rp == NULL){fprintf (stderr, "Could not bind\n");return -1;}freeaddrinfo (result);return sfd;}intmain (int argc, char *argv[]){int sfd, s;int efd;struct epoll_event event;struct epoll_event *events;if (argc != 2){fprintf (stderr, "Usage: %s [port]\n", argv[0]);exit (EXIT_FAILURE);}sfd = create_and_bind (argv[1]);if (sfd == -1)abort ();s = make_socket_non_blocking (sfd);if (s == -1)abort ();s = listen (sfd, SOMAXCONN);if (s == -1){perror ("listen");abort ();}efd = epoll_create1 (0);if (efd == -1){perror ("epoll_create");abort ();}event.data.fd = sfd;event.events = EPOLLIN | EPOLLET;s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);if (s == -1){perror ("epoll_ctl");abort ();}/* Buffer where events are returned */events = calloc (MAXEVENTS, sizeof event);/* The event loop */while (1){int n, i;n = epoll_wait (efd, events, MAXEVENTS, -1);for (i = 0; i < n; i++){if ((events[i].events & EPOLLERR) ||(events[i].events & EPOLLHUP) ||(!(events[i].events & EPOLLIN))){/* An error has occured on this fd, or the socket is notready for reading (why were we notified then?) */fprintf (stderr, "epoll error\n");close (events[i].data.fd);continue;}else if (sfd == events[i].data.fd){/* We have a notification on the listening socket, whichmeans one or more incoming connections. */while (1){struct sockaddr in_addr;socklen_t in_len;int infd;char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];in_len = sizeof in_addr;infd = accept (sfd, &in_addr, &in_len);if (infd == -1){if ((errno == EAGAIN) ||(errno == EWOULDBLOCK)){/* We have processed all incomingconnections. */break;}else{perror ("accept");break;}}s = getnameinfo (&in_addr, in_len,hbuf, sizeof hbuf,sbuf, sizeof sbuf,NI_NUMERICHOST | NI_NUMERICSERV);if (s == 0){printf("Accepted connection on descriptor %d ""(host=%s, port=%s)\n", infd, hbuf, sbuf);}/* Make the incoming socket non-blocking and add it to thelist of fds to monitor. */s = make_socket_non_blocking (infd);if (s == -1)abort ();event.data.fd = infd;event.events = EPOLLIN | EPOLLET;s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);if (s == -1){perror ("epoll_ctl");abort ();}}continue;}else{/* We have data on the fd waiting to be read. Read anddisplay it. We must read whatever data is availablecompletely, as we are running in edge-triggered modeand won't get a notification again for the samedata. */int done = 0;while (1){ssize_t count;char buf[512];count = read (events[i].data.fd, buf, sizeof buf);if (count == -1){/* If errno == EAGAIN, that means we have read alldata. So go back to the main loop. */if (errno != EAGAIN){perror ("read");done = 1;}break;}else if (count == 0){/* End of file. The remote has closed theconnection. */done = 1;break;}/* Write the buffer to standard output */s = write (1, buf, count);if (s == -1){perror ("write");abort ();}}if (done){printf ("Closed connection on descriptor %d\n",events[i].data.fd);/* Closing the descriptor will make epoll remove itfrom the set of descriptors which are monitored. */close (events[i].data.fd);}}}}free (events);close (sfd);return EXIT_SUCCESS;}
接下来我给你分析下这段程序。下面这句在创建一个 epoll 实例,这个实例本质上也是一个文件,文件中是对epoll
对象的调用序列。
efd = epoll_create1 (0);
下面这段程序在注册线程关心的事件:
struct epoll_event event;event.data.fd = sfd;event.events = EPOLLIN | EPOLLET;s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);
上面程序注册了两类关系的事件:
EPOLLIN ,关联的文件发生的读取;
EPOLLET, 关联的文件发生的写入。
接下来我们调用epoll_wait
来获取发生的事件:
n = epoll_wait (efd, events, MAXEVENTS, -1)
n
是需要响应的事件数量。 因为在这之前用make_socket_non_blocking
配置了非阻塞 IO,因此epoll_wait
有可能返回 0,也就是没有消息。 对于n>0
的情况,上面的示例程序中使用了 for 循环针对不同的消息类型进行处理。
下面这句if
判断是在看如果 sfd(服务端 Socket 文件描述符)和发生事件的文件描述符一致,代表这是一次客户端的连接操作。
if (sfd == events[i].data.fd)
于是再次调用epoll_ctl
将这个客户端的读写事件注册到关注列表。
如果上面的if
判断没有生效,说明这是一次客户端的读或写,这个时候使用read
或write
方法向客户端 Socket 文件中读取/写入数据。
【问题】在缓冲区的设计当中,还通常有一个 rewind 操作,这个操作是用来做什么的呢?
【解析】之前我们讨论了如果一个缓冲区是用来写入的,接下来要切换到读取状态可以使用 flip 操作。如果一个缓冲区进行了一次写和读,接下来要用它来处理另一批数据,可以使用 clear 操作来清空缓冲区。在实战当中,有时候一个缓冲区读取过了,需要再读取一次,此时就可以用 rewind 操作来重置缓冲区的 position 指针。
上面过程中 flip 和 rewind 都重置了 position 指针,那么它们的区别是什么呢?首先,你可以先从词义上理解下,flip 意味翻转(隐含读写状态切换),rewind 意味倒带(隐含重头读、重头写)。所以在实战中,首先我们应该从语义上区分它们的使用。
在实战的过程中,某些场景下 rewind 和 flip 结果相同。
比如现在缓冲区是 ABCDEFG,position=7, limit=7。这个时候代表我们已经完成了写入。如果需要切换到读取状态,用 flip 和 rewind 操作的结果相同,都会将 position 置零。
那么我提一个问题,这种情况下,应该用哪个呢?
写程序不只是为了正确,我们还为了可读。这种情况下,因为是读写状态的切换,因此当然用 flip。
再举个例子,比如现在缓冲区是 ABCDEFG,position=3,limit=7,缓冲区处于读取状态。如果我们想要重读,应该用什么呢?当然是 rewind,rewind 有倒带的语义。你可以思考,这个时候如果调 flip 结果对吗?
这个时候调 flip 处理会把 position 置为 0 外,limit 也会设置为 3(position 的旧值)。因为只有这样,才是读写状态的翻转。也就是说,如果写入了 3 个字符,不管 limit 现在是多少,flip 切换到读取状态也只能读 3 个字符。
所以,flip 和 rewind 实现不同是其次,最重要的是语义不同。建议你以后看到 API 的时候,先搞明白单词是什么意思,而不是急于分析具体实现。从这个话题引申出一个小的提示,就是不要盲目读源代码,在阅读一个项目的源代码前,思考下自己对要解决的问题、如何解决这些问题,带着这种根深的理解再去读源码。
【问题】I/O 多路复用用协程和用线程的区别?
【解析】线程是执行程序的最小单位。I/O 多路复用时,会用单个线程处理大量的 I/O。还有一种执行程序的模型,叫协作程,协程是轻量级的线程。操作系统将执行资源分配给了线程,然后再调度线程运行。如果要实现协程,就要利用分配给线程的执行资源,在这之上再创建更小的执行单位。协程不归操作系统调度,协程共享线程的执行资源。
而 I/O 多路复用的意义,是减少线程间的切换成本。因此从设计上,只要是用单个线程处理大量 I/O 工作,线程和协程是一样的,并无区别。如果是单线程处理大量 I/O,使用协程也是依托协程对应线程执行能力。
【问题】如何理解 Dubbo 的几个组成部分 Consumer、Provider、Monitor 和 Registry?
【解析】Dubbo 是一个开源、轻量级的 Java 服务框架。下图是它的架构:
Dubbo 的架构是容器化的,上 图中的 Container(容器)中是服务,服务的提供方被称作 Provider。比如要提供一个订单服务,那么服务会在容器中部署启动,启动后的实例就是 Provider。
Provider 在启动过程中,会在 Dubbo 中注册自己。负责注册和发现的模块,称为注册处(Registry)。注册处和学员报道时学校的注册处很像,每个新加入的服务都需要主动注册。这里需要注意,注册处对网络中的信息是信任的,如果 Provider 被攻击欺骗注册处会产生安全问题。Registry 需要实现分布式共识,具体可以使用 ZooKeeper实现(参考 Paxos 和 Raft 算法)。
服务的使用方被称为 Consumer,Consumer 会订阅注册表的变化(也就是 Provider 的变化)。相当于 Consumer 本地维护了一份和注册处一致的 Provider 清单。当调用服务的时候,Consumer 会使用本地清单去查询 Provider 信息,进行远程调用。
除了 Registry、Consumer、Provider 之外,Dubbo 还有一个 Monitor 模块。这个模块负责统计服务器的调用情况。
《网络编程》模块我们围绕着Socket展开,Socket 是程序也是文件。文件本质是数据,为了抽象数据,我们学习了流。这里再复习下,流是随着时间产生的数据。文件传输、视频播放、在线游戏……这些都是随着时间产生的数据。为了提升处理数据的效率,节省内存资源,我们还学习了缓冲区。关于缓冲区,目前向你介绍了 3 种操作:flip 用于读写切换、clear 用于重置缓冲区、rewind 用于重读数据。
为了减少线程的切换成本,我们会使用 I/O 的多路复用。为了让程序更可读,我们会选择适合的编程模型。这个模块介绍了 3 种编程模型,分别是 BIO/NIO/AIO。选择编程模型处理 I/O 还要思考数据拷贝的效率、事件通知的方式。思考事件通知的方式,又需要思考核心部分数据结构的设计。所以,如果你想在工作当中应对不同场景处理好 I/O 问题,不能死记硬背,而是要理解每个细微选择背后的逻辑,并在完成工作后认真对程序进行性能测试。这样才能做到万无一失。
发现求知的乐趣,我是林䭽,感谢你学习本次课程。 接下来我们将进入《模块四:Web 技术》的学习,下一讲介绍《14 | DNS 域名解析系统:CNAME 记录的作用是?》,再见!