2024年3月

一、使用BCryptPasswordEncoder加密的值可以解出来吗

Spring Security 中的 BCryptPasswordEncoder 是一种单向加密算法,它是为了安全性考虑而设计的,因此无法从加密后的密码值"解密"出原始密码。这是出于安全目的的设计。

BCryptPasswordEncoder 加密过程是不可逆的,即使你有加密后的密码值,也无法通过任何方式获得原始密码。这种单向加密的目的就是为了防止密码泄露后被破解。

通常,在实际应用中,我们只需要将用户输入的原始密码使用 BCryptPasswordEncoder 进行加密,然后将加密后的密码值存储在数据库中。在用户登录时,将用户输入的密码加密后,与存储在数据库中的加密密码进行比对,如果相同则验证通过,而无需知道原始密码。

因此,你无法从 BCryptPasswordEncoder 加密后的密码值中"解密"出原始密码。这正是它被设计用于密码加密的目的,以提高系统的安全性。如果你需要知道用户的原始密码,建议在注册时不对密码进行加密,或者提供"忘记密码"的功能让用户重置密码。

二、为什么每次BCryptPasswordEncoder加密的值不一样,却可以都匹配上原始密码

这是因为 BCryptPasswordEncoder 在加密时会自动添加一个随机的 Salt 值,这个 Salt 值的作用是使得相同的原始密码在每次加密时产生不同的密文,从而增加密码的安全性。

具体来说,BCryptPasswordEncoder 加密过程包括以下几个步骤:

1. 生成一个随机的 Salt 值,通常是 16 字节长度。
2. 将原始密码和 Salt 值进行合并。
3. 对合并后的值进行 BCrypt 哈希运算,计算出最终的密文。
4. 将 Salt 值和密文拼接在一起,作为最终的加密结果。

因此,每次加密相同的原始密码时,由于使用了不同的随机 Salt 值,所以最终的加密结果也是不同的。

在验证密码时,BCryptPasswordEncoder 会自动从加密结果中提取出 Salt 值,并使用相同的方式对输入的原始密码进行加密,然后比较两个密文是否相同。由于使用了相同的 Salt 值,所以即使每次加密后的结果不同,只要原始密码相同,通过 BCrypt 算法计算出的密文就是相同的。

这种机制不仅增加了密码的安全性,而且还能防止采用相同密码的不同用户的密文相同,从而增加了"彩虹表"攻击的难度。所以即使每次加密结果不同,BCryptPasswordEncoder 依然能够正确比对密码。

三、BCryptPasswordEncoder 加密结果的格式

在 BCryptPasswordEncoder 加密后的结果中,前 29 个字符是包含了 Salt 值和其他算法信息的部分,后面的字符串才是实际的密文部分。

具体来说,BCryptPasswordEncoder 加密结果的格式是:

$算法标识$rounds$salt$密文


其中:

  • `
    $算法标识
    ` 是一个固定的字符串 `$2a$`、`$2b$` 或 `$2y$`等,用于标识使用的哈希算法版本。
  • `
    $rounds
    ` 是一个十进制数字,表示算法迭代的次数。
  • `
    $salt
    ` 是一个 22 个字符长的 Base64 编码的字符串,这就是加密时使用的随机 Salt 值。
  • `
    $密文
    ` 是实际的密文部分,长度为 31 个字符。

例如,一个典型的 BCryptPasswordEncoder 加密结果可能是:

$2a$10$N9qo8uHh24cuh6zT6ZsEfe6Fzen7kNSNBqh.FkQPvQUNhxfrasXTi

在这个加密结果中:

  • `
    $2a$
    ` 表示使用 BCrypt 算法的版本 2a
  • `
    10
    ` 表示迭代次数为 10
  • `
    N9qo8uHh24cuh6zT6ZsEfe
    ` 就是 22 个字符长的随机 Salt 值
  • 最后 31 个字符 `
    6Fzen7kNSNBqh.FkQPvQUNhxfrasXTi
    ` 才是实际的密文部分

所以,BCryptPasswordEncoder 加密结果中,第 4 到第 25 个字符就是随机生成的 Salt 值。每次加密相同的密码,这部分 Salt 值都会不同,从而导致最终的加密结果也不相同。但在验证时,会自动提取出这个 Salt 值用于计算,所以能够正确比对密码。

四、Salt 值和真实密码合并后的值进行 BCrypt 哈希运算,如果知道salt值和最终的密文可以解出原来的密码吗

