目录

一、概述

二、bind

2.1TCP bind

2.1.2 UDP bind

2.1.3 bind tips

2.2 listen


一、概述

前面分析了socket流程,这里继续分析inet连接建立的其他socket API

二、bind

bind函数声明如下:

  • int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind就是将addr和创建的socket进行绑定,对inet(man 7 ip)来说,一个进程想要接收报文就要和本地接口地址(local interface address)进行绑定,就是一个pair (address, port),如果 address指定为INADDR_ANY,则绑定本地所有接口。当然如果没有调用bind函数,在connect或者listen的时候都会以(INADDR_ANY, random port)自动绑定。如果是其他address family,bind的方式有所不同,具体用法参见man,下面来看一下bind如何解决不同address family对应的addr的统一,其对sockaddr的定义如下:

struct sockaddr {sa_family_t sa_family;char        sa_data[14];
}struct sockaddr_in {__kernel_sa_family_t   sin_family; __be16      sin_port;   struct in_addr  sin_addr;   /* Pad to size of `struct sockaddr'. */unsigned char      __pad[__SOCK_SIZE__ - sizeof(short int) -sizeof(unsigned short int) - sizeof(struct in_addr)];
};

从上面可以看到,对于inet, 使用sockaddr_in{}对应sockaddr{}, 并以inet需要的地址(address, port) pair替代了sockaddr{}的sa_date域,这样再使用时强转一下就可以了。

接下来开始分析bind的实现,socket建立后,bind系统调用到inet_bind(sock->ops->bind)

int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{struct sock *sk = sock->sk;int err;/* If the socket has its own bind function then use it. (RAW) */if (sk->sk_prot->bind) {return sk->sk_prot->bind(sk, uaddr, addr_len);}return __inet_bind(sk, uaddr, addr_len, false, true);
}

sk->sk_prot->bind在tcp, udp协议下都是空,直接看__inet_bind(关键部分):

int __inet_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len,bool force_bind_address_no_port, bool with_lock)
{snum = ntohs(addr->sin_port);inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;/* Make sure we are allowed to bind here. */if (snum || !(inet->bind_address_no_port ||force_bind_address_no_port)) {if (sk->sk_prot->get_port(sk, snum)) {inet->inet_saddr = inet->inet_rcv_saddr = 0;err = -EADDRINUSE;goto out_release_sock;}err = BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk);if (err) {inet->inet_saddr = inet->inet_rcv_saddr = 0;goto out_release_sock;}}inet->inet_sport = htons(inet->inet_num);  inet->inet_daddr = 0;  inet->inet_dport = 0;
}

这段函数主要将源IP和源port与对应的sk进行绑定,根据注释:

  • inet->inet_rcv_saddr       rcv_saddr is the one  used by hash lookups
  • inet->inet_saddr              saddr is used for transmit

绑定时要确定(address, port) pair是否可用,具体调用sk->sk_prot->get_port(sk, snum),检测是否冲突,该接口在tcp和udp中是不同的,下面会分析

最终,该函数确定了四元组中的两个:inet_saddr, inet_sport

2.1 TCP bind

在tcp中,get_port对应的具体实现是inet_csk_get_port.

inet_csk_get_port是为指定的sock指定一个本地port,如果bind时没有指定,需要系统分配——奇数端口,偶数留给connect,bind使用hash表记录不同的二元组,hash结构的位置在proto{}中,

union {struct inet_hashinfo *hashinfo;struct udp_table   *udp_table;struct raw_hashinfo  *raw_hash;
} h;

TCP使用inet_hashinfo{}

struct inet_hashinfo {struct inet_ehash_bucket   *ehash;spinlock_t           *ehash_locks;unsigned int           ehash_mask;unsigned int         ehash_locks_mask;struct inet_bind_hashbucket    *bhash;unsigned int         bhash_size;struct kmem_cache        *bind_bucket_cachep;struct inet_listen_hashbucket   listening_hash[INET_LHTABLE_SIZE]____cacheline_aligned_in_smp;atomic_t          bsockets;
};

可以看到TCP使用三个hash表,分别是ehash,bhash,listening_hash,bind时用到bhash,其他用到在说,如下图:

bind时根据snum进行hash,冲突时挂入inet_bind_bucket链表,这里考虑同一个port复用的情况——相同port放在同一个inet_bind_bucket中,如果可复用就加入到链表中(owners)。

