19.TCP相关实验

理解CLOSE_WAIT状态

c 复制代码
"SYN_SENT" 的英文全拼是 "Synchronize Sent"(使同步发送)。这是与网络通信中的TCP连接状态相关的术语,表示在TCP握手过程中发送了同步信号(SYN)并处于等待确认状态。
    
"FIN" 的英文全拼是 "Finish"。在计算机网络通信中,"FIN" 是一种TCP报文标志,用于表示关闭一个TCP连接的请求。
    
"ACK" 的英文全拼是 "Acknowledgment",它在计算机通信中通常用于确认已成功接收或处理了数据包或消息。
  • 当客户端和服务器在进行TCP通信时,如果客户端调用close函数关闭对应的文件描述符,此时客户端底层操作系统就会向服务器发起FIN请求,服务器收到该请求后会对其进行ACK响应。

  • 但如果当服务器收到客户端的FIN请求后,服务器端不调用close函数关闭对应的文件描述符,那么服务器就不会给客户端发送FIN请求,相当于只完成了四次挥手当中的前两次挥手,此时客户端和服务器的连接状态分别会变为FIN_WAIT_2和CLOSE_WAIT。

我们可以编写一个简单的TCP套接字来模拟出该现象,实际我们只需要编写服务器端的代码,而采用一些网络工具来充当客户端向我们的服务器发起连接请求。

服务器的初始化需要进行套接字的创建、绑定以及监听,然后主线程就可以通过调用accept函数从底层获取建立好的连接了。获取到连接后主线程创建新线程为该连接提供服务,而新线程只需执行一个死循环逻辑即可。

c 复制代码
// 将如下的演示代码放入到test_tcp.cc文件中

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>

const int port = 8081;
const int num = 5;

void* Routine(void* arg)
{
	pthread_detach(pthread_self());
	int fd = *(int*)arg;
	delete (int*)arg;
	while (1){
		std::cout << "socket " << fd << " is serving the client" << std::endl;
		sleep(1);
	}

	return nullptr;
}

int main()
{
	//创建监听套接字
	int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
	if (listen_sock < 0){
		std::cerr << "socket error" << std::endl;
		return 1;
	}
	//绑定
	struct sockaddr_in local;
	memset(&local, 0, sizeof(local));
	local.sin_port = htons(port);
	local.sin_family = AF_INET;
	local.sin_addr.s_addr = INADDR_ANY;

	if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
		std::cerr << "bind error" << std::endl;
		return 2;
	}
    
	//监听
	if (listen(listen_sock, num) < 0){
		std::cerr << "listen error" << std::endl;
		return 3;
	}
	//启动服务器
	struct sockaddr_in peer;
	memset(&peer, 0, sizeof(peer));
	socklen_t len = sizeof(peer);
	
	for (;;){
		int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
		if (sock < 0){
			std::cerr << "accept error" << std::endl;
			continue;
		}
		std::cout << "get a new link: " << sock << std::endl;
		int* p = new int(sock);
		
		pthread_t tid;
		pthread_create(&tid, nullptr, Routine, (void*)p);
	}
	return 0;
}
c 复制代码
// 使用如下命令对上面的代码进行编译
[qwy@VM-4-17-centos test]$ g++ -std=c++11 -pthread  -o test test_tcp.cc

代码编写完毕后运行服务器,并用telnet工具连接我们的服务器,此时通过以下监控脚本就可以看到两条状态为ESTABLISHED的连接。

c 复制代码
 [qwy@VM-4-17-centos test]$ while :; do sudo netstat -ntp | head -2 && sudo netstat -ntp | grep 8081; sleep 1; echo "##################"; done

这两条连接当中,一条是客户端到服务器的连接,另一条就是服务器到客户端的连接。

现在我们让telnet退出,就相当于客户端向服务器发起了连接断开请求,但此时服务器端并没有调用close函数关闭对应的文件描述符,所以当telnet退出后,客户端维护的连接的状态会变为FIN_WAIT_2,而服务器维护的连接的状态会变为CLOSE_WAIT。

理解TIME_WAIT状态

当客户端和服务器在进行TCP通信时,客户端调用close函数关闭对应的文件描述符,如果服务器收到后也调用close函数进行了关闭,那么此时双方将正常完成四次挥手。

