赞
踩
在编写TCP套接字的服务器代码时,在进行了套接字的创建和绑定之后,需要调用listen
函数将创建的套接字设置为监听状态,此后服务器就可以调用accept
函数获取建立好的连接了。其中listen
函数的第一个参数就是需要设置为监听状态的套接字,而listen
的第二个参数我们一般设置为16,32,64,128,可是为什么要设置为这些值呢?listen
函数的第二个参数具体的含义是什么呢?下面我们先看代码的实验结果再来说为什么。
下面的代码通过一个实验来说明listen的第二个参数的具体含义:
accept
函数获取底层建立好的连接。listen
函数的第二个参数设置为1。#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 = 1; int main() { // 1. 创建监听套接字 int listen_sock = socket(AF_INET, SOCK_STREAM, 0); if (listen_sock < 0) { std::cerr << "socket error" << std::endl; return 1; } int opt = 1; setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 2. 绑定 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; } // 3. 监听 if (listen(listen_sock, num) < 0) { std::cerr << "listen error" << std::endl; return 3; } //启动服务器 while(true) { //不调用accept获取连接 sleep(1); } return 0; }
运行服务器后使用netstat -natp | grep 进程名
命令,可以看到该服务器当前正处于监听状态。
接下来新建三个会话,在三个会话中我们都使用telnet
进行连接我们的服务器程序。
然后此时我们使用netstat -natp | head -2 && netstat -natp | grep 8081
进行查看连接状态。
我们发现三个telnet
客户端连接的状态都是ESTABLISH
状态,而对于服务器端,却只有两个ESTABLISH
状态和一个SYN_RECV
状态。
对于SYN_RECV
的状态,也就意味着服务器没有收到客户端的ACK,但是这里并不是这样的(丢包的概率是很小的),对刚才客户端发来的ACK,服务器即使收到了但是服务器不能让自己进入ESTABLISH
状态(原因后面说)。
然后你后面使用更多的客户端连接服务器你会发现只有两个客户端能和服务器建立正常的连接,其他的都会让服务器处于SYN_RCVD
状态,而这个状态不稳定,如果一直无法进行三次握手,就会直接放弃建立连接。
实际TCP在进行连接管理时在内核中存在两个连接队列:
ESTABLISHED
状态,但没有被上层调用accept
取走的连接。SYN_SENT
和SYN_RECV
状态的连接,也就是还未完成三次握手的连接。而全连接队列的长度实际会受到listen
第二个参数的影响,全连接队列的长度由两个值决定:
listen
时传入的第二个参数backlog
。net.core.somaxconn
,默认值为128
。真实的全连接队列的长度等于listen
传入的backlog
和系统变量net.core.somaxconn
中的较小值加1
。
通过以下命令可以查看系统变量net.core.somaxconn
的值。
sudo sysctl -a | grep net.core.somaxconn
因为我们实验时设置listen
第二个参数的值为1,此时在服务器端全连接队列的长度就为2
,因此服务器最多只允许有两个处于ESTABLISHED
状态的连接。
所以第三个客户端发来建立连接请求时,此时服务器端就会新增状态为SYN_RECV
的状态,于是放入半连接队列当中,即使服务端收到了客户端的ACK响应,但是全连接队列已经满了,于是第三个连接就没有办法进入全连接队列,于是一直处于SYN_RECV
状态。
在linux
中可以使用 ss 命令,来查看 TCP 全连接队列的情况:
但需要注意的是 ss 命令获取的 Recv-Q/Send-Q
在「LISTEN 状态」和「非 LISTEN 状态」所表达的含义是不同的。
在「LISTEN 状态」时,Recv-Q/Send-Q 表示的含义如下:
Recv-Q
:当前全连接队列的大小,也就是当前已完成三次握手并等待服务端 accept()
的 TCP 连接;Send-Q
:值等于listen
的第二个参数,表示当前全连接最大队列长度 - 1,上面的输出结果说明监听 8081 端口的 TCP 服务,最大全连接长度为 2;在「非 LISTEN 状态」时,Recv-Q/Send-Q 表示的含义如下:
1. 问:不考虑其他参数,是不是listen的第二个参数为多少,服务器就可以同时处理几个连接?
答案是:不是,我们这个实验中我们没有使用accept
函数,于是导致了建立的连接无法从全连接队列中取走,如果我们使用了accept
函数连接被取走,服务器就可以对连接进行处理,全连接队列里面有连接进入,有连接被取走,服务器可以同时对所有已经取走的连接进行处理。
所以这个全连接队列本质是一个缓冲区,用来存放操作系统暂时没有办法处理的连接,这个模型本质也是一个生产者消费者模型。
2. 问:为什么底层要维护连接队列?
一般当服务器压力较大时连接队列的作用才会体现出来,如果服务器压力本身就不大,那么一旦底层有连接建立成功,上层就会立马将该连接读走并进行处理。
服务器端启动时一般会预先创建多个服务线程为客户端提供服务,主线程从底层accept
上来连接后就可以将其交给这些服务线程进行处理:
如果向服务器发起连接请求的客户端很少,那么连接一旦在底层建立好就被主线程立马accept
上来并交给服务线程处理了。
但如果向服务器发起连接请求的客户端非常多,当每个服务线程都在为某个连接提供服务时,底层再建立好连接主线程就不能获取上来了,此时底层这些已经建立好的连接就会被放到全连接队列当中,只有等某个服务线程空闲时,主线程就会从这个全连接队列当中获取建立好的连接。
如果没有这个连接队列,那么当服务器端的服务线程都在提供服务时,其他客户端发来的连接请求就会直接被拒绝,但有可能正当这个连接请求被拒绝时,某个服务线程提供服务完毕,此时这个服务线程就无法立马得到一个连接为之提供服务,所以一定有一段时间内这个服务线程是处于闲置状态的,直到再有客户端发来连接请求。
而如果设置了连接队列,当某个服务线程提供完服务后,如果连接队列当中有建立好的连接,那么主线程就可以立马从连接队列当中获取一个连接交给该服务线程进行处理,此时就可以保证服务器几乎是满载工作的。
3. 问:为什么连接队列既不能太长?
虽然维护连接队列能让服务器处于几乎满载工作的状态,但连接队列也不能设置得太长。
我们都知道 TCP 连接建立是需要三次握手,假设攻击者用大量的假IP地址发送初始连接请求(SYN)数据包,让服务端建立连接,然后切换IP继续发,服务端每接收到一个 SYN 报文,就进入SYN_RCVD
状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
首先这一定是一个综合性的解决方案,TCP作为传输控制协议需要对其进行处理,而上层应用层也要尽量避免遭到SYN洪水攻击。
在应用层:
在传输层:
现在核心的问题就是半连接队列被占满了,但不能简单的扩大半连接队列,就算半连接队列再大,恶意用户也能发送更多的SYN请求来占满,并且维护半连接队列当中的连接也是需要成本的。
于是TCP为了防范SYN洪水攻击,引入了syncookie
机制:
开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。
syncookie
的工作机制如下:
当服务器收到一个SYN请求时,它会生成一个加密的SYN cookie
,该cookie
包含了客户端的IP地址、端口号和服务器使用的一些其他信息,并将cookie
值放到第二次握手报文的「序列号」里,然后服务端回第二次握手给客户端;
服务器不会保存任何关于该SYN连接的状态,即该链接不会进入半连接队列中,但是服务器会保存cookie
的值。
客户端收到SYN + ACK应答后,会解析其中的SYN cookie
,并将其存储起来。
客户端发送带有解析出的SYN cookie
的ACK应答给服务器。
服务器接收到ACK应答后,会验证ACK中的SYN cookie
是否有效。如果有效,服务器会根据SYN cookie
中的信息创建连接(进入全连接队列)并提供服务。
可以看到,当开启了 tcp_syncookies
了,即使受到 SYN 攻击而导致半队列满时,也能保证正常的连接成功建立。
通过使用syncookie
机制,服务器可以避免存储大量未完成的连接状态,从而抵御SYN洪水攻击。这种机制的优势是能够快速且有效地处理大量的SYN请求,并且不需要为每个半连接维护状态信息,减少了服务器的负担。
net.ipv4.tcp_syncookies
参数主要有以下三个值:
那么在应对 SYN 攻击时,只需要设置为 1 即可。
wireshark
是 windows 下的一个网络抓包工具. 虽然 Linux 命令行中有 tcpdump
工具同样能完成抓包,但是 tcpdump
是纯命令行界面,使用起来不如 wireshark
方便。
打开wireshark 2.6.5,主界面如下,选择对应的网卡(这里我们选择WLAN),点击即可进行捕获该网络信息,开始抓取网络包。
在使用Wireshark时可以通过设置过滤器,来抓取满足要求的数据包。
针对IP进行过滤:
ip.src == 源IP地址
。ip.dst == 目的IP地址
。ip.addr == IP地址
等价于ip.src ==源IP地址
or ip.dst == 目的IP地址
。!(表达式)
。针对协议进行过滤:
协议名(只能小写)
。协议名1 or协议名2
。not 协议名 或 !协议名
。针对端口进行过滤(以TCP协议为例):
tcp.port == 端口号
。tcp.port >= 2048
(抓取端口号高于2048的包)。针对长度和内容进行过滤:
udp.length < 30 http.content_length <= 20
。http.request.urimatches "指定内容"
。这里我们抓取指定源IP地址或目的IP地址的数据包,选择完毕记得按回车。
telnet
作为客户端连接上服务器 telnet [ip] [port]
telnet
命令连接该服务器后,就可以抓取到三次握手时双方交互的数据包。telnet
连接后,就可以抓取到四次挥手时双方交互的数据包。(此处四次挥手时进行了捎带应答,第二次挥手和第三次挥手合并在了一起,第二个包不是TCP数据包,是SSH数据包)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。