Table of contents
I wanted to do this project to don't have to redo every time a server base, but just writing packets method i will need in a DLL.
Imagine switching from a chat server to a remote mouse control or whatever in less than a minute. Wouldn't it be cool? Just replace the DLL or change the DLL name in the server configuration with the new one and it's done!
The project contains:
- Flexible Server - Server
- Chat Client - Simple chat client to test if the server works
- Client Utils - Contains the class to help you connect to a server
Logging
- Class for better console readability using log level MethodResponse
- Class used in both server and DLL to communicate each other - TestDLL - Simple DLL to handle incoming chat client Packet
- (Optional) MysqlConnector - Contains the class to help you connect to a mysql server and perform queries
Logic diagram
Server screenshot
Pros
- Easier
- You only have to write your DLL with the methods you need
- You can change the server purpose in less than a minute, by changing the DLL
Cons
We are going to create a small Chat Server/Client to test how the server works
Load DLL
The server loads the assembly of the specified DLL (In this example the DLL is TestDLL.dll).
The DLL must contains PacketHandler
class. PacketHandler
must contains OnClientConnect
and OnClientDisconnect
methods, we will use them if we want to do something when a client connects or disconnects.
The server loads only public methods with MethodResponse
return type. MethodResponse
is used by both server and dll to communicate each other.
Finally the server stores the methods information inside a list to invoke them later
string handlerDLL = GetConfig().data["packetHandlerDLL"];
Assembly packetHandlerDllAssembly = null;
if (File.Exists(handlerDLL))
{
packetHandlerDllAssembly =
Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + handlerDLL);
Logging.WriteLine("Loading Packet Handler DLL", LogLevel.Information);
Type Class = packetHandlerDllAssembly.GetType("PacketHandler");
try
{
dllInstance = Activator.CreateInstance(Class);
}
catch (Exception e)
{
Logging.WriteLine("User Created DLL must have " +
"PacketHandler Class. Closing..", LogLevel.Error);
Thread.Sleep(5000);
Environment.Exit(0);
}
int MethodsCount = 0;
RegisteredMethods = new List<Method>();
bool OnClientConnectMethodFound = false;
bool OnClientDisconnectMethodFound = false;
foreach (MethodInfo MethodInfo in Class.GetMethods(BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.Instance))
{
if (MethodInfo.Name == "OnClientConnect")
{
OnClientConnectMethodFound = true;
continue;
}
if (MethodInfo.Name == "OnClientDisconnect")
{
OnClientDisconnectMethodFound = true;
continue;
}
if (MethodInfo.ReturnType != typeof(MethodResponse))
{
Logging.WriteLine("Method: " + MethodInfo.Name +
" must return MethodResponse currently: " +
MethodInfo.ReturnType.Name, LogLevel.Error);
Logging.WriteLine("Method: " + MethodInfo.Name +
" not registered", LogLevel.Error);
continue;
}
string param = "";
Method Method = new Method(MethodInfo.Name, MethodInfo);
bool connIDParameterFound = false;
foreach (ParameterInfo pParameter in MethodInfo.GetParameters())
{
Method.AddParameter(pParameter.Name, pParameter.ParameterType);
param += pParameter.Name + " (" + pParameter.ParameterType.Name + ") ";
if (pParameter.Name.ToLower() == "connid" &&
pParameter.ParameterType == typeof(int))
{
connIDParameterFound = true;
}
}
if (!connIDParameterFound)
{
Logging.WriteLine("Method: " + MethodInfo.Name +
" must have a connID(int) param", LogLevel.Error);
Logging.WriteLine("Method: " + MethodInfo.Name +
" not registered", LogLevel.Error);
continue;
}
if (param == "")
param = "none ";
RegisteredMethods.Add(Method);
Logging.WriteLine("Method name: " + MethodInfo.Name +
" parameters: " + param + "registered", LogLevel.Information);
MethodsCount++;
}
if (!OnClientConnectMethodFound || !OnClientDisconnectMethodFound)
{
Logging.WriteLine("PacketHandler must contain OnClientConnect and " +
"OnClientDisconnect methods. Closing..", LogLevel.Error);
Thread.Sleep(5000);
Environment.Exit(0);
}
if (MethodsCount == 0)
{
Logging.WriteLine("Any method loaded. Closing..", LogLevel.Information);
Thread.Sleep(5000);
Environment.Exit(0);
}
Logging.WriteLine("Registered " + MethodsCount + " Methods", LogLevel.Information);
Logging.WriteLine("Loaded Packet Handler DLL", LogLevel.Information);
}
else
{
Logging.WriteLine("Unable to locate Packet Handler DLL named: " +
handlerDLL + ". Closing..", LogLevel.Error);
Thread.Sleep(5000);
Environment.Exit(0);
}
New message from the client
When the server receives a new message from the client first it parses message to the Packet
class and then it passes the Packet
to the HandlePacket
method
private void ReceiveCallback(IAsyncResult result)
{
Connection conn = (Connection)result.AsyncState;
try
{
int bytesRead = conn.socket.EndReceive(result);
if (bytesRead > 0)
{
HandlePacket(ParseMessage(Encoding.ASCII.GetString(conn.buffer, 0, bytesRead), conn), conn);
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length,
SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
}
else
{
Core.GetLogging().WriteLine("[" + conn.connID +
"] Connection lost from " +
((IPEndPoint)conn.socket.RemoteEndPoint).Address, LogLevel.Debug);
OnClientDisconnect(conn);
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
catch (SocketException e)
{
Core.GetLogging().WriteLine("[" + conn.connID +
"] Connection lost from " +
((IPEndPoint)conn.socket.RemoteEndPoint).Address, LogLevel.Debug);
OnClientDisconnect(conn);
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
}
OnClientConnect and OnClientDisconnect
These methods will invoke OnClientConnect
/OnClientDisconnect
passing the id of the client connection in the DLL
private void OnClientConnect(Connection conn)
{
Core.dllInstance.GetType().GetMethod("OnClientConnect").Invoke(
Core.dllInstance, new object[] { conn.connID });
}
private void OnClientDisconnect(Connection conn)
{
Core.dllInstance.GetType().GetMethod("OnClientDisconnect").Invoke(
Core.dllInstance, new object[] { conn.connID });
}
Parse the incoming client message
Parse the incoming client message to the Packet
class.
First we get the Packet
Header/name
Then we need to parse the Packet
body values type from string to the correct type (int, float etc). This is needed because otherwise we will get the exception "parameter type mismatch".
private Packet ParseMessage(string Message, Connection conn)
{
string PacketHeader = Message.Split(Delimiter)[0];
Packet Packet = new Packet(PacketHeader);
Message = Message.Substring(Message.IndexOf(Delimiter) + 1);
foreach (string Parameter in Message.Split(Delimiter))
{
int intN;
bool boolN;
if (int.TryParse(Parameter, out intN))
{
Packet.AddInt32(intN);
}
else if (Boolean.TryParse(Parameter, out boolN))
{
Packet.AddBoolean(boolN);
}
else
{
Packet.AddString(Parameter);
}
}
Packet.AddInt32(conn.connID);
return Packet;
}
Handle Packet
Handle parsed Packet
The server invokes the respective Packet
method in the DLL giving the parameters that has been parsed before
The invoke method returns an object holding the return value of the method that needs to be parsed to the MethodResponse
type
Finally it loops Packet
s contained in MethodResponse
and sends the message back to the client/s
private void HandlePacket(Packet Packet, Connection conn)
{
Core.GetLogging().WriteLine("Received Packet: " + Packet.GetPacketString(), LogLevel.Debug);
Method Method = Core.GetMethodByName(Packet.Header.ToLower());
if (Method != null)
{
if (Method.GetParametersCount() != Packet.bodyValues.Count)
{
Core.GetLogging().WriteLine("Method: " + Method.GetName() +
" has " + Method.GetParametersCount() +
" params but client request has " +
Packet.bodyValues.Count + " params", LogLevel.Error);
}
else
{
MethodResponse result = null;
try
{
result = (MethodResponse)Method.GetMethodInfo().Invoke(
Core.dllInstance, Packet.bodyValues.ToArray());
}
catch (Exception e)
{
Core.GetLogging().WriteLine("Error handling Method: " +
Method.GetName() + " Exception Message: " + e.Message, LogLevel.Error);
}
if (result != null)
{
Core.GetLogging().WriteLine("Handled Method: " +
Method.GetName() + ". Sending response..", LogLevel.Information);
foreach (Packet PacketToSend in result.Packets)
{
string PacketString = PacketToSend.GetPacketString();
if (PacketToSend.sendToAll)
{
sendToAll(StrToByteArray(PacketString));
Core.GetLogging().WriteLine("Sent response: " +
PacketString + " to all clients", LogLevel.Debug);
}
else if (PacketToSend.sendTo != null)
{
foreach (int connID in PacketToSend.sendTo)
{
Send(StrToByteArray(PacketString), _sockets[connID]);
Core.GetLogging().WriteLine("Sent response: " +
PacketString + " to client id: " + connID, LogLevel.Debug);
}
}
else
{
Send(StrToByteArray(PacketString), conn);
Core.GetLogging().WriteLine("Sent response: " +
PacketString + " to client id: " + conn.connID, LogLevel.Debug);
}
}
}
}
}
else Core.GetLogging().WriteLine("Invoked Method: " +
Packet.Header + " does not exist", LogLevel.Error);
}
The DLL is where you can customize your server to your needs. Here you can write your custom methods
You can use the private flag on the methods you don't want the server load
PacketHandler
must contains both OnClientConnect
and OnClientDisconnect
methods
Every public method must have a return type of MethodResponse
public class PacketHandler
{
public static List<User> Users;
private Logging Logging;
public PacketHandler()
{
Users = new List<User>();
Logging = new Logging();
Logging.MinimumLogLevel = 0;
}
private User GetUserByConnID(int connID)
{
foreach (User u in Users)
{
if (u.connID == connID)
return u;
}
return null;
}
private User GetUserByName(string Name)
{
foreach (User u in Users)
{
if (u.Name == Name)
return u;
}
return null;
}
public MethodResponse Login(string username, string password, int connID)
{
MethodResponse MethodResponse = new MethodResponse();
bool loginFailed = true;
if (password == "password")
loginFailed = false;
if (loginFailed)
{
Packet LoginResponse = new Packet("LOGIN_RESPONSE");
LoginResponse.AddBoolean(false);
MethodResponse.AddPacket(LoginResponse);
}
else
{
Packet LoginResponse = new Packet("LOGIN_RESPONSE");
LoginResponse.AddBoolean(true);
LoginResponse.AddInt32(connID);
Packet UserJoin = new Packet("USER_JOIN", true);
UserJoin.AddString(username);
MethodResponse.AddPacket(LoginResponse);
MethodResponse.AddPacket(UserJoin);
Users.Add(new User(connID, username));
Logging.WriteLine("User: " + username + " has joined the chat", LogLevel.Information);
}
return MethodResponse;
}
...Other chat methods...
public void OnClientDisconnect(int connID)
{
if (GetUserByConnID(connID) != null)
{
Logging.WriteLine("User: " + GetUserByConnID(connID).Name +
" has left the chat", LogLevel.Information);
Users.Remove(GetUserByConnID(connID));
}
}
public void OnClientConnect(int connID)
{
}
}
This is the client of a simple Chat program
You can connect to the server and do the login. Then you will be able to send a message to all the users connected or send a whisper
static void Main(string[] args)
{
conn = new ServerConnection();
conn.Connect(IPAddress.Parse("127.0.0.1"), 8888);
conn.onPacketReceive += new ServerConnection.onPacketReceiveHandler(HandlePacket);
...
}
static void Login()
{
Console.WriteLine("Write username");
username = Console.ReadLine();
Console.WriteLine("Write password");
string password = Console.ReadLine();
Packet Login = new Packet("LOGIN");
Login.AddString(username);
Login.AddString(password);
conn.Send(Login);
}
static void HandlePacket(object sender, Packet Packet)
{
switch (Packet.Header)
{
case "LOGIN_RESPONSE":
{
bool loginResponse = Convert.ToBoolean(Packet.bodyValues[0]);
if (!loginResponse)
{
Console.WriteLine("Login failed");
Login();
}
else
{
id = int.Parse(Packet.bodyValues[1].ToString());
Console.WriteLine("Login Successful");
Logged = true;
}
}
break;
...
}
}
Screenshot
I also implemented a Flexible Web Service to create a sort of web server panel
Simple web server panel for the chat
webServiceConnector.php
This script performs the login on the Web Service and it calls GetUserCount
and GetUserList
methods.
The response is expressed in json format
<?php
if (isset($_GET['readData']))
{
$client = new SoapClient("http://localhost:8000/FlexibleServer/?wsdl",array(
'login' => "admin", 'password' => "password"));
try
{
$response = $client->__soapCall("GetUserCount",array());
$arr=objectToArray($response);
$response2 = $client->__soapCall("GetUserList",array());
$arr2=objectToArray($response2);
$result = array_merge($arr,$arr2);
echo json_encode($result);
}
catch (SoapFault $exception)
{
trigger_error("SOAP Fault: (faultcode: {$exception->faultcode}, faultstring:
{$exception->faultstring})");
var_dump($exception);
}
}
?>
Javascript function
This function performs an ajax request calling webServiceConnector.php to get the json response.
function read()
{
var xmlhttp;
xmlhttp = GetXmlHttpObject();
if(xmlhttp == null)
{
alert("Boo! Your browser doesn't support AJAX!");
return;
}
xmlhttp.onreadystatechange = stateChanged;
xmlhttp.open("GET", "http://127.0.0.1/webServiceConnector.php?readData", true);
xmlhttp.send(null);
function stateChanged()
{
if(xmlhttp.readyState == 4)
{
var obj = jQuery.parseJSON(xmlhttp.responseText);
g1.refresh(obj["GetUserCountResult"]);
var txtarea = document.getElementById("txtarea");
if (obj["GetUserListResult"]["string"] != null)
{
var length = obj["GetUserListResult"]["string"].length;
var s = "";
for (var i = 0; i < length; i++) {
s += obj["GetUserListResult"]["string"][i];
}
txtarea.innerHTML = s;
txtarea.scrollTop = txtarea.scrollHeight;
}
else
{
txtarea.innerHTML = "";
txtarea.scrollTop = txtarea.scrollHeight;
}
setTimeout("read()",1000);
}
}
function GetXmlHttpObject()
{
if(window.XMLHttpRequest){
return new XMLHttpRequest();
}
if(window.ActiveXObject){
return new ActiveXObject("Microsoft.XMLHTTP");
}
return null;
}
}
Inside the DLL
Methods that will be called from a php script
public class WebserviceHandler : IWebservice
{
public string[] GetUserList()
{
List<string> Names = new List<string>();
foreach (User User in PacketHandler.Users)
Names.Add(User.Name + "\n");
return Names.ToArray();
}
public int GetUserCount()
{
return PacketHandler.Users.Count;
}
}
[ServiceContract]
public interface IWebservice
{
[OperationContract]
string[] GetUserList();
[OperationContract]
int GetUserCount();
}
Server code to start the Web Service
public void Start()
{
Uri baseAddress = new Uri("http://" + IP.ToString() + ":" + Port + "/FlexibleServer/");
Type Webservice = packetHandlerDllAssembly.GetType("WebserviceHandler");
Type Interface = packetHandlerDllAssembly.GetType("IWebservice");
foreach (MethodInfo m in Interface.GetMethods(BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.Instance))
{
string param = "";
foreach (ParameterInfo pParameter in m.GetParameters())
{
param += pParameter.Name + " (" + pParameter.ParameterType.Name + ") ";
}
if (param == "")
param = "none ";
Core.GetLogging().WriteLine("webService Method name: " + m.Name +
" parameters: " + param + "registered", LogLevel.Information);
}
ServiceHost selfHost = new ServiceHost(Webservice, baseAddress);
BasicHttpBinding http = new BasicHttpBinding();
http.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly;
http.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
try
{
ServiceEndpoint endpoint = selfHost.AddServiceEndpoint(
Interface, http, "RemoteControlService");
endpoint.Behaviors.Add(new webServiceEvent());
selfHost.Credentials.UserNameAuthentication.UserNamePasswordValidationMode =
UserNamePasswordValidationMode.Custom;
selfHost.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator =
new LoginValidator();
ServiceMetadataBehavior smb =
selfHost.Description.Behaviors.Find<ServiceMetadataBehavior>();
if (smb == null)
{
smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
selfHost.Description.Behaviors.Add(smb);
}
try
{
selfHost.Open();
Core.GetLogging().WriteLine("webService is ready on http://" +
IP.ToString() + ":" + Port + "/FlexibleServer/", LogLevel.Information);
}
catch (Exception e)
{
if (e is AddressAccessDeniedException)
{
Core.GetLogging().WriteLine("Could not register url: http://" + IP +
":" + Port + ". Start server as administrator", LogLevel.Error);
}
if (e is AddressAlreadyInUseException)
{
Core.GetLogging().WriteLine("Could not register url: http://" +
IP + ":" + Port + ". Address already in use", LogLevel.Error);
}
Core.GetLogging().WriteLine("webService aborted due to an exception", LogLevel.Error);
}
}
catch (CommunicationException ce)
{
Console.WriteLine("An exception occurred: {0}", ce.Message);
selfHost.Abort();
}
}
Screenshot
Add a reference to MysqlConnector.dll and MySql.Data.dll in TestDLL
Add
using System.Data;
using MysqlConnector;
Example: Initialize mysql connection
public class PacketHandler
{
public static List<User> Users;
private Logging Logging;
public Mysql MysqlConn;
public PacketHandler()
{
Users = new List<User>();
Logging = new Logging();
Logging.MinimumLogLevel = 0;
MysqlConn = new Mysql();
MysqlConn.Connect("127.0.0.1", 3306, "root", "password", "databasename");
MysqlConn.GetClient();
}
...
}
Chat login method using MySql
public MethodResponse Login(object username, object password, int connID)
{
MethodResponse MethodResponse = new MethodResponse();
DataRow Row = MysqlConn.ReadDataRow("SELECT * FROM users where username = '" + username + "' AND password = '" + password + "'");
bool loginFailed = true;
if (Row != null)
{
loginFailed = false;
}
if (loginFailed)
{
Packet LoginResponse = new Packet("LOGIN_RESPONSE");
LoginResponse.AddBoolean(false);
MethodResponse.AddPacket(LoginResponse);
}
else
{
Packet LoginResponse = new Packet("LOGIN_RESPONSE");
LoginResponse.AddBoolean(true);
LoginResponse.AddInt32(connID);
Packet UserJoin = new Packet("USER_JOIN", true);
UserJoin.AddString(username.ToString());
MethodResponse.AddPacket(LoginResponse);
MethodResponse.AddPacket(UserJoin);
Users.Add(new User(connID, username.ToString()));
Logging.WriteLine("User: " + username + " has joined the chat", LogLevel.Information);
}
return MethodResponse;
}
- Compile TestDLL
- Move TestDLL.dll, MysqlConnector.dll, MySql.Data.dll to the server directory
- Open FlexibleServer.exe located in /Flexible Server/bin/Debug as administrator
- Open one or more ChatClient.exe located in /Chat Client/bin/Debug
- Insert an username
- The password is "password"
- To send a whisper write "whisper target message" (Replace target with the name of the user you want to send the message to)
- Create a new DLL project in Visual Studio
- Add reference to System.ServiceModel
- Add reference to Logging.dll to write on console using LogLevel
- (Optional) Add reference to MysqlConnector.dll and MySql.Data.dll if you want add Mysql support
- Write DLL base code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
public class PacketHandler
{
public PacketHandler()
{
}
public MethodResponse Yourincomingpacketname(string incomingpacketparameter, etc)
{
MethodResponse MethodResponse = new MethodResponse();
... Your code ....
return MethodResponse();
}
public void OnClientDisconnect(int connID)
{
}
public void OnClientConnect(int connID)
{
}
}
- Compile DLL
- Move DLL and its reference to server directory
- Change the packetHandlerDLL value inside flexibleserver-config.conf with your DLL name
Configuration file
Contains all server settings
## Flexible Server Configuration File
## Must be edited for the server to work
## Server Configuration
MinimumLogLevel=0
tcp.bindip=127.0.0.1
tcp.port=8888
packetHandlerDLL=TestDLL.dll
enableWebService=1
webservice.bindip=127.0.0.1
webservice.port=8000
webservice.username=admin
webservice.password=password
Logging
Class for better console readability using log level
Debug | 0 |
Information | 1 |
Warning | 2 |
Error | 3 |
Success | 4 |
WriteLine Code
public void WriteLine(string Line, LogLevel Level)
{
if (Level >= MinimumLogLevel)
{
DateTime _DTN = DateTime.Now;
StackFrame _SF = new StackTrace().GetFrame(1);
Console.Write("[");
Console.ForegroundColor = ConsoleColor.Green;
Console.Write(_SF.GetMethod().ReflectedType.Name + "." + _SF.GetMethod().Name);
Console.ForegroundColor = ConsoleColor.Gray;
Console.Write("] ยป ");
if (Level == LogLevel.Debug)
Console.ForegroundColor = ConsoleColor.Gray;
else if (Level == LogLevel.Error)
Console.ForegroundColor = ConsoleColor.Red;
else if (Level == LogLevel.Information)
Console.ForegroundColor = ConsoleColor.Yellow;
else if (Level == LogLevel.Success)
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(Line);
Console.ForegroundColor = ConsoleColor.Gray;
}
}
MethodResponse
This class allows the server and dll to communicate with each other
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class Packet
{
public string Header;
public List<object> bodyValues;
public bool sendToAll;
public List<int> sendTo;
private char Delimiter = (char)1;
public Packet(string Header, bool sendToAll = false, List<int> sendTo = null)
{
this.Header = Header;
this.bodyValues = new List<object>();
this.sendToAll = sendToAll;
this.sendTo = sendTo;
}
public void AddInt32(int Value)
{
bodyValues.Add(Value);
}
public void AddString(string Value)
{
bodyValues.Add(Value);
}
public void AddBoolean(bool Value)
{
bodyValues.Add(Value);
}
public string GetPacketString()
{
string PacketString = Header;
foreach (object o in bodyValues)
{
PacketString += Delimiter.ToString() + o.ToString();
}
return PacketString;
}
}
public class MethodResponse
{
public List<Packet> Packets;
public MethodResponse()
{
Packets = new List<Packet>();
}
public void AddPacket(Packet Packet)
{
Packets.Add(Packet);
}
}
Normal server
It tooks about 12ms to parse packet and sending response
Flexible server
It tooks 27ms to parse packet, invoke the method in the DLL and send the packets contained in MethodResponse
Conclusion: Flexible server has to do more operation. Of course it will slower. it's not recommended for large projects.
Thanks for reading this article; I hope you liked it.
Let me know if you find bugs or you have suggestions to improve it, i'll be happy to fix/implement them
- Version 1.0 - 19/07/2013: Initial release
- 20/07/2013: (optional) MysqlConnector including examples, How to create your own DLL, Points of interest, added code comments on php/javascript scripts
- 23/07/2013: Added table of contents, pros and cons, talk about perfomance, conclusion