但主动发起四次挥手的一方在四次挥手后,不会立即进入CLOSED状态,而是进入短暂的TIME_WAIT状态等待若干时间,最终才会进入CLOSED状态。

  • 我们可以继续刚才的实验,由于telnet退出后服务器端没有调用close关闭对应的文件描述符,因此客户端连接的状态停留在了FIN_WAIT_2状态,而服务器连接的状态停留在了CLOSE_WAIT状态。

  • 要让客户端和服务器继续完成后两次挥手,就需要服务器端调用close函数关闭对应的文件描述符。虽然服务器代码当中没有调用close函数,但因为文件描述符的生命周期是随进程的,当进程退出的时候,该进程所对应的文件描述符都会自动关闭。

  • 因此只需要在telnet退出后让服务器进程退出就行了,此时服务器进程所对应的文件描述符会自动关闭,此时服务器底层TCP就会向客户端发送FIN请求,完成剩下的两次挥手。四次挥手后客户端维护的连接就会进入到TIME_WAIT状态,而服务器维护的连接则会立马进入到CLOSED状态。

解决TIME_WAIT状态引起的bind失败的方法

主动发起四次挥手的一方在四次挥手后,会进入TIME_WAIT状态。如果在有客户端连接服务器的情况下服务器进程退出了,就相当于服务器主动发起了四次挥手,此时服务器维护的连接在四次挥手后就会进入TIME_WAIT状态。

在该连接处于TIME_WAIT期间,如果服务器想要再次重新启动,就会出现绑定失败的问题

因为在TIME_WAIT期间,这个连接并没有被完全释放,也就意味着服务器绑定的端口号仍然是被占用的,此时服务器想要继续绑定该端口号启动,就只能等待TIME_WAIT结束。

但当服务器崩溃后最重要实际是让服务器立马重新启动,如果想要让服务器崩溃后在TIME_WAIT期间也能立马重新启动,需要让服务器在调用socket函数创建套接字后,继续调用setsockopt函数设置端口复用,这也是编写服务器代码时的推荐做法。

setsockopt函数

在Linux中,setsockopt 函数用于设置套接字(Socket)的选项。这些选项可以影响套接字的行为,例如超时设置、缓冲区大小、套接字类型等。以下是 setsockopt 函数的基本用法和示例:

c 复制代码
#include <sys/types.h>
#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

// 返回值
On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.
  • sockfd:套接字描述符,指定要设置选项的套接字。
  • level:选项的级别,通常是 SOL_SOCKET 表示通用套接字选项。也可以是其他协议特定级别,如 IPPROTO_TCPIPPROTO_IP 等。
  • optname:选项的名称,用于指定要设置的选项。可以是以下之一:
    • SO_REUSEADDR:允许重用地址。
    • SO_RCVBUF:接收缓冲区大小。
    • SO_SNDBUF:发送缓冲区大小。
    • SO_RCVTIMEO:接收超时。
    • SO_SNDTIMEO:发送超时。
    • 等等,具体选项取决于您的需求。
  • optval:一个指向包含选项值的缓冲区的指针。
  • optlen:指定选项值的长度。

以下是一个示例,演示如何使用 setsockopt 设置套接字选项:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sockfd;
    int optval = 1;  // 设置选项值
    socklen_t optlen = sizeof(optval);

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 设置 SO_REUSEADDR 选项,允许地址重用
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, optlen) == -1) {
        perror("setsockopt");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 其他操作...
    
    close(sockfd);
    return 0;
}

在上面的示例中,我们创建了一个套接字并使用 setsockopt 设置了 SO_REUSEADDR 选项,允许地址重用。您可以根据需要选择不同的选项和级别,并使用适当的值和长度来设置选项。

解释 setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

setsockopt 函数调用 setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); 的作用是设置套接字 listen_sock 的选项,以允许地址重用。

让我解释一下每个参数的含义:

  • listen_sock:这是一个已创建的套接字,通常是用于监听连接的套接字,例如用于服务器的监听套接字。

  • SOL_SOCKET:这是选项的级别,表示通用套接字选项。在这个上下文中,SOL_SOCKET 表示我们要设置的选项是与套接字本身的属性相关的。

  • SO_REUSEADDR:这是要设置的选项名称,代表 "允许地址重用"。当设置了这个选项后,如果之前使用相同的地址和端口号绑定的套接字处于 TIME_WAIT 状态,那么新套接字可以立即重用相同的地址和端口号,而不需要等待 TIME_WAIT 定时器过期。

  • &opt:这是一个指向选项值的指针。在这里,opt 是一个整数变量,通常设置为1来启用 SO_REUSEADDR 选项。如果您希望禁用该选项,则将 opt 设置为0。

  • sizeof(opt):这是选项值的长度,通常是整数类型的大小。

