js udp通信_nodejs源码分析第十九章 -- udp模块

udp不是面向连接的协议,所以使用上会比tcp简单,他和tcp一样,使用四元组来标记通信的双方(单播的情况下)。我们看看udp作为服务器和客户端的时候的流程。

1 在c语言中使用udp

1.1 服务器流程(伪代码)

// 申请一个socket
int fd = socket(...);
// 绑定一个众所周知的地址,像tcp一样
bind(fd, ip, port);
// 直接阻塞等待消息的到来,因为udp不是面向连接的,所以不需要listen
recvmsg();

1.2 客户端流程

客户端的流程有多种方式,原因在于源ip和端口可以有多种设置方式,不像服务器一样,服务器的ip和端口是需要对外公布的,否则客户端就无法找到目的地进行通信。这就意味着服务器的ip端口是需要用户显式指定的,而客户端则不然,客户端的ip端口是随意选择的,用户可以自己指定,也可以由操作系统决定,下面我们看看各种使用方式。

1.2.1 显式指定ip端口

// 申请一个socket

1.2.2 由操作系统决定源ip和端口

// 申请一个socket

我们看到这里直接就给服务器发送数据,如果用户不指定ip和端口,则操作系统会提供默认的源ip和端口,不过端口是在第一个调用sendto的时候就设置了,并且不能修改,但是如果是多宿主主机,每次调用sendto的时候,操作系统会动态选择源ip。另外还有另外一种使用方式。

// 申请一个socket

我们可以先调用connect绑定服务器ip和端口到fd,然后直接调用write发送数据。 虽然使用方式很多,但是归根到底还是对四元组设置的管理。bind是绑定源ip端口到fd,connect是绑定服务器ip端口到fd。我们可以主动调用他们来对fd进行设置,也可以让操作系统随机选择。

1.3 发送数据

我们刚才看到使用udp之前都需要调用socket函数申请一个socket,虽然调用socket函数返回的是一个fd,但是在操作系统中,的确是新建了一个socket对象,fd只是一个索引,操作这个fd的时候,操作系统会根据这个fd找到对应的socket。socket是一个非常复杂的结构体,我们可以理解为一个对象。这个对象中有两个属性,一个是读缓冲区大小,一个是写缓冲区大小。当我们发送数据的时候,虽然理论上可以发送任意大小的数据,但是因为受限于发送缓冲区的大小,如果需要发送的数据比当前缓冲区大小大则会导致一些问题,我们分情况分析一下。 1 发送的数据大小比当前缓冲区大,如果设置了非阻塞模式,则返回EAGAIN,如果是阻塞模式,则会引起进程的阻塞。 2 如果发送的数据大小比缓冲区的最大值还大,则会导致一直阻塞或者返回EAGAIN。我们可能会想到修改缓冲区最大值的大小,但是这个大小也是有限制的。 讲完一些边界情况,我们再来看看正常的流程,我们看看发送一个数据包的流程 1 首先在socket的写缓冲区申请一块内存用于数据发送。 2 调用ip层发送接口,如果数据包大小超过了ip层的限制,则需要分包。因为udp不是可靠的,所以不需要缓存这个数据包。 这就是udp发送数据的流程。

1.4 接收数据

当收到一个udp数据包的时候,操作系统首先会把这个数据包缓存到socket的缓冲区,如果收到的数据包比当前缓冲区大小大,则丢弃数据包(关于大小的限制可以参考1.3章节),否则把数据包挂载到接收队列,等用户来读取的时候,就逐个摘下接收队列的节点。

2 udp模块在nodejs中的实现

2.1 udp服务器

我们从一个使用例子开始看看udp模块的实现。

const 

我们看到创建一个udp服务器很简单,首先申请一个socket对象,在nodejs中和操作系统中一样,socket是对网络通信的一个抽象,我们可以把他理解成对传输层的抽象,他可以代表tcp也可以代表udp。我们看一下createSocket做了什么。

function 

我们看到一个socket对象是对handle的一个封装。我们看看handle是什么。

function 

handle又是对UDP模块的封装,UDP是c++模块,我们看看该c++模块的定义。

// 定义一个v8函数模块

在c++层通用逻辑中我们讲过相关的知识,这里就不详细讲述了,当我们在js层new UDP的时候,会新建一个c++对象。

