TCP协议之liststen小解

liststen小议

TCP通过三次握手建立连接的过程应该都不陌生了。从服务器的角度看,它分为以下几步

将TCP状态设置为LISTEN状态,开启监听客户端的连接请求
收到客户端发送的SYN报文后,TCP状态切换为SYN RECEIVED,并发送SYN ACK报文
收到客户端发送的ACK报文后,TCP三次握手完成,状态切换为ESTABLISHED
在Unix系统中,开启监听是通过listen完成。

int listen(int sockfd, int backlog)
listen有两个参数,第一个参数sockfd表示要设置的套接字,本文主要关注的是其第二个参数backlog;
<Unix 网络编程>将其描述为已完成的连接队列(ESTABLISHED)与未完成连接队列(SYN_RCVD)之和的上限。

一般我们将ESTABLISHED状态的连接称为全连接,而将SYN_RCVD状态的连接称为半连接
在这里插入图片描述

当服务器收到一个SYN后,它创建一个子连接加入到SYN_RCVD队列。在收到ACK后,它将这个子连接移动到ESTABLISHED队列。最后当用户调用accept()时,会将连接从ESTABLISHED队列取出。
是 Posix 不是 TCP
listen只是posix标准,不是TCP的标准!不是TCP标准就意味着不同的内核可以有自己独立的实现

[POSIX是这么说的][3]:

The backlog argument provides a hint to the implementation which the implementation shall use to limit the number of outstanding connections in the socket’s listen queue.

Linux是什么行为呢 ? 查看listen的man page

The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests.

什么意思呢?就是说的在Linux 2.2以后, backlog只限制完成了三次握手,处于ESTABLISHED状态等待accept的子连接的数目了。

真的是这样吗?于是我决定抄一个小程序验证一下:

服务器监听8000端口,并且设置backlog = 1。注意,我为了将队列塞满,没有调用accept。
为了排除syncookie的干扰,我首先关闭了syncookie功能
echo 0 > /proc/sys/net/ipv4/tcp_syncookies**

由于我设置的backlog = 1并且服务器始终不会accept。因此预期会建立1?个全连接, 但实际却是看!
在这里插入图片描述

客户器竟然显示成功建立了?3?次连接!
用netstat看看TCP连接状态
客户端是:
在这里插入图片描述

服务端是:
在这里插入图片描述

从上面可以看出,一共有2条连接对是ESTABLISHED<->ESTABLISHED连接, 但还有1条连接对是SYN_RECV<->ESTABLISHED连接, 这表示对客户端三次握手已经完成了,但对服务器还没有! 回顾一下TCP三次握手的过程,造成这种连接对原因只有可能是服务器将客户端最后发送的握手ACK被丢弃了!
还有一个问题,我明明设置的backlog的值是 1,可为什么还能建立2个连接 ?!
去内核找原因
我实验用的机器内核是Red Hat 4.4.6-3
前面提到过已完成连接队列和未完成连接队列这两个概念, Linux有这两个队列吗 ? Linux 既有又没有! 说有是因为内核中可以得到两种连接各自的长度; 说没有是因为 Linux只有已完成连接队列实际存在, 而未完成连接队列只有长度的记录!
每一个LISTEN状态的套接字都有一个struct inet_connection_sock结构, 其中的accept_queue从名字上也可以看出就是已完成三次握手的子连接队列.只是这个结构里还记录了半连接请求的长度!
struct inet_connection_sock {
// code omitted
struct request_sock_queue icsk_accept_queue;
// code omitted
}

struct request_sock_queue {
// code omitted
atomic_t qlen; // 半连接的长度
atomic_t young; // 一般情况, 这个值 = qlen

struct request_sock	*rskq_accept_head;  // 已完成连接的队列头
struct request_sock	*rskq_accept_tail;  // 已完成连接的队列尾
// code omitted

};
所以一般情况下连接建立时,服务端的变化过程是这样的:
收到SYN报文, qlen++,young++
收到ACK报文, 三次握手完成,将连接加入accept队列,qlen–,young–
用户使用accept,将连接从accept取出.
再来看内核收到SYN握手报文时的处理, 由于我关闭了syncookie,所以一旦满足了下面代码中的两个条件之一就会丢弃报文
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)

if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
     inet_csk_reqsk_queue_is_full(sk)) && !isn) {   // 条件1: 半连接 >= backlog
	want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
	if (!want_cookie)
		goto drop;
} 

if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) { // 条件2: 全连接 > backlog 并且 半连接 > 1
	NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
	goto drop;
} 

// code omitted

下面是收到ACK握手报文时的处理

struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst,
struct request_sock *req_unhash,
bool *own_req)
{
// code omitted
if (sk_acceptq_is_full(sk)) // 全连接 > backlog, 就丢弃
goto exit_overflow;

 newsk = tcp_create_openreq_child(sk, req, skb); // 创建子套接字了
 // code omitted

}

以这样就可以解释实验现象了!

前1个连接请求都可以顺利创建子连接, 全连接队列长度 = backlog = 1, 半连接数目 = 0
第2个连接请求, 由于sk_acceptq_is_full的判断条件是>而不是>=,所以依然可以建立全连接
第3个连接请求到来时,由于半连接的数目还没有超过backlog,所以还是可以继续回复SYNACK,但收到ACK后已经不能再创建子套接字了,所以TCP状态依然为SYN_RECV.同时半连接的数目也增加到backlog.而对于客户端,它既然能收到SYNACK握手报文,因此它可以将TCP状态变为ESTABLISHED,
第10个请求到来时, 由于半连接的数目已经达到backlog,因此,这个SYN报文会被丢弃.
内核的问题
从以上的现象和分析中,我认为内核存在以下问题

accept队列是否满的判断用>=比>更合适, 这样才能体现backlog的作用
accept队列满了,就应该拒绝半连接了,因为即使半连接握手完成,也无法加入accept队列,否则就会出现SYN_RECV–ESTABLISHED这样状态的连接对!这样的连接是不能进行数据传输的!
问题2在16年的[补丁][5]中已经修改了! 所以如果你在更新版本的内核中进行相同的实验, 会发现客户端只能连接成功5次了,当然这也要先关闭syncookie

但问题1还没有修改! 如果以后修改了,我也不会意外

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值