通过调用 setsockopt 并传递正确的参数,您可以配置套接字以满足特定的需求。在这种情况下,使用 SO_REUSEADDR 选项有助于避免地址和端口号的冲突,从而更容易重新启动服务器。这在服务器编程中很常见,特别是在服务器需要频繁停止和启动的情况下。

演示代码

c 复制代码
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>

const int port = 8081;
const int num = 5;

void *Routine(void *arg)
{
	pthread_detach(pthread_self());
	int fd = *(int *)arg;
	delete (int *)arg;
	while (1)
	{
		std::cout << "socket " << fd << " is serving the client" << std::endl;
		sleep(1);
	}

	return nullptr;
}

int main()
{
	// 创建监听套接字
	int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
	if (listen_sock < 0)
	{
		std::cerr << "socket error" << std::endl;
		return 1;
	}

	设置 SO_REUSEADDR 选项,允许地址重用
	int opt = 1;
    if (setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
        perror("setsockopt");
        close(listen_sock);
		   // EXIT_FAILURE 是一个宏定义,通常等于1
        exit(EXIT_FAILURE);
    }


	// 绑定
	struct sockaddr_in local;
	memset(&local, 0, sizeof(local));
	local.sin_port = htons(port);
	local.sin_family = AF_INET;
	local.sin_addr.s_addr = INADDR_ANY;

	if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
	{
		std::cerr << "bind error" << std::endl;
		return 2;
	}

	// 监听
	if (listen(listen_sock, num) < 0)
	{
		std::cerr << "listen error" << std::endl;
		return 3;
	}

	// 启动服务器
	struct sockaddr_in peer;
	memset(&peer, 0, sizeof(peer));
	socklen_t len = sizeof(peer);

	for (;;)
	{
		int sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
		if (sock < 0)
		{
			std::cerr << "accept error" << std::endl;
			continue;
		}
		std::cout << "get a new link: " << sock << std::endl;
		int *p = new int(sock);

		pthread_t tid;
		pthread_create(&tid, nullptr, Routine, (void *)p);
	}
	return 0;
}

演示结果

连接是由TCP管理的

从上面的实验中可以看到,即便通信双方对应的进程都退出了,但服务器端依然存在一个处于TIME_WAIT状态的连接,这也更加说明了进程管理和连接管理是两个相对独立的单元。连接是由TCP自行管理的,连接不一定会随进程的退出而关闭。

理解listen的第二个参数

在编写TCP套接字的服务器代码时,在进行了套接字的创建和绑定之后,需要调用listen函数将创建的套接字设置为监听状态,此后服务器就可以调用accept函数获取建立好的连接了。其中listen函数的第一个参数就是需要设置为监听状态的套接字,而listen的第二个参数我们一般设置为5,那你知道listen函数的第二个参数具体的含义是什么吗?

下面通过一个实验来说明listen的第二个参数的具体含义:

  • 先编写TCP套接字的服务器端代码,服务器初始化时依次进行套接字创建、绑定、监听,但服务器初始化后不调用accept函数获取底层建立好的连接。
  • 为了方便验证,这里将listen函数的第二个参数设置为2。

代码如下:

c 复制代码
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>

const int port = 8081;
const int num = 2;

int main()
{
	//创建监听套接字
	int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
	if (listen_sock < 0){
		std::cerr << "socket error" << std::endl;
		return 1;
	}
    
	// 设置 SO_REUSEADDR 选项,允许地址重用
	int opt = 1;
    if (setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
        perror("setsockopt");
        close(listen_sock);
		   // EXIT_FAILURE 是一个宏定义,通常等于1
        exit(EXIT_FAILURE);
    }
	
    //绑定
	struct sockaddr_in local;
	memset(&local, 0, sizeof(local));
	local.sin_port = htons(port);
	local.sin_family = AF_INET;
	local.sin_addr.s_addr = INADDR_ANY;
	
	if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
		std::cerr << "bind error" << std::endl;
		return 2;
	}
    
	//监听
	if (listen(listen_sock, num) < 0){
		std::cerr << "listen error" << std::endl;
		return 3;
	}
    
	//启动服务器
	for (;;){
		//不调用accept获取连接
	}
    
	return 0;
}