下面开始分析代码,先来看传入的sport是0的情况:

[net/ipv4/inet_connection_sock.c]

 if (!port) {head = inet_csk_find_open_port(sk, &tb, &port);if (!head)return ret;if (!tb)goto tb_not_found;goto success;}

其核心函数inet_csk_find_open_port:

attempt_half = (sk->sk_reuse == SK_CAN_REUSE) ? 1 : 0;
other_half_scan:inet_get_local_port_range(net, &low, &high);high++; /* [32768, 60999] -> [32768, 61000[ */if (high - low < 4)attempt_half = 0;if (attempt_half) {int half = low + (((high - low) >> 2) << 1);if (attempt_half == 1)high = half;elselow = half;}remaining = high - low;if (likely(remaining > 1))remaining &= ~1U;offset = prandom_u32() % remaining;/* __inet_hash_connect() favors ports having @low parity* We do the opposite to not pollute connect() users.*/offset |= 1U;

初始化阶段inet_get_local_port_range先获取系统配置的端口范围,[low, high + 1),这里先不考虑端口reuse的情况.接下来计算在端口范围内查找的起始offset,可以看到是在remain范围内随机产生的,最后要保证offset是奇数,因为偶数给connect用

other_parity_scan:port = low + offset;for (i = 0; i < remaining; i += 2, port += 2) {if (unlikely(port >= high))port -= remaining;if (inet_is_local_reserved_port(net, port))continue;head = &hinfo->bhash[inet_bhashfn(net, port,hinfo->bhash_size)];spin_lock_bh(&head->lock);inet_bind_bucket_for_each(tb, &head->chain)if (net_eq(ib_net(tb), net) && tb->port == port) {if (!inet_csk_bind_conflict(sk, tb, false, false))goto success;goto next_port;}tb = NULL;goto success;
next_port:spin_unlock_bh(&head->lock);cond_resched();}offset--;if (!(offset & 1))goto other_parity_scan;if (attempt_half == 1) {/* OK we now try the upper half of the range */attempt_half = 2;goto other_half_scan;}return NULL;

从随机选取的port = low + offset位置进行查找,保证遍历所有的奇数端口(),端口选择,根据随机的port hash选定一个slot,遍历slot上的链,这里的原则是,如果和port有冲突就重新选择port (next_port)那接下来看一下inet_csk_bind_conflict:

