这里讲述三次握手,重点是讨论两个应用同时向对方发起请求的情况。
同时请求的情况描述:
应用程序A向B发起请求,本地端口号6001,目标端口号6002.
同时
应用程序B向A发起请求,本地端口号6002,目标端口号6001.
这时相当于互相给对方发送了SYN信号,请求建立链接。
这种情况在TCP/IP详解18.8,同时打开的情况可以看到。
结果是两个程序会成功建立一条从 IPaddrA:6001 到 IPaddrB:6002 的链接。
这里主要讨论两点:
- TCP如何建立链接(三次握手原理)
- 为什么同时请求能建立链接
- 能建立链接意味着什么
一、TCP如何建立链接(三次握手原理)
这里要讨论三次握手,本来不打算写的,翻了下博客,居然没有写过。那就写一下。
这是TCP/IP书中的示意图,讲解了基本的三次握手,四次挥手状态转换。
这里三次握手主要是为了什么呢?为了交换序列号Sequence Number和确认号ACK。
就是上面图中的序号,确认号。
交换这两个有什么用?
序列号和确认号可以提供可靠的服务。保证传送的数据,无差错,不丢失(实际上是丢失后会告知应用),不重复,且按序到达。
二、为什么同时请求能建立链接
这里我们提供一份代码,两个客户端代码大致类似,只是端口号不同。
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 |
#include <Winsock2.h> #include <stdio.h> #pragma comment(lib, "ws2_32") #define SERVER_ADDRESS "127.0.0.1" #define MSGSIZE 1024 char szSendMessage[MSGSIZE]; char szRecvMessage[MSGSIZE]; int main() { // 初始化环境 WSADATA wsaData; WORD wVersionRequested = MAKEWORD(2, 2); int err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) { return 0; } // 创建套接字 SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0); // 绑定套接字 SOCKADDR_IN addrClient; addrClient.sin_addr.S_un.S_addr = htonl(INADDR_ANY); addrClient.sin_family = AF_INET; addrClient.sin_port = htons(6001); bind(sockClient, (SOCKADDR *)&addrClient, sizeof(SOCKADDR)); // 向服务器发送连接请求 // 绑定服务器地址 SOCKADDR_IN addrSrver; addrSrver.sin_addr.S_un.S_addr = inet_addr(SERVER_ADDRESS); addrSrver.sin_family = AF_INET; addrSrver.sin_port = htons(6002); int success = connect(sockClient, (SOCKADDR *)&addrSrver, sizeof(SOCKADDR)); if (success < 0) { DWORD err = GetLastError(); printf("func connect Error Code = %u.\n", err); } while (success == 0) { printf("Send:"); // 从输入获取信息 fgets(szSendMessage, MSGSIZE, stdin); //提供退出功能 if (strcmp(szSendMessage, "quit\n") == 0) { printf("quit...\n"); break; } printf("发送数据...\n"); // 发送数据 send(sockClient, szSendMessage, strlen(szSendMessage), 0); printf("发送完毕...\n"); // 接受数据 int ret = recv(sockClient, szRecvMessage, MSGSIZE, 0); if (ret > 0) { szRecvMessage[ret] = '\0'; // 打印信息 printf("Received [%d bytes]: '%s'.\n", ret, szRecvMessage); } else { printf("func recv return %d.\n", ret); break; } } // Clean up closesocket(sockClient); WSACleanup(); return 0; } |
这份代码是6001端口请求6002端口,假设它叫A,我们复制一份叫B,修改为6002端口请求6001端口。
然后我们用Wireshark抓包看看,他们的请求包是什么。
P.S. 说点题外话,在还记得2MSL等待吗?就是Time_Wait的等待。
客户端执行主动关闭后最终会进入Time_Wait状态,服务端被动关闭不进入Time_Wait状态。
这告诉我们:
- 终止一个客户端程序,并立即重启客户端程序,新的客户程序不能重用相同的本地端口
- 同时连接的情况下,主动关闭的会进入TimeWait。然后就会体会到:func connect Error Code = 10048.
- 所以…我现在就在等着Time_Wait的时间,毕竟我懒得重新编译了。
另外顺便讲讲为什么要有TimeWait:主动关闭方发送最后一个ACK后会进入Time_Wait状态,也就是说需要过2MSL才会回到最初的CLOSE状态。
为什么呢,主要是解决以下两点:
- 实现TCP全双工连接的可靠释放
- 使旧数据包在网络因过期而消失
继续,下面是我们抓包的内容:
没法做到跟书上的一致,这里它返回的太快了(RST…自己去查)。要不我试试一个程序同时两个线程搞两个请求?
代码如下:
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 |
#include <Winsock2.h> #include <stdio.h> #include <thread> #include <windows.h> #pragma comment(lib, "ws2_32") #define SERVER_ADDRESS "127.0.0.1" #define MSGSIZE 1024 char szSendMessage[MSGSIZE]; char szRecvMessage[MSGSIZE]; SOCKADDR_IN addrClientA; SOCKADDR_IN addrClientB; int finishA = 0; int finishB = 0; void ClientA() { SOCKET sockClientA = socket(AF_INET, SOCK_STREAM, 0); bind(sockClientA, (SOCKADDR *)&addrClientA, sizeof(SOCKADDR)); // A向B发送连接请求 int succA = connect(sockClientA, (SOCKADDR *)&addrClientB, sizeof(SOCKADDR)); if (succA < 0) { DWORD err = GetLastError(); printf("func connect Error Code = %u.\n", err); } while (succA == 0) { printf("ASend:"); // 从输入获取信息 fgets(szSendMessage, MSGSIZE, stdin); //提供退出功能 if (strcmp(szSendMessage, "quit\n") == 0) { printf("quit...\n"); break; } printf("发送数据...\n"); // 发送数据 send(sockClientA, szSendMessage, strlen(szSendMessage), 0); printf("发送完毕...\n"); } closesocket(sockClientA); finishA = 1; } void ClientB() { SOCKET sockClientB = socket(AF_INET, SOCK_STREAM, 0); bind(sockClientB, (SOCKADDR *)&addrClientB, sizeof(SOCKADDR)); // B向A发送连接请求 int succB = connect(sockClientB, (SOCKADDR *)&addrClientA, sizeof(SOCKADDR)); if (succB < 0) { DWORD err = GetLastError(); printf("func connect Error Code = %u.\n", err); } while (succB == 0) { // 接受数据 int ret = recv(sockClientB, szRecvMessage, MSGSIZE, 0); if (ret > 0) { szRecvMessage[ret] = '\0'; // 打印信息 printf("B Received [%d bytes]: '%s'.\n", ret, szRecvMessage); } else { printf("func recv return %d.\n", ret); break; } } closesocket(sockClientB); finishB = 1; } int main() { // 初始化环境 WSADATA wsaData; WORD wVersionRequested = MAKEWORD(2, 2); int err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) { return 0; } addrClientA.sin_addr.S_un.S_addr = inet_addr(SERVER_ADDRESS); addrClientA.sin_family = AF_INET; addrClientA.sin_port = htons(6001); addrClientB.sin_addr.S_un.S_addr = inet_addr(SERVER_ADDRESS); addrClientB.sin_family = AF_INET; addrClientB.sin_port = htons(6002); std::thread threadA(ClientA); threadA.detach(); std::thread threadB(ClientB); threadB.detach(); while(finishA == 0 || finishB ==0) { //简单点,做个死循环等着完事了...不考虑同步问题 } // Clean up WSACleanup(); return 0; } |
Wireshark抓包结果如下:
Done…
对比书上的图,除了多出最终的ACK之外,完全一致。
发送SYN消息,然后双方互相发送SYN,ACK,然后最后确认一下对方发送的消息(这一步图中没有)。
通过这三次发包,完成了SEQ序列号和ACK确认号的交换。
三、这意味着什么
没错它完成了三次握手的本质:交换序列号和确认号。
然后接下来我要理解:
- 为什么会发两次SYN?
- 为什么两次SYN的序列号是一致的?
关于第一点:
最初我是简单理解为,收到SYN消息后,进入SYN_RECV状态,然后在这个状态下回复的消息都会有SYN标志。
但是实际上是客户端先发了消息,所以此时应该在SYN_SEND状态,应该接收一个SYN,ACK才对。
所以实际上应该是一个特殊的同时打开。
我们看下面的状态转换图:
图中有些许错误,但是不影响。
我们看,应用发送SYN包,进入SYN_SENT状态(我们认为对端和它状态一致),当它收到SYN包的情况下:
- 会进入同时打开状态,发送SYN, ACK数据包,
- 进入SYN_RECV状态,
- 最后收到ACK,进入ESTABLEISHED(已建立)状态。
那问题来了,它的对端也是SYN_RECV状态,那ACK由谁来发呢??
都发?没错,是该都发,接到对面的请求包,就应该回复一个确认包说自己收到了。
这是图上没有标出来的…
二、为什么两次SYN序列号是相同的…这个…
呵,我只能说,这说明序列号这个值的存储是跟Socket相关的,当Socket创建了之后就确定了,每次需要就读取…就讲得通了。
P.S. 一番瞎猜…没看过源码实现…丢人…