first commit

This commit is contained in:
2025-07-11 22:03:31 +08:00
commit 919ad0f879
14 changed files with 633 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

View File

@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Mvc;
using HikarinHeartRateMonitorService.Models;
namespace HikarinHeartRateMonitorService.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class HeartRateController : ControllerBase
{
private readonly ILogger<HeartRateController> _logger;
public HeartRateController(ILogger<HeartRateController> logger)
{
_logger = logger;
}
[HttpGet]
public IActionResult Get()
{
return Ok(new { Message = "心率监测WebSocket服务正在运行。请使用WebSocket连接到'/ws'路径。" });
}
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2"/>
<PackageReference Include="System.Text.Json" Version="8.0.0"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
@HikarinHeartRateMonitorService_HostAddress = http://localhost:5037
GET {{HikarinHeartRateMonitorService_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HikarinHeartRateMonitorService", "HikarinHeartRateMonitorService.csproj", "{81D483F5-853A-4614-9E01-F8121FDE8DAE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{81D483F5-853A-4614-9E01-F8121FDE8DAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{81D483F5-853A-4614-9E01-F8121FDE8DAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81D483F5-853A-4614-9E01-F8121FDE8DAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81D483F5-853A-4614-9E01-F8121FDE8DAE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,77 @@
using System.Net.WebSockets;
using System.Text;
using HikarinHeartRateMonitorService.Services;
using WebSocketManager = HikarinHeartRateMonitorService.Services.WebSocketManager;
namespace HikarinHeartRateMonitorService.Middleware
{
public class WebSocketMiddleware
{
private readonly RequestDelegate _next;
private readonly WebSocketManager _webSocketManager;
private readonly ILogger<WebSocketMiddleware> _logger;
public WebSocketMiddleware(RequestDelegate next, WebSocketManager webSocketManager,
ILogger<WebSocketMiddleware> logger)
{
_next = next;
_webSocketManager = webSocketManager;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await _next(context);
return;
}
var socket = await context.WebSockets.AcceptWebSocketAsync();
var socketId = Guid.NewGuid().ToString();
_webSocketManager.AddSocket(socketId, socket);
_logger.LogInformation($"WebSocket连接已建立: {socketId}");
await ReceiveMessages(socketId, socket);
}
private async Task ReceiveMessages(string socketId, WebSocket socket)
{
var buffer = new byte[4096];
try
{
while (socket.State == WebSocketState.Open)
{
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
await _webSocketManager.HandleMessageAsync(socketId, message);
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await _webSocketManager.RemoveSocket(socketId);
_logger.LogInformation($"WebSocket连接已关闭: {socketId}");
break;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"WebSocket处理时发生错误: {socketId}");
await _webSocketManager.RemoveSocket(socketId);
}
}
}
public static class WebSocketMiddlewareExtensions
{
public static IApplicationBuilder UseHeartRateWebSockets(this IApplicationBuilder builder)
{
return builder.UseMiddleware<WebSocketMiddleware>();
}
}
}

17
Models/HeartRateData.cs Normal file
View File

@ -0,0 +1,17 @@
namespace HikarinHeartRateMonitorService.Models
{
public class HeartRateData
{
public int HeartRate { get; set; }
public DateTime Timestamp { get; set; }
public string DeviceName { get; set; }
public readonly string Token = "1sZkzBKD3WpRT0eQ9Vk4";
}
public class HeartRateResponse
{
public int HeartRate { get; set; }
public DateTime Timestamp { get; set; }
public string DeviceName { get; set; }
}
}

47
Program.cs Normal file
View File

@ -0,0 +1,47 @@
using HikarinHeartRateMonitorService.Middleware;
using HikarinHeartRateMonitorService.Services;
using WebSocketManager = HikarinHeartRateMonitorService.Services.WebSocketManager;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
// 添加控制器支持
builder.Services.AddControllers();
// 注册WebSocket管理器
builder.Services.AddSingleton<WebSocketManager>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
// 添加WebSocket支持
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromMinutes(2),
AllowedOrigins = { "*" } // 生产环境中请设置具体的允许来源
});
// 使用自定义WebSocket中间件
app.UseHeartRateWebSockets();
// 添加静态文件支持
app.UseStaticFiles();
/*调试页面
app.MapGet("/", async context => {
context.Response.Redirect("/index.html");
});
*/
app.MapControllers();
app.Run();

View File

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:8081",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7060;http://localhost:5037",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

78
README.md Normal file
View File

@ -0,0 +1,78 @@
# 心率监测 WebSocket 服务
这是一个基于ASP.NET Core的WebSocket服务用于接收心率监测设备的数据并转发给其他连接的客户端。
## 功能
- 接收心率数据(包含心率值、时间戳和设备名称)
- 验证客户端Token确保安全性
- 向其他所有已连接的客户端广播心率数据不包含Token
## 技术栈
- ASP.NET Core 9.0
- WebSockets
- System.Text.Json
## 数据格式
### 接收的数据格式
```json
{
"heartRate": 75,
"timestamp": "2023-04-10T15:30:45.123Z",
"deviceName": "HeartMonitor-X1",
"token": "1sZkzBKD3WpRT0eQ9Vk4"
}
```
### 广播的数据格式
```json
{
"heartRate": 75,
"timestamp": "2023-04-10T15:30:45.123Z",
"deviceName": "HeartMonitor-X1"
}
```
## 使用方式
1. 启动服务
2. 通过WebSocket连接到 `ws://localhost:5000/ws``wss://localhost:5001/ws`
3. 发送包含有效Token的心率数据JSON
4. 接收来自其他客户端的心率数据广播
## 客户端示例代码
```javascript
// 创建WebSocket连接
const socket = new WebSocket('ws://localhost:5000/ws');
// 连接建立时
socket.onopen = function(e) {
console.log('连接已建立');
// 发送心率数据
const heartRateData = {
heartRate: 75,
timestamp: new Date(),
deviceName: 'HeartMonitor-X1',
token: '1sZkzBKD3WpRT0eQ9Vk4'
};
socket.send(JSON.stringify(heartRateData));
};
// 接收消息
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('收到心率数据:', data);
};
// 连接关闭
socket.onclose = function(event) {
console.log('连接已关闭', event);
};
```

View File

@ -0,0 +1,84 @@
using System.Collections.Concurrent;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using HikarinHeartRateMonitorService.Models;
namespace HikarinHeartRateMonitorService.Services
{
public class WebSocketManager
{
private readonly ConcurrentDictionary<string, WebSocket> _sockets = new();
private readonly string _validToken = "1sZkzBKD3WpRT0eQ9Vk4";
public void AddSocket(string id, WebSocket socket)
{
_sockets.TryAdd(id, socket);
}
public async Task RemoveSocket(string id)
{
if (_sockets.TryRemove(id, out var socket))
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure,
"Connection closed by the server", CancellationToken.None);
}
}
public async Task HandleMessageAsync(string senderId, string message)
{
try
{
var heartRateData = JsonSerializer.Deserialize<HeartRateData>(message);
// 验证Token
if (heartRateData?.Token != _validToken)
{
await CloseInvalidConnection(senderId, "Invalid token");
return;
}
// 创建不包含Token的响应对象
var response = new HeartRateResponse
{
HeartRate = heartRateData.HeartRate,
Timestamp = heartRateData.Timestamp,
DeviceName = heartRateData.DeviceName
};
var responseJson = JsonSerializer.Serialize(response);
var responseBytes = Encoding.UTF8.GetBytes(responseJson);
// 向除了发送者之外的所有客户端广播消息
var tasks = _sockets
.Where(kvp => kvp.Key != senderId)
.Select(kvp => SendMessageAsync(kvp.Value, responseBytes));
await Task.WhenAll(tasks);
}
catch (JsonException)
{
await CloseInvalidConnection(senderId, "Invalid message format");
}
}
private async Task CloseInvalidConnection(string id, string reason)
{
if (_sockets.TryGetValue(id, out var socket))
{
await socket.CloseAsync(WebSocketCloseStatus.InvalidMessageType,
reason, CancellationToken.None);
await RemoveSocket(id);
}
}
private static async Task SendMessageAsync(WebSocket socket, byte[] message)
{
if (socket.State == WebSocketState.Open)
{
await socket.SendAsync(new ArraySegment<byte>(message),
WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
appsettings.json Normal file
View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

226
wwwroot/index.html Normal file
View File

@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>心率监测WebSocket测试</title>
<style>
body {
font-family: 'Arial', sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
.card {
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
margin-bottom: 15px;
}
.card-header {
font-weight: bold;
margin-bottom: 10px;
color: #3498db;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
width: 60px;
}
#logs {
margin-top: 20px;
height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
background-color: #f9f9f9;
font-family: monospace;
}
.log-entry {
margin-bottom: 5px;
border-left: 3px solid #3498db;
padding-left: 10px;
}
.received {
border-left-color: #2ecc71;
}
.error {
border-left-color: #e74c3c;
}
</style>
</head>
<body>
<div class="container">
<h1>心率监测WebSocket测试</h1>
<div class="card">
<div class="card-header">连接控制</div>
<button id="connectBtn">连接WebSocket</button>
<button id="disconnectBtn" disabled>断开连接</button>
<span id="connectionStatus">未连接</span>
</div>
<div class="card">
<div class="card-header">发送心率数据</div>
<div>
<label for="heartRate">心率值:</label>
<input type="number" id="heartRate" value="75" min="0" max="250">
</div>
<div style="margin-top: 10px;">
<label for="deviceName">设备名称:</label>
<input type="text" id="deviceName" value="HeartMonitor-X1" style="width: 150px;">
</div>
<div style="margin-top: 10px;">
<label for="token">Token:</label>
<input type="text" id="token" value="1sZkzBKD3WpRT0eQ9Vk4" style="width: 200px;">
</div>
<button id="sendBtn" style="margin-top: 10px;" disabled>发送数据</button>
</div>
<div class="card">
<div class="card-header">日志</div>
<div id="logs"></div>
</div>
</div>
<script>
let socket = null;
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const sendBtn = document.getElementById('sendBtn');
const heartRateInput = document.getElementById('heartRate');
const deviceNameInput = document.getElementById('deviceName');
const tokenInput = document.getElementById('token');
const connectionStatus = document.getElementById('connectionStatus');
const logsContainer = document.getElementById('logs');
function addLogEntry(message, type = 'info') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logsContainer.appendChild(entry);
logsContainer.scrollTop = logsContainer.scrollHeight;
}
connectBtn.addEventListener('click', () => {
// 判断是HTTP还是HTTPS来决定WebSocket协议
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
const wsUrl = `${protocol}//${host}/ws`;
try {
socket = new WebSocket(wsUrl);
socket.onopen = () => {
addLogEntry('WebSocket连接已建立');
connectBtn.disabled = true;
disconnectBtn.disabled = false;
sendBtn.disabled = false;
connectionStatus.textContent = '已连接';
};
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
addLogEntry(`收到心率数据: ${JSON.stringify(data, null, 2)}`, 'received');
} catch (e) {
addLogEntry(`收到非JSON消息: ${event.data}`, 'error');
}
};
socket.onclose = (event) => {
addLogEntry(`WebSocket连接已关闭: 代码 ${event.code}, 原因: ${event.reason || '未指定'}`);
connectBtn.disabled = false;
disconnectBtn.disabled = true;
sendBtn.disabled = true;
connectionStatus.textContent = '未连接';
};
socket.onerror = (error) => {
addLogEntry(`WebSocket错误: ${error}`, 'error');
};
} catch (error) {
addLogEntry(`创建WebSocket连接时出错: ${error.message}`, 'error');
}
});
disconnectBtn.addEventListener('click', () => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.close();
addLogEntry('正在关闭WebSocket连接...');
}
});
sendBtn.addEventListener('click', () => {
if (!socket || socket.readyState !== WebSocket.OPEN) {
addLogEntry('WebSocket未连接无法发送数据', 'error');
return;
}
try {
const heartRate = parseInt(heartRateInput.value);
if (isNaN(heartRate) || heartRate < 0 || heartRate > 250) {
addLogEntry('心率值无效请输入0-250之间的数字', 'error');
return;
}
const deviceName = deviceNameInput.value.trim();
if (!deviceName) {
addLogEntry('设备名称不能为空', 'error');
return;
}
const token = tokenInput.value.trim();
if (!token) {
addLogEntry('Token不能为空', 'error');
return;
}
const data = {
heartRate: heartRate,
timestamp: new Date(),
deviceName: deviceName,
token: token
};
socket.send(JSON.stringify(data));
addLogEntry(`已发送心率数据: ${JSON.stringify(data, null, 2)}`);
} catch (error) {
addLogEntry(`发送数据时出错: ${error.message}`, 'error');
}
});
</script>
</body>
</html>