概述

tcp握手完成后,收到数据包后,调用路径为tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_established
在tcp_rcv_established中处理TCP_ESTABLISHED状态的包。 并分为快速路径和慢速路径。
快速路径只进行非常少量的处理。

快速路径:用于处理预期的,理想情况下的数据段,在这种情况下,不会对一些边缘情形进行检测,进而达到快速处理的目的;

慢速路径:用于处理那些非预期的,非理想情况下的数据段,即不满足快速路径的情况下数据段的处理;

首部预测字段格式:首页预测字段,实际上是与TCP首部中的【头部长度+保留字段+标记字段+窗口值】这个32位值完全对应的;进行快速路径判断的时候,只需要将该预测值与TCP首部中的对应部分进行比对即可,具体见tcp_rcv_established;

快速路径(Fast Path)

内核使用tcp_sock中的pred_flags作为判断条件,0表示使用慢速路径,非0则表示快速的判断条件,值为tcp首部的第13-16字节,包含首部长度,标记位,窗口大小。
因此使用pred_flags来检查tcp头就能避免了头部的一些控制信息的处理。

static inline void __tcp_fast_path_on(struct tcp_sock *tp, u32 snd_wnd)
{//tcphdr首部的第13-16字节,包含首部长度,标记位,窗口大小
tp->pred_flags = htonl((tp->tcp_header_len << 26) |
ntohl(TCP_FLAG_ACK) |
snd_wnd);
} static inline void tcp_fast_path_on(struct tcp_sock *tp)
{//snd_wnd已经缩放过,要还原tcp头信息这里缩放回去
__tcp_fast_path_on(tp, tp->snd_wnd >> tp->rx_opt.snd_wscale);
} static inline void tcp_fast_path_check(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
/*
1 没有乱序数据包
2 接收窗口不为0
3 还有接收缓存空间
4 没有紧急数据 */
if (RB_EMPTY_ROOT(&tp->out_of_order_queue) &&
tp->rcv_wnd &&
atomic_read(&sk->sk_rmem_alloc) < sk->sk_rcvbuf &&
!tp->urg_data)
tcp_fast_path_on(tp);
}

__tcp_fast_path_on调用时机

在tcp_finish_connect中没有开启wscale的时候,会调用__tcp_fast_path_on来设置快速路径条件。
因为没有开启wscale,所以不需要调用tcp_fast_path_on。
为什么开启wscale-窗口因子 后就要关闭快速路径呢?
这时候只是客户端进入TCP_ESTABLISHED状态,服务端还在等待客户端最后一次ack才能发送数据。
因此不会收到服务端的数据,也就不用考虑快速路径了。

void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
tcp_set_state(sk, TCP_ESTABLISHED);
...
if (!tp->rx_opt.snd_wscale) //对方没有开启wscale,则开启快速路径
__tcp_fast_path_on(tp, tp->snd_wnd);
else
tp->pred_flags = 0; //目前不会收到服务端数据,不用开启快速路径
if (!sock_flag(sk, SOCK_DEAD)) {
sk->sk_state_change(sk); //唤醒connect
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
}
}

tcp_fast_path_on调用时机

跟tcp_finish_connect一样,服务端进入TCP_ESTABLISHED状态的时候,也要尝试开启快速路径,因此调用tcp_fast_path_on设定快速路径判断条件

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
...
/* step 5: check the ACK field */
acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH |
FLAG_UPDATE_TS_RECENT) > 0;
switch (sk->sk_state) {
case TCP_SYN_RECV: //握手完成时的新建连接的初始状态
if (!acceptable)
return 1;
...
tcp_set_state(sk, TCP_ESTABLISHED);
sk->sk_state_change(sk);
...
tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale; //snd_wnd已经缩放过
tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
if (tp->rx_opt.tstamp_ok)
tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
...
tcp_fast_path_on(tp);
break;
}
...
}

tcp_fast_path_check调用时机

相比起前两个进入TCP_ESTABLISHED就设置pred_flags,因为建立连接前没有其他数据包作为判定依据。
tcp_fast_path_check主要是在连接过程中,有其他数据包作为判定依据的条件下调用:

      • 没有乱序的数据包
      • 接收窗口不为0
      • 接收缓存未用完
      • 非紧急数据

完成紧急数据的读取

紧急数据是由慢速路径处理,需要保持在慢速路径模式直到收完紧急数据,读完后就能检测是否能够开启fast path

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len, int nonblock, int flags, int *addr_len)
{
...
if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) {
tp->urg_data = 0;
tcp_fast_path_check(sk);
}
...
}

在慢速路径收到非乱序包的时候