netstat -nltp

netstat -nltp 是一个用于查看网络统计信息和活动网络连接的Linux命令。让我解释这个命令的各个部分:

  • netstat:这是用于显示网络统计信息的命令。

  • -n:这是netstat命令的选项之一,用于显示数字形式的IP地址和端口号,而不是将它们解析为域名和服务名称。这可以使输出更加清晰和易读。

  • -l:这是另一个选项,它表示仅显示监听(Listening)状态的套接字。这意味着它将显示正在等待连接的服务器端套接字。

  • -t:这个选项指示仅显示TCP协议的套接字。TCP是一种面向连接的协议,通常用于可靠的数据传输。

  • -p:这个选项用于显示与每个套接字相关的进程的信息,包括进程ID(PID)和进程名称。

综合起来,netstat -nltp 命令会显示当前系统上所有处于监听状态的TCP套接字,并列出与每个套接字相关的进程信息。

示例输出可能如下所示:

复制代码
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1234/sshd
tcp6       0      0 :::80                   :::*                    LISTEN      5678/apache2

在这个示例中,netstat -nltp 显示了两个监听状态的TCP套接字。第一个套接字监听本地地址 0.0.0.0 的端口 22,对应的进程是 sshd,其进程ID为 1234。第二个套接字监听IPv6地址 :: 的端口 80,对应的进程是 apache2,其进程ID为 5678

这个命令对于查看系统上运行的网络服务以及它们使用的端口和进程非常有用,有助于诊断网络问题和了解系统的网络活动。

演示

运行服务器后使用netstat -nltp命令,可以看到该服务器当前正处于监听状态

接下来用Postman向我们的服务器发起一个连接请求。

说明一下: 为了让实验现象更加更加明显,这里最好不要用浏览器进行测试,因为浏览器发起连接请求后如果得不到响应会进行重发。实验Postman发起连接请求后,此时通过以下命令就可以看到,此时在服务器端新增了一个连接,该连接当前处于ESTABLISHED状态。

c 复制代码
[qwy@VM-4-17-centos test]$ sudo netstat -ntp | head -2 && sudo netstat -ntp | grep 8081
  • 继续用Postman发起第二个、第三个连接请求。
  • 此时在服务器端也会新增对应的第二个、第三个已经建立好的连接,由于服务器没有调用accept函数获取已经建立好的连接,所以现在服务器端已经建立的连接的个数就是3。
  • 现在继续使用Postman向服务器发起第四次连接请求。
  • 此时在服务器端没有继续新增状态为ESTABLISHED的连接,而是新增了一个状态为SYN_RCVD的连接

现在服务器端新增了一个状态SYN_RCVD的连接,也就意味着服务器没有对刚才客户端发来的连接请求进行响应。

什么是SYN_RCVD

"SYN_RCVD" 的全拼是 "Synchronize Received",它是TCP连接状态的一种,表示已接收到同步信号(SYN)并等待确认以建立连接。

此时就算再用Postman向服务器发起连接请求,在服务器端也不会再新增任何状态的连接了。

而对于刚才状态为SYN_RCVD的连接,由于服务器长时间不对其进行应答,三次握手失败后该连接会被自动释放。

此时在Postman这里就会对应看到如下提示,表示向服务器发起的连接请求没有得到响应。

总结一下上面的实验现象:

  • 无论有多少客户端向服务器发起连接请求,最终在服务器端最多只有三个连接会建立成功。
  • 当发来第四个连接请求时,服务器只是收到了该客户端发来的SYN请求,但并没有对其进行响应。
  • 当发来更多的连接请求时,服务器会直接拒绝这些连接请求。

listen的第二个参数

实际TCP在进行连接管理时会用到两个连接队列:

  • 全连接队列(accept队列)。全连接队列用于保存处于ESTABLISHED状态,但没有被上层调用accept取走的连接。

  • 半连接队列。半连接队列用于保存处于SYN_SENT和SYN_RCVD状态的连接,也就是还未完成三次握手的连接。

而全连接队列的长度实际会受到listen第二个参数的影响,一般TCP全连接队列的长度就等于listen第二个参数的值加一。

因为我们实验时设置listen第二个参数的值为2,此时在服务器端全连接队列的长度就为3,因此服务器最多只允许有三个处于ESTABLISHED状态的连接。

