0%

TCP实验报告

实验目的

给定网络拓扑和节点配置,节点实现TCP最基本的连接管理和数据传输功能,使得节点之间能够在无丢包网络环境中传输数据。

实验拓扑

实验内容

  1. 运行给定网络拓扑(tcp_topo.py)

    1
    sudo python2 topo/tcp_topo.py
  2. 在节点h1上执行TCP程序

    执行脚本,禁止协议栈的相应功能,并在h1上运行TCP协议栈的服务器模式。

    1
    2
    3
    4
    5
    6
    ./scripts/disable_arp.sh
    ./scripts/disable_icmp.sh
    ./scripts/disable_ip_forward.sh
    ./scripts/disable_tcp_rst.sh
    export LD_LIBRARY_PATH=.;
    ./tcp_stack server 10001
  3. 在节点h2上执行TCP程序

    执行脚本,禁止协议栈的相应功能,并在h2上运行TCP协议栈的客户端模式,连接h1并从h1正确获得数据。

    1
    2
    3
    4
    5
    6
    ./scripts/disable_arp.sh
    ./scripts/disable_icmp.sh
    ./scripts/disable_ip_forward.sh
    ./scripts/disable_tcp_rst.sh
    export LD_LIBRARY_PATH=.;
    ./tcp_stack client 10.0.0.1 10001

主要数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//socket 对象
struct tcp_sock {
// sk_ip, sk_sport, sk_sip, sk_dport are the 4-tuple that represents a
// connection
struct sock_addr local;
struct sock_addr peer;
#define sk_sip local.ip
#define sk_sport local.port
#define sk_dip peer.ip
#define sk_dport peer.port

// pointer to parent tcp sock, a tcp sock which bind and listen to a port
// is the parent of tcp socks when *accept* a connection request
struct tcp_sock *parent;

// represents the number that the tcp sock is referred, if this number
// decreased to zero, the tcp sock should be released
int ref_cnt;

// hash_list is used to hash tcp sock into listen_table or established_table,
// bind_hash_list is used to hash into bind_table
struct list_head hash_list;
struct list_head bind_hash_list;

// when a passively opened tcp sock receives a SYN packet, it mallocs a child
// tcp sock to serve the incoming connection, which is pending in the
// listen_queue of parent tcp sock
struct list_head listen_queue;
// when receiving the last packet (ACK) of the 3-way handshake, the tcp sock
// in listen_queue will be moved into accept_queue, waiting for *accept* by
// parent tcp sock
struct list_head accept_queue;


#define TCP_MAX_BACKLOG 128
// the number of pending tcp sock in accept_queue
int accept_backlog;
// the maximum number of pending tcp sock in accept_queue
int backlog;

// the list node used to link listen_queue or accept_queue of parent tcp sock
struct list_head list;
// tcp timer used during TCP_TIME_WAIT state
struct tcp_timer timewait;

// async waiting structure of *connect*, *accept*, *recv*, and *send*
struct synch_wait *wait_connect;
struct synch_wait *wait_accept;
struct synch_wait *wait_recv;
struct synch_wait *wait_send;

// receiving buffer
struct ring_buffer *rcv_buf;

// tcp state, see enum tcp_state in tcp.h
int state;

// initial sending sequence number
u32 iss;

// the highest byte that is ACKed by peer
u32 snd_una;
// the highest byte sent
u32 snd_nxt;

// the highest byte ACKed by itself (i.e. the byte expected to receive next)
u32 rcv_nxt;

// the size of sending window (i.e. the receiving window advertised by peer)
u16 snd_wnd;
// the size of receiving window (advertised by tcp sock itself)
u16 rcv_wnd;
};

TCP状态转移图

TCP建立连接状态转移图

TCP断开连接状态转移图

实验过程

本实验老师已经给了框架,只需要将几个函数的功能实现就可以了。所以这里只介绍每个函数的功能,具体实现见源代码。

代码地址:https://gitee.com/873314461/tcp_stack

tcp_state_listen

这个函数是用来处理处于TCP_LISTEN状态的socket。

处于监听状态的ServerSocket,在收到第一个SYN报文时,将会:

  1. 生成一个新的子socket,并将子socket的parent指向原来的ServerSocket。
  2. 将子socket插入到原ServerSocket的listen_queue队列中。
  3. 用子socket发送SYN|ACK报文。(TCP连接的第二次握手)
  4. 将子socket的状态置为TCP_SYN_RECV,并将子socket插入到established_table中——因为这时区分TCP连接的四元组(saddr, daddr, sport, dport)已经确定了。

tcp_state_syn_sent

这个函数是用来处理处于TCP_SYN_SENT状态的socket。当客户端发出第一个SYN报文之后,就会处于这个状态。

在这个状态,需要处理一下内容:

  1. 判断这个收到的报文,是否是SYN|ACK报文,如果不是,回复RST报文并结束。
  2. 如果收到的报文正常,则需要回复ACK报文。(TCP第三次握手)
  3. 将socket转换为TCP_ESTABLISHED状态,并加入到established_table中。
  4. 唤醒阻塞于tcp_sock_connect的进程。

tcp_state_syn_recv

这个函数是用来处理处于TCP_SYN_RECV状态的socket。当服务器收到客户端发来的第一个SYN报文,并且回复了SYN|ACK报文后,处于这个状态。

在这个状态将要:

  1. 将socket从原ServerSocket的listen_queue队列中移除。
  2. 将socket插入到原ServerSocket的accept_queue队列中。
  3. 将socket的状态置为TCP_ESTABLISHED。
  4. 唤醒处于tcp_accpet的进程,即:唤醒原ServerSocket的wait_accept对象。