UDPWrap

执行了uv_udp_init初始化udp对应的handle。我们看一下libuv的定义。

int 

到这里,就是我们在js层执行dgram.createSocket('udp4')的时候,在nodejs中主要的执行过程。回到最开始的例子,我们看一下执行bind的时候的逻辑。

Socket

bind函数主要的逻辑是handle.bind和startListening。我们一个个看。我们看一下c++层的bind。

void 

也没有太多逻辑,处理参数然后执行uv_udp_bind,uv_udp_bind就不具体展开了,和tcp类似,设置一些标记和属性,然后执行操作系统bind的函数把本端的ip和端口保存到socket中。我们继续看startListening。

function 

重点是recvStart函数,我们到c++的实现。

void 

OnAlloc, OnRecv分别是分配内存接收数据的函数和数据到来时执行的回调。继续看libuv

int 

uvudp_recv_start主要是注册io观察者到loop,等待事件到来的时候,在poll io阶段处理。前面我们讲过,回调函数是uvudp_io。我们看一下事件触发的时候,该函数怎么处理的。

static 

我们这里先分析可读事件的逻辑。我们看uv__udp_recvmsg。

static 

最终通过操作系统调用recvmsg读取数据,操作系统收到一个udp数据包的时候,会挂载到socket的接收队列,如果满了则会丢弃,当用户调用recvmsg函数的时候,操作系统就把接收队列中节点逐个返回给用户。读取完后,libuv会回调c++层,然后c++层回调到js层,最后触发message事件,这就是对应开始那段代码的message事件。

2.2 客户端

udp客户端的流程是
1 调用bind绑定客户端的地址信息
2 调用connect绑定服务器的地址信息
3 调用sendmsg和recvmsg进行数据通信
我们看一下nodejs里的流程

const 

我们看到nodejs首先调用connect绑定服务器的地址,然后调用send发送信息,最后调用close。我们一个个分析。首先看connect。

Socket

这里分为两种情况,一种是在connect之前已经调用了bind,第二种是没有调用bind,如果没有调用bind,则在connect之前先要调用bind。我们只分析没有调用bind的情况,因为这是最长的链路。我们看一下bind的逻辑。

// port = {posrt: 0, exclusive : true}, address_ = null

因为bind函数中的lookup不是同步执行传入的callback,所以这时候会先返回到connect函数。从而connect函数执行以下代码。

if 

connect函数先把回调加入队列。

function 

enqueue把回调加入队列,并且监听了listening事件,该事件在bind成功后触发。这时候connect函数就执行完了,等待bind成功后(nexttick)会执行 startListening(this)。

function 

我们看到这里(bind成功后)触发了listening事件,从而执行我们刚才入队的回调onListenSuccess。

function 

回调就是把队列中的回调执行一遍,connect函数设置的回调是_connect。

function 

这里的address是服务器地址,_connect函数主要逻辑是
1 监听connect事件
2 对服务器地址进行dns解析(只能是本地的配的域名)。解析成功后执行afterDns,最后执行doConnect,并传入解析出来的ip。我们看看doConnect

function 

connect函数通过c++层,然后调用libuv,到操作系统的connect。作用是把服务器地址保存到socket中。connect的流程就走完了。接下来我们就可以调用send和recv发送和接收数据。

2.3 发送数据

发送数据接口是sendto,他是对send的封装。

Socket.prototype.send = function(buffer,offset,length,port,address,callback) {let list;const state = this[kStateSymbol];const connected = state.connectState === CONNECT_STATE_CONNECTED;// 没有调用connect绑定过服务端地址,则需要传服务端地址信息if (!connected) {if (address || (port && typeof port !== 'function')) {buffer = sliceBuffer(buffer, offset, length);} else {callback = port;port = offset;address = length;}} else {if (typeof length === 'number') {buffer = sliceBuffer(buffer, offset, length);if (typeof port === 'function') {callback = port;port = null;}} else {callback = offset;}// 已经绑定了服务端地址,则不能再传了if (port || address)throw new ERR_SOCKET_DGRAM_IS_CONNECTED();}// 如果没有绑定服务器端口,则这里需要传,并且校验if (!connected)port = validatePort(port);// 忽略一些参数处理逻辑// 没有绑定客户端地址信息,则需要先绑定,值由操作系统决定if (state.bindState === BIND_STATE_UNBOUND)this.bind({ port: 0, exclusive: true }, null);// bind还没有完成,则先入队,等待bind完成再执行if (state.bindState !== BIND_STATE_BOUND) {enqueue(this, this.send.bind(this, list, port, address, callback));return;}// 已经绑定了,设置服务端地址后发送数据const afterDns = (ex, ip) => {defaultTriggerAsyncIdScope(this[async_id_symbol],doSend,ex, this, ip, list, address, port, callback);};// 传了地址则可能需要dns解析if (!connected) {state.handle.lookup(address, afterDns);} else {afterDns(null, null);}
}

