MoreRSS

site iconuptoz | 小小修改

软件开发工程师,INFP,比较喜欢研究数码和软件,想要探究在互联网上的事物是如何被创造和发展。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

uptoz | 小小的 RSS 预览

【Docker】搭建一个功能强大的自托管虚拟浏览器 - n.eko

2025-01-18 22:46:06

前言

本教程基于群晖的NAS设备DS423+docker功能进行搭建,DSM版本为 DSM 7.2.2-72806 Update 2。

n.eko 支持多种类型浏览器在其虚拟环境中运行,本次教程使用 Chromium​ 浏览器镜像进行演示,支持访问内网设备和公网地址。

简介

n.eko 是一款基于 Docker 的自托管虚拟浏览器,利用 WebRTC 技术实现实时音视频传输和多人协作功能。它允许用户在虚拟环境中运行功能齐全的浏览器(如 Firefox、Chrome 等),并支持多人同时访问和操作,适用于远程协作、观看派对、互动演示等场景。

​​

核心功能

  1. 虚拟浏览器

    • 支持多种浏览器内核(如 Firefox、Chrome、Opera 等),用户可以在虚拟环境中浏览网页、运行应用程序。

    • 所有操作都在 Docker 容器中完成,确保安全性和隐私性。

  2. 多人协作

    • 支持多用户同时访问,用户可以共享浏览器画面并实时互动,适合团队协作、远程教学或家庭娱乐。

    • 提供聊天功能、文件传输和剪贴板同步,增强协作体验。

  3. 实时音视频传输

    • 基于 WebRTC 技术,实现低延迟的音视频传输,支持屏幕共享和远程控制。

    • 支持 RTMP 推流,可将内容广播到 Twitch 或 YouTube 等平台。

  4. 隐私与安全

    • 所有操作都在隔离的 Docker 容器中进行,避免数据泄露。

    • 支持管理员权限控制,如踢出用户、锁定房间等。

  5. 灵活部署

    • 通过 Docker 部署,支持多种操作系统(Windows、Linux、macOS)。

    • 提供丰富的配置选项,如分辨率、密码设置、文件传输路径等。

适用场景

  • 远程协作:团队成员可以共同浏览网页、调试代码或进行头脑风暴。

  • 观看派对:与朋友或家人一起观看视频、动漫,并实时聊天互动。

  • 教育培训:用于远程教学或演示,支持多人同时操作和互动。

  • 隐私浏览:在隔离环境中访问敏感网站,避免留下痕迹。

部署

  1. 在群晖NAS上面的“File Station”中新建一个docker映射文件,用于映射docker中neko-chromium的数据。

  2. 打开“Container Manager”,在“项目”中,点击“新增”。填写项目名称,路径选择创建好的映射文件夹,文件选择“创建 docker-compose.yml”,然后将以下配置代码复制粘贴进去。

    version: "3.8"
    
    services:
      neko:
        image: "m1k1o/neko:chromium"
        container_name: "neko-chromium"
        restart: "unless-stopped"
        shm_size: "3gb" # 设置共享内存大小为 3GB,此设置为必须。
        ports:
          - "19800:8080"
          - "52000-52100:52000-52100/udp"
        cap_add:
          - SYS_ADMIN # 使用 Chromium 内核时需添加,以获取必要的系统管理权限。
        volumes:
          - ./chromium/data:/home/neko/.config/chromium # 策略文件,重启依然能保留浏览器数据。
        environment:
          NEKO_SCREEN: 1280x720@30 # 自定义浏览器窗口分辨率。
          NEKO_PASSWORD: neko # 普通用户的登录密码。
          NEKO_PASSWORD_ADMIN: admin # 管理员(admin)用户的登录密码。  
          NEKO_EPR: 52000-52100 # 设置 WebRTC 的 UDP 端口范围,用于P2P连接。
          NEKO_ICELITE: true # 启用 Ice Lite 协议以优化连接性能,可选。
          NEKO_CONTROL_PROTECTION: true # 控制保护意味着,只有当至少有一个管理员在房间里时,用户才能获得控制权。
          NEKO_NAT1TO1: 192.168.1.111 # 局域网使用时设置为服务器本地 IP,公网则自动获取公网 IP,可选。

    最后点击“下一步”,等待镜像拉取和容器创建完成。

