LwIP源码分析(4):netif网卡驱动初始化代码分析

本篇文章以以太网网卡驱动为例,分析SDK中的TCP echo回传源码,主要是对netif的初始化进行分析,从而深入地理解LwIP中netif的作用。

  • 开发板:NXP Kinetis
  • 操作系统:FreeRTOS,LwIP版本:2.2.0
  • 文章中分析的代码均把没有用到的宏定义相关的代码去掉了,为了方便阅读,代码都是预编译完替换了#define后的代码,有些没有用的代码也去掉

1 TCP_ECHO程序

首先来看一下TCP回显的代码,大概流程就是初始化网卡netif,然后创建一个任务接收TCP数据并回显:

const mdio_operations_t enet_ops = {
	.mdioInit  = ENET_MDIO_Init,
	.mdioWrite = ENET_MDIO_Write,
	.mdioRead  = ENET_MDIO_Read,
	.mdioWriteExt = NULL,
	.mdioReadExt  = NULL
};

const phy_operations_t phyksz8081_ops = {
	.phyInit            = PHY_KSZ8081_Init,
	.phyWrite           = PHY_KSZ8081_Write,
	.phyRead            = PHY_KSZ8081_Read,
	.getAutoNegoStatus  = PHY_KSZ8081_GetAutoNegotiationStatus,
	.getLinkStatus      = PHY_KSZ8081_GetLinkStatus,
	.getLinkSpeedDuplex = PHY_KSZ8081_GetLinkSpeedDuplex,
	.setLinkSpeedDuplex = PHY_KSZ8081_SetLinkSpeedDuplex,
	.enableLoopback     = PHY_KSZ8081_EnableLoopback
};

static mdio_handle_t mdioHandle = {.ops = &enet_ops};
static phy_handle_t phyHandle   = {
    /* 开发板以太网0的物理地址 */
	.phyAddr = 0x00, 
	/* MDIO(Management Data Input/Output)操作结构体 */
	.mdioHandle = &mdioHandle, 
	/* 以太网收发器phyksz8081的驱动 */
	.ops = &phyksz8081_ops
};
/* 初始化函数 */
static void stack_init(void *arg)
{
    static struct netif netif;
    ip4_addr_t netif_ipaddr, netif_netmask, netif_gw;
    ethernetif_config_t enet_config = {
        .phyHandle  = &phyHandle,
        .macAddress = {0x02, 0x12, 0x13, 0x10, 0x15, 0x11},
    };

    mdioHandle.resource.csrClock_Hz = CLOCK_GetFreq(kCLOCK_CoreSysClk);
    /* 设置IP地址,子网掩码和网关 */
    IP4_ADDR(&netif_ipaddr, 192, 168, 0, 102);
    IP4_ADDR(&netif_netmask, 255, 255, 255, 0);
    IP4_ADDR(&netif_gw, 192, 168, 0, 100);

    tcpip_init(NULL, NULL);

    netifapi_netif_add(&netif, &netif_ipaddr, &netif_netmask, &netif_gw, &enet_config, ethernetif0_init,
                       tcpip_input);
    netifapi_netif_set_default(&netif);
    netifapi_netif_set_up(&netif);
    /* 创建TCP_ECHO任务进行回显 */
    tcpecho_init();
    vTaskDelete(NULL);
}

其中tcpip_init在我的另一篇博客tcpip_init和tcpip_thread函数分析中我简单地分析过。

可以看到上面主要是传入一些参数,然后初始化netif结构体。所以我们就从这个结构体出发来一探究竟:

  • 这里不对IPV6做分析,故在下面代码中将与IPV6相关的结构体成员的去掉以易于查看

2 netif结构体