我们继续看doSend函数。

function doSend(ex, self, ip, list, address, port, callback) {const state = self[kStateSymbol];// dns解析出错if (ex) {if (typeof callback === 'function') {process.nextTick(callback, ex);return;}process.nextTick(() => self.emit('error', ex));return;}// 定义一个请求对象const req = new SendWrap();req.list = list;  // Keep reference alive.req.address = address;req.port = port;// 设置nodejs和用户的回调,oncomplete由c++层调用,callback由oncomplete调用if (callback) {req.callback = callback;req.oncomplete = afterSend;}let err;// 根据是否需要设置服务端地址,调c++层函数if (port)err = state.handle.send(req, list, list.length, port, ip, !!callback);elseerr = state.handle.send(req, list, list.length, !!callback);// err大于等于1说明同步发送成功了,直接执行回调,否则等待异步回调if (err >= 1) {if (callback)process.nextTick(callback, null, err - 1);return;}// 发送失败if (err && callback) {// Don't emit as error, dgram_legacy.js compatibilityconst ex = exceptionWithHostPort(err, 'send', address, port);process.nextTick(callback, ex);}
}

我们穿过c++层,直接看libuv的代码。

int uv__udp_send(uv_udp_send_t* req,uv_udp_t* handle,const uv_buf_t bufs[],unsigned int nbufs,const struct sockaddr* addr,unsigned int addrlen,uv_udp_send_cb send_cb) {int err;int empty_queue;assert(nbufs > 0);// 还没有绑定服务端地址,则绑定if (addr) {err = uv__udp_maybe_deferred_bind(handle, addr->sa_family, 0);if (err)return err;}// 当前写队列是否为空empty_queue = (handle->send_queue_count == 0);// 初始化一个写请求uv__req_init(handle->loop, req, UV_UDP_SEND);if (addr == NULL)req->addr.ss_family = AF_UNSPEC;elsememcpy(&req->addr, addr, addrlen);// 保存上下文req->send_cb = send_cb;req->handle = handle;req->nbufs = nbufs;// 初始化数据,预分配的内存不够,则分配新的堆内存req->bufs = req->bufsml;if (nbufs > ARRAY_SIZE(req->bufsml))req->bufs = uv__malloc(nbufs * sizeof(bufs[0]));// 复制过去堆中memcpy(req->bufs, bufs, nbufs * sizeof(bufs[0]));// 更新写队列数据handle->send_queue_size += uv__count_bufs(req->bufs, req->nbufs);handle->send_queue_count++;// 插入写队列,等待可写事件的发生QUEUE_INSERT_TAIL(&handle->write_queue, &req->queue);uv__handle_start(handle);// 当前写队列为空,则直接开始写,否则设置等待可写队列if (empty_queue && !(handle->flags & UV_HANDLE_UDP_PROCESSING)) {// 发送数据uv__udp_sendmsg(handle);// 写队列是否非空,则设置等待可写事件,可写的时候接着写if (!QUEUE_EMPTY(&handle->write_queue))uv__io_start(handle->loop, &handle->io_watcher, POLLOUT);} else {uv__io_start(handle->loop, &handle->io_watcher, POLLOUT);}return 0;
}

该函数把写请求插入写队列中等待可写事件的到来。然后注册等待可写事件。当可写事件触发的时候,执行的函数是uv__udp_io。

