mirror of
https://gitee.com/fightroad/DicomSCP.git
synced 2025-12-06 10:38:57 +08:00
配置项完善
This commit is contained in:
@@ -6,7 +6,7 @@ namespace DicomSCP.Configuration;
|
||||
public class DicomSettings
|
||||
{
|
||||
[Required]
|
||||
[RegularExpression(@"^[A-Za-z0-9\-_]{1,16}$", ErrorMessage = "AE Title must be 1-16 characters and contain only letters, numbers, hyphen and underscore")]
|
||||
[RegularExpression(@"^[A-Za-z0-9\-_]{1,16}$")]
|
||||
public string AeTitle { get; set; } = "STORESCP";
|
||||
|
||||
[Range(1, 65535)]
|
||||
@@ -15,13 +15,11 @@ public class DicomSettings
|
||||
[Required]
|
||||
public string StoragePath { get; set; } = "./received_files";
|
||||
|
||||
public int MaxPDULength { get; set; } = 16384;
|
||||
|
||||
public bool ValidateCallingAE { get; set; } = false;
|
||||
|
||||
public string[] AllowedCallingAEs { get; set; } = Array.Empty<string>();
|
||||
|
||||
public LogSettings Logging { get; set; } = new();
|
||||
|
||||
public AdvancedSettings Advanced { get; set; } = new();
|
||||
|
||||
public SwaggerSettings Swagger { get; set; } = new();
|
||||
}
|
||||
|
||||
public class LogSettings
|
||||
@@ -31,4 +29,20 @@ public class LogSettings
|
||||
public LogEventLevel FileLogLevel { get; set; } = LogEventLevel.Debug;
|
||||
public int RetainedDays { get; set; } = 31;
|
||||
public string LogPath { get; set; } = "logs";
|
||||
}
|
||||
|
||||
public class AdvancedSettings
|
||||
{
|
||||
public bool ValidateCallingAE { get; set; } = false;
|
||||
public string[] AllowedCallingAEs { get; set; } = Array.Empty<string>();
|
||||
public int ConcurrentStoreLimit { get; set; } = 8;
|
||||
public int TempFileCleanupDelay { get; set; } = 300;
|
||||
}
|
||||
|
||||
public class SwaggerSettings
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string Title { get; set; } = "DICOM SCP API";
|
||||
public string Version { get; set; } = "v1";
|
||||
public string Description { get; set; } = "DICOM SCP服务器的REST API";
|
||||
}
|
||||
42
Program.cs
42
Program.cs
@@ -4,9 +4,14 @@ using Serilog.Events;
|
||||
using Serilog.Filters;
|
||||
using DicomSCP.Configuration;
|
||||
using DicomSCP.Services;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// 获取配置
|
||||
var settings = builder.Configuration.GetSection("DicomSettings").Get<DicomSettings>()
|
||||
?? new DicomSettings();
|
||||
|
||||
// 配置日志
|
||||
var logSettings = builder.Configuration
|
||||
.GetSection("DicomSettings:Logging")
|
||||
@@ -47,7 +52,6 @@ if (logSettings.EnableFileLog)
|
||||
}
|
||||
|
||||
Log.Logger = logConfig.CreateLogger();
|
||||
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
// 添加日志服务
|
||||
@@ -57,18 +61,31 @@ builder.Services.AddLogging(loggingBuilder =>
|
||||
// 添加服务
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// 配置 Swagger
|
||||
if (settings.Swagger.Enabled)
|
||||
{
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc(settings.Swagger.Version, new OpenApiInfo
|
||||
{
|
||||
Title = settings.Swagger.Title,
|
||||
Version = settings.Swagger.Version,
|
||||
Description = settings.Swagger.Description
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
builder.Services.Configure<DicomSettings>(builder.Configuration.GetSection("DicomSettings"));
|
||||
builder.Services.AddSingleton<DicomServer>();
|
||||
|
||||
// 配置 DICOM
|
||||
var settings = builder.Configuration.GetSection("DicomSettings").Get<DicomSettings>()
|
||||
?? new DicomSettings();
|
||||
CStoreSCP.Configure(settings.StoragePath);
|
||||
|
||||
// 优化线程池
|
||||
ThreadPool.SetMinThreads(Environment.ProcessorCount * 2, Environment.ProcessorCount);
|
||||
ThreadPool.SetMaxThreads(Environment.ProcessorCount * 4, Environment.ProcessorCount * 2);
|
||||
// 优化线程池 - 基于CPU核心数
|
||||
int processorCount = Environment.ProcessorCount;
|
||||
ThreadPool.SetMinThreads(processorCount * 4, processorCount * 2); // 增加最小线程数
|
||||
ThreadPool.SetMaxThreads(processorCount * 8, processorCount * 4); // 增加最大线程数
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -78,12 +95,19 @@ await dicomServer.StartAsync();
|
||||
app.Lifetime.ApplicationStopping.Register(() => dicomServer.StopAsync().GetAwaiter().GetResult());
|
||||
|
||||
// 配置中间件
|
||||
if (app.Environment.IsDevelopment())
|
||||
if (app.Environment.IsDevelopment() && settings.Swagger.Enabled)
|
||||
{
|
||||
// Swagger 中间件应该在其他中间件之前
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
c.SwaggerEndpoint("/swagger/v1/swagger.json", $"{settings.Swagger.Title} {settings.Swagger.Version}");
|
||||
// 可以设置为根路径
|
||||
c.RoutePrefix = "swagger";
|
||||
});
|
||||
}
|
||||
|
||||
// 其他中间件
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
|
||||
13
Properties/launchSettings.json
Normal file
13
Properties/launchSettings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"profiles": {
|
||||
"DicomSCP": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
README.md
17
README.md
@@ -30,8 +30,25 @@
|
||||
### Web服务器配置
|
||||
- `Kestrel.Endpoints.Http.Url`: Web API监听地址
|
||||
|
||||
### DICOM高级配置
|
||||
- `Advanced.ValidateCallingAE`: 是否验证调用方AE
|
||||
- `Advanced.AllowedCallingAEs`: 允许的调用方AE列表
|
||||
- `Advanced.ConcurrentStoreLimit`: 并发存储限制(0表示自动使用CPU核心数 * 2)
|
||||
- `Advanced.TempFileCleanupDelay`: 临时文件清理延迟(秒)
|
||||
|
||||
### Swagger配置
|
||||
- `Swagger.Enabled`: 是否启用Swagger
|
||||
- `Swagger.Title`: API文档标题
|
||||
- `Swagger.Version`: API版本
|
||||
- `Swagger.Description`: API描述
|
||||
|
||||
## API接口
|
||||
|
||||
+ API文档可以通过Swagger UI访问:
|
||||
+ ```
|
||||
+ http://localhost:5000/swagger
|
||||
+ ```
|
||||
+
|
||||
### 获取服务器状态
|
||||
```
|
||||
GET /api/dicom/status
|
||||
|
||||
@@ -4,6 +4,8 @@ using FellowOakDicom;
|
||||
using FellowOakDicom.Network;
|
||||
using Serilog;
|
||||
using ILogger = Serilog.ILogger;
|
||||
using Microsoft.Extensions.Options;
|
||||
using DicomSCP.Configuration;
|
||||
|
||||
namespace DicomSCP.Services;
|
||||
|
||||
@@ -32,7 +34,7 @@ public class CStoreSCP : DicomService, IDicomServiceProvider, IDicomCStoreProvid
|
||||
|
||||
private static string StoragePath = "./received_files";
|
||||
|
||||
// 修改为实例级别的信号量和文件锁,以便能够正确释放
|
||||
private readonly DicomSettings _settings;
|
||||
private readonly SemaphoreSlim _concurrentLimit;
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _fileLocks;
|
||||
private readonly ILogger _logger = Log.ForContext<CStoreSCP>();
|
||||
@@ -47,10 +49,16 @@ public class CStoreSCP : DicomService, IDicomServiceProvider, IDicomCStoreProvid
|
||||
INetworkStream stream,
|
||||
Encoding fallbackEncoding,
|
||||
Microsoft.Extensions.Logging.ILogger log,
|
||||
DicomServiceDependencies dependencies)
|
||||
DicomServiceDependencies dependencies,
|
||||
IOptions<DicomSettings> settings)
|
||||
: base(stream, fallbackEncoding, log, dependencies)
|
||||
{
|
||||
_concurrentLimit = new SemaphoreSlim(Environment.ProcessorCount * 2);
|
||||
_settings = settings.Value;
|
||||
var advancedSettings = _settings.Advanced;
|
||||
int concurrentLimit = advancedSettings.ConcurrentStoreLimit > 0
|
||||
? advancedSettings.ConcurrentStoreLimit
|
||||
: Environment.ProcessorCount * 2;
|
||||
_concurrentLimit = new SemaphoreSlim(concurrentLimit);
|
||||
_fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
}
|
||||
|
||||
@@ -61,6 +69,19 @@ public class CStoreSCP : DicomService, IDicomServiceProvider, IDicomCStoreProvid
|
||||
association.CalledAE,
|
||||
association.CallingAE);
|
||||
|
||||
var advancedSettings = _settings.Advanced;
|
||||
|
||||
// 应用验证配置
|
||||
if (advancedSettings.ValidateCallingAE &&
|
||||
!advancedSettings.AllowedCallingAEs.Contains(association.CallingAE))
|
||||
{
|
||||
_logger.Warning("拒绝未授权的调用方AE: {CallingAE}", association.CallingAE);
|
||||
return SendAssociationRejectAsync(
|
||||
DicomRejectResult.Permanent,
|
||||
DicomRejectSource.ServiceUser,
|
||||
DicomRejectReason.CallingAENotRecognized);
|
||||
}
|
||||
|
||||
foreach (var pc in association.PresentationContexts)
|
||||
{
|
||||
if (pc.AbstractSyntax == DicomUID.Verification)
|
||||
@@ -209,6 +230,8 @@ public class CStoreSCP : DicomService, IDicomServiceProvider, IDicomCStoreProvid
|
||||
// 移动到最终位置
|
||||
File.Move(tempFilePath, filePath);
|
||||
_logger.Information("文件保存成功 - 路径: {FilePath}", filePath);
|
||||
// 启动异步清理
|
||||
_ = Task.Run(() => CleanupTempFileAsync(tempFilePath)); // 使用 Task.Run 避免阻塞
|
||||
return new DicomCStoreResponse(request, DicomStatus.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -332,4 +355,25 @@ public class CStoreSCP : DicomService, IDicomServiceProvider, IDicomCStoreProvid
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
private async Task CleanupTempFileAsync(string tempFilePath)
|
||||
{
|
||||
var advancedSettings = _settings.Advanced;
|
||||
if (advancedSettings.TempFileCleanupDelay > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(advancedSettings.TempFileCleanupDelay));
|
||||
if (File.Exists(tempFilePath))
|
||||
{
|
||||
File.Delete(tempFilePath);
|
||||
_logger.Debug("清理临时文件: {TempFile}", tempFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "清理临时文件失败: {TempFile}", tempFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,32 @@
|
||||
{
|
||||
"DicomSettings": {
|
||||
// DICOM服务器的AE标题,长度1-16字符,只能包含字母、数字、连字符和下划线
|
||||
"AeTitle": "STORESCP",
|
||||
|
||||
// DICOM服务器监听端口,范围1-65535
|
||||
"Port": 11112,
|
||||
|
||||
// DICOM文件存储路径,支持相对或绝对路径
|
||||
"StoragePath": "./received_files",
|
||||
|
||||
// 日志配置
|
||||
"Logging": {
|
||||
// 是否启用控制台日志(仅显示服务状态变化和错误)
|
||||
"EnableConsoleLog": true,
|
||||
|
||||
// 是否启用文件日志
|
||||
"EnableFileLog": true,
|
||||
|
||||
// 文件日志级别:Verbose|Debug|Information|Warning|Error|Fatal
|
||||
"FileLogLevel": "Debug",
|
||||
|
||||
// 日志文件保留天数
|
||||
"RetainedDays": 31,
|
||||
|
||||
// 日志文件存储路径
|
||||
"LogPath": "logs"
|
||||
},
|
||||
"Advanced": {
|
||||
"ValidateCallingAE": false,
|
||||
"AllowedCallingAEs": [],
|
||||
"ConcurrentStoreLimit": 0,
|
||||
"TempFileCleanupDelay": 300
|
||||
},
|
||||
"Swagger": {
|
||||
"Enabled": true,
|
||||
"Title": "DICOM SCP API",
|
||||
"Version": "v1",
|
||||
"Description": "DICOM SCP服务器的REST API"
|
||||
}
|
||||
},
|
||||
|
||||
// 允许的主机配置,"*" 表示允许所有
|
||||
"AllowedHosts": "*",
|
||||
|
||||
// Kestrel Web服务器配置
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
// Web API监听地址
|
||||
"Url": "http://localhost:5000"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user