sk_for_each_bound(sk2, &tb->owners) {if (sk != sk2 &&(!sk->sk_bound_dev_if ||!sk2->sk_bound_dev_if ||sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {if ((!reuse || !sk2->sk_reuse ||sk2->sk_state == TCP_LISTEN) &&(!reuseport || !sk2->sk_reuseport ||rcu_access_pointer(sk->sk_reuseport_cb) ||(sk2->sk_state != TCP_TIME_WAIT &&!uid_eq(uid, sock_i_uid(sk2))))) {if (inet_rcv_saddr_equal(sk, sk2, true))break;}if (!relax && reuse && sk2->sk_reuse &&   //这个relax条件注意一下sk2->sk_state != TCP_LISTEN) {if (inet_rcv_saddr_equal(sk, sk2, true))break;}}}

这里遍历tb->owners,检测冲突的条件。我们在bind时要限制(address, port)二元组的唯一性,bhash按照port进行hash,那么在port相同的条件下,检测冲突必然落在address的唯一性检查上,如下,如果sk和sk2相比较,只要满足下列任一条件,就会不产生冲突:

  • sk是同一个                                                                                                     sk是同一个了就无所谓冲突了
  • sk和sk2对应的sk_bound_dev_if 不是同一个                                                 不同的端口自然对应不同的地址
  • sk和sk2都指定sk_reuse,且此时sk2的状态不是TCP_LISTEN
  • sk和sk2都指定sk_reuseport且此时sk2->sk_state状态是TCP_WAIT
  • sk和sk2 IP地址不同                                                                                       同一个端口上不同的地址

当然了,用户也可以指定reuse控制复用。

而下面的代码是说即使sk和sk2都指定sk_reuse,且此时sk2的状态不是TCP_LISTEN,在 !relax情况下,只要IP地址相等还是认为是冲突

         if (!relax && reuse && sk2->sk_reuse &&   //这个relax条件注意一下sk2->sk_state != TCP_LISTEN) {if (inet_rcv_saddr_equal(sk, sk2, true))break;}

接下分析当成功分配了一个sport后的情形,有两种情况:

1. hash未冲突(head!=0, tb=NULL)这时候要为sport新建一个tb,走下面的流程

tb_not_found:tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,net, head, port);if (!tb)goto fail_unlock;
tb_found:if (!hlist_empty(&tb->owners)) {if (sk->sk_reuse == SK_FORCE_REUSE)goto success;if ((tb->fastreuse > 0 && reuse) ||sk_reuseport_match(tb, sk))goto success;if (inet_csk_bind_conflict(sk, tb, true, true))goto fail_unlock;}

可以看到首先新建了一个tb,此时tb一定是空的,不会执行tb_found流程,跳过去直接执行success流程

success:if (!hlist_empty(&tb->owners)) {tb->fastreuse = reuse;if (sk->sk_reuseport) {tb->fastreuseport = FASTREUSEPORT_ANY;tb->fastuid = uid;tb->fast_rcv_saddr = sk->sk_rcv_saddr;tb->fast_ipv6_only = ipv6_only_sock(sk);} else {tb->fastreuseport = 0;}} else {if (!reuse)tb->fastreuse = 0;if (sk->sk_reuseport) {if (!sk_reuseport_match(tb, sk)) {tb->fastreuseport = FASTREUSEPORT_STRICT;tb->fastuid = uid;tb->fast_rcv_saddr = sk->sk_rcv_saddr;tb->fast_ipv6_only = ipv6_only_sock(sk);}} else {tb->fastreuseport = 0;}}if (!inet_csk(sk)->icsk_bind_hash)inet_bind_hash(sk, tb, port);

此时,tb下的链表tb->owner一定是空的,走else流程,这里关注tb上的两个字段:tb->fastreuse和tb->fastreuseport,前面说过满足下面任意一个条件一定不会产生冲突

  • sk和sk2都指定sk_reuse,且此时sk2的状态不是TCP_LISTEN
  • sk和sk2都指定sk_reuseport且此时sk2->sk_state状态是TCP_WAIT

这两个标记的意思是如果标记置位,那么意味着tb的链中所有的sk都满足不产生冲突的条件,就不用再去调用很重的inet_csk_bind_conflict操作了,从而简化了判断过程。最后将sk挂入tb->owner进行管理,在这里源端口inet_num赋值,并记录tb

void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb,const unsigned short snum)
{inet_sk(sk)->inet_num = snum;sk_add_bind_node(sk, &tb->owners);inet_csk(sk)->icsk_bind_hash = tb;
}

2.  hash冲突(head!=NULL,tb!=NULL)但是sport没有冲突,这时候省去了分配tb,执行tb_found流程,此时tb->owners一定不为空,为了方便看,再贴一下:

tb_found:if (!hlist_empty(&tb->owners)) {if (sk->sk_reuse == SK_FORCE_REUSE)goto success;if ((tb->fastreuse > 0 && reuse) ||sk_reuseport_match(tb, sk))goto success;if (inet_csk_bind_conflict(sk, tb, true, true))goto fail_unlock;}

这里先进行的上面说的快速检索tb->fastreuse > 0 && reuse,sk_reuseport_match道理相同,但是其条件多一些,就封装成函数了。如果快速检索没有匹配,会再次调用inet_csk_bind_conflict检查冲突,注意这里和自动搜寻sport时参数是不一致的(false,false->true,true),上面是没有检查reuseport条件的,而且认为下面的代码是冲突行为:

if (!relax && reuse && sk2->sk_reuse &&sk2->sk_state != TCP_LISTEN) {if (inet_rcv_saddr_equal(sk, sk2, true))break;
}

接下来走success流程的tb非空还是在设置快速检索的方法,不再赘述。

继续分析bind指定了port的流程,流程上和前面差不多:

head = &hinfo->bhash[inet_bhashfn(net, port,hinfo->bhash_size)];
spin_lock_bh(&head->lock);
inet_bind_bucket_for_each(tb, &head->chain)if (net_eq(ib_net(tb), net) && tb->port == port)goto tb_found;
tb_not_found:
...
tb_found:
...
success:
...

前面都分析过了,只是省去了搜寻过程而已。

2.2 UDP bind

UDP获取端口的函数是udp_v4_get_port

[net/ipv4/udp.c]

int udp_v4_get_port(struct sock *sk, unsigned short snum)
{unsigned int hash2_nulladdr =ipv4_portaddr_hash(sock_net(sk), htonl(INADDR_ANY), snum);unsigned int hash2_partial =ipv4_portaddr_hash(sock_net(sk), inet_sk(sk)->inet_rcv_saddr, 0);/* precompute partial secondary hash */udp_sk(sk)->udp_portaddr_hash = hash2_partial;return udp_lib_get_port(sk, snum, hash2_nulladdr);
}

接下来看udp_lib_get_port,还是先看bind未指定源端口的情况:

 if (!snum) {int low, high, remaining;unsigned int rand;unsigned short first, last;DECLARE_BITMAP(bitmap, PORTS_PER_CHAIN);inet_get_local_port_range(net, &low, &high);remaining = (high - low) + 1;rand = prandom_u32();first = reciprocal_scale(rand, remaining) + low;/** force rand to be an odd multiple of UDP_HTABLE_SIZE*/rand = (rand | 1) * (udptable->mask + 1);last = first + udptable->mask + 1;...
}

(address, port)之间的关系仍然是通过hash实现的,对应下面的udp部分:

union {struct inet_hashinfo  *hashinfo;struct udp_table  *udp_table;struct raw_hashinfo  *raw_hash;struct smc_hashinfo   *smc_hash;
} h;struct udp_table {struct udp_hslot  *hash;struct udp_hslot  *hash2;unsigned int     mask;unsigned int       log;
};

2.3 bind tips

  • 同一个socket多次绑定相同或者不同的端口

不允许重复bind或者绑定多个port,来自inet_bind,如果第一次绑定成功,对应的inet_sock的inet_num已经赋值,不允许继续执行了。

​/* Check these errors (active socket, double bind). */err = -EINVAL;if (sk->sk_state != TCP_CLOSE || inet->inet_num)goto out_release_sock;
  • 不同的socket绑定相同的端口(同一个协议,udp)

三、 listen

只有tcp需要listen,其函数声明:

  • int listen(int sockfd, int backlog);

listen将socket标记为passive,表明已经准备好接受新的连接,其实就是将状态机的状态从TCP_CLOSE变成TCP_LISTEN,即执行一个被动打开操作。

[net/ipv4/af_inet.c]

int inet_listen(struct socket *sock, int backlog)
{struct sock *sk = sock->sk;unsigned char old_state;int err, tcp_fastopen;lock_sock(sk);err = -EINVAL;if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)goto out;old_state = sk->sk_state;if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))goto out;if (old_state != TCP_LISTEN) {tcp_fastopen = sock_net(sk)->ipv4.sysctl_tcp_fastopen;if ((tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) &&(tcp_fastopen & TFO_SERVER_ENABLE) &&!inet_csk(sk)->icsk_accept_queue.fastopenq.max_qlen) {fastopen_queue_tune(sk, backlog);tcp_fastopen_init_key_once(sock_net(sk));}err = inet_csk_listen_start(sk, backlog);if (err)goto out;}sk->sk_max_ack_backlog = backlog;err = 0;out:release_sock(sk);return err;
}
EXPORT_SYMBOL(inet_listen);
  • 参数检查
  • sk状态,处于TCP_CLOSE状态可以进入TCP_LISTEN状态,处于TCP_LISTEN状态根据后面的逻辑只允许设置sk_max_ack_backlog

接下来看核心部分,有一个TCP_FASTOPEN的概念,这是一个优化点,目前暂时不分析了。直接看inet_csk_listen_start

int inet_csk_listen_start(struct sock *sk, int backlog)
{struct inet_connection_sock *icsk = inet_csk(sk);struct inet_sock *inet = inet_sk(sk);int err = -EADDRINUSE;reqsk_queue_alloc(&icsk->icsk_accept_queue);sk->sk_max_ack_backlog = backlog;sk->sk_ack_backlog = 0;inet_csk_delack_init(sk);inet_sk_state_store(sk, TCP_LISTEN);if (!sk->sk_prot->get_port(sk, inet->inet_num)) {inet->inet_sport = htons(inet->inet_num);sk_dst_reset(sk);err = sk->sk_prot->hash(sk);if (likely(!err))return 0;}inet_sk_set_state(sk, TCP_CLOSE);return err;
}

首先,通过分配

void reqsk_queue_alloc(struct request_sock_queue *queue)
{spin_lock_init(&queue->rskq_lock);spin_lock_init(&queue->fastopenq.lock);queue->fastopenq.rskq_rst_head = NULL;queue->fastopenq.rskq_rst_tail = NULL;queue->fastopenq.qlen = 0;queue->rskq_accept_head = NULL;
}

我们知道bind可以通过某种方式复用(address, port),这主要是针对客户端,由于客户端可以自行指定远端主机地址,因此绑定多个本地地址是没有问题的。但是对于服务端来说,只能确定一个(address, port),因为服务端是被动连接,它不能区分client的连接由那个app处理。实现该唯一绑定的途径就是listen

注意到,listen时再次调用了分配端口号函数,这是因为bind和listen调用之间有一个race window。从上面我们知道对于bind,即使是同一个(address, port)也能绑定成功(如指定了sk->reuse), 但是这两者不能同时listen成功,即不能有多个app同时监听同一个连接。那么当一个进程listen时,需要检测当前sk是不是还是可用的(因为可能在这个race windows中其他的sk已经将状态变成TCP_LISTEN, 或者清除了sk->reuse),总之,再次调用get_port就是要保证TCP_LISTEN状态的sk对端口的独占。

我们看到在调用get_port函数时,先将TCP状态设置为TCP_LISTEN,这是没有问题的,虽然有race window的存在,但是结果对我们并没有影响——get_port要么有一个成功,要么都不成功。

最终将sk加入到listening_hash中

int inet_hash(struct sock *sk)
{int err = 0;if (sk->sk_state != TCP_CLOSE) {local_bh_disable();err = __inet_hash(sk, NULL);local_bh_enable();}return err;
}
int __inet_hash(struct sock *sk, struct sock *osk)
{struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;struct inet_listen_hashbucket *ilb;int err = 0;if (sk->sk_state != TCP_LISTEN) {inet_ehash_nolisten(sk, osk);return 0;}WARN_ON(!sk_unhashed(sk));ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];spin_lock(&ilb->lock);if (sk->sk_reuseport) {err = inet_reuseport_add_sock(sk, ilb);if (err)goto unlock;}if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport &&sk->sk_family == AF_INET6)hlist_add_tail_rcu(&sk->sk_node, &ilb->head);elsehlist_add_head_rcu(&sk->sk_node, &ilb->head);inet_hash2(hashinfo, sk);ilb->count++;sock_set_flag(sk, SOCK_RCU_FREE);sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);
unlock:spin_unlock(&ilb->lock);return err;
}