即使知道了Salt值和最终的密文,也无法通过任何方式反解出原始的密码。

这是因为BCrypt是一种经过加盐(Salting)的单向自适应密钥导出函数(Key Derivation Function),它的设计目的就是使得从密文反推原始密码是计算上不可行的。

具体来说,BCrypt算法包含以下几个关键设计:

1. 使用了Eksblowfish算法作为核心,这是一种密钥设置可变的块加密算法,具有良好的密码学性质。
2. 在密钥设置中引入了可变的迭代次数,默认为2^10次,这使得暴力破解的计算代价大大增加。
3. 在密钥设置中引入了随机的Salt值,使得相同的密码每次产生的密文都不同,防止彩虹表攻击。
4. 密钥设置过程是自适应和可变的,可以根据硬件性能动态调整计算代价。
5. 最终密文使用Base64编码,长度为53个字符,其中包含了Salt和其他算法参数。

因此,即使已知Salt值和密文,要反解出原始密码,仍然需要进行大量的计算和暴力破解。由于BCrypt算法设计的目的就是使这种破解行为在计算上不可行,所以实际上是无法通过任何方式从Salt和密文中反解出原始密码的。

这就是BCrypt作为密码哈希算法的设计理念和安全保证。除非原始密码非常简单且Salt和迭代次数设置较低,否则从BCrypt密文中反解是极其困难的。

Web Audio API 第2章 完美的播放时机控制

相较于

低延时对于游戏或交互式应用来说非常重要,因为交互操作时要快速响应给用户的听觉。如果响应的不及时,用户就会察觉到延时,这种体验相当不好。

在实践中,由于人类听觉的不完美,延迟的余地可达20毫秒左右,但具体延迟多少取决于许多因素。精确的可控时间使得能够在特定时间安排事件。这对于脚本场景和音乐应用来说非常重要

时间模型

其中一个重要的点是,音频上下文 AudioContext 提供了一致的计时模型和时间的帧率。重要的是此模型有别于我们常用的 Javascript 脚本所用的计时器 如 setTimeout, setInterval, new Date()。也有别于 window.performance.now() 提供的性能分析时钟

在 Web Audio API 音频上下文系统坐标中所有你打交道的的绝对时间单位是秒而不是毫秒。当前时间可通过音频上下文的 currentTime 属性获取。同样它也是秒为单位,时间存储为高精度的浮点数存储。

精确的播放与复播

在游戏或其它需要精确时间控制的应用中 start() 方法用于控制安排精确的播放。为了保证正确运行,需要确保缓冲已提前加载。如果没有提前缓冲, 为了 Web Audio API 能解码,需要等等浏览器完成加载音频文件。如果没有加载或解码完毕就去播放或精准的控制播放那么很有可能会失败。

start() 方法的第一个参数可用于声音精确定位控制在哪里开始播放。此参数是 AudioContext 音频上下文坐标系内的 currentTime, 如果传参小于 currentTIme, 则它会立即播放。因为 start(0) 就是直接开始播放的意思 ,如果想要控制延迟 5 秒后播放,则需要 start(context.currentTime + 5)。

声音的缓冲也可以从特定位置开始播放,使用 start() 方法的第二个参数控制,第三个可选参数用于时长特殊限制。举个例子,如果我们想暂停后在暂停的位置重新开始恢复播放,我们需要实现记录声音在当前 session 播放了多久并追踪偏移量用于后面恢复播放

start 方法即 AudioBufferSourceNode.start([when][, offset][, duration]);

可参考
https://developer.mozilla.org/zh-CN/docs/Web/API/AudioBufferSourceNode/start

// 假定 context 是网页 audio context 上下文
var startOffset = 0; 
var startTime = 0;
function pause() {
  source.stop();
  // 计算距离上次播放暂停时过去了多久
  startOffset += context.currentTime - startTime;
}

一旦源节点播放完毕,它无法再重播。为了重播底层的缓冲区,你需要新建一个新的源节点(AudioBufferSourceNode) 并调用 start():

function play() {
  startTime = context.currentTime;
  var source = context.createBufferSource();
  source.buffer = this.buffer;
  source.loop = true;
  source.connect(context.destination);
  // 开始播放,但确保我们限定在 buffer 缓冲区的范围内 
  source.start(0, startOffset % buffer.duration);
  
}