tcp_data_queue是在慢速路径,对数据部分进行处理。
只有当前包是非乱序包,且接收窗口非0的时候,才能调用tcp_fast_path_check尝试开启快速路径

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
bool fragstolen = false;
int eaten = -1;
if (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq) { //没有数据部分,直接释放
__kfree_skb(skb);
return;
}
...
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) { //非乱序包
if (tcp_receive_window(tp) == 0) //接受窗口满了,不能接受
goto out_of_window;
...
tcp_fast_path_check(sk); //当前是slow path, 尝试开启快速路径
...
}
...
}

当收到新的通告窗口值时

因为pred_flags中包含了窗口值,显然收到新的通告窗口时,需要更新

static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack,
u32 ack_seq)
{
struct tcp_sock *tp = tcp_sk(sk);
int flag = 0;
u32 nwin = ntohs(tcp_hdr(skb)->window);
if (likely(!tcp_hdr(skb)->syn))
nwin <<= tp->rx_opt.snd_wscale;
if (tcp_may_update_window(tp, ack, ack_seq, nwin)) { //更新滑动窗口,或者收到新的窗口通知
flag |= FLAG_WIN_UPDATE;
tcp_update_wl(tp, ack_seq); //snd_wl1=ack_seq
if (tp->snd_wnd != nwin) { //窗口更新
tp->snd_wnd = nwin;
/* Note, it is the only place, where
* fast path is recovered for sending TCP.
*/
tp->pred_flags = 0;
tcp_fast_path_check(sk); //窗口更新了,要重新设置fast path检测条件
...
}
}
...
return flag;
}

快速路径包处理

在tcp_rcv_established中,通过快速路径判断后,

