即使端口被转发,Udp连接的客户端也不会收到本地网络以外的消息。

问题描述 投票:0回答:1

好吧,我建立了一个半简单的多人游戏,通过UDP连接。然而,我不能让它完全连接到我的本地网络之外。现在!我看到这个问题被问过很多次,也回答过很多次。我已经看到这个问题被问过很多次,也回答过很多次,所以这里是它与类似问题的不同之处。

我已经把正确的端口转发了。

没错!我已经把正确的端口转发了。我是一个Minecraft老手,所以我知道如何做的一件事是转发端口.)

现在,这在我的本地网络上工作得很好。当然,如果我在同一台机器上运行两个实例,它也可以工作。但是,如果我使用不同的计算机连接到LAN IP,或者如果我使用不同的计算机连接到我的路由器公共地址,它也可以工作。只有当消息来自于他们被托管的网络之外时,连接似乎才会失败。

主机收到来自客户端的传入消息,但客户端并没有收到来自主机的消息。而且谁是主机,谁是客户端都不重要。所以我让我的朋友试着从他家连接到我的游戏,并且在接收消息的功能上有一个断点。果然我看到预期的消息进入主机,然后主机发出了确认消息,但是他的客户端一直没有收到。同理,如果我的朋友主机和我尝试连接,他的主机看到消息后发送了确认消息,但是我的客户端一直没有收到。所以此时我们都有端口转发,但只有主机收到过任何信息。

所以...。有谁知道我可能做错了什么?或者说我需要杀死互联网上的什么Cthulhu?

下面是我的代码。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public enum MessageType
{
    None = 0,
    Connect = 1,
    Command = 2,
    EntityUpdate = 3,
    Response = 4,
}

[System.Serializable]
public class Message
{
    public MessageType _MessageType = MessageType.None;
    public object Data = null;
    public Guid Origin = Guid.Empty;
    public Guid MessageID;

    public Message(object data, MessageType type, Guid origin)
    {
        if (type == MessageType.None || data == null)
        {
            throw new Exception("Invalid Message!");
        }

        Data = data;
        _MessageType = type;
        Origin = origin;

        MessageID = Guid.NewGuid();
    }
}

public static class NetLayer
{
    private const int FRAMES_BETWEEN_SENDS = 2;
    private const int NUM_RETRIES = 10;
    private const int FAMES_BEFORE_RETRY_ENTITY = 120;
    private const int FAMES_BEFORE_RETRY_CLIENT = 30;
    private const int BUFF_SIZE = 8 * 2048;
    private const int DEFAULT_PORT = 27015;
    private const string DEFAULT_ADDRESS = "localhost";

    private const int MAX_BYTES = 1100;
    private const int MAX_BYTES_PER_PACKET = 1100 - (322 + 14); //281 is number of existing bytes in empty serilized packet //14 is... I dunno. the overhead for having the byte aray populated I assume

    private static Socket mSocket = null;
    private static List<EndPoint> mClients = null;
    private static Dictionary<EndPoint, PlayerData> mUnconfirmedClients = null;
    private static string mServerAdress = DEFAULT_ADDRESS;
    private static int mPort = DEFAULT_PORT;

    public static bool IsServer { get { return mClients != null; } }
    public static bool IsConnected = false;
    public static bool Initiated { get { return mSocket != null; } }

    private static Dictionary<Guid, Packet> mUnconfirmedPackets = null;
    private static Queue<Packet> ResendQueue = new Queue<Packet>();
    private static EndPoint mCurrentPacketEndpoint;
    private static Guid mCurrentMessageID;
    private static byte[] mDataToSend;
    private static int mCurrentDataIndex = 0;
    private static int mCurrentPacketIndex = 0;
    private static int mTotalPacketsToSend = 0;

    private static Queue<Packet> ResponseQueue = new Queue<Packet>();

    private static Dictionary<Guid, Dictionary<int, Packet>> IncomingPacketGroups = new Dictionary<Guid, Dictionary<int, Packet>>();

