内核版本:3.4.39

因为过往的开发工作中既包括内核网络层模块的开发,又包括应用层程序的开发,所以对于网络数据的通信有那么一些了解。但是对于网络通信过程中,内核和应用层之间接口是如何运作的不是很清楚,很多问题无从回答,比如应用层数据如何传递给内核协议栈,网卡硬件收到报文后传递给网络协议栈,协议栈又是如何传递给用户层的?多线程共用同一个UDP套接字发送,数据会错乱吗?那么多套接字,内核如何区分?UDP有发送队列吗等等。本篇文章主要分析UDP套接字发送数据过程中应用层和内核层主要做了哪些工作。

通常我们开发网络通信程序的时候只需要调用gblic封装的库函数就可以了,比如说UDP通信,标准流程大概如下:

1. socket()函数创建套接字
2. bind()绑定本地地址或者连接connect()
3. send()、sendto()、sendmsg()发送

顺着函数调用顺序来分析:

首先是socket()调用,socket()创建一个套接字,成功后返回一个文件描述符。该调用由glibc封装,实际会调用内核的socket函数进行处理,简单的流程图如下:

socket内核实现

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{int retval;struct socket *sock;    //通用套接字结构int flags;.............//创建一个套接字retval = sock_create(family, type, protocol, &sock);if (retval < 0)goto out;retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));if (retval < 0)goto out_release;........
}

在成功创建套接字后,该套接字仅仅是一个文件描述符,并没有任何地址与之关联。使用该socket发送数据包时,由于该socket没有任何IP地址,内核会根据策略自动选择一个地址。但是,在某些情况下,我们需要手工指定socket使用哪个IP地址进行发送。这时,就需要使用bind系统调用了。

bind源码入口位于net/socket.c中:

/**  Bind a name to a socket. Nothing much to do here since it's*   the protocol's responsibility to handle the local address.**   We move the socket address to kernel space before we call*  the protocol layer (having also checked the address is ok).*/SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{struct socket *sock;struct sockaddr_storage address;int err, fput_needed;/* 由文件描述符得到套接字在内核中对应的结构struct socket */sock = sockfd_lookup_light(fd, &err, &fput_needed);if (sock) {/* umyaddr是用户空间地址,这里将其复制到内核空间address变量中 */err = move_addr_to_kernel(umyaddr, addrlen, &address);if (err >= 0) {/* 对bind动作进行安全性检查 */ err = security_socket_bind(sock,(struct sockaddr *)&address,addrlen);if (!err)/* 调用对应协议的bind动作 */err = sock->ops->bind(sock,(struct sockaddr *)&address, addrlen);}fput_light(sock->file, fput_needed);}return err;
}

在bind的调用中,根据不同的协议调用不同的实现函数(Linux的内核代码中,大量使用了这种面向对象的设计思路)。对于AF_INET协议族来说,无论是面向连接的SOCK_STREAM类型,还是SOCK_DGRAM协议类型,其实现函数均是inet_bind。下面来看一下inet_bind的具体实现:

int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;struct sock *sk = sock->sk;struct inet_sock *inet = inet_sk(sk);unsigned short snum;int chk_addr_ret;int err;/* If the socket has its own bind function then use it. (RAW) *//*如果具体协议实现了bind函数,则调用协议的bind函数。AF_INET协议族中,只有IPPROTO_ICMP和IPPROTO_IP实现了自己的bind函数,IPPROTO_TCP和IPPROTO_UDP都使用AF_INET通用的函数,即这个inet_bind。     */ if (sk->sk_prot->bind) {err = sk->sk_prot->bind(sk, uaddr, addr_len);goto out;}err = -EINVAL;/* 检查地址长度 */if (addr_len < sizeof(struct sockaddr_in))goto out;/* 本来要求地址的协议族要与sock相同,必须为AF_INET,但是这里有个兼容性问题。允许协议族为AF_UNSPEC并且地址为INADDR_ANY的任意地址 */ if (addr->sin_family != AF_INET) {/* Compatibility games : accept AF_UNSPEC (mapped to AF_INET)* only if s_addr is INADDR_ANY.*/err = -EAFNOSUPPORT;if (addr->sin_family != AF_UNSPEC ||addr->sin_addr.s_addr != htonl(INADDR_ANY))goto out;}/* 判断地址类型 */chk_addr_ret = inet_addr_type(sock_net(sk), addr->sin_addr.s_addr);/* Not specified by any standard per-se, however it breaks too* many applications when removed.  It is unfortunate since* allowing applications to make a non-local bind solves* several problems with systems using dynamic addressing.* (ie. your servers still start up even if your ISDN link*  is temporarily down)*//*sysctl_ip_nonlocal_bind系统控制开关,允许bind非本地IP; inet->freebind为一个socket选项,允许该socket bind任意IP;在上面这些变量均不成立时,指定地址又不是任意的本地地址INADDR_ANY,地址类型又不是本地地址类型,多播或广播时,则bind失败。*/        err = -EADDRNOTAVAIL;if (!sysctl_ip_nonlocal_bind &&!(inet->freebind || inet->transparent) &&addr->sin_addr.s_addr != htonl(INADDR_ANY) &&chk_addr_ret != RTN_LOCAL &&chk_addr_ret != RTN_MULTICAST &&chk_addr_ret != RTN_BROADCAST)goto out;snum = ntohs(addr->sin_port);err = -EACCES;/* 如果源端口小于PROT_SOCK(1024),则需要检查用户是否有权限创建知名端口*/if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))goto out;/*      We keep a pair of addresses. rcv_saddr is the one*      used by hash lookups, and saddr is used for transmit.**      In the BSD API these are the same except where it*      would be illegal to use them (multicast/broadcast) in*      which case the sending device address is used.*/lock_sock(sk);/* Check these errors (active socket, double bind). */err = -EINVAL;/* 确保套接字不会被bind两次 */if (sk->sk_state != TCP_CLOSE || inet->inet_num)goto out_release_sock;/* 使用参数设置套接字的接收和发送地址 */inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;/* 如果参数地址是多播或广播类型,则重置发送源地址为0,表示在发送时,使用的是设备地址 */if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)inet->inet_saddr = 0;  /* Use device *//* Make sure we are allowed to bind here. *//*  调用协议自定义的操作函数get_port,判断该端口是否可以使用。    虽然这里是一个查询的动作,但是却会有修改的动作。    当该端口可以使用时,会让inet_sk(sk)->inet_num = snum;    这样做,是因为查询动作已经获得了锁。在确定可以使用该端口时,直接修    改inet_num,这样既可以保证设置端口的原子性,同时还可以提高性能    */if (sk->sk_prot->get_port(sk, snum)) {inet->inet_saddr = inet->inet_rcv_saddr = 0;err = -EADDRINUSE;goto out_release_sock;}/* 如果设置了bind地址,则置上相应的标志 */ if (inet->inet_rcv_saddr)sk->sk_userlocks |= SOCK_BINDADDR_LOCK;/* 如果设置了源端口,则设置相应的标志 */if (snum)sk->sk_userlocks |= SOCK_BINDPORT_LOCK;/* 设置inet_sport,其为网络序 */  inet->inet_sport = htons(inet->inet_num);/* 重置目的地址和端口 */inet->inet_daddr = 0;inet->inet_dport = 0;/* 重置该套接字的路由信息 */sk_dst_reset(sk);err = 0;
out_release_sock:release_sock(sk);
out:return err;
}
EXPORT_SYMBOL(inet_bind);

connect的源码入口位于socket.c,代码如下:

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,int, addrlen)
{struct socket *sock;struct sockaddr_storage address;int err, fput_needed;/* 通过套接字文件描述符获得对应的struct socket */sock = sockfd_lookup_light(fd, &err, &fput_needed);if (!sock)goto out;/* 将用户空间地址复制到内核空间变量address中 */   err = move_addr_to_kernel(uservaddr, addrlen, &address);if (err < 0)goto out_put;/* 安全性检查 */err =security_socket_connect(sock, (struct sockaddr *)&address, addrlen);if (err)goto out_put;/* 与bind类似,调用与协议族对应的connect操作函数 */err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,sock->file->f_flags);
out_put:fput_light(sock->file, fput_needed);
out:return err;
}

对于AF_INET协议族来说,面向连接的协议类型是SOCK_STREAM,其连接函数为inet_stream_connect,而非面向连接的协议类型SOCK_DGRAM,其连接函数为inet_dgram_connect。这很合理,因为从connect的功能实现上看,两者的实现效果完全不同。

