So, after taking the advice of the forum some 6 months ago to start my game-making career off with a simple text adventure, I am now neck-deep in a (somewhat) working MUD. What can I say? Go big or go home. Up until a few days ago, the networking portion of my MUD was working beautifully, but as I started sending more and more messages between client and server, I began noticing some odd behavior. Namely, my poorly implemented NetworkModel class had no way to deal with message overlap. I have since attempted to remedy this problem, as I will show you shortly.
First, let me explain my strategy. I'm sending strings separated with ` and ~ to denote the various parts of the message. These strings are then split, routed, and examined by the engines on both sides to generate an effect. Pretty standard. When a client connects to my server, I store the open connection in two dictionaries - one by characterID, the other by acccountID. Both are filled outside of the class by the Game Manager class. Requests are added to a list that the Game Manager pulls from, executing the top-most request then discarding it. Both dictionaries and the list are custom thread-safe class I've designed (cuz I'm using VS 2008).
Alright, code avalanche incoming.
public class ClientInfo
{
public Socket WorkSocket = null;
public byte[] Buffer;
public byte[] BackBuffer;
public string RecIncom = "";
public ClientInfo(Socket client, int buffersize)
{
WorkSocket = client;
Buffer = new byte[buffersize];
BackBuffer = new byte[buffersize];
}
}
public class NetworkModel
{
public const int BUFFER_SIZE = 1024;
private const string HEADER = "``";
private const string FOOTER = "~~";
#region PROPERTIES
private Socket Server;
public SafeDictionary<string, Socket> ConnectionsbyCharacter;
public SafeDictionary<string, Socket> ConnectionsbyAccount;
public SafeList<RequestModel> RequestQueue;
#endregion PROPERTIES
#region CONSTRUCTORS
public NetworkModel(int port, SafeList<RequestModel> requests)
{
ConnectionsbyAccount = new SafeDictionary<string, Socket>();
ConnectionsbyCharacter = new SafeDictionary<string, Socket>();
RequestQueue = requests;
Server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint EndPoint = new IPEndPoint(IPAddress.Any, port);
Server.Bind(EndPoint);
}
#endregion CONSTRUCTORS
#region METHODS
public void Begin()
{
Server.Listen(20);
Server.BeginAccept(new AsyncCallback(Accept), null);
}
private void Accept(IAsyncResult Result)
{
ClientInfo newClient = new ClientInfo(Server.EndAccept(Result), BUFFER_SIZE);
Server.BeginAccept(new AsyncCallback(Accept), null);
newClient.WorkSocket.BeginReceive(newClient.Buffer, 0, 1024, SocketFlags.None,
new AsyncCallback(ReceiveMessage), newClient);
}
public void Send(Socket client, string ToSend)
{
byte[] Message = Encoding.ASCII.GetBytes(HEADER + ToSend + FOOTER);
client.BeginSend(Message, 0, Message.Length, SocketFlags.None,
new AsyncCallback(SendMessage), client);
}
public void Send(string ID, string ToSend)
{
Socket Client = ConnectionsbyCharacter[ID];
byte[] Message = Encoding.ASCII.GetBytes(HEADER + ToSend + FOOTER);
Client.BeginSend(Message, 0, Message.Length, SocketFlags.None,
new AsyncCallback(SendMessage), Client);
}
public void SendToAllConnected(string ToSend)
{
foreach (Socket client in ConnectionsbyCharacter)
{
Send(client, ToSend);
}
}
private void SendMessage(IAsyncResult Result)
{
Socket Client = (Socket)Result.AsyncState;
int AmountSent = Client.EndSend(Result);
//Look into comparing amount sent vs. size of original message?
}
private void ReceiveMessage(IAsyncResult Result)
{
ClientInfo thisClient = (ClientInfo)Result.AsyncState;
string Received = "";
try
{
int AmountReceived = thisClient.WorkSocket.EndReceive(Result);
//If we've received any sort of message...
if (AmountReceived > 0)
{
//Translate the message and determine begin points and end points
string Contents = Encoding.ASCII.GetString(thisClient.Buffer, 0, AmountReceived);
int HeadLoc = Contents.IndexOf(HEADER);
int TermLoc = Contents.IndexOf(FOOTER);
//If we have a begin point...
if (HeadLoc > -1)
{
//...and an end point, and begin comes first...
if (TermLoc > -1 & HeadLoc < TermLoc)
{
//...we've received one whole message!
Received = Contents.Substring(HeadLoc, TermLoc - HeadLoc + 1);
//Now we need to check if there is another message, partial or otherwise, also stored in the buffer
if (TermLoc != 1022 & thisClient.Buffer[TermLoc + 2] != null)
{
//If so, copy the unused buffer portion to temp storage while we clear the main buffer, then
//copy back so we can begin receiving it again
int Len = thisClient.Buffer.Length - TermLoc - 1;
Array.Copy(thisClient.Buffer, TermLoc + 2, thisClient.BackBuffer, 0, Len);
thisClient.Buffer = new byte[BUFFER_SIZE];
Array.Copy(thisClient.BackBuffer, thisClient.Buffer, Len);
thisClient.BackBuffer = new byte[BUFFER_SIZE];
thisClient.WorkSocket.BeginReceive(thisClient.Buffer, 0, 1024, SocketFlags.None,
new AsyncCallback(ReceiveMessage), thisClient);
}
else
{
//Otherwise, just clear the main buffer
thisClient.Buffer = new byte[BUFFER_SIZE];
}
}
//...and an end point, but end comes first...
else if (TermLoc > -1 & TermLoc < HeadLoc)
{
//This means we are looking at the end of a previous message, so get the last bit of the last message
//and add it to the previous.
thisClient.RecIncom += Contents.Substring(0, TermLoc + 2);
Received = thisClient.RecIncom;
thisClient.RecIncom = "";
//Now we need to move the next message to a temp buffer to clear the main buffer, then copy it all back
//and start receiving again
int Len = thisClient.Buffer.Length - TermLoc - 1;
Array.Copy(thisClient.Buffer, TermLoc + 2, thisClient.BackBuffer, 0, Len);
thisClient.Buffer = new byte[BUFFER_SIZE];
Array.Copy(thisClient.BackBuffer, thisClient.Buffer, Len);
thisClient.BackBuffer = new byte[BUFFER_SIZE];
thisClient.WorkSocket.BeginReceive(thisClient.Buffer, 0, 1024, SocketFlags.None,
new AsyncCallback(ReceiveMessage), thisClient);
}
//...but no end point
else
{
//This means we have a partial message. This partial goes into a holding string while we
//clear our buffer and start receiving the rest of the message
thisClient.RecIncom = Contents.Substring(0, AmountReceived);
thisClient.Buffer = new byte[BUFFER_SIZE];
thisClient.WorkSocket.BeginReceive(thisClient.Buffer, 0, 1024, SocketFlags.None,
new AsyncCallback(ReceiveMessage), thisClient);
}
}
//If we DON'T have a begin point...
else
{
//...but we DO have an end point
if (TermLoc > -1)
{
//This is the rest of a partial message, so we just add the two together and clear our buffer!
Received = thisClient.RecIncom + Contents.Substring(0, TermLoc + 1);
thisClient.RecIncom = "";
thisClient.Buffer = new byte[BUFFER_SIZE];
}
//...and we don't have an end point
else
{
//This is more of a partial message, so just add it, clear the buffer and start receiving again
thisClient.RecIncom += Contents;
thisClient.Buffer = new byte[BUFFER_SIZE];
thisClient.WorkSocket.BeginReceive(thisClient.Buffer, 0, 1024, SocketFlags.None,
new AsyncCallback(ReceiveMessage), thisClient);
}
}
}
//If a complete message was received, parse it out into RequestModel object and add it to the game loop
if (Received != "")
{
Received = Received.Substring(2, Received.Length - 2);
string[] Split = Received.Split('`');
string[] AccountSplit = Split[0].Split('~');
string[] MessageSplit = Split[1].Split('~');
//Message received format = AccountID~CharacterID`ect~ect.
RequestModel Request = new RequestModel();
Request.Client = thisClient.WorkSocket;
Request.AccountName = AccountSplit[0];
Request.CharacterID = AccountSplit[1];
Request.Request = MessageSplit;
RequestQueue.Add(Request);
}
//Do I need this?
thisClient.WorkSocket.BeginReceive(thisClient.Buffer, 0, 1024, SocketFlags.None,
new AsyncCallback(ReceiveMessage), thisClient);
}
catch
{
}
}
#endregion METHODS
}
I plan on implementing something very similar to this client-side. I THINK I have all my message overlap problems ironed out, but I would like some advice from those with more experience. I couldn't find much more than theory when it came to solving message overlap, so I had to pretty much make this up as I went. Is there anywhere I can shore this up? Will it cause me problems in the future? Is it going to be too slow? Thanks in advance!