您的位置:首页 > 理论基础 > 计算机网络

TCP三次握手和四次挥手

2016-06-04 19:08 671 查看
一、TCP 连接

TCP是一个面向连接的协议,是因为在一个应用进程开始向另一个进程发送数据时,这两个进程必须先建立连接,也称之为“握手”,即它们必须发送些预备报文段,以建立确保数据传输的参数。

TCP连接提供的是全双工服务,也就是单个发送方与单个接收方之间的连接。比如客户端的进程A和服务端的进程B存在一条TCP连接,那么A的数据可以发送到B,B的数据可以发送到A。

二、建立TCP连接(三次握手)
1.



TCP三次握手

三次握手步骤如下:

(1)Client的TCP先向Server的TCP发送连接请求。报文段TCP SYN中不含应用层要发送的数据。报文段首部标志位SYN置1。Client随机选择一个初始化序号ISN, 本例为Client_ISN,将此序号放在报文段TCP SYN 32位序号字段中。Client TCP状态: CLOSED ->SYN_SENT。

(2)一旦包含TCP SYN报文段的的IP数据报到达Server(这里假设到达)。Server会从IP数据报提取出TCP SYN报文段,为该TCP连接分配TCP缓存和变量,并向该客户TCP发送允许连接的报文段。在该报文中也不包含应用层数据,SYN被置1,TCP报文段首部32位确认序号字段被置为Client_ISN + 1,并且ACK也被置为1,对Client发来的报文段进行确认。Server也会随机选择一个初始化序号ISN,本例为Server_ISN。该报文段被称为SYN
ACK报文段。Server TCP状态:Client TCP状态: CLOSED -> LISTEN -> SYN_RCVD。

(3)Client收到Server发送的SYN ACK报文段,Client TCP状态: SYN_SENT -> ESTABLISHED。Client也要为TCP连接分配缓存和变量。此时Client只需要确认Server发送的报文段,因此SYN 置0,ACK置1。报文段首部32位确认序号被置为Server_ISN + 1。此时TCP连接建立完成。Server TCP状态:Client TCP状态: SYN_RCVD -> ESTABLISHED.

1. 为什么需要三次握手,不是两次握手?

如果Client发送的请求连接报文段在某种情况下并没有到达Server,报文段并不是丢失了,而是在某个网络节点长时间滞留,以致延误到连接释放才到Server。这个报文段已经失效,但是Server却误认为是Client发送的请求连接报文。如果是两次握手,此时Server向Client发送ACK包,但Client不会理会Server发来的ACK包,也不会向Server发送ACK包。于是Server以为与Client已经建立连接,等待Client发送数据。此时会造成Server资源的浪费,只能等待Client发送数据。也就是说Client的TCP状态一开始为SYN_SENT状态,等待Server
的SYN + ACK 包进入ESTABLISHED状态。当连接失效时,Client处于CLOSED状态,尽管Server发来SYN + ACK包,Client也不会接收Server发来的包。如果在Server收到失效的SYN包,Client通过调用Connect变为SYN_SENT状态,此时接收到Server发送的SYN + ACK包,变为ESTABLISHED状态。但是Client重新调用Connect函数发送的SYN包会被Server丢弃。发送包由于seq是以被丢弃的SYN包的序号为准,而服务器接收序号是以那个延误旧连接SYN报文序号为准。

“三次握手”的目的是为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

三、TCP连接终止(四次挥手)
1. 因为TCP是全双工,因此每个方向必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。这利用了TCP的半关闭。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。



TCP四次挥手

(1)Client执行主动关闭,FIN置1,向Server发送FIN包,FIN序号与SYN序号,也占用TCP首部32位序号字段。用来关闭Client到Server的数据传送。
(2)
Server接收到Client发送的FIN包,ACK置1,将FIN包32位序号Client_FIN + 1,放置到确认序号,向Client发送ACK包。
(3)
Server再向Client发送FIN包,FIN置1。用来关闭Server到Client的数据传送。
(4)
Client收到FIN包,ACK置1,并将FIN序号Server_FIN + 1,放置到确认序号字段。向Server发送ACK包。
1. 四次挥手的TCP状态
(1)FIN_WAIT1 当Client应用层想注动关闭连接,使用套接字通知TCP,让TCP发送一个FIN包给Server,此时Client TCP进入FIN_WAIT1状态,等待Server发送发送FIN包。
(2)CLOSE_WAIT 表示Server等待关闭。如果没有数据发送,向Client发送一个FIN包,关闭连接。如果Server还有数据发送,向Client发送一个ACK包,然后Server进入CLOSE_WAIT状态。
(3)FIN_WAIT2 此时Client收到Server发送的ACK包,此时处于半关闭状态,只有Client向Server主动关闭连接,Server还可以向Client发送数据。
(4)TIME_WAIT 也称为2MSL等待状态。每个具体的TCP实现必须选择一个最大生存时间MSL。它是任何报文段被丢弃前在网络内的最长时间。由于最后Server收到ACK包并不会向Client发送确认报文,因此需要等待一段时间希望Server收到ACK包,如果因为超时没有收到,Server会重发FIN包,此时Client还会发送ACK包。但之前的ACK包会被丢弃。由于处在TIME_WAIT状态下,实际TCP连接已经断开,但socket还会占用当前的端口,并不能被一个新的连接所使用。
2. 为什么TCP连接终止需要四次。
前面说过,三次握手是因为Client与Server都要确认对方发来的请求连接SYN包。并且Server向Client发送SYN + ACK包时,将SYN与ACK放在了同一个报文段。而连接终止时,Client主动关闭连接时,Server先回复ACK包,但可能还有一些数据没有向Client发送完,处于半连接,所以TCP状态 FIN_WAIT1 -> FIN_WAIT2。如果Server发送了FIN包,Server要与Client断开连接。
四、tcpdump获取TCP连接与终止状态
采用socket编写一个Client与Server,采用本地回环地址作为Client的IP。

