为什么做这个

大部分原因是之前经历的事情给我带来的部分感触吧,让我决定花时间研究一下这些基础知识。一方面为面试做准备,一方面巩固自己作为计算机科班的基础知识水平。


TCP 和 UDP 的区别

  1. 连接方式:
    TCP 是面向连接(connection-oriented)的,UDP 是无连接(connectionless)的,这是最主要的区别,也是造成 TCP 稳定、UDP 不稳定的主要原因之一。
  2. 可靠性:
    TCP 传输前需要经历三次握手,结束时需要四次挥手,可靠性强。
  3. 流量控制与拥塞控制:
    TCP 具备流量控制(flow control)与拥塞控制(congestion control)机制,防止网络过载和数据丢失;UDP 两者都不具备,相对自由。
  4. 传输方式:
    TCP 传输的是字节流(byte stream),接收端按序重组;UDP 传输的是数据报(datagram),不排序,收到什么就是什么。
  5. 传输效率:
    TCP 由于需要建立连接、确认、重传等操作,效率相对低但可靠性高;UDP 开销小、传输速度快,适合竞技游戏这类时延敏感应用。

用 C++ 实现 TCP 客户端与服务端

与 UDP 相比,使用 TCP 时,套接字需要额外调用 listen() 来监听对应的端口。

