网络协议栈简单设计(tcp)

104 阅读4分钟

网络协议栈简单设计(tcp)

接着这篇文章写的

TCP相对于Udp,分为两个部分:连接(三次握手、四次挥手)、交互(数据传输)

三次握手

tcp包结构体定义

依照tcp包头字段定义就行:

20230204154214

注意,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数据传输

滑动窗口:

20230204164652
  • 发送端:

慢启动:采用发一确认一的模式,数据发送太慢了,所以tcp允许多个包同时发送,tcp发送端的滑动窗口按照1mss、2mss、4*mssd的指数级增长发送数据,直到到达一直阈值

拥塞避免:达到阈值后,按照线性增长发送数据,如果出现网络抖动了,就会将接收端滑动窗口大小减半,然后继续线程增长 - 网络抖动:一个包出发出去到接收响应包的间隔RTT,如果小于0.1新包的rtt+0.9旧包的rtt,就说明发生了网络抖动,也就是数据接收时延太大

快重传:不等重传定时器过期,只要连续收到三个相同的ack,就立即重传

tip:如果buff里只有50字节的空间,但我要写512字节,会将50个字节写入到buff,并返回50

  • 接收端:

服务器从buff中读取数据,但是当buff剩余空间不足时,比如滑动窗口(可看作buff上的两个指针,滑动窗口前的是可读的,滑动窗口内部的是正在接收组织的,比如丢包超时重传,按照序列号将包进行排列)大小为0,将会通知客户端暂时无法接收数据。对于什么时候恢复数据发送,一般有两种做法:

  • 服务器检测到buff可写时,发送主推消息给客户端;这种方法实时性较高,但是有如下缺点:
    • 主推包丢失 =》 设置定时器发送多次主推 =》 客户端可能关机 =》 服务端资源浪费
    • 不符合TCP协议的潜规则,不利于编程实现,主动的一般是客户端
  • 客户端轮询服务器:TCP就采用这种发送,定时发送探测包

心跳包:TCP内部实现了keeplive机制,但TCP超时了会直接回收tcb,很不灵活。所以需要在用户态设计keeplive策略,比如第一次超时后,将超时间隔设置为当前的2倍

四次挥手:略