/*
* TCP receive function for the ESTABLISHED state.
*
* It is split into a fast path and a slow path. The fast path is
* disabled when:
* - A zero window was announced from us - zero window probing
* is only handled properly in the slow path.
* - Out of order segments arrived.
* - Urgent data is expected.
* - There is no buffer space left
* - Unexpected TCP flags/window values/header lengths are received
* (detected by checking the TCP header against pred_flags)
* - Data is sent in both directions. Fast path only supports pure senders
* or pure receivers (this means either the sequence number or the ack
* value must stay constant)
* - Unexpected TCP option.
*
* When these conditions are not satisfied it drops into a standard
* receive procedure patterned after RFC793 to handle all cases.
* The first three cases are guaranteed by proper pred_flags setting,
* the rest is checked inline. Fast processing is turned on in
* tcp_data_queue when everything is OK.
*/
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
struct tcp_sock *tp = tcp_sk(sk); skb_mstamp_get(&tp->tcp_mstamp);
if (unlikely(!sk->sk_rx_dst)) /* 路由为空,则重新设置路由 */
inet_csk(sk)->icsk_af_ops->sk_rx_dst_set(sk, skb);
/*
* Header prediction.
* The code loosely follows the one in the famous
* "30 instruction TCP receive" Van Jacobson mail.
*
* Van's trick is to deposit buffers into socket queue
* on a device interrupt, to call tcp_recv function
* on the receive process context and checksum and copy
* the buffer to user space. smart...
*
* Our current scheme is not silly either but we take the
* extra cost of the net_bh soft interrupt processing...
* We do checksum and copy also but from device to kernel.
*/ tp->rx_opt.saw_tstamp = 0; /* pred_flags is 0xS?10 << 16 + snd_wnd
* if header_prediction is to be made
* 'S' will always be tp->tcp_header_len >> 2
* '?' will be 0 for the fast path, otherwise pred_flags is 0 to
* turn it off (when there are holes in the receive
* space for instance)
* PSH flag is ignored.
*/
/* 快路检查&& 序号正确 && ack序号正确 */ if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
int tcp_header_len = tp->tcp_header_len; /* tcp头部长度 */ /* Timestamp header prediction: tcp_header_len
* is automatically equal to th->doff*4 due to pred_flags
* match.
*/ /* Check timestamp */ /* 有时间戳选项 */
if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
/* No? Slow path! /* 解析时间戳选项失败,执行慢路 */
if (!tcp_parse_aligned_timestamp(tp, th))
goto slow_path; /* If PAWS failed, check it more carefully in slow path
*/
/* 序号回转,执行慢路 */
if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
goto slow_path; /* DO NOT update ts_recent here, if checksum fails
* and timestamp was corrupted part, it will result
* in a hung connection since we will drop all
* future packets due to the PAWS test.
*/
} if (len <= tcp_header_len) { /* 无数据 */
/* Bulk data transfer: sender */
if (len == tcp_header_len) {
/* Predicted packet is in window by definition.
* seq == rcv_nxt and rcv_wup <= rcv_nxt.
* Hence, check seq<=rcv_wup reduces to:
*//*
有时间戳选项
&& 所有接收的数据段均确认完毕
保存时间戳
*/
if (tcp_header_len ==
(sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp); /* We know that such packets are checksummed
* on entry.
*/ /* 输入/快速路径ack处理 */
tcp_ack(sk, skb, 0);
__kfree_skb(skb);
/* 检查是否有数据要发送,并检查发送缓冲区大小
收到ack了,给数据包一次发送机会,tcp_push_pending_frames*/
tcp_data_snd_check(sk);
return;
} else { /* Header too small */
/* 数据多小,比头部都小,错包 */
TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
goto discard;
}
} else { /* 有数据 */
int eaten = 0;
bool fragstolen = false;
/* 进程上下文 */
if (tp->ucopy.task == current &&
/* 期待读取的和期待接收的序号一致 */
tp->copied_seq == tp->rcv_nxt &&
len - tcp_header_len <= tp->ucopy.len && /* 数据<= 待读取长度 */
/* 控制块被用户空间锁定 */
sock_owned_by_user(sk)) {
__set_current_state(TASK_RUNNING); /* 设置状态为running??? */
/* 拷贝数据到msghdr */
if (!tcp_copy_to_iovec(sk, skb, tcp_header_len)) {
/* Predicted packet is in window by definition.
* seq == rcv_nxt and rcv_wup <= rcv_nxt.
* Hence, check seq<=rcv_wup reduces to:
*/ /* 有时间戳选项&& 收到的数据段均已确认,更新时间戳 */
if (tcp_header_len ==
(sizeof(struct tcphdr) +
TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp); tcp_rcv_rtt_measure_ts(sk, skb); /* 接收端RTT估算 */ __skb_pull(skb, tcp_header_len);
/* 更新期望接收的序号 */
tcp_rcv_nxt_update(tp, TCP_SKB_CB(skb)->end_seq);
NET_INC_STATS(sock_net(sk),
LINUX_MIB_TCPHPHITSTOUSER);
eaten = 1;
}
}
/* 未拷贝数据到用户空间,或者拷贝失败----没有把数据放到ucopy中 */
if (!eaten) {
if (tcp_checksum_complete(skb))
goto csum_error;
/* skb长度> 预分配长度 */
if ((int)skb->truesize > sk->sk_forward_alloc)
goto step5; /* Predicted packet is in window by definition.
* seq == rcv_nxt and rcv_wup <= rcv_nxt.
* Hence, check seq<=rcv_wup reduces to:
*/ /* 有时间戳选项,且数据均已确认完毕,则更新时间戳 */
if (tcp_header_len ==
(sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)//在收到这个数据包之前,没有发送包也没有收到其他数据包,并且这个包不是乱序包
tcp_store_ts_recent(tp); tcp_rcv_rtt_measure_ts(sk, skb); NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPHPHITS); /* Bulk data transfer: receiver */ /* 数据加入接收队列 添加数据到sk_receive_queue中 */
eaten = tcp_queue_rcv(sk, skb, tcp_header_len,
&fragstolen);
} tcp_event_data_recv(sk, skb);//inet_csk_schedule_ack, 更新rtt
/* 确认序号确认了数据 */
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
/* Well, only one small jumplet in fast path... */
tcp_ack(sk, skb, FLAG_DATA);/* 处理ack */
tcp_data_snd_check(sk); /* 检查是否有数据要发送,需要则发送 */
if (!inet_csk_ack_scheduled(sk)) /* 没有ack要发送 在tcp_event_data_recv标记过,但可能ack已经发出了,就不用检测是否要发送了*/
goto no_ack;
}
/* 检查是否有ack要发送,需要则发送 */
__tcp_ack_snd_check(sk, 0);
no_ack:
if (eaten)
kfree_skb_partial(skb, fragstolen);
sk->sk_data_ready(sk);
return;
}
------------------------------

慢速路径

pred_flags=0或者tcp包头匹配pred_flags失败的时候则为slow path处理

  • 本地接受缓存不足通告0窗口的时候, 因为0窗口探测包需要在慢速路径处理

static u16 tcp_select_window(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
u32 old_win = tp->rcv_wnd;
u32 cur_win = tcp_receive_window(tp); //根据过去接收窗口值,当前还能通告给对方的接收窗口配额
u32 new_win = __tcp_select_window(sk); //根据接收缓存计算出的新窗口值
...
/* If we advertise zero window, disable fast path. */
if (new_win == 0) { //cur_win scale也为0
tp->pred_flags = 0; //开启0窗口, 关闭快速路径
if (old_win)
NET_INC_STATS(sock_net(sk),
LINUX_MIB_TCPTOZEROWINDOWADV);
} else if (old_win == 0) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFROMZEROWINDOWADV);
}
return new_win;
}
  • 收到乱序包的时候

