余弦G的博客 - C#
type: channel
link: https://www.cosineg.com/tag/CSharp/
items: ASP.NET Core 实现 SSE 流式响应
items: Effective C# 阅读笔记（四）
items: Effective C# 阅读笔记（三）
items: 更新了OpenCC.NET，加上记Nuget打包的一个坑
items: Effective C# 阅读笔记（二）
items: Effective C# 阅读笔记（一）
items: OpenCC.NET GUI——方便的中文转换工具
items: 基于RSA算法的软件授权生成和验证
items: 写了一个OpenCC.NET
items: Fluent Blog上线了

ASP.NET Core 实现 SSE 流式响应
type: item
date: 2024-09-10T21:45:17+08:00
description: 重点先说一个踩过的坑：如果要使用 SSE 流式响应，那么在 Nginx 里不要开启该接口的 Gzip ！！！因为开启 Gzip 后会导致数据达到一定大小才会被压缩然后返回给前端，导致前端不能实时获得数据，就无法形成流式的文本效果。例子举个在 ASP.NET Core 中实现 SSE 流式响应的简单例子。[HttpGet] public async Task<IActionResult> SseExample() { // 请求头 Response.Headers.Add("Content-Type", "text/event-stream"); Response.Headers.Add("Cache-Control", "no-cache"); Response.Headers.Add("Connection", "keep-alive"); // 假设不断得到新数据 for (int i = 0; i < 10; i++) { // 每个数据参照 SSE 返回数据格式进行组装 var content = "event: message\n" + "data: {\"color\":\"66ccff\"}\n\n"; // 立刻写入响应 await Response.WriteAsync(content); await Response.Body.FlushAsync(); } // 结束 return new EmptyResult(); }测试的时候可以从控制台的网络详情里看到本次响应的数据流：备注我目前主要是希望搭一套向量知识库（选了MaxKB），查询出知识库中的已知信息然后利用大模型分析回答。因为需要对知识库的输入做处理（比如增加关键字，过滤某些信息，相同问题查缓存等），直接暴露知识库 API 风险较高，可能 token 都被人用来问大模型其他问题了，因此需要做一层处理和转发。而当前后端是基于 ASP.NET Core 的，所以需要在 ASP.NET Core 里实现 SSE 流式响应，使得能够在前端展示一个字一个字蹦出来的效果。

Effective C# 阅读笔记（四）
type: item
date: 2023-05-16T23:29:00+08:00
description: 前言为了提升一下自己的姿势水平，买了 Effective C# 和 More Effective C# 两本书，想一边阅读一边强迫自己做点笔记（主要是自己的理解），不然怕是看了就忘。另外手头还有一本《深入解析C# ( C# in Depth )》其实我已经阅读了一遍，但感觉没咋读懂，下次有机会也用这种方式重读一遍。忙着搞秋招和毕业论文，以及自己是在是太能摸了，导致距离上次更新已经快一年了。不过以后工作还是要写.NET的，所以还是不能把相关学习落下，今天开始边写变更吧。今天这篇是 Effective C# 的第四章。第四章 合理地运用LINQLINQ全名Language Integrated Query，中文译为语言集成查询，简单来说就是用类似SQL语言的方式来对数据进行查询和操作，C#里提供了两种可使用的语法，分别是查询语法和方法语法。查询语法更贴近SQL语法，但其实我从来没写过...方法语法就是通过扩展方法实现了一系列方法，在数据集合之后调用，比如list.Where(n => n > 0).ToList();，我觉得使用起来更加方便，而且这个应该能算作是函数式编程吧。LINQ给数据操作还是带来了很大的方便的，尤其是写后端的时候操作数据库，像Java也是从8开始引进了stream，用起来感觉也很相似，之前实习的时候基本都是在写stream。第29条 优先考虑提供迭代器方法，而不要返回集合当我们要返回一系列对象的时候，应该优先考虑提供迭代器方法而不是集合，这样可以立刻返回，并且等调用方需要实际使用的时候才会创建对应数据，节省时间和空间（就是yield return返回IEnumerable<T>而不是直接返回一个List）。举个最简单的例子来说，我可能需要用到所有非负int值组成的序列中的一部分，如果直接创建一个包含这些数值的List<int>很划不来，而通过Enumerable.Range(0, int.MaxValue)会返回IEnumerable<int>，可以在后续使用的时候再去生成数值。而如果调用方真的需要对这些数据反复使用或者缓存下来，那让他们自己调用ToList()或者ToArray()就好了，相当于提供了两种选择，方便灵活使用，因此即使调用方确实需要把数据保存到集合里，我们还是应该优先提供迭代器方法（其实LINQ就是基于这个原则的）。另外在编写迭代器方法的时候要注意一个问题，就是参数检查和生成序列的逻辑分开写。因为如果合在一起的话：// 求正数序列 public static IEnumerable<int> GeneratePositiveNumber(int first) { // 参数检查 if (first <= 0) { throw new ArgumentException("first must be postive", nameof(first)); } // 序列生成 var num = first; while (num <= int.MaxValue) { yield return num; num++; } }如果按照上面这么写的话，当进行var nums = GeneratePositiveNumber(-11);的时候，并不会抛出异常，只有当真正使用到nums里面的数值的时候，才会抛异常，这样很不合理而且难以诊断错误。因为编译器固定如此，无法修改，但我们可以把检查和生成分成两段函数来规避这个问题，例如：// 求正数序列 public static IEnumerable<int> GeneratePositiveNumber(int first) { // 参数检查 if (first <= 0) { throw new ArgumentException("first must be postive", nameof(first)); } // 序列生成 return GeneratePositiveNumberImpl(first); } // 序列生成的实现 private static IEnumerable<int> GeneratePositiveNumberImpl(int first) { var num = first; while (num <= int.MaxValue) { yield return num; num++; } }这样的话，调用的时候，在进入到迭代器方法之前就能抛出异常。第30条 优先考虑通过查询语句来编写代码，而不要使用循环语句C#提供了经典的比如for、foreazh、while等循环结构，但他们的功能其实也可以通过查询语句来实现。与采用循环语句所编写的命令式结构相比，查询语句能够更为清晰地表现开发着的意图。比我想要打印一批成绩高于80分的…

Effective C# 阅读笔记（三）
type: item
date: 2022-05-09T16:07:22+08:00
description: 前言为了提升一下自己的姿势水平，买了 Effective C# 和 More Effective C# 两本书，想一边阅读一边强迫自己做点笔记（主要是自己的理解），不然怕是看了就忘。另外手头还有一本《深入解析C# ( C# in Depth )》其实我已经阅读了一遍，但感觉没咋读懂，下次有机会也用这种方式重读一遍。之前为了找实习（今年真难啊），花了很多时间在了刷LeetCode和背八股文上。现在离去实习也没多久了，啥也不会过去不太好，所以后续应该也要花相当时间在学Java上。今天这篇是 Effective C# 的第三章。第三章 合理地运用泛型泛型还是一个比较方便和重要的特性。Java的泛型基于类型擦除，也就是编译器会保证类型安全，但实际运行时类型都被擦除了，所有不同List<T>都是同一个类型List（个人的理解，有错误请务必指出）。而C#的泛型则是真正的泛型类型，比如通过typeof(List<int>)可以看到其运行时类型System.Collections.Generic.List`1[System.Int32]。而这些泛型类只有在运行时碰到了，CLR才会将其实例化，可以避免不必要的开销。另外值类型和引用类型分别作为类型参数的泛型类也不相同。泛型参数包含值类型时，CLR在JIT时都会生成单独特化的代码，不同泛型类之间不共享代码。这样能够防止值类型的装箱和拆箱问题。而对于引用类型，CLR生成的不同泛型类的代码是共享的，类型安全通过编译器在编译时保证（但我觉得即使生成的代码相同，也应该还另外保存了类型信息，因为typeof()是可以看到运行时泛型类中的泛型参数究竟是什么引用类型的，这一块书上没说），这样可以缩减程序尺寸和提升性能。第18条 只定义刚好够用的约束条件给泛型的类型参数指定约束条件，这样只有满足特定条件的类型才能作为该泛型的参数。约束条件具体都包括哪些，请参考文档 类型参数的约束（C# 编程指南）（中文版在表格的倒数第2-5行有显示错误，英文版没问题，分别应该是基类名和接口名）。书中主要是约束条件限制得太宽，那就很难使用，因为缺少信息，很多操作不能使用，比如public static bool AreEqual<T>(T left, T right) where T : IEquatable<T> { return left.Equals(right) }如果设定了IEquatable<T>的约束条件，那么在内部对于T类型的变量就可以使用IEquatable<T>.Equals()，否则就只能调用Object.Equals()。同样，如果设置得过严，那就失去了泛型的优势，比如：// 我想的一个例子，实际上不太合理，但用来说明应该足够了 static void Print<T>(T list) where T: IList<int> { foreach (var item in list) { Console.WriteLine(item + 1); } }HashSet<int>对象按理说应该也能完成上面循环输出的操作，但实际上却无法使用，因为HashSet<int>并没有实现IList<int>接口。而foreach操作只要实现了IEnumerable<T>就能使用，因此把IList<int>约束换为IEnumerable<int>更加合理。另外书中提醒还有new()约束也要注意使用，因为有的时候代码中的new()可以用default()替代。其中对于值类型，new()返回的就是值类型的默认值，所以和default()没区别（注意C# 10开始支持自定义struct的无参构造函数，对于这样的struct，new()和default()就不同了！）；对于引用类型，默认值都是null，显然new()和default()和差异很大。总的来说，泛型就是为了在各种场合能够广泛使用，所以要仔细考虑约束条件。约束条件太宽，有很多特性难以使用；约束条件太严，使用场合就受限。因此要仔细考虑每一条约束条件，只保留必要的条件。第19条 通过运行期类型检查实现特定的泛型算法放宽类型参数的限制（比如让输入参数的类型为接口，而非一个具体类型），可以让程序更加通用和泛化，但是这又会导致实现的时候无法使用一些具体类型才拥有的特性。因此可以通过运行期类型检查来检测参数的运行时类型，如果是一些特定类型则使用其的某些特性，以此达到提升程序性能的目的。书中举了个作者自己写的例子，但我觉得直接看List<T>的源码就好，List<T>有一个参数为IEnumerable<T>类型的构造函数：public List(IEnumerable<T> collection) { if (collection == null) ThrowHelper.ThrowA…

更新了OpenCC.NET，加上记Nuget打包的一个坑
type: item
date: 2022-04-26T17:30:00+08:00
description: 更新OpenCC.NET这两天有点空，把OpenCC.NET重构和优化了一下，版本号也正式来到了1.0。本来是想加入异步API，结果测试发现可能因为本来转换就是查字典速度很快，加上异步的话还需要Task的创建开销，导致性能还不如同步版本。然后尝试把原来用StringBuilder改成栈上的Span<char>，结果性能也没啥区别。不过原来的内部转换的实现复用性不强而且乱，还是决定重写成OpenCC原本那种链式处理。简单来说就是分词完后得到IEnumerable<string>格式的词组，给IEnumerable<string>写一个扩展方法：private static IEnumerable<string> ConvertBy(this IEnumerable<string> phrases, params IDictionary<string, string>[] dictionaries) { return phrases.Select(phrase => { ... // 根据字典进行转换 } }一批词组先用一些字典进行转换，得到中间结果，然后可以再用其他字典继续转换，直到得到结果。这样就可以等效出原OpenCC中的转换链。比如原OpenCC中的s2twp.json（简=>繁台+词汇）：{ "name": "Simplified Chinese to Traditional Chinese (Taiwan standard, with phrases)", "segmentation": { "type": "mmseg", "dict": { "type": "ocd2", "file": "STPhrases.ocd2" } }, "conversion_chain": [{ "dict": { "type": "group", "dicts": [{ "type": "ocd2", "file": "STPhrases.ocd2" }, { "type": "ocd2", "file": "STCharacters.ocd2" }] } }, { "dict": { "type": "ocd2", "file": "TWPhrases.ocd2" } }, { "dict": { "type": "ocd2", "file": "TWVariants.ocd2" } }] }根据conversion_chain部分，利用ConvertBy()可以等效出：public static string HansToTWWithPhrase(string text) { // 分词 var phrases = ZhSegment.Segment(text); // 链式转换 return phrases.ConvertBy(ZhDictionary.STPhrases, ZhDictionary.STCharacters) .ConvertBy(ZhDictionary.TWPhrases) .ConvertBy(ZhDictionary.TWVariants) // Join()是我写的扩展方法，其实就是调用string.Join()把词组重新合并成句子 .Join() }于是我就把原来的API全部重写成这种转换链的形式了，另外还加了日语汉字新旧字体转换的API（因为原OpenCC带了相应的字典，干脆也就实现了），虽然感觉也就图一乐用。顺带做了一下性能测试。程序的第一次转换，就算是转换空文本都要差不多花费500ms的时间，后续就不用了，可能是因为要加载Jieba分词的模型。然后测试了一本15.5M大小的网络小说，共14万行，同步的话大概要总共花费9s；改用异步方式，每行都Task.Run()调用转换方法然后await，可以缩短到4.5s左右。另外还尝试了一下GitHub上的Action，用了别人写好的脚本，可以自动把最新的工程构建和发布到Nuget上，很是方便。Nuget打包的一个坑先说结论，我在Visual Studio 2022 17.1.0上设置C#项目自动生成Nuget包时，如果在ItemGroup的Content上配置了PackageCopyToOutput属性，且同时又配置了PackagePath属性，那么PackageCopyToOutput会被无效。OpenCC.NET是我第一次打Nuget包，所以啥也不懂。其实打包都是自动化的，只要在工程配置文件里设置一下库的信息比如名字版本号啥的就好。但问题是这个库需要用到OpenCC字典和Jieba.NET的资源文件，所以按理说应该也要打进Nuget包里，然后其他程序如果引用了这个包，编译的时候会自动把这些文件复制到输出目录。一开始我实在是不知道咋操作，所以只能在r…

Effective C# 阅读笔记（二）
type: item
date: 2022-01-21T11:00:00+08:00
description: 前言为了提升一下自己的姿势水平，买了 Effective C# 和 More Effective C# 两本书，想一边阅读一边强迫自己做点笔记（主要是自己的理解），不然怕是看了就忘。另外手头还有一本《深入解析C# ( C# in Depth )》其实我已经阅读了一遍，但感觉没咋读懂，下次有机会也用这种方式重读一遍。这半个月要回家过年，得等年后才能继续更新了。今天这篇是 Effective C# 的第二章。第二章 .NET的资源管理第11条 理解并善用.NET的资源管理机制简单来说.Net提供了垃圾回收器GC，帮助控制托管内存，让开发者不用担心内存泄漏等问题。GC在运行时会判断不再使用的对象为垃圾，将其回收，并压缩托管堆，把剩余的活动对象转移到连续的内存区域上。而非托管资源回收需要由开发者来管理控制。NET中提供释放非托管资源的方式主要是：finalizer（终结器）和IDisposeable接口。finalizer在C#中和C++类似，是通过Destructor()析构函数实现的，实际函数名为类名前面加上一个~，比如：class UnManagedClass { ~UnManagedClass() { Console.WriteLine("被释放了"); } }但这种方式适用于C++，并不适用于C#，因为GC回收得并不及时，只能保证finalizer会被执行，但不确定何时执行。而且当GC回收垃圾时，发现该对象需要运行finalizer，就不得不先挂起回收操作来运行finalizer，等到下一次才能把其回收掉。同时GC还有代（generation）的概念，分为第0，1，2代，每运行完一次GC，剩余留在内存里的对象就会长一代（用来区分短期变量和长期变量，短期变量更有可能是垃圾），GC对于越后面的代，检测的次数越少，这会导致拥有finalizer的对象会长时间占在内存里得不到回收。因此释放资源首选实现IDisposeable接口，然后主动调用Dispose()方法来释放资源。// 定义了class UnManagedClass : IDisposeable // 在finally中调用Dispose()确保资源被释放 var unManaged = new UnManagedClass(); try { // 操作unManaged } finally { unManaged.Dispose(); } // 更通常的情况是使用using using (var unManaged = new UnManagedClass()) { // 操作unManaged }//到此，using域结束，unManaged自动释放注：C# 8.0提供了新的using声明语法，不需要原来的大括号了，在using的变量的生命周期结束后将自动释放。public void DoSomeWork() { using var unManaged = new UnManagedClass() // 操作unManaged }// 到此，unManaged生命周期结束，自动释放详细的IDisposeable接口实现将在第17条讲述。第12条 声明字段时，尽量直接为其设定初始值如果都把字段的初始值设定放在构造函数里做的话，在构造函数多了之后可能就会忘记给部分字段设定初始值，所以不如在声明字段的时候就直接初始化。但书中也给了三种不应该使用初始化语句的情况：第一种：把对象初始化成0和null。因为默认的初始化逻辑就是把对象初始化成0或null，自己手动再度设0和null没有意义。第二种：构造函数都各自以不同方式初始化了字段。public class MyClass { private List<string> _labels = new List<string>(); public MyClass() { } public MyClass(int size) { _labels = new List<string>(size); } } // 就类似于 public class MyClass { private List<string> _labels; public MyClass() { _labels = new List<string>(); } public MyClass(int size) { // 白白创建了一个List然后舍弃 _labels = new List<string>(); _labels = new List<string>(size); } }不过我觉得书本这里逻辑也有点问题，本来前面就是说为了防止因构造函数太多导致漏掉字段赋初值，所以要在声明时初始化。结果这里又说如果构造函数都进行了初始化，那么就不要在声明的时候初始化了，那万一漏掉…

Effective C# 阅读笔记（一）
type: item
date: 2022-01-14T15:51:00+08:00
description: 前言为了提升一下自己的姿势水平，买了 Effective C# 和 More Effective C# 两本书，想一边阅读一边强迫自己做点笔记（主要是自己的理解），不然怕是看了就忘。另外手头还有一本《深入解析C# ( C# in Depth )》其实我已经阅读了一遍，但感觉没咋读懂，下次有机会也用这种方式重读一遍。今天这篇就先从 Effective C# 的第一章开始，后续慢慢更新。第一章 C#语言的编程习惯第1条 优先使用隐式类型的局部变量简单来说就是在声明局部变量时，应该使用var而尽量避免使用类型的全称。var的引入主要是为了匿名类型和Linq，var跟dynamic动态类型不同，它是靠编译器自动推断出变量的实际类型。这个还是比较好理解的，因为用var很多时候方便且不会影响对代码的理解，常用的几个场景比如// 很容易就能看出该变量是什么类型，用var简洁 string s1 = "66ccff"; var s2 = "66ccff"; Dictionary<int, List<Tuple<int, int>>> d1 = new Dictionary<int, List<Tuple<int, int>>>(); var d2 = new Dictionary<int, List<Tuple<int, int>>>(); // 匿名类型，因为“匿名” var a = new { A = 1, B = 2 }; // Linq，编译器推断比人工指定更合适，更方便 var student = Students.Where(s => s.Score >= 80).OrderBy(s => s.Id)但也不是所有局部变量都适合用var，因为此时不好看出变量的类型，或编译器推断并不适合，比如// 这个很好看出变量类型 var count = GetBooksCount(); // 遇到起名不规范的，就难办了 var result = DoSomeWork(); // 手工指定方便阅读 string result = DoSomeWork(); // 函数默认返回int，做除后结果为int var num = GetNumber(); var result = num / 7; // 手工指定double，做除后结果为double double num = GetNumber(); var result = num / 7;所以在保证能在阅读时清楚知道变量类型的情况下，应尽量使用var做到简洁明了；反之就要明确指出类型，比如面对int，float，double等时。第2条 考虑使用readonly代替constconst是编译期常量，readonly是运行期常量。区别在于编译时，所有用到const常量的地方，该常量都会被常量实际值给完全替换，而用到readonly常量的地方仍会保留对readonly常量的引用，类似于// 声明 const int a = 712; readonly int b = 712; // 使用 void DoSomeWork() { Console.WriteLine(a); Console.WriteLine(b); } // 等价于 void DoSomeWork() { // 在编译的时候，const常量就被替换成了值 Console.WriteLine(712); // 仍引用了readonly常量 Console.WriteLine(b); }问题在于在库A中声明了一常量const int num = 100;，若项目B引用了A的num，编译B时也会自动将B中用到的num自动替换为100。若后续库A将num更新为200，但项目B没有重新编译，则B中num仍为100。而改为readonly常量则没有这个问题，因为使用的时候引用的时真实变量，而不是编译时就写死的值。因此只有在对性能极端敏感和有需求将值在编译期就固定下来的时候才使用const，不然最好选择readonly。第3条 优先考虑is或as运算符，尽量少用强制类型转换is运算符用来判断变量是否兼容（支持转换成）某种类型，as运算符用来转换变量的类型。object o = "66ccff"; // 强制转换 // 如果无法转换会抛出异常 try { var s1 = (string)o; Console.WriteLine(s1); } catch (InvalidCastException e) { Console.WriteLine(e); throw; } // is 判断类型是否兼容某类型 // null或不兼容返回false if (o is string) { string s2 = (string)o; Console.WriteLi…

OpenCC.NET GUI——方便的中文转换工具
type: item
date: 2021-12-03T17:00:00+08:00
description: 前言之前我基于OpenCC写过一个OpenCC.NET的库，写得也比较烂，就想稍微重构一下然后再写个即开即用的桌面端程序，于是就有了这个OpenCC.NET GUI。本来是想尝鲜用Win UI3来实现的，最后还是决定稳妥点用了WPF+UI库ModernWpf。这次也是我第一次用MVVM Toolkit，尽量照着MVVM的思想去写代码，也算是有所收获。虽然我完全不懂设计，但程序总得有个Logo。于是我把「字」字做了一点改造，搞出一个类似转换箭头的结构，代表中文转换；然后分层加阴影和光照，尝试去模仿Fluent Design图标的那种质感，最后就得到了：程序已经开源放在了GitHub和Gitee上，有兴趣的可以取看看，地址：https://github.com/CosineG/OpenCC.NET.GUIhttps://gitee.com/cosineg/OpenCC.NET.GUI下面的部分我就偷懒直接照抄readme了。介绍OpenCC.NET GUI是基于OpenCC(Open Chinese Convert, 开放中文转换)，使用OpenCC.NET实现的中文转换工具，支持中文简繁体之间词汇级别的转换，同时还支持地域间异体字以及词汇的转换。特点支持简=>繁，繁=>简，繁=>繁的转换模式支持包括OpenCC，中国大陆，台湾，香港四种异体字标准支持中国大陆和台湾地区的常用词汇转换支持txt文本文件的批量转换开始获取可以直接在仓库页面右侧获取最新Release版本，或下载源码后自行编译。若无法运行，请确保安装了.NET Core 3.1运行时。使用软件提供了文本编辑转换和文件批量转换两种功能。文本编辑转换此模式下直接在文本框中输入你所需要转换的语句，在右侧选择转换选项后，点击转换按钮即可得到结果。文件批量转换此模式下可以批量导入txt文本文件进行转换。若生成文件为乱码，请确保原文件为UTF-8编码。引用OpenCC.NETOpenCC.NET实现文本转换。OpenCCOpenCC为OpenCC.NET提供词库。jieba.NETjieba.NET为OpenCC.NET提供分词。ModernWpfModernWpf提供UI控件。Ookii.Dialogs.WpfOokii.Dialogs.Wpf提供选择文件夹窗口（没错，WPF没有自带选择文件夹窗口）。

基于RSA算法的软件授权生成和验证
type: item
date: 2021-11-08T09:37:00+08:00
description: 前言又摸了好久，结果博客都好几个月没更新，是时候水一水了。刚好前段时间听说项目要加授权功能，便对如何实现这种利用激活码进行授权的形式产生了兴趣，决定动手试一试，并以此文加以记录。RSA算法我对加密算法相关的东西完全一窍不通，据我搜索的资料(指用百度和Google，如有错误请指出)，适用于生成软件激活码这种场景的是非对称加密算法。非对称加密算法和对称加密算法的区别主要在于密钥，对称加密算法只有一个密钥，如果用于激活码生成，那么在生成端和验证端都必须保存这个密钥，用户就能比较简单地获取。之后用户一旦得到一个可用激活码，就可以将其解码后按照授权信息结构伪造激活码。而非对称加密算法则有一个公钥(较简单)和一个密钥(较复杂)，公钥可以公开，而私钥则要保密。生成激活码时使用私钥，用户只能接触到公钥，而依靠公钥只能验证激活码但无法生成激活码，就达到了我们的目的。非对称加密算法中一种常用算法是RSA算法，如果使用SSH时有生成和使用过SSH key的话应该会有印象。它的公钥(PEM格式)形式为：-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZQUM9rHDxlYwo+4R2USqzrhtO Mt5s9LWei+CsxGSGFWKmW/7VvZX7xl/fpgpImJ4mnu2a03N1zc24u5/kpPo3yXHF H6ikvtUooXgfW1AjDDddgXOV8cIBO2XCLet9zIS+hBrHJlsbF4Yx5g+g9j6hjVOt EDvxX6dl3dLSQX1mmQIDAQAB -----END PUBLIC KEY-----私钥(PEM格式)形式为：-----BEGIN PRIVATE KEY----- [binary] -----END PRIVATE KEY-----RSA算法主要有两种用途，一种是加密与解密信息，另外一种是签名与验证信息。加密与解密RSA算法的加密与解密功能，主要是把原始信息利用公钥加密，传输给他人后，利用私钥将其解密重新得到原始信息。这主要保证了信息不会被泄露，因为公钥只能加密不能解密，即使被他人获取了也没有关系。而只有手中有私钥的人才能解密，获取原始信息的内容。这里引用一段《技术宅de小坑 - RSA加密、解密、签名、验签的原理及方法》所举的例子：第一个场景：战场上，B要给A传递一条消息，内容为某一指令。RSA的加密过程如下：（1）A生成一对密钥（公钥和私钥），私钥不公开，A自己保留。公钥为公开的，任何人可以获取。（2）A传递自己的公钥给B，B用A的公钥对消息进行加密。（3）A接收到B加密的消息，利用A自己的私钥对消息进行解密。在这个过程中，只有2次传递过程，第一次是A传递公钥给B，第二次是B传递加密消息给A，即使都被敌方截获，也没有危险性，因为只有A的私钥才能对消息进行解密，防止了消息内容的泄露。那么能不能利用加密和解密来实现激活码生成呢？我个人认为算是勉强可以。虽然按照原理来说必须靠公钥加密，私钥解密，也就是公钥生成激活码，私钥对激活码解码。可上文说了私钥必须保密，怎么能保存在客户端呢？但我们其实也可以把两者的地位交换，即把原始公钥保密，作为密钥；而把原始密钥公开，作为公钥。这样就可以用新密钥(原公钥)加密授权信息制作激活码，然后在客户端保存新公钥(原密钥)，用于解密和检查激活码。但实际上RSA算法的密钥包含了公钥信息(所以密钥才会相对公钥更复杂)，用这种方式制作的激活码只能防住一般用户，如果用户从新公钥(原密钥)中推导出了新密钥(原公钥)，那么我们授权系统也基本等于被攻破了。签名与验证RSA算法的签名与验证功能，主要是把原始信息利用密钥对其进行签名，附上原始信息一起传输给他人后(签名只能供验证用途，无法恢复出原始信息，所以要附带上原始信息)，利用公钥对签名和原始信息进行验证，若验证通过则说明信息真实有效。这主要保证了信息不会被篡改，因为只有手中有密钥的人才能制造有效签名来实现和篡改的信息一致，否则篡改信息无法通过公钥的验证。这里再引用一段《技术宅de小坑 - RSA加密、解密、签名、验签的原理及方法》所举的例子：第二个场景：A收到B发的消息后，需要进行回复“收到”。RSA签名的过程如下：（1）A生成一对密钥（公钥和私钥），私钥不公开，A自己保留。公钥为公开的，任何人可以获取。（2）A用自己的私钥对消息加签，形成签名，并将加签的消息和消息本身一起传递给B。（3）B收到消息后，在获取A的公钥进行验签，如果验签出来的内容与消息本身一致，证明消息是A回复的。在这个过程中，只有2次传递过程，第一次是A传递加签的消息和消息本身给B，第二次是B获取A的公钥，即使都被敌方截获，也没有危险性，因为只有A的私钥才能对消息进行签名…

写了一个OpenCC.NET
type: item
date: 2021-06-11T00:22:00+08:00
description: 之前看到一个库叫OpenCC，试了试它提供的网页版感觉很不错，比一般的简繁傻转强多了（比如不会干出把“一”都转换成“壹”这种傻事），而且还支持常用词的转换（比如“内存”和“記憶體”）。于是自己写了个C#版本叫OpenCC.NET，已经放在Github和Gitee上了，点进项目地址可以看到详细的使用说明。nuget上也可以直接下载包，搜索OpenCCNET（OpenCC.NET的这个带点的ID已经被人用了）即可。

Fluent Blog上线了
type: item
date: 2021-04-22T11:08:00+08:00
description: 学习ASP.NET Core的同时练习编写的渣作Fluent Blog今天终于上线了，地址为https://fluent.cosineg.com/，欢迎访问。特点：采用ASP.NET Core 5.0开发的博客，基于MVC模式界面使用了bootstrap + bootstrap-fluent-design，是极简、带有fluent design风格的简易博客目前完成的功能：文章（markdown支持）评论（valine）动态归档友情链接简易的后台管理接下来可能的目标：改为前后端分离，使用Blazor框架Gitee项目地址：https://gitee.com/cosineg/fluent-blog代码写的很烂，请多多包涵