相关代码
1
2
3
4
5
6
if(listen(listen_fd,SOMAXCONN)==SOCKET_ERROR){
printf("listen failed: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return 1;
}

由于不用关心内部的各种机制,基本不需要额外的代码,只需关注收发。唯一值得提及的是:TCP 是字节流传输,数据不会自己断开,需要提前约定好收发协议,确定每次的内容长度,然后手动截断。

相关代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Client
// ---------- 发:先长度,再数据 ----------
uint32_t len = (uint32_t)strlen(line);
uint32_t net_len = htonl(len);
send(cli_fd,(char*)&net_len,4,0);
int s = send(cli_fd,line,(int)len,0);
if(s==SOCKET_ERROR){
printf("send failed: %d\n",WSAGetLastError());
break;
}

// ---------- 收:同样先长度,再数据 ----------
uint32_t net_rlen;
if(recv_all(cli_fd,(char*)&net_rlen,4)<=0){
printf("服务端关闭了连接\n"); break;
}
int rlen = ntohl(net_rlen);
if(rlen<=0||rlen>1024){
printf("非法长度: %d\n",rlen); break;
}
if(recv_all(cli_fd,buf,rlen)<=0){
printf("服务端关闭了连接\n"); break;
}

这里每次先读 4 个字节的长度前缀,确认数据长度后再读对应长度的数据,做到不粘包读取。

用 C++ 实现 UDP 模拟 TCP 连接

UDP 使用时直接 bind 对应 IP 地址就可以开始工作,无需 listen,十分便捷;而且 UDP 基于数据报形式传输,也无需考虑粘包,一次传输多少,对方就能拿到多少。而要实现 TCP 的稳定传输,首先要规划好传输协议格式。

协议头

由于只是简单模拟,我设计了如下的报文头来模拟 TCP:

偏移 长度 字段 说明
0 2 checksum 校验和(必须落在偶数偏移)
2 1 flags 标志位:SYN / ACK / FIN
3 4 seq 序号(sequence number)
7 4 ack 确认号(acknowledgement number)
11 2 window 接收窗口,未具体实现逻辑

然后就只需要模拟 TCP 的过程,完成三次握手、四次挥手,以及中间的数据传输即可。由于这些步骤有许多重复的部分,我中途借助 AI 完成了部分工作。接下来讲述我的开发过程以及中途遇到的问题。

雏形

首先,我先用 UDP 简单地做了一个收发文本内容的回显,然后在此基础上逐步迭代。这一块和前面 TCP 基本没什么区别,就不再赘述。

校验和的实现

报文头的前两个字节存放校验和字段。每次接收数据报时调用一次校验和算法,判断原数据是否出现偏差。校验和的具体原理是:把所有数据的二进制累加求和,与传输前计算的值比对,以此校验数据是否可靠。

校验和代码段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint16_t inetChecksum(uint8_t *buf,int len){
uint32_t sum=0;
while(len>1){
sum+=(buf[0]<<8|buf[1]);//每次取两个字节分别放到16位的前段和后段,组成的数字加到sum上
len-=2;
buf+=2;
}
if(len==1){
sum+=buf[0]<<8;//总长为奇数就默认后8位是0
}
while(sum>>16){
sum=(0xFFFF&sum)+(sum>>16); //如果sum溢出了16位,就重新将溢出的部分加到前16位上面,直到不溢出
}
return (uint16_t)~sum;//返回反码,这样子接收方校验的时候就可以直接判断是否等于0来直到是否可靠就像是-a+a=0这个样子
}

我考虑过 32 位累加溢出的问题,但这不会导致校验和判断出错:因为校验方式是对称的,两边计算都会溢出,最后结果仍然一致。不过,由于它只是单纯的和运算,无法判断数据顺序是否正确,也防不住恶意篡改(改变数据但和不变)。

超时设置

由于 recvfrom 这个函数是阻塞的,如果中途数据丢失就会无限等待,因此需要 setsockopt 设置超时时间,时间到了自动返回 -1。但是写死的超时时间,要么浪费性能(多等),要么太短导致频繁超时,因此需要动态设置超时时间。于是我使用了 TCP 经典的 RTO 超时重传。

RTO超时重传

RTO(Retransmission TimeOut,重传超时)是决定何时触发重传的倒计时器,其设定是动态且自适应的。建立 TCP 连接时,重传计时器初始化为 3 秒(3000 毫秒),这是一个常见的默认起点。此后,RTO 会根据网络往返时间 RTT(Round-Trip Time,往返时间)实时调整。TCP 使用指数加权移动平均法计算平滑 RTT SRTT(Smoothed RTT,平滑往返时间)和 RTT 的偏差 RTTVAR(RTT Variance,往返时间方差),并按 RTO = SRTT + 4 × RTTVAR 这一经典公式估算下一次的超时时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
double SRTT   = 0;                   // Smoothed RTT,平滑往返时间
double RTTVAR = 0; // RTT Variance,往返时间方差
double RTO = 3000; // Retransmission TimeOut,重传超时(毫秒)
// ★ 初始值必须【足够大】,否则见下方"自举"注释
int sampled = 0; // 采过样没有:第一次采样要特殊处理
void setRTO(ULONGLONG time){
double sample_rtt = (double)(GetTickCount64() - time);

if (!sampled) {
// 第一次:直接赋值。不要从 0 平滑过去,否则要收敛好几轮
SRTT = sample_rtt;
RTTVAR = sample_rtt / 2.0;
sampled = 1;
} else {
// ★ 顺序照 RFC:先用【旧的】SRTT 算 RTTVAR,再更新 SRTT
RTTVAR = (1.0 - BETA) * RTTVAR + BETA * fabs(SRTT - sample_rtt);
SRTT = (1.0 - ALPHA) * SRTT + ALPHA * sample_rtt;
}

RTO = SRTT + 4.0 * RTTVAR;
if (RTO < 100.0) RTO = 100.0; // 下限,防止算飞

printf(" RTT=%-5d SRTT=%-7.1f RTTVAR=%-6.1f → RTO=%.1f\n",
(int)sample_rtt, SRTT, RTTVAR, RTO);
fflush(stdout);
}

由于使用的是已知的算法模型,没有太多需要赘述的地方,只要按 RFC 的内容写出代码即可。唯一需要注意的是:RTO 采样更新只在一次传输成功时进行,因为重传时收到的 ACK 无法确认对应的是哪一次发送,有可能是之前网络延迟导致的旧 ACK(这正是 Karn 算法要解决的问题)。这样 RTO 才能收敛;而 RTO 的扩增则直接在超时的时候 ×2(即指数退避)即可,但要注意给一个最大上限,否则会无限指数增大。

Go-Back-N 重传

说到重传,就不得不提 GBN(Go-Back-N,回退 N 步)这个特点:TCP 的超时重传中,只要当前窗口内有一个包超时,那么所有的包都要重传。实现逻辑没有什么特别好说道的地方。

Go-Back-N 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int reSendJud(SOCKET fd,sockaddr_in addr){
if(last_timeout_base!=base)
tries=0;
last_timeout_base=base;
bool isTimeOut=false;
ULONGLONG tNow=GetTickCount64();
for(uint32_t i=base;i<next_seq;i++){
PktStatus &s=slots[i%WINDOWS_LEN];
if((ULONGLONG)(s.sendTime + RTO) <= tNow){
if(tries>=MAX_TRIES){
printf("seq=%u 重传 %d 次仍未确认,本次传输失败\n", i, tries);
fflush(stdout);
return XFER_TIMEOUT;
}
isTimeOut=true;
tries++;
printf("超时,第 %d 次重传 seq=%u --> seq=%u(本次 RTO=%d 毫秒)\n",
tries, base,next_seq-1, (int)RTO);
fflush(stdout);
break;
}
}
if(isTimeOut){
for(uint32_t i=base;i<next_seq;i++){
PktStatus &tmp=slots[i%WINDOWS_LEN];
sendto(fd,tmp.pkt,tmp.len,0,(sockaddr*)&addr,sizeof(addr));
tmp.sendTime=GetTickCount64();
}
RTO*=2;
if(RTO>=60000)
RTO=60000;
}
return XFER_OK;
}

三次握手

三次握手算是很经典的 TCP 知识了。需要三次握手,是为了在不可靠的网络上建立相对可靠的连接。那为什么不是两次?非要三次?

三次握手

主要原因有两方面:

  1. 三次握手确保双方都知道对方的起始序列号(ISN,Initial Sequence Number,初始序列号);两次握手的话,只有客户端能确认服务端收到了自己的起始序列号。
  2. 三次握手可以防止过期到达的 SYN 让服务端进入单方面的连接状态,浪费资源。

总结:三次握手给双方都提供了判断对方是否收到自己信息的方式,而两次握手只有客户端知道对方是否收到,服务端没法确认。

四次挥手

四次挥手的相关问题:为什么是四次?CLOSE_WAIT、TIME_WAIT 分别有什么作用?

四次挥手

  1. CLOSE_WAIT 阶段:被动关闭的一方继续发送剩余数据的阶段。
  2. TIME_WAIT 阶段:主动关闭的一方留出一段时间,保证对方已经收到 ACK;若对方未收到,会重传 FIN,此时还能再补一次 ACK,防止对方卡在这个阶段。同时,也能确保新连接建立时,旧连接的残余报文已在网络中消亡,不产生其他影响。
  3. 为什么是四次?可以两两来看:每一方发出的信息都需要得到对方确认,防止遗漏。
握手代码

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
int findServer(SOCKET fd, sockaddr_in addr){
int lenf = sizeof(addr);

// ---------- ① 组 SYN ----------
char syn[HDR_LEN] = {};
syn[FLAGS_INDEX] = flags::SYN;
uint32_t cli_isn = htonl(ISN);
memcpy(syn + SEQ_INDEX, &cli_isn, 4);
setChecksum((uint8_t*)syn, HDR_LEN);

// ---------- ② 发 + 等 ----------
char rbuf[HDR_LEN];
sockaddr_in from{};
int tries = 0;
ULONGLONG sendTime = GetTickCount64();

int n = sendAndWait(fd, syn, HDR_LEN, addr,
rbuf, sizeof(rbuf), from,
RTO, MAX_TRIES, &tries);
if (n < HDR_LEN) return CONN_TIMEOUT; // sendAndWait 返回 -1
if (tries == 1) setRTO(sendTime); // Karn:重传过就不采样

// ---------- ③ 判别 SYN|ACK ----------
// ★★ 必须从 rbuf 读!从 syn 读等于在解析【自己刚发出去】的那个包,
// 那里 ACK 字段是 0,于是 ack!=2 恒成立 → 握手永远 -3
if (inetChecksum((uint8_t*)rbuf, n) != 0) return CONN_BAD_CHECKSUM;
if (rbuf[FLAGS_INDEX] != (flags::ACK | flags::SYN)) return CONN_BAD_TYPE;

uint32_t got_ack, got_seq;
memcpy(&got_ack, rbuf + ACK_INDEX, 4); // 他确认了我哪个号
memcpy(&got_seq, rbuf + SEQ_INDEX, 4); // 他自己的序号
got_ack = ntohl(got_ack);
got_seq = ntohl(got_seq);

// 他该确认我的 ISN+1,且他的序号该是他的 ISN
if (got_ack != ISN + 1 || got_seq != ISN) return CONN_BAD_SEQ;

srv_next_seq = got_seq + 1; // 挥手时 ACK 用

char ackp[HDR_LEN] = {};
ackp[FLAGS_INDEX] = flags::ACK;
uint32_t my_seq = htonl(ISN + 1); // 我的下一个序号
uint32_t my_ack = htonl(got_seq + 1); // 期望对方的下一个序号
memcpy(ackp + SEQ_INDEX, &my_seq, 4);
memcpy(ackp + ACK_INDEX, &my_ack, 4);
setChecksum((uint8_t*)ackp, HDR_LEN);
sendto(fd, ackp, HDR_LEN, 0, (sockaddr*)&addr, lenf);

// ---------- ⑤ 序号空间对齐 ----------
// SYN 已经吃掉 ISN 这个号,所以数据必须从 ISN+1 开始。
base = ISN + 1;
next_seq = ISN + 1;

printf("握手完成:我数据 seq 从 %u 起,期望对方 seq 从 %u 起\n",
base, srv_next_seq);
return CONN_OK;
}

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
ConnResult acceptClient(SOCKET fd, sockaddr_in &peer, char *buf){
// ---------- ① 校验第一个包 ----------
if (inetChecksum((uint8_t*)buf, HDR_LEN) != 0) return CONN_BAD_CHECKSUM;
if (buf[FLAGS_INDEX] != flags::SYN) return CONN_BAD_TYPE;

uint32_t cli_isn;
memcpy(&cli_isn, buf + SEQ_INDEX, 4);
cli_isn = ntohl(cli_isn);

// ---------- ② 组 SYN|ACK ----------
buf[FLAGS_INDEX] = (flags::ACK | flags::SYN);
uint32_t my_isn = htonl(ISN);
uint32_t ack_back = htonl(cli_isn + 1); // ★ 对方 ISN+1
memcpy(buf + SEQ_INDEX, &my_isn, 4);
memcpy(buf + ACK_INDEX, &ack_back, 4);
setChecksum((uint8_t*)buf, HDR_LEN);

// ---------- ③ 发 + 等第三个 ACK ----------
char rbuf[HDR_LEN]; // ★ 和 buf 分开!
sockaddr_in from{};
int tries = 0;

int n = sendAndWait(fd, buf, HDR_LEN, peer,
rbuf, sizeof(rbuf), from,
300.0, MAX_TRIES, &tries);
if (n < HDR_LEN) return CONN_TIMEOUT; // ★ 超时 → 调用方别建记录

// ---------- ④ 判别第三个包 ----------
if (inetChecksum((uint8_t*)rbuf, HDR_LEN) != 0) return CONN_BAD_CHECKSUM;
if (rbuf[FLAGS_INDEX] != flags::ACK) return CONN_BAD_TYPE;

uint32_t got_ack;
memcpy(&got_ack, rbuf + ACK_INDEX, 4);
if (ntohl(got_ack) != ISN + 1) return CONN_BAD_SEQ;

return CONN_OK;
}
挥手代码

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
int closeConn(SOCKET fd, sockaddr_in addr){
int lenf = sizeof(addr);
char rbuf[MAX_PKT + 1];
sockaddr_in from{};
bool got_fin_ack = false;
bool got_peer_fin = false;

// ---------- 组 FIN ----------
// ★ FIN 和 SYN 一样【占一个序号】,所以用 next_seq 这个号
uint32_t fin_seq = next_seq;
char fin[HDR_LEN] = {};
fin[FLAGS_INDEX] = (flags::FIN | flags::ACK);
uint32_t s = htonl(fin_seq);
uint32_t a = htonl(srv_next_seq); // 顺手捎带确认对方最后那个包
memcpy(fin + SEQ_INDEX, &s, 4);
memcpy(fin + ACK_INDEX, &a, 4);
setChecksum((uint8_t*)fin, HDR_LEN);
next_seq++; // ★ 只加这一次,重发时原样再发

// ============ ① FIN_WAIT_1:发 FIN,等它的 ACK ============
printf("[%s] 发送 FIN seq=%u\n", stateName(ST_FIN_WAIT_1), fin_seq);

int tries = 0;
double rto = RTO;
while (tries < MAX_TRIES){
sendto(fd, fin, HDR_LEN, 0, (sockaddr*)&addr, lenf);

DWORD w = (DWORD)rto;
if (w < 100) w = 100;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&w, sizeof(w));

int fl = sizeof(from);
int r = recvfrom(fd, rbuf, MAX_PKT, 0, (sockaddr*)&from, &fl);
if (r < HDR_LEN){ // 超时或残包
tries++;
rto *= 2; if (rto > 60000) rto = 60000;
printf(" FIN 没等到 ACK,第 %d 次重发\n", tries);
continue;
}
if (inetChecksum((uint8_t*)rbuf, r) != 0) continue; // 坏包,当没收到

uint8_t f2 = rbuf[FLAGS_INDEX];
uint32_t ack_num;
memcpy(&ack_num, rbuf + ACK_INDEX, 4);
ack_num = ntohl(ack_num);

if ((f2 & flags::ACK) && ack_num == fin_seq + 1){
got_fin_ack = true;
printf("[%s → %s] 我的 FIN 被确认了(ack=%u)\n",
stateName(ST_FIN_WAIT_1), stateName(ST_FIN_WAIT_2), ack_num);

if (f2 & flags::FIN){
uint32_t hs;
memcpy(&hs, rbuf + SEQ_INDEX, 4);
srv_next_seq = ntohl(hs) + 1;
MySendAck(fd, addr, srv_next_seq, rbuf);
got_peer_fin = true;
}
break;
}
// 序号对不上 = 过期的旧 ACK,忽略,继续等
}
if (!got_fin_ack){
printf("挥手失败:FIN 重传 %d 次仍无确认\n", tries);
return XFER_TIMEOUT; // 连接语义上已经不可靠了,直接放弃
}

// ============ ② FIN_WAIT_2:等对端的 FIN ============
ULONGLONG fw2_deadline = GetTickCount64() + FIN_WAIT2_MS;
printf("[%s] 等对端的 FIN……\n", stateName(ST_FIN_WAIT_2));

while (!got_peer_fin && GetTickCount64() < fw2_deadline){
DWORD w = 500;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&w, sizeof(w));

int fl = sizeof(from);
int r = recvfrom(fd, rbuf, MAX_PKT, 0, (sockaddr*)&from, &fl);
if (r < HDR_LEN) continue; // 纯超时,接着等
if (inetChecksum((uint8_t*)rbuf, r) != 0) continue;

if (rbuf[FLAGS_INDEX] & flags::FIN){
uint32_t his_seq;
memcpy(&his_seq, rbuf + SEQ_INDEX, 4);
his_seq = ntohl(his_seq);
srv_next_seq = his_seq + 1; // 他的 FIN 也占一个号

printf("收到对端 FIN seq=%u,回 ACK(ack=%u)\n", his_seq, srv_next_seq);
MySendAck(fd, addr, srv_next_seq, rbuf);
got_peer_fin = true;
}
// 迟到的数据包 / 旧 ACK:丢弃
}
if (!got_peer_fin){
printf("挥手失败:等对端 FIN 超时(对端失联)\n");
return CONN_PEER_GONE;
}