    private static Queue<QueueuedMessage> MessageQueue = new Queue<QueueuedMessage>();
    public static int QueueCount { get { return MessageQueue.Count; } }

    private static int mCurrentFramesToRetryClient;
    private static int mCurrentFramesToRetryEntity;
    private static int mLastSendFrame = 0;

    [System.Serializable]
    public class Packet
    {
        public byte[] Data;

        public Guid MessageID;
        public Guid PacketID;
        public int PacketNumber;
        public int NumPacketsInGroup;
        public int NumBytesInGroup;
        public bool RequireResponse;

        [System.NonSerialized]
        public int retries; //We don't need to send this data, just record it locally

        [System.NonSerialized]
        public EndPoint Destination; //We don't need to send this data, just record it locally

        public Packet(byte[] data, Guid messageID, int packetNumber, int numPacketsinGroup, int numBytesInGroup, EndPoint destination, bool requireResponse)
        {
            Data = data;

            if(data.Length > MAX_BYTES_PER_PACKET)
            {
                Debug.LogError("Creating a packet with a data size of " + data.Length + " which is greater then max data size of " + MAX_BYTES_PER_PACKET);
            }

            MessageID = messageID;
            PacketID = Guid.NewGuid();
            PacketNumber = packetNumber;
            NumPacketsInGroup = numPacketsinGroup;
            NumBytesInGroup = numBytesInGroup;
            retries = 0;
            Destination = destination;
            RequireResponse = requireResponse;
        }
    }

    private class QueueuedMessage
    {
        public Message m;
        public EndPoint e;

        public QueueuedMessage(Message message, EndPoint endpoint)
        {
            m = message;
            e = endpoint;
        }
    }

    [System.Serializable]
    private class PacketResponse
    {
        public Guid MessageIDToConfirm;

        public PacketResponse(Guid newGuid)
        {
            MessageIDToConfirm = newGuid;
        }
    }

    public static void InitAsServer(string address, int port)
    {
        Reset();
        mClients = new List<EndPoint>();
        mUnconfirmedClients = new Dictionary<EndPoint, PlayerData>();
        mSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        mSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true);
        mSocket.Blocking = false;
        mSocket.SendBufferSize = BUFF_SIZE;
        mSocket.ReceiveBufferSize = BUFF_SIZE;
        mSocket.Bind(new IPEndPoint(IPAddress.Parse(address), port));
        mPort = port;
        IsConnected = true;

        mUnconfirmedPackets = new Dictionary<Guid, Packet>();