如果将刚才代码中listen的第二个参数值设置为3,此时服务器端最多就允许存在4个处于ESTABLISHED状态的连接。在服务器端已经有4个ESTABLISHED状态的连接的情况下,再有客户端发来建立连接请求,此时服务器端就会新增状态为SYN_RCVD的连接,该连接实际就是放在半连接队列当中的。

此后就算再有客户端发来连接请求,在服务器端也不会新增任何状态的连接。

为什么底层要维护连接队列?

一般当服务器压力较大时连接队列的作用才会体现出来,如果服务器压力本身就不大,那么一旦底层有连接建立成功,上层就会立马将该连接读走并进行处理。

  • 服务器端启动时一般会预先创建多个服务线程为客户端提供服务,主线程从底层accept上来连接后就可以将其交给这些服务线程进行处理。
  • 如果向服务器发起连接请求的客户端很少,那么连接一旦在底层建立好就被主线程立马accept上来并交给服务线程处理了。
  • 但如果向服务器发起连接请求的客户端非常多,当每个服务线程都在为某个连接提供服务时,底层再建立好连接主线程就不能获取上来了,此时底层这些已经建立好的连接就会被放到连接队列当中,只有等某个服务线程空闲时,主线程就会从这个连接队列当中获取建立好的连接。
  • 如果没有这个连接队列,那么当服务器端的服务线程都在提供服务时,其他客户端发来的连接请求就会直接被拒绝。
  • 但有可能正当这个连接请求被拒绝时,某个服务线程提供服务完毕,此时这个服务线程就无法立马得到一个连接为之提供服务,所以一定有一段时间内这个服务线程是处于闲置状态的,直到再有客户端发来连接请求。
  • 而如果设置了连接队列,当某个服务线程提供完服务后,如果连接队列当中有建立好的连接,那么主线程就可以立马从连接队列当中获取一个连接交给该服务线程进行处理,此时就可以保证服务器几乎是满载工作的。

为什么连接队列不能太长?

全连接队列不能太长,系统一般设置为5

虽然维护连接队列能让服务器处于几乎满载工作的状态,但连接队列也不能设置得太长。

  • 如果队列太长,也就意味着在队列较尾部的连接需要等待较长时间才能得到服务,此时客户端的请求也就迟迟得不到响应。
  • 此外,服务器维护连接也是需要成本的,连接队列设置的越长,系统就要花费越多的成本去维护这个队列。
  • 但与其与其维护一个长连接,造成客户端等待过久,并且占用大量暂时用不到的资源,还不如将部分资源节省出来给服务器使用,让服务器更快的为客户端提供服务。

因此虽然需要维护连接队列,但连接队列不能维护的太长

全连接队列的长度

全连接队列的长度由两个值决定:

  • 用户层调用listen时传入的第二个参数backlog。
  • 系统变量net.core.somaxconn,默认值为128。

通过以下命令可以查看系统变量net.core.somaxconn的值。

c 复制代码
[qwy@VM-4-17-centos test]$ sudo sysctl -a | grep net.core.somaxconn

全连接队列的长度实际等于listen传入的backlog和系统变量net.core.somaxconn中的较小值加一。

SYN洪水攻击

连接正常建立的过程:

  • 当客户端向服务器发起连接建立请求后,服务器会对其进行SYN+ACK响应,并将该连接放到半连接队列(syns queue)当中。
  • 当服务器发出的SYN+ACK得到客户端响应后,就会将该连接由半连接队列移到全连接队列(accept queue)当中。
  • 此时上层就可以通过调用accept函数,从全连接队列当中获取建立好的连接了。

连接建立异常:

  • 但如果客户端在发起连接建立请求后突然死机或掉线,那么服务器发出的SYN+ACK就得不到对应的ACK应答。
  • 这种情况下服务器会进行重试(再次发送SYN+ACK给客户端)并等待一段时间,最终服务器会因为收不到ACK应答而将这个连接丢弃,这段时间长度就称为SYN timeout。
  • 在SYN timeout时间内,这个连接会一直维护在半连接队列当中。

此时服务器虽然需要短暂维护这些异常连接,但这种情况毕竟是少数,不会对服务器造成太大影响。