Client
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "InOut.h"
#include "err.h"

#define    MAXLINE       4096

void err_sys(const char * fmt, ...)  __attribute__((noreturn));
void err_quit(const char * fmt, ...) __attribute__((noreturn));

int main(int argc, char ** argv) {

struct sockaddr_in       ClientAddr;
char   RecvLine[MAXLINE + 1];

if (argc != 2)
err_quit("usage: a.out <IPaddress>");

int ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);                   /* 定义套接字,使用TCP协议 */
if (ClientSocket < 0)
err_sys("socket error");

bzero(&ClientAddr, sizeof(ClientAddr));

struct in_addr * addrptr = &ClientAddr.sin_addr;

ClientAddr.sin_family = AF_INET;
ClientAddr.sin_port = htons(8888);
if (inet_pton(ClientAddr.sin_family, *(argv + 1), addrptr) < 0)
err_quit("inet_pton error for %s", argv[1]);

if (connect(ClientSocket, (struct sockaddr *)&ClientAddr, sizeof(ClientAddr)) < 0)  /* 与服务端建立连接 */
err_sys("connect error");

scanf("%s", RecvLine);
if (writen(ClientSocket, RecvLine, strlen(RecvLine)) < 0)                            /* 向服务端发送数据 */
err_sys("write error");

close(ClientSocket);

exit(0);
}
Server

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "InOut.h"
#include "err.h"

#define    MAXLINE       4096
#define    LISTENQ       1024
void err_sys(const char * fmt, ...) __attribute__((noreturn));

int main(int argc, char ** argv) {

struct sockaddr_in    ServerAddr;
struct sockaddr_in    ClientAddr;
char   BuffSize[MAXLINE] = {0};
int    SizeAddr = sizeof(ClientAddr);

int    ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);        /* 定义监听套接字,使用TCP协议 */

bzero(&ServerAddr, sizeof(ServerAddr));                                 /* 套接字结构地址初始化0 */
bzero(&ClientAddr, sizeof(ClientAddr));

ServerAddr.sin_family = AF_INET;
ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);
ServerAddr.sin_port = htons(8888);

if (bind(ListenSocket, (struct sockaddr *)&ServerAddr, sizeof(ServerAddr)) < 0)       /* 将监听套接字与端口,IP进行绑定 */
err_sys("bind error");

listen(ListenSocket, LISTENQ);                                                        /* 监听8888端口 */

//    for(; ;) {
int ConnectSocket = accept(ListenSocket, (struct sockaddr*)&ClientAddr, &SizeAddr);   /* 接受客户端的请求连接 */
if (ConnectSocket < 0)
err_sys("accept error");

printf("Client ip is %s\n", inet_ntoa(ClientAddr.sin_addr));
printf("Client port is %d\n", ntohs(ClientAddr.sin_port));

while (readn(ConnectSocket, BuffSize, MAXLINE) < 0) {                                  /* 读取客户端发送的数据 */
err_sys("read error");

if (!strcmp(BuffSize, "quit"))
break;
}
//    }

close(ConnectSocket);                                                                  /* 关闭TCP连接 */
close(ListenSocket);

exit(0);
}

使用tcpdump观察
1. TCP连接状态

root@Ubuntu:~# tcpdump -i lo tcp port 8888 and host 127.0.0.1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
18:06:25.290805 IP localhost.36340 > localhost.8888: Flags [S], seq 1201714844, win 43690, options [mss 65495,sackOK,TS val 2372902 ecr 0,nop,wscale 7], length 0
18:06:25.290923 IP localhost.8888 > localhost.36340: Flags [S.], seq 2836719335, ack 1201714845, win 43690, options [mss 65495,sackOK,TS val 2372902 ecr 2372902,nop,wscale 7], length 0
18:06:25.290953 IP localhost.36340 > localhost.8888: Flags [.], ack 1, win 342, options [nop,nop,TS val 2372902 ecr 2372902], length 0
2. Client向Client发送数据

18:06:29.030547 IP localhost.36340 > localhost.8888: Flags [P.], seq 1:5, ack 1, win 342, options [nop,nop,TS val 2373837 ecr 2372902], length 4
3. TCP终止状态

18:06:29.030657 IP localhost.36340 > localhost.8888: Flags [F.], seq 5, ack 1, win 342, options [nop,nop,TS val 2373837 ecr 2372902], length 0
18:06:29.030715 IP localhost.8888 > localhost.36340: Flags [.], ack 5, win 342, options [nop,nop,TS val 2373837 ecr 2373837], length 0
18:06:29.030775 IP localhost.8888 > localhost.36340: Flags [F.], seq 1, ack 6, win 342, options [nop,nop,TS val 2373837 ecr 2373837], length 0
18:06:29.030788 IP localhost.36340 > localhost.8888: Flags [.], ack 2, win 342, options [nop,nop,TS val 2373837 ecr 2373837], length 0


由以上测试结果可以很明显的观察到TCP的三次握手和四次挥手。
总结
1. 在三次握手阶段考虑了为什么不能进行两次握手,对两次握手进行了分析。在两次握手,一定要注意失效的报文段可能并没有丢弃,可能会到达服务端。
2. 四次挥手的时候重点在于为什么是四次。对TIME_WAIT状态的分析,可能还有没有分析到的地方,会在以后找出来。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  tcp 网络 数据 应用