网络协议栈简单设计(tcp)
接着这篇文章写的
TCP相对于Udp,分为两个部分:连接(三次握手、四次挥手)、交互(数据传输)
三次握手
tcp包结构体定义
依照tcp包头字段定义就行:

注意,tcp协议头不像udp有包长字段,因此TCP在建立连接时,客户端和服务端会协商设置每个报文的最大长度mss,比如send(buff)中buff的数据长度为2k,mss设置为0.5k,那么这个数据将会被切割成4个包进行传输
mtu和mss的区别:mtu处于数据链路层,最小传输单元,通过设置为1500,而mss处于传输层
// TCP协议头
struct tcphdr {
unsigned short sport; // 源目端口
unsigned short dport;
// 初始值:随机值,最大值4G,越界了可从1开始。
// 序列号是字节的数量,不是包的数量,比如客户端发送的第一个tcp包是512个字节,第一个包的序列号是0,发送的第二个包的序列号就是512(或者服务器对这个包的确认好ack就是512),然后是1024...
// 这也是TCP基于流传输的重要体现
unsigned int seqnum;
unsigned int acknum;
unsigned char hdrlen_resv;
// 标志位,通过位操作实现包类别定义,包含FIN、SYN、RST等bit位标志,也可以像udp那样一个个定义
unsigned char flag;
unsigned short window; // 滑动窗口大小,发送端和接收端都有
unsigned short checksum; // 校验和
unsigned short urgent_pointer;
unsigned int options[0];
};
struct tcppkt {
struct ethhdr eh; // 14
struct iphdr ip; // 20
struct tcphdr tcp; // 8
unsigned char data[0];
};
#define TCP_CWR_FLAG 0x80
#define TCP_ECE_FLAG 0x40
#define TCP_URG_FLAG 0x20
#define TCP_ACK_FLAG 0x10
#define TCP_PSH_FLAG 0x08
#define TCP_RST_FLAG 0x04
#define TCP_SYN_FLAG 0x02
#define TCP_FIN_FLAG 0x01
tcb
服务端收到第一次握手后,需要初始化tcb,将连接加入半连接队列,其结构体定义如下,
struct ntcb {
unsigned int sip;
unsigned int dip;
unsigned short sport;
unsigned short dport;
// arp table mac地址可以从arp表读取
unsigned char smac[ETH_ADDR_LENGTH];
unsigned char dmac[ETH_ADDR_LENGTH];
unsigned char status;
};
然后,进行三次握手,主要涉及连接状态的改变,以及每个状态下所作的事情
typedef enum _tcp_status {
TCP_STATUS_CLOSED,
TCP_STATUS_LISTEN,
TCP_STATUS_SYN_REVD,
TCP_STATUS_SYN_SENT,
TCP_STATUS_ESTABLISHED,
TCP_STATUS_FIN_WAIT_1,
TCP_STATUS_FIN_WAIT_2,
TCP_STATUS_CLOSING,
TCP_STATUS_TIME_WAIT,
TCP_STATUS_CLOSE_WAIT,
TCP_STATUS_LAST_ACK,
};
int main() {
struct nm_pkthdr h;
struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
if (nmr == NULL) return -1;
struct pollfd pfd = {0};
pfd.fd = nmr->fd;
pfd.events = POLLIN;
struct ntcb tcb;
while (1) {
int ret = poll(&pfd, 1, -1);
if (ret < 0) continue;
if (pfd.revents & POLLIN) {
unsigned char *stream = nm_nextpkt(nmr, &h);
struct ethhdr *eh = (struct ethhdr *)stream;
if (ntohs(eh->h_proto) == PROTO_IP) {
struct udppkt *udp = (struct udppkt *)stream;
if (udp->ip.type == PROTO_UDP) {
int udplength = ntohs(udp->udp.length);
udp->data[udplength-8] = '\0';
printf("udp --> %s\n", udp->data);
} else if (udp->ip.type == PROTO_ICMP) {
} else if (udp->ip.type == PROTO_TCP) { // tcp协议
struct tcppkt *tcp = (struct tcppkt *)stream;
/*
unsigned int sip = tcp->ip.sip;
unsigned int dip = tcp->ip.dip;
unsigned short sport = tcp->tcp.sport;
unsigned short dport = tcp->tcp.dport;
tcb = search_tcb();
*/
if (tcb->status == TCP_STATUS_LISTEN) { // 监听状态
if (tcp->tcp.flag & TCP_SYN_FLAG) {
tcb->status = TCP_STATUS_SYN_REVD; // 转状态
// send syn, ack pkt // 发送ack
// seqnum, ack
}
} else if (tcb->status == TCP_STATUS_SYN_REVD) {
if (tcp->tcp.flag & TCP_ACK_FLAG) {
tcb->status = TCP_STATUS_ESTABLISHED;
}
}
}
} else if (ntohs(eh->h_proto) == PROTO_ARP) {
}
}
}
}
处理完tcb状态机,就说明三次握手完成了,接下来进入数据传输阶段
tcp数据传输
滑动窗口:

- 发送端:
慢启动:采用发一确认一
的模式,数据发送太慢了,所以tcp允许多个包同时发送,tcp发送端的滑动窗口按照1mss、2mss、4*mssd的指数级增长发送数据,直到到达一直阈值
拥塞避免:达到阈值后,按照线性增长发送数据,如果出现网络抖动
了,就会将接收端滑动窗口大小减半,然后继续线程增长
- 网络抖动:一个包出发出去到接收响应包的间隔RTT,如果小于0.1新包的rtt+0.9旧包的rtt,就说明发生了网络抖动,也就是数据接收时延太大
快重传:不等重传定时器过期,只要连续收到三个相同的ack,就立即重传
- 接收端:
服务器从buff中读取数据,但是当buff剩余空间不足时,比如滑动窗口(可看作buff上的两个指针,滑动窗口前的是可读的,滑动窗口内部的是正在接收组织的,比如丢包超时重传,按照序列号将包进行排列)大小为0,将会通知客户端暂时无法接收数据。对于什么时候恢复数据发送,一般有两种做法:
- 服务器检测到buff可写时,发送主推消息给客户端;这种方法实时性较高,但是有如下缺点:
- 主推包丢失 =》 设置定时器发送多次主推 =》 客户端可能关机 =》 服务端资源浪费
- 不符合TCP协议的潜规则,不利于编程实现,主动的一般是客户端
- 客户端轮询服务器:TCP就采用这种发送,定时发送探测包
心跳包
:TCP内部实现了keeplive机制,但TCP超时了会直接回收tcb,很不灵活。所以需要在用户态设计keeplive策略,比如第一次超时后,将超时间隔设置为当前的2倍
四次挥手:略