但如果有一个恶意用户故意大量模拟这种情况:向服务器发送大量的连接建立请求,但在收到服务器发来的SYN+ACK后故意不对其进行ACK应答。

  • 此时服务器就需要维护一个非常大的半连接队列,并且这些连接最终都不会建立成功,也就不会被移到全连接队列当中供上层获取,最后会导致半连接队列越来越长。
  • 当半连接队列被占满后,新来的连接就会直接被拒绝,哪怕是正常的连接建立请求,此时就会导致正常用户无法访问服务器。
  • 这种向服务器发送大量SYN请求,但并不对服务器的SYN+ACK进行ACK响应,最终可能导致服务器无法对外提供服务,这种攻击方式就叫做SYN洪水攻击(SYN Flood)。

如何解决SYN洪水攻击?

首先这一定是一个综合性的解决方案,TCP作为传输控制协议需要对其进行处理,而上层应用层也要尽量避免遭到SYN洪水攻击。

  • 比如应用层可以记录,向服务器发起连接建立请求的主机信息,如果发现某个主机多次向服务器发起SYN请求,但从不对服务器的SYN+ACK进行ACK响应,此时就可以对该主机进行黑名单认证,此后该主机发来的SYN请求一概不进行处理。

TCP为了防范SYN洪水攻击,引入了syncookie机制:

  • 现在核心的问题就是半连接队列被占满了,但不能简单的扩大半连接队列,就算半连接队列再大,恶意用户也能发送更多的SYN请求来占满,并且维护半连接队列当中的连接也是需要成本的。
  • 因此TCP引入了syncookie机制,当服务器收到一个连接建立请求后,会根据这个SYN包计算出一个cookie值,将其作为将要返回的SYN+ACK包的初始序号,然后将这个连接放到一个暂存队列当中。
  • SYN cookie 技术通过在服务器上生成加密的cookie(SYN cookie)来应对这种攻击,而不是将连接请求存储在半开放连接队列中。当服务器收到一个TCP连接请求时,它会生成一个SYN cookie,将cookie发送给客户端,而不将请求存储在半开放连接队列中。只有在客户端返回带有正确cookie的确认(ACK)时,服务器才会创建正式的连接。这意味着服务器可以有效地避免半开放连接队列被填满,因为它不必为每个连接请求分配内存。
  • 当服务器收到客户端的ACK响应时,会提取出当中的cookie值进行对比,对比成功则说明是一个正常连接,此时该连接就会从暂存队列当中移到全连接队列供上层读取。

引入了syncookie机制的好处:

  • 引入syncookie机制后,这些异常连接就不会堆积在半连接队列队列当中了,也就不会出现半连接队列被占满的情况了。
  • 对于正常的连接,一般会立即对服务器的SYN+ACK进行ACK应答,因此正常连接会很快建立成功。
  • 而异常的连接,不会对服务器的SYN+ACK进行ACK应答,因此异常的连接最终都会堆积到暂存队列当中。

什么是SYN+ACK包的初始序号

在TCP协议中,SYN+ACK 包的初始序号(Initial Sequence Number,ISN)是在建立新的TCP连接时服务器端(接收端)用于初始化序号的值。ISN 是一个32位的随机数,用于确保每个TCP连接都有唯一的序号。这个序号在TCP通信中的每个数据段中都会用到,用于数据的顺序和可靠传输。

以下是关于SYN+ACK包的初始序号的一些重要事项:

  1. 三次握手:SYN+ACK 包通常在TCP的三次握手握手过程中使用。在三次握手中,客户端首先发送一个带有SYN标志的数据包到服务器,请求建立连接。服务器接收到客户端的请求后,会回应一个带有SYN和ACK标志的数据包,表示接受连接请求,并将其ISN包含在其中。

  2. 随机性:ISN 是一个随机数,通常是由操作系统内核生成的,以增加序号的随机性和安全性。这可以防止攻击者预测序号并发起恶意攻击。

  3. 唯一性:每个TCP连接都应该具有不同的ISN,以确保每个连接的序号是唯一的。这有助于避免数据包在网络中的混淆和错误的数据传递。

  4. 顺序号演变:一旦建立了TCP连接,序号会逐渐增加,用于表示已传输的数据量。这个逐渐增加的过程可以确保数据包的顺序性和可靠传输。

总之,SYN+ACK 包的初始序号(ISN)是服务器用于初始化TCP连接序号的随机数值。它是TCP通信中的一个关键组成部分,用于确保数据的可靠传输和连接的唯一性。

使用Wireshark分析TCP通信流程

