使用 DLL 的灵活 TCP/IP 服务器及 Web 服务






4.98/5 (19投票s)
灵活的服务器,使用用户创建的 DLL 来处理进出的客户端数据包。
目录
引言
我创建这个项目的目的是为了不必每次都重新编写服务器基础,而只需编写我需要的 DLL 中的数据包方法。
想象一下,从聊天服务器切换到远程鼠标控制或其他任何东西,不到一分钟就能完成。是不是很酷?只需替换 DLL 或在服务器配置中将 DLL 名称更改为新的即可!
项目包含
- 灵活服务器 - 服务器
- 聊天客户端 - 简单的聊天客户端,用于测试服务器是否正常工作
- 客户端工具 - 包含帮助您连接到服务器的类
日志记录
- 用于通过日志级别提高控制台可读性的类MethodResponse
- 服务器和 DLL 之间用于通信的类- TestDLL - 用于处理进出的聊天客户端数据包的简单 DLL
- (可选)MysqlConnector - 包含帮助您连接到 mysql 服务器并执行查询的类
逻辑图
服务器截图
优点和缺点
优点
- 更容易
- 您只需编写包含所需方法的 DLL
- 通过更改 DLL,您可以在不到一分钟的时间内更改服务器的目的
缺点
- 速度稍慢。请阅读 关于性能章节了解更多信息
目标是什么?
我们将创建一个小型的聊天服务器/客户端来测试服务器的工作方式
服务器
加载 DLL
服务器加载指定 DLL 的程序集(在本例中为 TestDLL.dll)。
DLL 必须包含 PacketHandler
类。PacketHandler
必须包含 OnClientConnect
和 OnClientDisconnect
方法,如果我们想在客户端连接或断开连接时执行某些操作,我们将使用它们。
服务器仅加载返回类型为 MethodResponse
的公共方法。MethodResponse
由服务器和 DLL 共享,用于相互通信。
最后,服务器将方法信息存储在一个列表中,以便以后调用
//Get User Created DLL
string handlerDLL = GetConfig().data["packetHandlerDLL"];
Assembly packetHandlerDllAssembly = null;
//Check if User Created DLL exists else close Server
if (File.Exists(handlerDLL))
{
//Load User Created DLL Assembly
packetHandlerDllAssembly =
Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + handlerDLL);
Logging.WriteLine("Loading Packet Handler DLL", LogLevel.Information);
//Get PacketHandler Class
Type Class = packetHandlerDllAssembly.GetType("PacketHandler");
try
{
//Create a instance of PacketHandler Class
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;
//Create a list of methods
RegisteredMethods = new List<Method>();
bool OnClientConnectMethodFound = false;
bool OnClientDisconnectMethodFound = false;
//Get methods created by user
foreach (MethodInfo MethodInfo in Class.GetMethods(BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.Instance))
{
//Check if OnClientConnect and OnClientDisconnect methods exist
if (MethodInfo.Name == "OnClientConnect")
{
OnClientConnectMethodFound = true;
continue;
}
if (MethodInfo.Name == "OnClientDisconnect")
{
OnClientDisconnectMethodFound = true;
continue;
}
//Only load methods with MethodResponse return type
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 = "";
//Create a new method class. MethodInfo is necessary for future invokes of DLL Methods
Method Method = new Method(MethodInfo.Name, MethodInfo);
//Method must have connID(int) Param
bool connIDParameterFound = false;
//Get method parameters
foreach (ParameterInfo pParameter in MethodInfo.GetParameters())
{
//Add Parameter
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 ";
//Add method to the registered methods list
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);
}
//Close server if there is any registered method
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);
}
来自客户端的新消息
当服务器收到来自客户端的新消息时,首先将消息解析为 Packet
类,然后将 Packet
传递给 HandlePacket
方法。
/// <summary>
/// On Packet received callback</summary>
/// <param name="result">Status of asynchronous operation</param>
/// </summary>
private void ReceiveCallback(IAsyncResult result)
{
//get our connection from the callback
Connection conn = (Connection)result.AsyncState;
try
{
//Grab our buffer and count the number of bytes receives
int bytesRead = conn.socket.EndReceive(result);
if (bytesRead > 0)
{
HandlePacket(ParseMessage(Encoding.ASCII.GetString(conn.buffer, 0, bytesRead), conn), conn);
//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length,
SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
}
else //Client disconnected
{
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 和 OnClientDisconnect
这些方法将调用 OnClientConnect
/OnClientDisconnect
,并将连接的客户端 ID 传递给 DLL。
/// <summary>
/// Invoke OnClientConnect on User Created Dll</summary>
/// <param name="conn">Client connection</param>
/// </summary>
private void OnClientConnect(Connection conn)
{
Core.dllInstance.GetType().GetMethod("OnClientConnect").Invoke(
Core.dllInstance, new object[] { conn.connID });
}
/// <summary>
/// Invoke OnClientDisconnect on User Created Dll</summary>
/// <param name="conn">Client connection</param>
/// </summary>
private void OnClientDisconnect(Connection conn)
{
Core.dllInstance.GetType().GetMethod("OnClientDisconnect").Invoke(
Core.dllInstance, new object[] { conn.connID });
}
解析进来的客户端消息
将进来的客户端消息解析为 Packet
类。
首先,我们获取 Packet
的 Header/名称
然后,我们需要将数据包体值的类型从字符串解析为正确的类型(int、float 等)。 这是必需的,否则我们会收到“参数类型不匹配”的异常。
/// <summary>
/// Parse message string to Packet class</summary>
/// <param name="message">Packet string</param>
/// <param name="conn">Client connection</param>
/// </summary>
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); //Only Packet Body
//Parse type from incoming packet body values
foreach (string Parameter in Message.Split(Delimiter))
{
//TO-DO more type parsing
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);
}
}
//Always add connID to Packet to get client id on User Created DLL
Packet.AddInt32(conn.connID);
return Packet;
}
处理数据包
处理已解析的 Packet
服务器调用 DLL 中相应的 Packet
方法,传入之前已解析的参数。
invoke 方法返回一个对象,其中包含需要解析为 MethodResponse
类型的方法的返回值。
最后,它循环 MethodResponse
中包含的 Packet
,并将消息发送回客户端。
/// <summary>
/// Invoke the packet-associated method and send response packets contained in MethodResponse</summary>
/// <param name="Packet">The incoming packet</param>
/// <param name="conn">Client connection</param>
/// </summary>
private void HandlePacket(Packet Packet, Connection conn)
{
Core.GetLogging().WriteLine("Received Packet: " + Packet.GetPacketString(), LogLevel.Debug);
//Get associated Packet method using packet header/name
Method Method = Core.GetMethodByName(Packet.Header.ToLower());
if (Method != null)
{
//Packet body values count must match with method parameters count
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
{
//Try invoke associated method given packet body values as parameters
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);
//Invoke succeed! now read Packets contained
//in MethodResponse and send them to the specified clients
foreach (Packet PacketToSend in result.Packets)
{
string PacketString = PacketToSend.GetPacketString();
if (PacketToSend.sendToAll) //Send to all clients
{
sendToAll(StrToByteArray(PacketString));
Core.GetLogging().WriteLine("Sent response: " +
PacketString + " to all clients", LogLevel.Debug);
}
else if (PacketToSend.sendTo != null) //Only send to clients specified in a list
{
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 to sender
{
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);
}
DLL
DLL 是您可以根据需要自定义服务器的地方。您可以在此处编写自定义方法。
您可以对不想让服务器加载的方法使用 private 关键字。
PacketHandler
必须同时包含 OnClientConnect
和 OnClientDisconnect
方法。
每个公共方法都必须返回 MethodResponse
类型。
//PacketHandler class must be public to let server reads methods
public class PacketHandler
{
public static List<User> Users;
private Logging Logging;
/// <summary>
/// Initialize variables
/// </summary>
public PacketHandler()
{
Users = new List<User>();
Logging = new Logging();
Logging.MinimumLogLevel = 0;
}
/// <summary>
/// Return Chat User by Connection ID
/// <param name="connID">Connection ID</param>
/// </summary>
//Prevent server to load these methods using private flag
private User GetUserByConnID(int connID)
{
foreach (User u in Users)
{
if (u.connID == connID)
return u;
}
return null;
}
/// <summary>
/// Return Chat User by Name
/// <param name="Name">User Name</param>
/// </summary>
private User GetUserByName(string Name)
{
foreach (User u in Users)
{
if (u.Name == Name)
return u;
}
return null;
}
/// <summary>
/// Handle Chat User Login
/// <param name="username">Username given by Chat User</param>
/// <param name="password">Password given by Chat User</param>
/// <param name="connID">Connection ID provided by server</param>
/// </summary>
//Public method must return a result of type MethodResponse
public MethodResponse Login(string username, string password, int connID)
{
//Create a new MethodResponse
MethodResponse MethodResponse = new MethodResponse();
bool loginFailed = true;
if (password == "password")
loginFailed = false;
if (loginFailed)
{
//Create a new Packet LOGIN_RESPONSE and send Packet to the sender
Packet LoginResponse = new Packet("LOGIN_RESPONSE");
//Add a boolean value to Packet. It means login failed
LoginResponse.AddBoolean(false);
//Add Packet to MethodResponse
MethodResponse.AddPacket(LoginResponse);
}
else
{
Packet LoginResponse = new Packet("LOGIN_RESPONSE");
LoginResponse.AddBoolean(true);//It means successful login
//Add a int value to Packet. It provides client the connection ID for future use
LoginResponse.AddInt32(connID);
//Announce to all clients a new user joined
//Set sendToAll parameter to true (default false)
//if you want to send Packet to all clients
Packet UserJoin = new Packet("USER_JOIN", true);
//Add the name of the Chat User joined
UserJoin.AddString(username);
//Add Packets to MethodResponse
MethodResponse.AddPacket(LoginResponse);
MethodResponse.AddPacket(UserJoin);
Users.Add(new User(connID, username)); //Add the Chat User to a List
//Write on server console from dll
Logging.WriteLine("User: " + username + " has joined the chat", LogLevel.Information);
}
return MethodResponse; //Return MethodResponse to Server
}
...Other chat methods...
/// <summary>
/// Must always be declared. it will be called when a client disconnect
/// <param name="connID">Connection ID provided by server</param>
/// </summary>
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));
}
}
/// <summary>
/// Must always be declared. it will be called when a client connect
/// <param name="connID">Connection ID provided by server</param>
/// </summary>
public void OnClientConnect(int connID)
{
}
}
客户端
这是简单的聊天程序的客户端。
您可以连接到服务器并登录。然后,您将能够向所有连接的用户发送消息或进行耳语。
static void Main(string[] args)
{
conn = new ServerConnection();
//Connect to the server on 127.0.0.1:8888
conn.Connect(IPAddress.Parse("127.0.0.1"), 8888);
//Handle received Packet
conn.onPacketReceive += new ServerConnection.onPacketReceiveHandler(HandlePacket);
...
}
/// <summary>
/// Ask Login
/// </summary>
static void Login()
{
Console.WriteLine("Write username");
username = Console.ReadLine(); //Read user input
Console.WriteLine("Write password");
string password = Console.ReadLine(); //Read user input
Packet Login = new Packet("LOGIN"); //Create a new Packet LOGIN
Login.AddString(username); //Add the username to Packet
Login.AddString(password); //Add the password to Packet
conn.Send(Login); //Send the Packet to the server
}
/// <summary>
/// Handle the received Packet
/// <param name="sender">Class on which the event has been fired</param>
/// <param name="Packet">The received packet</param>
/// </summary>
static void HandlePacket(object sender, Packet Packet)
{
switch (Packet.Header)
{
case "LOGIN_RESPONSE": //Received LOGIN_RESPONSE Packet
{
bool loginResponse = Convert.ToBoolean(Packet.bodyValues[0]);
//Get Login Response from Packet Body
if (!loginResponse)
{
Console.WriteLine("Login failed");
Login(); //Ask login until logged
}
else
{
id = int.Parse(Packet.bodyValues[1].ToString());
//Get Connection ID from Packet Body
Console.WriteLine("Login Successful");
Logged = true; //User has logged in
}
}
break;
...
}
}
屏幕截图
Web服务
我还实现了一个灵活的 Web 服务,用于创建一个类似 Web 服务器面板的功能。
聊天用的简单 Web 服务器面板
webServiceConnector.php
此脚本执行 Web 服务的登录,并调用 GetUserCount
和 GetUserList
方法。
响应以 JSON 格式表示。
<?php
if (isset($_GET['readData']))
{
//Connect to WebService
$client = new SoapClient("https://:8000/FlexibleServer/?wsdl",array(
'login' => "admin", 'password' => "password"));
try
{
//Get user count
$response = $client->__soapCall("GetUserCount",array());
$arr=objectToArray($response);
//Get user list
$response2 = $client->__soapCall("GetUserList",array());
$arr2=objectToArray($response2);
//Merge results
$result = array_merge($arr,$arr2);
//Encode array to json
echo json_encode($result);
}
catch (SoapFault $exception)
{
trigger_error("SOAP Fault: (faultcode: {$exception->faultcode}, faultstring:
{$exception->faultstring})");
var_dump($exception);
}
}
?>
Javascript 函数
此函数执行一个 ajax 请求,调用 webServiceConnector.php 来获取 JSON 响应。
function read()
{
var xmlhttp;
xmlhttp = GetXmlHttpObject();
if(xmlhttp == null)
{
alert("Boo! Your browser doesn't support AJAX!");
return;
}
xmlhttp.onreadystatechange = stateChanged;
//Get page source
xmlhttp.open("GET", "http://127.0.0.1/webServiceConnector.php?readData", true);
xmlhttp.send(null);
function stateChanged()
{
if(xmlhttp.readyState == 4)
{
//Parse json from source
var obj = jQuery.parseJSON(xmlhttp.responseText);
//Refresh gage with user count value
g1.refresh(obj["GetUserCountResult"]);
//Get textarea element
var txtarea = document.getElementById("txtarea");
if (obj["GetUserListResult"]["string"] != null)
{
var length = obj["GetUserListResult"]["string"].length;
var s = "";
//Append Users' Names
for (var i = 0; i < length; i++) {
s += obj["GetUserListResult"]["string"][i];
}
//Display names
txtarea.innerHTML = s;
txtarea.scrollTop = txtarea.scrollHeight;
}
else
{
txtarea.innerHTML = "";
txtarea.scrollTop = txtarea.scrollHeight;
}
//Refresh every second
setTimeout("read()",1000);
}
}
function GetXmlHttpObject()
{
if(window.XMLHttpRequest){
return new XMLHttpRequest();
}
if(window.ActiveXObject){
return new ActiveXObject("Microsoft.XMLHTTP");
}
return null;
}
}
在 DLL 中
将从 php 脚本调用的方法
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();
}
用于启动 Web 服务的服务器代码
/// <summary>
/// Start webService
/// </summary>
public void Start()
{
Uri baseAddress = new Uri("http://" + IP.ToString() + ":" + Port + "/FlexibleServer/");
//Get WebserviceHandler from User Created DLL
Type Webservice = packetHandlerDllAssembly.GetType("WebserviceHandler");
//Get Webservice interface from User Created DLL
Type Interface = packetHandlerDllAssembly.GetType("IWebservice");
//Get webService methods created by user
foreach (MethodInfo m in Interface.GetMethods(BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.Instance))
{
string param = "";
//Get method parameters
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);
}
// Create the ServiceHost. Bind on http://ip:port/FlexibleServer/
ServiceHost selfHost = new ServiceHost(Webservice, baseAddress);
//Binding to configure endpoint
BasicHttpBinding http = new BasicHttpBinding();
//Set a basic username/password authentication
http.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly;
http.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
try
{
//Add the endpoint to the service host
ServiceEndpoint endpoint = selfHost.AddServiceEndpoint(
Interface, http, "RemoteControlService");
//Add the Custom webService Behavior to endpoint
endpoint.Behaviors.Add(new webServiceEvent());
//Set the custom username/password validation
selfHost.Credentials.UserNameAuthentication.UserNamePasswordValidationMode =
UserNamePasswordValidationMode.Custom;
selfHost.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator =
new LoginValidator();
// Enable metadata publishing.
ServiceMetadataBehavior smb =
selfHost.Description.Behaviors.Find<ServiceMetadataBehavior>();
if (smb == null)
{
smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
selfHost.Description.Behaviors.Add(smb);
}
try
{
//Start webService
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();
}
}
屏幕截图
如何操作 - MysqlConnector
在 TestDLL 中添加对 MysqlConnector.dll 和 MySql.Data.dll 的引用。
添加
using System.Data;
using MysqlConnector;
示例:初始化 mysql 连接
public class PacketHandler
{
public static List<User> Users;
private Logging Logging;
public Mysql MysqlConn;
/// <summary>
/// Initialize variables
/// </summary>
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();
}
...
}
使用 MySql 的聊天登录方法
/// <summary>
/// Handle Chat User Login
/// <param name="username">Username given by Chat User</param>
/// <param name="password">Password given by Chat User</param>
/// <param name="connID">Connection ID provided by server</param>
/// </summary>
//Public method must return a result of type MethodResponse
public MethodResponse Login(object username, object password, int connID)
{
//Create a new MethodResponse
MethodResponse MethodResponse = new MethodResponse();
//Check if user exists from mysql
DataRow Row = MysqlConn.ReadDataRow("SELECT * FROM users where username = '" + username + "' AND password = '" + password + "'");
bool loginFailed = true;
if (Row != null)
{
loginFailed = false;
}
if (loginFailed)
{
//Create a new Packet LOGIN_RESPONSE and send Packet to the sender
Packet LoginResponse = new Packet("LOGIN_RESPONSE");
//Add a boolean value to Packet. It means login failed
LoginResponse.AddBoolean(false);
//Add Packet to MethodResponse
MethodResponse.AddPacket(LoginResponse);
}
else
{
Packet LoginResponse = new Packet("LOGIN_RESPONSE");
LoginResponse.AddBoolean(true);//It means successful login
//Add a int value to Packet. It provides client the connection ID for future use
LoginResponse.AddInt32(connID);
//Announce to all clients a new user joined
//Set sendToAll parameter to true (default false) if you want to send Packet
//to all clients
Packet UserJoin = new Packet("USER_JOIN", true);
//Add the name of the Chat User joined
UserJoin.AddString(username.ToString());
//Add Packets to MethodResponse
MethodResponse.AddPacket(LoginResponse);
MethodResponse.AddPacket(UserJoin);
Users.Add(new User(connID, username.ToString())); //Add the Chat User to a List
//Write on server console from dll
Logging.WriteLine("User: " + username + " has joined the chat", LogLevel.Information);
}
return MethodResponse; //Return MethodResponse to Server
}
- 编译 TestDLL
- 将 TestDLL.dll、MysqlConnector.dll、MySql.Data.dll 移动到服务器目录。
如何测试项目
- 以管理员身份打开位于 /Flexible Server/bin/Debug 的 FlexibleServer.exe。
- 打开位于 /Chat Client/bin/Debug 的一个或多个 ChatClient.exe。
- 输入用户名。
- 密码是“password”。
- 要发送耳语,请键入“whisper target message”(将 target 替换为您想发送消息的用户姓名)。
创建您自己的 DLL
- 在 Visual Studio 中创建一个新的 DLL 项目。
- 添加对 System.ServiceModel 的引用。
- 添加对 Logging.dll 的引用,以使用 LogLevel 写入控制台。
- (可选)添加对 MysqlConnector.dll 和 MySql.Data.dll 的引用,如果您想添加 Mysql 支持。
- 编写 DLL 基础代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
//using MysqlConnector;
//PacketHandler class must be public to let server reads methods
public class PacketHandler
{
/// <summary>
/// Initialize variables/mysql connection etc..
/// </summary>
public PacketHandler()
{
}
public MethodResponse Yourincomingpacketname(string incomingpacketparameter, etc)
{
MethodResponse MethodResponse = new MethodResponse();
... Your code ....
return MethodResponse();
}
/// <summary>
/// Must always be declared. it will be called when a client disconnect
/// <param name="connID">Connection ID provided by server</param>
/// </summary>
public void OnClientDisconnect(int connID)
{
}
/// <summary>
/// Must always be declared. it will be called when a client connect
/// <param name="connID">Connection ID provided by server</param>
/// </summary>
public void OnClientConnect(int connID)
{
}
}
- 编译 DLL
- 将 DLL 及其引用移动到服务器目录。
- 在 flexibleserver-config.conf 中更改 packetHandlerDLL 的值为您的 DLL 名称。
关注点
配置文件
包含所有服务器设置。
## 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
日志记录
用于通过日志级别提高控制台可读性的类
调试 | 0 |
信息 | 1 |
警告 | 2 |
错误 | 3 |
成功 | 4 |
WriteLine 代码
/// <summary>
/// Write Line to Console with specified Log Level
/// <param name="Line">Line text</param>
/// <param name="Level">LogLevel</param>
/// </summary>
public void WriteLine(string Line, LogLevel Level)
{
//Don't write line to Console if LogLevel is lower than MinimumLogLevel
if (Level >= MinimumLogLevel)
{
DateTime _DTN = DateTime.Now;
StackFrame _SF = new StackTrace().GetFrame(1);
Console.Write("[");
Console.ForegroundColor = ConsoleColor.Green;
//Write current Class.Method
Console.Write(_SF.GetMethod().ReflectedType.Name + "." + _SF.GetMethod().Name);
Console.ForegroundColor = ConsoleColor.Gray;
Console.Write("] » ");
//Change color based on log level
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
此类允许服务器和 DLL 相互通信。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class Packet
{
public string Header; //Name of the Packet
public List<object> bodyValues; //List of Values
public bool sendToAll; //Send to all Clients?
//List of Connection ID to which the server will send the Packet
public List<int> sendTo;
//Delimit Packet header and Packet body values using char(1)
private char Delimiter = (char)1;
/// <summary>
/// New Packet</summary>
/// <param name="Header">The Packet header/name</param>
/// <param name="sendToAll">Send to all Clients?</param>
/// <param name="sendTo">List of Connection ID to which the server will send the Packet</param>
/// </summary>
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;
}
/// <summary>
/// Add integer value to Packet body</summary>
/// <param name="Value">Integer value</param>
/// </summary>
public void AddInt32(int Value) //Add a integer to Packet body
{
bodyValues.Add(Value);
}
/// <summary>
/// Add string value to Packet body</summary>
/// <param name="Value">String value</param>
/// </summary>
public void AddString(string Value) //Add a string to Packet body
{
bodyValues.Add(Value);
}
/// <summary>
/// Add boolean value to Packet body</summary>
/// <param name="Value">Boolean value</param>
/// </summary>
public void AddBoolean(bool Value) //Add a boolean to Packet body
{
bodyValues.Add(Value);
}
/// <summary>
/// Return the final string value to be sent to client
/// </summary>
public string GetPacketString()
{
string PacketString = Header;
foreach (object o in bodyValues)
{
//Add delimiter to each Packet body value
PacketString += Delimiter.ToString() + o.ToString();
}
return PacketString;
}
}
public class MethodResponse
{
public List<Packet> Packets; //List of Packets
public MethodResponse()
{
Packets = new List<Packet>();
}
/// <summary>
/// Add new Packet to MethodResponse</summary>
/// <param name="Packet">Packet</param>
public void AddPacket(Packet Packet)
{
Packets.Add(Packet);
}
}
关于性能
普通服务器
解析数据包并发送响应大约需要 12 毫秒。
灵活服务器
解析数据包、调用 DLL 中的方法并将 MethodResponse
中包含的数据包发送回去大约需要 27 毫秒。
结论:灵活服务器需要执行更多操作。当然,它会更慢。不推荐用于大型项目。
结论
感谢阅读本文,希望您喜欢。
如果您发现错误或有改进建议,请告诉我,我很乐意修复/实现它们。
历史
- 版本 1.0 - 2013/07/19:初始发布
- 2013/07/20: (可选)包含 MysqlConnector 示例、如何创建您自己的 DLL、关注点、为 php/javascript 脚本添加了代码注释
- 2013/07/23: 添加了目录、优缺点、关于性能的讨论、结论