static void uv__udp_io(uv_loop_t* loop, uv__io_t* w, unsigned int revents) {uv_udp_t* handle;if (revents & POLLOUT) {uv__udp_sendmsg(handle);uv__udp_run_completed(handle);}
}

我们先看uv__udp_sendmsg

static void uv__udp_sendmsg(uv_udp_t* handle) {uv_udp_send_t* req;QUEUE* q;struct msghdr h;ssize_t size;// 逐个节点发送while (!QUEUE_EMPTY(&handle->write_queue)) {q = QUEUE_HEAD(&handle->write_queue);req = QUEUE_DATA(q, uv_udp_send_t, queue);memset(&h, 0, sizeof h);// 忽略参数处理h.msg_iov = (struct iovec*) req->bufs;h.msg_iovlen = req->nbufs;do {size = sendmsg(handle->io_watcher.fd, &h, 0);} while (size == -1 && errno == EINTR);if (size == -1) {// 繁忙则先不发了,等到可写事件if (errno == EAGAIN || errno == EWOULDBLOCK || errno == ENOBUFS)break;}// 记录发送结果req->status = (size == -1 ? UV__ERR(errno) : size);// 发送“完”移出写队列QUEUE_REMOVE(&req->queue);// 加入写完成队列QUEUE_INSERT_TAIL(&handle->write_completed_queue, &req->queue);// 有节点数据写完了,把io观察者插入pending队列,pending阶段执行回调uv__udp_iouv__io_feed(handle->loop, &handle->io_watcher);}
}

该函数遍历写队列,然后逐个发送节点中的数据,并记录发送结果, 1 如果写繁忙则结束写逻辑,等待下一次写事件触发。
2 如果写成功则把节点插入写完成队列中,并且把io观察者插入pending队列,等待pending阶段执行回调uvudp_io。 我们再次回到uvudp_io中

if (revents & POLLOUT) {uv__udp_sendmsg(handle);uv__udp_run_completed(handle);
}

当写事件触发时,执行完数据发送的逻辑后还会处理写完成队列。我们看uv__udp_run_completed。

static void uv__udp_run_completed(uv_udp_t* handle) {uv_udp_send_t* req;QUEUE* q;handle->flags |= UV_HANDLE_UDP_PROCESSING;// 逐个节点处理while (!QUEUE_EMPTY(&handle->write_completed_queue)) {q = QUEUE_HEAD(&handle->write_completed_queue);QUEUE_REMOVE(q);req = QUEUE_DATA(q, uv_udp_send_t, queue);uv__req_unregister(handle->loop, req);// 更新待写数据大小handle->send_queue_size -= uv__count_bufs(req->bufs, req->nbufs);handle->send_queue_count--;// 如果重新申请了堆内存,则需要释放if (req->bufs != req->bufsml)uv__free(req->bufs);req->bufs = NULL;if (req->send_cb == NULL)continue;// 执行回调if (req->status >= 0)req->send_cb(req, 0);elsereq->send_cb(req, req->status);}// 写队列为空,则注销等待可写事件if (QUEUE_EMPTY(&handle->write_queue)) {uv__io_stop(handle->loop, &handle->io_watcher, POLLOUT);if (!uv__io_active(&handle->io_watcher, POLLIN))uv__handle_stop(handle);}handle->flags &= ~UV_HANDLE_UDP_PROCESSING;
}

这就是发送的逻辑,发送完后libuv会调用c++回调,最后回调js层回调。具体到操作系统也是类似的实现,操作系统首先判断数据的大小是否小于写缓冲区,是的话申请一块内存,然后构造udp协议数据包,再逐层往下调,最后发送出来,但是如果数据超过了底层的报文大小限制,则会被分片。

2.4 多播

udp支持多播,tcp则不支持,因为tcp是基于连接和可靠的,多播则会带来过多的连接和流量。多播分为局域网多播和广域网多播,我们知道在局域网内发生一个数据,是会以广播的形式发送到各个主机的,主机根据目的地址判断是否需要处理该数据包。如果udp是单播的模式,则只会有一个主机会处理该数据包。如果udp是多播的模式,则有多个主机处理该数据包。多播的时候,存在一个多播组的概念,只有加入这个组的主机才能处理该组的数据包。假设有以下局域网

a50e1846308b9de53b446cb715f3bc0d.png