struct netif {
  /* 如果程序中有多个网卡,如用PPP,以太网等连接,则不同的网卡netif用该链表连接 */
  struct netif *next;
  /* IPV4地址配置(网络字节序):IP,子网掩码,网关 */
  ip_addr_t ip_addr;
  ip_addr_t netmask;
  ip_addr_t gw;
  /* 由网络设备驱动调用该函数在TCP/IP协议栈上传递一组数据包 */
  netif_input_fn input;
  /* 由IP模块调用来解析硬件地址再发送一组数据包,对于以太网的物理层来说,该函数为etharp_output */
  netif_output_fn output;
  /* 当想要发送一组数据包时,在ethernet_output中调用,它输出链路层中的原始pbuf */
  netif_linkoutput_fn linkoutput;
  /* 当链路层网卡状态改变时(连接/断开)时调用 */
  netif_status_callback_fn status_callback;
  /* 当链路层网卡发起连接或断开时调用 */
  netif_status_callback_fn link_callback;
  /* 当一个netif网卡结构体移除时调用 */
  netif_status_callback_fn remove_callback;
  /* 供用户传递数据使用 */
  void *state;
  /* 存放一些客户端的数据,如DHCP客户端结构体 */
  void* client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX + LWIP_NUM_NETIF_CLIENT_DATA];
  /* netif的主机名 */
  const char*  hostname;
  /* 每个netif都可以使能/失能校验和的生成和校验 */
  u16_t chksum_flags;
  /* 最大传输单元(单位:bytes) */
  u16_t mtu;
  /* 链路层的硬件地址 */
  u8_t hwaddr[NETIF_MAX_HWADDR_LEN];
  /* 硬件地址的长度 */
  u8_t hwaddr_len;
  /* 网卡状态信息标志位,包括网卡功能使能、广播使能、 ARP使能等标志位 */
  u8_t flags;
  /** 该netif的名字 */
  char name[2];
  /* 用来标示使用同种驱动类型的不同网卡的数量 */
  u8_t num;
  /* 该函数用来添加或删除以太网MAC层组播的过滤表中的条目 */
  netif_igmp_mac_filter_fn igmp_mac_filter;
  /* ACD模块:有关自动IP获取 */
  struct acd *acd_list;
  /* VLAN PCP相关 */
  struct netif_hint *hints;
  /* 环回相关:略 */
};

部分不好理解的参数具体在代码中用到了我们再来研究。我们注意到网卡接口netif是在netifapi_netif_add函数中进行初始化的,现在来看看这个函数:

3 netif_add函数详解

struct netifapi_msg {
  struct tcpip_api_call_data call;
  struct netif *netif;
  union {
    struct {
      const ip4_addr_t * ipaddr;
      const ip4_addr_t * netmask;
      const ip4_addr_t * gw;

      void *state;
      netif_init_fn init;
      netif_input_fn input;
    } add;
    struct {
      netifapi_void_fn voidfunc;
      netifapi_errt_fn errtfunc;
    } common;
    struct {
      char *name;
      u8_t index;
    } ifs;
  } msg;
};

err_t netifapi_netif_add(struct netif *netif, const ip4_addr_t *ipaddr, const ip4_addr_t *netmask, const ip4_addr_t *gw,
                   			 void *state, netif_init_fn init, netif_input_fn input)
{
	err_t err;
	struct netifapi_msg msg;
	
	msg.netif = netif;
	msg.msg.add.ipaddr = ipaddr;
	msg.msg.add.netmask = netmask;
	msg.msg.add.gw = gw;
	
	msg.msg.add.state = state;
	msg.msg.add.init = init;
	msg.msg.add.input = input;
	/* msg.call为结构体第一个元素的地址,也就是结构体的地址 */
	err = tcpip_api_call(netifapi_do_netif_add, &msg.call);
	
	return err;
}

tcpip_api_call实际上就是执行netifapi_do_netif_add函数,然后在执行前获得互斥锁,执行后释放互斥锁,这样可以让用户在自己的代码中实现LwIP的一些操作。

err_t tcpip_api_call(tcpip_api_call_fn fn, struct tcpip_api_call_data *call)
{
  err_t err;
  sys_lock_tcpip_core();
  err = fn(call);
  sys_unlock_tcpip_core();
  return err;
}

现在来看看netifapi_do_netif_add函数:

static err_t netifapi_do_netif_add(struct tcpip_api_call_data *m)
{
	struct netifapi_msg *msg = (struct netifapi_msg *)(void *)m;
	 if (!netif_add( msg->netif,
		msg->msg.add.ipaddr,
		msg->msg.add.netmask,
		msg->msg.add.gw,
		msg->msg.add.state,
		msg->msg.add.init,
		msg->msg.add.input)) 
	{
		return ERR_IF;
	} 
	return ERR_OK;
}

所以到头来就是调用了netif_add函数,由于例程中有操作系统所以需要考虑互斥,现在来看看netif_add