看下UDP的inet_dgram_connect:

int inet_dgram_connect(struct socket *sock, struct sockaddr * uaddr,int addr_len, int flags)
{struct sock *sk = sock->sk;/* 长度合法性检查 */if (addr_len < sizeof(uaddr->sa_family))return -EINVAL;/* 如果协议族为AF_UNSPEC,则先执行disconnect */ if (uaddr->sa_family == AF_UNSPEC)return sk->sk_prot->disconnect(sk, flags);/* 如果该套接字没有指定源端口,并且系统自动绑定端口失败,则返回错误 */if (!inet_sk(sk)->inet_num && inet_autobind(sk))return -EAGAIN;/* 调用具体协议的connect实现函数 */ return sk->sk_prot->connect(sk, (struct sockaddr *)uaddr, addr_len);
}
EXPORT_SYMBOL(inet_dgram_connect);

udp_prot是UDP协议中所有自定义操作函数的集合。其connect的实现函数为ip4_datagram_connect/net/ipv4/datagram.c。

int ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{struct inet_sock *inet = inet_sk(sk);struct sockaddr_in *usin = (struct sockaddr_in *) uaddr;struct flowi4 *fl4;struct rtable *rt;__be32 saddr;int oif;int err;/* 地址的长度性检查 */if (addr_len < sizeof(*usin))return -EINVAL;/* 检查是否为AF_INET协议族 */if (usin->sin_family != AF_INET)return -EAFNOSUPPORT;/* 因为connect会改变目的地址,所有socket中保存的路由缓存已经无用,必须重置。    */ sk_dst_reset(sk);lock_sock(sk);/* 得到套接字绑定的发送接口 */oif = sk->sk_bound_dev_if;saddr = inet->inet_saddr;/* 在目的地址是多播地址的情况下,    如果该套接字没有绑定网卡,则出口网卡为设置的多播网卡索引;    如果该套接字没有绑定源IP,则使用设置的多播源地址;*/if (ipv4_is_multicast(usin->sin_addr.s_addr)) {if (!oif)oif = inet->mc_index;if (!saddr)saddr = inet->mc_addr;}fl4 = &inet->cork.fl.u.ip4;/* 判断设置的目的地址是否存在正确的路由 */rt = ip_route_connect(fl4, usin->sin_addr.s_addr, saddr,RT_CONN_FLAGS(sk), oif,sk->sk_protocol,inet->inet_sport, usin->sin_port, sk, true);if (IS_ERR(rt)) {err = PTR_ERR(rt);if (err == -ENETUNREACH)IP_INC_STATS_BH(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);goto out;}/* 如果路由是广播类型,而套接字不是广播类型,则出错 */ if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST)) {ip_rt_put(rt);err = -EACCES;goto out;}/* 如果套接字没有设置发送地址或接收地址,则使用对应路由的源地址*/if (!inet->inet_saddr)inet->inet_saddr = fl4->saddr;   /* Update source address */if (!inet->inet_rcv_saddr) {inet->inet_rcv_saddr = fl4->saddr;if (sk->sk_prot->rehash)sk->sk_prot->rehash(sk);}/* 设置目的地址和端口 */inet->inet_daddr = fl4->daddr;inet->inet_dport = usin->sin_port;sk->sk_state = TCP_ESTABLISHED;inet->inet_id = jiffies;/* 重新设置路由信息 */sk_dst_set(sk, &rt->dst);err = 0;
out:release_sock(sk);return err;
}
EXPORT_SYMBOL(ip4_datagram_connect);

UDP套接字创建、绑定或者连接后就可以发送数据了。

Linux提供了如下发送接口:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

send只能用于处理已连接状态的套接字。而sendto可以在调用时,指定目的地址。这样的话,如果套接字已经是连接状态,那么目的地址dest_addr与地址长度就应该为NULL和0,不然就可能会返回错误。sendmsg则比较特殊,无论是要发送的数据还是目的地址,都保存在msg中。其中msg.msg_name和msg.msg_len用于指明目的地址,而msg.msg_iov则用于保存要发送的数据。这三个系统调用都支持设置指示标志位flags。