收到乱序包后会调用tcp_data_queue_ofo添加skb到ofo队列中

static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb)
{
...
/* Disable header prediction. */
tp->pred_flags = 0;
...
}
  • 收到紧急数据
static void tcp_check_urg(struct sock *sk, const struct tcphdr *th)
{
...
tp->urg_data = TCP_URG_NOTYET;
tp->urg_seq = ptr;
/* Disable header prediction. */
tp->pred_flags = 0;
}
  • 接受缓存不足
static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb,
unsigned int size)
{
if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || //接收缓存不够
!sk_rmem_schedule(sk, skb, size)) { //并且超过系统设置的最大分配空间了
if (tcp_prune_queue(sk) < 0) //尝试合并ofo/sk_receive_queue来腾出空间
return -1; //还是不够,超过sk_rcvbuf, 返回失败
while (!sk_rmem_schedule(sk, skb, size)) { //再次确认
if (!tcp_prune_ofo_queue(sk)) //释放ofo队列中的数据
return -1;
}
}
return 0;
}
static int tcp_prune_queue(struct sock *sk)
{
...
/* Massive buffer overcommit. */
tp->pred_flags = 0; //接收缓存还是不足,关闭快速路径
return -1;
}

慢速路径包处理

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
...
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags && // 快速路径包头检测
TCP_SKB_CB(skb)->seq == tp->rcv_nxt && // 非乱序包
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) { // 确认的序号是已经发送的包
//快速路径处理
...
} slow_path:
/* 长度错误|| 校验和错误 */
if (len < (th->doff << 2) || tcp_checksum_complete(skb))
goto csum_error;
/* 无ack,无rst,无syn */
if (!th->ack && !th->rst && !th->syn)
goto discard; /*
* Standard slow path.
/* 种种校验
*/ if (!tcp_validate_incoming(sk, skb, th, 1))
return; step5:
/* 处理ack */
if (tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) < 0)
goto discard;
/* 计算rtt */
tcp_rcv_rtt_measure_ts(sk, skb); /* Process urgent data. */
/* 处理紧急数据 */
tcp_urg(sk, skb, th); /* step 7: process the segment text数据段处理 */
tcp_data_queue(sk, skb); tcp_data_snd_check(sk);/* 发送数据检查,有则发送 */
tcp_ack_snd_check(sk);/* 发送ack检查,有则发送 */
return;

tcp_data_queue

  • tcp_data_queue主要把非乱序包copy到ucopy或者sk_receive_queue中,并调用tcp_fast_path_check尝试开启快速路径
  • 对重传包设置dsack,并快速ack回去
  • 对于乱序包,如果包里有部分旧数据也设置dsack,并把乱序包添加到ofo队列中

tcp_data_queue_ofo

在新内核的实现中ofo队列实际上是一颗红黑树。
在tcp_data_queue_ofo中根据序号,查找到合适位置,合并或者添加到rbtree中。
同时设置dsack和sack,准备ack给发送方。

static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack,
u32 ack_seq)
{
struct tcp_sock *tp = tcp_sk(sk);
int flag = 0;
u32 nwin = ntohs(tcp_hdr(skb)->window);
if (likely(!tcp_hdr(skb)->syn))
nwin <<= tp->rx_opt.snd_wscale;
if (tcp_may_update_window(tp, ack, ack_seq, nwin)) { //更新滑动窗口,或者收到新的窗口通知
flag |= FLAG_WIN_UPDATE;
tcp_update_wl(tp, ack_seq); //snd_wl1=ack_seq
if (tp->snd_wnd != nwin) { //窗口更新
tp->snd_wnd = nwin;
/* Note, it is the only place, where
* fast path is recovered for sending TCP.
*/
tp->pred_flags = 0;
tcp_fast_path_check(sk); //窗口更新了,要重新设置fast path检测条件
...
}
}
...
return flag;
}

