UDP发送数据包的函数是udp_sendmsg,完成从用户地址空间接受数据包然后赋值到内核空间。udp_sendmsg函数主输入参数有四个:

(1)、kiocb:为了提高对用户地址空间操作效率的数据结构体。

(2)、sk:打开的套接字数据结构,包含了套接字的所有设置信息和选项。

(3)、msg:存放管理用户地址空间的数据结构。

(4)、len:从用户空间接受的数据包长度。

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,size_t len)

struct msghdr结构体:

struct msghdr {void  *   msg_name;   /* Socket name      目的地址选项  */int       msg_namelen;    /* Length of name       目的地址长度*/struct iovec *  msg_iov;    /* Data blocks      消息数组    */__kernel_size_t   msg_iovlen; /* Number of blocks     */void  *   msg_control;    /* Per protocol magic (eg BSD file descriptor passing) 控制信息*/__kernel_size_t    msg_controllen; /* Length of cmsg list */unsigned   msg_flags;  //接受数据的标志
};

1、正确性检查

首先是做数据包的正确检查,如果发送错误比如数据指针越内存了可能操作系统崩溃,所以必须做数据的正确性检查,首先检查数据包长度是否小于0xFFFF,因为udp数据包的长度表示位最大16位,然后检查套接字标志是否设置位非法的MSG_OOB(允许带外发送数据)

...//检查数据包长度if (len > 0xFFFF)return -EMSGSIZE;/** Check the flags.*///套接字非法标志if (msg->msg_flags & MSG_OOB) /* Mirror BSD error message compatibility */return -EOPNOTSUPP;...

2、处理早期悬挂的数据

先检查当前套接字是否有挂起等待发送的数据,如果有就跳转到do_append_data标签处,先处理挂起等待发送的数据包,

...//是否有挂起等待发送的数据包if (up->pending) {/** There are pending frames.* The socket lock must be held while it's corked.*/lock_sock(sk);if (likely(up->pending)) {//挂起的数据包是否是AF_INET协议族if (unlikely(up->pending != AF_INET)) {release_sock(sk);return -EINVAL;}//先处理挂起的数据包复制到IP层goto do_append_data;}release_sock(sk);}...

3、处理新的数据

如果没有挂起等待发送的数据包,udp_sendmsg就处理用户空间传来的数据。新数据处理主要有三个方面:目的IP地址检查、判断是否已经建立连接、控制信息处理。

(1)、目的IP检查

如果目的Ip地址msg->msg_name不为空,就要检查目的ip地址,目的ip地址由套接字名给出,而套接字名保存在msg->msg_name数据域,首先检查目的Ip地址的长度和协议族是否是AF_INET,如果不是就返回错误,检查通过后将目的ip和目的端口赋值给局部变量daddr、dport。

...//检查目的IPif (msg->msg_name) {struct sockaddr_in * usin = (struct sockaddr_in *)msg->msg_name;//目的地址长度检查if (msg->msg_namelen < sizeof(*usin))return -EINVAL;//目的地址协议族检查if (usin->sin_family != AF_INET) {if (usin->sin_family != AF_UNSPEC)return -EAFNOSUPPORT;}//目的ip目的端口赋值daddr = usin->sin_addr.s_addr;dport = usin->sin_port;if (dport == 0)return -EINVAL;} else {...

(2)、已经建立连接

msg->msg_name中的目的地址为空时就要检查是否已经建立连接状况,判断sk->sk_state是否等于TCP_ESTABLISHED,如果不是就返回错误无效的目的地址,如果sk->sk_state等于TCP_ESTABLISHED说明目的路由保存在路由高速缓寄存器中,这时即使应用层传进来来目的ip为空也可以发送数据包,从Inet中取出目的ip和目的端口赋值给局部变量daddr、dport。

    ...//目的IP为空检查连接状态是否为TCP_ESTABLISHEDif (sk->sk_state != TCP_ESTABLISHED)return -EDESTADDRREQ;//将连接状态的路由信息中目的ip目的端口赋值daddr = inet->inet_daddr;dport = inet->inet_dport;/* Open fast path for connected socket.Route will not be used, if at least one option is set.*/connected = 1;...

(3)、控制信息处理

处理和目的IP后就要处理udp的控制信息,udp控制信息保存在msg->msg_control数据域中,如果msg->msg_controllen不为零说明有控制信息,控制信息通过函数ip_cmsg_send解析保存在局部变量struct ipcm_cookie  ipc中。