尽管重新新建一个源节点看起来非常的低效,牢记,这种模式下源节点被着重优化过了。请记住,如果你在处理 AudioBuffer, 播放同一个声音你无需重新请求资源。当 AudioBuffer 缓冲区与播放功能被分拆后,就可以实现同一时间内播放不同版本的缓冲区。如果你感觉需要重复这样的方式调用 ,那么你可以在将它封装成一个简单的方法函数比如 playSound(buffer) 就像在第一章代码片断中有提到过的。

以上代码实现 demo 可参考 examples/ch02/index1.html

规划精确的节奏

Web Audio API 允许开发人员在精确地规划播放。为了演示,让我们先设置一个简单的节奏轨道。也许最简单的要属广为人知的 爵士鼓模式(drumkit pattern) 如图 2-1,hihat每8个音符演奏一次,kick和snare在四分音符上交替演奏

注: kick 是底鼓,就是架子鼓组里面最下面最大的那个鼓,声音是咚咚咚的;

hihat是鼓手左边两片合在一起的镲片 闭镲 是次次次的声音,开镲是擦擦擦的声音

snare 是离鼓手最*的*放的小鼓,叫军鼓,打上去是咔咔咔的声音;

image

假定我们已搞定了 kick, snare,和 hihat 缓冲,那么代码实现就比较简单:

for (var bar = 0; bar < 2; bar++) {
  var time = startTime + bar * 8 * eighthNoteTime; 
  // Play the bass (kick) drum on beats 1, 5 
  playSound(kick, time);
  playSound(kick, time + 4 * eighthNoteTime);
  // Play the snare drum on beats 3, 7
  playSound(snare, time + 2 * eighthNoteTime);
  playSound(snare, time + 6 * eighthNoteTime);
  // Play the hihat every eighth note.
  for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
  } 
}

代码中对时间进行硬编码是不明智的。所以如果你正在处理一个快速变化的应用程序,那是不可取的。处理此问题的一个好方法是使用JavaScript计时器和事件队列创建自己的调度器。这种方法在《双钟的故事》中有描述

译者注:《双钟的故事》即 《A Tale of Two Clocks》 寓言故事大致告诉人们不能依靠单独一种方式,需要依靠多种方式方法解决问题

译者注:具体音乐原理不重要,重要的是反应出可对音频延时播放, 听的就是个“动次打次”

以上代码实现 demo 可参考
https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch02/index2.html

更改音频参数

很多音频节点类形的参数都是可配的。举个例子,GainNode 拥有 gain 参数用于控制通过 gain 节点的声音音量乘数。特别的一点是,参数如果是1则不影响幅度,0.5 降一半,2 则是双倍。让我们设置一个:

译者注: gain节点或称增益节点通常用于调节音频信号的音量

  // 创建 gain node.
  var gainNode = context.createGain();
  // 连接  source 到 gain node. 
  source.connect(gainNode);
  // 连接  gain node 至  destination. 
  gainNode.connect(context.destination);

在 context API 中,音频参数用音频实例表示。这些值可通过节点直接变更:

// 减小音量
gainNode.gain.value = 0.5;

当然也可以晚一点修改值,通过精确安排在后续更改。我们也可以使用 setTimeout 来延时修改,但它不够精确,原因有几点:

  1. 毫秒基数的计时可能不够精确
  2. 主 JS 进程可能很忙需要处理更高优先级的任务比如页面布局,垃圾回收以及其它 API 可能导致延时的回调函数队列等
  3. JS 计时器可能会受到浏览器 tab 的状态影响。举个例子,interval 计时器相比于 tab 在前台运行时,tab 在后台运行时触发的更慢

我们可以直接调用 setValueAtTime() 方法来代替直接设值,它需要一个值与开始时间作为参数。举个例子,下面的代码片断一秒就搞定了 GainNode的 gain 值设置

gainNode.gain.setValueAtTime(0.5, context.currentTime + 1);

以上代码实现 demo 可参考
https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch02/index3.html

渐变的音频参数

在很多例子中,相较于直接硬生生设置一个参数,你可能更倾向于渐变设值。举个例子,当开发音乐应用时,我们希望当前的声音轨道渐隐,然后新的声音轨道渐入而避免生硬的切换。当然你也可以利用多次调用 setValueAtTime 函数的方法实现类似的效果,但显然这种方法不太方便。

Web Audio API 提供了一个堆方便的 RampToValue 方法,能够渐变任何参数。 它们是 linearRampToValueAtTime() 和 exponentialRampToValueAtTime()。两者的区别在于发生变换的方式。在一些用例中,exponential 变换更加敏感,因为我们以指数方式感知声音的许多方面。