使用

  1. 输入IP:Port​访问,使用管理员登录,用户名为登录后显示的名称,可自定义,密码根据之前配置进行填写。

  2. 设置中文语言。默认是英文,点击左下角en​,选择cn​切换至中文。

  3. 获取浏览器控制权。点击正下方的键盘图标,获取控制权。

  4. 调整屏幕尺寸。点击右上角显示器图标,进行分辨率切换。

  5. 对普通用户进行操作(需要管理员账号)。选中对应头像,鼠标右键单击,可对其进行选择“给予控制”或“踢出”等操作。

  6. 聊天室。点击右上角侧栏图标,然后点击聊天。

  7. 粘贴板共享。在右下角有个粘贴板图标,如果需要将文字内容复制进浏览器,需要将内容粘贴至粘贴板内。

    仅支持纯文本。

  8. 使用自动加入链接。

    示例:http(s)://[URL:Port](URL:Port)/?pwd=neko&usr=guest&cast=1​

    • 添加?pwd=<password>​将预填充密码。

    • 添加?usr=<display-name>​将预填充用户名。

    • 添加?cast=1​将隐藏所有控件,只显示视频。

    • 添加?embed=1​将隐藏大多数附加组件,仅显示视频

    • 添加?volume=<0-1>​将音量设置为给定值。

    • 添加?lang=<language>​将语言设置为给定值。

    • 添加?show_side=1​将在启动时显示侧边栏。

    • 添加?mute_chat=1​将在启动时静音聊天。

  9. 更多使用教程,请参考官方文档 n.keo Doc(https://neko.m1k1o.net/#/getting-started/

【CSharp】使用SpeechSynthesizer类将文本转换为语音

2025-01-16 17:08:28

简介

SpeechSynthesizer​ 是 .NET Framework 和 .NET Core/5+ 中用于文本到语音(Text-to-Speech, TTS)转换的类。它属于 System.Speech.Synthesis​ 命名空间,主要用于将文本转换为语音并播放或保存为音频文件。

SpeechSynthesizer 的主要功能

  1. 文本到语音转换

    • 将文本转换为语音并播放。

    • 支持多种语言和语音库。

  2. 语音库管理

    • 获取系统上安装的语音库。

    • 选择特定的语音库进行语音合成。

  3. 语音控制

    • 调整语速、音量和音调。

    • 支持暂停、恢复和停止语音播放。

  4. 音频输出

    • 将合成的语音保存为音频文件(如 WAV 文件)。

    • 支持直接播放到音频设备。

SpeechSynthesizer 的常用属性和方法

常用属性

  • Voice​:获取或设置当前使用的语音库。

  • Rate​:获取或设置语速(范围:-10 到 10)。

  • Volume​:获取或设置音量(范围:0 到 100)。

  • State​:获取语音合成器的当前状态(如 Speaking​、Paused​、Ready​)。

常用方法

  • ​Speak(string text)​:同步播放指定的文本。

  • ​SpeakAsync(string text)​:异步播放指定的文本。

  • Pause()​:暂停语音播放。

  • ​Resume()​:恢复语音播放。

  • ​Stop()​:停止语音播放。

  • ​SetOutputToWaveFile(string path)​:将语音输出保存为 WAV 文件。

  • SetOutputToDefaultAudioDevice()​:将语音输出到默认音频设备。

  • GetInstalledVoices()​:获取系统上安装的语音库列表。

SpeechSynthesizer 的基本用法

以下是一个简单的示例,展示如何使用 SpeechSynthesizer​ 进行文本到语音转换:

using System;
using System.Speech.Synthesis;

class Program
{
    static void Main()
    {
        // 创建 SpeechSynthesizer 实例
        using (SpeechSynthesizer synthesizer = new SpeechSynthesizer())
        {
            // 设置语音输出到默认音频设备
            synthesizer.SetOutputToDefaultAudioDevice();

            // 获取系统上安装的语音库
            foreach (var voice in synthesizer.GetInstalledVoices())
            {
                Console.WriteLine("语音库: " + voice.VoiceInfo.Name);
            }

            // 设置语音库(例如 Microsoft David Desktop)
            synthesizer.SelectVoice("Microsoft David Desktop");

            // 设置语速和音量
            synthesizer.Rate = 2;  // 语速(-10 到 10)
            synthesizer.Volume = 80; // 音量(0 到 100)

            // 播放文本
            synthesizer.Speak("Hello, welcome to the world of text-to-speech!");

            // 将语音保存为 WAV 文件
            synthesizer.SetOutputToWaveFile("output.wav");
            synthesizer.Speak("This text will be saved to a file.");
        }
    }
}

以下是对 SpeechSynthesizer​ 的简单封装示例:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Speech.Synthesis;

namespace UtilityClass
{
    public class VoiceUtil
    {
        public static string SelectVoice = "";
        private static readonly SpeechSynthesizer synthesizer;

        static VoiceUtil()
        {
            synthesizer = new SpeechSynthesizer();
            synthesizer.SetOutputToDefaultAudioDevice();
        }

        /// <summary> 取消所有排队、异步语音合成操作 </summary>
        public static void CancelAll()
        {
            if (synthesizer.State == SynthesizerState.Speaking)
                synthesizer.SpeakAsyncCancelAll();
        }

        /// <summary> 文本转音频输出 </summary>
        /// <param name="text"> </param>
        /// <remarks> 异步方式 </remarks>
        public static void ConvertTextToSpeechAsync(string text)
        {
            CancelAll();
            if (!string.IsNullOrEmpty(SelectVoice) && GetInstalledVoices().Contains(SelectVoice))
                synthesizer.SelectVoice(SelectVoice);
            if (!string.IsNullOrEmpty(text))
                synthesizer.SpeakAsync(text);
        }

        /// <summary> 获取系统语音库 </summary>
        /// <returns> </returns>
        public static List<string> GetInstalledVoices()
        {
            ReadOnlyCollection<InstalledVoice> list = synthesizer.GetInstalledVoices();
            List<string> voices = new List<string>();
            foreach (var item in list)
            {
                if (item.Enabled && item.VoiceInfo.Culture.Name == "zh-CN")
                    voices.Add(item.VoiceInfo.Name);
            }
            return voices;
        }
    }
}

SpeechSynthesizer 的高级用法

1. 异步语音播放

使用 SpeakAsync​ 方法可以异步播放语音,避免阻塞主线程。

synthesizer.SpeakAsync("This is an asynchronous speech.");

2. 处理语音事件

SpeechSynthesizer​ 提供了多个事件,可以用于监控语音播放状态。

synthesizer.SpeakStarted += (sender, e) => Console.WriteLine("语音播放开始");
synthesizer.SpeakCompleted += (sender, e) => Console.WriteLine("语音播放完成");
synthesizer.SpeakProgress += (sender, e) => Console.WriteLine($"正在播放: {e.Text}");

3. 选择特定语言的语音库

可以通过 VoiceInfo.Culture​ 属性选择特定语言的语音库。

foreach (var voice in synthesizer.GetInstalledVoices())
{
    if (voice.VoiceInfo.Culture.Name == "en-US")
    {
        synthesizer.SelectVoice(voice.VoiceInfo.Name);
        break;
    }
}

4. 调整语音参数

通过 PromptBuilder​ 类可以更灵活地调整语音参数。

PromptBuilder builder = new PromptBuilder();
builder.StartStyle(new PromptStyle()
{
    Rate = PromptRate.Slow,
    Volume = PromptVolume.ExtraLoud
});
builder.AppendText("This text is spoken slowly and loudly.");
builder.EndStyle();
synthesizer.Speak(builder);

SpeechSynthesizer 的局限性

  1. 平台限制:

    • ​System.Speech.Synthesis​ 仅适用于 Windows 平台。

    • 在 .NET Core 或 .NET 5+ 中,需要使用兼容库或第三方 TTS 库。

  2. 语音库依赖:

    • 需要系统上安装相应的语音库(如 Microsoft David Desktop 或 Microsoft Zira Desktop)。

  3. 功能限制:

    • 不支持高级语音合成功能(如情感语音或自定义发音)。

总结

SpeechSynthesizer​ 是一个简单易用的文本到语音转换工具,适用于 Windows 平台上的 .NET 应用程序。它支持多种语音库、语音控制和音频输出功能,适合用于语音提示、语音助手等场景。

【CSharp】NLog日志记录库的简介及简单使用

2025-01-15 23:07:43

简介

NLog 是一个灵活且高性能的日志记录库,专为 .NET 平台设计。它允许开发者在应用程序中轻松地记录日志,并将日志输出到多种目标(如文件、数据库、控制台、邮件等)。NLog 以其配置简单、扩展性强和高性能著称,是 .NET 开发中最流行的日志记录库之一。

特点

  1. 高性能:NLog 经过优化,能够在高负载环境下高效记录日志。

  2. 灵活的配置:支持通过代码或配置文件(如 XML)进行配置。

  3. 多种日志目标:支持将日志输出到文件、数据库、控制台、邮件、网络等多种目标。

  4. 强大的日志格式控制:支持自定义日志格式,包括时间、日志级别、线程 ID、调用方法等信息。

  5. 日志过滤:可以根据日志级别、日志来源等条件过滤日志。

  6. 异步日志记录:支持异步日志记录,减少对主线程性能的影响。

  7. 跨平台:支持 .NET Framework、.NET Core、.NET 5/6/7 以及 Xamarin 等平台。

使用

  1. 安装 NLog

    通过 NuGet 安装 NLog

    dotnet add package NLog
    

    或者使用 NuGet 包管理器搜索并安装 NLog​。

  2. 配置 NLog

    NLog 支持通过代码或配置文件进行配置。推荐使用配置文件(NLog.config​),因为它更灵活且易于维护。

    在项目中添加 NLog.config​ 文件:

    <?xml version="1.0" encoding="utf-8" ?>
    <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          autoReload="true"
          throwConfigExceptions="true">
    	<targets>
    		<!-- 输出到控制台 -->
    		<target name="console" xsi:type="ColoredConsole" layout=">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ${newline}${longdate} ${level:uppercase=true} ${newline}Message:${message}${newline}Exception: ${exception:format=ToString}" />
    
    		<!-- 输出到文件 -->
    		<target name="errorfile" xsi:type="File" fileName="Logs/${shortdate}.log" archiveEvery="Day" archiveNumbering="Rolling" maxArchiveFiles="7" concurrentWrites="true" keepFileOpen="false"
    				layout=">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ${newline}${longdate} ${level:uppercase=true} [${machinename}] ${newline}Namespace:${event-properties:item=Namespace} ${newline}Class:${event-properties:item=Class} ${newline}Method:${event-properties:item=Method} ${newline}Message:${message} ${newline}Stacktrace:${stacktrace} ${newline}Exception: ${exception:format=ToString}" />
    
    		<target name="infofile" xsi:type="File" fileName="Logs/${shortdate}.log" archiveEvery="Day" archiveNumbering="Rolling" maxArchiveFiles="7" concurrentWrites="true" keepFileOpen="false"
    			    layout=">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ${newline}${longdate} ${level:uppercase=true} [${machinename}] ${newline}Message:${message}" />
    	</targets>
    
    	<rules>
    		<!-- 将所有日志输出到控制台 -->
    		<logger name="*" minlevel="Debug" writeTo="console" />
    
    		<logger name="*" minlevel="Debug" maxlevel="Warn" writeTo="infofile" />
    
    		<!-- 将 Error 及以上级别的日志输出到文件 -->
    		<logger name="*" minlevel="Error" writeTo="errorfile" />
    	</rules>
    </nlog>
    
  3. 在代码中使用 NLog

    在代码中,首先需要创建一个 Logger 实例,然后使用它记录日志

    using NLog;
    using System;
    
    class Program
    {
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
    
        static void Main(string[] args)
        {
            Logger.Info("应用程序启动");
    
            try
            {
                int result = 10 / 0; // 故意引发异常
            }
            catch (Exception ex)
            {
                Logger.Error(ex, "发生了一个错误");
            }
    
            Logger.Info("应用程序结束");
        }
    }

    如果 NLog.config​ 文件不在项目根目录,而是位于其他目录(例如 Config​ 文件夹),你需要通过代码显式指定配置文件的位置。NLog 默认会在应用程序的根目录查找 NLog.config​ 文件,如果文件不在默认位置,可以通过以下方式加载配置文件。

    using NLog;
    using System;
    using System.IO;
    
    class Program
    {
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
    
        static void Main(string[] args)
        {
            // 指定 NLog.config 文件的路径
            string nlogConfigPath = Path.Combine(AppContext.BaseDirectory, "Config", "NLog.config");
    
            // 加载配置文件
            LogManager.Setup().LoadConfigurationFromFile(nlogConfigPath);
    
            Logger.Info("应用程序启动");
    
            try
            {
                int result = 10 / 0; // 故意引发异常
            }
            catch (Exception ex)
            {
                Logger.Error(ex, "发生了一个错误");
            }
    
            Logger.Info("应用程序结束");
        }
    }
  4. 运行结果

    • 控制台输出:

      2025-01-01 12:00:00.0000 INFO 应用程序启动
      2025-01-01 12:00:00.0010 ERROR 发生了一个错误
      2025-01-01 12:00:00.0020 INFO 应用程序结束
    • 文件输出(logs/2025-01-01.log​):

      2025-01-01 12:00:00.0010 ERROR 发生了一个错误
  5. 可根据实际业务需求对该库做进一步的封装:

    using NLog;
    using System;
    using System.IO;
    using System.Reflection;
    
    namespace UtilityClass
    {
        public class LogUtil
        {
            private NLog.Logger _logger;
    
            static LogUtil()
            {
                Default = new LogUtil(NLog.LogManager.GetCurrentClassLogger());
                // 指定 NLog.config 文件的路径
                string nlogConfigPath = Path.Combine(AppContext.BaseDirectory, "Config", "NLog.config");
    
                // 加载配置文件
                LogManager.Setup().LoadConfigurationFromFile(nlogConfigPath);
            }
    
            public LogUtil(string name) : this(LogManager.GetLogger(name))
            {
            }
    
            private LogUtil(NLog.Logger logger)
            {
                _logger = logger;
            }
    
            public static LogUtil Default { get; private set; }
    
            #region Debug
    
            public void Debug(string msg)
            {
                if (_logger.IsDebugEnabled)
                    _logger.Debug(msg);
            }
    
            public void Debug(string msg, Exception err)
            {
                _logger.Debug(err, msg);
            }
    
            #endregion Debug
    
            #region Info
    
            public void Info(string msg)
            {
                if (_logger.IsInfoEnabled)
                    _logger.Info(msg);
            }
    
            #endregion Info
    
            #region Warn
    
            public void Warn(string msg)
            {
                if (_logger.IsWarnEnabled)
                    _logger.Warn(msg);
            }
    
            public void Warn(string msg, Exception err)
            {
                if (_logger.IsWarnEnabled)
                    _logger.Warn(err, msg);
            }
    
            #endregion Warn
    
            #region Trace
    
            public void Trace(string msg)
            {
                if (_logger.IsTraceEnabled)
                    _logger.Trace(msg);
            }
    
            public void Trace(string msg, Exception err)
            {
                if (_logger.IsTraceEnabled)
                    _logger.Trace(err, msg);
            }
    
            #endregion Trace
    
            #region Error
    
            public void Error(string msg)
            {
                if (_logger.IsErrorEnabled)
                    _logger.Error(msg);
            }
    
            public void Error(string msg, Exception err)
            {
                if (_logger.IsErrorEnabled)
                    _logger.Error(err, msg);
            }
    
            #endregion Error
    
            #region Fatal
    
            public void Fatal(string msg)
            {
                _logger.Fatal(msg);
            }
    
            public void Fatal(string msg, Exception err)
            {
                _logger.Fatal(err, msg);
            }
    
            #endregion Fatal
    
            #region Custom
    
            public void Error(MethodBase methodBase, Exception ex, string msg = null)
            {
                var logEvent = new LogEventInfo(LogLevel.Error, _logger.Name, null, msg, null, ex);
                logEvent.Properties["Namespace"] = methodBase.DeclaringType.Namespace;
                logEvent.Properties["Class"] = methodBase.DeclaringType.FullName;
                logEvent.Properties["Method"] = methodBase.Name;
                _logger.Error(logEvent);
            }
    
            #endregion Custom
    
            /// <summary> Flush any pending log messages (in case of asynchronous targets). </summary>
            /// <param name="timeoutMilliseconds">
            /// Maximum time to allow for the flush. Any messages after that time will be discarded.
            /// </param>
            public void Flush(int? timeoutMilliseconds = null)
            {
                if (timeoutMilliseconds != null)
                    NLog.LogManager.Flush(timeoutMilliseconds.Value);
    
                NLog.LogManager.Flush();
            }
        }
    }

NLog 的日志级别

NLog 支持以下日志级别(从低到高):

  1. Trace:用于调试的详细信息。

  2. Debug:调试信息,通常用于开发环境。

  3. Info:常规信息,例如应用程序启动、配置加载等。

  4. Warn:警告信息,表示潜在问题。

  5. Error:错误信息,表示发生了错误但应用程序仍可运行。

  6. Fatal:致命错误,表示应用程序无法继续运行。

NLog 的常用配置选项

  1. 输出目标(Targets)

    • ​Console​:输出到控制台。

    • File​:输出到文件。

    • Database​:输出到数据库。

    • Mail​:通过邮件发送日志。

    • Network​:通过网络发送日志。

  2. 日志格式(Layout)

    • ​${longdate}​:日期和时间。

    • ​${level}​:日志级别。

    • ${message}​:日志消息。

    • ${exception}​:异常信息。

    • ${stacktrace}​:堆栈跟踪信息。

    • ​${logger}​:日志记录器的名称。

    • ​${callsite}​:调用日志的方法名。

    • ​${callsite-linenumber}​:调用日志的代码行号。

    • ​${threadid}​:当前线程的 ID。

    • ​${machinename}​:机器名称。

    • ​${newline}​:换行符。

    • ​${event-properties:item}​:自定义属性(通过 LogEventInfo.Properties​ 设置)。

  3. 日志过滤(Rules)

    • 通过 minlevel​ 和 maxlevel​ 设置日志级别范围。

    • 通过 writeTo​ 指定输出目标。

NLog 的高级功能

  1. 异步日志记录:

    • 使用 AsyncWrapper​ 将日志记录操作异步化,减少对主线程的影响。

    <target name="asyncFile" xsi:type="AsyncWrapper">
      <target xsi:type="File" fileName="logs/${shortdate}.log" />
    </target>
  2. 日志归档:

    • 使用 File​ 目标的 archiveFileName​ 和 archiveEvery​ 参数实现日志归档。

    <target name="file" xsi:type="File"
            fileName="logs/${shortdate}.log"
            archiveFileName="logs/archive/{#}.log"
            archiveEvery="Day"
            archiveNumbering="Rolling"
            maxArchiveFiles="7" />
  3. 自定义日志目标:

    • 可以通过实现 Target​ 类创建自定义日志目标。

总结

NLog 是一个功能强大且易于使用的日志记录库,适用于各种 .NET 应用程序。通过灵活的配置和丰富的功能,NLog 能够满足从简单到复杂的日志记录需求。无论是开发调试还是生产环境,NLog 都是一个可靠的选择。

【CSharp】INI文件的简介及读写操作

2025-01-13 11:26:53

简介

INI文件是一种常见的配置文件格式,通常用于存储应用程序的配置信息。它的名称来源于“Initialization”(初始化),因为这种文件通常用于在程序启动时加载初始配置。INI文件以文本形式存储,结构简单、易于阅读和编辑,因此在早期的Windows应用程序中广泛使用。

INI文件的基本结构

INI文件由多个节(Section)和键值对(Key-Value Pair)组成,格式如下:

[Section1]
Key1=Value1
Key2=Value2

[Section2]
Key3=Value3
Key4=Value4
  • 节(Section):用方括号[]​括起来,表示一个配置分组。例如:

    [Settings]
  • 键值对(Key-Value Pair):每个键值对占一行,格式为Key=Value​。例如:

    Username=Admin
    Password=123456

INI文件的特点

  1. 简单易读:INI文件是纯文本文件,可以用任何文本编辑器打开和编辑。

  2. 层次结构:通过节(Section)将配置信息分组,便于管理。

  3. 轻量级:适合存储简单的配置信息,不需要复杂的解析器。

  4. 跨平台支持:虽然INI文件起源于Windows,但其格式简单,可以在其他操作系统中使用。

INI文件的优缺点

优点

  • 易于理解:结构简单,适合非技术人员编辑。

  • 轻量级:文件体积小,加载速度快。

  • 兼容性好:许多编程语言和操作系统都支持INI文件的读写。

缺点

  • 功能有限:不支持复杂的数据类型(如数组、嵌套结构)。

  • 缺乏标准化:不同程序对INI文件的解析方式可能略有不同。

  • 不适合大规模配置:对于需要存储大量复杂配置的场景,INI文件可能不够灵活。

INI文件的常见用途

  1. 应用程序配置:存储程序的设置,如窗口大小、语言、主题等。

  2. 游戏配置:存储游戏的图形设置、控制键位等。

  3. 硬件设备配置:存储设备的初始化参数。

  4. 脚本配置:为脚本程序提供运行参数。

读写文件教程

使用Windows API

  1. 需要导入kernel32.dll​中的相关函数来操作INI文件。

    using System;
    using System.Runtime.InteropServices;
    using System.Text;
    
    class IniFile
    {
        private string filePath;
    
        // 导入Windows API函数
        [DllImport("kernel32")]
        private static extern long WritePrivateProfileString(string section, string key, string value, string filePath);
    
        [DllImport("kernel32")]
        private static extern int GetPrivateProfileString(string section, string key, string defaultValue, StringBuilder retVal, int size, string filePath);
    
        // 构造函数,传入INI文件路径
        public IniFile(string path)
        {
            filePath = path;
        }
    
        // 写入INI文件
        public void Write(string section, string key, string value)
        {
            WritePrivateProfileString(section, key, value, filePath);
        }
    
        // 读取INI文件
        public string Read(string section, string key, string defaultValue = "")
        {
            StringBuilder retVal = new StringBuilder(255);
            GetPrivateProfileString(section, key, defaultValue, retVal, 255, filePath);
            return retVal.ToString();
        }
    }
  2. 使用INI文件读写类

    class Program
    {
        static void Main(string[] args)
        {
            // 指定INI文件路径
            string iniFilePath = @"C:\example.ini";
    
            // 创建IniFile对象
            IniFile iniFile = new IniFile(iniFilePath);
    
            // 写入数据到INI文件
            iniFile.Write("Settings", "Username", "Admin");
            iniFile.Write("Settings", "Password", "123456");
            iniFile.Write("Preferences", "Theme", "Dark");
    
            // 从INI文件读取数据
            string username = iniFile.Read("Settings", "Username");
            string password = iniFile.Read("Settings", "Password");
            string theme = iniFile.Read("Preferences", "Theme", "Light"); // 如果不存在,返回默认值"Light"
    
            // 输出读取的值
            Console.WriteLine("Username: " + username);
            Console.WriteLine("Password: " + password);
            Console.WriteLine("Theme: " + theme);
        }
    }
  3. 运行结果

    运行上述代码后,会在C:\example.ini​文件中生成以下内容:

    [Settings]
    Username=Admin
    Password=123456
    
    [Preferences]
    Theme=Dark

    同时,控制台会输出

    Username: Admin
    Password: 123456
    Theme: Dark
    

使用第三方库ini-parser

  1. 安装ini-parser库:

    dotnet add package ini-parser
  2. 使用ini-parser读写INI文件:

    using IniParser;
    using IniParser.Model;
    
    class Program
    {
        static void Main(string[] args)
        {
            string iniFilePath = @"C:\example.ini";
    
            // 读取INI文件
            var parser = new FileIniDataParser();
            IniData data = parser.ReadFile(iniFilePath);
    
            // 写入数据
            data["Settings"]["Username"] = "Admin";
            data["Settings"]["Password"] = "123456";
            data["Preferences"]["Theme"] = "Dark";
    
            // 保存INI文件
            parser.WriteFile(iniFilePath, data);
    
            // 读取数据
            string username = data["Settings"]["Username"];
            string password = data["Settings"]["Password"];
            string theme = data["Preferences"]["Theme"];
    
            Console.WriteLine("Username: " + username);
            Console.WriteLine("Password: " + password);
            Console.WriteLine("Theme: " + theme);
        }
    }
  3. 可根据实际业务需求对该库做进一步的封装:

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using IniParser;
    using IniParser.Model;
    
    namespace UtilityClass
    {
        public class IniFileUtil
        {
            private readonly string _filePath;
            private readonly FileIniDataParser _parser;
            private readonly Encoding _encoding;
    
            public IniFileUtil(string filePath, Encoding encoding = null)
            {
                _filePath = filePath;
                _parser = new FileIniDataParser();
                _encoding = encoding ?? Encoding.UTF8;
            }
    
            /// <summary> 读取INI文件 </summary>
            public IniData Read()
            {
                // 使用指定编码读取文件内容
                string iniContent;
                using (var reader = new StreamReader(_filePath, _encoding))
                {
                    iniContent = reader.ReadToEnd();
                }
    
                // 解析 INI 内容
                return _parser.Parser.Parse(iniContent);
            }
    
            /// <summary> 写入INI文件 </summary>
            public void Write(IniData data)
            {
                // 使用指定编码写入文件
                using (var writer = new StreamWriter(_filePath, false, _encoding))
                {
                    _parser.WriteData(writer, data);
                }
            }
    
            /// <summary> 获取指定键的值 </summary>
            public string GetValue(string section, string key)
            {
                var data = Read();
                return data[section]?[key];
            }
    
            /// <summary> 设置指定键的值 </summary>
            public void SetValue(string section, string key, string value)
            {
                var data = Read();
                if (data[section] == null)
                {
                    data.Sections.AddSection(section);
                }
                data[section][key] = value;
                Write(data);
            }
    
            /// <summary> 删除指定键 </summary>
            public void DeleteKey(string section, string key)
            {
                var data = Read();
                if (data[section] != null && data[section].ContainsKey(key))
                {
                    data[section].RemoveKey(key);
                    Write(data);
                }
            }
    
            /// <summary> 删除指定节 </summary>
            public void DeleteSection(string section)
            {
                var data = Read();
                if (data.Sections.ContainsSection(section))
                {
                    data.Sections.RemoveSection(section);
                    Write(data);
                }
            }
        }
    }

总结

INI文件是一种简单、轻量级的配置文件格式,适合存储基本的配置信息。虽然它在现代应用程序中逐渐被更强大的格式取代,但在某些场景下仍然有其独特的优势。如果你需要快速存储和读取简单的配置信息,INI文件仍然是一个不错的选择。

【CSharp】使用Fleck库实现WebSocket服务

2024-12-18 20:45:18

前言

最近公司有个项目需要用C/S架构的桌面应用程序与B/S架构的网页程序进行通信做数据的交互功能。在网上查了一下资料,发现 Fleck 实现一个WebSocket服务竟然如此简单明了,于是在此记录和整理了一下 Fleck 实现WebSocket服务的简单应用,希望对你有所帮助。

简介

Fleck 是一个用C#编写的轻量级WebSocket服务器库,它易于使用且高性能,同时保持代码的简洁性。

特点:

  • 无需继承:Fleck不需要你继承任何类,也不需要依赖于容器或额外的引用。

  • 无依赖:Fleck不依赖于HttpListener​HTTP.sys​,这意味着它可以在Windows 7和Server 2008主机上工作。

  • 跨平台:由于不依赖于HttpListener​,Fleck可以在非Windows平台上运行。

使用

安装Fleck

通过NuGet包管理器安装Fleck库

Install-Package Fleck

创建WebSocket服务器

以下是一个简单的WebSocket服务器示例:

using Fleck;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

namespace WebSocketServiceDemo
{
    public class WebSocketService
    {
        /// <summary> 客户端url以及其对应的Socket对象字典 </summary>
        public IDictionary<string, IWebSocketConnection> dic_Sockets = new Dictionary<string, IWebSocketConnection>();
      
        private readonly object _lockObject = new object();

        private WebSocketServer _server;

        public WebSocketService()
        {
            //创建WebSocket服务端实例
            _server = new WebSocketServer("ws://0.0.0.0:9997");
            _server.RestartAfterListenError = true; //出错后进行重启
            _server.Start(ws=>
            {
                ws.OnOpen = () =>
                {   
                    WebSocket_OnOpen(ws);
                };
                ws.OnClose = () =>
                {
                    WebSocket_OnClose(ws);
                };
                ws.OnMessage = message =>
                {
                    Task.Run(() => { WebSocket_OnMessage(ws, message); });
                };
                ws.OnError = exp => 
                {
                    WebSocket_OnError(ws, exp);
                };
            });
        }

        private void WebSocket_OnOpen(IWebSocketConnection wsConnection)
        {
            string clientUrl = wsConnection.ConnectionInfo.ClientIpAddress + ":" + wsConnection.ConnectionInfo.ClientPort;
            AddSocket(clientUrl, wsConnection);
            Debug.WriteLine($"服务器和客户端网页:{clientUrl} 建立WebSock连接!当前连接数量:{dic_Sockets.Count}");
        }

        private void WebSocket_OnClose(IWebSocketConnection wsConnection)
        {
            string clientUrl = wsConnection.ConnectionInfo.ClientIpAddress + ":" + wsConnection.ConnectionInfo.ClientPort;
            if (dic_WSSockets.ContainsKey(clientUrl))
            RemoveSocket(clientUrl);
            Debug.WriteLine($"服务器和客户端网页:{clientUrl} 断开WebSock连接!当前连接数量:{dic_Sockets.Count}");
        }

        private void WebSocket_OnError(IWebSocketConnection wsConnection, Exception exception)
        {
            string clientUrl = wsConnection.ConnectionInfo.ClientIpAddress + ":" + wsConnection.ConnectionInfo.ClientPort;
            if (dic_WSSockets.ContainsKey(clientUrl))
            {
                RemoveSocket(clientUrl);
                Debug.WriteLine($"服务器和客户端网页:{clientUrl} 意外断开WebSock连接!当前连接数量:{dic_Sockets.Count}");
            }
        }

        private void WebSocket_OnMessage(IWebSocketConnection wsConnection, string msg)
        {
            string clientUrl = wsConnection.ConnectionInfo.ClientIpAddress + ":" + wsConnection.ConnectionInfo.ClientPort;
            Debug.WriteLine($"服务器:【收到】来客户端网页:{clientUrl}的信息:\n{msg}");
        }

        private bool RemoveSocket(string key)
        {
            lock (_lockObject)
            {
                return dic_WSSockets.Remove(key);
            }
        }

        private void AddSocket(string key, IWebSocketConnection socket)
        {
            lock (_lockObject)
            {
                dic_Sockets.Add(key, socket);
            }
        }
    }
}

安全WebSockets (wss://)

要启用安全连接,需要使用wss​而不是ws​,并指向包含公钥和私钥的x509证书:

var server = new WebSocketServer("wss://0.0.0.0:9997");
server.Certificate = new X509Certificate2("MyCert.pfx");
server.Start(ws =>
{
    //...use as normal
});

子协议协商

Fleck允许你指定支持的子协议,并在WebSocketServer.SupportedSubProtocols​属性上进行协商。如果客户端请求中没有找到支持的子协议,连接将被关闭:

var server = new WebSocketServer("ws://0.0.0.0:9997");
server.SupportedSubProtocols = new []{ "superchat", "chat" };
server.Start(socket =>
{
    //socket.ConnectionInfo.NegotiatedSubProtocol 被填充
});

禁用Nagle算法

你可以通过设置WebSocketConnection.ListenerSocket.NoDelay​true​来禁用Nagle算法:

var server = new WebSocketServer("ws://0.0.0.0:9997");
server.ListenerSocket.NoDelay = true;
server.Start(socket =>
{
    //子连接将不使用Nagle算法
});

监听错误后自动重启

你可以通过设置WebSocketServer.RestartAfterListenError​true​来在监听错误后自动重启服务器:

var server = new WebSocketServer("ws://0.0.0.0:9997");
server.RestartAfterListenError = true;
server.Start(socket =>
{
    //...正常使用
});

自定义日志记录

Fleck可以记录到Log4Net或任何其他第三方日志系统,只需覆盖FleckLog.LogAction​属性:

ILog logger = LogManager.GetLogger(typeof(FleckLog));
FleckLog.LogAction = (level, message, ex) =>
{
    switch (level)
    {
        case LogLevel.Debug:
            logger.Debug(message, ex);
            break;

        case LogLevel.Error:
            logger.Error(message, ex);
            break;

        case LogLevel.Warn:
            logger.Warn(message, ex);
            break;

        default:
            logger.Info(message, ex);
            break;
    }
};

【CSharp】lock(this)与lock(private object)区别

2024-12-18 10:50:32

前言

在使用多线程编程时,我们会对代码关键部分确保其一次只由一个线程执行,对于防止争用条件和保持数据完整性至关重要。在C#中,lock 语句就是用于通过同步对共享资源的访问来实现此目的工具。本文介绍lock(this) 与lock(private object) 两种方法及区别。

简介

lock(this) lock(private object) 两种方法都可用于控制对代码块的访问,但在安全性和意外交互的可能性方面有所不同。正确选择对象进行锁定可能会对代码的安全性和可靠性产生重大影响。

1、lock(this)

使用 lock(this) 时,我们是锁定类的当前实例。也就是说锁被放置在代码当前正在操作的对象上。

public class MessageBox
{
    public void ShowMessage()
    {
        lock (this)
        {
            // 输出信息
            Console.WriteLine("感谢您的支持!");
        }
    }
}

上面示例,使用lock(this)确保ShowMessage方法一次只由一个线程对MessageBox的每个实例执行。这将存在被暴露于公开场合的问题。当有其他人也引用此对象,并对其锁定时,这这可能会导致死锁或其他问题。

2、lock(private object)

使用lock(private object)时,我们将锁定一个私有对象,该对象通常是专为此目的而创建的对象的实例。这是一种更安全、更常见的做法。

public class MessageBox
{
    // 定义锁定对象
    private readonly object lockobject = new object();
  
    public void ShowMessage()
    {
        lock (lockobject)
        {
            // 输出信息
            Console.WriteLine("感谢您的支持!");
        }
    }
}

上面示例,lock (lockobject) 确保一次只有一个线程可以执行 ShowMessage 方法,lockobject 是私有的,其他类或代码无法访问。

3、使用 lock(private object) 的好处

封装: 私有对象(private object) 对程序的其他部分是隐藏的,因此其他代码不会意外或有意地锁定它。

安全: 降低因外部代码尝试锁定同一对象而导致的死锁或其他同步问题的风险。

区别

lock(this) 为锁定类的当前实例。此法简单,但可能存在风险,因为其他代码可能会锁定同一对象。

lock(private object) 为锁定只有类知道的私有对象。这更安全,也是最常用且建议的首选方法。此法可以更好地控制并避免因锁定公共对象而导致的意外问题。

总结

以上是关于多线程编程安全中,使用 lock 对共享资源的访问锁定的方法及其二中使用方式的区别,希望对对您有所帮助。