Peer to Peer Networking Example Using the Lidgren.Network Framework

Brief topic switch: Peer to peer networking. I had wanted to get this example in a simple movement game in IceCream but found I needed to start with a console app first. So here it is. I’ve cleverly named it NetConsoles, and the purpose is to teach myself how to link multiple nodes in a peer to peer fashion using the Lidgren.Network framework. While I had some initial issues, it wasn’t all that hard.

Nearly every example I could find using Lidgren.Network was a client/server architecture, so I had to do quite a bit of experimenting to get peer to peer going. Finding sample code for it was completely fruitless. But I did see a post indicating it is possible and relatively easy, so I pressed on. It turns out that the base object of NetClient and NetServer is NetPeer, and it’s all you need to connect p2p (I’m now tired of typing it out).

Two things stand out from this experience: the first is that I tried to create a semi-contained and reusable networking object. It’s far from robust, but I think it will be a sufficient base to carry into my next networking endeavor. The second is that I don’t typically need to do much threading, but it was necessary for this application. The reason is that we need to be constantly polling for incoming network data while also waiting for the user to type commands (like to send a test message). Console.WriteLine is blocking, so my main networking object is written so that when Listen() is called, it spins off a thread that continuously asks Lidgren.Network, “do we have messages to process?” If yes, parse and handle. While basic, I enjoyed the exercise because threads are not in my day to day toolbox (but maybe they should be!).

You can download all the code here, which includes binaries, in case people have trouble building: NetConsoles

Note that the folder structure expects the¬†lidgren-network-gen3 folder to be just one-level above the solution folder. I’d rather include the code library than just a DLL to make debugging and stepping through easier. While learning, it was very handle to be able to visually scan the internal workings of the library rather than be stuck with just IntelliSense (even though IntelliSense is very nice).

Some highlights of the code include:

Construction of the NetPeerConfiguration object (only the first two NetIncomingMessageType are required for basic functionality, but I wanted to see the other 2 in action).

Config = new NetPeerConfiguration("test_console");
Config.EnableMessageType(NetIncomingMessageType.DiscoveryRequest);
Config.EnableMessageType(NetIncomingMessageType.DiscoveryResponse);
Config.EnableMessageType(NetIncomingMessageType.ConnectionApproval);
Config.EnableMessageType(NetIncomingMessageType.UnconnectedData);
Config.LocalAddress = NetUtility.Resolve("localhost");

The initialization of the NetPeer object (extremely simple) and starting up the listener thread:

Peer = new NetPeer(Config);
Peer.Start();
Console.WriteLine("listening on " + Config.Port.ToString());
 
NetWorker = new MyNetWorker(Peer, this);
NetThread = new Thread(NetWorker.ProcessNet);
NetThread.Start();

Sending a basic string message (the MessageType enum is my own, not part of the framework):

public void SendMessage(string message) {
    if (Peer.Connections == null || Peer.Connections.Count == 0) {
        Console.WriteLine("No connections to send to.");
        return;
    }
    NetOutgoingMessage msg = Peer.CreateMessage();
    msg.Write((int)MessageType.StringMessage);
    msg.Write(message);
    Peer.SendMessage(msg, Peer.Connections, NetDeliveryMethod.ReliableOrdered, 0);
}

Sending peer info to all connected peers (by passing Peer.Connections to Peer.SendMessage):

public void SendPeerInfo(IPAddress ip, int port) {
    Console.WriteLine(string.Format("Broadcasting {0}:{1} to all (count: {2})", ip.ToString(),
        port.ToString(), Peer.ConnectionsCount));
    NetOutgoingMessage msg = Peer.CreateMessage();
    msg.Write((int)MessageType.PeerInformation);
    byte[] addressBytes = ip.GetAddressBytes();
    msg.Write(addressBytes.Length);
    msg.Write(addressBytes);
    msg.Write(port);
    Peer.SendMessage(msg, Peer.Connections, NetDeliveryMethod.ReliableOrdered, 0);
}

And finally–huge code dump–the listen thread class:

public class MyNetWorker
{
    NetPeer peer = null;
    NetManager netManager = null;
    public volatile bool shouldQuit = false;
 
    public MyNetWorker(NetPeer inPeer, NetManager inNetManager) {
        peer = inPeer;
        netManager = inNetManager;
    }
 
    public void ProcessNet() {
        // read messages
        while (!shouldQuit) {
            Thread.Sleep(1);
            if (peer == null)
                continue;
            NetIncomingMessage msg;
            while ((msg = peer.ReadMessage()) != null) {
                switch (msg.MessageType) {
                    case NetIncomingMessageType.DiscoveryRequest:
                        Console.WriteLine("ReceivePeersData DiscoveryRequest");
                        peer.SendDiscoveryResponse(null, msg.SenderEndpoint);
                        break;
                    case NetIncomingMessageType.DiscoveryResponse:
                        // just connect to first server discovered
                        Console.WriteLine("ReceivePeersData DiscoveryResponse CONNECT");
                        peer.Connect(msg.SenderEndpoint);
                        break;
                    case NetIncomingMessageType.ConnectionApproval:
                        Console.WriteLine("ReceivePeersData ConnectionApproval");
                        msg.SenderConnection.Approve();
                        //broadcast this to all connected clients
                        //msg.SenderEndpoint.Address, msg.SenderEndpoint.Port
                        netManager.SendPeerInfo(msg.SenderEndpoint.Address, msg.SenderEndpoint.Port);
                        break;
                    case NetIncomingMessageType.Data:
                        //another client sent us data
                        Console.WriteLine("BEGIN ReceivePeersData Data");
                        MessageType mType = (MessageType)msg.ReadInt32();
                        if (mType == MessageType.StringMessage) {
                            Console.WriteLine("Message received: " + msg.ReadString());
                        }
                        else if (mType == MessageType.PeerInformation) {
                            Console.WriteLine("Data::PeerInfo BEGIN");
                            int byteLenth = msg.ReadInt32();
                            byte[] addressBytes = msg.ReadBytes(byteLenth);
                            IPAddress ip = new IPAddress(addressBytes);
                            int port = msg.ReadInt32();
                            //connect
                            IPEndPoint endPoint = new IPEndPoint(ip, port);
                            Console.WriteLine("Data::PeerInfo::Detecting if we're connected");
                            if (peer.GetConnection(endPoint) == null) {//are we already connected?
                                //Don't try to connect to ourself!
                                if (peer.Configuration.LocalAddress.GetHashCode() != endPoint.Address.GetHashCode()
                                        || peer.Configuration.Port.GetHashCode() != endPoint.Port.GetHashCode()) {
                                    Console.WriteLine(string.Format("Data::PeerInfo::Initiate new connection to: {0}:{1}",
                                        endPoint.Address.ToString(), endPoint.Port.ToString()));
                                    peer.Connect(endPoint);
                                }
                            }
                            Console.WriteLine("Data::PeerInfo END");
                        }
                        Console.WriteLine("END ReceivePeersData Data");
                        break;
                    case NetIncomingMessageType.UnconnectedData:
                        string orphanData = msg.ReadString();
                        Console.WriteLine("UnconnectedData: " + orphanData);
                        break;
                    default:
                        Console.WriteLine("ReceivePeersData Unknown type: " + msg.MessageType.ToString());
                        try {
                            Console.WriteLine(msg.ReadString());
                        }
                        catch {
                            Console.WriteLine("Couldn't parse unknown to string.");
                        }
                        break;
                }
            }
        }
        Console.WriteLine("Exiting net thread!");
    }
}

Whew! It’s a lot, but p2p is pretty cool! Good luck!