wireshark是 windows 下的一个网络抓包工具. 虽然 Linux 命令行中有 tcpdump 工具同样能完成抓包, 但是tcpdump 是纯命令行界面, 使用起来不如 wireshark 方便.

下载 wireshark

链接:https://pan.baidu.com/s/159UUIoZ8b7guWDeuAHoF9A 提取码:k79r

启用 telnet 客户端

参考 https://jingyan.baidu.com/article/95c9d20d96ba4aec4f756154.html

启动 wireshark 并设置过滤器

由于机器上的网络数据报可能较多, 我们只需要关注我们需要的. 因此需要设置过滤器

在过滤器栏中写入

c 复制代码
ip.addr == [服务器 ip]

或者在过滤器中写入

c 复制代码
tcp.port == 9090

则只关注 9090 端口的数据

更多过滤器的设置, 参考

https://blog.csdn.net/donot_worry_be_happy/article/details/80786241

抓包示例

只抓取指定源IP地址或目的IP地址的数据包。

观察三次握手过程

观察确认应答

在 telnet 中输入一个字符

可以看到客户端发送一个长度为 1 字节的数据, 此时服务器返回了一个 ACK 以及一个 9 个字节的响应(捎带应答), 然后客户端再反馈一个 ACK(注意观察 序列号和确认序号)

观察四次挥手

在 telnet 中输入 ctrl + ], 回到 telnet 控制界面, 输入 quit 退出.

实际上是 "三次挥手", 由于捎带应答, 导致其中的两次重合在了一起

TCP与UDP对比

TCP协议

TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。

TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法。

UDP协议

UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议。

使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。

TCP/UDP对比

TCP协议虽然是保证可靠性的协议,但不能说TCP就一定比UDP好,因为TCP保证可靠性也就意味着TCP需要做更多的工作,而UDP不保证可靠性也就意味着UDP足够简单。

  • TCP常用于可靠传输的情况,应用于文件传输,重要状态更新等场景。
  • UDP常用于对高速传输和实时性较高的通信领域,例如早期的QQ、视频传输等。另外UDP可以用于广播。

也就是说,UDP和TCP没有谁最好,只有谁最合适,网络通信时具体采用TCP还是UDP完全取决于上层的应用场景。


版权声明:本文为CSDN博主「2021dragon」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/chenlong_cxy/article/details/125215957

立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法。

UDP协议

UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议。

使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。

TCP/UDP对比

TCP协议虽然是保证可靠性的协议,但不能说TCP就一定比UDP好,因为TCP保证可靠性也就意味着TCP需要做更多的工作,而UDP不保证可靠性也就意味着UDP足够简单。

  • TCP常用于可靠传输的情况,应用于文件传输,重要状态更新等场景。
  • UDP常用于对高速传输和实时性较高的通信领域,例如早期的QQ、视频传输等。另外UDP可以用于广播。

也就是说,UDP和TCP没有谁最好,只有谁最合适,网络通信时具体采用TCP还是UDP完全取决于上层的应用场景。


版权声明:本文为CSDN博主「2021dragon」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/chenlong_cxy/article/details/125215957

相关推荐
MarkHD1 小时前
第八天 - paramiko/ssh模块 - 远程服务器管理 - 练习:批量服务器命令执行工具
运维·服务器·ssh
DBWYX1 小时前
c++项目 网络聊天服务器 实现;QPS测试
c++
写代码的小王吧2 小时前
【安全】Web渗透测试(全流程)_渗透测试学习流程图
linux·前端·网络·学习·安全·网络安全·ssh
XYY3693 小时前
前缀和 一维差分和二维差分 差分&差分矩阵
数据结构·c++·算法·前缀和·差分
longlong int3 小时前
【每日算法】Day 16-1:跳表(Skip List)——Redis有序集合的核心实现原理(C++手写实现)
数据库·c++·redis·算法·缓存
24白菜头3 小时前
C和C++(list)的链表初步
c语言·数据结构·c++·笔记·算法·链表
Tee xm3 小时前
清晰易懂的跨平台 MySQL 安装与配置教程
linux·windows·mysql·macos·安装
啥都鼓捣的小yao3 小时前
利用C++编写操作OpenCV常用操作
开发语言·c++·opencv
GalaxyPokemon3 小时前
MySQL基础 [一] - Ubuntu版本安装
linux·运维·ubuntu
musk12123 小时前
wsl2 配置ubuntu 固定ip
linux·tcp/ip·ubuntu