查看“︁编写一个TCP/IP栈3-TCP基础与握手”︁的源代码
←
编写一个TCP/IP栈3-TCP基础与握手
跳转到导航
跳转到搜索
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
如今我们的用户空间TCP/IP堆栈在以太网和IPv4上的实现已降至最低,现在是时候研究令人棘手的传输控制协议(TCP)了。 TCP运行在OSI网络的第4层负责端对端传输操作, 负责恢复数据包递送中的错误连接和故障。事实上,TCP 是互联网的主力,如今几乎在所有计算机网络中都提供了可靠的通信服务。 TCP 并非全新的协议——第一个规范于1974年问世。从那以后,很多事情都发生了变化,也获得了许多扩展和修正。 本文将深入探讨TCP背后的基本理论,并尝试为其设计提供动力。此外,我们将查看TCP头部并讨论建立连接(TCP握手)。作为最后一步,我们将展示TCP在网络栈中的首个功能。 = 可靠性机制 = 可靠地发送数据的问题看似肤浅,但其实际实现却涉及问题。主要涉及数据报式网络中的错误修复问题: * 发送方应等待接收方确认多长时间? * 如果接收方无法按发送速度处理数据怎么办? * 如果中间的网络(例如路由器)无法像发送数据时那样快速处理数据,该怎么办? 在所有情况下,数据包交换网络的潜在危险都适用——接收方的确认可能会被损坏,甚至在传输过程中丢失,从而使发送方陷入棘手的境地。 为解决这些问题,可以使用多种机制。最常见的就是滑动窗口技术,即双方对传输的数据进行记录。窗口数据被视为顺序(如数组的切片),该窗口在数据被双方处理(并确认)时向前滑动: [[文件:屏幕截图 2025-12-25 222529.png|居中|缩略图|630x630像素]] 使用这种滑动窗口的便利之处在于,它能缓解流量控制。当接收端无法以发送速度快速处理数据时,需要进行流量控制。在这种情况下,滑动窗口的大小将被协商以降低,从而限制发送方的字节流输出。 拥塞控制有助于发送方和接收方之间的网络堆栈不受拥堵影响。有两种通用方法:在显式版本中,协议有一个字段,用于向发送方明确告知拥塞状态。在隐式版本中,发送方会尝试猜测网络何时出现拥堵,并限制其输出。总体而言,拥塞控制是一个复杂且反复出现的网络问题,相关研究至今仍在进行。 = TCP基础 = TCP 中的实现机制比其他协议(如 UDP 和 IP)要多得多。''即TCP是一种面向连接的协议'',这意味着作为第一步,在通信双方之间建立统一通信通道。双方积极维护这一联系:建立联系(握手),向对方告知数据状况等可能的问题。 TCP的另一个重要特性是它是一种流式协议。与UDP不同,TCP 无法保证应用程序在发送和接收时能够稳定地“记录”数据。TCP 实现必须缓冲数据,当数据包丢失、重新排序或损坏时,TCP 必须等待并组织缓冲区中的数据。只有当数据被认定为完整时,TCP 才能将数据交给应用程序的套接字。 当TCP以数据流的形式运行时,来自该流的“块”必须转换为IP可以承载的数据包。''这称为包化'',其中TCP头块包含流中当前索引的序列号。这还具有方便的属性,即流可以分成许多可变大小的段,而TCP则知道如何重新集成它们。 与IP类似,TCP也会检查消息是否具有完整性。这是通过与IP相同的校验和算法实现的,但增加了更多细节。主要情况下,校验和是端到端的,这意味着报文头和数据都包含在校验中。此外,还包含一个由IP头编造的伪头。 如果TCP实现接收损坏的片段,就会丢弃它们,并且不会通知发送方。此错误由发送方设置的定时器进行纠正,如果接收方从未确认该片段,则可用于重新传输片段。 TCP ''full-duplex''是一个全双工系统,意味着流量可以双向同时流动。通信端必须将两个方向数据顺序保存在内存中。更具体地说,TCP 通过在发送的数据段中包含对反向流量的确认信息来减少流量占用。 从本质上讲,数据流的排序是 TCP 的主要原则。然而,保持数据流同步并非易事。 = TCP 报头格式 = 接下来,我们将定义消息头并描述其字段。TCP 消息头看似简单,但实际上包含大量关于通信状态的信息。 TCP 报头大小为 20 字节: [[文件:屏幕截图 2025-12-25 223342.png|居中|缩略图|558x558像素]] ''源端口''和''目标端口''字段用于建立与主机之间的多个连接。具体来说,伯克利套接字是应用程序绑定到 TCP 网络协议栈的常用接口。网络协议栈通过端口来确定流量的路由。由于这些字段为 16 位,因此端口值的范围为 0 到 65535。 由于数据流中的每个字节都已编号,因此''序列号''代表 TCP 段的窗口索引。在握手过程中,该序列号包含''初始序列号'' (ISN)。 ''确认号''包含发送方预期接收的下一个字节的窗口索引。握手完成后,ACK 字段必须始终填充。 ''头部长度'' (HL) 字段以 32 位表示头部的长度。 接下来,会显示几个标志位。前 4 位( ''rsvd'' )未使用。 # ''拥塞窗口减少'' (C) 用于通知发送方降低发送速率。 # ''ECN 回显'' (E) 表示发送方已收到拥塞通知。 # ''紧急指针'' (U) 表示该段包含优先数据。 # ''ACK'' (A)字段用于传递 TCP 握手的状态。在连接的剩余时间内,它将一直保持开启状态。 # ''PSH'' (P)用于指示接收方应尽快将数据“推送”到应用程序。 # ''RST'' (R)重置 TCP 连接。 # ''SYN'' (S) 用于在初始握手中同步序列号。 # ''FIN'' (F)表示发送方已完成数据发送。 ''窗口大小''字段用于指定窗口大小。换句话说,它表示接收方愿意接收的字节数。由于这是一个 16 位字段,因此最大窗口大小为 65,535 字节。 ''TCP 校验和''字段用于验证 TCP 数据段的完整性。其算法与互联网协议相同,但输入数据段除了包含 TCP 数据外,还包含来自 IP 数据报的伪头部。 当设置了 U 标志时,会使用''紧急指针'' 。该指针指示数据流中紧急数据的位置。 在邮件头之后,可以提供多个选项。例如, ''最大段大小'' (MSS) 就是其中之一,发送方可以通过它告知接收方数据段的最大长度。 在列出所有可选方案之后,才是实际数据。然而,这些数据并非必需。例如,握手过程仅需 TCP 头部即可完成。 = TCP握手 = TCP 连接通常经历以下几个阶段:连接建立(握手)、数据传输和连接关闭。下图展示了 TCP 的典型握手过程: [[文件:屏幕截图 2025-12-25 224135.png|居中|缩略图|551x551像素]] # 主机 A 的套接字处于关闭状态,这意味着它不接受连接。相反,主机 B 绑定到特定端口的套接字正在监听新的连接。 # 主机 A 打算与主机 B 建立连接。因此,A 构造了一个 TCP 段,该段的 SYN 标志已设置,并且序列字段的值也已填充为 (100)。 # 主机 B 以一个设置了 SYN 和 ACK 字段的 TCP 段进行响应,并通过将 A 的序列号加 1 来确认(ACK=101)。同样,B 也生成一个序列号(300)。 # 三次握手以连接请求发起方 (A) 发送的确认 (ACK) 结束。确认字段反映了主机接下来期望从另一方收到的序列号。 # 数据开始流动,主要是因为双方都确认了对方的数据序号。 这是建立 TCP 连接的常见场景。然而,由此会产生几个问题: # 初始序列号是如何选择的? # 如果双方同时向对方提出连接请求呢? # 如果某些环节延迟一段时间甚至无限期延迟怎么办? ''初始序列号'' (ISN) 由通信双方在首次连接时独立选择。由于它是识别连接的关键部分,因此必须选择尽可能唯一且不易猜测的序列号。事实上, ''TCP 序列号攻击'' 指的是攻击者可以复制 TCP 连接并有效地向目标发送数据,从而冒充可信主机的情况。 The original specification suggests that the ISN is chosen by a counter that increments every 4 microseconds. This, however, can be guessed by an attacker. In reality, modern networking stacks generate the ISN by more complicated methods. 原始规范表明,ISN 由一个每 4 微秒递增一次的计数器生成。然而,攻击者可以猜到这一点。实际上,现代网络协议栈采用更复杂的方法生成 ISN。 当两端都收到对方的连接请求(SYN)时,这种情况称为''同时打开'' 。TCP 握手过程中会通过额外的消息交换来解决这个问题:双方都发送一个 ACK(彼此并不知道对方是否也发送了 ACK),然后双方都发送 SYN-ACK 来确认请求。之后,数据传输开始。 最后,TCP 实现必须包含一个计时器,用于判断何时放弃建立连接。系统会尝试重新建立连接,通常采用指数退避算法,但一旦达到最大重试次数或时间阈值,连接就会被视为已断开。 = TCP 选项 = TCP 报头段的最后一个字段保留用于存储 TCP 选项。最初的规范提供了三个选项,但后来的规范又增加了更多选项。接下来,我们将介绍最常用的选项。 ''最大段大小'' (MSS) 选项指定 TCP 实现能够接收的最大 TCP 段大小。在 IPv4 中,该值的典型值为 1460 字节。 ''选择性确认'' (SACK) 选项优化了传输过程中大量数据包丢失、接收端数据窗口出现“空洞”的情况。为了弥补由此导致的吞吐量下降,TCP 实现可以通过 SACK 通知发送方哪些数据包未收到。因此,与累积确认机制相比,发送方能够以更直接的方式获取数据状态信息。 ''窗口缩放''选项可以增大原本受限的 16 位窗口大小。具体来说,如果双方都在握手段中包含此选项,则窗口大小将乘以该缩放比例。更大的窗口大小对于大批量数据传输尤为重要。 ''时间戳''选项允许发送方在 TCP 数据段中插入时间戳,该时间戳可用于计算每个 ACK 数据段的往返时间 (RTT)。此信息随后可用于计算 TCP 重传超时时间。 = 测试TCP握手 = 现在我们已经模拟了TCP握手程序,并且它有效地监听了每个端口,接下来我们来测试一下: [[文件:屏幕截图 2025-12-25 225216.png|居中|缩略图|627x627像素]] 由于nmap进行SYN扫描(它只等待 SYN-ACK 来判断目标端口是否打开),因此很容易误以为我们只需返回一个SYN-ACK TCP段即可在端口上监听应用程序。 = 关键实现 = <syntaxhighlight lang="c" line="1"> struct net_ops tcp_ops = { .alloc_sock = &tcp_alloc_sock, .init = &tcp_v4_init_sock, .connect = &tcp_v4_connect, .disconnect = &tcp_disconnect, .write = &tcp_write, .read = &tcp_read, .recv_notify = &tcp_recv_notify, .close = &tcp_close, .abort = &tcp_abort, }; void tcp_init() { } static void tcp_init_segment(struct tcphdr *th, struct iphdr *ih, struct sk_buff *skb) { th->sport = ntohs(th->sport); th->dport = ntohs(th->dport); th->seq = ntohl(th->seq); th->ack_seq = ntohl(th->ack_seq); th->win = ntohs(th->win); th->csum = ntohs(th->csum); th->urp = ntohs(th->urp); skb->seq = th->seq; skb->dlen = ip_len(ih) - tcp_hlen(th); skb->len = skb->dlen + th->syn + th->fin; skb->end_seq = skb->seq + skb->dlen; skb->payload = th->data; } static void tcp_clear_queues(struct tcp_sock *tsk) { skb_queue_free(&tsk->ofo_queue); } void tcp_in(struct sk_buff *skb) { struct sock *sk; struct iphdr *iph; struct tcphdr *th; iph = ip_hdr(skb); th = (struct tcphdr*) iph->data; tcp_init_segment(th, iph, skb); sk = inet_lookup(skb, th->sport, th->dport); if (sk == NULL) { print_err("No TCP socket for sport %d dport %d\n", th->sport, th->dport); free_skb(skb); return; } socket_wr_acquire(sk->sock); tcp_in_dbg(th, sk, skb); /* if (tcp_checksum(iph, th) != 0) { */ /* goto discard; */ /* } */ tcp_input_state(sk, th, skb); socket_release(sk->sock); } int tcp_udp_checksum(uint32_t saddr, uint32_t daddr, uint8_t proto, uint8_t *data, uint16_t len) { uint32_t sum = 0; sum += saddr; sum += daddr; sum += htons(proto); sum += htons(len); return checksum(data, len, sum); } int tcp_v4_checksum(struct sk_buff *skb, uint32_t saddr, uint32_t daddr) { return tcp_udp_checksum(saddr, daddr, IP_TCP, skb->data, skb->len); } struct sock *tcp_alloc_sock() { struct tcp_sock *tsk = malloc(sizeof(struct tcp_sock)); memset(tsk, 0, sizeof(struct tcp_sock)); tsk->sk.state = TCP_CLOSE; tsk->sackok = 1; tsk->rmss = 1460; // Default to 536 as per spec tsk->smss = 536; skb_queue_init(&tsk->ofo_queue); return (struct sock *)tsk; } int tcp_v4_init_sock(struct sock *sk) { tcp_init_sock(sk); return 0; } int tcp_init_sock(struct sock *sk) { return 0; } void __tcp_set_state(struct sock *sk, uint32_t state) { sk->state = state; } static uint16_t generate_port() { /* TODO: Generate a proper port */ static int port = 40000; pthread_rwlock_wrlock(&tcplock); int copy = ++port + (timer_get_tick() % 10000); pthread_rwlock_unlock(&tcplock); return copy; } int generate_iss() { /* TODO: Generate a proper ISS */ return (int)time(NULL) * rand(); } int tcp_v4_connect(struct sock *sk, const struct sockaddr *addr, int addrlen, int flags) { uint16_t dport = ((struct sockaddr_in *)addr)->sin_port; uint32_t daddr = ((struct sockaddr_in *)addr)->sin_addr.s_addr; sk->dport = ntohs(dport); sk->sport = generate_port(); sk->daddr = ntohl(daddr); /* TODO: Do not hardcode lvl-ip local interface */ sk->saddr = parse_ipv4_string("10.0.0.4"); return tcp_connect(sk); } int tcp_disconnect(struct sock *sk, int flags) { return 0; } int tcp_write(struct sock *sk, const void *buf, int len) { struct tcp_sock *tsk = tcp_sk(sk); int ret = sk->err; if (ret != 0) goto out; switch (sk->state) { case TCP_ESTABLISHED: case TCP_CLOSE_WAIT: break; default: ret = -EBADF; goto out; } return tcp_send(tsk, buf, len); out: return ret; } int tcp_read(struct sock *sk, void *buf, int len) { struct tcp_sock *tsk = tcp_sk(sk); int ret = -1; switch (sk->state) { case TCP_CLOSE: ret = -EBADF; goto out; case TCP_LISTEN: case TCP_SYN_SENT: case TCP_SYN_RECEIVED: /* Queue for processing after entering ESTABLISHED state. If there is no room to queue this request, respond with "error: insufficient resources". */ case TCP_ESTABLISHED: case TCP_FIN_WAIT_1: case TCP_FIN_WAIT_2: /* If insufficient incoming segments are queued to satisfy the request, queue the request. */ break; case TCP_CLOSE_WAIT: /* If no text is awaiting delivery, the RECEIVE will get a "error: connection closing" response. Otherwise, any remaining text can be used to satisfy the RECEIVE. */ if (!skb_queue_empty(&tsk->sk.receive_queue)) break; if (tsk->flags & TCP_FIN) { tsk->flags &= ~TCP_FIN; return 0; } break; case TCP_CLOSING: case TCP_LAST_ACK: case TCP_TIME_WAIT: ret = sk->err; goto out; default: goto out; } return tcp_receive(tsk, buf, len); out: return ret; } int tcp_recv_notify(struct sock *sk) { if (&(sk->recv_wait)) { return wait_wakeup(&sk->recv_wait); } // No recv wait lock return -1; } int tcp_close(struct sock *sk) { switch (sk->state) { case TCP_CLOSE: case TCP_CLOSING: case TCP_LAST_ACK: case TCP_TIME_WAIT: case TCP_FIN_WAIT_1: case TCP_FIN_WAIT_2: /* Respond with "error: connection closing". */ sk->err = -EBADF; return -1; case TCP_LISTEN: case TCP_SYN_SENT: case TCP_SYN_RECEIVED: return tcp_done(sk); case TCP_ESTABLISHED: /* Queue this until all preceding SENDs have been segmentized, then form a FIN segment and send it. In any case, enter FIN-WAIT-1 state. */ tcp_set_state(sk, TCP_FIN_WAIT_1); tcp_queue_fin(sk); break; case TCP_CLOSE_WAIT: /* Queue this request until all preceding SENDs have been segmentized; then send a FIN segment, enter LAST_ACK state. */ tcp_queue_fin(sk); break; default: print_err("Unknown TCP state for close\n"); return -1; } return 0; } int tcp_abort(struct sock *sk) { struct tcp_sock *tsk = tcp_sk(sk); tcp_send_reset(tsk); return tcp_done(sk); } static int tcp_free(struct sock *sk) { struct tcp_sock *tsk = tcp_sk(sk); tcp_clear_timers(sk); tcp_clear_queues(tsk); wait_wakeup(&sk->sock->sleep); return 0; } int tcp_done(struct sock *sk) { tcp_set_state(sk, TCP_CLOSING); tcp_free(sk); return socket_delete(sk->sock); } void tcp_clear_timers(struct sock *sk) { struct tcp_sock *tsk = tcp_sk(sk); tcp_stop_rto_timer(tsk); tcp_stop_delack_timer(tsk); timer_cancel(tsk->keepalive); tsk->keepalive = NULL; timer_cancel(tsk->linger); tsk->linger = NULL; } void tcp_stop_rto_timer(struct tcp_sock *tsk) { if (tsk) { timer_cancel(tsk->retransmit); tsk->retransmit = NULL; tsk->backoff = 0; } } void tcp_release_rto_timer(struct tcp_sock *tsk) { if (tsk) { timer_release(tsk->retransmit); tsk->retransmit = NULL; } } void tcp_stop_delack_timer(struct tcp_sock *tsk) { timer_cancel(tsk->delack); tsk->delack = NULL; } void tcp_release_delack_timer(struct tcp_sock *tsk) { timer_release(tsk->delack); tsk->delack = NULL; } void tcp_handle_fin_state(struct sock *sk) { switch (sk->state) { case TCP_CLOSE_WAIT: tcp_set_state(sk, TCP_LAST_ACK); break; case TCP_ESTABLISHED: tcp_set_state(sk, TCP_FIN_WAIT_1); break; } } static void *tcp_linger(void *arg) { struct sock *sk = (struct sock *) arg; socket_wr_acquire(sk->sock); struct tcp_sock *tsk = tcp_sk(sk); tcpsock_dbg("TCP time-wait timeout, freeing TCB", sk); timer_cancel(tsk->linger); tsk->linger = NULL; tcp_done(sk); socket_release(sk->sock); return NULL; } static void *tcp_user_timeout(void *arg) { struct sock *sk = (struct sock *) arg; socket_wr_acquire(sk->sock); struct tcp_sock *tsk = tcp_sk(sk); tcpsock_dbg("TCP user timeout, freeing TCB and aborting conn", sk); timer_cancel(tsk->linger); tsk->linger = NULL; tcp_abort(sk); socket_release(sk->sock); return NULL; } void tcp_enter_time_wait(struct sock *sk) { struct tcp_sock *tsk = tcp_sk(sk); tcp_set_state(sk, TCP_TIME_WAIT); tcp_clear_timers(sk); /* RFC793 arbitrarily defines MSL to be 2 minutes */ tsk->linger = timer_add(TCP_2MSL, &tcp_linger, sk); } void tcp_rearm_user_timeout(struct sock *sk) { struct tcp_sock *tsk = tcp_sk(sk); if (sk->state == TCP_TIME_WAIT) return; timer_cancel(tsk->linger); /* RFC793 set user timeout */ tsk->linger = timer_add(TCP_USER_TIMEOUT, &tcp_user_timeout, sk); } void tcp_rtt(struct tcp_sock *tsk) { if (tsk->backoff > 0 || !tsk->retransmit) { // Karn's Algorithm: Don't measure retransmissions return; } int r = timer_get_tick() - (tsk->retransmit->expires - tsk->rto); if (r < 0) return; if (!tsk->srtt) { /* RFC6298 2.2 first measurement is made */ tsk->srtt = r; tsk->rttvar = r / 2; } else { /* RFC6298 2.3 a subsequent measurement is made */ double beta = 0.25; double alpha = 0.125; tsk->rttvar = (1 - beta) * tsk->rttvar + beta * abs(tsk->srtt - r); tsk->srtt = (1 - alpha) * tsk->srtt + alpha * r; } int k = 4 * tsk->rttvar; /* RFC6298 says RTO should be at least 1 second. Linux uses 200ms */ if (k < 200) k = 200; tsk->rto = tsk->srtt + k; } int tcp_calculate_sacks(struct tcp_sock *tsk) { struct tcp_sack_block *sb = &tsk->sacks[tsk->sacklen]; sb->left = 0; sb->right = 0; struct sk_buff *next; struct list_head *item, *tmp; list_for_each_safe(item, tmp, &tsk->ofo_queue.head) { next = list_entry(item, struct sk_buff, list); if (sb->left == 0) { sb->left = next->seq; tsk->sacklen++; } if (sb->right == 0) sb->right = next->end_seq; else if (sb->right == next->seq) sb->right = next->end_seq; else { if (tsk->sacklen >= tsk->sacks_allowed) break; sb = &tsk->sacks[tsk->sacklen]; sb->left = next->seq; sb->right = next->end_seq; tsk->sacklen++; } } return 0; } </syntaxhighlight>
返回
编写一个TCP/IP栈3-TCP基础与握手
。
导航菜单
个人工具
登录
命名空间
页面
讨论
大陆简体
查看
阅读
查看源代码
查看历史
更多
搜索
导航
首页
最近更改
随机页面
特殊页面
工具
链入页面
相关更改
页面信息