UDP数据包接收逻辑的优化修改以及对性能的影响

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>static int totalMsg = 0;void sigINT(int dwsigno)
{printf("totalMsg: %d\n", totalMsg);exit(0);
}int openServer()
{struct addrinfo hints;memset(&hints, 0, sizeof(hints));hints.ai_flags = AI_PASSIVE;hints.ai_socktype = SOCK_DGRAM;struct addrinfo *res;static char *port = "4020";int e = getaddrinfo(NULL, port, &hints, &res);if (e == EAI_SYSTEM){printf("openServer: getaddrinfo error=%d(%s)!!!\n", errno, strerror(errno));return -1;}else if (e != 0){printf("openServer: getaddrinfo error=%s!!!\n", gai_strerror(e));return -1;}int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);if (fd < 0){printf("openServer: create socket error=%d(%s)!!!\n", errno, strerror(errno));freeaddrinfo(res);return -1;}int rcvBufSize = 131071;socklen_t optlen = sizeof(rcvBufSize);if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvBufSize, optlen) < 0){printf("openServer: setsockopt error=%d(%s)!!!\n", errno, strerror(errno));freeaddrinfo(res);return -1;}int rcvRealSize = -1;if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvRealSize, &optlen) < 0){printf("openServer: getsockopt error=%d(%s)!!!\n", errno, strerror(errno));freeaddrinfo(res);return -1;}printf("recrive-buff-size: %d\n", rcvRealSize);if (bind(fd, (struct sockaddr *)res->ai_addr, res->ai_addrlen) < 0){printf("openServer: bind error=%d(%s)!!!\n", errno, strerror(errno));freeaddrinfo(res);return -1;}printf("create udp socket(%d) ok!\n", fd);return fd;
}void monUdpSock(int udpSock)
{static fd_set fds;FD_ZERO(&fds);FD_SET(udpSock, &fds);static struct timeval tv = {0, 20000};int readyNum = select(udpSock+1, &fds, NULL, NULL, &tv);if (readyNum < 0){printf("monUdpSock: select error=%d(%s)!!!\n", errno, strerror(errno));// 异常处理return;}else if (readyNum == 0)return; // select超时,do nothingelse; // 存在可读写fdif (!FD_ISSET(udpSock, &fds))return;static char udpMsg[1024*64]; // 64KBint rbytes = read(udpSock, udpMsg, sizeof(udpMsg));if (rbytes <= 0)return;// 处理收到的Udp消息totalMsg++;
}int main()
{if (signal(SIGINT,sigINT) == SIG_ERR){printf("set single handler error!\n");exit(1);}int udpSock = openServer();while (1){monUdpSock(udpSock);usleep(3); // 做其他工作,占用了3ms}
}

如上所示是一个UDP服务端程序,用以接收UDP数据包并统计接收总量。

这个UDP Server设置自己的UDP服务Socket接收缓冲区为128K,实际在内核占用的缓冲区为256K。

在主循环中的睡眠,是为了模拟程序做其他的工作,每次耗时3ms。

性能统计小脚本:

#!/bin/bash

while [ true ]; dosleep 1netstat -an | grep $1
done

脚本功能是每秒中去netstat查看/列出指定UDP Socket的接收缓冲区队列中滞留的,尚未被read到用户态的UDP数据量。

模拟压力测试,每个UDP数据包为1800字节左右:

1)1000caps,总量100000个UDP数据包:

脚本输出部分如下:

udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp     4304      0 0.0.0.0:4020                0.0.0.0:*
udp     4304      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp     2152      0 0.0.0.0:4020                0.0.0.0:*
udp     2152      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp     2152      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp     2152      0 0.0.0.0:4020                0.0.0.0:*
udp     4304      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp     4304      0 0.0.0.0:4020                0.0.0.0:*
udp     4304      0 0.0.0.0:4020                0.0.0.0:*
udp     2152      0 0.0.0.0:4020                0.0.0.0:*

输出表明:

程序执行时,接收缓冲区存在很多数据来不及收,虽然没有太大的堆积量,也没有到接收缓冲区上限256K,但是UDP服务器并不能及时的将数据取出来(即:UDP服务器还是有问题的!!!)。

[jiang@localhost svr]$ ./main
recrive-buff-size: 262142
create udp socket(3) ok!
^CtotalMsg: 100000

实际UDP服务端收到了100000个UDP数据包,并未发生丢包。

但未丢包不代表万事大吉,只能说明,UDP服务器的处理速度(从内核读缓冲区中取数据的速度)和模拟端的写速度达到了一个相对平衡的状态。

此时如果增加一倍的caps呢?

2)2000caps,总量200000个UDP数据包:

脚本输出部分如下:

[jiang@localhost pm]$ ./netstat.sh 4020
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp    17216      0 0.0.0.0:4020                0.0.0.0:*
udp     4304      0 0.0.0.0:4020                0.0.0.0:*
udp     8608      0 0.0.0.0:4020                0.0.0.0:*
udp    15064      0 0.0.0.0:4020                0.0.0.0:*
udp     4304      0 0.0.0.0:4020                0.0.0.0:*
udp    15064      0 0.0.0.0:4020                0.0.0.0:*
udp     8608      0 0.0.0.0:4020                0.0.0.0:*
udp    15064      0 0.0.0.0:4020                0.0.0.0:*
udp     8608      0 0.0.0.0:4020                0.0.0.0:*
udp     4304      0 0.0.0.0:4020                0.0.0.0:*
udp     6456      0 0.0.0.0:4020                0.0.0.0:*
udp     8608      0 0.0.0.0:4020                0.0.0.0:*
udp   262544      0 0.0.0.0:4020                0.0.0.0:*
udp   258240      0 0.0.0.0:4020                0.0.0.0:*
udp   262544      0 0.0.0.0:4020                0.0.0.0:*
udp   260392      0 0.0.0.0:4020                0.0.0.0:*
udp   260392      0 0.0.0.0:4020                0.0.0.0:*
udp   262544      0 0.0.0.0:4020                0.0.0.0:*

输出表明:

程序运行时,由于服务端程序从内核读缓冲区中read慢了(读出速度小于写入速度),导致读缓冲区中数据堆积,UDP接收缓冲区溢出,出现丢包的情况。

[jiang@localhost svr]$ ./main
recrive-buff-size: 262142
create udp socket(3) ok!
^CtotalMsg: 170327

我模拟发送20万个UDP数据包,实际只收到170327个,丢了近3w个数据包。

我们的主循环,除了接收UDP数据包,还会干别的事情(3ms)。

最理想状态下,假如CPU时间片切换、信号唤醒等立刻完成(不耗用时间),睡眠3ms,在2000caps的情况下,意味着这3ms中将会有6个UDP数据包一定堆积。

假设一个UDP数据包100K,也就是说,从3ms开始的时刻计算,到3ms结束的时刻,共计有600K的数据堆积在缓冲区中,得不到处理。

实际情况更为复杂,堆积的数据只会比理想状态下多,绝不会比其少。

仔细观察UDP数据包接收逻辑:

    static struct timeval tv = {0, 20000};int readyNum = select(udpSock+1, &fds, NULL, NULL, &tv);if (readyNum < 0){printf("monUdpSock: select error=%d(%s)!!!\n", errno, strerror(errno));// 异常处理return;}else if (readyNum == 0)return; // select超时,do nothingelse; // 存在可读写fdif (!FD_ISSET(udpSock, &fds))return;static char udpMsg[1024*64]; // 64KBint rbytes = read(udpSock, udpMsg, sizeof(udpMsg));if (rbytes <= 0)return;// 处理收到的Udp消息totalMsg++;

由于每时每刻都有数据到来,所以select并不会真的等待20ms,而是立刻返回。

即,主循环在不停歇地干两件事情:

1)从udp server sock fd中读取【一个】UDP数据包;
2)干其他事情,花费3ms;

换言之,我们可以认为至少每3ms这个周期中,才会去接收**一个**UDP数据包。

