383 lines
11 KiB
Markdown
383 lines
11 KiB
Markdown
# SocketService 使用示例
|
||
|
||
## 概述
|
||
|
||
`SocketService` 是一个基于原生 TcpListener 实现的稳定可靠的 Socket 服务器,支持多客户端同时连接。
|
||
|
||
## 主要特性
|
||
|
||
- ✅ 基于原生 TcpListener,稳定可靠
|
||
- ✅ 支持多客户端同时连接
|
||
- ✅ 使用 CRLF (\r\n) 行分隔符的文本协议
|
||
- ✅ UTF-8 编码
|
||
- ✅ 详细的 NLog 日志记录
|
||
- ✅ 事件驱动架构,方便外部处理业务逻辑
|
||
- ✅ 支持单播和广播发送数据
|
||
|
||
## 配置参数
|
||
|
||
```csharp
|
||
// 创建服务实例
|
||
var socketService = new SocketService();
|
||
|
||
// 配置参数(可选,有默认值)
|
||
socketService.ListenIp = "127.0.0.1"; // 监听 IP,默认 127.0.0.1
|
||
socketService.ListenPort = 6001; // 监听端口,默认 6001
|
||
socketService.ReceiveTimeout = 30; // 接收超时(秒),默认 30
|
||
socketService.SendTimeout = 30; // 发送超时(秒),默认 30
|
||
```
|
||
|
||
## 基本使用
|
||
|
||
### 1. 启动服务器
|
||
|
||
```csharp
|
||
// 启动服务器
|
||
bool success = await socketService.StartAsync();
|
||
if (success)
|
||
{
|
||
Console.WriteLine("服务器启动成功");
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine("服务器启动失败");
|
||
}
|
||
```
|
||
|
||
### 2. 订阅事件
|
||
|
||
#### 数据接收事件
|
||
|
||
```csharp
|
||
socketService.DataReceived += (sender, e) =>
|
||
{
|
||
// 处理接收到的数据
|
||
Console.WriteLine($"收到数据:SessionId={e.SessionId}, IP={e.RemoteIp}:{e.RemotePort}");
|
||
Console.WriteLine($"数据内容:{e.Data}");
|
||
Console.WriteLine($"接收时间:{e.ReceivedTime}");
|
||
|
||
// 在这里处理你的业务逻辑
|
||
// 例如:解析数据、保存到数据库、触发其他操作等
|
||
};
|
||
```
|
||
|
||
#### 客户端连接事件
|
||
|
||
```csharp
|
||
socketService.ClientConnected += (sender, e) =>
|
||
{
|
||
Console.WriteLine($"客户端已连接:SessionId={e.SessionId}, IP={e.RemoteIp}:{e.RemotePort}");
|
||
Console.WriteLine($"连接时间:{e.ConnectedTime}");
|
||
Console.WriteLine($"当前连接数:{socketService.ConnectedClientCount}");
|
||
};
|
||
```
|
||
|
||
#### 客户端断开事件
|
||
|
||
```csharp
|
||
socketService.ClientDisconnected += (sender, e) =>
|
||
{
|
||
Console.WriteLine($"客户端已断开:SessionId={e.SessionId}, IP={e.RemoteIp}:{e.RemotePort}");
|
||
Console.WriteLine($"断开原因:{e.Reason}");
|
||
Console.WriteLine($"断开时间:{e.DisconnectedTime}");
|
||
Console.WriteLine($"当前连接数:{socketService.ConnectedClientCount}");
|
||
};
|
||
```
|
||
|
||
#### 服务器错误事件
|
||
|
||
```csharp
|
||
socketService.ServerError += (sender, ex) =>
|
||
{
|
||
Console.WriteLine($"服务器发生错误:{ex.Message}");
|
||
Console.WriteLine($"错误堆栈:{ex.StackTrace}");
|
||
};
|
||
```
|
||
|
||
### 3. 发送数据
|
||
|
||
#### 向指定客户端发送数据
|
||
|
||
```csharp
|
||
string sessionId = "Session_1_xxx"; // 从事件参数中获取的 SessionId
|
||
string data = "Hello Client!";
|
||
|
||
bool success = await socketService.SendToClientAsync(sessionId, data);
|
||
if (success)
|
||
{
|
||
Console.WriteLine("数据发送成功");
|
||
}
|
||
```
|
||
|
||
#### 向所有客户端广播数据
|
||
|
||
```csharp
|
||
string data = "Broadcast Message to All!";
|
||
|
||
int successCount = await socketService.BroadcastAsync(data);
|
||
Console.WriteLine($"广播成功发送到 {successCount} 个客户端");
|
||
```
|
||
|
||
### 4. 断开指定客户端
|
||
|
||
```csharp
|
||
string sessionId = "Session_1_xxx";
|
||
|
||
bool success = await socketService.DisconnectClientAsync(sessionId);
|
||
if (success)
|
||
{
|
||
Console.WriteLine("客户端已断开");
|
||
}
|
||
```
|
||
|
||
### 5. 获取所有已连接客户端
|
||
|
||
```csharp
|
||
List<string> sessionIds = socketService.GetConnectedSessionIds();
|
||
Console.WriteLine($"当前连接的客户端数量:{sessionIds.Count}");
|
||
foreach (var sessionId in sessionIds)
|
||
{
|
||
Console.WriteLine($" - {sessionId}");
|
||
}
|
||
```
|
||
|
||
### 6. 停止服务器
|
||
|
||
```csharp
|
||
await socketService.StopAsync();
|
||
Console.WriteLine("服务器已停止");
|
||
```
|
||
|
||
## 完整示例
|
||
|
||
```csharp
|
||
using FATrace.OEMApp.Services;
|
||
using System;
|
||
using System.Threading.Tasks;
|
||
|
||
public class SocketServerExample
|
||
{
|
||
private SocketService _socketService;
|
||
|
||
public async Task RunAsync()
|
||
{
|
||
// 1. 创建服务实例
|
||
_socketService = new SocketService
|
||
{
|
||
ListenIp = "0.0.0.0", // 监听所有网卡
|
||
ListenPort = 6001,
|
||
ReceiveTimeout = 30,
|
||
SendTimeout = 30
|
||
};
|
||
|
||
// 2. 订阅事件
|
||
_socketService.DataReceived += OnDataReceived;
|
||
_socketService.ClientConnected += OnClientConnected;
|
||
_socketService.ClientDisconnected += OnClientDisconnected;
|
||
_socketService.ServerError += OnServerError;
|
||
|
||
// 3. 启动服务器
|
||
bool success = await _socketService.StartAsync();
|
||
if (!success)
|
||
{
|
||
Console.WriteLine("服务器启动失败");
|
||
return;
|
||
}
|
||
|
||
Console.WriteLine("服务器已启动,按任意键停止...");
|
||
Console.ReadKey();
|
||
|
||
// 4. 停止服务器
|
||
await _socketService.StopAsync();
|
||
}
|
||
|
||
private void OnDataReceived(object sender, SocketService.DataReceivedEventArgs e)
|
||
{
|
||
Console.WriteLine($"[数据接收] SessionId={e.SessionId}, 数据={e.Data}");
|
||
|
||
// 处理业务逻辑
|
||
// 例如:回复客户端
|
||
_ = _socketService.SendToClientAsync(e.SessionId, $"收到:{e.Data}");
|
||
}
|
||
|
||
private void OnClientConnected(object sender, SocketService.ClientConnectedEventArgs e)
|
||
{
|
||
Console.WriteLine($"[客户端连接] SessionId={e.SessionId}, IP={e.RemoteIp}:{e.RemotePort}");
|
||
|
||
// 欢迎消息
|
||
_ = _socketService.SendToClientAsync(e.SessionId, "欢迎连接到服务器!");
|
||
}
|
||
|
||
private void OnClientDisconnected(object sender, SocketService.ClientDisconnectedEventArgs e)
|
||
{
|
||
Console.WriteLine($"[客户端断开] SessionId={e.SessionId}, 原因={e.Reason}");
|
||
}
|
||
|
||
private void OnServerError(object sender, Exception ex)
|
||
{
|
||
Console.WriteLine($"[服务器错误] {ex.Message}");
|
||
}
|
||
}
|
||
```
|
||
|
||
## 在 WinForms 中使用
|
||
|
||
```csharp
|
||
public partial class MainApp : Form
|
||
{
|
||
private SocketService _socketService;
|
||
|
||
private async void MainApp_Load(object sender, EventArgs e)
|
||
{
|
||
// 初始化 Socket 服务
|
||
_socketService = new SocketService
|
||
{
|
||
ListenIp = "127.0.0.1",
|
||
ListenPort = 6001
|
||
};
|
||
|
||
// 订阅事件(注意:需要使用 Invoke 更新 UI)
|
||
_socketService.DataReceived += (s, args) =>
|
||
{
|
||
this.Invoke(new Action(() =>
|
||
{
|
||
// 更新 UI
|
||
txtLog.AppendText($"收到数据:{args.Data}\r\n");
|
||
}));
|
||
};
|
||
|
||
// 启动服务器
|
||
bool success = await _socketService.StartAsync();
|
||
if (success)
|
||
{
|
||
lblStatus.Text = "服务器运行中";
|
||
lblStatus.ForeColor = Color.Green;
|
||
}
|
||
}
|
||
|
||
private async void MainApp_FormClosing(object sender, FormClosingEventArgs e)
|
||
{
|
||
// 停止服务器
|
||
if (_socketService != null && _socketService.IsRunning)
|
||
{
|
||
await _socketService.StopAsync();
|
||
}
|
||
}
|
||
|
||
private async void btnSendToAll_Click(object sender, EventArgs e)
|
||
{
|
||
// 广播消息
|
||
string message = txtMessage.Text;
|
||
int count = await _socketService.BroadcastAsync(message);
|
||
MessageBox.Show($"已发送到 {count} 个客户端");
|
||
}
|
||
}
|
||
```
|
||
|
||
## 协议说明
|
||
|
||
### 数据格式
|
||
|
||
- **编码**:UTF-8
|
||
- **行分隔符**:CRLF (\r\n)
|
||
- **每次发送/接收**:一行文本数据
|
||
|
||
### 客户端示例(C#)
|
||
|
||
```csharp
|
||
using System;
|
||
using System.IO;
|
||
using System.Net.Sockets;
|
||
using System.Text;
|
||
|
||
public class SimpleClient
|
||
{
|
||
public static async Task Main()
|
||
{
|
||
using var client = new TcpClient();
|
||
await client.ConnectAsync("127.0.0.1", 6001);
|
||
|
||
var stream = client.GetStream();
|
||
var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
|
||
var reader = new StreamReader(stream, Encoding.UTF8);
|
||
|
||
// 发送数据
|
||
await writer.WriteLineAsync("Hello Server!");
|
||
|
||
// 接收数据
|
||
string response = await reader.ReadLineAsync();
|
||
Console.WriteLine($"服务器回复:{response}");
|
||
}
|
||
}
|
||
```
|
||
|
||
### 客户端示例(Python)
|
||
|
||
```python
|
||
import socket
|
||
|
||
# 连接服务器
|
||
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
client.connect(('127.0.0.1', 6001))
|
||
|
||
# 发送数据(注意:需要添加 \r\n)
|
||
message = "Hello Server!\r\n"
|
||
client.send(message.encode('utf-8'))
|
||
|
||
# 接收数据
|
||
response = client.recv(1024).decode('utf-8')
|
||
print(f"服务器回复:{response}")
|
||
|
||
client.close()
|
||
```
|
||
|
||
## 日志说明
|
||
|
||
SocketService 使用 NLog 记录详细日志,包括:
|
||
|
||
- **Info 级别**:服务器启动/停止、客户端连接/断开、广播完成
|
||
- **Debug 级别**:数据接收/发送的详细内容
|
||
- **Warn 级别**:参数错误、会话未找到、连接异常
|
||
- **Error 级别**:异常堆栈信息
|
||
|
||
日志示例:
|
||
```
|
||
2026-01-01 21:00:00.123 [INFO] 正在启动 Socket 服务器,监听地址:127.0.0.1:6001,超时时间:30秒
|
||
2026-01-01 21:00:00.456 [INFO] Socket 服务器启动成功,监听地址:127.0.0.1:6001
|
||
2026-01-01 21:00:05.789 [INFO] 客户端已连接,SessionId=Session_1_xxx,远程地址=127.0.0.1:54321,当前连接数=1
|
||
2026-01-01 21:00:10.123 [DEBUG] 接收到客户端数据,SessionId=Session_1_xxx,远程地址=127.0.0.1:54321,数据长度=13字符,内容=Hello Server!
|
||
```
|
||
|
||
## 注意事项
|
||
|
||
1. **线程安全**:所有公共方法都是线程安全的,可以在多线程环境中使用
|
||
2. **事件处理**:事件回调在后台线程执行,如需更新 UI,请使用 `Invoke`
|
||
3. **超时设置**:如果客户端长时间无数据交互,会自动断开连接
|
||
4. **数据格式**:发送的数据会自动添加 CRLF,接收的数据会自动去除 CRLF
|
||
5. **异常处理**:所有异常都会被捕获并记录日志,不会导致服务器崩溃
|
||
|
||
## 常见问题
|
||
|
||
### Q1: 如何监听所有网卡?
|
||
```csharp
|
||
socketService.ListenIp = "0.0.0.0";
|
||
```
|
||
|
||
### Q2: 如何获取客户端的 SessionId?
|
||
SessionId 会在事件参数中提供(`DataReceivedEventArgs.SessionId`),你可以保存它用于后续发送数据。
|
||
|
||
### Q3: 如何处理粘包问题?
|
||
当前实现使用行分隔符(CRLF),每次 `ReadLineAsync` 读取一行完整数据,自动处理粘包。
|
||
|
||
### Q4: 如何实现心跳检测?
|
||
可以在客户端定期发送心跳数据,服务器通过 `ReceiveTimeout` 自动断开无响应的客户端。
|
||
|
||
### Q5: 如何修改协议格式?
|
||
如需使用其他协议(如固定长度、带长度头等),需要修改 `HandleClientAsync` 方法中的数据读取逻辑。
|
||
|
||
## 性能建议
|
||
|
||
1. **连接数**:理论上支持数千个并发连接,实际取决于服务器性能
|
||
2. **数据量**:适合中小数据量传输,大文件传输建议使用其他方案
|
||
3. **日志级别**:生产环境建议将 Debug 日志关闭,减少性能开销
|