struct netif* netif_add(struct netif *netif,const ip4_addr_t *ipaddr, const ip4_addr_ *netmask, 
                        const ip4_addr_t *gw, void *state, netif_init_fn init, netif_input_fn input)
{
	sys_check_core_locking();
	/* 如果IP、子网掩码、网关为0的话,赋一个默认值,地址用32位数表示 */
	if (ipaddr == 0) {
		ipaddr = ((&ip_addr_any));
	}
	if (netmask == 0) {
		netmask = ((&ip_addr_any));
	}
	if (gw == 0) {
		gw = ((&ip_addr_any));
	}
	/* 将netif结构体中的IP、子网掩码、网关清零 */
	((&netif->ip_addr)->addr = 0);
	((&netif->netmask)->addr = 0);
	((&netif->gw)->addr = 0);
	/* 设置默认output函数为netif_null_output_ip4,该函数中没有内容 */
	netif->output = netif_null_output_ip4;
	/* 设置结构体的初始值 */
	netif->mtu = 0;
	netif->flags = 0;
	memset(netif->client_data, 0, sizeof(netif->client_data));
	/* 用户传的自定义传输,这里为enet_config  */
	netif->state = state;
	/* 记录netif网卡的数量 */
	netif->num = netif_num;
	/* 网卡向TCP协议栈发送数据的函数,这里为tcpip_input */
	netif->input = input;
	/* 设置 */
	netif_set_addr(netif, ipaddr, netmask, gw);     //(1)

	/* 调用前面传入的init参数(函数),即ethernetif0_init */
	if (init(netif) != ERR_OK) {                    //(2)
		return 0;
	}
	/* 前面netif->num已经赋值为netif_num,这里遍历整个netif_list寻找一个唯一的num给当前的netif */
	struct netif *netif2;
	do {
      if (netif->num == 255) {
        netif->num = 0;
      }
      for (netif2 = netif_list; netif2 != NULL; netif2 = netif2->next) {
        if (netif2->num == netif->num) {
          netif->num++;
          break;
        }
      }
    } while (netif2 != NULL);
	/* netif_num用来记录上次分配的netif->num+1,方便下次分配 */
	if (netif->num == 254) {
		netif_num = 0;
	} else {
		netif_num = (u8_t)(netif->num + 1);
	}
	/* 将当前netif结构体加入netif_list链表中 */
	netif->next = netif_list;
	netif_list = netif;

	return netif;
}

(1)netif_set_addr

void netif_set_addr(struct netif *netif, const ip4_addr_t *ipaddr, 
                    const ip4_addr_t *netmask, const ip4_addr_t *gw)
{
	ip_addr_t *old_nm = 0;
	ip_addr_t *old_gw = 0;

	ip_addr_t old_addr;
	int remove;
	sys_check_core_locking();
	/* 再判断IP、子网掩码和网关是否为0,是的话设置为ip_addr_any:该部分代码略 */
	/* 如果没有设置IP地址或者设置为ip_addr_any,则remove为真 */
	remove = ((ipaddr) == 0 || ((*(ipaddr)).addr == ((u32_t)0x00000000UL)));
	if (remove) {
		/* 检查IP和之前netif中设置的是否一样,若不一样,则保存之前的IP到old_addr
		 * 然后调用tcp_netif_ip_addr_changed修改tcp_active_pcbs和tcp_bound_pcbs
		 * 两个TCP链表,最后再判断如何在listen之前的地址,改为listen新设置的地址
		 */
		netif_do_set_ipaddr(netif, ipaddr, &old_addr);
	}
	/* 设置子网掩码:仅仅修改netif结构体中的netmask项 */
	netif_do_set_netmask(netif, netmask, old_nm));
	/* 设置网关:仅仅修改netif结构体中的gateway项 */
	netif_do_set_gw(netif, gw, old_gw);
	/* 如果前面没有设置IP,则最后设置,这样做的原因:移除地址前必须先修改子网掩码和网关 
	 * 以保证tcp RST段可以正确地发送,可以在最前面设置是因为remove表示该netif没有建立连接
	 */
	if (!remove) {
		netif_do_set_ipaddr(netif, ipaddr, &old_addr);
	}
}

(2)ethernetif0_init