实际上,在select指明有数据到来时,我们可以不断地读取接收缓冲区中的数据包,直到缓冲区再无可读取的数据包。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <netdb.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>static int totalMsg = 0;void sigINT(int dwsigno)
{printf("totalMsg: %d\n", totalMsg);exit(0);
}int openServer()
{struct addrinfo hints;memset(&hints, 0, sizeof(hints));hints.ai_flags = AI_PASSIVE;hints.ai_socktype = SOCK_DGRAM;struct addrinfo *res;static char *port = "4020";int e = getaddrinfo(NULL, port, &hints, &res);if (e == EAI_SYSTEM){printf("openServer: getaddrinfo error=%d(%s)!!!\n", errno, strerror(errno));return -1;}else if (e != 0){printf("openServer: getaddrinfo error=%s!!!\n", gai_strerror(e));return -1;}int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);if (fd < 0){printf("openServer: create socket error=%d(%s)!!!\n", errno, strerror(errno));freeaddrinfo(res);return -1;}int rcvBufSize = 131071;socklen_t optlen = sizeof(rcvBufSize);if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvBufSize, optlen) < 0){printf("openServer: setsockopt error=%d(%s)!!!\n", errno, strerror(errno));freeaddrinfo(res);return -1;}int rcvRealSize = -1;if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvRealSize, &optlen) < 0){printf("openServer: getsockopt error=%d(%s)!!!\n", errno, strerror(errno));freeaddrinfo(res);return -1;}printf("recrive-buff-size: %d\n", rcvRealSize);if (bind(fd, (struct sockaddr *)res->ai_addr, res->ai_addrlen) < 0){printf("openServer: bind error=%d(%s)!!!\n", errno, strerror(errno));freeaddrinfo(res);return -1;}freeaddrinfo(res);// 设置UDP服务Socket为非阻塞模式int sockflag;if ((sockflag = fcntl(fd, F_GETFL, 0)) < 0){printf("openServer: get socket(%d) flag error=%d(%s)", fd, errno, strerror(errno));return -1;}sockflag = sockflag | O_NONBLOCK;if (fcntl(fd, F_SETFL, sockflag) < 0){printf("openServer: set socket(%d) flag error=%d(%s)", fd, errno, strerror(errno));return -1;}printf("create udp socket(%d) ok!\n", fd);return fd;
}void monUdpSock(int udpSock)
{static fd_set fds;FD_ZERO(&fds);FD_SET(udpSock, &fds);static struct timeval tv = {0, 20000};int readyNum = select(udpSock+1, &fds, NULL, NULL, &tv);if (readyNum < 0){printf("monUdpSock: select error=%d(%s)!!!\n", errno, strerror(errno));// 异常处理return;}else if (readyNum == 0)return; // select超时,do nothingelse; // 存在可读写fdif (!FD_ISSET(udpSock, &fds))return;static char udpMsg[1024*64]; // 64KBint rbytes = read(udpSock, udpMsg, sizeof(udpMsg));while (rbytes > 0){// 处理收到的Udp消息totalMsg++;rbytes = read(udpSock, udpMsg, sizeof(udpMsg));}if (rbytes < 0 &&errno != EAGAIN &&errno != EWOULDBLOCK &&errno != EINTR &&errno != EINVAL){printf("monUdpSock: read error=%d(%s)!!!\n", errno, strerror(errno));return;}
}int main()
{if (signal(SIGINT,sigINT) == SIG_ERR){printf("set single handler error!\n");exit(1);}int udpSock = openServer();while (1){monUdpSock(udpSock);usleep(3); // 做其他工作,占用了3ms}
}

注意:

除了在monUdpSock中不断read直到read返回非正值退出的逻辑改动之外,在openServer时,还将服务端的UDP Socket设置为非阻塞模式。

如果不是非阻塞模式会有什么后果呢?

如果read不发生错误,monUdpSock永远不会结束,即使没有UDP数据到来。

由于sock是阻塞型的,读不到就一直挂着自己(read挂起),这个线程再也没办法做别的事情(例如usleep(3))。

如果设置为非阻塞模式,read不到数据并不会真的把自己(执行线程)挂起来,而是立刻返回-1,设置errno为EAGAIN。

我们的核心思想就是:

用read从内核接收缓冲区中尝试收一个UDP数据包,收的到,就继续尝试再收下一个……直到收不到返回。

新版的UDP服务端程序压测如下:

UDP服务端Socket接收缓冲区256KB(同上),每个消息1800B,3000caps:

udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp    10760      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp    12912      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*

略微有点堆积,但是也是正常的,那个时刻可能由于CPU调度线程没得到时间片,没来得及收。

[jiang@localhost svr]$ ./main
recrive-buff-size: 262142
create udp socket(3) ok!
^CtotalMsg: 30000

3000cap毫无压力!

最高压测:

UDP服务端Socket接收缓冲区256KB(同上),每个消息1800B,30000caps(不是3000,是3wcaps):

[jiang@localhost pm]$ ./netstat.sh 4020
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp   122664      0 0.0.0.0:4020                0.0.0.0:*
udp   126968      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*

此次测试是30000个包/秒,数据量也就是:30000*1800B=51MB/秒。

接收缓冲区略微有点堆积也应该是系统调度的原因。

测试共计5次,30000caps无丢包现象。

UDP Socket接收缓冲区在256KB,30000caps,1800B/消息,不会出接收缓冲区满发生丢包。

但是此时再增加量到40000caps,则总出现少许的丢包现象。


其实可以通过增大内核读缓冲区,支持更大的数据量。

通过setsockopt修改读缓冲区大小为1MB,测试100000caps(10wcaps),1800B/消息:

udp   294824      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp   294824      0 0.0.0.0:4020                0.0.0.0:*
udp   299128      0 0.0.0.0:4020                0.0.0.0:*
udp   294824      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp   290520      0 0.0.0.0:4020                0.0.0.0:*
udp   355080      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp   288368      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp   458376      0 0.0.0.0:4020                0.0.0.0:*
udp   393816      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp   292672      0 0.0.0.0:4020                0.0.0.0:*
udp   294824      0 0.0.0.0:4020                0.0.0.0:*
udp   314192      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*
udp   294824      0 0.0.0.0:4020                0.0.0.0:*
udp        0      0 0.0.0.0:4020                0.0.0.0:*

堆积量挺多,不过还远未接近1MB上限,并不会发生丢包。

[jiang@localhost pm]$ ./main 10000000 100000
totalUdp: 10000000
maxRate: 100000
udp data len: 1806
loaded 1806 Bytes Data
finish 10000000 in 100.001, rate: 99999
[jiang@localhost svr]$ ./main
recrive-buff-size: 2097152
create udp socket(3) ok!
^CtotalMsg: 10000000

1000万个UDP包,一个也没有丢失,全部收到。

所谓的缓冲区,除了提供UDP数据包的数据之外,还有一个功能就是容错,容时差。

UDP数据包可以在接收缓冲区中堆积,但一定是由于某种原因暂时的堆积,例如时间片切换,可以堆积部分包,到下次线程轮到时间片时全部处理。

如果服务端程序从接收缓冲区读出的速度小于写入速度,那即使缓冲区设置再大也没有用,终将会堆满接收缓冲区并溢出丢包。

增大接收缓冲区,可以提高由于某些原因导致服务端“抖动”(间隔不规律的读出)而导致接收缓冲区满(UDP溢出)的抗性;

增强服务器消耗(从缓冲区中读出数据)的性能,可以从根本上减少读缓冲区溢出的问题。

UDP数据包是否丢失,与一个UDP数据包的大小、服务端接收缓冲区的大小、读出(接收缓冲区数据)的性能密切相关。

UDP数据包接收逻辑的优化修改以及对性能的影响相关推荐

  1. JAVA实现udp接收文件数据,java – 播放以UDP数据包接收的原始PCM音频

    以下是获取输出线并在其上播放PCM的简单示例.在运行时,它会播放大约一秒长的恼人的哔哔声. import javax.sound.sampled.AudioFormat; import javax.s ...

  2. java 远程udp_远程客户端不接收UDP数据包

    我有简单的UDP服务器/客户端程序,我转发我的端口和服务器通过互联网接收和发送数据包,但远程机器上的客户端无法接收它们,所以我想知道如何在客户端没有转发端口的情况下接收数据包(如果它甚至可能)?如果它 ...

  3. android+udp传输大小,Android UDP数据包如何接收可变大小的数据包

    我有一个Android应用程序,它监视UDP数据包并调用一个方法来处理收到的消息.我有一个问题,如果传入的消息更长,它将调用方法来处理消息.但是如果传入的消息较短,则不会调用该方法,但如果我发送短消息 ...

  4. java发送接收UDP数据包:字符串,byte[]字节数组,文件等

    全栈工程师开发手册 (作者:栾鹏) java教程全解 java发送接收UDP数据包,数据内容为byte[],包括一切可以转换为byte[]的内容. 测试代码 public static void ma ...

  5. 如何在Linux命令行下发送和接收UDP数据包

    众所周知,在传输层有两个常用的协议 TCP 和 UDP,本文介绍在 Linux 命令行下,如何使用 nc 命令发送或接收 UDP 数据包,这些命令的用法对调试 UDP 通信程序将有所帮助. 1. 问题 ...

  6. Linux内核网络协议栈:udp数据包发送(源码解读)

    <监视和调整Linux网络协议栈:接收数据> <监控和调整Linux网络协议栈的图解指南:接收数据> <Linux网络 - 数据包的接收过程> <Linux网 ...

  7. 【Socket网络编程】7.以太网数据包、IP数据包、UDP数据包

    以太网数据包.ip数据包.udp数据包 搭配这篇博文服用,效果更好:数据封装 和 数据拆封:https://blog.csdn.net/u011754972/article/details/11794 ...

  8. IP、TCP、UDP数据包长度问题

      IP数据包长度问题总结 首先要看TCP/IP协议,涉及到四层:链路层,网络层,传输层,应用层. 其中以太网(Ethernet)的数据帧在链路层 IP包在网络层 TCP或UDP包在传输层 TCP或U ...

  9. linux内核丢弃udp报文,c++ Linux UDP数据包丢失的原因

    我有一个 Linux C应用程序接收有序的UDP数据包.由于排序,我可以很容易地确定数据包何时丢失或重新排序,即当遇到"间隙"时.该系统具有处理差距的恢复机制,但最好避免出现差距. ...

最新文章

  1. Hive 整合Hbase(来自学习资料--博学谷)
  2. S2SH框架入门之使用struts2
  3. 【转载】eclipse常用插件在线安装地址或下载地址
  4. mysql服务的启动和停止 net stop mysql net start mysql
  5. 介绍Spring Integration
  6. java递归空瓶换饮料_问题描述:一次买n瓶可乐,k个空瓶可以换一瓶饮料,那么一共能喝多少瓶饮料? | 学步园...
  7. (转)Spring4.2.5+Hibernate4.3.11+Struts1.3.8集成方案一
  8. VC++ : VS2008 使用ATL开发COM组件
  9. 一个插排引发的设计思想 (二) 抽象类与接口
  10. Mac安装 Navicat
  11. c语言程序怎样输出一个图形,C语言循环输出各种 * 组成的图形
  12. 自动控制原理学习--奈奎斯特稳定判据
  13. Spell of the rising moon
  14. 《小岛经济学》读书笔记
  15. Python求积分(定积分)
  16. 博弈论——序论(读书笔记)
  17. SOEM 源码解析 ecx_lookup_prev_sii
  18. python32位和64位有什么区别_python32位和64位有什么区别
  19. 【无标题】8421码,5421码,2421码,余三码之间的区别及对数的表示规则
  20. 美林数据“智能反窃电分析应用”荣获大数据星河奖

热门文章

  1. 软考程序员Java答题速成_软考程序员考试下午题解题技巧
  2. java实战:邮件群发推广微信公众号(二),内含java操作excel及java操作mysql
  3. python程序设计基础知识
  4. SlickEdit V21 2016 破解教程,win linux mac
  5. EasyPR--开发详解(2)车牌定位
  6. Libtorch的介绍与使用方法
  7. 阿里云OSS对象存储 , js 上传文件
  8. python ovito模块计算某一类原子的MSD均方位移
  9. 高校邦java_高校邦Java核心开发技术【实境编程】答案
  10. 苹果nfc功能怎么开启_苹果连夜开放NFC——雷霆NFC免电源智能锁开启千亿市场