send的内核实现代码如下:

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,unsigned, flags)
{/*send可以视为sendto的一种特例,即不设置目的地址的sendto调用。所以内核实现也是让send直接调用sendto。*/return sys_sendto(fd, buff, len, flags, NULL, 0);
}

既然其内核实现是让send直接调用sendto,那么,下面我们就来看一下sendto的内核实现,代码如下:

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,unsigned, flags, struct sockaddr __user *, addr,int, addr_len)
{struct socket *sock;struct sockaddr_storage address;int err;struct msghdr msg;struct iovec iov;int fput_needed;/* 长度合法性检查 */if (len > INT_MAX)len = INT_MAX;/* 从文件描述符获得套接字socket的结构 */ sock = sockfd_lookup_light(fd, &err, &fput_needed);if (!sock)goto out;/* 将数据转换为iovec结构,来调用后面的sendmsg */iov.iov_base = buff;iov.iov_len = len;msg.msg_name = NULL;msg.msg_iov = &iov;msg.msg_iovlen = 1;msg.msg_control = NULL;msg.msg_controllen = 0;msg.msg_namelen = 0;/* 如果设置了地址,则设置msg_name */if (addr) {/* 将地址参数复制到内核变量中 */err = move_addr_to_kernel(addr, addr_len, &address);if (err < 0)goto out_put;msg.msg_name = (struct sockaddr *)&address;msg.msg_namelen = addr_len;}/* 如果socket设置了非阻塞,则消息的标志设置为DONTWAIT(其实也是非阻塞的语义)*/if (sock->file->f_flags & O_NONBLOCK)flags |= MSG_DONTWAIT;msg.msg_flags = flags;/* 调用sock_sendmsg来发送数据包 */err = sock_sendmsg(sock, &msg, len);out_put:fput_light(sock->file, fput_needed);
out:return err;
}

这里最终调用sock_sendmsg,我们先看下sendmsg调用,看看最后是不是也是调用sock_sendmsg:

SYSCALL_DEFINE3(sendmsg, int, fd, struct msghdr __user *, msg, unsigned, flags)
{int fput_needed, err;struct msghdr msg_sys;/* 通过文件描述符获得socket套接字结构 */struct socket *sock = sockfd_lookup_light(fd, &err, &fput_needed);if (!sock)goto out;/* 调用__sys_sendmsg来发送数据包 */err = __sys_sendmsg(sock, msg, &msg_sys, flags, NULL);fput_light(sock->file, fput_needed);
out:return err;
}

接下来进入__sys_sendmsg,代码如下:

