• 关于TCP关闭想到的一些问题


    一、问题的引入
    在客户端希望通过http协议到服务器来拉取数据时,这种交互大多就是一次性的交互,客户端从httpsvr把数据拉取回来之后,服务器会主动关闭套接口。通常来说,如果是我们通过传统的PC端来连接,这个问题不是很大,因为这些客户端通常就是专门围着这个httpsvr来转的,就等着httpsvr的回包来下锅处理接下来的流程。客户端可以这么任性的等待服务器,进而阻塞整个逻辑。但是在这个“客户端”升级为一个代理服务器,它本身管理并维护了很多和httpsvr之间的多条连接,此时代理服务器就无法像PC客户端那样即时的处理httpsvr的回包,它不再是为一条链路负责,而是需要对多条链路负责,此时httpsvr的客户端就不能保证自己会及时的处理httpsvr的回包(此时的这httpsvr客户端应该会使用异步epoll等待,或者是通过定时器定时查询)。进而导致的问题是当代理回过头来有时间来处理httpsvr的回包时,可能httpsvr端的数据已经关闭了。这时候问题就来了,当httpsvr把socket关闭之后,假设代理服务器没有处理httpsvr的回包吗,当代理服务器抽出时间来读取socket中httpsvr的回包时,这个数据还在吗?再强调一下,此时httpsvr的服务器和代理服务器的socket连接已经关闭。
    二、从TCP连接的主动关闭方来看
    对于BSD socket API来说,一个socket的shutdown接口可以仅关闭发送,也可以仅关闭接收,当然也可以两者都关闭。再看TCP协议中的关闭协议,主动方只是发送一个FIN标志位报文,这个FIN包对应的是shutdown的哪个动作呢?直观上说,应该是在关闭读操作时发送FIN包,因为接收关闭就意味着接下来发送过来的包要吃闭门羹,而这进一步意味着会有数据丢失(即发送方发送的数据没有被接收方接收到),这通常无法容忍,这也就是SIGPIPE的意义。SIGPIPE信号的默认处理动作就是关闭整个进程,感觉操作系统的意思就是把事情闹大,既然数据丢失你都不关系,这么不负责任,那索性把把你直接弄死吧。
    事实上只有当shutdown“发送”时,socket才会向对方发送FIN数据包,这一点可能要从网络的“全双工”模型来看待这个问题。这里的双工就是说这个链路上同时存在两个方向上的数据流,任何一方只能关闭自己这一侧的数据源,而不能直接明确告诉对方我关闭了数据接收。虽然通过shutdown关闭了自己在该端口上的读操作,但事实上这仅仅是一个本地的状态,并不会通过网络传递给地方,事实上TCP的协议中也没有这样的协议来完成这样的事件同步。
    当主动关闭本地连接的send功能之后,如果有本地进程继续向这个socket中写入数据,会收到SIGPIPE信号;如果关闭本地socket的read功能,本地从中读取数据时会马上返回,读取有效数据长度为0。
    三、从TCP连接被动方来看
    前面说到了当对端发送关闭之后,本地会收到对方发送的FIN字段,这个字段说明对方已经保证自己不会再发送数据过来了。这对于本地来说是一个重要信息,它意味着本地的所有当前读取操作和后续的读取操作都不用再等待了,所以所有这样的阻塞进程可以被唤醒,接下来的所有读取动作不会有有效数据返回了。从API的角度来看,如果传入要求读入数据的长度非零而read的返回值为0,则表示说这个文件已经到了文件的结尾,对于socket来说就是对端已经关闭。这个行为从代码中来看主要体现在两个方面,一个是从socket读取时,tcp_recvmsg中判断如果对端已经关闭则直接返回;查询socket状态时可读状态始终置位,相关代码在tcp_poll中。
    还有前面还说到过,对方关闭read并没有通知到本地,如果本地socket在对端shutdown read之后还继续向对方发送数据,就会收到对方的reset回包,这个包会导致之后从该socket 发送和接收数据都返回SIGPIPE错误,相关代码在tcp_sendmsg和tcp_recvmsg中。
    四、一方close socket之后另一方不关闭会怎样
    现在回到开始的问题,代理服务器发送请求,发送后开始休眠,等待下一个定时器到来时检测回包,httpsvr在本地定时器等待时已经关闭了socket,本地操作系统进而完成了协议内的收发包逻辑,当代理服务器在定时器到期后检测该socket时,服务器回包的数据还可以得到吗?从TCP协议上看,主动关闭方并不能要求被动方什么时候关闭链路,它甚至不能要求对方是否一定关闭链路。当服务器关闭了socket,如果对端一直不关闭怎么办呢?就这么一直傻等着那服务器很快就会被DOS攻击了。
    所以操作系统在socket关闭之后会设置一个定时器,用来处理协议的一些善后处理问题,这些状态在TCP协议中有说明,在内核tcp.c文件开始的注释中也有说明,代码中流程为tcp_close-->>tcp_close_state中将socket进入TCP_FIN_WAIT1,在本地socket中所有buffer内数据发送成功并被对方ack之后进入在tcp_rcv_state_process中进入TCP_FIN_WAIT2,同时通过tcp_fin_time(sk)获得超时时间,如果对方在这么长时间内一直不发送FIN包,那么对不起了,我先撤了,自己单方面关闭这个socket了。
    从这个逻辑可以进一步看到,对端关闭事实上对本地socket已经接收到的数据是没有任何影响的,也就是说,在对方socket关闭之前本地socket接收的数据在任何时候通过read接口来读取依然可以获得,前提是不要向对方写入数据。因为写入数据会收到对方的reset信息,操作系统在tcp_rcv_state_process-->>tcp_reset函数中设置sk->sk_err = EPIPE状态,在读取时tcp_recvmsg函数内if (sk->sk_err) copied = sock_error(sk);,这个发送动作可能就是所谓的猪一样的队友吧,如果不发送任何数据,对方不会回复reset,本地通过tcp_recvmsg还可以接收到对方的回包数据。
    这里说的是通常服务器端回包数据较小,可以存储在socket的recv缓冲区中,本地会对对端发送的所有数据进行ack,这样对端可以顺利的在超时之后完成老化关闭。
    五、是否可能实现DOS攻击
    再继续引申一个有趣的问题:假设说客户端发送请求之后,故意不去读取服务器的回包数据,那么此时是否能够用多台机器对服务器形成DOS攻击?回过头来再看下tcp_close,它并不是一个阻塞的过程,当一个进程执行close动作时,此时这个socket(的缓冲区)中可以有未发送或者未确认的数据,这也就是FIN_WAIT1状态的意义,它在等待所有的发送被确认之后才进入FIN_WAIT2。由于TCP中的发送和确认是操作系统层完成的,所以如果说服务器发送数据小于接收侧socket的缓冲区,那么操作系统就会完成除了发送FIN之外的所有和服务器的TCP协议交互,服务器就可以在FIN_TIMEOUT之后释放这个socket资源,客户端如果不读取的话那资源消耗在了客户端,所以不能对服务器构成攻击。
    那么如果客户端的缓冲区小于服务器回包的长度呢?此时服务器将会由于客户端接收窗口不足出现阻塞等待,如果大量的客户端这样来阻塞服务器,那么此时服务器就可能会出现拒绝服务,在内核的tcp_close函数中有这么一层保护,保证系统中不会出现太多的orphan socket,并且它们不会消耗太多的资源,这就杜绝了通过DOS攻击消耗掉操作系统资源的可能性
    if (sk->sk_state != TCP_CLOSE) {
    sk_stream_mem_reclaim(sk);
    if (atomic_read(sk->sk_prot->orphan_count) > sysctl_tcp_max_orphans ||
        (sk->sk_wmem_queued > SOCK_MIN_SNDBUF &&
         atomic_read(&tcp_memory_allocated) > sysctl_tcp_mem[2])) {
    if (net_ratelimit())
    printk(KERN_INFO "TCP: too many of orphaned "
           "sockets ");
    tcp_set_state(sk, TCP_CLOSE);
    tcp_send_active_reset(sk, GFP_ATOMIC);
    NET_INC_STATS_BH(LINUX_MIB_TCPABORTONMEMORY);
    }
    }
    这篇文章描述了这样的现象和效果,作者强调到说要让回包数据量大于socket的发送缓冲区,否则应用进程把数据发送给操作系统之后就相当于完成了这条链路的功能,进而回收了这个用户态的socket pool,所以攻击相当于攻击了操作系统的资源而不是进程本身。
    这里还有另一个TCP中经常提到的问题,就是zero windows detection,当一方知道对方接收窗口已经满了之后,就会周期性的向对方发送 zero windows detection包,它和超时定时器的不同在于,它没有超时错误(至少我没看到),也就是可以尝试任意多次并一直继续下去,而超时定时器在一定次数之后会认为链路已经关闭。
    这篇文章提到了这个问题,下面有人的回答是说用户态进程可以轻易的知道是谁卡了这么久时间,进而结束掉进程。
    六、如何知道对方已经关闭写操作
    对方发送FIN包之后,本地处理逻辑为:
    static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
    {
    struct tcp_sock *tp = tcp_sk(sk);
     
    inet_csk_schedule_ack(sk);
     
    sk->sk_shutdown |= RCV_SHUTDOWN;
    ……
    }
    本地读取数据时
    int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
    size_t len, int nonblock, int flags, int *addr_len)
    {
    ……
    if (sk->sk_shutdown & RCV_SHUTDOWN)
    break;
    ……
    本地poll该socket状态时:
    unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
    {
    ……
    if (sk->sk_shutdown & RCV_SHUTDOWN)
    mask |= POLLIN | POLLRDNORM | POLLRDHUP;
    }
    也就是在异步epoll模式下,poll得到POLLIN状态并且读取socket中长度为零,可以认为对方已经关闭,下面文章描述了通过read返回值判断对方时候关闭了写操作的可行性,并且该网页也包含了很多常见的TCP相关的问题。
     
     
     
     
     
  • 相关阅读:
    python基础学习24----使用pymysql连接mysql
    HTML基本标签
    python基础学习20----线程
    MySQL基础操作
    python永久添加第三方模块,PYTHONPATH的设置
    MySQL压缩包zip安装
    汇编语言debug命令与指令机器码
    python基础学习23----IO模型(简)
    python基础学习22----协程
    python基础学习21----进程
  • 原文地址:https://www.cnblogs.com/tsecer/p/10487660.html
Copyright © 2020-2023  润新知