这里先不分析reuseport的情况了,只给出一张bind的示意图:

注意到inet_hash2(hashinfo, sk),对应hashinfo->lhash2,结构和上面一样,当lhash被初始化的时候,使用(address, port)作为key

四、小结

bind的作用是将(address, port)二元组和socket绑定,Linux实现中使用bhash这个以sport为key的hash函数来保证唯一性。所以当涉及到二元组的冲突检测时,一个条件是如果bhash冲突,那么优先考虑address是否是不同的:这体现在同一个网口的不同address或者不同网口不同address。当二元组完全相同的时候,还可以根据是否指定reuse,reuseport来进一步决定是否能复用。

虽然允许bind多个地址,但想要套接字进入监听状态,就一定要保证进程对socket是独占的,即不再允许复用,如果允许多个socket进入监听状态,那么用户连接来了不能确定那个进程。使用listen来完成这一功能。而listen的也对全连接队列的设置有影响,即指定完成三次握手sock最多的个数。

linux网络子系统分析(三)—— INET连接建立API分析之bind listen相关推荐

  1. Linux网络子系统

    今天分享一篇经典Linux协议栈文章,主要讲解Linux网络子系统,看完相信大家对协议栈又会加深不少,不光可以了解协议栈处理流程,方便定位问题,还可以学习一下怎么去设计一个可扩展的子系统,屏蔽不同层次 ...

  2. 一文搞定 | Linux 网络子系统

    今天分享一篇经典Linux协议栈文章,主要讲解Linux网络子系统,看完相信大家对协议栈又会加深不少,不光可以了解协议栈处理流程,方便定位问题,还可以学习一下怎么去设计一个可扩展的子系统,屏蔽不同层次 ...

  3. linux网络编程(三)select、poll和epoll

    linux网络编程(三)select.poll和epoll 一.为什么会有多路I/O转接服务器? 二.select 三.poll 三.epoll 一.为什么会有多路I/O转接服务器? 为什么会有多路I ...

  4. linux 驱动 (2)---Linux input子系统最清晰、详尽的分析

    Linux input子系统最清晰.详尽的分析 Linux input分析之二:解构input_handler.input_core.input_device 输入输出是用户和产品交互的手段,因此输入 ...

  5. Linux中断子系统(三)之GIC中断处理过程

    Linux中断子系统(三)之GIC中断处理过程 备注:   1. Kernel版本:5.4   2. 使用工具:Source Insight 4.0   3. 参考博客: Linux中断子系统(一)中 ...

  6. linux网络子系统研究:数据收发简略流程图

    Linux网络子系统十分庞大复杂,总想着等自己全部弄明白后再动笔写些笔记,但实在太耗时.后来想通了,先从宏观上掌握大体框图,然后再研究细节. 本文先给出一张自己画的网络数据收发简略流程图,每个路径都可 ...

  7. Linux协议栈:基于ping流程窥探Linux网络子系统,及常用优化方法

    初识 Linux 网络栈及常用优化方法 RToax 2020年9月 初识 Linux 网络栈及常用优化方法 1. 文章简介 基于 ping 流程窥探 Linux 网络子系统,同时介绍各个模块的优化方法 ...

  8. 27.Linux网络编程 掌握三次握手建立连接过程掌握四次握手关闭连接的过程掌握滑动窗口的概念掌握错误处理函数封装实现多进程并发服务器实现多线程并发服务器

    基本概念叫协议 什么叫协议? 协议是一个大家共同遵守的一个规则, 那么在这个网络通信当中,其实就是双方通信和解释数据的一个规则,这个概念 你也不用记,你只要心里明白就可以了, 分层模型, 物数网传会表 ...

  9. Linux网络编程之sockaddr与sockaddr_in,sockaddr_un分析

    sockaddr struct sockaddr { unsigned short sa_family; /* address family, AF_xxx */ char sa_data[14]; ...

  10. 【Linux从青铜到王者】第二十篇:Linux网络基础第三篇之IP协议

    系列文章目录 文章目录 系列文章目录 前言 一.IP协议基本概念 二.IPv4首部 三.网络号和主机号 四.早期地址管理方式 五.CIDR(Classless Interdomain Routing) ...

最新文章

  1. iOS UIKit:UITableView
  2. CentOS下Hive2.0.0单机模式安装详解
  3. node 的path模块中 path.resolve()和path.join()的区别
  4. 【caffe-Windows】以mnist为例lmdb格式数据
  5. php 缩略图增加水印,PHP生成缩略图加图片水印代码
  6. 华为云专家私房课:视频传输技术选型的三大法宝
  7. ETL学习总结(1)——ETL 十大功能特性详解
  8. 【软件测试】白盒测试与黑盒测试的区别(不同)
  9. python︱函数、for、if、_name_、迭代器、防范报错、类定义、装饰器、argparse模块、yield
  10. VC6.0工程设置说明
  11. 买了北京亲子年票但没有小孩的朋友,接下来的一年我都给你安排好啦!!...
  12. 怎么让计算机文件格式显示,怎么显示文件后缀名,详细教您如何让电脑显示文件后缀名...
  13. mysql的innodb引擎_浅谈MYSQL引擎之INNODB引擎
  14. 人生不怕晚,就看敢不敢|优锘科技 X《无尽攀登》专场见面会
  15. hbase 0.98.1集群安装
  16. 3月30日----4月3日二年级课程表
  17. 2020年4月各编程语言占比及各语言创始人发量情况
  18. ORA-01034: ORACLE not available Process ID: 0 Session ID: 0 Serial number: 0
  19. 索尼 电视 android 8,索尼4K液晶电视X9500G采用安卓8.0智能系统 是游戏爱好者的最佳选择...
  20. MySQL No compatible servers were found.You’ll need to cancel this wizard and install one

热门文章

  1. Socket编程--TCP粘包问题
  2. Linq to Sql 聚合查询
  3. sdut 2493 Constructing Roads (图论)
  4. Java NIO框架Netty教程(三) – Object对象传递
  5. 运用Vue Router的进程守护修改单页的title
  6. pyCharm-激活码(2018)
  7. MySQL中索引的长度的限制
  8. 《I'm a Mac:雄狮训练手册》——2.3 账户类型
  9. 在EF4.1的DBContext中实现事务处理(BeginTransaction)和直接执行SQL语句的示例
  10. 【坐在马桶上看算法】算法12:堆——神奇的优先队列(下)