        Packet newPacket = new Packet(new byte[MAX_BYTES_PER_PACKET], Guid.NewGuid(), 1, 15, MAX_BYTES_PER_PACKET, null, true);
        byte[] serilized = Util.Serialize(newPacket);
        Debug.Log("Total Packet Size = " + serilized.Length);
    }

    public static void InitAsClient(string address, int port)
    {
        Reset();
        mSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        mSocket.Blocking = false;
        mSocket.SendBufferSize = BUFF_SIZE;
        mSocket.ReceiveBufferSize = BUFF_SIZE;
        mSocket.Bind(new IPEndPoint(IPAddress.Parse(GetLocalIPAddress()), port));
        mServerAdress = address;
        mPort = port;

        mUnconfirmedPackets = new Dictionary<Guid, Packet>();
    }

    public static void NetTick()
    {
        if (Initiated)
        {
            //Keep retrying connection messages untill we get a response from the player, in case they miss our first message
            if(IsServer)
            {
                mCurrentFramesToRetryClient++;
                if (mCurrentFramesToRetryClient >= FAMES_BEFORE_RETRY_CLIENT)
                {
                    mCurrentFramesToRetryClient = 0;
                    foreach (EndPoint ep in mUnconfirmedClients.Keys)
                    {
                        Debug.Log("Pinging client again: " + ep);
                        SendConnectionConfirmationToClient(ep, mUnconfirmedClients[ep]); //TODO Add timeout. Should stop pinging after 10 attempts
                    }
                }
            }

            //Retry messages untill we get a responce or give up
            mCurrentFramesToRetryEntity++;
            if (mCurrentFramesToRetryEntity >= FAMES_BEFORE_RETRY_ENTITY)
            {
                List<Guid> deadMessages = new List<Guid>();
                List<Guid> resendMessages = new List<Guid>();

                mCurrentFramesToRetryEntity = 0;
                foreach (Guid messageID in mUnconfirmedPackets.Keys)
                {
                    Packet packet = mUnconfirmedPackets[messageID];
                    packet.retries++;
                    if (packet.retries > NUM_RETRIES)
                    {
                        Debug.LogError("Failed to send Packet " + messageID);
                        deadMessages.Add(messageID);
                    }
                    else
                    {
                        resendMessages.Add(messageID);
                    }
                }

                //Resend everything we have selected to resend
                for (int i = 0; i < resendMessages.Count; i++)
                {
                    Packet packet = mUnconfirmedPackets[resendMessages[i]];
                    ResendQueue.Enqueue(packet);
                }

                //Give up on messages that have been marked dead
                for (int i = 0; i < deadMessages.Count; i++)
                {
                    mUnconfirmedPackets.Remove(deadMessages[i]);
                }
            }

            if (Time.frameCount - mLastSendFrame >= FRAMES_BETWEEN_SENDS)
            {
                if (ResponseQueue.Count > 0) //Send responses first
                {
                    Packet p = ResponseQueue.Dequeue();
                    SendImmediate(p, p.Destination, false);
                }
                else if (ResendQueue.Count > 0) //Then resends
                {
                    Packet p = ResendQueue.Dequeue();
                    SendImmediate(p, p.Destination, false);
                }
                else //Then normal messages
                {
                    StreamSend();
                }

                mLastSendFrame = Time.frameCount;
            }
        }
    }

    public static string GetLocalIPAddress()
    {
        var host = Dns.GetHostEntry(Dns.GetHostName());
        foreach (var ip in host.AddressList)
        {
            if (ip.AddressFamily == AddressFamily.InterNetwork)
            {
                return ip.ToString();
            }
        }

        return null;
    }

    public static void Reset()
    {
        if(mSocket != null)
        {
            mSocket.Close();
        } 

        mSocket = null;
        mClients = null;
        mServerAdress = DEFAULT_ADDRESS;
        mPort = DEFAULT_PORT;

        IsConnected = false;
    }

    public static void Send(Message newMessage, EndPoint endpoint = null)
    {
        if (IsServer)
        {
            if (endpoint != null)
            {
                SendToQueue(new QueueuedMessage(newMessage, endpoint));
            }
            else
            {
                for (int i = 0; i < mClients.Count; i++)
                {
                    SendToQueue(new QueueuedMessage(newMessage, mClients[i]));
                }
            }
        }
        else
        {
            SendToQueue(new QueueuedMessage(newMessage, new IPEndPoint(IPAddress.Parse(mServerAdress), mPort)));
        }
    }

    private static void SendToQueue(QueueuedMessage newPacket)
    {
        MessageQueue.Enqueue(newPacket);
    }

    private static void StreamSend()
    {
        if(mDataToSend != null)
        {
            //Get number of bytes to send in this packet
            int bytesToSend = Mathf.Min(mDataToSend.Length - mCurrentDataIndex, MAX_BYTES_PER_PACKET);

            //Populate packet byte array
            byte[] data = new byte[bytesToSend];
            for(int i = 0; i < bytesToSend; i++)
            {
                data[i] = mDataToSend[mCurrentDataIndex + i];
            }

            //Increment index by the nubmer of bytes sent
            mCurrentDataIndex += bytesToSend;

            //Create and send packet
            Packet newPacket = new Packet(data, mCurrentMessageID, mCurrentPacketIndex, mTotalPacketsToSend, mDataToSend.Length, mCurrentPacketEndpoint, true);
            SendImmediate(newPacket, mCurrentPacketEndpoint, true);

            //Increment packet index
            mCurrentPacketIndex++;

            if(mCurrentDataIndex >= mDataToSend.Length)
            {
                //We have finished sending. Clear data array
                mDataToSend = null;
            }
        }
        else if(MessageQueue.Count > 0)
        {
            //Prepare the next group for packing
            QueueuedMessage queueuedMessage = MessageQueue.Dequeue();
            mCurrentMessageID = queueuedMessage.m.MessageID;
            mCurrentPacketEndpoint = queueuedMessage.e;
            mDataToSend = Util.Serialize(queueuedMessage.m);
            mCurrentDataIndex = 0;

            mCurrentPacketIndex = 1;
            mTotalPacketsToSend = Mathf.CeilToInt((float)mDataToSend.Length / (float)MAX_BYTES_PER_PACKET);
        }
    }

    public static void SendImmediate(Packet packet, EndPoint endpoint, bool verify)
    {
        byte[] data = Util.Serialize(packet);
        if(data.Length > MAX_BYTES)
        {
            Debug.LogError("Attempting to send packet with " + data.Length + " bytes. This will most likely fail! Please ensure all packets are limited to " + MAX_BYTES + " bytes or less!");
        }

        mSocket.SendTo(data, endpoint);

        //Don't assume the packet is late untill we have actually sent it.
        //The old way of verifying. Instead things sending imediate that don't want to be veiried should set verify to false if (newMessage._MessageType != MessageType.Response && newMessage._MessageType != MessageType.Connect && !mUnconfirmedPackets.ContainsKey(newMessage.MessageID))
        if (verify)
        {
            mUnconfirmedPackets.Add(packet.PacketID, packet);
        }
    }

    public static void SendConnectionMessage(PlayerData playerData)
    {
        Message message = new Message(playerData, MessageType.Connect, playerData.ID);
        Send(message);
    }

    public static void SendCommand(Command command, EndPoint ep = null)
    {
        Message message = new Message(command, MessageType.Command, GameManager.LocalPlayer.PlayerID);
        Send(message, ep);
    }

    public static void SendEntityUpdate(EntityData entityData, EndPoint ep = null)
    {
        Message message = new Message(entityData, MessageType.EntityUpdate, GameManager.LocalPlayer.PlayerID);
        Send(message, ep);
    }

    public static void Receive()
    {
        if (Initiated && mSocket.Available > 0)
        {
            byte[] bytes = new byte[BUFF_SIZE];

            EndPoint epFrom = new IPEndPoint(IPAddress.Any, 0); //TODO Do something here to stop reciving messages from dead clients...
            mSocket.ReceiveFrom(bytes, ref epFrom);

            Reassemble(bytes, epFrom);
        }
    }

    public static void Reassemble(byte[] bytes, EndPoint epFrom)
    {

        Packet receivedPacket = Util.Deserilize(bytes) as Packet;

        //Create new packet group if one does not already exist
        if(!IncomingPacketGroups.ContainsKey(receivedPacket.MessageID))
        {
            IncomingPacketGroups.Add(receivedPacket.MessageID, new Dictionary<int, Packet>());
        }

        Dictionary<int, Packet> currentPacketGroup = IncomingPacketGroups[receivedPacket.MessageID];
        currentPacketGroup[receivedPacket.PacketNumber] = receivedPacket;

        if(currentPacketGroup.Count == receivedPacket.NumPacketsInGroup)
        {
            byte[] completedData = new byte[receivedPacket.NumBytesInGroup];
            int index = 0;
            for(int i = 0; i < receivedPacket.NumPacketsInGroup; i++)
            {
                Packet currentPacket = currentPacketGroup[i + 1];
                for(int j = 0; j < currentPacket.Data.Length; j++)
                {
                    completedData[index] = currentPacket.Data[j];
                    index++;
                }
            }

            //Clean up the packet group now that it is compleated
            IncomingPacketGroups.Remove(receivedPacket.MessageID);

            //Call this once we have re-assembled a whole message 
            Message receivedMessage = Util.Deserilize(completedData) as Message;
            ReceiveCompleateMessage(receivedMessage, epFrom);
        }

        //Make sure to call back to sender to let them know we got this packet
        if(receivedPacket.RequireResponse)
        {
            SendMessageResponse(receivedPacket.PacketID, epFrom);
        }
    }

    private static void ReceiveCompleateMessage(Message receivedMessage, EndPoint epFrom)
    {
        if (receivedMessage.Origin == GameManager.LocalPlayer.PlayerID)
        {
            //Reject messages we sent ourselves. Prevent return from sender bug.
            return;
        }

        //TODO Consider adding a check that adds the endpoing into the connections even if we don't have a connecton message because players can sometimes re-connect.
        //TODO or maybe reject messages from unknown clients becasue we shouldn't trust them


        switch (receivedMessage._MessageType)
        {
            case MessageType.None:
                Debug.LogError("Recived message type of None!");
                break;
            case MessageType.EntityUpdate:
                GameManager.ProcessEntityUpdate(receivedMessage);
                break;
            case MessageType.Command:
                GameManager.ProcessCommand(receivedMessage);
                break;
            case MessageType.Response:
                ProcessResponse(receivedMessage);
                break;
            case MessageType.Connect:
                ProcessConnection(receivedMessage, epFrom);
                break;
        }

        //If this is the server reciving a messgae, and that message was not sent by us
        if (IsServer && receivedMessage._MessageType != MessageType.Connect && receivedMessage._MessageType != MessageType.Response && receivedMessage.Origin != GameManager.LocalPlayer.PlayerID)
        {
            //Mirror the message to all connected clients
            Send(receivedMessage);
        }
    }

    public static void ProcessConnection(Message receivedMessage, EndPoint epFrom)
    {
        //This is the only time where we should process the data directly in net layer since we need the endpoint data
        if (IsServer)
        {
            if (!mClients.Contains(epFrom))
            {
                if (epFrom != null)
                {
                    mClients.Add(epFrom);
                    Debug.Log("Client attempting to join: " + epFrom.ToString()); //TODO becasue we are binding socket this won't work locally anymore but see if it at least works globally before trying to fix

                    //Get new player from recived data
                    PlayerData newPlayer = receivedMessage.Data as PlayerData;
                    newPlayer.Team = GameManager.GetNewTeam();
                    newPlayer.HostHasResponded = true;

                    if (!mUnconfirmedClients.ContainsKey(epFrom))
                    {
                        mUnconfirmedClients.Add(epFrom, newPlayer);
                    }

                    SendConnectionConfirmationToClient(epFrom, newPlayer);
                }
                else
                {
                    Debug.LogError("Got connection message from null!");
                }
            }
            else
            {
                if (mUnconfirmedClients.ContainsKey(epFrom))
                {
                    PlayerData newPlayerData = receivedMessage.Data as PlayerData;

                    if (newPlayerData.HostHasResponded)
                    {
                        mUnconfirmedClients.Remove(epFrom);
                        if (epFrom != null)
                        {
                            Debug.Log("Client Confirmed, sending update: " + epFrom.ToString());

                            //Sent all known entitys to the new client so it's up to date
                            foreach (Guid key in GameManager.Entities.Keys)
                            {
                                SendEntityUpdate(GameManager.Entities[key].GetEntityData(), epFrom);
                            }
                        }
                    }
                    else
                    {
                        Debug.LogWarning("We sent client an confirmation but it is still sending us initial connection messages: " + epFrom.ToString());
                    }
                }
                else
                {
                    Debug.LogWarning("Client Already connected but still sent connnection message: " + epFrom.ToString());
                }
            }
        }
        else
        {
            ReciveConnectionConfirmationFromHost(receivedMessage, epFrom);
        }
    }

    public static void ProcessResponse(Message message)
    {
        PacketResponse response = (PacketResponse)message.Data;
        mUnconfirmedPackets.Remove(response.MessageIDToConfirm);
    }

    public static void SendConnectionConfirmationToClient(EndPoint epFrom, PlayerData newPlayer)
    {
        //endpoint can be null if this is the server connecting to itself
        if (epFrom != null)
        {
            //Tell client we have accepted its connection, and send back the player data with the updated team info
            Message message = new Message(newPlayer, MessageType.Connect, GameManager.LocalPlayer.PlayerID);
            //Send(message, epFrom);

            byte[] data = Util.Serialize(message);

            Packet newPacket = new Packet(data, message.MessageID, 1, 1, data.Length, epFrom, false);
            ResponseQueue.Enqueue(newPacket);
        }
    }

    public static void ReciveConnectionConfirmationFromHost(Message receivedMessage, EndPoint epFrom)
    {
        //Clear the loading win we had active
        GameObject.Destroy(GameManager.LoadingWin);

        PlayerData newPlayer = receivedMessage.Data as PlayerData;
        GameManager.LocalPlayer.SetPlayerData(newPlayer);
        IsConnected = true;

        PingBack(epFrom);
    }

    public static void PingBack(EndPoint ep)
    {
        PlayerData localPlayer = GameManager.LocalPlayer.GetPlayerData();
        Message message = new Message(localPlayer, MessageType.Connect, localPlayer.ID);

        byte[] data = Util.Serialize(message);

        Packet newPacket = new Packet(data, message.MessageID, 1, 1, data.Length, ep, false);
        ResponseQueue.Enqueue(newPacket);
    }

    private static void SendMessageResponse(Guid packetIDToConfirm, EndPoint epFrom)
    {
        PacketResponse messageIDtoConfirm = new PacketResponse(packetIDToConfirm);
        Message message = new Message(messageIDtoConfirm, MessageType.Response, GameManager.LocalPlayer.PlayerID);

        byte[] data = Util.Serialize(message);

        Packet newPacket = new Packet(data, message.MessageID, 1, 1, data.Length, epFrom, false);
        ResponseQueue.Enqueue(newPacket);
    }
}