让我们用一个例子来展示交叉变换吧。给定一个播放列表,我们可以在音轨间安排变换降低当前播放的音轨音量并且增加下一条音轨的音量。两者都发生在当前曲目结束播放之前稍早的时候:

function createSource(buffer) {
  var source = context.createBufferSource(); 
  var gainNode = context.createGainNode(); 
  source.buffer = buffer;
  // Connect source to gain. 
  source.connect(gainNode);
  // Connect gain to destination. 
  gainNode.connect(context.destination);
  return {
    source: source, 
    gainNode: gainNode
  }; 
}

function playHelper(buffers, iterations, fadeTime) { 
  var currTime = context.currentTime;
  for (var i = 0; i < iterations; i++) {
    for (var j = 0; j < buffers.length; j++) { 
      var buffer = buffers[j];
      var duration = buffer.duration;
      var info = createSource(buffer);
      var source = info.source;
      var gainNode = info.gainNode;
      // 渐入
      gainNode.gain.linearRampToValueAtTime(0, currTime); 
      gainNode.gain.linearRampToValueAtTime(1, currTime + fadeTime);
      // 渐出
      gainNode.gain.linearRampToValueAtTime(1, currTime + duration-fadeTime);
      gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
      // 播放当前音频.
      source.noteOn(currTime);
      // 为下次迭代累加时间
      currTime += duration - fadeTime;
    }
  } 
}

译者注: 原文中的代码过时了, 实际实现请参考我的 demo 实现

标准
https://webaudio.github.io/web-audio-api/#dom-gainnode-gain

以上代码实现 demo 可参考
https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch02/index4.html

定制时间曲线

如果线性曲线和指数曲线都无法满足你的需求,你也可以自己定制自己的曲线值通过传递一个数组给 setValueCurveAtTime 函数实现。有了这个函数,你可以通过传递数组实现自定义时间曲线。它是创建一堆 setValueAtTime 函数调用的快捷调用。举个例子,如果我们想创建颤音效果,我们可以通过传递振荡曲线作为 GainNode 的 gain 参数值,如图 2-2

image

上图的振荡曲线实现代码如下:


var DURATION = 2; 
var FREQUENCY = 1; 
var SCALE = 0.4;
// Split the time into valueCount discrete steps.
var valueCount = 4096;
// Create a sinusoidal value curve.
var values = new Float32Array(valueCount); 
for (var i = 0; i < valueCount; i++) {
  var percent = (i / valueCount) * DURATION*FREQUENCY;
  values[i] = 1 + (Math.sin(percent * 2*Math.PI) * SCALE);
  // Set the last value to one, to restore playbackRate to normal at the end. 
  if (i == valueCount - 1) {
    values[i] = 1;
  }
}
// Apply it to the gain node immediately, and make it last for 2 seconds.
this.gainNode.gain.setValueCurveAtTime(values, context.currentTime, DURATION);

上面的代码片断我们手动计算出了正弦曲线并将其设置到 gain 的参数内创造出颤音效果。好吧,它用了一点点数学..

这给我们带来了 Web Audio API 的一个非常重要的特性, 它使得我们创建像颤音这样的特效变的非常容易。这个重要的点子是很多音频特效的基础。上述的代码实际上是被称为低频振荡(LFO)效果应用的一个例子, LFO 经常用于创建特效,如 vibrato 震动 phasing 分队 和 tremolo 颤音。通过对音频节点应用振荡,我们很容易重写之前的例子:

// Create oscillator.
var osc = context.createOscillator(); 
osc.frequency.value = FREQUENCY;
var gain = context.createGain(); 
gain.gain.value = SCALE; 
osc.connect(gain); 
gain.connect(this.gainNode.gain);
// Start immediately, and stop in 2 seconds.
osc.start(0);
osc.stop(context.currentTime + DURATION);

createOscillator
https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode

相比于我们之前创建的自定义曲线后面的代码要更高效,重现了效果但它帮我们省了用手动循环创建正弦函数

以上振荡器节点的代码实现 demo 可参考
https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch02/index5.html


注:转载请注明出处博客园:王二狗Sheldon池中物 (willian12345@126.com)

“ 亏功一篑,未成丘山。凿井九阶,不次水泽。行百里者半九十,小狐汔济濡其尾。故曰时乎,时不再来。终终始始,是谓君子。”

01


前言