TCP数据接收及快速路径和慢速路径的更多相关文章

  1. TCP输入 之 快速路径和慢速路径

    概述 快速路径:用于处理预期的,理想情况下的数据段,在这种情况下,不会对一些边缘情形进行检测,进而达到快速处理的目的: 慢速路径:用于处理那些非预期的,非理想情况下的数据段,即不满足快速路径的情况下数 ...

  2. tcp输入数据 慢速路径处理 && oob数据 接收 && 数据接收 tcp_data_queue

    大致的处理过程 TCP的接收流程:在tcp_v4_do_rcv中的相关处理(网卡收到报文触发)中,会首先通过tcp_check_urg设置tcp_sock的urg_data为TCP_URG_NOTYE ...

  3. Java TCP异步数据接收

    之前一直采用.Net编写服务端程序,最近需要切换到Linux平台下,于是尝试采用Java编写数据服务器.TCP异步连接在C#中很容易实现,网上也有很多可供参考的代码.但Java异步TCP的参考资料较少 ...

  4. 网络编程.TCP分块接收数据(AIO)(IOCP)

    1.前提:每次投递的接收缓冲区 在它返回后 就不再用它进行2次投递了 于是 接收缓冲区 在返回的时候,数据都是 从接收缓冲区的偏移[0]处开始填充的,且 接收缓冲区 可能被填满 也可能未被填满. 1. ...

  5. 终端参数上报后,平台通过tcp协议接收到相应数据并处理。

    终端将终端参数以json格式的数据发送至平台.终端上电后上报,可以不认证直接上报. 实现流程如下. 1.设置终端参数上报的协议类型,例如:0x0000. public static final int ...

  6. [置顶] NS2中对TCP数据包和ACK包的TCP Sink类的主要实现代码详尽剖析--吐血放送

    NS2中对TCP数据包和ACK包的TCP Sink类的主要实现代码详尽剖析,限于个人水平,如有错误请留言指出! TcpSink类的recv()方法: void TcpSink::recv(Packet ...

  7. TCP数据包结构

    源端口号( 16 位):它(连同源主机 IP 地址)标识源主机的一个应用进程.目的端口号( 16 位):它(连同目的主机 IP 地址)标识目的主机的一个应用进程.这两个值加上 IP 报头中的源主机 I ...

  8. s6-4 TCP 数据段

    传输控制协议  TCP (Transmission Control Protocol) 是专门为了在不可靠的互联网络上提供可靠的端到端字节流而设计的  TCP必须动态地适应不同的拓扑.带宽.延迟. ...

  9. Wireshark抓包工具--TCP数据包seq ack等解读

    1.Wireshark的数据包详情窗口,如果是用中括号[]括起来的,表示注释,在数据包中不占字节 2.在二进制窗口中,如“DD 3D”,表示两个字节,一个字节8位 3.TCP数据包中,seq表示这个包 ...

随机推荐

  1. 云服务器、euleros系统自动断开连接解决方案

    我这里的云服务器,网上查的修改sshd.config文件并不有效 我提供另一种方法解决这个问题: vim /etc/profile 再最底部新增 export TMOUT=6000 #6000代表60 ...

  2. MeteoInfo脚本示例:读取FY3A AOD HDF文件

    FY3A卫星有AOD产品数据,HDF格式,这里示例用MeteoInfo脚本程序读取和显示该类数据. 脚本程序如下: #----------------------------------------- ...

  3. docker查看ip

    docker查看容器的网络ip   docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' conta ...

  4. centos8上配置openssh的安全

    一,openssh服务版本号的查看 1,查看当前sshd的版本号 : [root@yjweb ~]# sshd --help unknown option -- - OpenSSH_7.8p1, Op ...

  5. centos8平台使用pstree查看进程树

    一,pstree用途 Linux pstree命令将所有行程以树状图显示,树状图将会以 pid (如果有指定) 或是以 systemd 这个基本行程为根 (root) 说明:centos6及更旧版本为 ...

  6. LeetCode 45跳跃游戏&46全排列

    原创公众号:bigsai,回复进群加入力扣打卡群. 昨日打卡:LeetCode 42字符串相乘&43通配符匹配 跳跃游戏 题目描述: 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中 ...

  7. poj1837 01背包(雾

    Description A train has a locomotive that pulls the train with its many passenger coaches. If the lo ...

  8. Spring Boot注解与资源文件配置

    date: 2018-11-18 16:57:17 updated: 2018-11-18 16:57:17 1.不需要多余的配置文件信息 application.properties mybatis ...

  9. Jupyter Notebook使用教程

    关于安装我就不说了,可以参考知乎https://zhuanlan.zhihu.com/p/33105153(总结的很全面) 首先打开Jupyter Notebook后,新建notebook:点击右上角 ...

  10. 安装Redis(Windows版本&Linux版本)

    1.版本: Redis官网上有Linux版本,Redis官网:https://redis.io/download GitHub上有Windows版本,地址是:https://github.com/Mi ...