1. TCP三次握手过程
TCP(传输控制协议)的三次握手过程是建立TCP连接的基础过程,以下是其步骤:
-
SYN:客户端发送一个SYN(同步)包到服务器以建立连接。在这个数据包里,客户端会随机产生一个序列号(Seq=X)。这个步骤可以看作是客户端向服务器申请连接。
-
SYN-ACK:服务器接收到SYN包后,会发送一个SYN-ACK(同步确认)包作为回应。在这个数据包里,服务器也会随机产生一个序列号(Seq=Y),并且确认客户端的SYN(Ack=X+1)。这个步骤可以看作是服务器接受了客户端的连接请求,并告知客户端已经准备好接收数据。
-
ACK:最后,客户端收到服务器的SYN-ACK包后,还需要发送一个ACK(确认)包,确认服务器的SYN(Ack=Y+1)。这个步骤完成后,TCP连接就建立成功,可以开始传输数据了。
这个过程叫做“三次握手”,是因为在建立连接的过程中,会有三个数据包在客户端和服务器之间传输。这个过程可以确保TCP连接的可靠性,即在数据传输开始前,客户端和服务器都已经确认对方已经准备好建立连接和接收数据。
值得注意的是,在TCP的四次挥手(断开连接的过程)中,也使用了类似的机制来保证连接的可靠关闭。这些过程都是为了使TCP成为一种可靠的,面向连接的,传输控制协议。
以上就是TCP的三次握手过程,而以下是三次握手过程在代码中的一个简单实现:
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
int main(int argc , char *argv[])
{
int socket_desc;
struct sockaddr_in server;
char *message , server_reply[2000];
// 创建socket
socket_desc = socket(AF_INET , SOCK_STREAM , 0);
if (socket_desc == -1)
{
printf("Could not create socket");
}
server.sin_addr.s_addr = inet_addr("93.184.216.34"); // example.com的IP
server.sin_family = AF_INET;
server.sin_port = htons( 80 );
// 第一次握手:发送SYN包,请求建立连接
// 第二次握手:接收SYN-ACK包,表示对方已经准备好建立连接
// 第三次握手:发送ACK包,表示自己已经准备好建立连接
// connect函数就完成了这个过程
if (connect(socket_desc , (struct sockaddr *)&server , sizeof(server)) < 0)
{
puts("connect error");
return 1;
}
puts("Connected\n");
// 发送数据
message = "GET / HTTP/1.1\r\n\r\n";
if( send(socket_desc , message , strlen(message) , 0) < 0)
{
puts("Send failed");
return 1;
}
// 接收并打印服务器回应的数据
if( recv(socket_desc, server_reply , 2000 , 0) < 0)
{
puts("recv failed");
}
puts("Reply received\n");
puts(server_reply);
return 0;
}
在这个例子中,我们创建了一个TCP客户端,该客户端连接到服务器并发送了一个HTTP GET请求。值得注意的是,C语言的socket库将TCP的三次握手过程封装在了connect()
函数中,这在大多数语言中都是通用的。
2. TCP四次挥手过程
TCP的四次挥手过程用于终止一个已经建立的TCP连接。这个过程确保了双方都能终止连接,而且所有的数据包都已经被接收。
以下是TCP四次挥手的过程:
-
FIN:当主动关闭连接的一方(可以是客户端,也可以是服务器)决定关闭连接时,它会发送一个FIN(结束)包到对方,表示数据发送完毕。这个时候,这一方进入FIN_WAIT_1状态。
-
ACK:接收到FIN包的一方会发送一个ACK(确认)包作为回应,并进入CLOSE_WAIT状态。同时,发送FIN包的一方收到ACK后,会进入FIN_WAIT_2状态。
-
FIN:当接收到FIN包的一方的数据也发送完毕后,它会发送一个FIN包到对方,告诉对方它的数据也发送完毕了。这个时候,这一方进入LAST_ACK状态。
-
ACK:最后,发送第一个FIN包的一方收到了最后一个FIN包后,它会发送一个ACK包,并进入TIME_WAIT状态,等待一段时间确保对方收到ACK包后,再关闭连接。
以下是一个使用C语言创建TCP连接并终止的示例代码。注意,C语言的socket库已经在底层处理了TCP的四次挥手过程,我们只需要调用shutdown()
函数,就能完成四次挥手并关闭连接。
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
int main(int argc , char *argv[])
{
int socket_desc;
struct sockaddr_in server;
char *message , server_reply[2000];
// 创建socket
socket_desc = socket(AF_INET , SOCK_STREAM , 0);
if (socket_desc == -1)
{
printf("Could not create socket");
}
server.sin_addr.s_addr = inet_addr("93.184.216.34"); // example.com的IP
server.sin_family = AF_INET;
server.sin_port = htons( 80 );
// 连接到服务器
if (connect(socket_desc , (struct sockaddr *)&server , sizeof(server)) < 0)
{
puts("connect error");
return 1;
}
puts("Connected\n");
// 发送数据
message = "GET / HTTP/1.1\r\n\r\n";
if( send(socket_desc , message , strlen(message) , 0) < 0)
{
puts("Send failed");
return 1;
}
// 接收服务器回应的数据
if( recv(socket_desc, server_reply , 2000 , 0) < 0)
{
puts("recv failed");
}
puts("Reply received\n");
puts(server_reply);
// 关闭连接
if (shutdown(socket_desc, 2) < 0) // 这一步就完成了四次挥手过程
{
puts("Shutdown failed");
}
puts("Disconnected");
return 0;
}
这段代码中的shutdown()
函数就是执行TCP四次挥手过程并断开连接的操作,参数2表示断开输入流和输出流。
请注意,这个示例代码仅仅用于说明,并没有包含错误处理和其他的复杂情况。在真实的编程实践中,我们需要更严谨的代码来处理各种可能的情况。
3. 为什么建立连接需要三次握手,而断开连接需要四次握手
TCP协议设计之初就是为了确保网络通信的可靠性,即数据能够准确无误地从发送方传送到接收方。为了达成这个目标,TCP协议在建立连接和断开连接时都使用了握手的机制。这里的"握手"其实就是信息的交换过程,用于确认双方的状态。
之所以建立连接只需要三次握手,而断开连接却需要四次挥手,是因为服务端在接收到FIN包时,可能还有数据没有发送完,所以需要多出一个步骤来确保所有的数据都能发送完毕。而在建立连接时,只需要交换一次序列号就能建立连接,所以只需要三次握手。
总的来说,无论是三次握手还是四次挥手,其根本目的都是为了确认双方的状态,从而保证TCP连接的可靠性。
4. TIME_WAIT状态持续时间及原因
在TCP四次挥手结束,即收到对方的最后一个ACK后,主动关闭连接的一方会进入TIME_WAIT状态。这个状态会持续一段时间,这段时间的长度通常被设置为2个最大段生存时间(MSL,Maximum Segment Lifetime)。
进入TIME_WAIT状态并持续一段时间的原因主要有两个:
-
保证最后一个ACK能够被对方正确接收:如果对方(被动关闭连接的一方)没有收到最后一个ACK,可能会重新发送FIN包。因此,TIME_WAIT状态可以使主动关闭连接的一方有足够的时间来处理这种情况。
-
让网络中还在传输的数据包有时间消失:即使TCP连接已经关闭,网络中可能还存在着这个连接的旧的数据包。TIME_WAIT状态可以让这些数据包在网络中消失,防止新的连接收到这些旧的数据包。
在C语言的网络编程中,我们无法直接控制TCP连接进入TIME_WAIT状态,因为这是由TCP协议自动处理的。但我们可以通过设置socket选项SO_LINGER来控制连接关闭的行为:
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
int main(int argc , char *argv[])
{
int socket_desc;
struct sockaddr_in server;
char *message , server_reply[2000];
struct linger sl;
// 创建socket
socket_desc = socket(AF_INET , SOCK_STREAM , 0);
if (socket_desc == -1)
{
printf("Could not create socket");
}
// 设置SO_LINGER选项
sl.l_onoff = 1; // 开启SO_LINGER
sl.l_linger = 0; // 设置延迟关闭的时间为0秒
setsockopt(socket_desc, SOL_SOCKET, SO_LINGER, &sl, sizeof(sl));
server.sin_addr.s_addr = inet_addr("93.184.216.34"); // example.com的IP
server.sin_family = AF_INET;
server.sin_port = htons( 80 );
// 连接到服务器
if (connect(socket_desc , (struct sockaddr *)&server , sizeof(server)) < 0)
{
puts("connect error");
return 1;
}
puts("Connected\n");
// 发送数据
message = "GET / HTTP/1.1\r\n\r\n";
if( send(socket_desc , message , strlen(message) , 0) < 0)
{
puts("Send failed");
return 1;
}
// 接收服务器回应的数据
if( recv(socket_desc, server_reply , 2000 , 0) < 0)
{
puts("recv failed");
}
puts("Reply received\n");
puts(server_reply);
// 关闭连接
close(socket_desc); // 这一步会立即关闭连接,而不会进入TIME_WAIT状态
return 0;
}
这段代码中的setsockopt()
函数设置了SO_LINGER选项,使得连接在关闭时不会进入TIME_WAIT状态,而是立即关闭。这个选项可以用于控制连接关闭的行为,但并不会改变TIME_WAIT状态的存在意义和功能。在实际的网络编程中,我们应该根据具体的需求和情况来设置这个选项。
5. 超时重传和快速重传
TCP 协议为了保证网络通信的可靠性,提供了超时重传和快速重传两种重传机制。
1. 超时重传:超时重传是 TCP 协议中最基本的重传机制。在这种机制下,当发送一个数据包后,发送方会启动一个定时器,等待接收方的确认(ACK)。如果在定时器到期之前没有收到确认,发送方就会认为这个数据包可能且很可能丢失,于是就会重传这个数据包。
2. 快速重传:快速重传是 TCP 协议中的另一种重传机制,用于在超时之前就检测到可能的数据包丢失。在这种机制下,当发送方连续收到三个重复的 ACK 时,它就会立即重传可能丢失的数据包,而不需要等待超时。这是因为连续的重复 ACK 是一个信号,表示接收方已经收到了这个丢失数据包之后的数据包,因此这个数据包很可能已经丢失。
以下是一个使用 C 语言的 TCP 服务器的例子,这个服务器会接收客户端的数据,并且发送确认(ACK)。请注意,这个示例仅供理解超时重传和快速重传的概念,并不真实地实现这两种重传机制,因为这些机制是在 TCP 协议的底层实现的,我们通常无法直接在应用程序中控制这些机制。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8080
int main()
{
int sockfd;
struct sockaddr_in server_addr, client_addr;
char buffer[1024];
socklen_t addr_size;
// 创建 socket
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&server_addr, 0, sizeof(server_addr));
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 绑定 socket 到服务器地址
if (bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 开始监听连接
if (listen(sockfd, 3) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
addr_size = sizeof(client_addr);
int new_sock = accept(sockfd, (struct sockaddr *)&client_addr, &addr_size);
// 循环接收数据并发送 ACK
while (1) {
memset(buffer, 0, sizeof(buffer));
ssize_t len = recv(new_sock, buffer, sizeof(buffer) - 1, 0);
if (len <= 0) {
break;
}
printf("Received: %
s\n", buffer);
// 发送 ACK
char *msg = "ACK\n";
send(new_sock, msg, strlen(msg), 0);
}
close(sockfd);
return 0;
}
在这个代码中,我们并没有实现超时重传和快速重传,因为这些是由 TCP 协议自动处理的。但是,通过循环接收数据并发送确认(ACK),我们可以模拟 TCP 协议中的确认和重传机制。实际上,如果网络出现问题,比如数据包丢失,TCP 协议会自动使用超时重传或快速重传来尝试解决这个问题,以保证数据能够正确地从发送方传送到接收方。