This is the next post in my Fondusi’s Development Blog series. Click here to check out week 5 or start at the beginning.
Week 6 – September 5th – September 11th
This week we spent some time doing little things – fixing bugs, changing how things look, etc. We also started work on the networking code which is going to be what I’m going to talk about here.
A TCP Network Server in C#
My first step was to develop a way for multiple clients to connect with the server. Before starting this, I hadn’t really done much with threading or sockets in C#. However, I knew the idea behind threading, and I’d used TCP sockets in VB6 (for the old version of Fondusi’s).
If you’ve never used sockets in VB6, it works similarly to Async sockets in C#. Basically, you would add a socket object to a form and whenever it received a connection or data, a callback event would be fired. Trying to *easily* translate this to C# was proving difficult, however. Not only because of the differences in the languages, but also because the new server is a console application and the old one was windows forms.
I finally settled on creating a threaded server where there was one listening socket which, upon connection, spawned a receiving thread for the connected socket. When the socket disconnects, the receive thread function throws an error and the thread ends.
Now, I haven’t tested this with many connections yet, since I don’t yet have working client code for players (we’re only doing the map editor, remember). But, with the number of connections in tests I’ve done, everything works fine.
Anyways, enough with my background story, let’s get to the meat. Mmmm delicious meat. Here’s how the server works.
Listening for Connections
First things first, we need to set up our listening socket and thread. Create a new class (I called mine “NetServer”) and add these instance variables:
1 2 3 4 5 |
Socket _listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Thread _listeningThread; volatile bool _stopListening = false; Socket[] _socketList = new Socket[30]; |
The next thing we need to do is create a method that will listen for and accept connections on the listener thread.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private void Listen() { //Allow connections to any IP address of this machine on // port 55555 _listenSocket.Bind(new IPEndPoint(IPAddress.Any, 55555)); //Start listening with a connection queue of 30 _listenSocket.Listen(30); //Output information to the console Console.WriteLine("Server is waiting on socket {0}:{1}", _listenSocket.LocalEndPoint, 55555); //This is the bread and butter of the listening thread. See the // following paragraph for the explanation. while (!_stopListening) try { AcceptConnection(_listenSocket.Accept()); } catch { } } |
This method begins by binding the listening socket to any address on the host machine on port 55555. The next line tells it to start listening with a connection queue of 30. This means that if multiple connections come in at the same time, they will be queued (to a maximum queue length of 30) until Accept() is called. After that, we output to the console that we’re listening and on what IP/port. We then begin our accept connection loop.
The boolean variable _stopListening allows us to exit the loop later on. Within the loop, we see that the return value of _listenSocket.Accept() is passed to the AcceptConnection function. One thing I want to note here is that the Accept function is “blocking”, which means that the function blocks the calling thread and waits to return until it has something to return. This means that, if we ran it on the main thread, it would stop the main thread’s execution until someone tried to make a connection. This is why we have to run the listener on it’s own thread.
Accepting Connections
Now, you’re probably wondering what the deal is with the AcceptConnection method. (Note: If you’re not wondering that, you should be!) _listenSocket.Accept() returns the newly connected socket and that gets passed into the AcceptConnection method. AcceptConnection does a few things for us. It:
- Sets NoDelay for the TCP socket so that the packets don’t get buffered before sending.
- Finds an open slot for the connection.
- Adds the connection into the slot or sends an alert to the client saying there are no open slots.
- Creates and starts the receiver thread for that client.
You could put all the code in the Listen method, but for the sake of readability and explanation, I decided to split it up. So here’s the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
private void AcceptConnection(Socket socket) { socket.NoDelay = true; //Prevent packets from being buffered short openSocket = -1; //Find an open socket from the array for (int i = 0; i < _socketList.Length; i++) { if (_socketList[i] == null) { openSocket = i; } } if (openSocket == -1) { //If no slot is available send that info back and // close the connection socket.Send(new byte[] { 0, 0, 0, 1 }); socket.Close(); } else { //Add the socket to the list _socketList[openSocket] = socket; //Create the socket thread Thread sockThread = new Thread(new ParameterizedThreadStart(ReceiveData)); sockThread.IsBackground = true; //Start the socket thread sockThread.Start(openSocket); } } |
I’ve added comments to the code and I think it’s pretty self-explanatory so I won’t describe it any more.
Time-out
This post has become longer than I was expecting and there’s a lot more left to do discussing the ReceiveData function. So, I’ve decided to split that into a separate post. However, I do want to finish off some stuff here first.
Time-in – Initializing the Listening Thread
If you’re sharp, you may have noticed that the listening thread is never created or started. To set up the listening thread, add these lines to your class’s constructor:
1 2 |
_listeningThread = new Thread(new ThreadStart(Listen)); _listeningThread.IsBackground = true; |
This is basically just telling the listening thread to execute the Listen method when _listeningThread.Start() is called. If you’re wondering what the IsBackground property does, it tells the CLR that we don’t want the application to wait for this thread to finish if it’s trying to terminate (close). The actual description from MSDN is this:
Background threads are identical to foreground threads, except that background threads do not prevent a process from terminating. Once all foreground threads belonging to a process have terminated, the common language runtime ends the process. Any remaining background threads are stopped and do not complete.
Controlling the Server
The last bit is the two control functions for our NetServer class. These allow the server to be started and stopped.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public void StartServer() { //Make sure the listening thread isn't running if (!_listeningThread.IsAlive) { //Reset the stop listening variable _stopListening = false; //Start the listening thread _listeningThread.Start(); } } public void StopServer() { //Make sure we're listening if (_listeningThread.IsAlive) { //Tell the Accept while loop to stop looping _stopListening = true; //Close the listen socket so the thread will stop blocking _listenSocket.Close(); //Close all client sockets, this should also stop the threads for(int i = 0; i < _sockets.Length; i++) { //Close the socket _sockets[i].Close(); //Reset the value to null so it'll be "open" if we start again _sockets[i] = null; } } } |
Again, the code here is pretty self-explanatory (especially with comments) so I’m not going to delve into it. Check out Part 2 (when I post it) for the ReceiveData function and explanation.
The Rest
Finally, here’s the change log for the rest of the stuff we did this week:
- Fixed bug with players appering to be on the wrong map/layer
- Fixed a bug switching player layers in the entry behaviours
- Added box texture to tile engine
- Fixed drawing of player names (made them centred)
- Added player hp bar
- Fixed a bug with region editing
- Added player stats
- Updated tile entry damage, heal and kill behaviours
- Removed error thrown when no maps found
- Added more to Animation Form and added button to open animation form on Controls Form
- Changed size of player HP bar
- Set player health by default
- Added Resource object to be used for text and icons and the such
- Added icons for the map edit controls (Tile Draw, etc)
- Resized and moved around map edit controls
- Fixed bug with tile editor not reseting to Tile Draw control, and drawing a 2×2 tile when nothing is selected
- Added tool tips and string resources for the Tile Edit [radio]buttons
- Removed ImageComboBox
- Added ImageListBox
- Have hard coded tiles adding to the Animated Tile image stack, very close to finishing the create/edit form!
- Added map server project with basic threading in the console
- Added the ability to remove layer switching entry behaviours by right clicking
- Show arrows on the map for layer change tiles while the layer change tab is active
- Added players array and server info constants to GameObjects
- Added basic Player class
- Renamed network thread to server thread
- Created a socket list class
- Added some classes/code for testing the map server
- Got some basic socket connections working