随着Android智能驾舱系统的普及各种信息交互、影音娱乐场景应用的不断创新, 需要Android Framework开发人员更深入地了解多屏同显/异显的基本原理。从这篇文章开始,我们将从简单的C++多屏显示的示例开始,同大家从浅入深、从表入里,初步剖析安卓多屏显示的基本原理。

在前期的文章中,我们曾对安卓多屏显示做过一些探索和准备。

如何模拟多屏显示以方便开发测试,给出了两种方式:

1. 使用Emulator来创建虚拟屏幕

Android Emulator - 模拟器多屏输出

2. 模拟多个物理显示屏

Android Graphics 显示系统 - 如何模拟多(物理)显示屏?

也曾写过简单的demo来演示多屏互动的效果

1. 演示如何把画面显示到指定的屏幕上

Android 多屏显示 -- 从一个简单的Demo开始

2. 复杂的多屏互动的demo,多指触控下移动画面

Android Graphics 显示系统 - 多屏互动演示

早前写过的demo要么过于简单,无法完整展现关键功能;要么过于复杂,无法聚焦Graphics多屏显示核心原理。为此,特意写作了C++版本的多屏同显/异显的演示程序,直接调用Surface/BufferQueue/SurfaceComposer的C++ APIs来实现多屏互动的效果。这样我们可以抛开复杂的WMS、AMS等模块复杂的逻辑,聚焦底层多屏显示的基本原理。

02


多屏互动Demo效果展示

C++版本的多屏互动程序提供两个版本:标准版和升级版。

标准版多屏互动Demo

标准版Demo是在早前Surface绘图示例Demo的基础上加上了多屏显示的功能,在交替显示R/G/B纯色图像的同时可以接收实时输入的命令进行多屏互动。

早前Surface绘图示例Demo可以参考:

Android Graphics 显示系统 - Surface绘图示例(四)

Android Graphics 显示系统 - Surface绘图示例(五)

多屏同显

多屏异显(画面拼接)

升级版多屏互动Demo

升级版Demo是在标准版程序基础上加上了视频播放功能,场景展示上更加生动

多屏同显

多屏异显(画面拼接)

03


多屏系列文章基本内容

Android Graphics多屏同显异显系列文章是Graphics系列文章的延续与扩展,我们将开始讲解多屏显示相关的基本原理和常用功能的接口使用。

该系列文章会从上面提到的两个多屏互动的Demo开始,分享、分析、讲解Demo程序设计的思路、原理。从看得到的API入手,探讨相关功能实现所依赖的接口功能逻辑,研究解析SurfaceFlinger在多屏幕下对Layer的处理逻辑,探究HWC在多屏下的行为等。

同时,结合开发过程中遇到的问题及现象,讲解如何通过dumpsys SurfaceFlinger来查看layer的信息进行debug。

阅读原文:

Android Graphics 多屏同显/异显 - 开篇


欢迎关注公众号,阅读系列文章,获取源码!

SystemEvents
是一个开发 win32 窗口项目很常用的类,其中封装了一些常用的系统广播消息。在 WinUI3 项目中,SystemEvents 事件经常无法触发,简单排查了一下原因。

SystemEvent 内封装了一个线程和一个窗口,通过窗口消息在内部线程上调用事件,内部使用了
SystemEventInvokeInfo
对象来保存委托,
RaiseEvent
方法遍历调用保存的
SystemEventInvokeInfo.Invoke
方法来触发事件。

public SystemEventInvokeInfo(Delegate d)
{
    _delegate = d;
    _syncContext = AsyncOperationManager.SynchronizationContext;
}
// fire the given event with the given params.
public void Invoke(bool checkFinalization, params object[] args)
{
    try
    {
        // If we didn't get call back invoke directly.
        if (_syncContext == null)
        {
            InvokeCallback(args);
        }
        else
        {
            // otherwise tell the context to do it for us.
            _syncContext.Send(new SendOrPostCallback(InvokeCallback), args);
        }
    }
    catch (InvalidAsynchronousStateException)
    {
        // if the synch context is invalid -- do the invoke directly for app compat.
        // If the app's shutting down, don't fire the event (unless it's shutdown).
        if (!checkFinalization || !AppDomain.CurrentDomain.IsFinalizingForUnload())
        {
            InvokeCallback(args);
        }
    }
}

我们可以注意到
SystemEventInvokeInfo.Invoke
判断了 _syncContext 变量,_syncContext 变量在 SystemEventInvokeInfo 构造时捕获,Invoke 时使用
_syncContext.Send
方法调用。

