最新要闻
- 不是“空中楼阁”:努比亚Pad 3D搭载全球最大Leia 3D内容生态
- 【报资讯】男子车停路边去吃烧烤 回来瞬间崩溃:路边已装上护栏
- 【独家焦点】作文游西湖300字(精选40篇)
- 千里托运奔驰GLC被淋满牛粪 女子崩溃:花1900元洗了5遍
- 【世界速看料】情侣打车3小时后跑单拉黑司机 司机:246元车费没了
- 世界资讯:微软承认向无法升级的设备推荐Win11:已进行修复
- 环球即时:压水堆
- 当前滚动:这些“领导”短信收到没?专门针对iPhone用户诈骗:全国多地预警
- 环球精选!王一博、梁朝伟《无名》北美院线扩映!豆瓣降至6.7分
- 当前简讯:大爷怒斥夜市挂日本元素油纸伞:主办方回应令人不解
- 环球头条:导演新海诚:中国动画电影迟早会超过日本
- 天天热文:开办以来首位!跨性别演员柏林电影节获奖
- 微速讯:长城放出王炸?长城水平对置八缸发动机摩托曝光 真猛兽
- 环球热头条丨可以两天一充的骁龙8 Gen2手机:出现了
- 每日热讯!马里肯涅巴地区发生武装抢劫 中使馆提醒关注当地安全情况
- 威马汽车再发内部信:部分员工复工 其余人员无薪休假
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
LWIP学习记录---ARP协议(2)ARP数据包发送过程
(一)ARP之 数据包接收过程
(资料图片)
先看一下整个数据流的传输过程。
首先etherneti_input()函数 从底层网卡驱动接收到原始数据,若是ip包或者ARP包则调用ethernet_input()。
s32_t ethernetif_input(struct netif *netif){ struct ethernetif *ethernetif; //网络接口信息结构体,此处无用。 struct eth_hdr *ethhdr; //以太网帧 头部结构体指针。 struct pbuf *p; ethernetif = netif->state; p = low_level_input(netif); //!!调用底层函数读取一个数据包。 if (p == NULL) { return 0; } ethhdr = p->payload; //将ethhdr指针指向数据包中以太网头部 switch (htons(ethhdr->type)) { //判断数据包中的帧类型 ,要大小端转换。 case ETHTYPE_IP: case ETHTYPE_ARP: /* full packet send to tcpip_thread to process */ if (netif->input(p, netif)!=ERR_OK) //调用netif->input 进行处理。 { pbuf_free(p); //释放Pbuf p = NULL; } break; default: pbuf_free(p); p = NULL; break; } return 1;}
err_t ethernet_input(struct pbuf *p, struct netif *netif){ struct eth_hdr* ethhdr; //以太网帧头部结构体指针 u16_t type; if (p->len <= SIZEOF_ETH_HDR) { //长度校验,ARP包必须包含在第一个PBUF的数据区。 goto free_and_return; } ethhdr = (struct eth_hdr *)p->payload; //以太网帧指针 指向以太网帧头部 type = ethhdr->type; //获取帧类型 switch (type) { case PP_HTONS(ETHTYPE_IP):#if ETHARP_TRUST_IP_MAC /* update ARP table */ etharp_ip_input(netif, p); //使用IP头部以及 以太网头部MAC 更新 ARP表。#endif /* ETHARP_TRUST_IP_MAC */ /* skip Ethernet header */ if(pbuf_header(p, -ip_hdr_offset)) { //去掉以太网头部 goto free_and_return; //若操作失败,则释放pbuf } else { /* pass to IP layer */ ip_input(p, netif); //若头部去除成功,则调用IP输入处理函数处理数据。 } break; case PP_HTONS(ETHTYPE_ARP): //若是ARP数据包 /* pass p to ARP module */ etharp_arp_input(netif, (struct eth_addr*)(netif->hwaddr), p); //调用ARP数据包处理函数。 break; default: goto free_and_return; } return ERR_OK;free_and_return: pbuf_free(p); return ERR_OK;}
这个函数也很简单,就是根据数据包的type 类型进行判断 是IP包还是ARP包?是IP包则去掉 **以太网帧头部**,调用**ip_input**函数处理,若是ARP包,则调用**etharp_arp_input() **处理数据包。那我们接下来看看数据包是怎么处理的呢?
(二)ARP之 数据包处理过程
从之前讲述的知识可以了解到 etharp_arp_input()函数有两个功能:
若接收到是ARP应答包,则需要根据应答信息更新ARP缓存表。
若接收到ARP请求包,则需要:
若这个请求包,与本机IP地址不符合,则不需要应答,但是要将请求包中的 IP和MAC加入到自己的缓存表中,以备到时候需要使用。
若这个请求包,与本机IP地址符号,除了要将源主机的IP与MAC加入缓存表之外,还要回复一个应答,告诉本机的MAC地址是多少。
好,有了上面的思路,我们具体来看一看代码是怎么实现的。
(1)etharp_arp_input()函数分析
细细品味~~
static void etharp_arp_input(struct netif *netif, struct eth_addr *ethaddr, struct pbuf *p){ struct etharp_hdr *hdr; //ARP数据包包头部结构指针。 struct eth_hdr *ethhdr; //以太网头部结构体指针 ip_addr_t sipaddr, dipaddr; u8_t for_us; //指示ARP包是否发送给本机的。 /* drop short ARP packets: we have to check for p->len instead of p->tot_len here since a struct etharp_hdr is pointed to p->payload, so it musn"t be chained! */ if (p->len < SIZEOF_ETHARP_PACKET) { //arp 数据包不能分装在两个PBUF中,不然不能用指针来操作内部了。 pbuf_free(p); return; } ethhdr = (struct eth_hdr *)p->payload; hdr = (struct etharp_hdr *)((u8_t*)ethhdr + SIZEOF_ETH_HDR); //hdr 指向ARP数据包首部 /* RFC 826 "Packet Reception": */ if ((hdr->hwtype != PP_HTONS(HWTYPE_ETHERNET)) || //判断ARP数据包的合法型,要是不合法,则直接删除该数据包。 (hdr->hwlen != ETHARP_HWADDR_LEN) || (hdr->protolen != sizeof(ip_addr_t)) || (hdr->proto != PP_HTONS(ETHTYPE_IP))) { pbuf_free(p); return; } /* Copy struct ip_addr2 to aligned ip_addr, to support compilers without * structure packing (not using structure copy which breaks strict-aliasing rules). */ //由于arp 数据包中的IP地址地段并不是字节对齐的,不能直接使用,需要将IP 拷贝到sipaddr,dipaddr中使用。 IPADDR2_COPY(&sipaddr, &hdr->sipaddr); IPADDR2_COPY(&dipaddr, &hdr->dipaddr); //1.判断请求包是否给我 if (ip_addr_isany(&netif->ip_addr)) { //若本机的网卡IP未配置,那么肯定不是发给我们的 for_us = 0; } else { /* ARP packet directed to us? */ for_us = (u8_t)ip_addr_cmp(&dipaddr, &(netif->ip_addr)); //若配置了本机IP,那比较一下IP是否相同。 } //2.那么接下来添加一下ARP缓存表 //若这个ARP包(不论请求还是相应包)是给我们的,那么更新ARP表。 如果不是给我们的,也要更新ARP表,然后不设置ETHARP_TRY_HARD标志 etharp_update_arp_entry(netif, &sipaddr, &(hdr->shwaddr), for_us ? ETHARP_FLAG_TRY_HARD : ETHARP_FLAG_FIND_ONLY); /* 3.now 处理 ARP数据包 */ switch (hdr->opcode) { /* 是ARP请求包吗? */ case PP_HTONS(ARP_REQUEST): if (for_us) { //是ARP请求包,且是发给我的 hdr->opcode = htons(ARP_REPLY); //改变标志为为 应答包 IPADDR2_COPY(&hdr->dipaddr, &hdr->sipaddr); //ip地址交换 IPADDR2_COPY(&hdr->sipaddr, &netif->ip_addr); //设置4个MAC地址字段 ETHADDR16_COPY(&hdr->dhwaddr, &hdr->shwaddr); //ARP首部中接收端MAC ETHADDR16_COPY(ðhdr->dest, &hdr->shwaddr); ETHADDR16_COPY(&hdr->shwaddr, ethaddr); ETHADDR16_COPY(ðhdr->src, ethaddr); /* hwtype, hwaddr_len, proto, protolen and the type 这些字段保持不变 */ /*发送ARP应答包*/ netif->linkoutput(netif, p); } else if (ip_addr_isany(&netif->ip_addr)) { //如果这个ARP请求包不是给我的,不做任何处理。 } else { //如果这个ARP请求包不是给我的,不做任何处理。 } break; case PP_HTONS(ARP_REPLY): //如果是ARP应答包,前面已经处理过了,现在什么也不用做。 break; default: break; } /* free ARP packet */ pbuf_free(p);}
总结一下,这个函数就是实现了上面上述的思路。更新ARP表项的这个动作是放在函数**etharp_update_arp_entry()**中完成的关于这个函数,后续会详细讲解。此处说明一些,标志位**ETHARP_FLAG_TRY_HARD ** 的含义:ETHARP_FLAG_TRY_HARD :告诉函数无论如何要创建这个地址对的表项,如果所有的ARP表项都用完了,就删除最老的表项。
ETHARP_FLAG_FIND_ONLY:遍历整个缓存表寻找空闲表项,如果找不到,就不添加了。
(2)ARP缓存表的更新
ARP缓存表的更新是调用etharp_update_arp_entry()来实现的。在介绍这个函数之前要先介绍etharp_find_entry()这个函数。
1. etharp_find_entry()
该函数功能是 寻找 一个与IP地址符合的ARP表项 或者 创建一个新的ARP表项,并返回该表项的索引号
如果 ipaddr 非零,则返回一个处于pending或stable 状态的表项。* 若没有,则返回一个empty表项,且该表项ip地段要设置为ipaddr。这样etharp_find_entry()函数返回后,调用者需要将empty 状态,改变为pending状态。再如果,没有empty表项呢?那么根据传入的关键字来做处理,若是ETHART_TRY_HARD,则**删除**所有表项中年龄最老的那个,建立新表项。若ipaddr为0,则要返回一个empty状态的表项。
所有这个过程要怎么实现呢?要循环遍历3 遍吗,并不需要。Lwip 一边判断表项是否匹配,一边记录索引编号最低的empty表项,一边记录年龄最大的stable表项,一边记录年龄最大且缓存队列为空的Pending表项,一边记录年龄最大且缓存队列不为空的Pending表项。
static s8_tetharp_find_entry(ip_addr_t *ipaddr, u8_t flags){ s8_t old_pending = ARP_TABLE_SIZE, old_stable = ARP_TABLE_SIZE; s8_t empty = ARP_TABLE_SIZE; u8_t i = 0, age_pending = 0, age_stable = 0; /* oldest entry with packets on queue */ s8_t old_queue = ARP_TABLE_SIZE; /* its age */ u8_t age_queue = 0; /** * a) do a search through the cache, remember candidates * b) select candidate entry * c) create new entry */ /* a) in a single search sweep, do all of this * 1) remember the first empty entry (if any) * 2) remember the oldest stable entry (if any) * 3) remember the oldest pending entry without queued packets (if any) * 4) remember the oldest pending entry with queued packets (if any) * 5) search for a matching IP entry, either pending or stable * until 5 matches, or all entries are searched for. */ for (i = 0; i < ARP_TABLE_SIZE; ++i) { u8_t state = arp_table[i].state; /* no empty entry found yet and now we do find one? */ if ((empty == ARP_TABLE_SIZE) && (state == ETHARP_STATE_EMPTY)) { empty = i; } else if (state != ETHARP_STATE_EMPTY) state == ETHARP_STATE_PENDING || state >= ETHARP_STATE_STABLE); /* if given, does IP address match IP address in ARP entry? */ if (ipaddr && ip_addr_cmp(ipaddr, &arp_table[i].ipaddr)) { /* found exact IP address match, simply bail out */ return i; } /* pending entry? */ if (state == ETHARP_STATE_PENDING) { /* pending with queued packets? */ if (arp_table[i].q != NULL) { if (arp_table[i].ctime >= age_queue) { old_queue = i; age_queue = arp_table[i].ctime; } } else /* pending without queued packets? */ { if (arp_table[i].ctime >= age_pending) { old_pending = i; age_pending = arp_table[i].ctime; } } /* stable entry? */ } else if (state >= ETHARP_STATE_STABLE) { { /* remember entry with oldest stable entry in oldest, its age in maxtime */ if (arp_table[i].ctime >= age_stable) { old_stable = i; age_stable = arp_table[i].ctime; } } } } } /* { we have no match } => try to create a new entry */ /* don"t create new entry, only search? */ if (((flags & ETHARP_FLAG_FIND_ONLY) != 0) || /* or no empty entry found and not allowed to recycle? */ ((empty == ARP_TABLE_SIZE) && ((flags & ETHARP_FLAG_TRY_HARD) == 0))) { return (s8_t)ERR_MEM; } /* b) choose the least destructive entry to recycle: * 1) empty entry * 2) oldest stable entry * 3) oldest pending entry without queued packets * 4) oldest pending entry with queued packets * * { ETHARP_FLAG_TRY_HARD is set at this point } */ /* 1) empty entry available? */ if (empty < ARP_TABLE_SIZE) { i = empty; } else { /* 2) found recyclable stable entry? */ if (old_stable < ARP_TABLE_SIZE) { /* recycle oldest stable*/ i = old_stable; } else if (old_pending < ARP_TABLE_SIZE) { /* recycle oldest pending */ i = old_pending; /* 4) found recyclable pending entry with queued packets? */ } else if (old_queue < ARP_TABLE_SIZE) { /* recycle oldest pending (queued packets are free in etharp_free_entry) */ i = old_queue; /* no empty or recyclable entries found */ } else { return (s8_t)ERR_MEM; } etharp_free_entry(i); } arp_table[i].state == ETHARP_STATE_EMPTY); /* IP address given? */ if (ipaddr != NULL) { /* set IP address */ ip_addr_copy(arp_table[i].ipaddr, *ipaddr); } arp_table[i].ctime = 0; return (err_t)i;}
此函数较为复杂,稍微整理一下流程:
- 对于每个表项,判断是否是empty,
- 若不是,则表明已经被占用,立即检查IP是否匹配?
- 若匹配,则返回该索引值
- 若不匹配,判断是否是pending状态,且该索引的数据包指针是否为空?该函数试图分别记录生存时间最长的有数据缓存或无数据缓存的表项索引。如果一个表项也不是Pending状态,则判断是不是stable状态?
- 对于stable状态,与pending状态处理过程类似,该函数试图记录生产时间最长的stable的表项索引。
- 若不是,则表明已经被占用,立即检查IP是否匹配?
2.etharp_updata_arp_entry()
接下来看看这个函数就比较轻松了。先看代码:
tatic err_tetharp_update_arp_entry(struct netif *netif, ip_addr_t *ipaddr, struct eth_addr *ethaddr, u8_t flags){ s8_t i; if (ip_addr_isany(ipaddr) || //地址有效性验证,不能为广播,多播地址建立表项 ip_addr_isbroadcast(ipaddr, netif) || ip_addr_ismulticast(ipaddr)) { return ERR_ARG; } /* 查找或新建一个表项 */ i = etharp_find_entry(ipaddr, flags); /*若不合法,返回 */ if (i < 0) { return (err_t)i; } /* mark it stable */ arp_table[i].state = ETHARP_STATE_STABLE; //将对应的表项状态改为stable /* 更新这个表项的网络接口 */ arp_table[i].netif = netif; /* 更新缓存标志的MAC地址 */ ETHADDR32_COPY(&arp_table[i].ethaddr, ethaddr); /* reset time stamp */ arp_table[i].ctime = 0; /* 该表项中有未发生出去的数据包,则将这些数据包发送出去! */ while (arp_table[i].q != NULL) { struct pbuf *p; struct etharp_q_entry *q = arp_table[i].q; //指向缓冲队列头部。此结构前文有过介绍。 arp_table[i].q = q->next; //缓冲队列指针指向下一个结点 p = q->p; //取得缓冲队列第一个数据包pbuf memp_free(MEMP_ARP_QUEUE, q); //释放etharp_q_entry内存池空间 etharp_send_ip(netif, p, (struct eth_addr*)(netif->hwaddr), ethaddr); //发送数据包 pbuf_free(p); } return ERR_OK;}
函数的执行流程如下:
首先调用entry_find_entry()找到一个缓冲表项,然后设置表项的相应成员(state,netif,ethaddr,cttime),若这个表项中有未发送的数据,那么就发送这个数据包。根据返回的arp表项的不同状态会有以下三种情况:
如果是empty状态的表项,那么就填充表项的字段,并设置未stable状态。
如果是pending 状态的表项,那么就说明已经有与相同IP地址的MAC地址返回,则改变未Stable状态。
如果是stable 状态的表项,则只需要将ctime复位即可。
-
59.类的自动转换和强制类型转换
程序清单11 16stonewt h pragmaonce stone h--Stonewt类声明 ifndefSTONEWT_H_ defineSTONEWT_H_class
来源: LWIP学习记录---ARP协议(2)ARP数据包发送过程
go A*寻路记录
59.类的自动转换和强制类型转换
不是“空中楼阁”:努比亚Pad 3D搭载全球最大Leia 3D内容生态
【报资讯】男子车停路边去吃烧烤 回来瞬间崩溃:路边已装上护栏
【独家焦点】作文游西湖300字(精选40篇)
千里托运奔驰GLC被淋满牛粪 女子崩溃:花1900元洗了5遍
【世界速看料】情侣打车3小时后跑单拉黑司机 司机:246元车费没了
世界资讯:微软承认向无法升级的设备推荐Win11:已进行修复
环球即时:压水堆
当前滚动:这些“领导”短信收到没?专门针对iPhone用户诈骗:全国多地预警
环球精选!王一博、梁朝伟《无名》北美院线扩映!豆瓣降至6.7分
当前简讯:大爷怒斥夜市挂日本元素油纸伞:主办方回应令人不解
环球头条:导演新海诚:中国动画电影迟早会超过日本
天天最资讯丨pat乙级链表问题
LWIP学习记录------ARP协议(1)
天天热文:开办以来首位!跨性别演员柏林电影节获奖
微速讯:长城放出王炸?长城水平对置八缸发动机摩托曝光 真猛兽
环球热头条丨可以两天一充的骁龙8 Gen2手机:出现了
每日热讯!马里肯涅巴地区发生武装抢劫 中使馆提醒关注当地安全情况
威马汽车再发内部信:部分员工复工 其余人员无薪休假
【全球热闻】视觉四边等宽!魅族20系列边框仅1.57mm:比iPhone 14 Pro都窄
全球热点!Go编程实战:博客备份
Markdown简明教程
《使命召唤》前景动荡
世界新资讯:上海一高校推出高启强同款猪脚面:师生直呼“舌尖上的《狂飙》”
乌苏啤酒大促:立减64元 折合3元/瓶到手
信息:女子考研期间生娃初试395分 回应外界好奇:多亏家人替自己分担很多
每日焦点!高德、百度地图红绿灯读秒很神奇 接入交管平台?真相并非如此
【天天新要闻】《我们的日子》里,不要忽视这些法律问题
天天资讯:俄州“毒火车”引发环境灾难后 美国又一货运列车脱轨
中兴通
全球热讯:读Java性能权威指南(第2版)笔记02_ Java SE API技巧上
世界动态:你昨晚关注的那个福利姬 可能是假的
世界即时看!国产新能源疯狂内卷!哈弗H6 PHEV官降1.5万 配置全系顶配
【世界报资讯】iPhone 15 Pro Max渲染图出炉:对比14 Pro Max边框更窄、机身更厚
对接水仙后台(支持AndLua+、FA、FA2、AIDE lua、Simple Lua等)
【全球报资讯】Golang基于Mysql分布式锁实现集群主备
世界观热点:薪资4K-5K!公司招聘财务要求做饭被吐槽像保姆
天天百事通!男子长期高血糖导致视网膜病变:不可逆
热头条丨不愧是万元机皇!酷安网友给三星Galaxy S23 Ultra打最高分
当前聚焦:《蚁人3》上映9天中国内地票房破2亿 网友:回到小众也挺好
世界微资讯!如何给公众号投稿赚钱_怎样给公众号投稿赚钱
双亲委派机制
天天微动态丨中国教师队伍建设研究/京师教师教育论丛
当前视讯!即将让核污水倒入大海!日本港口大量有毒海胆聚集 或出现爆发式增长
三星降低QD-OLED面板成本!让电视更具竞争力
世界关注:努比亚Z50新版下周首销:骁龙8 Gen2旗舰焊门员 性价比无敌
最新:python实现客户端和服务端的UDP相互通信
【报资讯】hbuilderx打正式包所需的私钥证书的创建方法
全球新动态:2.【go-kit教程】go-kit启动http服务
室内单目深度估计-4
最新:kaggle中训练得到的output太大该怎么下载?
世界热消息:2消息,中超新贵签约32岁国脚,5中超外援上诉国际足联
环球新动态:超市宣称1元纸币将退出历史引热议 网友直呼太突然:官方回应不属实
视点!女子患异食癖3年吃上百块粉饼:体检身体无异常
天天热点!刷题疑问
环球速读:史上最好的真全面屏手机!努比亚Z50 Ultra上架接受预约
天天精选!禁止自带食材 关停300家店 海底捞从巨亏41亿到盈利13亿
天天讯息:day04-原生的API&注解方式
【环球新要闻】Git使用
美食博主三亚买3888元海鲜被好心人提醒多花1700:当事人心累
热消息:秋裤先别着急脱!“春捂”到底该“捂”哪儿?
前沿资讯!2023年安卓之光!小米13 Ultra手机壳曝光:背部造型抢眼
餐馆接到网吧10个外卖订单 结果被刷9个差评 店主:下次亲自送餐
天天微速讯:门店2299元 GXG男士羊毛大衣0.8折清仓大促:实付199元!
世界热资讯!乐堡苏打气泡酒12罐到手19.9元:低糖0脂无负担
威马员工在线讨薪:被恶心到了、恶心的事还有更多
广州塞车登“热搜”?“甜蜜的烦恼”重回一线城市,中国经济活力加快恢复
【Tire树】高效统计字符串
80、90后泪目 国产暗黑《赵云传重制版》试玩
1岁男童误食降糖药成植物人:愿康复顺利
环球速看:中央人民广播电台民族节目中心
Ubuntu安装Zabbix6.0
秒睡令人羡慕?医生提醒:可能是种睡眠障碍
《流浪地球2》科幻成真?武汉国博用特效“加建”太空电梯
今头条!【element UI】在 el-select 与 el-tree 结合组件
环球热文:python教程:模块的搜索路径
Python中模块的四种方式
《原子之心》种族主义漫画引争议:涉嫌歧视黑人!官方道歉
世界速读:我国载人航天将对国际开放 多国航天员希望参与中国空间站
每日热讯!驱动拖后腿 Intel显卡被低估:2000元档A770理论可刚RTX 3070
受贿、泄露内幕信息!湖北原副省长曹广晶被公诉
天天快看点丨下周发售!《卧龙:苍天陨落》新预告发布:PC/主机通吃
最新消息:EQ电动车中国表现欠佳 奔驰CEO:打价格战不是好事
环球热点!101岁老人每天赶2场麻将 医生:身体状态70岁
What's past is prologue
全球新资讯:车商不收特斯拉 新车一个月亏7万:新能源二手车都不受待见 厂商频降价
H5N1禽流感致死事件引世卫关注:事发柬埔寨、9年来首次
环球热点评!下单就送鲜蔬汤 海福盛冻干粥5杯大促:券后仅20.9元!
“真香定律”稳定发挥 迈凯伦全新SUV效果图曝光:预计售价282万
世界今日讯!关于修订《中国高尔夫球协会赛事活动管理办法》的通知
环球微头条丨中越边境民族文化艺术考察研究
每日热点:女子在门缝发现针头 是大妈拿注射器推大量不明液体:整栋楼遭殃 网友气愤
地球中心到底有什么?科学家发现竟是一个超大铁球 跟书里讲得不一样
NET6接口项目基础框架项目
Java 8 Lambda 方法引用 简记
全球看热讯:(数据库系统概论|王珊)第七章数据库设计-第三节:概念结构设计
Blender插件:水滴生成器(Droplet Generator)
环球速递!《分布式技术原理与算法解析》学习笔记Day22