struct ipcm_cookie{__be32              addr;        //输出网络设备地址int                 oif          //输出网络设备索引struct ip_option    *opt;        //ip选项
}

ip_cmsg_send处理udp的控制信息主要有两种:

a、IP_RETOPTS:从ip协议头中获取ip选项保存到ipc->opt中

b、IP_PKTINFO:主要讲网络设备的索引和ip地址返回为ipc->addr、ipc->oif

int ip_cmsg_send(struct net *net, struct msghdr *msg, struct ipcm_cookie *ipc)
{int err;struct cmsghdr *cmsg;for (cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) {if (!CMSG_OK(msg, cmsg))return -EINVAL;if (cmsg->cmsg_level != SOL_IP)continue;switch (cmsg->cmsg_type) {//ip选项获取case IP_RETOPTS:err = cmsg->cmsg_len - CMSG_ALIGN(sizeof(struct cmsghdr));//从ip协议头中获取Ip选项err = ip_options_get(net, &ipc->opt, CMSG_DATA(cmsg),err < 40 ? err : 40);if (err)return err;break;//获取输出设备网络索引和接口ipcase IP_PKTINFO:{struct in_pktinfo *info;if (cmsg->cmsg_len != CMSG_LEN(sizeof(struct in_pktinfo)))return -EINVAL;info = (struct in_pktinfo *)CMSG_DATA(cmsg);//获取输出网络设备索引ipc->oif = info->ipi_ifindex;//获取接口地址ipc->addr = info->ipi_spec_dst.s_addr;break;}default:return -EINVAL;}}return 0;
}

如果套接字中没有设置ip选项就从inet中获取ip选项数据域

...//设置了套接字控制信息if (msg->msg_controllen) {//获取套接字控制信息保存在ipc结构体中err = ip_cmsg_send(sock_net(sk), msg, &ipc);if (err)return err;if (ipc.opt)free = 1;connected = 0;}//如果没有设置控制信息//就从inet中获取ip选项if (!ipc.opt)ipc.opt = inet->opt;//接口地址复制局部变量saddrsaddr = ipc.addr;//目的地址复制ipc.addripc.addr = faddr = daddr;...

4、路由判断

向ip层发送数据,首先要判断路由,路由的情况有三种

4.1、不需要设置路由

不需要设置路由的主要有四种情况

a、数据包是本地局域网发送标志是sk->localroute。

b、输入信息选项msg->msg_flags设置了不需要路由标志。

c、ip选项设置了严格路由选项。

d、目的地址是组地址也不要设置路由

...tos = RT_TOS(inet->tos);//如果数据是本地局域网传送标志SOCK_LOCALROUTE//或者不需要路由msg_flags标志MSG_DONTROUTE//或者配置了严格路由,就不需要寻址路由tos设置为RTO+ONLINKif (sock_flag(sk, SOCK_LOCALROUTE) ||(msg->msg_flags & MSG_DONTROUTE) ||(ipc.opt && ipc.opt->is_strictroute)) {tos |= RTO_ONLINK;connected = 0;}//目的地址是组地址也不需要寻址路由if (ipv4_is_multicast(daddr)) {if (!ipc.oif)ipc.oif = inet->mc_index;if (!saddr)saddr = inet->mc_addr;connected = 0;}
...

4.2、路由已知

路由已知也就是connected标志位1,就从路由高速缓冲区寄存器中区路由保存到局部变量rt中

...//路由已知,检查目的路由并赋值给路由高速缓冲区的局部变量rtif (connected)rt = (struct rtable *)sk_dst_check(sk, 0);
...

4.3、目的路由无效

检查到目的路由rt是NULL,就要调用ip_route_output_flow函数从路由表中搜索路由,要搜索路由首先要建立struct flowi结构体,主要更加源ip、目的ip、源端口、目的端口、输出网络设备获取路由。如果获取的路由是一个广播路由,但套接字没有设置SO_BROADCAST选项就返回错误。

...//目的路由无效if (rt == NULL) {struct flowi fl = { .oif = ipc.oif,.mark = sk->sk_mark,.nl_u = { .ip4_u ={ .daddr = faddr,.saddr = saddr,.tos = tos } },.proto = sk->sk_protocol,.flags = inet_sk_flowi_flags(sk),.uli_u = { .ports ={ .sport = inet->inet_sport,.dport = dport } } };struct net *net = sock_net(sk);security_sk_classify_flow(sk, &fl);//搜索路由表建立目的路由err = ip_route_output_flow(net, &rt, &fl, sk, 1);if (err) {if (err == -ENETUNREACH)IP_INC_STATS_BH(net, IPSTATS_MIB_OUTNOROUTES);goto out;}err = -EACCES;//目的路由是广播路由,但套接字没有设置SO_BROADCAST就返回错误if ((rt->rt_flags & RTCF_BROADCAST) &&!sock_flag(sk, SOCK_BROADCAST))goto out;if (connected)//建立路由连接,保存路由信息到局部变量rtsk_dst_set(sk, dst_clone(&rt->u.dst));}...

4.4、路由已知

到目的地址的有效路由已经建立,判断套接字选项是否设置了MSG_CONFIRM(套接字返回有效路由信息),如果设置了就要跳转到返回路由信息处理标签:do_confirm


...//套接字设置了MSG_CONFIRM标志//调转到返回路由信息处理标签if (msg->msg_flags&MSG_CONFIRM)goto do_confirm;...

5、向IP层发送数据

向ip层发送数据主要分三个步骤

(1)、加锁,如果套接字已经阻塞就是否套接字并返回错误信息

...lock_sock(sk); if (unlikely(up->pending)) {/* The socket is already corked while preparing it. *//* ... which is an evident application bug. --ANK *///套接字已经阻塞,则释放套接字release_sock(sk);LIMIT_NETDEBUG(KERN_DEBUG "udp cork app bug 2\n");err = -EINVAL;goto out;}
...

(2)、为套接字添加源ip、目的Ip、源端口、目的端口

...//锁定成功,添加源IP、目的IP、等信息准备发送数据inet->cork.fl.fl4_dst = daddr;inet->cork.fl.fl_ip_dport = dport;inet->cork.fl.fl4_src = saddr;inet->cork.fl.fl_ip_sport = inet->inet_sport;up->pending = AF_INET;...

(3)、调用Ip_append_data把数据包复制到IP层缓冲区,getfrag复制把数据包从应用层复制到IP层缓冲区,如果corkreq没有设置MSG_MORE标志,那么立即调用udp_push_pending_frames把刚缓存的数据包发送出去。

...do_append_data:up->len += ulen;getfrag  =  is_udplite ?  udplite_getfrag : ip_generic_getfrag;//缓冲数据包err = ip_append_data(sk, getfrag, msg->msg_iov, ulen,sizeof(struct udphdr), &ipc, &rt,corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);//缓存数据包失败删除把skb从sk_write_queue队列中释放if (err)udp_flush_pending_frames(sk);//corkreq标志没有设置MSG_MORE立即调用udp_push_pending_frames发送//发送刚缓存的数据包else if (!corkreq)err = udp_push_pending_frames(sk);else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))up->pending = 0;//释放skrelease_sock(sk);...

6、ip_generic_getfrag函数

ip_generic_getfrag函数复制把数据从用户空间复制到内核空间,如果复制时不需要做校验和就调用memcpy_formiovecend,校验和留给硬件去做,如果需要做校验和就调用csum_partial_copy_fromviovecend计算部分校验和在复制到IP层。udp校验和分三个阶段:数据校验和、udp协议头校验和、ip协议头校验和。最终将数据从iov指针指向的用户地址空间复制到内核地址空间的局部缓冲区sk_buff中。

ip_generic_getfrag函数:

int
ip_generic_getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb)
{struct iovec *iov = from;if (skb->ip_summed == CHECKSUM_PARTIAL) {//复制时不需要计算校验和if (memcpy_fromiovecend(to, iov, offset, len) < 0)return -EFAULT;} else {__wsum csum = 0;//计算校验和并复制数据if (csum_partial_copy_fromiovecend(to, iov, offset, len, &csum) < 0)return -EFAULT;skb->csum = csum_block_add(skb->csum, csum, odd);}return 0;
}

struct iov结构体

struct iovec
{void __user *iov_base; /* 应用层数据包存放在缓冲区的地址*/__kernel_size_t iov_len; /* 缓冲区能接受的最大数据或者能写入的实际数据长度 */
};

memcpy_fromiovecend函数:

int memcpy_fromiovecend(unsigned char *kdata, const struct iovec *iov,int offset, int len)
{/* Skip over the finished iovecs *///跳过已经复制完成的数据while (offset >= iov->iov_len) {offset -= iov->iov_len;iov++;}while (len > 0) {//取数据的起始地址u8 __user *base = iov->iov_base + offset;//iov->iov_len是struct iov结构中保存数据的最大长度、offset需要复制数据包的偏移量//取len和iov->iov_len-offset的最小值int copy = min_t(unsigned int, len, iov->iov_len - offset);offset = 0;//拷贝数据到内核空间if (copy_from_user(kdata, base, copy))return -EFAULT;len -= copy;kdata += copy;iov++;}return 0;
}

udp_sendmsg函数代码:

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,size_t len)
{struct inet_sock *inet = inet_sk(sk);struct udp_sock *up = udp_sk(sk);int ulen = len;//控制信息结构体struct ipcm_cookie ipc;//路由高速缓冲去入口链表struct rtable *rt = NULL;int free = 0;//路由连接标志int connected = 0;__be32 daddr, faddr, saddr;__be16 dport;u8  tos;int err, is_udplite = IS_UDPLITE(sk);int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;int (*getfrag)(void *, char *, int, int, int, struct sk_buff *);//检查数据包长度if (len > 0xFFFF)return -EMSGSIZE;/**   Check the flags.*///套接字非法标志if (msg->msg_flags & MSG_OOB) /* Mirror BSD error message compatibility */return -EOPNOTSUPP;ipc.opt = NULL;ipc.shtx.flags = 0;//是否有挂起等待发送的数据包if (up->pending) {/** There are pending frames.* The socket lock must be held while it's corked.*/lock_sock(sk);if (likely(up->pending)) {//挂起的数据包是否是AF_INET协议族if (unlikely(up->pending != AF_INET)) {release_sock(sk);return -EINVAL;}//先处理挂起的数据包复制到IP层goto do_append_data;}release_sock(sk);}ulen += sizeof(struct udphdr);/** Get and verify the address.*///检查目的IPif (msg->msg_name) {struct sockaddr_in * usin = (struct sockaddr_in *)msg->msg_name;//目的地址长度检查if (msg->msg_namelen < sizeof(*usin))return -EINVAL;//目的地址协议族检查if (usin->sin_family != AF_INET) {if (usin->sin_family != AF_UNSPEC)return -EAFNOSUPPORT;}//目的ip目的端口赋值daddr = usin->sin_addr.s_addr;dport = usin->sin_port;if (dport == 0)return -EINVAL;} else {//目的IP为空检查连接状态是否为TCP_ESTABLISHEDif (sk->sk_state != TCP_ESTABLISHED)return -EDESTADDRREQ;//将连接状态的路由信息中目的ip目的端口赋值daddr = inet->inet_daddr;dport = inet->inet_dport;/* Open fast path for connected socket.Route will not be used, if at least one option is set.*/connected = 1;}//输出网络设备地址ipc.addr = inet->inet_saddr;//输出网络设备索引号ipc.oif = sk->sk_bound_dev_if;err = sock_tx_timestamp(msg, sk, &ipc.shtx);if (err)return err;//设置了套接字控制信息if (msg->msg_controllen) {//获取套接字控制信息保存在ipc结构体中err = ip_cmsg_send(sock_net(sk), msg, &ipc);if (err)return err;if (ipc.opt)free = 1;connected = 0;}//如果没有设置控制信息//就从inet中获取ip选项if (!ipc.opt)ipc.opt = inet->opt;//接口地址复制局部变量saddrsaddr = ipc.addr;//目的地址复制ipc.addripc.addr = faddr = daddr;//如果设置了源路由//那么下一站点的目的地址从源路由的IP地址列表中获取if (ipc.opt && ipc.opt->srr) {if (!daddr)return -EINVAL;faddr = ipc.opt->faddr;connected = 0;}tos = RT_TOS(inet->tos);//如果数据是本地局域网传送标志SOCK_LOCALROUTE//或者不需要路由msg_flags标志MSG_DONTROUTE//或者配置了严格路由,就不需要寻址路由tos设置为RTO+ONLINKif (sock_flag(sk, SOCK_LOCALROUTE) ||(msg->msg_flags & MSG_DONTROUTE) ||(ipc.opt && ipc.opt->is_strictroute)) {tos |= RTO_ONLINK;connected = 0;}//目的地址是组地址也不需要寻址路由if (ipv4_is_multicast(daddr)) {if (!ipc.oif)ipc.oif = inet->mc_index;if (!saddr)saddr = inet->mc_addr;connected = 0;}//路由已知,检查目的路由并赋值给路由高速缓冲区的局部变量rtif (connected)rt = (struct rtable *)sk_dst_check(sk, 0);//目的路由无效if (rt == NULL) {struct flowi fl = { .oif = ipc.oif,.mark = sk->sk_mark,.nl_u = { .ip4_u ={ .daddr = faddr,.saddr = saddr,.tos = tos } },.proto = sk->sk_protocol,.flags = inet_sk_flowi_flags(sk),.uli_u = { .ports ={ .sport = inet->inet_sport,.dport = dport } } };struct net *net = sock_net(sk);security_sk_classify_flow(sk, &fl);//搜索路由表建立目的路由err = ip_route_output_flow(net, &rt, &fl, sk, 1);if (err) {if (err == -ENETUNREACH)IP_INC_STATS_BH(net, IPSTATS_MIB_OUTNOROUTES);goto out;}err = -EACCES;//目的路由是广播路由,但套接字没有设置SO_BROADCAST就返回错误if ((rt->rt_flags & RTCF_BROADCAST) &&!sock_flag(sk, SOCK_BROADCAST))goto out;if (connected)//建立路由连接,保存路由信息到局部变量rtsk_dst_set(sk, dst_clone(&rt->u.dst));}//套接字设置了MSG_CONFIRM标志//调转到返回路由信息处理标签if (msg->msg_flags&MSG_CONFIRM)goto do_confirm;
back_from_confirm:saddr = rt->rt_src;if (!ipc.addr)daddr = ipc.addr = rt->rt_dst;lock_sock(sk); if (unlikely(up->pending)) {/* The socket is already corked while preparing it. *//* ... which is an evident application bug. --ANK *///套接字已经阻塞,则释放套接字release_sock(sk);LIMIT_NETDEBUG(KERN_DEBUG "udp cork app bug 2\n");err = -EINVAL;goto out;}/**  Now cork the socket to pend data.*///锁定成功,添加源IP、目的IP、等信息准备发送数据inet->cork.fl.fl4_dst = daddr;inet->cork.fl.fl_ip_dport = dport;inet->cork.fl.fl4_src = saddr;inet->cork.fl.fl_ip_sport = inet->inet_sport;up->pending = AF_INET;do_append_data:up->len += ulen;getfrag  =  is_udplite ?  udplite_getfrag : ip_generic_getfrag;//缓冲数据包err = ip_append_data(sk, getfrag, msg->msg_iov, ulen,sizeof(struct udphdr), &ipc, &rt,corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);//缓存数据包失败删除把skb从sk_write_queue队列中释放if (err)udp_flush_pending_frames(sk);//corkreq标志没有设置MSG_MORE立即调用udp_push_pending_frames发送//发送刚缓存的数据包else if (!corkreq)err = udp_push_pending_frames(sk);else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))up->pending = 0;//释放skrelease_sock(sk);out:ip_rt_put(rt);if (free)kfree(ipc.opt);if (!err)return len;/** ENOBUFS = no kernel mem, SOCK_NOSPACE = no sndbuf space.  Reporting* ENOBUFS might not be good (it's not tunable per se), but otherwise* we don't have a good statistic (IpOutDiscards but it can be too many* things).  We could add another new stat but at least for now that* seems like overkill.*/if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) {UDP_INC_STATS_USER(sock_net(sk),UDP_MIB_SNDBUFERRORS, is_udplite);}return err;do_confirm:dst_confirm(&rt->u.dst);if (!(msg->msg_flags&MSG_PROBE) || len)goto back_from_confirm;err = 0;goto out;
}

UDP发送数据包流程相关推荐

  1. linux网络接口数据重新封包,Linux网络之设备接口层:发送数据包流程dev_queue_xmit...

    写在前面 本文主要是分析kernel-3.8的源代码,主要集中在Network的netdevice层面,来贯穿interface传输数据包的流程,kernel 博大精深,这也仅仅是一点个人愚见,作为一 ...

  2. 【Java 网络编程】UDP 服务器 客户端 通信 ( DatagramSocket | DatagramPacket | UDP 发送数据包 | UDP 接收数据包 | 端口号分配使用机制 )

    文章目录 I UDP 信息发送接收原理 II UDP 发送和接收端口相同 III UDP 发送信息代码示例 IV UDP 接收信息代码示例 V UDP 服务器端代码示例 VI UDP 客户端代码示例 ...

  3. TCP,UDP发送数据包大小浅析

    MTU最大传输单元,这个最大传输单元实际上和链路层协议有着密切的关系,EthernetII帧的结构DMAC+SMAC+Type+Data+CRC由于以太网传输电气方面的限制,每个以太网帧都有最小的大小 ...

  4. linux网卡发送数据包流程,linux内核Ethernet以太网卡驱动收发数据过程

    linux内核Ethernet以太网卡驱动收发数据过程 linux内核Ethernet以太网卡驱动收发数据过程 下图简单描述了网卡驱动与Linux内核之间的联系: 关于上图的一些说明: 系统初始化: ...

  5. linux 监听数据包,linux下网络监听与发送数据包的方法(即libpcap、libnet两种类库的使用方法)...

    linux下可以用libpcap函数库实现监听数据包,使用libnet 函数库发送数据包 安装: 在命令行下apt-get install 就可以了 libpcap的使用: /*author hjj ...

  6. linux下网络监听与发送数据包的方法(即libpcap、libnet两种类库的使用方法)

    linux下可以用libpcap函数库实现监听数据包,使用libnet 函数库发送数据包 安装: 在命令行下apt-get install 就可以了 libpcap的使用: /*author hjjd ...

  7. 负载均衡原理与实践详解 第五篇 负载均衡时数据包流程详解

    负载均衡原理与实践详解 第五篇 负载均衡时数据包流程详解 系列文章: 负载均衡详解第一篇:负载均衡的需求 负载均衡详解第二篇:服务器负载均衡的基本概念-网络基础 负载均衡详解第三篇:服务器负载均衡的基 ...

  8. coap php 发送,如何使用coap协议向服务器发送数据包

    如何使用coap协议向服务器发送数据包以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧! CoAP的URL 在HTTP的世界 ...

  9. 江科大32——USART发送数据包原理

    1.HEX数据包格式  数据包的作用: 把一个个单独的数据打包起来,方便进行多字节的数据通信. 比如:陀螺仪传感器需要用串口发送数据到STM32,陀螺仪的数据有X轴一个字节,Y轴一个字节,Z轴一个字节 ...

最新文章

  1. 2021.04.07 oppo HR面
  2. Operations Manager 2007 监控Active Directory SCOM-Part 3
  3. Shiro Shiro Web Support and EnvironmentLoaderListener
  4. 自定义右键菜单,禁用浏览器自带的右键菜单[右键菜单实现--Demo]
  5. 百度小程序html解析图片过大_如何快速高效爬取谷歌百度必应的图片
  6. 软件测试术语中英文对照
  7. sklearn安装包下载
  8. 各个操作系统的命令行窗口的样式
  9. 后端——》Java程序推送微信订阅消息
  10. 超体分享 | 迭代思维:你感觉原地踏步,只是因为你想一步到位
  11. python中len用法_简单介绍Python中的len()函数的使用
  12. 【C语言】求s=1+(1+2)+(1+2+3)+....+(1+2+3+....+n)值
  13. 2014小学计算机统计表,2014年春小学部考试成绩统计表.xls
  14. 高等职业教育扩招100万与产业学院的建设
  15. .Hisi 3516d_ov4689_5658调试
  16. direct3D 学习笔记【转】
  17. 经济学-人类面临的四大约束
  18. “卷积神经网络目标检测:原理、分析与应用场景一览“
  19. spring依赖注入原理详解
  20. Core Data 教程入门

热门文章

  1. 单摄手机逆袭,OPPO R9s Plus对比三大拍照神器
  2. 【Vue】ref引用,插槽
  3. Python批量压缩图片大小并保存到相应的新文件夹,不覆盖源文件
  4. 获取自己手机的电话号码
  5. SQL developer远程连接 Oracle 数据库
  6. 小程序云开发全套实战教程(最全)
  7. nodejs 设计模式
  8. 关于 cursor_sharing = similar
  9. ffmpeg 同宽度 画中画_ffmpeg画中画效果
  10. ks抖音超火摸头源码