Java:TCP连接中有序和坏序连接释放的比较

本文包含如下主题:

概要

问题描述

问题如何避免


概要

       如何保证TCP连接能有序或优雅的释放对于TCP网络应用是一个挑战。如果对此问题没有特别关注,可能会导致坏序的或不优雅的释放。java中未预料到的坏序释放会导致应用在读写socket时收到java.net.SocketException。正常情况下read()和write()会返回数字值,分别表示接收或发送的字节数。
       如果收到异常,说明连接中断了并且数据可能已丢失或被抛弃。除了应用主动中断连接的场景外,此文将介绍导致socket连接中断的原因,并提供一些帮助以避免这种场景。

问题描述

       首先,需要区分坏序和有序连接释放的区别。为了理解这种区别需要看看在TCP协议层发生了什么。可以将TCP连接想像成2个分开的半独立的数据流。如果存在A和B两端,一个流从A到B传输数据,另一个从B到A传输数据。有序的释放会在2种场景中发生。一边(比如A)决定停止发送数据并发送FIN消息给B。当B的TCP栈收到FIN后它知道不再会收到从A发来的数据了,B将socket中已有数据读取完成后,再进一步读取将会返回-1,表示已是文件结尾(end-of-file)。这个过程就叫TCP半关闭,因为只有半个连接关闭了。同样这个过程对另一边也完全一样,B向A发送FIN消息,B从socket中读取完从A发来的已有数据后最终也会收到-1。

       相反,坏序的关闭使用RST(Reset)消息。如果某一边发送RST,这意味着整个连接中断,TCP栈会抛弃队列中的所有未发送或未接收的数据。
    
       那么java应用如何执行有序和坏序释放?我们先考虑坏序释放。自从最初BSD socket出现开始就存在一条规则:"linger" socket参数能用来执行坏序连接释放。任意应用都可以通过调用Socket.setLinger (true, 0)告诉TCP栈当socket关闭了会采用坏序(RST)流程。设置linger参数不会立即起作用,只有当调用Socket.close(),然后连接中断就会发送RST消息。一会我们会看到其它方式使连接中断,但这个是最简单的方式。

      与坏序释放一样,close()也用来执行有序释放。简单来说,有序释放和坏序释放的区别可能只是没有设置上面描述过的需在调用Socket.close()之前配置的linger参数。以互联的两端A和B为例,如果A调用Socket.close(),FIN消息从A发送给B;然后当B调用Socket.close(),FIN消息从B发送给A。事实上,linger参数的默认配置不会使用无序关闭;所以如果2个应用终止它们的连接只使用Socket.close(),结果将是有序释放,那么问题是什么呢?

       问题是Socket.close()的语义与TCP FIN消息有细微差别。发送一个FIN消息意思是"我已结束发送",而Socket.close()的意思是"我已结束发送和接收"。当调用Socket.close(),明确地表示不能再发送数据也不能接收数据。所以会发生什么呢,举个例子,当A试图采用关闭socket的方式进行有序关闭,但是B继续发送数据?这种场景TCP规范完全允许,因为就TCP而言,只有半个连接关闭了。但因为A的socket关闭了,就算B继续发送,A也读取不到数据了。这种情况下A的TCP栈必须发送RST去强制终止连接。

      另一种可能会导致不可预知的SocketException的场景是:如果A已经向B发送数据,但是B在关闭socket前没有将所有数据都读取出来。这种场景中B的TCP栈知道一些数据实际上会丢失,它会强迫使用RST终止连接,而不会使用有序的FIN流程。如果A仍尝试使用socket发送或接收数据它将会得到SocketException。

问题如何避免

有一些办法可以避免这个问题:
       1.使用更高层次的协议构造数据。比如基于TCP之上的HTTP,它使2端清楚成知道什么时候可以关闭socket从而解决这个问题。
       2.总是在关闭socket前消费数据,这能避免上面描述的第二个场景。
       3.使用shutdownOutput(),此方法跟close()一样可以发送FIN表示本端已经结束发送,但它仍能从socket读取直到远端发送FIN并且文件结尾(end-of-file)可从流中读取到。然后socket可以使用Socket.close()关闭。

原文:

Orderly Versus Abortive Connection Release in Java


This article covers the following topics:

Overview

To ensure orderly or graceful release of TCP connections is a challenge with TCP networking applications. Abortive or ungraceful release may result if special care is not taken. In Java, an unexpected abortive release can manifest itself by the application receiving a java.net.SocketException when reading or writing to the socket. read() and write() normally return a numeric value indicating, respectively, the number of bytes received or sent. If an exception is received instead, this indicates the connection has been aborted and also that data may have been lost or discarded. This article explains what causes socket connections to be aborted and provides tips for avoiding the situation, except for the case where an application intends to abort the connection.

Description of the Problem

First, we need to distinguish the differences between an abortive and an orderly connection release. To understand this distinction we need to look at what happens at the TCP protocol level. It is helpful to imagine an established TCP connection as actually two separate, semi-independent streams of data. If the two peers are A and B, then one stream delivers data from A to B, and the other stream from B to A. An orderly release occurs in two stages. First one side (say A) decides to stop sending data and sends a FIN message across to B. When the TCP stack at B's side receives the FIN it knows that no more data is coming from A, and whenever B reads all preceding data off the socket, further reads will return the value -1 to indicate end-of-file. This procedure is known as the TCP half-close, because only one half of the connection is closed. Not surprisingly, the procedure for the other half is exactly the same. B sends a FIN message to A, who eventually receives a -1 after reading all preceding data sent by A off the socket.

By contrast, an abortive close uses the RST (Reset) message. If either side issues an RST, this means the entire connection is aborted and the TCP stack can throw away any queued data which has not been sent or received by either application.

So, how do Java applications perform orderly and abortive releases? Let's consider abortive releases first. A convention that has existed since the days of the original BSD sockets is that the "linger" socket option can be used to force an abortive connection release. Either application can call Socket.setLinger (true, 0) to tell the TCP stack that when this socket is closed, the abortive (RST) procedure is to be used. Setting the linger option has no immediate effect, except that when Socket.close() is called subsequently, the connection is aborted with an RST message. As we will see shortly, there are other ways that may cause a connection to be aborted, but this is the simplest way to make it happen.

The close() method is also used to perform orderly release, as well as abortive. So, at its simplest, the difference between an orderly release and an abortive release could be as little as not setting the linger(0) option, described above, prior to calling Socket.close(). Take the example of two connected peers A and B: If A calls Socket.close(), a FIN message is sent from A to B; and when B calls Socket.close(), a FIN is sent from B to A. In fact, the default setting for the linger option is to not use abortive close; so if two applications terminate their connection just by using Socket.close(), then the outcome should be an orderly release. So what, then, is the problem?

The problem is a slight mismatch between the semantics of Socket.close() and the TCP FIN message. Sending a TCP FIN message means "I am finished sending", whereas Socket.close() means "I am finished sending and receiving." When you call Socket.close(), clearly it is no longer possible to send data; nor, however, is it possible to receive data. So what happens, for example, when A attempts an orderly release by closing the socket, but B continues to send data? This is perfectly allowable in the TCP specification, because as far as TCP is concerned only one half of the connection has been closed. But since A's socket is closed there is nobody to read data if B should continue sending. In this situation A's TCP stack must send an RST to forcibly terminate the connection.

Another common scenario, which may result in an unintended SocketException, is the following: Say A has sent data to B, but B closes the socket without reading all the data. In this situation, B's TCP stack knows that some data is effectively lost and it will forcibly terminate with RST rather than use the orderly FIN procedure. A will get a SocketException if it then tries to send or receive data from the socket.

How to avoid the problem

There are a number of simple ways to avoid being surprised by this problem.

  1. Structure data with a higher-level protocol. Protocols such as HTTP, which are built on top of TCP, deal with the issue by making it clear to both sides when it is safe to close the socket.
  2. Always consume data from a socket before closing it. This will help avoid the second scenario described above.
  3. Use shutdownOutput(). This method has the same effect as close() in that a FIN is sent to indicate that this peer has finished sending, but it is still possible to read from the socket until such time as the remote peer sends a FIN and end-of-file is read from the stream. Then the socket can be closed with Socket.close().

参考文档

TCP出现RST的几种情况
从TCP协议的原理来谈谈rst复位攻击
tcp rst产生的几种情况

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值