err_t ethernetif0_init(struct netif *netif)
{
    static struct ethernetif ethernetif_0;
    __attribute__((aligned((16U)))) static enet_rx_bd_struct_t rxBuffDescrip_0[5];
    __attribute__((aligned((16U)))) static enet_tx_bd_struct_t txBuffDescrip_0[3];
    __attribute__((aligned((16U)))) static rx_buffer_t rxDataBuff_0[5*2];
    __attribute__((aligned((16U)))) static tx_buffer_t txDataBuff_0[3];

    ethernetif_0.RxBuffDescrip = &(rxBuffDescrip_0[0]);
    ethernetif_0.TxBuffDescrip = &(txBuffDescrip_0[0]);
    ethernetif_0.RxDataBuff = &(rxDataBuff_0[0]);
    ethernetif_0.TxDataBuff = &(txDataBuff_0[0]);

    return ethernetif_init(netif, &ethernetif_0, ethernetif_get_enet_base(0U), (ethernetif_config_t *)netif->state);
}

可以看到ethernetif_0就是声明了几个数组填充到ethernetif结构体中,然后调用ethernetif_init来初始化以太网:

err_t ethernetif_init(struct netif *netif,
                      struct ethernetif *ethernetif,
                      void *enetBase,
                      const ethernetif_config_t *ethernetifConfig)
{
	/* netif->state赋值为ethernetif0_init()中声明的ethernetif_0结构体 */
    netif->state = ethernetif;
    netif->name[0] = 'e';
    netif->name[1] = 'n';
	/* output函数为解析硬件地址并发送数据包,以太网的output函数是etharp_output */
    netif->output = etharp_output;
	/* 以太网输出链路层中的原始pbuf的函数为ethernetif_linkoutput */
    netif->linkoutput = ethernetif_linkoutput;
	/* 设置ethernetif->base函数参数中的enetBase,即芯片中以太网的物理地址 */
    *ethernetif_enet_ptr(ethernetif) = enetBase;
	/* 设置MAC硬件地址长度 */
    netif->hwaddr_len = 6;
	/* ethernetifConfig即netif_add函数传的用户变量state,实际上是enet_config */
    memcpy(netif->hwaddr, ethernetifConfig->macAddress, 6U);
	/* 设置以太网MTU */
    netif->mtu = 1500;
	/* 设置以太网Braodcast、ARP和LinkUp的Flag */
    netif->flags |= NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
	/* 以太网初始化函数 */
    ethernetif_enet_init(netif, ethernetif, ethernetifConfig);
    return ERR_OK;
}

可以看到ethernetif_init就是设置了一些参数,最后调用ethernetif_enet_init来初始化,最后我们来看看这个函数做了什么事

