1.Tcp在listen时的参数backlog的意义?
①TCP/IP 流程(服务器端):
(1)创建socket套接字。设置协议类型和套接字类型。
int socket(int domain, int type, int protocol);
- domain:地址域:
AF_INET(协议类型)一般使用ipv4协议
取值 含义 AF_INET IPv4协议 AF_INET6 IPv6协议 AF_UNIX UNIX域协议,可用于进程间通讯 AF_UPSPEC 未指定域 AF_ROUTE 路由套接字,内核路由表接口 AF_KEY 密钥套接字,内核密钥表接口 - type: 套接字类型:
常用SOCK_STREAM 流式套接字,SOCK_DGRAM 数据报套接字取值 含义 SOCK_STREAM 字节流套接字,有序的、可靠的、双向的、面向连接的字节流,可用于TCP SOCK_DGRAM 数据报套接字,提供固定长度、无连接、不可靠的报文传递,可用于UDP SOCK_RAW 原始套接字,可用于IP协议编程 SOCK_SEQPACKET 有序分组套接字,固定长度、有序的、可靠的、面向连接的报文传递,可用于SCTP SOCK_STREAM和SOCK_SEQPACKET区别:
前者基于流,无法区分报文的界限,可能需要多次函数调用才能获取一个完整数据报文,后者单次调用就能获得完整报文。 - protocol:协议类型:
0-默认;
流式套接字默认TCP协议IPPROTO_TCP;
数据报套接字默认UDP协议IPPROTO_UDP;取值 含义 0 选择默认协议,通常使用 IPPROTO_TCP TCP IPPROTO_UDP UDP IPPROTO_IP ipv4 IPPROTO_IPV6 ipv6 IPPROTO_ICMP ICMP IPPROTO_RAW 原始IP数据包 - 返回值sockfd:套接字描述符供之后绑定使用,失败:-1
(2)为套接字绑定地址
服务端套接字需要先调用bind函数绑定地址和端口后,才能调用listen函数,将套接字转换为监听套接字,进入监听状态。
客户端调用connect之前可选择性调用bind,如果不调用,由系统决定使用哪个源IP和port。
int bind(int sockfd, struct sockaddr *addr,socklen_t addrlen);
- sockfd:套接字描述符,服务器调用socket函数的返回值。
- addr:描述IP地址I的结构体指针
struct sockaddr_in //IPV4的结构体 struct sockaddr_in6 //IPV6的结构体 struct sockaddr //兼容IPV4和IPV6的结构体
- addrlen:addr地址信息的长度,可由 sizeof() 计算得出。
(3)服务器进入监听状态。
int listen(int sockfd, int backlog);
- sockfd:套接字描述符,服务器调用socket函数的返回值。
- backlog:与未完成连接队列和已完成连接队列总共的最大的等待连接队列数目有关。
-
三次握手
- (1)client发送SYN报文至server,并指明client端的初始化序列号 ISN。其状态变为SYN_SEND。
首部的同步位SYN=1,初始序号seq=x,同步位SYN=1的报文段不能携带数据,但要消耗掉一个序号。
(2)如果server收到请求,将状态改为SYN_RECV,并将请求放至syns队列。以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把client端的 ISN + 1 作为ack 的值,表示收到了client端的SYN包。
在应答报文中同步位SYN=1,确认位ACK=1,确认号ack=x+1,初始序号seq=y。
(3)client端收到SYN报文后,将状态改为ESTABLISHED状态。回复server端一个ACK报文,报文中确认位ACK=1,确认号ack=y+1,序列号seq=x+1(因为是第二次发送)。
server端收到ACK报文后,将状态改为ESTABLISHED状态,双方建立连接。并且将请求从syns队列移除,然后创建新的完全连接,放至accpet队列中。accept
函数会从accpet队列头部处理一个连接。
ACK报文可以携带数据,不携带数据则不消耗序号。 - 服务器需要接收客户端发起的连接,就需要使用队列接收。所以TCP的三次握手中,便产生了半连接状态队列和全连接队列。
- 详情见源码分析:TCP 的backlog详解及半连接队列和全连接队列_Blue summer的博客-CSDN博客_backlog队列
- 半连接状态队列:每个客户端第一次握手都会发送SYN报文,服务器使用半连接状态队列对其进行接收,也称为syns队列。之后服务器会对客户端发送SNY+ACK报文。
队列长度为大于8的以下三个数中最小值+1,向上去最近的2的幂数倍:
backlog:listen()函数的第二个参数;
somaxconn为/proc/sys/net/core/somaxconn的值,默认为128;
tcp_max_syn_backlog为/proc/sys/net/ipv4/tcp_max_syn_backlog的值;
eg: 15 -> 16 <= 2^4 -> 长度为16;
16 -> 17 <=2^5 -> 长度为32;
ps:原理是在调用listen()接口中ipv4里调用的inet_listen()里有一个初始化的操作
inet_csk_listen_start(),在其中设置了max_qlen_log,判断队列是否填满的
方法是,return queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log;
①半连接状态队列满了且没有开启 tcp_syncookies,之后的请求会直接丢弃。
// inet_csk_reqsk_queue_is_full(sk) => 半连接状态队列是否满了 // !isn => 不懂??? // net->ipv4.sysctl_tcp_syncookies == 2 =>没有开启 tcp_syncookies if (inet_csk_reqsk_queue_is_full(sk) && !isn) { want_cookie = tcp_syn_flood_action(sk, skb, "TCP"); if (!want_cookie) goto drop; }
② 恶意DOS攻击就是
建立大量的半连接状态的请求,不回复ACK报文,导致syns队列溢出,
不能保存其它正常的请求。
③查看方式: 用netstat在server端查找处于SYN_REVD的TCP连接。
$ netstat -natp | grep SYN_RECV | wc -l# 查看当前网络中状态是 SYN_RECV 的TCP连接的个数 # -n --numeric 直接使用ip地址,而不通过域名服务器 # -a --all 显示所有连线中的 Socket # -t --tcp 显示 TCP 传输协议的连线状况 # -p --programs 显示正在使用 Socket 的程序识别码和程序名称 # wc -l 统计输出信息的行数或个数 # -s 或 --statistice 显示网络工作信息统计表 $ netstat -natp | grep SYN_RECV | wc -l 256 # 表示目前处于半连接状态的TCP连接有256个 # 注意达到最大值,数字就不会变了
④$ netstat -s | grep "SYNs to LISTEN"
123456 SYNs to LISTEN sockets dropped
可以查看因为ysns队列溢出后,半连接状态队列溢出而被丢弃的次数,注意是累计值。
⑤开启syncookies 功能
开启syncookies后就可以在不使用 SYN 半连接队列的情况下成功建立连接,当开启了 syncookies 功能就不会丢弃连接。
原理:server接收SYN,根据当前的状态计算出一个SYN cookie,放入己方的SYN+ACK报文中发出,当client返回ACK报文时,验证此值,合法则验证成功。
syncookies有三个状态。
0 值,表示关闭该功能;
1 值,表示仅当 SYN 半连接队列放不下时,再启用它;
2 值,表示无条件开启功能;
开启方式:$ echo 1 > /proc/sys/net/ipv4/sysctl_tcp_syncookies
⑥防御SYN攻击的办法
可以增加半连接状态队列的长度;
可以开启tcp_syncookies 功能;
可以减少 SYN+ACK 重传次数;
服务器受到SYN攻击后会有大量处于SYN_RECV的TCP连接,他们会重传
SYN+ACK,到达上限后断开连接。减少重传的次数,加快TCP连接的断开。
$ echo 1 > /proc/sys/net/ipv4/tcp_synack_retries #重传上限设为1次 - 全连接队列:当服务器收到客户端的ACK报文时,将新建一个结构用于转移之前半连接状态队列中对应的报文,交给全连接队列进行管理,也称accept队列。
队列长度为/proc/sys/net/core/somaxconn 与 参数backlog中的最小值。
原理:在调用listen()接口中ipv4里调用的inet_listen()里赋值sk->sk_max_ack_backlog。
①全连接队列满了且没有重传 SYN+ACK包的连接请求多于 1 个,之后的请求也会丢弃。// sk_acceptq_is_full(sk) => 全连接队列是否满了 // inet_csk_reqsk_queue_young(sk) > 1 => 没有重传 SYN+ACK 包的连接请求多于1个 if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) { NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS); goto drop; }
②实际上,丢弃是因为linux参数/proc/sys/net/ipv4/tcp_abort_on_overflow的参数默认为0。
若设置为1,server会发送一个reset
包给client端,表示废掉这个握手过程和这个连接。
ps: 通常建议设为0,请求被丢弃后,只要server没有回复ACK报文,那么请求就会被多次重发,当之后某一时刻accpet队列有空位后,仍旧可以对其进行处理。
③增大全连接队列的方式:调整/proc/sys/net/core/somaxconn与参数backlog,取最小值。
④查看方式:可以使用 ss 命令来查看全连接队列的情况。
$ ss -lnt# ss 命令用来显示处于活动状态的套接字信息 # -l 显示处于监听状态的套接字 # -n 不解析服务名称,以数字方式显示 # -t 只显示TCP套接字 $ ss -lnt State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 *:8088 *:*
LISTEN 状态下,
Recv-Q:当前全连接队列的大小,也就是当前已完成三次握手并等待服务端accept()
的 TCP 连接个数;
Send-Q:当前全连接最大队列长度,上面的输出结果说明监听 8088 端口的 TCP 服务进程,最大全连接长度为 128。
⑤ $ netstat -s | grep overflowed
123456 times the listen quene of a socket overflowed
可以查看因为accpet队列溢出后,全连接队列溢出的次数,注意是累计值。
(4)接收客户端的连接请求
-
accpet函数是一个阻塞型函数,连接成功的队列中如果没有新来的连接,就会一直阻塞,直到有客户端的连接请求到达为止。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
sockfd:套接字描述符,服务器调用socket函数的返回值。
-
addr:客户端的地址信息
-
addrlen:addr地址信息的长度,可由 sizeof() 计算得出。
-
返回值:成功则返回新的socket描述符,失败返回-1。
当服务器接受连接请求后,会使用accept为新的连接重新创建一个socket描述符,用于之后的会话。
(5)接收数据
服务端和客户端都可以使用recv()函数读取网络数据。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
-
sockfd:套接字描述符,连接成功的socket描述符
-
buf:用于指定接收数据的存放位置。
-
len:用于指定希望接收的数据长度。
-
flags:默认0-阻塞式接收
标志 描述 MSG_CMSG_CLOEXEC 为 UNIX 域套接字上接收的文件描述符设置执行时关闭标志 MSG_DONTWAIT 启动非阻塞操作(相当于 O_NONBLOCK) MSG_ERRQUEUE 接收错误信息作为辅助数据 MSG_OOB 如果协议支持,获取带外数据 MSG_PEEK 返回数据包内容而不真正取走数据包 MSG_TRUNC 即使数据包被截断,也返回数据包的长度 MSG_WAITALL 等待知道所有的数据可用(仅 SOCK_STREAM) -
返回值:
错误 : -1;
连接关闭 : 0;
成功 : 实际接受长度>0;
(6)发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
-
sockfd:套接字描述符,连接成功的socket描述符
-
buf:需要发送的数据的存放位置。
-
len:需要发送的数据长度。
-
flags:默认0
标志 描述 MSG_CONFIRM 提供链路层反馈以保持地址映射有效 MSG_DONTROUTE 勿将数据包路由出本地网络 MSG_DONTWAIT 允许非阻塞操作(等价于使用 O_NONBLOCK) MSG_EOR 如果协议支持,标志记录结束 MSG_MORE 延迟发送数据包允许写更多数据 MSG_NOSIGNAL 在写无连接的套接字时不产生 SIGPIPE 信号 MSG_OOB 如果协议支持,发送带外数据 -
返回值:
即使 send()成功返回,也并不表示连接的另一端的进程就一定接收了数据,我们所能保证的只是当 send成功返回时,数据已经被无错误的发送到网络驱动程序上。(7)关闭套接字
int close(int sockfd);
-
sockfd:套接字描述符。
套接字属于文件描述符,在使用完毕后需要调用close函数释放掉,避免内存泄漏。
close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。