closesocket, shutdown, tcp::socket.close()

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Windgs_YF/article/details/81389528

来,咱们彻底的来讨论一下这个shutdown   和closesocket 
从函数调用上来分析(msdn):一旦完成了套接字的连接,应当将套接字关闭,并且释放其套接字句柄所占用的所有资源。真正释放一个已经打开的套接字句柄的资源直接调用closesocket即可,但要明白closesocket的调用可能会带来负面影响,具体的影响和如何调用有关,最明显的影响是数据丢失,因此一般都要在closesocket之前调用shutdown来关闭套接字。 
        shutdown:为了保证通信双方都能够收到应用程序发出的所有数据,一个合格的应用程序的做法是通知接受双发都不在发送数据!这就是所谓的“正常关闭”套接字的方法,而这个方法就是由shutdown函数,传递给它的参数有SD_RECEIVE,SD_SEND,SD_BOTH三种,如果是SD_RECEIVE就表示不允许再对此套接字调用接受函数。这对于协议层没有影响,另外对于tcp套接字来说,无论数据是在等候接受还是即将抵达,都要重置连接(注意对于udp协议来说,仍然接受并排列传入的数据,因此udp套接字而言shutdown毫无意义)。如果选择SE_SEND,则表示不允许再调用发送函数。对于tcp套接字来说,这意味着会在所有数据发送出并得到接受端确认后产生一个FIN包。如果指定SD_BOTH,答案不言而喻。 
        closesocket:对此函数的调用会释放套接字的描述,这个道理众所周知(凡是经常翻阅msdn的程序员),因此,调用此函数后,再是用此套接字就会发生调用失败,通常返回的错误是WSAENOTSOCK。此时与被closesocket的套接字描述符相关联的资源都会被释放,包括丢弃传输队列中的数据!!!!对于当前进程中的线程来讲,所有被关起的操作,或者是被挂起的重叠操作以及与其关联的任何事件,完成例程或完成端口的执行都将调用失败!另外SO_LINGER标志还影响着closesocket的行为,但对于传统的socket程序,这里不加解释 
        因此可以可以看出shutdown对切断连接有着合理的完整性。 
        下面从tcp协议上来分析shutdown和closesocket的行为(behavior):closesocket或shutdown(使用SD_SEND当作参数时),会向通信对方发出一个fin包,而此时套接字的状态会由ESTABLISHED变成FIN_WAIT_1,然后对方发送一个ACK包作为回应,套接字又变成FIN_WAIT_2,如果对方也关闭了连接则对方会发出FIN,我方会回应一个ACK并将套接字置为TIME_WAIT。因此可以看出closesocket,shutdown所进行的TCP行为是一样的,所不同的是函数部分,shutdown会确保windows建立的数据传输队列中的数据不被丢失,而closesocket会冒然的抛弃所有的数据,因此如果你愿意closesocket完全可以取代shutdown,然而在数据交互十分复杂的网络协议程序中,最好还是shutdown稳妥一些!


 

closesocket, shutdown, tcp::socket.close()

tcp关闭连接有2种方式,一种是关闭端发送FIN,对方回应FINACK,关闭端再回ACK,这是优雅的关闭连接。双方可以保证所有数据都发送接收完成了。另一种是硬关闭,关闭方直接发送RSET。对方收到后立刻断开连接。

首先应该了解win32 api closesocket,这里只说msdn文档中叙述不够清晰的地方。

首先说缺省情况:l_onoff为0,closesocket立刻返回,但底层依然在持续发包,并且试图优雅关闭连接。这种情况下对于应用程序来说,该连接已经关闭,但底层socket的相关资源还没有释放,且不知道要等待多长时间。该方法对于一般程序来说没有问题,对于服务器来说,因为有可能有非常多的socket(超过100k),这样处理可能会导致系统可用的socket资源不足。

其次说l_onoff不为0的情况,此时取决于l_linger的值,系统会尽可能的优雅的关闭连接,如果在l_linger的时间内还无法关闭连接,则硬关闭。也就是说,此时如果l_linger为0,则肯定硬关闭,且closesocket立刻返回,同时释放所有资源。如果l_linger不为0,则尽可能优雅关闭并返回,释放所有资源,如果超时,则硬关闭,closesocket返回,释放所有资源。注意此种情况下closesocket可能是同步的。

shutdown有2个作用。首先禁止后续的send或者recv,但注意它不会影响底层,也就是说,此前发出的异步send/recv不会返回。其次,在所有发送的包被对方确认后,会发送FIN包给对方,试图优雅的关闭连接。但shutdown本身并不影响任何底层的东西,因此,shutdown并且优雅关闭了连接之后,该socket以及其所关联的资源依然存在,必须调用closesocket才能释放。

以上是msdn文档的说明。但实际过程中发现并非完全如此。测试程序使用asio实现了一个tcp server,在handle_send,也即一个包已经异步返回表明发送成功(成功发送到了系统缓冲区),这时候调用socket_.close,结果发现该包并没有真正发送(也可能发送了),系统发出了一个RSET而不是FIN。如果在socket_.close之前调用一次socket_.shutdown(both),则发出FIN包。此处颇有些奇怪,实验平台是windows 20008和windows xp。实验时用socket_.get_option检查了l_onoff和l_linger的值,的确为缺省值。

因为许多时候都存在这样的需求,也即client或者server在发出最后一个包之后就关闭连接,一般来说是在发出最后一个包之后就调用socket_.close,或者不再发出任何异步调用让socket_自动进入析构函数。这样很可能有问题。在发完最后一个包之后先调用一次shutdown在网速快的情况下可能有一定帮助。但有2个缺点,一方面是网速未必快,那个包未必很快就发出,由于shutdown根本不影响前面已经发出的包,所以FIN未必能发出,另一个缺点是随后的closesocket并不真正意味着资源已释放。

综上述,好的做法是在handle_send里面,如果是最后一个包,那么设置一个标志,shutdown,然后启动一个1s的deadline_timer:

socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, boost::system::error_code());           

if(!graceful_close_)

{

    handle_hard_close(boost::system::error_code());

}

else

{

    timer_hard_close_.expires_from_now(boost::posix_time::seconds(1));

    timer_hard_close_.async_wait(

        strand_.wrap(boost::bind(

        &tcp_client::handle_hard_close,

        shared_from_this(),

        boost::asio::placeholders::error)));

}

然后

void handle_hard_close(const boost::system::error_code& error)

{

    if(!error)

    {

        boost::asio::socket_base::linger option(true, 0);

        socket_.set_option(option);

        socket_.close();

    }

}

附录:

asio的部分源代码:

tcp::socket的析构函数:

win_iocp_socket_service.hpp

  void close_for_destruction(implementation_type& impl)

  {

    if (is_open(impl))

    {

      // Check if the reactor was created, in which case we need to close the

      // socket on the reactor as well to cancel any operations that might be

      // running there.

      reactor_type* reactor = static_cast<reactor_type*>(

            interlocked_compare_exchange_pointer(

              reinterpret_cast<void**>(&reactor_), 0, 0));

      if (reactor)

        reactor->close_descriptor(impl.socket_, impl.reactor_data_);

      // The socket destructor must not block. If the user has changed the

      // linger option to block in the foreground, we will change it back to the

      // default so that the closure is performed in the background.

      if (impl.flags_ & implementation_type::close_might_block)

      {

        ::linger opt;

        opt.l_onoff = 0;

        opt.l_linger = 0;

        boost::system::error_code ignored_ec;

        socket_ops::setsockopt(impl.socket_,

            SOL_SOCKET, SO_LINGER, &opt, sizeof(opt), ignored_ec);

      }

      boost::system::error_code ignored_ec;

      socket_ops::close(impl.socket_, ignored_ec);

      impl.socket_ = invalid_socket;

      impl.flags_ = 0;

      impl.cancel_token_.reset();

#if defined(BOOST_ASIO_ENABLE_CANCELIO)

      impl.safe_cancellation_thread_id_ = 0;

#endif // defined(BOOST_ASIO_ENABLE_CANCELIO)

    }

  }

tcp::socket.close

win_iocp_socket_service.hpp

  boost::system::error_code close(implementation_type& impl,

      boost::system::error_code& ec)

  {

    if (is_open(impl))

    {

      // Check if the reactor was created, in which case we need to close the

      // socket on the reactor as well to cancel any operations that might be

      // running there.

      reactor_type* reactor = static_cast<reactor_type*>(

            interlocked_compare_exchange_pointer(

              reinterpret_cast<void**>(&reactor_), 0, 0));

      if (reactor)

        reactor->close_descriptor(impl.socket_, impl.reactor_data_);

      if (socket_ops::close(impl.socket_, ec) == socket_error_retval)

        return ec;

      impl.socket_ = invalid_socket;

      impl.flags_ = 0;

      impl.cancel_token_.reset();

#if defined(BOOST_ASIO_ENABLE_CANCELIO)

      impl.safe_cancellation_thread_id_ = 0;

#endif // defined(BOOST_ASIO_ENABLE_CANCELIO)

    }

    ec = boost::system::error_code();

    return ec;

  }

以上代码很清晰的说明了asio的实现。

首先如果是析构,那么不管此前是否设置过setsockopt,asio都把socket设置回缺省状态,也即不可预测时间的优雅关闭连接。如果socket.close,那么直接调用closesocket。

另一个值得注意的地方是,这两个函数都把socket设置为了invalid_socket,从而避免了多次关闭的问题。

多次关闭的问题是这样:首先假定socket a = 0x1234,调用closesocket关闭了这个socket,然后再次createsocket得到一个socket b也为0x1234,而代码(可能是另一个thread或者iocp得到一个错误返回)试图再此关闭socket a,结果把socket b关闭了。这种情况在windows平台下很常见,也即socket b很可能也为0x1234。 

==============================================

CP连接断开的时候调用closesocket函数,已经讨论过有优雅的断开和强制断开,那么如何设置断开连接的方式呢?是通过设置socket描述符一个linger结构体属性。

linger结构体数据结构如下:

struct linger

{

     int l_onoff;

     int l_linger;

};

有三种组合方式:

第一种

    l_onoff = 0;

    l_linger忽略

    这种方式下,就是在closesocket的时候立刻返回,底层会将未发送完的数据发送完成后再释放资源,也就是优雅的退出


第二种

    l_onoff非零

    l_linger = 0;

    这种方式下,在调用closesocket的时候同样会立刻返回,但不会发送未发送完成的数据,而是通过一个REST包强制的关闭socket描述符,也就是强制的退出


第三种

    l_onoff非零

    l_linger > 0

    这种方式下,在调用closesocket的时候不会立刻返回,内核会延迟一段时间,这个时间就由l_linger得值来决定。如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,closesocket会返回正确,socket描述符优雅性退出。否则,closesocket会直接返回错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的时,如果socket描述符被设置为非堵塞型,则closesocket会直接返回值。

猜你喜欢

转载自blog.csdn.net/Windgs_YF/article/details/81389528