static int __sys_sendmsg(struct socket *sock, struct msghdr __user *msg,struct msghdr *msg_sys, unsigned flags,struct used_address *used_address)
{struct compat_msghdr __user *msg_compat =(struct compat_msghdr __user *)msg;struct sockaddr_storage address;struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;unsigned char ctl[sizeof(struct cmsghdr) + 20]__attribute__ ((aligned(sizeof(__kernel_size_t))));/* 20 is size of ipv6_pktinfo */unsigned char *ctl_buf = ctl;int err, ctl_len, iov_size, total_len;err = -EFAULT;/* 从用户空间得到用户消息 */if (MSG_CMSG_COMPAT & flags) {/* 紧凑消息类型 */if (get_compat_msghdr(msg_sys, msg_compat))return -EFAULT;} else if (copy_from_user(msg_sys, msg, sizeof(struct msghdr)))return -EFAULT;/* do not move before msg_sys is valid */err = -EMSGSIZE;/* 消息数据块个数检查 */if (msg_sys->msg_iovlen > UIO_MAXIOV)goto out;/* Check whether to allocate the iovec area */err = -ENOMEM;/* 在内核空间申请消息数据长度 */iov_size = msg_sys->msg_iovlen * sizeof(struct iovec);if (msg_sys->msg_iovlen > UIO_FASTIOV) {iov = sock_kmalloc(sock->sk, iov_size, GFP_KERNEL);if (!iov)goto out;}/* This will also move the address data into kernel space *//* 前面只是将消息头,或者说消息的结构体,复制到内核空间,现在是将消息的真正内容,即iov的内容复制到内核空间 */if (MSG_CMSG_COMPAT & flags) {err = verify_compat_iovec(msg_sys, iov, &address, VERIFY_READ);} elseerr = verify_iovec(msg_sys, iov, &address, VERIFY_READ);if (err < 0)goto out_freeiov;total_len = err;err = -ENOBUFS;/* 与消息数据块类似,复制控制消息块,就不详细描述了 */if (msg_sys->msg_controllen > INT_MAX)goto out_freeiov;ctl_len = msg_sys->msg_controllen;if ((MSG_CMSG_COMPAT & flags) && ctl_len) {err =cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl,sizeof(ctl));if (err)goto out_freeiov;ctl_buf = msg_sys->msg_control;ctl_len = msg_sys->msg_controllen;} else if (ctl_len) {if (ctl_len > sizeof(ctl)) {ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);if (ctl_buf == NULL)goto out_freeiov;}err = -EFAULT;/** Careful! Before this, msg_sys->msg_control contains a user pointer.* Afterwards, it will be a kernel pointer. Thus the compiler-assisted* checking falls down on this.*/if (copy_from_user(ctl_buf,(void __user __force *)msg_sys->msg_control,ctl_len))goto out_freectl;msg_sys->msg_control = ctl_buf;}/* 设置消息标志 */msg_sys->msg_flags = flags;/* 如果套接字是非阻塞的,则设置消息标志MSG_DONTWAIT */if (sock->file->f_flags & O_NONBLOCK)msg_sys->msg_flags |= MSG_DONTWAIT;/** If this is sendmmsg() and current destination address is same as* previously succeeded address, omit asking LSM's decision.* used_address->name_len is initialized to UINT_MAX so that the first* destination address never matches.*//* 如果这次发送的目的地址与上次成功发送的目的地址一致,那就可以省略安全性检查 */ if (used_address && msg_sys->msg_name &&used_address->name_len == msg_sys->msg_namelen &&!memcmp(&used_address->name, msg_sys->msg_name,used_address->name_len)) {/* 调用不进行安全性检查的函数 */err = sock_sendmsg_nosec(sock, msg_sys, total_len);goto out_freectl;}/* 调用sock_sendmsg,需要安全性检查,最终仍然会调用到sock_send_msg_nosec函数 */err = sock_sendmsg(sock, msg_sys, total_len);/** If this is sendmmsg() and sending to current destination address was* successful, remember it.*//* 如果本次发送成功,则保存当前的目的地址 */ if (used_address && err >= 0) {used_address->name_len = msg_sys->msg_namelen;if (msg_sys->msg_name)memcpy(&used_address->name, msg_sys->msg_name,used_address->name_len);}out_freectl:if (ctl_buf != ctl)sock_kfree_s(sock->sk, ctl_buf, ctl_len);
out_freeiov:if (iov != iovstack)sock_kfree_s(sock->sk, iov, iov_size);
out:return err;
}

看完了__sys_sendmsg,我们可以确定,无论是哪个发送数据的系统调用,最终都会调用到sock_sendmsg。

文章有点长,sock_sendmsg放到下篇分析。

参考文档:

1. 《Linux环境编程:从应用到内核》

2.  浅析Linux网络子系统(一)