tcp_recv_data

这个函数用来接收报文发送的数据,并将数据送到socket的环形缓冲区(rcv_buf)中。

  1. 将报文的负载写入到环形缓冲区中。
  2. 唤醒正在等待接收消息的进程,即:socket的wait_recv对象。

tcp_process

这个函数是处理接收到的TCP报文的主函数。根据各种不同的状态,选择合适的策略,处理报文。在理解这个函数过程中,请全程参考TCP状态转移图。

  1. 如果socket处于TCP_CLOSED状态,则交给tcp_state_closed函数处理。
  2. 如果socket处于TCP_LISTEN状态,则交给tcp_state_listen函数处理。
  3. 如果socket处于TCP_SYN_SENT状态,则交给tcp_state_syn_sent函数处理。
  4. 调用is_tcp_seq_valid函数验证报文的序列号是否正确,若不正确,丢弃这个包
  5. 若RST为1,则关闭这个连接,并释放这个连接的资源。因为RST表示连接重置,这个连接已经不再使用了。
  6. 若SYN为1,则回复RST,并关闭这个连接。因为正常的SYN连接,已经在第2、3步处理过了。
  7. 若ACK为0,则丢弃这个包。因为除了第一个SYN报文之外,所有的报文的ACK都应该为1。(需要实验验证一下)
  8. 如果socket处于TCP_SYN_RECV状态,调用tcp_state_syn_recv函数处理。
  9. 如果socket处于TCP_FIN_WAIT_1,则将socket状态转换到TCP_FIN_WAIT_2状态。
  10. 如果socket处于TCP_LAST_ACK,则将socket状态转换到TCP_CLOSED状态,并从established_table中移除,并释放资源。
  11. 如果socket处于TCP_FIN_WAIT_2状态,则需要判断收到的报文是否是FIN|ACK报文:若是,则回复ACK报文,并启动定时器;否则,丢弃这个包。
  12. 如果接收的报文确认了发送的报文,则更新窗口大小。
  13. 若报文有负载数据,则接收数据。
  14. 若FIN为1,则将socket状态转变到TCP_CLOSE_WAIT状态,并回复ACK报文,表名接收到FIN报文。发送FIN|ACK报文,请求关闭这个连接,并将socket状态置为TCP_LAST_ACK。
  15. 若前面没有返回,则应当发送ACK报文,确认当前接收的报文。

PS: 本函数的逻辑可能存在问题,需要再考虑一下。

alloc_tcp_sock

这个函数是创建一个tcp_sock对象,但是根据函数名,我认为这个函数应当只申请内存,并不进行初始化,但是,我在头文件中没有找到初始化函数,因此只能在这个函数中,对tcp_sock对象进行初始化。

  1. 申请内存。
  2. 初始化两个队列:listen_queue和accept_queue。
  3. 初始化四个等待对象:wait_accept、wait_connect、wait_recv和wait_send
  4. 初始化发送序列号以及发送、接收窗口大小。
  5. 初始化环形缓冲区。
  6. 设置socket状态为TCP_CLOSED。

tcp_sock_lookup_established

这个函数从从established_table中查询符合要求的socket,并返回。

查询过程如下:

  1. 用四元组(saddr, daddr, sport, dport)计算hash。
  2. 遍历相应的队列,将socket取出,并返回。
  3. 若没有找到,则返回NULL。

tcp_sock_lookup_listen

这个函数跟tcp_sock_lookup_established作用类似,是在listen_table中查询。计算hash时,只使用sport,其他位置用0代替。

tcp_sock_connect

这个函数是客户端用来发起连接的函数。需要:

  1. 设置源地址、目的地址、源端口、目的端口。
  2. 将socket加入到bind_table中。
  3. 将socket置为TCP_SYN_SENT状态,并加入到established_table中。
  4. 发送SYN报文。
  5. 阻塞在wait_connect,等待被唤醒。

PS:一定要注意2和3的顺序,如果先发送SYN报文,在网速快并且CPU特别慢的时候,会出现收到SYN|ACK报文时,socket还没有插入到established_table中,这就会造成在tcp_sock_lookup_established函数中找不到相应的socket,造成程序错误。

tcp_sock_listen

这个函数使socket进入TCP_LISTEN状态,并插入到listen_table中。

tcp_sock_accept

ServerSocket使用这个函数,获取一个新建立的连接,进行处理。

  1. 若ServerSocket的accept_queue不为空,则直接弹出队列的第一个元素,并返回。
  2. 若accept_queue为空,则等待wait_accept。被唤醒后,弹出队列的第一个元素,并返回。

tcp_sock_read

使用该函数接收socket发送过来的数据。

  1. 若环形缓冲区为空,则等待wait_recv,唤醒后读取数据、调整窗口并返回。
  2. 若环形缓冲区不为空,则直接调整窗口并返回数据。

tcp_sock_write

使用该函数发送数据。直接调用tcp_send_data函数发送数据。
PS:获取需要验证窗口大小。

实验收获

  • 通过这次实验,真正清晰的了解了TCP连接建立与断开的详细过程。
  • 通过阅读老师给的代码,学习到了一种新的链表结构,与之前学习的链表有很大的不同,非常高效且不浪费空间。
  • 通过阅读代码,并查阅资料,了解了如何禁用系统的协议栈,实现用户态的TCP/IP栈。