// ============ ③ TIME_WAIT:2×MSL ============
// TIME_WAIT 存在的理由,就是在这 2 秒里随时能再补一个 ACK。
//
// 另一个作用:让本次连接的残留报文在网络里自然消亡,
// 免得串进下一个碰巧复用同一组 IP:端口的新连接。
printf("[%s] 开始 2×MSL 等待(%d 毫秒)\n", stateName(ST_TIME_WAIT), TIME_WAIT_MS);

ULONGLONG tw_start = GetTickCount64();
while (1){
ULONGLONG elapsed = GetTickCount64() - tw_start;
if (elapsed >= TIME_WAIT_MS) break;

// ★ 超时用【剩余时间】。若写成固定的 500ms,一次空唤醒就把 2MSL 拖长了
DWORD w = (DWORD)(TIME_WAIT_MS - elapsed);
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&w, sizeof(w));

int fl = sizeof(from);
int r = recvfrom(fd, rbuf, MAX_PKT, 0, (sockaddr*)&from, &fl);
if (r < HDR_LEN) continue; // 纯超时:什么都不做
if (inetChecksum((uint8_t*)rbuf, r) != 0) continue;

if (rbuf[FLAGS_INDEX] & flags::FIN){ // ★★ 对端没收到我的末次 ACK
printf(" [TIME_WAIT] 收到重复 FIN → 重发 ACK,2×MSL 重新计时\n");
MySendAck(fd, addr, srv_next_seq, rbuf);
tw_start = GetTickCount64(); // ★ 只有这一种情况才重置
}
// 其它任何东西(迟到的数据、旧 ACK、乱序包):丢弃,计时器绝不动
}