当主机1给多播组1发送数据的时候,主机2,4可以收到,主机3则无法收到。我们再来看看广域网的多播。广域网的多播需要路由器的支持,多个路由器之间会使用多播路由协议交换多播组的信息。假设有以下广域网。

3e1851cc5f6d9370fe06f0fa2201578e.png

当主机1给多播组1发送数据的时候,路由器1会给路由器2发送一份数据(通过多播路由协议交换了信息,路由1知道路由器2的主机4在多播组1中),但是路由器2不会给路由器3发送数据,因为他知道路由器3对应的网络中没有主机在多播组1。以上是多播的一些概念。nodejs中关于多播的实现,基本是对操作系统api的封装,所以就不打算讲解,我们直接看操作系统中对于多播的实现。

2.4.1 加入一个多播组

可以通过以下代码加入一个多播组。

setsockopt

mreq的结构体定义如下

struct 

我们看一下setsockopt的实现(只列出相关部分代码)

case 

拿到加入的多播组ip和device后,调用ip_mc_join_group,在socket结构体中,有一个字段维护了该socket加入的多播组信息。

36bd42cce2e9c037321bf374841c65b1.png
int 

ip_mc_join_group函数的主要逻辑是把socket想加入的多播组信息记录到socket的ip_mc_list字段中(如果还没有加入过该多播组的话)。接着调ip_mc_inc_group往下走。device层维护了主机中使用了该device的多播组信息。

92b8acade8032a8fb446fd817efb7038.png
static 

ip_mc_inc_group函数的主要逻辑是判断socket想要加入的多播组是不是已经存在于当前device中,如果不是则新增一个节点。继续调用igmp_group_added

static 

我们看看igmp_send_report和ip_mc_filter_add的具体逻辑。

static 

igmp_send_report其实就是构造一个igmp协议数据包,然后发送出去,igmp的协议格式如下

struct 

接着我们看ip_mc_filter_add

void 

我们知道ip地址是32位,mac地址是48位,但是IANA规定,ipv4组播MAC地址的高24位是0x01005E,第25位是0,低23位是ipv4组播地址的低23位。而多播的ip地址高四位固定是1110。另外低23位被映射到mac多播地址的23位,所以多播ip地址中,有5位是可以随机组合的。这就意味着,每32个多播ip地址,映射到一个mac地址。这会带来一些问题,假设主机x加入了多播组a,主机y加入了多播组b,而a和b对应的mac多播地址是一样的。当主机z给多播组a发送一个数据包的时候,这时候主机x和y的网卡都会处理该数据包,并上报到上层,但是多播组a对应的mac多播地址和多播组b是一样的。我们拿到一个多播组ip的时候,可以计算出他的多播mac地址,但是反过来就不行,因为一个多播mac地址对应了32个多播ip地址。那主机x和y怎么判断是不是发给自己的数据包?因为device维护了一个本device上的多播ip列表,操作系统根据收到的数据包中的ip目的地址和device的多播ip列表对比。如果在列表中,则说明是发给自己的。最后我们看看dev_mc_add。device中维护了当前的mac多播地址列表,他会把这个列表信息同步到网卡中,使得网卡可以处理该列表中多播mac地址的数据包。

ee8761f18eb3c34b60f18e2badd0d084.png
void 

网卡的工作模式有几种,分别是正常模式(只接收发给自己的数据包)、混杂模式(接收所有数据包)、多播模式(接收一般数据包和多播数据包)。网卡默认是只处理发给自己的数据包,所以当我们加入一个多播组的时候,我们需要告诉网卡,当收到该多播组的数据包时,需要处理,而不是忽略。dev_mc_upload函数就是通知网卡。

void 

最后我们看一下set_multicast_list

static 

set_multicast_list就是设置网卡工作模式的函数。至此,我们就成功加入了一个多播组。离开一个多播组也是类似的过程。

2.4.2 开启多播

udp的多播能力是需要用户主动开启的,原因是防止用户发送udp数据包的时候,误传了一个多播地址,但其实用户是想发送一个单播的数据包。我们可以通过setBroadcast开启多播能力。我们看libuv的代码。

int uv_udp_set_broadcast(uv_udp_t* handle, int on) {if (setsockopt(handle->io_watcher.fd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on))) {return UV__ERR(errno);}return 0;
}

