上一篇:DIY TCP/IP TCP模块的实现9
9.12 TCP滑动窗口的实现4
本节在9.11节的基础上,实现9.9节介绍的TCP滑动窗口算法。DIY TCP/IP在TCP连接建立之后,接收到TCP Client发送的数据帧时,将收到的TCP数据帧携带的数据放入滑动窗口中。正确更新滑动窗口数据结构中的”指针”位置,根据滑动窗口的指针位置计算接收缓存中剩余的空间大小,即本地TCP连接的window size。正确构造TCP ACK数据帧,并回复确认给TCP Client。通知模拟的读取线程,从滑动窗口中提取确认过的数据,使滑动窗口能够正确的循环”向右”滑动。
9.9节介绍的滑动窗口算法可以分成四个部分实现:向滑动窗口存放数据并更新recv_end;从滑动窗口提取数据并更新ack_start;回复TCP ACK后更新滑动窗口的ack_end;计算滑动窗口中剩余的空间大小。
先来看向滑动窗口中存放数据并更新recv_end的函数sw_push_data。该函数是TCP模块的静态函数,定义在tcp.c文件中。函数的入参:sw指向滑动窗口的指针,data指向需要放入滑动窗口的数据的指针,data_len是需要放入滑动窗口的数据长度。成功返回0,失败返回非0。
/* copy data to sliding window */
static int sw_push_data(tcp_slide_window_t *sw,
unsigned char *data, unsigned int len)
{
unsigned int recv_end = 0;
unsigned int copy_sz = 0;
unsigned int left_buf_sz = 0;
if (sw == NULL || data == NULL )
return -1;
left_buf_sz = sw_left_buf_sz(sw);
if (len == 0 || left_buf_sz < len)
return -1;
recv_end = sw->recv_end;
copy_sz = len;
/* len exceeded buf end offset */
if (recv_end + len > sw->buf_sz)
copy_sz = sw->buf_sz - sw->recv_end;
assert(copy_sz <= len);
memcpy(&sw->buf[recv_end], data, copy_sz);
recv_end += copy_sz;
/* update recv_end */
sw->recv_end = recv_end;
if (copy_sz == len) {
return 0;
}
assert(recv_end == sw->buf_sz);
/* copy left data to the start of buffer
* max(recv_end) <= acked_start, this is checked in tcp_recv_buf_sz
*/
recv_end = sw->buf_start;
data += copy_sz;
copy_sz = len - copy_sz;
/* max(recv_end) <= acked_start */
assert((recv_end + copy_sz) <= sw->acked_start);
memcpy(&sw->buf[recv_end], data, copy_sz);
/* update recv_end */
recv_end += copy_sz;
sw->recv_end = recv_end;
return 0;
}
Line 5-31: 数据存入滑动窗口,从recv_end开始存放,首先调用sw_left_buf_sz计算滑动窗口中剩余的内存空间大小left_buf_sz(蓝色部分)。left_buf_sz大于等于待拷贝的数据长度时,继续执行。如果recv_end + len没有超出buf_sz,则直接拷贝数据到recv_end,然后更新recv_end到recv_end + len,函数返回。如果recv_end + len超出buf_sz,将数据分成两个部分(黄色部分),第一部分的长度copy_sz = buf_sz – recv_end,第二部分长度left_to_copy=len – copy_sz。将第一部分数据拷贝到recv_end开始的内存中,更新recv_end。第一部分数据拷贝完成后,recv_end指向接收缓存末尾字节的下一个字节(copy_sz表示的黄色部分的末尾)。第二部分待拷贝数据(left_to_copy表示的黄色部分),需要回绕到接收缓存开始buf_start处存放。
Line 32 – 44: 将剩余数据放入接收缓存之前,先计算接收缓存中可用的内存空间大小acked_start – buffer_start,如果剩余的内存空间大于left_to_copy,则将剩余数据存放到接收缓存buffer_start指向的内存中。
最后更新recv_end到buffer_start + left_to_copy,函数返回。
再来看sw_push_data函数中用到的sw_left_buf_sz的实现。该函数同样是tcp.c文件中的静态函数,入参是指向滑动窗口的指针,返回值是滑动窗口中剩余的内存大小,即9.9节介绍的滑动窗口结构图中蓝色部分的大小。
static unsigned int sw_left_buf_sz(tcp_slide_window_t *sw)
{
unsigned int acked_sz = 0;
unsigned int unacked_sz = 0;
if (sw == NULL)
return 0;
acked_sz = sw_acked_data_sz(sw);
unacked_sz = sw_unacked_data_sz(sw);
return (sw->buf_sz - acked_sz - unacked_sz);
}
sw_left_buf_sz调用sw_acked_data_dz,计算已经ack过的数据长度acked_sz,对应9.9节滑动窗口结构图中的红色部分,sw_unacked_data_sz计算已经接收但是尚未ack的数据unacked_sz,对应9.9节滑动窗口结构图中的绿色部分。接收缓存大小buf_sz减去acked_sz和unacked_sz后,返回剩余内存大小。
static unsigned int sw_acked_data_sz(tcp_slide_window_t *sw)
{
unsigned int acked_end = 0;
unsigned int acked_start = 0;
if (sw == NULL)
return 0;
acked_start = sw->acked_start;
acked_end = sw->acked_end;
if (acked_end < acked_start)
acked_end += sw->buf_sz;
return acked_end - acked_start;
}
sw_ack_data_sz返回滑动窗口中已经接收过并且也回复了TCP ACK的数据,对应图中的红色部分。如果acked_end > acked_start,则红色部分的大小为acked_end – acked_start,如果acked_end < acked_start,则说明发生了回绕,acked_end + buf_end – acked_start即红色部分的大小。
static unsigned int sw_unacked_data_sz(tcp_slide_window_t *sw)
{
unsigned int recv_end = 0;
unsigned int recv_start = 0;
if (sw == NULL)
return 0;
recv_start = sw->recv_start;
recv_end = sw->recv_end;
if (recv_end < recv_start)
recv_end += sw->buf_sz;
return recv_end - recv_start;
}
sw_ack_data_sz返回滑动窗口中已经接收但尚未回复TCP ACK的数据,对应图中的绿色部分。如果received end > received start,则绿色部分的大小为received end – received start,如果received end < received start,则说明发生了回绕,received end + buf_end – received end即绿色部分的大小。
再来看从滑动窗口中提取数据并更新acked_start的函数实现。sw_pull_data是定义在tcp.c文件中的静态函数,入参sw为指向滑动窗口的指针,buf指向存放提取数据的内存空间,buf_sz是存放提取数据的内存空间的大小。
static int sw_pull_data(tcp_slide_window_t *sw,
unsigned char *buf, unsigned int buf_sz)
{
unsigned int acked_sz = 0;
unsigned int acked_start = 0;
unsigned int copy_sz = 0;
if (sw == NULL || buf == NULL)
return -1;
acked_sz = sw_acked_data_sz(sw);
if (acked_sz == 0) {
log_printf(ERROR, "No data available\n");
return -1;
}
if (acked_sz < buf_sz)
buf_sz = acked_sz;
acked_start = sw->acked_start;
copy_sz = buf_sz;
if (acked_start + copy_sz > sw->buf_sz)
copy_sz = sw->buf_sz - acked_start;
memcpy(buf, &sw->buf[acked_start], copy_sz);
acked_start += copy_sz;
/* update acked_start */
sw->acked_start = acked_start;
if (copy_sz == buf_sz)
return 0;
assert(sw->acked_start == sw->buf_sz);
acked_start = sw->buf_start;
buf += copy_sz;
copy_sz = buf_sz - copy_sz;
/* wrap around and copy left */
assert(acked_start + copy_sz <= sw->acked_end);
memcpy(buf, &sw->buf[acked_start], copy_sz);
acked_start += copy_sz;
/* updated acked_start */
sw->acked_start = acked_start;
log_printf(VERBOSE, "pull %u data\n", buf_sz);
}
Line 4-31:调用sw_acked_data_sz计算滑动窗口中已经接收并且回复过TCP ACK的数据acked_sz(红色部分)。acked_sz大于等于待提取数据的长度时,继续执行。如果acked_start + buf_sz没有超出buf_end,则直接拷贝数据到buf,然后更新acked_start到acked_start + buf_sz,函数返回。如果acked_start + buf_sz超出buf_end,将数据分成两个部分提取,第一部分的长度copy_sz = buf_end – acked_start,第二部分长度left_to_copy=buf_sz – copy_sz。将第一部分数据提取到buf指向的内存中,更新acked_start。第一部分数据提取完成后,acked_start指向接收缓存末尾字节的下一个字节。第二部分待提取数据(left_to_copy表示的白色部分),需要回绕到接收缓存开始buf_start处提取。
Line 32-44: 提取剩余数据left_to_copy之前,再次计算接收缓存中可被提取的数据大小acked_end – buffer_start,如果可被提取的数据大小大于等于left_to_copy,则将数据提取到buf指向的内存中,buf在第一部分提取完成后已经指向了下一个待提取数据的字节地址处。
最后更新acked_start到buf_start + left_to_copy,函数返回。
再来看回复TCP ACK后,根据接收到的数据长度,更新滑动窗口的acked_end的实现。
static void sw_ack_data(tcp_slide_window_t *sw, unsigned int len)
{
if (sw == NULL || len == 0)
return;
sw->acked_end += len;
if (sw->acked_end > sw->buf_sz)
sw->acked_end -= sw->buf_sz;
assert(sw->acked_end <= sw->recv_end);
sw->recv_start = sw->acked_end;
}
sw_ack_data的第一个入参是指向滑动窗口的指针,第二个入参是接收到的TCP数据帧携带的数据部分的长度。如果acked_end + len大于buf_end,需要回绕acked_end,将acked_end更新到buf_end – (acked_end + len),检查acked_end的位置是否小于等于recv_end, 如果是,则更新recv_start到acked_end。
滑动窗口四个部分的算法实现sw_push_data,sw_pull_data,sw_left_buf_sz和sw_ack_data已经实现完成。接下来是TCP模块接收到TCP数据帧时,通过sw_push_data将TCP数据帧携带的数据放入滑动窗口,构建TCP ACK时,调用sw_left_buf_sz计算接收缓存中可用的内存空间,填入TCP ACK头部的window size字段。回复TCP ACK后调用sw_ack_data更新滑动窗口的acked_end,并通知模拟的读取线程调用sw_pull_data读取滑动窗口中的数据。
先来看tcppkt_recv函数的修改
static int tcp_process_data_pkt(tcp_conn_t *conn,
tcphdr_t *tcppkt, unsigned short tcppkt_len)
{
…
pdbuf_push(pdbuf, tcpack_len);
tcpack = (tcphdr_t *)pdbuf->payload;
tcpack_flags.b.ack = 1;
tcpack_flags.b.hdr_len = (tcpack_len >> 2);
build_tcp_header(conn, tcpack, 0, tcpack_flags.v);
…
ret = tcppkt_send(conn, pdbuf);
if (ret == 0)
tcp_input_data(conn, tcppkt, tcppkt_len);
return ret;
out:
if (pdbuf)
pdbuf_free(pdbuf);
return ret;
}
高亮显示tcp_process_data_pkt在本节的改动,tcp_process_data_pkt其余部分的代码与9.10节一致。TCP连接建立之后,tcp_process_data_pkt处理接收到的TCP数据帧,构建TCP ACK,build_tcp_header中填入接收缓存剩余空间大小,tcppkt_send回复TCP ACK之后,调用tcp_input_data将TCP数据放入滑动窗口,并通知模拟读取线程读取数据。
static int build_tcp_header(tcp_conn_t *conn,
tcphdr_t *tcphdr, unsigned short data_len,
unsigned short hflags)
{
int ret = 0;
unsigned int left_buf_sz = 0;
if (conn == NULL || tcphdr == NULL)
return -1;
tcphdr->src_port = HSTON(conn->l_port);
tcphdr->dst_port = HSTON(conn->r_port);
tcphdr->seq_num = HLTON(conn->send_seq);
tcphdr->ack_num = HLTON(conn->ack_seq);
tcphdr->flags_len = HSTON(hflags);
left_buf_sz = tcp_recv_wnd_sz(conn);
tcphdr->wnd_sz = HSTON(left_buf_sz);
tcphdr->cksum = 0;
tcphdr->ugtp = 0;
return ret;
}
tcp_recv_wnd_sz是对sw_left_buf_sz的封装,由于DIY TCP/IP的TCP模块和模拟的读取线程都需要读取和修改滑动窗口的”指针”。在封装函数tcp_recv_wnd_sz中,首先获得保护滑动窗口的互斥量,再调用sw_left_buf_sz读取滑动窗口数据结构中的数据。
static unsigned int tcp_recv_wnd_sz(tcp_conn_t *conn)
{
tcp_slide_window_t *sw = NULL;
unsigned int left_buf_sz = 0;
if (conn == NULL)
return 0;
sw = &conn->sw;
pthread_mutex_lock(&sw->data_available_mutex);
left_buf_sz = sw_left_buf_sz(sw);
pthread_mutex_unlock(&sw->data_available_mutex);
return left_buf_sz;
}
tcp_process_data_pkt中调用的tcp_input_data也是对sw_push_data的封装。tcp_input_data的第一个参数是指向TCP连接的指针,第二个参数是接收到的TCP数据帧的指针,第三个参数是TCP数据帧的长度。
static int tcp_input_data(tcp_conn_t *conn,
tcphdr_t *tcppkt, unsigned short tcppkt_len)
{
int ret = 0;
tcphdr_flags_t flags;
unsigned int tcphdr_len = 0;
unsigned int data_len = 0;
unsigned char *data = NULL;
tcp_slide_window_t *sw = NULL;
if (conn == NULL || tcppkt == NULL || tcppkt_len ==0 )
return -1;
flags.v = NTOHS(tcppkt->flags_len);
tcphdr_len = flags.b.hdr_len << 2;
data_len = tcppkt_len - tcphdr_len;
if (data_len == 0)
return 0;
sw = &conn->sw;
data = (unsigned char *)tcppkt + tcphdr_len;
pthread_mutex_lock(&sw->data_available_mutex);
ret = sw_push_data(sw, data, data_len);
pthread_mutex_unlock(&sw->data_available_mutex);
tcp_notify_data_available(sw, data_len);
dump_slide_window(conn);
return ret;
}
解析TCP数据帧头部,计算TCP数据帧携带的数据长度data_len,获取保护滑动窗口的互斥量data_available_mutex。调用sw_push_data将TCP数据存入滑动窗口中。tcp_input_data在tcp_process_data_pkt函数中的调用是在回复了TCP ACK之后,所以再调用tcp_notify_data_available通知读取线程。
static void tcp_notify_data_available(tcp_slide_window_t *sw,
unsigned int len)
{
pthread_mutex_lock(&sw->data_available_mutex);
sw_ack_data(sw, len);
pthread_mutex_unlock(&sw->data_available_mutex);
/* indicate data available */
pthread_cond_signal(&sw->data_available_cond);
}
传入tcp_notify_data_avaliable的第二个参数len是TCP数据帧携带的数据部分的长度,获取data_available_mutex互斥量后,调用sw_ack_data更新滑动窗口的acked_end和recv_start指针,再唤醒读取线程读取数据。
最后再修改读取线程的主循环体simulate_read_routine函数
static void * simulate_read_routine(void *arg)
{
#define EXPECT_SZ 1460 * 16
unsigned char *data_buf = NULL;
tcp_slide_window_t *sw = NULL;
if (arg == NULL)
return NULL;
sw = (tcp_slide_window_t *)arg;
data_buf = (unsigned char *)malloc(EXPECT_SZ);
if (data_buf == NULL) {
log_printf(DEBUG, "No memory "
"for user data, %s (%d)\n",
strerror(errno), errno);
return NULL;
}
log_printf(DEBUG, "User simulator started\n");
while(sw->stop_simulator != 1) {
pthread_mutex_lock(&sw->data_available_mutex);
pthread_cond_wait(&sw->data_available_cond,
&sw->data_available_mutex);
pthread_mutex_unlock(&sw->data_available_mutex);
if (sw->stop_simulator)
break;
memset(data_buf, 0, EXPECT_SZ);
tcp_output_data(sw, data_buf, EXPECT_SZ);
printf("%s\n", data_buf);
}
if (data_buf)
free(data_buf);
log_printf(DEBUG, "User simulator stopped\n");
return NULL;
}
相比9.11节simulate_read_routine的实现,本节只是添加了一个函数调用tcp_output_data。读取线程被唤醒后,判断退出条件不成立时,调用tcp_output_data从滑动窗口中读取数据,将读取到的数据已字符形式打印输出。
static int tcp_output_data(tcp_slide_window_t *sw,
unsigned char *buf, unsigned int buf_sz)
{
int ret = 0;
if (sw == NULL || buf == NULL || buf_sz == 0)
return -1;
pthread_mutex_lock(&sw->data_available_mutex);
ret = sw_pull_data(sw, buf, buf_sz);
pthread_mutex_unlock(&sw->data_available_mutex);
return ret;
}
tcp_output_data是对sw_pull_data函数的封装,参数与sw_pull_data一致。获取到保护滑动窗口的互斥量后,调用sw_pull_data读取滑动窗口中的数据。
本节测试通过两个侧面验证滑动窗口的算法实现,第一个侧面是ipert TCP发送大量数据,验证滑动窗口可以接收数据,并处理回绕的情况,如果DIY TCP/IP能够长时间运行,不发生assert错误,则说明滑动窗口”指针”更新是正确的,大数据量情况下不容易确定接收到的数据的正确性,所以第二个侧面是,写一个简单的TCP Client程序,向DIY TCP/IP建立TCP连接,并发送字符串。如果读取线程能够正确接收TCP Cient发出的字符,则说明滑动窗口提取的数据是正确的。
先来看第二个侧面的测试结果。
TCP Client 测试代码用于向指定的TCP server建立TCP连接,并通过标准输入向TCP Server发送数据。
Line 10-22: TCP client有3个运行参数,arg0 是./client,arg1是IP address, arg2是port。检查运行参数个数合法后,通过socket系统调用,创建套接字,domain为AF_INET (IPv4 inernet protocols),type为SOCK_STREAM,面向连接的全双工字节流类型,protocol是默认值0。
Line 24-34: 设置TCP server的IP地址和端口号,通过connect系统调用与TCP Server建立TCP连接。
Line 35-48: TCP连接建立后,通过循环读取标准输入,read系统调用的第一个参数0,是进程的标准输入文件描述符。再将读取到的数据通过write系统调用发送到TCP server。循环的结束条件是read在标准输入中读到EOF,即终端键入ctrl+d。
#include
#include
#include
#include
#include
#include
#include
int main(int argc,const char* argv[])
{
if(argc != 3)
{
printf("Usage:%s [ip] [port]\n",argv[0]);
return 0;
}
//create TCP socket
int sock = socket(AF_INET,SOCK_STREAM, 0);
if(sock < 0)
{
perror("socket");
return 1;
}
//set TCP server IP address and port number
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t len = sizeof(struct sockaddr_in);
if(connect(sock, (struct sockaddr*)&server, len) < 0 )
{
perror("connect");
return 2;
}
//connected to TCP server to send data
char buf[1024];
while(1)
{
printf("send###");
fflush(stdout);
ssize_t _s = read(0, buf, sizeof(buf)-1);
if (_s == 0)
break;
buf[_s] = 0;
write(sock, buf, _s);
}
close(sock);
return 0;
}
TCP Clinet通过GCC编译,命令为gcc -o client client.c。DIY TCP/IP运行在主机A上,client运行在主机D上,主机D与主机A处于同一局域网。DIY TCP/IP模拟TCP Server,指定局域网中不存在的IP地址192.168.1.7,端口号为7000。
Client运行时指定IP地址为192.168.1.7, 端口号为7000。先看主机D上Client的运行结果: client可以和DIY User Space TCP/IP建立TCP连接,通过标准输入键入”tcp hello world”发送给DIY TCP/IP。
主机A上DIY User Sapce TCP/IP的运行结果:
DIY TCP/IP首先收到主机D发出的ARP Request,回复ARP Reply建立虚拟IP地址192.168.1.7和主机A上eth0接口硬件地址的映射。主机D发出TCP SYN触发TCP三步握手,DIY TCP/IP正确接收TCP SYN并回复TCP SYN-ACK后,收到主机D发出的TCP ACK,完成TCP三步握手,建立TCP连接。
主机D上的client发送TCP 数据帧,数据长度为16,与主机D上终端输入的”tcp hello wolrd”的长度一致。DIY TCP/IP接收TCP 数据帧,构建TCP ACK,TCP ACK头部的window size并没有改变,回顾tcp_process_data_pkt的代码实现,先回复TCP ACK,再将数据部分存入滑动窗口,并更新recv_end到16,所以build_tcp_header时的接收缓存大小没有变化。回复TCP ACK后更新acked end和recv start到16,并通知模拟读取线程从滑动窗口中读取数据,dump_slide_window打印输入可以看到滑动窗口”指针”的移动是正确的,模拟读取线程的输出为”tcp hello world”,表明DIY TCP/IP可以正确接收TCP数据帧并提取数据部分到接收缓存中。
第一个侧面的测试结果
DIY TCP/IP运行在主机A上,主机C(Android手机)与主机A处于同一局域网,运行iperf TCP client。指定DIY TCP/IP的IP地址为局域网中不存在的IP地址192.168.1.7,TCP端口号为7000。主机C通过iperf与DIY TCP/IP进行吞入量测试,通过DIY TCP/IP的打印输出,查看滑动窗口指针在回绕的情况处理是否正确。
DIY TCP/IP打印输出太多,截取其中一个片段来分析,当滑动窗口的recv_end更新到16776488时,有接收到了长度为1448的TCP 数据,同样是先构建TCP ACK更新acked_end和recv_start,将1448的数据放入滑动窗口更新recv_end,这三个指针均发生回绕,回绕的位置为1448 - (16776960 - 16776488)。dump_slide_window的打印输入是符合预期的。
主机C端iperf的吞吐量为3 ~ 5Mbps,由于现阶段滑动窗口的存入和提取都需要拷贝数据,后续章节对滑动窗口实现优化时,提升吞吐量。
本章小结
本章介绍了TCP数据帧的头部结构,TCP选项字段,实现了TCP数据帧的接收,TCP伪头部校验和的检查和计算。实现了简单的TCP连接状态机,和简化的TCP滑动窗口,测试结果表明DIY TCP/IP已经可以正常与别的TCP client通信,并能支持局域网内的iperf 吞吐量测试。后续章节将专注于TCP滑动窗口的性能提升,扩展DIY TCP/IP的库函数接口,将DIY TCP/IP编译成动态库,供其他进应用程序使用。
目录结构
下一阶段:DIY TCP/IP的扩展和性能优化