【计网实验——prj16】网络传输机制实验三
实验内容
支持TCP可靠数据传输
- 网络丢包
- 超时重传机制
- 有丢包场景下的连接建立和断开
- 发送队列和接收队列
- 超时定时器实现
实验步骤
- 修改tcp_apps.c(以及tcp_stack.py),使之能够收发文件
- 执行create_randfile.sh,生成待传输数据文件client-input.dat
- 运行给定网络拓扑(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协议栈的客户端模式 (./tcp_stack client 10.0.0.1 10001)
- Client发送文件client-input.dat给server,server将收到的数据存储到文件server-output.dat
- 使用md5sum比较两个文件是否完全相同
- 使用tcp_stack.py替换其中任意一端,对端都能正确收发数据
实现方案
本次实验中,我们需要通过实现超时重传机制以及发送和接收队列的维护来支持TCP可靠数据传输。
超时重传机制
超时重传机制基于定时器实现,为每一个连接维护一个超时重传定时器。
定时器管理方法:
- 发送数据/FIN/SYN包时,若定时器没有开启,则启动定时器,并设置时间为200ms
- 当ACK确认了部分数据,重启定时器,重设时间为200ms
- 当ACK确认了全部数据(包括FIN和SYN包),则关闭定时器
定时器维护:
- 在tcp_sock中维护定时器
struct tcp_timer retrans_timer
,其中,type变量用于记录是否为重传连接
struct tcp_timer {
int type; // time-wait: 0 retrans: 1
int timeout; // in micro second
int retrans_time;
struct list_head list;
int enable;
};
- 当开启定时器时,将
retrans_timer
放到timer_list
中 - 关闭定时器时,将
retrans_timer
从timer_list
中移除 - 定时器扫描,每10ms扫描一次定时器队列,重传定时器的值为200ms * 2^N
触发定时器后:
触发定时器等待超时后,重传机制设计如下:
- 重传发送队列 snd_buffer 中的第一个数据包
- 将定时器等待时间增大一倍
- 如果已经重传了三次仍没有得到应答,则发送 TCP_RST 包,关闭连接
以上过程通过函数tcp_scan_timer_list
实现,具体代码如下:
// scan the timer_list, find the tcp sock which stays for at 2*MSL, release it
void tcp_scan_timer_list()
{
struct tcp_sock *sock;
struct tcp_timer *time;
int i = 0;
list_for_each_entry(time, &timer_list, list){
if(time->type == 0){
time->timeout -= TCP_TIMER_SCAN_INTERVAL;
if(time->timeout <= 0){
list_delete_entry(&time->list);
sock = timewait_to_tcp_sock(time);
if(sock->parent == NULL){
tcp_bind_unhash(sock);
}
tcp_set_state(sock, TCP_CLOSED);
free_tcp_sock(sock);
}
}else if(time->type == 1){
sock = retranstimer_to_tcp_sock(time);
pthread_mutex_lock(&sock->time_lock);
time->timeout -= TCP_TIMER_SCAN_INTERVAL;
if(time->timeout <= 0){
pthread_mutex_lock(&sock->sb_lock);
struct send_buffer* temp;
list_for_each_entry(temp, &(sock->send_buf), list){
if(temp->number >= 3){
//printf("have send three times\n");
tcp_send_control_packet(sock, TCP_RST);
exit(1);
}else{
char *packet_temp = (char *)malloc(temp->total_len);
memcpy(packet_temp, temp->packet, temp->total_len);
ip_send_packet(packet_temp, temp->total_len);
time->timeout = TCP_RETRANS_INTERVAL_INITIAL;
temp->number += 1;
for(i=0; i<temp->number; i++){
time->timeout = 2*time->timeout;
}
}
break;
}
pthread_mutex_unlock(&sock->sb_lock);
}
pthread_mutex_unlock(&sock->time_lock);
}
}
}
发送队列维护
将数据包插入发送队列
- 所有未确认的数据/SYN/FIN包,在收到其对应的ACK之前,都要放在发送队列snd_buffer中,以备后面可能的重传
- 发送新的数据时,放到snd_buffer队尾,打开定时器
以上过程根据将要发送数据包的序列号 seq、数据长度 data_len、总长度total_len等信息创建发送队列中的新表项,打开定时器,并将该表项加入到发送队列中。通过函数insert_send_buf
实现,其主要在 tcp_send_packet
、tcp_send_control_packet
函数中被调用,具体代码如下:
void insert_send_buf(struct tcp_sock* tsk, char *packet, u32 seq, int data_len, int total_len){
struct send_buffer *temp = (struct send_buffer*)malloc(sizeof(struct send_buffer));
char *packet_temp = (char *)malloc(total_len);
memcpy(packet_temp, packet, total_len);
temp->packet = packet_temp;
temp->seq = seq;
temp->data_len = data_len;
pthread_mutex_lock(&tsk->sb_lock);
pthread_mutex_lock(&tsk->time_lock);
if((tsk->retrans_timer).type == 0){
(tsk->retrans_timer).type = 1;
(tsk->retrans_timer).timeout = TCP_RETRANS_INTERVAL_INITIAL;
add_timer_list(tsk);
tsk->ref_cnt += 1;
}
list_add_tail(&temp->list, &tsk->send_buf);
pthread_mutex_unlock(&tsk->time_lock);
pthread_mutex_unlock(&tsk->sb_lock);
}
其中,自定义send_buffer表项的数据结构如下:
struct send_buffer {
struct list_head list;
char * packet;
u32 seq;
int data_len;
};
从发送队列中删除包
- 收到新的ACK时, 如果发送队列表项的 seq_end(即 seq+data_len) <= ack,将该表项移除,并更新定时器
void delete_send_buf(struct tcp_sock *tsk, u32 ack){
int length = 0;
struct send_buffer *temp;
pthread_mutex_lock(&tsk->sb_lock);
list_for_each_entry(temp, &tsk->send_buf, list){
if(less_or_equal_32b(temp->seq+temp->datalen, ack)){
length += temp->datalen;
list_delete_entry(&temp->list);
pthread_mutex_lock(&tsk->time_lock);
(tsk->retrans_timer).timeout = TCP_RETRANS_INTERVAL_INITIAL;
pthread_mutex_unlock(&tsk->time_lock);
}
}
pthread_mutex_unlock(&tsk->sb_lock);
tsk->snd_wnd += length;
if(tsk->wait_send->sleep == 1){
wake_up(tsk->wait_send);
}
}
接收队列维护
- 数据接收方需要维护两个队列
- 已经连续收到的数据,放在rcv_ring_buffer中供app读取
- 收到不连续的数据,放到rcv_ofo_buffer队列中
- TCP属于发送方驱动传输机制
- 接收方只负责在收到数据包时回复相应ACK
- 收到不连续的数据包时
- 放在rcv_ofo_buffer队列,如果队列中包含了连续数据,则将其移到rcv_ring_buffer中
以上过程通过函数handle_rcv_data
实现,具体代码如下:
void handle_rcv_data(struct tcp_sock *tsk, struct tcp_cb *cb, char *packet){
if(cb->seq == tsk->rcv_nxt){
pthread_mutex_lock(&(tsk->rcv_buf->rb_lock));
write_ring_buffer(tsk->rcv_buf, cb->payload, cb->pl_len);
pthread_mutex_unlock(&(tsk->rcv_buf->rb_lock));
tsk->rcv_nxt += cb->pl_len;
u32 record = tsk->rcv_nxt;
while(1){
int flag = 0;
struct recv_buffer *temp;
list_for_each_entry(temp, &tsk->rcv_ofo_buf, list){
if(temp->seq == record){
struct tcphdr *tcphdr_t = packet_to_tcp_hdr(temp->packet);
char *data_t = (char *)tcphdr_t + tcphdr_t->off * 4;
pthread_mutex_lock(&(tsk->rcv_buf->rb_lock));
write_ring_buffer(tsk->rcv_buf, data_t, temp->datalen);
pthread_mutex_unlock(&(tsk->rcv_buf->rb_lock));
tsk->rcv_nxt += temp->datalen;
record = tsk->rcv_nxt;
flag = 1;
list_delete_entry(&temp->list);
break;
}
}
if(flag == 0){
break;
}
}
tcp_send_control_packet(tsk, TCP_ACK);
if(tsk->wait_recv->sleep == 1){
wake_up(tsk->wait_recv);
}
}else if(less_than_32b(tsk->rcv_nxt, cb->seq)){
struct iphdr *iphdr_t = packet_to_ip_hdr(packet);
int all_len = ntohs(iphdr_t->tot_len) + ETHER_HDR_SIZE;
struct recv_buffer *temp = (struct recv_buffer*)malloc(sizeof(struct recv_buffer));
char *packet_temp = (char *)malloc(all_len);
memcpy(packet_temp, packet, all_len);
temp->packet = packet_temp;
temp->total_len = all_len;
temp->seq = cb->seq;
temp->datalen = cb->pl_len;
list_add_tail(&temp->list, &tsk->rcv_ofo_buf);
}else{
tcp_send_control_packet(tsk, TCP_ACK);
}
tsk->snd_una = cb->ack - 1;
delete_send_buf(tsk, cb->ack);
}
运行结果
h2运行python tcp_stack.py测试h1
实验结果如下:
h1运行python tcp_stack.py测试h2
实验结果如下:
h1和h2同时运行本实验中实现的tcp_stack程序
实验结果如下:
通过比对可知本次实验结果符合预期,客户端发送的文件与服务器端接受的文件一致。