Nodejs源码解析之UDP服务器

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

 本文转载自微信公众号「编程杂技 」,作者theanarkh。转载本文请联系编程杂技 公众号。    

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

  1. const dgram = require('dgram'); 
  2. // 创建一个socket对象 
  3. const server = dgram.createSocket('udp4'); 
  4. // 监听udp数据的到来 
  5. server.on('message', (msg, rinfo) => { 
  6.   // 处理数据});// 绑定端口 
  7. server.bind(41234); 

[[341486]]

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

  1. function createSocket(type, listener) { 
  2.   return new Socket(type, listener); 
  3.  
  4. function Socket(type, listener) { 
  5.   EventEmitter.call(this); 
  6.   let lookup; 
  7.   let recvBufferSize; 
  8.   let sendBufferSize; 
  9.  
  10.   let options; 
  11.   if (type !== null && typeof type === 'object') { 
  12.     options = type; 
  13.     type = options.type; 
  14.     lookup = options.lookup; 
  15.     recvBufferSize = options.recvBufferSize; 
  16.     sendBufferSize = options.sendBufferSize; 
  17.   } 
  18.   const handle = newHandle(type, lookup);  
  19.   this.type = type; 
  20.   if (typeof listener === 'function'
  21.     this.on('message', listener); 
  22.  
  23.   this[kStateSymbol] = { 
  24.     handle, 
  25.     receiving: false
  26.     bindState: BIND_STATE_UNBOUND, 
  27.     connectState: CONNECT_STATE_DISCONNECTED, 
  28.     queue: undefined, 
  29.     reuseAddr: options && options.reuseAddr, // Use UV_UDP_REUSEADDR if true
  30.     ipv6Only: options && options.ipv6Only, 
  31.     recvBufferSize, 
  32.     sendBufferSize 
  33.   };} 

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

  1. function newHandle(type, lookup) { 
  2.   // 用于dns解析的函数,比如我们调send的时候,传的是一个域名 
  3.   if (lookup === undefined) { 
  4.     if (dns === undefined) { 
  5.       dns = require('dns'); 
  6.     } 
  7.  
  8.     lookup = dns.lookup; 
  9.   }  
  10.  
  11.   if (type === 'udp4') { 
  12.     const handle = new UDP(); 
  13.     handle.lookup = lookup4.bind(handle, lookup); 
  14.     return handle; 
  15.   } 
  16.   // 忽略ipv6的处理} 

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

  1. // 定义一个v8函数模块 
  2. Local<FunctionTemplate> t = env->NewFunctionTemplate(New); 
  3.   // t新建的对象需要额外拓展的内存 
  4.   t->InstanceTemplate()->SetInternalFieldCount(1); 
  5.   // 导出给js层使用的名字 
  6.   Local<String> udpString = FIXED_ONE_BYTE_STRING(env->isolate(), "UDP"); 
  7.   t->SetClassName(udpString); 
  8.   // 属性的存取属性 
  9.   enum PropertyAttribute attributes = static_cast<PropertyAttribute>(ReadOnly | DontDelete); 
  10.  
  11.   Local<Signature> signature = Signature::New(env->isolate(), t); 
  12.   // 新建一个函数模块 
  13.   Local<FunctionTemplate> get_fd_templ = 
  14.       FunctionTemplate::New(env->isolate(), 
  15.                             UDPWrap::GetFD, 
  16.                             env->as_callback_data(), 
  17.                             signature); 
  18.   // 设置一个访问器,访问fd属性的时候,执行get_fd_templ,从而执行UDPWrap::GetFD 
  19.   t->PrototypeTemplate()->SetAccessorProperty(env->fd_string(), 
  20.                                               get_fd_templ, 
  21.                                               Local<FunctionTemplate>(), 
  22.                                               attributes); 
  23.   // 导出的函数 
  24.   env->SetProtoMethod(t, "open"Open); 
  25.   // 忽略一系列函数 
  26.   // 导出给js层使用 
  27.   target->Set(env->context(), 
  28.               udpString, 
  29.               t->GetFunction(env->context()).ToLocalChecked()).Check(); 

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

  1. UDPWrap::UDPWrap(Environment* env, Local<Object> object) 
  2.     : HandleWrap(env, 
  3.                  object, 
  4.                  reinterpret_cast<uv_handle_t*>(&handle_), 
  5.                  AsyncWrap::PROVIDER_UDPWRAP) { 
  6.   int r = uv_udp_init(env->event_loop(), &handle_);} 

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

  1. int uv_udp_init_ex(uv_loop_t* loop, uv_udp_t* handle, unsigned int flags) { 
  2.   int domain; 
  3.   int err; 
  4.   int fd; 
  5.  
  6.   /* Use the lower 8 bits for the domain */ 
  7.   domain = flags & 0xFF; 
  8.   // 申请一个socket,返回一个fd 
  9.   fd = uv__socket(domain, SOCK_DGRAM, 0); 
  10.   uv__handle_init(loop, (uv_handle_t*)handle, UV_UDP); 
  11.   handle->alloc_cb = NULL
  12.   handle->recv_cb = NULL
  13.   handle->send_queue_size = 0; 
  14.   handle->send_queue_count = 0; 
  15.   // 初始化io观察者(还没有注册到事件循环的poll io阶段),监听的文件描述符是fd,回调是uv__udp_io 
  16.   uv__io_init(&handle->io_watcher, uv__udp_io, fd); 
  17.   // 初始化写队列 
  18.   QUEUE_INIT(&handle->write_queue); 
  19.   QUEUE_INIT(&handle->write_completed_queue); 
  20.   return 0;} 

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

  1. Socket.prototype.bind = function(port_, address_ /* , callback */) { 
  2.   let port = port_; 
  3.   // socket的状态 
  4.   const state = this[kStateSymbol]; 
  5.   // 已经绑定过了则报错 
  6.   if (state.bindState !== BIND_STATE_UNBOUND) 
  7.     throw new ERR_SOCKET_ALREADY_BOUND(); 
  8.   // 否则标记已经绑定了 
  9.   state.bindState = BIND_STATE_BINDING; 
  10.   // 没传地址则默认绑定所有地址 
  11.   if (!address) { 
  12.     if (this.type === 'udp4'
  13.       address = '0.0.0.0'
  14.     else 
  15.       address = '::'
  16.   } 
  17.   // dns解析后在绑定,如果需要的话 
  18.   state.handle.lookup(address, (err, ip) => { 
  19.     if (err) { 
  20.       state.bindState = BIND_STATE_UNBOUND; 
  21.       this.emit('error', err); 
  22.       return
  23.     } 
  24.     const err = state.handle.bind(ip, port || 0, flags); 
  25.     if (err) { 
  26.        const ex = exceptionWithHostPort(err, 'bind', ip, port); 
  27.        state.bindState = BIND_STATE_UNBOUND; 
  28.        this.emit('error', ex); 
  29.        // Todo: close
  30.        return
  31.      } 
  32.  
  33.      startListening(this); 
  34.   return this;} 

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

  1. void UDPWrap::DoBind(const FunctionCallbackInfo<Value>& args, int family) { 
  2.   UDPWrap* wrap; 
  3.   ASSIGN_OR_RETURN_UNWRAP(&wrap, 
  4.                           args.Holder(), 
  5.                           args.GetReturnValue().Set(UV_EBADF)); 
  6.  
  7.   // bind(ip, port, flags) 
  8.   CHECK_EQ(args.Length(), 3); 
  9.   node::Utf8Value address(args.GetIsolate(), args[0]); 
  10.   Local<Context> ctx = args.GetIsolate()->GetCurrentContext(); 
  11.   uint32_t port, flags; 
  12.   if (!args[1]->Uint32Value(ctx).To(&port) || 
  13.       !args[2]->Uint32Value(ctx).To(&flags)) 
  14.     return
  15.   struct sockaddr_storage addr_storage; 
  16.   int err = sockaddr_for_family(family, address.out(), port, &addr_storage); 
  17.   if (err == 0) { 
  18.     err = uv_udp_bind(&wrap->handle_, 
  19.                       reinterpret_cast<const sockaddr*>(&addr_storage), 
  20.                       flags); 
  21.   } 
  22.  
  23.   args.GetReturnValue().Set(err);} 

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

  1. function startListening(socket) { 
  2.   const state = socket[kStateSymbol]; 
  3.   // 有数据时的回调,触发message事件 
  4.   state.handle.onmessage = onMessage; 
  5.   // 重点,开始监听数据 
  6.   state.handle.recvStart(); 
  7.   state.receiving = true
  8.   state.bindState = BIND_STATE_BOUND; 
  9.  
  10.   if (state.recvBufferSize) 
  11.     bufferSize(socket, state.recvBufferSize, RECV_BUFFER); 
  12.  
  13.   if (state.sendBufferSize) 
  14.     bufferSize(socket, state.sendBufferSize, SEND_BUFFER); 
  15.  
  16.   socket.emit('listening');} 

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

  1. void UDPWrap::RecvStart(const FunctionCallbackInfo<Value>& args) { 
  2.   UDPWrap* wrap; 
  3.   ASSIGN_OR_RETURN_UNWRAP(&wrap, 
  4.                           args.Holder(), 
  5.                           args.GetReturnValue().Set(UV_EBADF)); 
  6.   int err = uv_udp_recv_start(&wrap->handle_, OnAlloc, OnRecv); 
  7.   // UV_EALREADY means that the socket is already bound but that's okay 
  8.   if (err == UV_EALREADY) 
  9.     err = 0; 
  10.   args.GetReturnValue().Set(err);} 

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

  1. int uv__udp_recv_start(uv_udp_t* handle, 
  2.                        uv_alloc_cb alloc_cb, 
  3.                        uv_udp_recv_cb recv_cb) { 
  4.   int err; 
  5.  
  6.  
  7.   err = uv__udp_maybe_deferred_bind(handle, AF_INET, 0); 
  8.   if (err) 
  9.     return err; 
  10.   // 保存一些上下文 
  11.   handle->alloc_cb = alloc_cb; 
  12.   handle->recv_cb = recv_cb; 
  13.   // 注册io观察者到loop,如果事件到来,等到poll io阶段处理 
  14.   uv__io_start(handle->loop, &handle->io_watcher, POLLIN); 
  15.   uv__handle_start(handle); 
  16.  
  17.   return 0;} 

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

  1. static void uv__udp_io(uv_loop_t* loop, uv__io_t* w, unsigned int revents) { 
  2.   uv_udp_t* handle; 
  3.  
  4.   handle = container_of(w, uv_udp_t, io_watcher); 
  5.   // 可读事件触发 
  6.   if (revents & POLLIN) 
  7.     uv__udp_recvmsg(handle); 
  8.   // 可写事件触发 
  9.   if (revents & POLLOUT) { 
  10.     uv__udp_sendmsg(handle); 
  11.     uv__udp_run_completed(handle); 
  12.   }} 

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

  1. static void uv__udp_recvmsg(uv_udp_t* handle) { 
  2.   struct sockaddr_storage peer; 
  3.   struct msghdr h; 
  4.   ssize_t nread; 
  5.   uv_buf_t buf; 
  6.   int flags; 
  7.   int count
  8.  
  9.   count = 32; 
  10.  
  11.   do { 
  12.     // 分配内存接收数据,c++层设置的 
  13.     buf = uv_buf_init(NULL, 0); 
  14.     handle->alloc_cb((uv_handle_t*) handle, 64 * 1024, &buf); 
  15.     memset(&h, 0, sizeof(h)); 
  16.     memset(&peer, 0, sizeof(peer)); 
  17.     h.msg_name = &peer; 
  18.     h.msg_namelen = sizeof(peer); 
  19.     h.msg_iov = (void*) &buf; 
  20.     h.msg_iovlen = 1; 
  21.     // 调操作系统的函数读取数据 
  22.     do { 
  23.       nread = recvmsg(handle->io_watcher.fd, &h, 0); 
  24.     } 
  25.     while (nread == -1 && errno == EINTR); 
  26.     // 调用c++层回调 
  27.     handle->recv_cb(handle, nread, &buf, (const struct sockaddr*) &peer, flags); 
  28.   }} 

libuv会回调c++层,然后c++层回调到js层,最后触发message事件,这就是对应开始那段代码的message事件。

 

 

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2021-05-18 09:01:09

Windows操作系统NodeJs服务器

2020-11-06 08:13:03

服务器Nodejs客户端

2010-07-08 13:19:34

UDP协议

2019-12-24 14:42:51

Nginx服务器架构

2018-07-17 09:59:10

PythonUDP服务器

2014-04-10 09:51:36

2022-06-13 14:18:39

电源管理子系统耗电量服务

2009-01-03 09:21:00

2011-09-07 10:44:36

DHCP服务器配置

2010-08-31 16:47:43

DHCP服务器

2012-02-23 10:02:08

Web服务器应用服务器

2010-09-25 09:23:11

2003 dhcp服务

2018-01-19 10:30:48

HTTP服务器代码

2009-02-27 15:06:00

IA架构服务器服务器解析

2011-03-11 15:52:59

LAMP优化

2017-03-02 11:58:31

NodeJS服务器

2023-04-12 15:31:11

系统服务管理鸿蒙

2011-08-22 11:33:48

nagios

2010-05-19 18:46:59

SVN服务器配置

2018-02-08 08:52:37

点赞
收藏

51CTO技术栈公众号