/// <summary>
/// DispatcherQueueSyncContext allows developers to await calls and get back onto the
/// UI thread. Needs to be installed on the UI thread through DispatcherQueueSyncContext.SetForCurrentThread
/// </summary>
public class DispatcherQueueSynchronizationContext : SynchronizationContext
{
    private readonly DispatcherQueue m_dispatcherQueue;

    public DispatcherQueueSynchronizationContext(DispatcherQueue dispatcherQueue)
    {
        m_dispatcherQueue = dispatcherQueue;
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        if (d == null)
            throw new ArgumentNullException(nameof(d));

        m_dispatcherQueue.TryEnqueue(() => d(state));
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        throw new NotSupportedException("Send not supported");
    }

    public override SynchronizationContext CreateCopy()
    {
        return new DispatcherQueueSynchronizationContext(m_dispatcherQueue);
    }
}

而 WinUI3 的 UI 线程的默认 SynchronizationContext 为
DispatcherQueueSynchronizationContext
,简单查看源码可以发现
DispatcherQueueSynchronizationContext.Send
并未实现,而是直接抛出了异常,所以从 UI 线程注册的 SystemEvents 事件默认是不会触发的。

解决方案也很简单:

SystemEvents.InvokeOnEventsThread(() =>
{
    // 不需要设置,因为默认就是null
    //SynchronizationContext.SetSynchronizationContext(null);

    SystemEvents.DisplaySettingsChanged += (s, a) =>
    {
        Debug.WriteLine("DisplaySettingsChanged");
    };
});

我们借用一下 SystemEvents 的内部线程,在此线程上注册事件时 SystemEventInvokeInfo 捕获不到 SynchronizationContext,就会在 SystemEvents 内部线程上触发事件,自然就能正常触发了。

gRPC入门学习之旅(一)

2.3、创建自定义服务

除上面的模板中自带的一个gRPC服务之后,我们再创建一个自己的服务,我将创建一个用户信息gRPC服务,主要功能有三个,登录、获取用户信息、修改用户信息。

  • 创建UserInfo.proto协议文件
  1. 在“解决方案资源管理器”中,使用鼠标左键选中“Protos”文件夹,然后在菜单栏上,依次选择“添加-->新建项”。如下图。

  1. 在“添加新项”对话框中,选择“ASP.NET Core-->常规”节点,然后选择“协议缓冲区文件”项。
  2. 在“名称”文本框中,输入 UserInfo.proto,然后选择“添加”按钮。如下图。

4.Visual Studio 2022会在Protos目录中添加一个UserInfo.proto协议文件。如下图。

5.这个UserInfo.proto文件中的代码如下:

syntax = "proto3";



option csharp_namespace
= "Demo.GrpcService.Protos";//包名 package user;//服务名称 service UserInfo{//方法名称 rpc GetUserInfo (UserInfoRequest) returns (UserInfoResult);

rpc Login(UserInfoRequest) returns(UserInfoResult);

rpc Save(UserInfoRequest) returns (UserInfoResult);

}
//请求的参数对象 message UserInfoRequest{string UserName = 1;string Password=2;

}
//请求响应的对象message UserInfoResult{string UserName = 1;string Password=2;

int32 Age
= 3;string Tel=4;string Name=5;

int32 Sex
=6;string City=7;bool IsLogin=8;

}
  • 添加userinfo.proto协议文件的关联配置

6.在“解决方案资源管理器”中,使用鼠标左键选中项目名称“Demo.GrpcService”,然后使用鼠标双击这个名称,Visual Studio 2022将在文本编辑器中打开项目文件。 如下图。

7. 在文本编辑器中找到ItemGroup节点,在
ItemGroup
节点中增加如下配置:如下图。

 <ItemGroup>

    <ProtobufInclude="Protos\greet.proto"GrpcServices="Server" />

      <ProtobufInclude="Protos\UserInfo.proto"GrpcServices="Server" />

  </ItemGroup>

8.在“解决方案资源管理器”中,使用鼠标左键选中项目名称“Demo.GrpcService”,然后单击鼠标右键,在弹出的快捷菜单中选择“重新生成”菜单项。如下图。

9.我们打开“文件资源管理器”,进入到Demo.Grpc.Service
\obj\Debug\net7.0\Protos
目录,发现此时目录下比刚才多了2个.cs文件,就是我们自己定义的UserInfo.proto协议文件对应的类文件,如下图所示: