如何正确清理TcpListener,以免出现“地址已在使用中”错误?

时间:2018-07-16 18:36:14

标签: c# sockets .net-core

多次运行程序时,我不断收到SocketException: Address already in use

最小示例:

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace test
{
    class Program
    {
        static TcpListener listener;
        static void Main(string[] args)
        {
            listener = new TcpListener(new IPEndPoint(IPAddress.Any, 3349));
            listener.Start();
            listener.BeginAcceptSocket(Accept, null);
            Console.WriteLine("Started!");

            // Simulate a random other client connecting, nc localhost 3349 does the same thing
            var client = new TcpClient();
            client.Connect("localhost", 3349);

            Thread.Sleep(2000);
            listener.Stop();

            Console.WriteLine("Done!");
        }

        static void Accept(IAsyncResult result)
        {
            using(var socket = listener.EndAcceptSocket(result))
            {
                Console.WriteLine("Accepted socket");
                Thread.Sleep(500);
                socket.Shutdown(SocketShutdown.Both);
            }

            Console.WriteLine("Socket fully closed");
        }
    }
}

运行程序两次(dotnet run):第一次将正常完成,但第二次将失败,提示“地址已在使用中”。

请注意,client缺少的配置不是这里的问题-我可以使用nc localhost 3349手动复制相同的错误。

如何清理监听器,以免出现错误?

操作系统和.NET信息:

dotnet --version
2.1.103

lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.4 LTS
Release:        16.04
Codename:       xenial

在Windows上不存在此问题。使用mono时也不会发生这种情况,因此这似乎是特定于Microsoft的Linux实现的。

1 个答案:

答案 0 :(得分:1)

这实际上是预期的行为。要更正此错误,只有在代码未在Windows上运行时,才应将ReuseAddress套接字选项设置为true

if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
    socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}

Windows和Linux(可能是MacOS)之间的差异是由differences in the socket implementations引起的:

  • 在Windows上,除非在同一地址/ IP组合上存在另一个活动连接,否则您可以绑定到地址和IP。
  • 在Linux上,除非在同一地址/ IP组合上存在任何个其他连接,否则您可以绑定到一个地址。这意味着,如果仍然有连接处于TIME_WAIT或CLOSE_WAIT状态,则无法绑定。

this other question中所述,SO_REUSEADDR可用于放宽对Linux的这些限制。

Mono尝试通过将SO_REUSEADDR设置为true by default来模仿Windows的行为。官方.NET核心运行时不执行此操作,因此将在Linux上导致“地址已在使用中”错误。

但是,这并不意味着始终设置SO_REUSEADDR是安全的!在Windows上,SO_REUSEADDR的含义有所不同(source):

  

具有SO_REUSEADDR的套接字可以始终绑定到完全相同的源   地址和端口作为已绑定的套接字,,即使另一个套接字   绑定时未设置此选项。此行为是   有点危险,因为它允许应用程序“窃取”   另一个应用程序的连接端口。

因此,SO_REUSEADDR仅应在非Windows系统上设置。