再看看操作系统的实现。

int sock_setsockopt(struct sock *sk, int level, int optname,char *optval, int optlen){...case SO_BROADCAST:sk->broadcast=val?1:0;
}

我们看到实现很简单,就是设置一个标记位。当我们发送消息的时候,如果目的地址是多播地址,但是又没有设置这个标记,则会报错。

if(!sk->broadcast && ip_chk_addr(sin.sin_addr.s_addr)==IS_BROADCAST)return -EACCES;

上面代码来自调用udp的发送函数(例如sendto)时,进行的校验,如果发送的目的ip是多播地址,但是没有设置多播标记,则报错。

2.4.3 其他功能

udp模块还提供了其他一些功能
1 获取本端地址address
如果用户没有显示调用bind绑定自己设置的ip和端口,那么操作系统就会随机选择。通过address函数就可以获取操作系统选择的源ip和端口。
2 获取对端的地址
通过remoteAddress函数可以获取对端地址。该地址由用户调用connect或sendto函数时设置。
3 获取/设置缓冲区大小get/setRecvBufferSize,get/setSendBufferSize
4 setMulticastLoopback
发送多播数据包的时候,如果多播ip在出口设备的多播列表中,则给回环设备也发一份。
5 setMulticastInterface
设置多播数据的出口设备
6 加入或退出多播组addMembership/dropMembership
7 addSourceSpecificMembership/dropSourceSpecificMembership
这两个函数是设置本端只接收特性源(主机)的多播数据包。
8 setTTL
单播ttl(单播的时候,ip协议头中的ttl字段)。
9 setMulticastTTL
多播ttl(多播的时候,ip协议的ttl字段)。
10 ref/unref
这两个函数设置如果nodejs主进程中只有udp对应的handle时,是否允许nodejs退出。nodejs事件循环的退出的条件之一是是否还有ref状态的handle。 这些都是对操作系统api的封装,就不一一分析。

3 具体例子

局域网中有两个局域网ip,分别是192.168.8.164和192.168.8.226

单播

服务器端

const dgram = require('dgram');
const udp = dgram.createSocket('udp4');
udp.bind(1234);
udp.on('message', (msg, remoteInfo) => {console.log(`receive msg: ${msg} from ${remoteInfo.address}:${remoteInfo.port}`);
});

客户端

const dgram = require('dgram');
const udp = dgram.createSocket('udp4');
udp.bind(1234);
udp.send('test', 1234, '192.168.8.226');

我们会看到服务端会显示receive msg test from 192.168.8.164:1234。

多播

服务器

const dgram = require('dgram');
const udp = dgram.createSocket('udp4');udp.bind(1234, () => {// 局域网多播地址(224.0.0.0~224.0.0.255,该范围的多播数据包,路由器不会转发)udp.addMembership('224.0.0.114');
});udp.on('message', (msg, rinfo) => {console.log(`receive msg: ${msg} from ${rinfo.address}:${rinfo.port}`);
});

服务器绑定1234端口后,加入多播组224.0.0.114,然后等待多播数据的到来。

客户端

const dgram = require('dgram');
const udp = dgram.createSocket('udp4');
udp.bind(1234, () => {udp.addMembership('224.0.0.114');
});
udp.send('test', 1234, '224.0.0.114', (err) => {});

客户端绑定1234端口后,也加入了多播组224.0.0.114,然后发送数据,但是发现服务端没有收到数据,客户端打印了receive msg test from 169.254.167.41:1234。这怎么多了一个ip出来?原来我主机有两个局域网地址。当我们加入多播组的时候,不仅可以设置加入哪个多播组,还能设置出口的设备和ip。当我们调用udp.addMembership('224.0.0.114')的时候,我们只是设置了我们加入的多播组,没有设置出口。这时候操作系统会为我们选择一个。根据输出,我们发现操作系统选择的是169.254.167.41(子网掩码是255.255.0.0)。因为这个ip和192开头的那个不是同一子网,但是我们加入的是局域网的多播ip,所有服务端无法收到客户端发出的数据包。下面是nodejs文档的解释。

