C# UDP套接字:获取接收方地址

20

我有一个异步UDP服务器类,绑定在IPAddress.Any上的套接字,我想知道接收到的数据包被发送到哪个IPAddress(或者在哪个IPAddress上接收到)。看起来我不能仅仅使用Socket.LocalEndPoint属性,因为它总是返回0.0.0.0(这是有道理的,因为它绑定在那里...)。

这是我目前正在使用的代码的有趣部分:

private Socket udpSock;
private byte[] buffer;
public void Starter(){
    //Setup the socket and message buffer
    udpSock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    udpSock.Bind(new IPEndPoint(IPAddress.Any, 12345));
    buffer = new byte[1024];

    //Start listening for a new message.
    EndPoint newClientEP = new IPEndPoint(IPAddress.Any, 0);
    udpSock.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP, DoReceiveFrom, udpSock);
}

private void DoReceiveFrom(IAsyncResult iar){
    //Get the received message.
    Socket recvSock = (Socket)iar.AsyncState;
    EndPoint clientEP = new IPEndPoint(IPAddress.Any, 0);
    int msgLen = recvSock.EndReceiveFrom(iar, ref clientEP);
    byte[] localMsg = new byte[msgLen];
    Array.Copy(buffer, localMsg, msgLen);

    //Start listening for a new message.
    EndPoint newClientEP = new IPEndPoint(IPAddress.Any, 0);
    udpSock.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP, DoReceiveFrom, udpSock);

    //Handle the received message
    Console.WriteLine("Recieved {0} bytes from {1}:{2} to {3}:{4}",
                      msgLen,
                      ((IPEndPoint)clientEP).Address,
                      ((IPEndPoint)clientEP).Port,
                      ((IPEndPoint)recvSock.LocalEndPoint).Address,
                      ((IPEndPoint)recvSock.LocalEndPoint).Port);
    //Do other, more interesting, things with the received message.
}

如前所述,这段代码总是打印出以下一行信息:

Received 32 bytes from 127.0.0.1:1678 to 0.0.0.0:12345

而我希望它变成这样:

Received 32 bytes from 127.0.0.1:1678 to 127.0.0.1:12345

谢谢您提前的任何想法! --Adam

更新

好吧,我找到了一个解决方案,虽然我不太喜欢它... 基本上,我不再打开一个绑定到 IPAddress.Any 的 udp 套接字,而是为每个可用的 IPAddress 创建一个唯一的套接字。 因此,新的 Starter 函数如下:

public void Starter(){
    buffer = new byte[1024];

    //create a new socket and start listening on the loopback address.
    Socket lSock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    lSock.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 12345);

    EndPoint ncEP = new IPEndPoint(IPAddress.Any, 0);
    lSock.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref ncEP, DoReceiveFrom, lSock);

    //create a new socket and start listening on each IPAddress in the Dns host.
    foreach(IPAddress addr in Dns.GetHostEntry(Dns.GetHostName()).AddressList){
        if(addr.AddressFamily != AddressFamily.InterNetwork) continue; //Skip all but IPv4 addresses.

        Socket s = new Socket(addr.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
        s.Bind(new IPEndPoint(addr, 12345));

        EndPoint newClientEP = new IPEndPoint(IPAddress.Any, 0);
        s.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP, DoReceiveFrom, s);
    }
}

这只是为了说明概念,这段代码存在的最大问题是每个套接字都试图使用相同的缓冲区...这通常是一个不好的想法...

必须有更好的解决方案; 我的意思是,源和目标是UDP数据包头的一部分!哦,好吧,我想我会继续使用这个方法,直到有更好的解决方案。

5个回答

19

我之前遇到了同样的问题。使用ReceiveFrom或其异步变体似乎无法检索接收到的数据包的目标地址。

然而,如果你使用ReceiveMessageFrom或其变体,你将会得到一个IPPacketInformation对象(通过ReceiveMessageFromEndReceiveMessageFrom的引用方式,或者通过ReceiveMessageFromAsync中传递给回调函数的SocketAsyncEventArgs对象的属性方式)。该对象将包含数据包被接收的IP地址和接口编号。

(请注意,由于我使用了ReceiveMessageFromAsync而不是虚假的Begin/End调用,因此没有测试过此代码。)

private void ReceiveCallback(IAsyncResult iar)
{
    IPPacketInformation packetInfo;
    EndPoint remoteEnd = new IPEndPoint(IPAddress.Any, 0);
    SocketFlags flags = SocketFlags.None;
    Socket sock = (Socket) iar.AsyncState;

    int received = sock.EndReceiveMessageFrom(iar, ref flags, ref remoteEnd, out packetInfo);
    Console.WriteLine(
        "{0} bytes received from {1} to {2}",
        received,
        remoteEnd,
        packetInfo.Address
    );
}

请注意,在设置套接字之前,显然应该调用SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true),在Bind它之前。...ReceiveMessageFrom...方法将为您设置它,但您可能只会看到Windows在选项设置后看到的任何数据包上的有效信息。(实际上,这不是什么大问题 - 但如果它发生了,原因将成为一个谜。最好完全防止它。)