printf("[%s] 2×MSL 结束,连接彻底关闭\n", stateName(ST_CLOSED));
return XFER_OK;
}

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
if (buf[FLAGS_INDEX] & flags::FIN){

if (c->state == ST_LAST_ACK){
// 我已经发过 FIN 了,这是对方重传的旧 FIN —— 再回一次 ACK 就行
MySendAck(fd, peer, c->last_seq + 1, buf);
return;
}

if (peer_seq == c->last_seq + 1){ // 新的 FIN,也占一个序号
c->last_seq = peer_seq; // ★ 和累积 ACK 同一套校验
c->state = ST_CLOSE_WAIT;
c->state_deadline = GetTickCount64() + CLOSE_WAIT_MS;
printf("[%s:%d] 收到 FIN seq=%u → CLOSE_WAIT,停留 %d 毫秒\n",
inet_ntoa(peer.sin_addr), ntohs(peer.sin_port),
peer_seq, CLOSE_WAIT_MS);
}
// ★ 不管是新 FIN 还是重复 FIN,都必须回 ACK —— 对方可能只是没收到我的 ACK。
MySendAck(fd, peer, c->last_seq + 1, buf);
fflush(stdout);
return;
}

// ============ ② 我在等末次 ACK,只认这个 ============
if (c->state == ST_LAST_ACK){
uint32_t got_ack;
memcpy(&got_ack, buf + ACK_INDEX, 4);
got_ack = ntohl(got_ack);
if (got_ack == c->my_next_seq){
printf("[%s:%d] 收到末次 ACK → 连接彻底关闭,回收记录\n",
inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
fflush(stdout);
DeleteClient(peer);
}
return;
}

总结

TCP 相较于 UDP 最大的区别就是:TCP 会进行双端的信息交互,共同维护连接状态。
本来打算把tcp做完的,但是发现每次添加新东西的时候有好多内容要改,然后做到流控这里就放弃了,半途而废这一块