Tells the kernel to join a multicast group at the given multicastAddress and multicastInterface using the IP_ADD_MEMBERSHIP socket option. If the multicastInterface argument is not specified, the operating system will choose one interface and will add membership to it. To add membership to every available interface, call addMembership multiple times, once per interface.

我们看一下操作系统的相关逻辑。

if(MULTICAST(daddr) && *dev==NULL && skb->sk && *skb->sk->ip_mc_name)*dev=dev_get(skb->sk->ip_mc_name);

上面的代码来自操作系统发送ip数据包时的逻辑,如果目的ip似乎多播地址并且ip_mc_name非空(即我们通过addMembership第二个参数设置的值),则出口设备就是我们设置的值。否则操作系统自己选。所以我们需要显示指定这个出口,把代码改成udp.addMembership('224.0.0.114', '192.168.8.164');重新执行发现客户端和服务器都显示了receive msg test from 192.168.8.164:1234。为什么客户端自己也会收到呢?原来操作系统发送多播数据的时候,也会给自己发送一份。我们看看相关逻辑

// 目的地是多播地址,并且不是回环设备 
if (MULTICAST(iph->daddr) && !(dev->flags&IFF_LOOPBACK))
{// 是否需要给自己一份,默认为trueif(sk==NULL || sk->ip_mc_loop){   // 给所有多播组的所有主机的数据包,则直接给自己一份if(iph->daddr==IGMP_ALL_HOSTS)ip_loopback(dev,skb);else{   // 判断目的ip是否在当前设备的多播ip列表中,是的回传一份struct ip_mc_list *imc=dev->ip_mc_list;while(imc!=NULL){if(imc->multiaddr==iph->daddr){ip_loopback(dev,skb);break;}imc=imc->next;}}}
}

以上代码来自ip层发送数据包时的逻辑。如果我们设置了sk->ip_mc_loop字段为1,并且数据包的目的ip在出口设备的多播列表中,则需要给自己回传一份。那么我们如何关闭这个特性呢?调用udp.setMulticastLoopback(false)就可以了。
更多参考
1 通过源码理解IGMP v1的实现(基于linux1.2.13)
2 UDP协议源码解析之接收
3 UDP协议源码解析之发送

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/410513.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SVN 清理失败解决方案

SVN有时因各种不明原因导致清理失败,可以采取如下解决办法进行处理: 方法一: 删除根目录下隐藏文件夹“.svn” 然后在根目录文件夹 外面的空白处 检出。比如你项目文件夹名为“D:/source” 则svn检出时,在“source”外面的D盘(D:/) 空白处上右…

将SQL-SERVER逆向工程导入Power-Design中并给表的字段添加注释

PD是一款不错的数据库设计工具,我们在项目开发的时候直接采用正向工程,设计好数据库后逆向将数据库导入PD中,并在PD中添加数据库字段的注释,便于新人的理解和学习,PD支持Oracle、SqlServer等数据库,是很强大…

腾讯微博Android客户端开发——自动获取验证码

上一节给大家讲解通过调用android系统自带的浏览器进行授权认证的,使用该种方式能很容易的完成认证,但是该种方式有个弊端,也就是如果使用第三方的浏览器如UC、天天等,输入完QQ账号信息点击“授权”后并不能再次跳转到MainActivit…

put请求方式参数如何传_TP5请求(request)变量

可以通过Request对象完成全局输入变量的检测、获取和安全过滤,支持包括$_GET、$_POST、$_REQUEST、$_SERVER、$_SESSION、$_COOKIE、$_ENV等系统变量,以及文件上传信息。检测变量是否设置可以使用has方法来检测一个变量参数是否设置,如下&…

python numpy的var std cov研究

var:表示方差, 即各项-均值的平方求和后再除以N , std:表示标准差,是var的平方根。 cov:协方差 ,与var类似,但是除以(N-1) import numpy as np# 构建测试数据,均值为10 sc [9.7, 10…

Vue手动封装实现一个五星评价得效果

我是歌谣 放弃很难 但是坚持一定很酷 微信公众号关注小歌谣 一起学习前后端知识 今天要说得是实现一个vue中实现五星评价得效果 简单来说 就是封装组件把 具体需要我们了解组件间得相互传值 数据绑定等知识 先用脚手架起个项目先 脚手架启动 ​ 安装依赖 包括 npm ins…