void ethernetif_enet_init(struct netif *netif,
                          struct ethernetif *ethernetif,
                          const ethernetif_config_t *ethernetifConfig)
{
    enet_config_t config;
    uint32_t sysClock;
    enet_buffer_config_t buffCfg[1U];
    phy_speed_t speed;
    phy_duplex_t duplex;
    int i;
	/* 接收buffer描述符号码 */
    buffCfg[0].rxBdNumber = 5;
	/* 发送buffer描述符号码 */
    buffCfg[0].txBdNumber = 3;
	/* 接收buffer的对齐字节数 */
    buffCfg[0].rxBuffSizeAlign = sizeof(rx_buffer_t);
	/* 发送buffer的对齐字节数 */
    buffCfg[0].txBuffSizeAlign = sizeof(tx_buffer_t);
	/* 接收buffer描述符的起始地址(ethernetif0_init中声明的数组地址) */
    buffCfg[0].rxBdStartAddrAlign = &(ethernetif->RxBuffDescrip[0]);
	/* 发送buffer描述符的起始地址(ethernetif0_init中声明的数组地址) */
    buffCfg[0].txBdStartAddrAlign = &(ethernetif->TxBuffDescrip[0]);
	/* 接收buffer的起始地址,NULL表示该buffer由回调函数分配 */
    buffCfg[0].rxBufferAlign = NULL;
    /* 发送buffer的起始地址(ethernetif0_init中声明的数组地址) */
    buffCfg[0].txBufferAlign = &(ethernetif->TxDataBuff[0][0]);
    /* 发送帧信息的起始地址 */
    buffCfg[0].txFrameInfo = NULL;
    /* 接收buffer的cache维护 */
    buffCfg[0].rxMaintainEnable = 1;
    /* 发送buffer的cache维护 */
    buffCfg[0].txMaintainEnable = 1;
	/* csrClock_Hz在前面stack_init()函数中初始化 */
    sysClock = ethernetifConfig->phyHandle->mdioHandle->resource.csrClock_Hz;
	/* 获取默认配置结构体:MII mode,全双工,100Mbps等 */
    ENET_GetDefaultConfig(&config);
    /* 仅使用一个ring */
    config.ringNum = 1U;
    /* 接收buffer的分配函数:事先分配好ENET_RXBUFF_NUM个数组作为接收buffer */
    config.rxBuffAlloc = ethernetif_rx_alloc;
    /* 接收buffer的释放函数 */
    config.rxBuffFree = ethernetif_rx_free;
    /* netif结构体作为用户参数 */
    config.userData = netif;
	/* 调用之前填入的硬件上的以太网收发器phyksz8081的ops中的初始化函数PHY_KSZ8081_Init对网卡进行初始化
     * 然后调用PHY_KSZ8081_GetAutoNegotiationStatus和PHY_KSZ8081_GetLinkStatus判断是否初始化成功
     * 若初始化成功则调用PHY_KSZ8081_GetLinkSpeedDuplex获得交互后实际设置的网卡速度和全/半双工
     */
    ethernetif_phy_init(ethernetif, ethernetifConfig, &speed, &duplex);
    /* 将获取的网卡速度和全/半双工状态记录到config结构体中 */
    config.miiSpeed = (enet_mii_speed_t)speed;
    config.miiDuplex = (enet_mii_duplex_t)duplex;

    uint32_t instance;
    /* 硬件上ENET的基地址 */
    static ENET_Type *const enetBases[] = { ((ENET_Type *)(0x400C0000u)) };
    /* 以太网硬件发送中断IRQ */
    static const IRQn_Type enetTxIrqId[] = { ENET_Transmit_IRQn };
    /* 以太网硬件接收中断IRQ */
    static const IRQn_Type enetRxIrqId[] = { ENET_Receive_IRQn };
    /* 创建一个事件标志位来处理多个发送请求 */
    ethernetif->enetTransmitAccessEvent = xEventGroupCreate();
    ethernetif->txFlag = 0x1;
	/* 打开TX/RX Frame中断、TX Buffer中断和Late Collision(在本该发生collision的时间窗口结束后产生collision)中断 */
    config.interrupt |=
        kENET_RxFrameInterrupt | kENET_TxFrameInterrupt | kENET_TxBufferInterrupt | kENET_LateCollisionInterrupt;
    /* 回调函数:处理以太网数据的输入和发送时enetTransmitAccessEvent事件位的管理 */
    config.callback = ethernet_callback;
	/* 芯片支持一个/多个以太网,遍历所有以太网的物理地址,若和当前初始化的以太网地址匹配,则设置发送/接收IRQ的优先级 */
    for (instance = 0; instance < (sizeof(enetBases) / sizeof((enetBases)[0])); instance++)
    {
        if (enetBases[instance] == ethernetif->base)
        {
            __NVIC_SetPriority(enetRxIrqId[instance], (6U));
            __NVIC_SetPriority(enetTxIrqId[instance], (6U));
            break;
        }
    }
	/* 前面提到的ENET_RXBUFF_NUM个分配好的接收buffer需要初始化 */
    for (i = 0; i < ENET_RXBUFF_NUM; i++)
    {
    	/* 设置释放函数 */
        ethernetif->RxPbufs[i].p.custom_free_function = ethernetif_rx_release;
        /* 原始数据buffer */
        ethernetif->RxPbufs[i].buffer = &(ethernetif->RxDataBuff[i][0]);
        /* 标记是否使用 */
        ethernetif->RxPbufs[i].buffer_used = 0;
        /* 网卡netif结构体 */
        ethernetif->RxPbufs[i].netif = netif;
    }
	/* 使能ENET时钟并复位ENET,最后调用ENET_Up函数,完成如下工作:
	 * 1.检查前面的发送/接收buffer描述符的合法性;
	 * 2.根据配置设置MAC控制器相关寄存器;
	 * 3.保存buffCfg的部分到ethernetif->handle,再将该handle保存到变量s_ENETHandle供后续使用,最后设置中断处理函数 
	 */
    ENET_Init(ethernetif->base, &ethernetif->handle, &config, &buffCfg[0], netif->hwaddr, sysClock);
    /* 设置ENET->RDAR寄存器以开启以太网的数据读取 */
    ENET_ActiveRead(ethernetif->base);
}