tcp/ip 协议栈Linux内核源码分析12 udp套接字发送流程一相关推荐

  1. tcp/ip 协议栈Linux内核源码分析14 udp套接字接收流程一

    内核版本:3.4.39 前面两篇文章分析了UDP套接字从应用层发送数据到内核层的处理流程,这里继续分析相反的流程,看看数据是怎么从内核送到应用层的. 与发送类似,内核也提供了多个接收数据的系统调用接口 ...

  2. tcp/ip 协议栈Linux内核源码分析13 udp套接字发送流程二

    内核版本:3.4.39 继续UDP套接字发送,上一篇讲到了sock_sendmsg,这里继续,下面是sock_sendmsg的相关代码 int sock_sendmsg(struct socket * ...

  3. tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二

    内核版本:3.4.39 上篇我们分析了UDP套接字如何接收数据的流程,最终它是在内核套接字的接收队列里取出报文,剩下的问题就是谁会去写入这个队列,当然,这部分工作由内核来完成,本篇剩下的文章主要分析内 ...

  4. tcp/ip 协议栈Linux内核源码分析九 IPv6分片ip6_fragment 分析

    内核版本:3.4.39 IPv6的分片流程和IPv4基本一致,这一点内核源码作者也说了.流程比较简单,分片的时候判断是否满足快速分片,满足的话直接一个接一个加上分片扩展选项发送出去,不满足的话就只能走 ...

  5. tcp/ip 协议栈Linux内核源码分析十 邻居子系统分析一 概述通用邻居框架

    内核版本:3.4.39 为什么需要邻居子系统呢?因为在网络上发送报文的时候除了需要知道目的IP地址还需要知道邻居的L2 mac地址,为什么是邻居的L2地址而不是目的地的L2地址呢,这是因为目的地网络可 ...

  6. tcp/ip 协议栈Linux内核源码分析六 路由子系统分析一路由缓存

    内核版本:3.4.39 收到报文或者发送报文的时候都需要查找路由表,频繁的路由表查找操作时需要耗费一部分CPU的,Linux提供了路由缓存来减少路由表的查询,路由缓存由hash表组织而成,路由缓存的初 ...

  7. tcp/ip 协议栈Linux内核源码分析八 路由子系统分析三 路由表

    内核版本:3.4.39 Linux路由子系统代码量虽说不是很多,但是难度还是有的,最近在分析路由子系统这一块,对它的框架有了基本的了解,如果要想掌握的话估计还得再花点时间阅读代码,先把框架记录下来.路 ...

  8. tcp/ip 协议栈Linux内核源码分析11 邻居子系统分析二 arp协议的实现处理

    内核版本:3.4.39 内核邻居子系统定义了一个基本的框架,使得不同的邻居协议可以共用一套代码.比起其它的内核模块,邻居子系统框架代码还是比较简单易懂的.邻居子系统位于网络层和流量控制子系统中间,它提 ...

  9. tcp/ip 协议栈Linux内核源码分析七 路由子系统分析二 策略路由

    内核版本:3.4.39 策略路由就是根据配置策略查找路由表,早期的Linux版本是不支持策略路由的,默认的查找策略就是先查找local路由表,找不到再继续查找main表,当支持策略路由功能时,内核最多 ...

最新文章

  1. 微信小程序 长按图片不出现菜单_微信更新,新功能上了热搜
  2. celery 运行时 (from . import async, base)SyntaxError: invalid syntax 异常的解决方案
  3. Vue-Cli Error: EACCES: permission denied 解决方案
  4. docker 仓库镜像 替换_自己动手创建 Docker 镜像并分享到镜像仓库,容器引擎的用途越来越广泛!...
  5. 早买早享受 晚买有折扣 !特斯拉大降价致消费者拉横幅抗议
  6. 爆炸了!刚刚!小姐姐说她找了个程序员男友
  7. word2vec 数学原理
  8. JavaEE学习14--过滤器filter
  9. lua把userdata写入mysql_Lua中的userdata
  10. 整理了70个Python实战项目列表,都有完整且详细的教程
  11. 山西毕业主题研学夏令营
  12. 【STM32H7教程】第2章 STM32H7的开发环境搭建
  13. 论文标题管理----WPS自定义多级自动编号列表
  14. 推荐一款办公室必备股票看盘工具
  15. 【系统测试报告】苏科大App系统测试报告
  16. 计算机学后感作文400,考试后的感想作文400字(精选10篇)
  17. 判断各类无盘软件是否处于超管状态
  18. 【Windows】Win11右键恢复完整右键菜单
  19. 高级数据结构之K-D-TREE
  20. 摘抄各类语言设计模式(仅做笔记)

热门文章

  1. 2011年的总结:Fans同学的成败得失
  2. php偷取,PHP偷取UTF-8目标网页内容输出为空白
  3. php动态网站开发报告,PHP动态网站开发pdf
  4. java fft 频谱算法_快速傅里叶变换(FFT)算法原理及代码解析
  5. 【小项目关键技术六】控制北斗 GPS 定位 / UWB 室内定位
  6. 2.3 词嵌入的特性-深度学习第五课《序列模型》-Stanford吴恩达教授
  7. Ardino基础教程 14_倾斜开关
  8. STM32 电机教程 5 - 步进电机基础知识介绍
  9. 了解万用表历史,万用表的前世今生
  10. 【任务脚本】0601更新autojs客户端,回顾之前战绩,注意事项淘宝618活动领喵币autojs脚本,向大神致敬...