LetCode-MSSQL查找重复的电子邮箱

sql的题目如下所示,查询出重复的电子邮箱 解法(1):查询出查询出Email相等 Id不相同的数据具体语句如下所示: select a.Email from Person as a,Person as b where a.Emailb.Email and a.Id!b.Id此时我们可以看到我们的语句中输出了2次结果但是预期结果只输出了1次…

鸿蒙内核是闭源吗_鸿蒙出世,中华有为!

作者:飞翔吧!橙哥转载授权(文末留言,或添加微信:mzy2117)8月9日,超强台风“利奇马”登陆中国。当沿海各地的人们都在琢磨下班如何回家的时候,在广东东莞举行的华为2019年开发者大会上,华为正式发…

oracle 添加字段

alter table 表名 add 新增字段名(类型长度);#添加字段alter table asset_orders add remark varchar2(255);#查看describe asset_orders;转载于:https://www.cnblogs.com/zhaojingyu/p/11236747.html

儿童学文字编程python_手把手教你python数字知识

上篇文章讲述了python的数据类型。 我们先回顾一下:包括:数字,字符串,列表,元组,字典。接下来我们详细的介绍这几种类型。 今天要说的是关于数字的教程。 说到数字,可能你的头脑里首先会反应出&…

LetCode-MSSQL超过5名学生的课

此图关键在于去重后使用having count查询出大于5的值 select class from courses group by class having count(distinct student) > 5;

在laravel5.8中集成swoole组件----初步测试

铺垫前提是先安装swoole组件,我采用从pecl-----php扩展组件网下载swoole扩展包,然后切入到解压缩的扩展包中运行phpize命令, phpize是一种编译命令,可以在安装文件中生成configure文件,从而方便我们编译安装&#xff0…

python改文件名_通过python顺序修改文件名字的方法

通过python顺序修改文件名字的方法 更新时间:2018年07月11日 11:48:55 作者:longma666666 今天小编就为大家分享一篇通过python顺序修改文件名字的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 问题&…

LetCode-MSSQL从不订购的客户

解法(1):思路为先查询出订购的客户再使用not in查询出不包含订购客户的其他人也就是从来不订购的客户 查询出订购的客户语句: select a.Id from Customers as a,Orders as b where b.CustomerIda.Id再使用not in 查询 不再里面的客户 select Name as Customers fr…

python loadtxt_Python 数据科学入门2:Matplotlib

第七章 从文件加载数据很多时候,我们想要绘制文件中的数据。 有许多类型的文件,以及许多方法,你可以使用它们从文件中提取数据来图形化。 在这里,我们将展示几种方法。 首先,我们将使用内置的csv模块加载CSV文件&#…

LetCode-MSSQL销售分析-I

此题是查询出销售额最高的人的ID 首先我们通过语句查询出最高的销售额 select top 1 sum(price) from Sales group by seller_id order by sum(price) desc然后我们通过查询总和的值和 最高销售额相等的ID即可 select seller_id from Sales group by seller_id having sum(p…

在laravel5.8中集成swoole组件----用协程实现的服务端和客户端(一)

注意&#xff0c;这种风格的服务端需要swoole4.4以上&#xff0c;这种风格的服务端需要swoole4.4以上&#xff0c;这种风格的服务端需要swoole4.4以上&#xff0c;重要的事情说三遍&#xff01;&#xff01;&#xff01; 服务端<?php //namespace Swoole; use Swoole\Corou…

Zen Coding 系列教程一:入门

Zen Coding 是一款高效用于开发HTML与CSS的编码插件&#xff0c;可以安装到很多软件中使用Zen Coding 项目&#xff1a;http://code.google.com/p/zen-coding/ DemoDemo (使用 Ctrl , 展开缩写&#xff0c;需要JavaScript支持)中文版演示下载(完全支持)Aptana (跨平台);Coda,…

LetCode-MySql删除重复的电子邮箱

解法(1)&#xff1a;思路为先查询出查询出重复的ID并且取最小值 select min(Id) Id,Email from Person group by Email或者 Select min(Id) as Id,distinct Email from Person然后删除不在ID为此里面的值 delete from Person where Id not in(select Id from ( select min(Id…