배경
오늘은 동료들과 코드 및 공학적 표준화에 대해 이야기를 나눴는데, 얼떨결에 업무에서 흔히 사용하는 TcpListener도 표준화해서 패키징하면 프로그래밍 속도를 높이고 낚시할 시간을 벌 수 있겠다는 생각이 들었습니다. 이 기사에 내재되어 있습니다.
계획
1. [TcpListenerLibrary.ini] 파일에 매개변수를 구성합니다.
# 本机地址
ip=127.0.0.1
# 监听端口
port=3306
# 启动对具有最大挂起连接数的传入连接请求的侦听。
BackLog=10
# 接收数据缓存区多少K
K=10
2. C# 라이브러리 프로젝트 사용자 지정 [TcpListenerLibrary]
3. 코드를 작성하고, 구성 파일을 읽어 매개변수를 로드하고, 이벤트를 트리거하여 후속 프로그램 구현을 위한 인터페이스를 남겨둡니다.
클래스 파일 [BL_Server.cs]
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace TcpListenerLibrary
{
/// <summary>
/// 自定义Tcp服务类
/// </summary>
public class BL_Server
{
/// <summary>
/// 服务端
/// </summary>
public TcpListener Server { get; set; }
/// <summary>
/// 客户端集合,字典形式储存
/// </summary>
public ConcurrentDictionary<string, TcpClient> Clients { set; get; }
/// <summary>
/// 配置文件的物理路径
/// </summary>
private string IniPath { get; }
/// <summary>
/// ip地址
/// </summary>
private string Ip { get; }
/// <summary>
/// 端口号
/// </summary>
private int Port { get; }
/// <summary>
/// 最大挂起连接数
/// </summary>
private int BackLog { get; } = int.MaxValue;
/// <summary>
/// 缓冲区多少K
/// </summary>
private int K { get; } = 10;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="iniPath">配置文件的物理路径</param>
public BL_Server(string iniPath)
{
IniPath = iniPath;
string[] lines = File.ReadAllLines(IniPath, Encoding.UTF8);
foreach (string line in lines)
{
// #开头的当作注释,不处理
if (line.Trim().StartsWith("#") || line.Trim().Length == 0)
{
continue;
}
string[] parameters = line.Split('=');
switch (parameters[0].ToLower())
{
case "ip":
Ip = parameters[1];
break;
case "port":
Port = int.Parse(parameters[1]);
break;
case "backlog":
BackLog = int.Parse(parameters[1]);
break;
case "k":
K = int.Parse(parameters[1]);
break;
}
}
}
/// <summary>
/// 开启服务
/// </summary>
public void Start()
{
if (ReceiveEvent == null)
{
throw new Exception("接收消息事件[ReceiveEvent]未编写处理方法,开启服务失败");
}
IPAddress iPAddress = IPAddress.Parse(Ip);
Server = new TcpListener(iPAddress, Port);
Clients = new ConcurrentDictionary<string, TcpClient>();
Server.Start(BackLog);
Console.WriteLine($"Tcp服务端已开启,[ip={Ip},port={Port},backlog={BackLog},k={K}]");
Task.Run(() =>
{
while (true)
{
TcpClient client = Server.AcceptTcpClient();
string key = client.Client.RemoteEndPoint.ToString();
Clients.TryAdd(key, client);
Console.WriteLine($"客户端[{key}]已连接,当前连接数[{Clients.Count}]");
Task.Run(() =>
{
while (true)
{
try
{
byte[] buffer = new byte[1024 * K];
int length = client.Client.Receive(buffer);
//客户端主动终止连接
if (length == 0)
{
try
{
client.Close();
}
catch { }
Clients.TryRemove(key, out _);
Console.WriteLine($"{key}客户端主动断开,当前连接数[{Clients.Count}]");
break;
}
try
{
ReceiveEvent.Invoke(key, buffer, length);
}
catch (Exception e)
{
Console.WriteLine($"[{key}]消息处理异常,请检查[ReceiveEvent事件代码]");
Console.WriteLine(e.StackTrace);
}
}
catch (Exception e)
{
try
{
client.Close();
}
catch { }
Clients.TryRemove(key, out _);
Console.WriteLine($"{key}客户端异常断开,当前连接数[{Clients.Count}]");
Console.WriteLine(e.StackTrace);
break;
}
}
});
}
});
}
/// <summary>
/// 向所有客户端发送数据
/// </summary>
/// <param name="buffer">消息内容</param>
///
public void SendToAllClients(byte[] buffer)
{
foreach (TcpClient client in Clients.Values)
{
client.Client.Send(buffer);
}
}
/// <summary>
/// 向特定客户端发送数据
/// </summary>
/// <param name="key">客户端标识</param>
/// <param name="buffer">消息内容</param>
public void SendToClient(string key, byte[] buffer)
{
Clients[key].Client.Send(buffer);
}
/// <summary>
/// 接收消息委托
/// </summary>
/// <param name="key"></param>
/// <param name="buffer"></param>
/// <param name="length"></param>
public delegate void ReceiveDelegate(string key, byte[] buffer, int length);
/// <summary>
/// 接收消息事件,开启服务前必须给此事件绑定处理消息方法
/// </summary>
public event ReceiveDelegate ReceiveEvent;
}
}
4. 솔루션을 생성하면 bin/Debug/net5.0 폴더에 해당 dll 파일이 표시됩니다.
사용
1. 새로운 콘솔 프로젝트[TcpListenerLibrary_Test]를 생성하고, 위에서 생성한 dll 파일을 임포트하고, 구성 파일을 이 테스트 프로젝트의 bin/Debug/net5.0 폴더에 복사합니다.
2. 클래스 파일 [Program.cs]를 다시 작성합니다.
using System;
using System.Text;
using TcpListenerLibrary;
namespace TcpListenerLibrary_Test
{
class Program
{
static void Main(string[] args)
{
BL_Server bL_Server = new("TcpListenerLibrary.ini");
bL_Server.ReceiveEvent += (key, buffer, length) =>
{
string message = Encoding.ASCII.GetString(buffer, 0, length);
Console.WriteLine($"{key}:{message}");
bL_Server.SendToAllClients(Encoding.ASCII.GetBytes($"Server:hi[{DateTime.Now:HHmmss}]"));
};
bL_Server.Start();
Console.ReadKey();
}
}
}
3. 네트워크 디버깅 도우미를 열고 연결을 테스트하고 메시지를 보내고 받습니다.
성공적으로 완성되어 기대에 부응했음을 알 수 있습니다.
요약하다
위의 예에서 일반적으로 사용되는 일부 클래스를 다시 패키징하여 표준 및 재사용성을 달성하고 프로그래밍 효율성을 크게 향상시킬 수 있음을 알 수 있습니다. 더 많이 하고 더 빨리 한다고 해서 반드시 더 많은 돈을 벌 수 있는 것은 아니지만 당황스러울 텐데, 낚시를 위해 더 많은 시간을 절약하는 것이 좋지 않을까요? 칼을 갈아도 실수로 장작이 잘리지 않고, 포장과 재사용이 정말 간편합니다!