这里的初始化还是比较复杂的,但无非就是根据用户的配置对ENET的寄存器进行相应的配置,然后将一些软件上的配置,如buffer,保存在本地供后续以太网收发时使用这些变量。如果要深入了解ENET的寄存器,建议详细阅读ENET_Up函数。

4 netif_set_default和netif_setup

netif_add一样,netifapi_netif_set_defaultnetifapi_netif_set_up最终将调用netif_set_defaultnetif_setup。最后来看看这两个函数:

void netif_set_default(struct netif *netif)
{
	netif_default = netif;
}

顾名思义,netif_set_default就是设置默认网卡,LwIP支持多个网卡同时使用,比如一个用以太网上网,一个用4G PPP拨号上网,但LwIP要怎么区分当前使用哪个网卡呢?就是通过netif_default


void netif_set_up(struct netif *netif)
{
	if(!(netif->flags & NETIF_FLAG_UP)){
		/* 设置该netif的状态为UP */
		netif_set_flags(netif, NETIF_FLAG_UP);
		/* netif状态的变化在一些特定情况下,还需要通知ARP/IGMP/MLD/RS层
		 * 这里会调用etharp_gratuitous来发送一个ARP请求包来请求IP地址,然后设置到netif结构体
		 * /
		netif_issue_reports(netif, NETIF_REPORT_TYPE_IPV4 | NETIF_REPORT_TYPE_IPV6);
	}
}

5 总结

本文主要介绍了LwIP中netif网卡的初始化代码流程,如果想深入了解LwIP协议中的以太网,最好还是要先理解以太网协议,比如前面phyksz8081网卡是怎么根据参考手册进行配置的、用户设置的哪些参数需要设置到芯片的ENET寄存器中。这个如果后续有时间,我会专门写一个博客进行介绍。

本文中一些变量的作用也不太明显,光看名字看不出来是干什么的,也没有在我们分析的过程中再出现过,我们也不可能一个个刨根问底地进行分析。但是在后续代码分析过程中遇到了这些变量,回过头来看的时候,我们便会恍然大悟,或者再回来补充这些。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
官网下载的最新的LWIP源码,非常详细,不但有完整的IP,TCP源码,还包括http, mttp源码。 FEATURES * IP (Internet Protocol, IPv4 and IPv6) including packet forwarding over multiple network interfaces * ICMP (Internet Control Message Protocol) for network maintenance and debugging * IGMP (Internet Group Management Protocol) for multicast traffic management * MLD (Multicast listener discovery for IPv6). Aims to be compliant with RFC 2710. No support for MLDv2 * ND (Neighbor discovery and stateless address autoconfiguration for IPv6). Aims to be compliant with RFC 4861 (Neighbor discovery) and RFC 4862 (Address autoconfiguration) * DHCP, AutoIP/APIPA (Zeroconf), ACD (Address Conflict Detection) and (stateless) DHCPv6 * UDP (User Datagram Protocol) including experimental UDP-lite extensions * TCP (Transmission Control Protocol) with congestion control, RTT estimation fast recovery/fast retransmit and sending SACKs * raw/native API for enhanced performance * Optional Berkeley-like socket API * TLS: optional layered TCP ("altcp") for nearly transparent TLS for any TCP-based protocol (ported to mbedTLS) (see changelog for more info) * PPPoS and PPPoE (Point-to-point protocol over Serial/Ethernet) * DNS (Domain name resolver incl. mDNS) * 6LoWPAN (via IEEE 802.15.4, BLE or ZEP) APPLICATIONS * HTTP server with SSI and CGI (HTTPS via altcp) * SNMPv2c agent with MIB compiler (Simple Network Management Protocol), v3 via altcp * SNTP (Simple network time protocol) * NetBIOS name service responder * MDNS (Multicast DNS) responder * iPerf server implementation * MQTT client (TLS support via altcp)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tilblackout

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值