现在已经测试过了,伙计,它可以正常工作。这绝对是关于这个主题的几个问题中最好的答案。我很想看看ReceiveMessageFromAsync版本,但如果你不回答,我可能可以自己解决。 - Stephen Kennedy
@StephenKennedy:老实说,我觉得我甚至没有那段代码了。 :P 不过,如果我没记错的话,它并不是特别复杂;只需设置一个 SocketAsyncEventArgs,将处理程序附加到其 Completed 事件,然后将对象传递给 ReceiveMessageFromAsync 即可。 - cHao
没问题。我已经让SendToAsync()正常工作了,明天可能会试试ReceiveMessageFromAsync() :) 谢谢。 - Stephen Kennedy
3
实际上,“IPPacketInformation”包含接收数据包的网卡(实际上是索引)和发送数据包的目标地址。因此,您可以判断它是否被发送到多播或广播地址,而不仅仅是您的网卡上的哪个单播地址。 - Jesse Chisholm

1
关于缓冲区问题,请尝试以下方法:
创建一个名为StateObject的类,用于存储您想要在回调中使用的任何数据,包括缓冲区和套接字(如果需要的话,因为我看到您当前正在将udpSock作为stateObject传递)。将新创建的对象传递给异步方法,然后您就可以在回调中访问它了。
public void Starter(){
    StateObject state = new StateObject();
    //set any values in state you need here.

    //create a new socket and start listening on the loopback address.
    Socket lSock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    lSock.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 12345);

    EndPoint ncEP = new IPEndPoint(IPAddress.Any, 0);
    lSock.BeginReceiveFrom(state.buffer, 0, state.buffer.Length, SocketFlags.None, ref ncEP,    DoReceiveFrom, state);

    //create a new socket and start listening on each IPAddress in the Dns host.
    foreach(IPAddress addr in Dns.GetHostEntry(Dns.GetHostName()).AddressList){
        if(addr.AddressFamily != AddressFamily.InterNetwork) continue; //Skip all but IPv4 addresses.

        Socket s = new Socket(addr.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
        s.Bind(new IPEndPoint(addr, 12345));

        EndPoint newClientEP = new IPEndPoint(IPAddress.Any, 0);

        StateObject objState = new StateObject();
        s.BeginReceiveFrom(objState.buffer, 0, objState.buffer.length, SocketFlags.None, ref newClientEP, DoReceiveFrom, objState);
     }
} 

在搜索这个问题时,我发现:

http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.beginreceivefrom.aspx

然后您可以像当前使用udpSock和缓冲区一样将StateObject从AsyncState强制转换,以及任何其他需要存储的数据都存储在那里。

我想现在唯一的问题是如何在哪里存储数据,但是由于我不知道您的实现方法,因此无法提供帮助。


0

我认为如果你绑定到127.0.0.1而不是IPAddress.Any,你会得到你想要的行为。

0.0.0.0有意表示“每个可用的IP地址”,并且它根据你的绑定语句非常字面地执行。


1
虽然是真的,但我不想绑定到127.0.0.1(或任何其他特定的IP地址)。如果我表达不清楚,我很抱歉,但理想情况下,我希望在具有多个网络适配器的机器上运行此UDP服务器(在所有适配器上监听),并且我想知道数据包被发送到哪一个适配器。 - chezy525

0

从该套接字获取地址的一种方法是将其连接到发送方。

这样做后,您将能够获取本地地址(或至少一个可路由到发送方的地址),但是您只能从已连接的终点接收消息。

要取消连接,您需要再次使用connect,这次指定一个带有AF_UNSPEC系列的地址。不幸的是,我不知道如何在C#中实现这一点。

(免责声明:我从未编写过一行C#代码,这适用于Winsock的通用性)


-2

亚当

这还没有测试过...让我们试一下

private void DoReceiveFrom(IAsyncResult iar){
//Get the received message.
Socket recvSock = (Socket)iar.AsyncState;
//EndPoint clientEP = new IPEndPoint(IPAddress.Any, 0);
Socket clientEP = recvSock.EndAccept(iar);
int msgLen = recvSock.EndReceiveFrom(iar, ref clientEP);
byte[] localMsg = new byte[msgLen];
Array.Copy(buffer, localMsg, msgLen);

//Start listening for a new message.
EndPoint newClientEP = new IPEndPoint(IPAddress.Any, 0);
udpSock.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP, DoReceiveFrom, udpSock);

//Handle the received message
/*
Console.WriteLine("Recieved {0} bytes from {1}:{2} to {3}:{4}",
                  msgLen,
                  ((IPEndPoint)recvSock.RemoteEndPoint).Address,
                  ((IPEndPoint)recvSock.RemoteEndPoint).Port,
                  ((IPEndPoint)recvSock.LocalEndPoint).Address,
                  ((IPEndPoint)recvSock.LocalEndPoint).Port);
//Do other, more interesting, things with the received message.
*/
Console.WriteLine("Recieved {0} bytes from {1}:{2} to {3}",
                  msgLen,
                  ((IPEndPoint)recvSock.RemoteEndPoint).Address,
                  ((IPEndPoint)recvSock.RemoteEndPoint).Port,
                  clientEP.RemoteEP.ToString();
 }

1
谢谢,但我相当确定这段代码不会工作,甚至无法编译...你不能在没有开始异步接受调用(Socket.BeginAccept)的情况下结束它(Socket.EndAccept),而在无连接套接字上接受也没有意义。此外,Socket.EndReceiveFrom调用需要对EndPoint的引用,而不是Socket。 - chezy525

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接