[编辑]于是我继续研究和完善我的代码。上面的代码已经更新到当前版本)。我之前肯定是做错了一件事,就是把MTU吹大了,因为我没有意识到外部限制比内部限制小很多。不过还是会出现同样的问题。服务器可以看到客户端的消息,用传入的端点发回连接消息,客户端却一直收不到。目前服务器会尝试无限期地重新发送连接确认,直到客户端回复,而这永远不会发生,因为服务器的回复都没有到达客户端。

我对UDP打通的理解是,两台机器必须首先连接到一个已知的外部服务器。然后,外部服务器会给每个进入的连接提供对方的IP和端口,允许他们直接与对方对话。不过,由于我是手动给客户端输入游戏服务器的IP和端口,所以应该不需要外部服务器来做这个握手。

客户端不需要先连接到外部服务器来获取游戏服务器的IP和端口,因为我手动输入游戏服务器的IP和端口,游戏服务器的网络已经转发了端口,所以服务器的地址是已经知道的。我知道客户端的消息是到达游戏服务器的,因为服务器会记录输入的连接。

游戏服务器从传入的消息中得到客户端的IP和端口,这正是外部服务器通过握手会交给它的内容。我仔细检查了服务器从连接中接收到的IP,与客户端的外部IP地址一致。我还验证了两台机器的防火墙是否允许程序通过。

如果能得到更多的帮助,以弄清为什么服务器无法将消息传回客户端,我会非常感激。请解释或链接到有用的文件或研究。告诉我做更多的研究或在网上寻找答案是没有帮助的。我已经在这样做了。问题和答案的目的是为了从已经知道答案的人那里得到帮助。

networking udp udpclient
1个回答
0
投票

问题是,LAN和WLAN之间存在NAT。你不能只是通过它这么简单的工作。

© www.soinside.com 2019 - 2024. All rights reserved.