【计网实验——prj13】网络传输机制实验一
实验要求
- 运行给定网络拓扑(tcp_topo.py)
- 在节点h1上执行TCP程序
- 执行脚本(disable_tcp_rst.sh, disable_offloading.sh),禁止协议栈的相应功能
- 在h1上运行TCP协议栈的服务器模式 (./tcp_stack server 10001)
- 在节点h2上执行TCP程序
- 执行脚本(disable_tcp_rst.sh, disable_offloading.sh),禁止协议栈的相应功能
- 在h2上运行TCP协议栈的客户端模式,连接至h1,显示建立连接成功后自动关闭连接 (./tcp_stack client 10.0.0.1 10001)
- 可以在一端用tcp_stack.py替换tcp_stack执行,测试另一端
- 通过wireshark抓包来来验证建立和关闭连接的正确性
实现方案
本次实验的主要任务是实现TCP协议栈,分为TCP数据包处理和连接管理两个部分。
1. 连接管理
(1)建立连接
TCP协议中存在主动建立连接和被动建立连接两种方式,服务器为被动建立连接的一方,客户端为主动建立连接的一方。
被动建立连接
被动建立连接的过程如下:
①申请占用一个端口号 (bind操作)
②监听该端口号 (listen操作)
③收到SYN数据包,状态切换为 TCP_SYN_RECV (accept操作)
④回复ACK并发送SYN数据包
⑤收到ACK数据包,状态切换为 TCP_ESTABLISHED
本次实验中需要实现上述过程中的listen和accept操作,对应函数如下:
tcp_sock_listen
函数
设置backlog,切换状态到TCP_LISTEN
,并且利用hash_tcp
函数把TCP Socket加入到listen_table
中。具体实现如下:
// set backlog (the maximum number of pending connection requst), switch the
// TCP_STATE, and hash the tcp sock into listen_table
int tcp_sock_listen(struct tcp_sock *tsk, int backlog)
{
fprintf(stdout, "TODO: implement %s please.\n", __FUNCTION__);
tsk->backlog = backlog;
tcp_set_state(tsk, TCP_LISTEN);
return tcp_hash(tsk);
}
tcp_sock_accept
函数
若accept_queue非空,取出队列中第一个 tcp socket 并 accept,否则,调用sleep_on
函数进行阻塞,等待TCP协议栈完成相应数据包是收发,直到收到ACK
后被唤醒。具体实现如下:
// if accept_queue is not emtpy, pop the first tcp sock and accept it,
// otherwise, sleep on the wait_accept for the incoming connection requests
struct tcp_sock *tcp_sock_accept(struct tcp_sock *tsk)
{
fprintf(stdout, "TODO: implement %s here.\n", __FUNCTION__);
while (list_empty(&tsk->accept_queue)) {
sleep_on(tsk->wait_accept);
}
struct tcp_sock * pop_stack;
if ((pop_stack = tcp_sock_accept_dequeue(tsk)) != NULL) {
pop_stack->state = TCP_ESTABLISHED;
tcp_hash(pop_stack);
return pop_stack;
} else {
return NULL;
}
}
主动建立连接
主动建立连接的过程如下:
①发送目的端口的SYN数据包,状态切换为 TCP_SYN_SENT (connect操作)
②收到SYN 数据包(设置TCP_ACK标志位)
③回复ACK数据包,状态切换为 TCP_ESTABLISHED
本次实验中需要实现上述过程中的connect操作,对应函数如下:
tcp_sock_connect
函数
- 初始化tcp socket的四元组
(sip, sport, dip, dport)
; - 将tcp socket 按照端口号将 bind_hash_list 节点 hash 到
bind_table
; - 发送
SYN
信号包,切换TCP状态到TCP_SYN_SENT
,通过调用sleep_on
函数来等待
SYN
包; - 若
SYN
到达,该线程被唤醒,说明此时连接已经建立完成。具体实现如下:
int tcp_sock_connect(struct tcp_sock *tsk, struct sock_addr *skaddr)
{
fprintf(stdout, "TODO: implement %s please.\n", __FUNCTION__);
u16 sport = tcp_get_port();
if (sport == 0) {
return -1;
}
u32 saddr = longest_prefix_match(ntohl(skaddr->ip))->iface->ip;
tsk->sk_sip = saddr;
tsk->sk_sport = sport;
tsk->sk_dip = ntohl(skaddr->ip);
tsk->sk_dport = ntohs(skaddr->port);
tcp_bind_hash(tsk);
tcp_send_control_packet(tsk, TCP_SYN);
tcp_set_state(tsk, TCP_SYN_SENT);
tcp_hash(tsk);
sleep_on(tsk->wait_connect);
return sport;
}
(2)断开连接
与建立连接相对应的,关闭连接也存在主动关闭和被动关闭两种方式,
主动关闭
主动关闭连接涉及如下过程:
①发送FIN包,切换状态为TCP_FIN_WAIT_1 (close操作)
②收到FIN对应的ACK包,切换状态为TCP_FIN_WAIT_2
③收到对方发送的FIN包,回复ACK,切换状态为TCP_TIME_WAIT
④等待2*MSL时间,切换状态为TCP_CLOSED,连接结束(需要实现定时器线程,定期扫描,适时结束TCP_TIME_WAIT状态的流)
被动关闭
被动关闭连接涉及如下过程:
①收到FIN包,回复相应ACK,切换状态为TCP_CLOSE_WAIT,还可以发送数据
②自己没有待发送数据,断开连接,发送FIN包,切换状态为TCP_LAST_ACK (close操作)
③收到FIN包对应的ACK,切换状态为TCP_CLOSED,连接结束
本次实验中需要实现上述过程中的close操作和定时器线程,对应函数如下:
tcp_sock_close
函数
根据不同的TCP状态进行关闭连接操作,即发送FIN/ACK
包,并切换到相应的状态。具体实现如下:
// close the tcp sock, by releasing the resources, sending FIN/RST packet
// to the peer, switching TCP_STATE to closed
void tcp_sock_close(struct tcp_sock *tsk)
{
fprintf(stdout, "TODO: implement %s here.\n", __FUNCTION__);
if (tsk->state == TCP_CLOSE_WAIT) {
tcp_send_control_packet(tsk, TCP_FIN|TCP_ACK);
tcp_set_state(tsk, TCP_LAST_ACK);
} else if (tsk->state == TCP_ESTABLISHED) {
tcp_set_state(tsk, TCP_FIN_WAIT_1);
tcp_send_control_packet(tsk, TCP_FIN|TCP_ACK);
} else {
tcp_unhash(tsk);
tcp_set_state(tsk, TCP_CLOSED);
}
}
tcp_timer_thread
函数
该函数为定时器线程,定期扫描。若等待超过2*MSL的时间,进入TCP_CLOSED
状态,结束TCP_TIME_WAIT
状态的流。
void *tcp_timer_thread(void *arg)
{
init_list_head(&timer_list);
while (1) {
usleep(TCP_TIMER_SCAN_INTERVAL);
struct tcp_timer * time_entry, *time_q;
list_for_each_entry_safe (time_entry, time_q, &timer_list, list) {
if (time_entry->enable == 1 && (time(NULL) * TCP_MSL - time_entry->timeout * TCP_MSL > TCP_TIMEWAIT_TIMEOUT)) {
struct tcp_sock * tsk = timewait_to_tcp_sock(time_entry);
list_delete_entry(&time_entry->list);
tcp_set_state(tsk, TCP_CLOSED);
tcp_bind_unhash(tsk);
}
}
}
return NULL;
}
2. TCP数据包处理
节点在收到一个TCP数据包后,会首先检查TCP校验和是否正确,然后进行TCP查找,找到连接相应的socket并返回,之后再根据包的种类和状态机进行数据包的处理。其主要过程通过函数void handle_tcp_packet(char *packet, struct iphdr *ip, struct tcphdr *tcp)
实现,如下(框架中已写好):
// handle TCP packet: find the appropriate tcp sock, and let the tcp sock
// to process the packet.
void handle_tcp_packet(char *packet, struct iphdr *ip, struct tcphdr *tcp)
{
//检查校验和
if (tcp_checksum(ip, tcp) != tcp->checksum) {
log(ERROR, "received tcp packet with invalid checksum, drop it.");
return ;
}
struct tcp_cb cb;
tcp_cb_init(ip, tcp, &cb);
//在established_table和listen_table中查找tcp sock
struct tcp_sock *tsk = tcp_sock_lookup(&cb);
//根据状态机进行数据包处理
tcp_process(tsk, &cb, packet);
}
其中,tcp_sock_lookup
函数中调用了tcp_sock_lookup_established
函数和tcp_sock_lookup_listen
函数实现在established_table
和listen_table
中的socket查找。
tcp_sock_lookup_established
函数
查找时首先根据四元组查找已经建立好的的socket,若找到则返回对应的socket。
// lookup tcp sock in established_table with key (saddr, daddr, sport, dport)
struct tcp_sock *tcp_sock_lookup_established(u32 saddr, u32 daddr, u16 sport, u16 dport)
{
fprintf(stdout, "TODO: implement %s here.\n", __FUNCTION__);
int hash = tcp_hash_function(saddr, daddr, sport, dport);
struct list_head * list = &tcp_established_sock_table[hash];
struct tcp_sock *tmp;
list_for_each_entry(tmp, list, hash_list) {
if (saddr == tmp->sk_sip && daddr == tmp->sk_dip &&
sport == tmp->sk_sport && dport == tmp->sk_dport) {
return tmp;
}
}
return NULL;
}
tcp_sock_lookup_listen
函数
若没有找到已经完成建立的socket,则对处在LISTEN
状态下的socket进行查找,若找到则返回对应的socket。
// lookup tcp sock in listen_table with key (sport)
//
// In accordance with BSD socket, saddr is in the argument list, but never used.
struct tcp_sock *tcp_sock_lookup_listen(u32 saddr, u16 sport)
{
fprintf(stdout, "TODO: implement %s here.\n", __FUNCTION__);
int hash = tcp_hash_function(0, 0, sport, 0);
struct list_head * list = &tcp_listen_sock_table[hash];
struct tcp_sock *tmp;
list_for_each_entry(tmp, list, hash_list) {
if (sport == tmp->sk_sport) {
return tmp;
}
}
return NULL;
}
3. 状态迁移
tcp_process
函数
在完成socket查找后,由该函数根据TCP状态机的不同状态进行数据包处理以及状态切换,具体步骤为:
- (1)检查是否为
RST
包,如果是,直接结束连接; - (2)检查当前TCP状态以及数据包类型,按照下图虚线部分的内容(与连接管理过程紧密相关,详细描述见上文)进行相应类型的数据包转发以及状态转换:
具体代码实现如下:
// Process the incoming packet according to TCP state machine.
void tcp_process(struct tcp_sock *tsk, struct tcp_cb *cb, char *packet)
{
fprintf(stdout, "\nTODO: implement %s here.\n", __FUNCTION__);
struct tcphdr * tcp = packet_to_tcp_hdr(packet);
if ((tcp->flags & TCP_RST) == TCP_RST) {
tcp_sock_close(tsk);
return;
}
if (tsk->state == TCP_LISTEN) {
if ((tcp->flags & TCP_SYN) == TCP_SYN) {
tcp_set_state(tsk, TCP_SYN_RECV);
struct tcp_sock * child = alloc_child_tcp_sock(tsk, cb);
tcp_send_control_packet(child, TCP_ACK|TCP_SYN);
}
}
if (tsk->state == TCP_SYN_SENT) {
if ((tcp->flags & TCP_ACK) == TCP_ACK) {
wake_up(tsk->wait_connect);
tsk->rcv_nxt = cb->seq + 1;
tsk->snd_una = cb->ack;
tcp_send_control_packet(tsk, TCP_ACK);
tcp_set_state(tsk, TCP_ESTABLISHED);
}
}
if (tsk->state == TCP_SYN_RECV) {
if ((tcp->flags & TCP_ACK) == TCP_ACK) {
if (!tcp_sock_accept_queue_full(tsk)) {
struct tcp_sock * csk = tcp_sock_listen_dequeue(tsk);
tcp_sock_accept_enqueue(csk);
if (!is_tcp_seq_valid(csk,cb)) {
return;
}
csk->rcv_nxt = cb->seq;
csk->snd_una = cb->ack;
tcp_set_state(csk, TCP_ESTABLISHED);
wake_up(tsk->wait_accept);
}
}
}
if (!is_tcp_seq_valid(tsk,cb)) {
return;
}
if (tsk->state == TCP_ESTABLISHED) {
if ((tcp->flags & TCP_FIN) == TCP_FIN) {
tcp_set_state(tsk, TCP_CLOSE_WAIT);
tsk->rcv_nxt = cb->seq + 1;
tcp_send_control_packet(tsk, TCP_ACK);
}
}
if (tsk->state == TCP_LAST_ACK) {
if ((tcp->flags & TCP_ACK) == TCP_ACK) {
tcp_set_state(tsk, TCP_CLOSED);
tcp_unhash(tsk);
}
}
if (tsk->state == TCP_FIN_WAIT_1) {
if ((tcp->flags & TCP_ACK) == TCP_ACK) {
tcp_set_state(tsk, TCP_FIN_WAIT_2);
}
}
if (tsk->state == TCP_FIN_WAIT_2) {
if ((tcp->flags & TCP_FIN) == TCP_FIN) {
tsk->rcv_nxt = cb->seq + 1;
tcp_send_control_packet(tsk, TCP_ACK);
tcp_set_state(tsk, TCP_TIME_WAIT);
tcp_set_timewait_timer(tsk);
}
}
}
其中,状态为LISTEN
,数据包为SYN
时,调用了alloc_child_tcp_sock
函数为被动建立连接一方建立一个Child Socket。
运行结果
结点h1作为服务器,结点h2作为客户端,分别运行TCP程序,wireshark抓包结果如下:
h2运行python tcp_stack.py测试h1
实验结果如下:
抓包结果与状态转移符合预期。
h1运行python tcp_stack.py测试h2
实验结果如下:
抓包结果与状态转移符合预期。
h1和h2同时运行本实验中实现的tcp_stack程序
实验结果如下:
上图中,蓝色部分为本实验中Server和Client的结果,红色部分为标准Server和Client的结果。通过比对可知本次实验结果符合预期。