您现在的位置是:首页 > 正文

c#:grpc初体验

2024-04-01 00:32:47阅读 3

环境:

  • window10x64 企业版
  • vs2019 16.7.7
  • .net core3.1

参照:
《ASP.NET Core 3.0 使用gRPC》
《Asp.Net Core Grpc使用C#对象取代Proto定义》

实验代码下载:
https://download.csdn.net/download/u010476739/13090566

一、什么是grpc?

首先,RPC(Remote Procedure Call)是用来实现不同服务之间的调用的。像之前的microsoft .net remoting、WCF、WebService都是这类功能。
然后,随着http和web前端的迅速发展,也越来越多的服务接口采用restful设计。
现在,微服务概念盛行,伴随着而来的就是微服务之间的互相调用,那么google就自己开发了一套微服务间通信的标准,称之为:GRPC。

二、grpc有什么特点?

  • 现代高性能轻量级 RPC 框架。
  • 使用 HTTP/2 进行传输
  • 通过Protocol Buffers二进制序列化减少网络使用
  • 约定优先的 API 开发,默认使用 Protocol Buffers 作为描述语言,允许与语言无关的实现。

三、创建一个grpc服务

新建一个空白解决方案:grpc-trial,然后新建一个空的web工程grpc-server1,如下:
在这里插入图片描述
修改grpc-server1工程:

添加nuget包Grpc.AspNetCore

<ItemGroup>
  <PackageReference Include="Grpc.AspNetCore" Version="2.27.0" />
</ItemGroup>

我们在解决方案目录下新建文件夹Protos
在这里插入图片描述

在这个文件夹下,新建文件TestServer.proto,内容如下:

syntax = "proto3";

// 命名空间
option csharp_namespace = "Protos";

package Protos;

// 服务接口
service TestServer {
  // 接口内的方法
  rpc SayHello (HelloRequest) returns (HelloReply);
}

// 请求参数模型
message HelloRequest {
  string name = 1;
}

// 响应参数模型
message HelloReply {
  string message = 1;
}

将文件夹Protos下的内容全部包含到grpc-server1工程下:

<ItemGroup>
	<Protobuf Include="..\Protos\**\*.proto" GrpcServices="Server" Link="Protos\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

效果如下图所示:
在这里插入图片描述
在工程上新建服务类TestServer,如下图所示:
在这里插入图片描述
服务类的代码如下:

using Grpc.Core;
using Microsoft.Extensions.Logging;
using Protos;
using System.Threading.Tasks;

namespace grpc_server1.GrpcServices
{
    public class TestServer : Protos.TestServer.TestServerBase
    {
        private readonly ILogger<TestServer> _logger;
        public TestServer(ILogger<TestServer> logger)
        {
            _logger = logger;
        }

        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
			_logger.LogInformation($"接受到了请求:{request.Name}");
            return Task.FromResult(new HelloReply
            {
                Message = "Hello " + request.Name
            });
        }
    }
}

Protos.TestServer.TestServerBase这个类是怎么来的?vs根据TestServer.proto文件自动生成的。

由于grpc是用的http2,所以我们在appsettings.json中配置Kestrel使用http2:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http2"
    }
  }
}

现在,让我们在web程序中引入grpcStartup.csConfigureServices中注入grpc服务:

 public void ConfigureServices(IServiceCollection services)
 {
     services.AddGrpc();
 }

Configure方法中,注入TestServer服务

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGrpcService<TestServer>();
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

好了,现在让我们编译并启动它吧!
在这里插入图片描述

四、创建一个客户端,调用grpc服务

在解决方案上新建.net core控制台项目TestClient1
修改工程TestClient1

引入nuget包Grpc.Net.ClientGoogle.Protobuf以及Grpc.Tools

<ItemGroup>
  <PackageReference Include="Google.Protobuf" Version="3.13.0" />
  <PackageReference Include="Grpc.Net.Client" Version="2.33.1" />
  <PackageReference Include="Grpc.Tools" Version="2.33.1">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
</ItemGroup>

然后也将Protos文件夹下的*.proto文件包含进来(注意GrpcServices属性的值):

<ItemGroup>
	<Protobuf Include="..\Protos\**\*.proto" GrpcServices="Client" Link="Protos\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

最终如下:
在这里插入图片描述
在Program.cs中编写代码:

using Grpc.Net.Client;
using Protos;
using System;
using System.Threading.Tasks;

namespace TestClient1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client = new TestServer.TestServerClient(channel);

            var reply = await client.SayHelloAsync(
                new HelloRequest { Name = "jackletter" });
            Console.WriteLine("Greeter 服务返回数据: " + reply.Message);
            Console.WriteLine("ok");
            Console.ReadLine();
        }
    }
}

五、测试效果

编译并运行工程TestClient1如下:

在这里插入图片描述

而grpc服务端输出如下:
在这里插入图片描述

到此,一个简单的grpc调用就完成了。

六、给客户端和服务端加上拦截器

拦截器就像MVC的过滤器或者是ASP.NET Core middleware 一样,具有面向切面的思想,可以在调用服务的时候进行一些统一处理, 很适合在这里处理验证、日志等流程。

Interceptor类是gRPC服务拦截器的基类,是一个抽象类,它定了几个虚方法,分别如下:

//拦截阻塞调用
public virtual TResponse BlockingUnaryCall<TRequest, TResponse>();
//拦截异步调用
public virtual AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>();
//拦截异步服务端流调用
public virtual AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>();
//拦截异步客户端流调用
public virtual AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>();
//拦截异步双向流调用
public virtual AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>();
//用于拦截和传入普通调用服务器端处理程序
public virtual Task<TResponse> UnaryServerHandler<TRequest, TResponse>();
//用于拦截客户端流调用的服务器端处理程序
public virtual Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>();
//用于拦截服务端流调用的服务器端处理程序
public virtual Task ServerStreamingServerHandler<TRequest, TResponse>();
//用于拦截双向流调用的服务器端处理程序
public virtual Task DuplexStreamingServerHandler<TRequest, TResponse>();

6.1 客户端拦截器

TestClient1工程上新建类:ClientLoggerInterceptor

public class ClientLoggerInterceptor : Interceptor
{
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        LogCall(context.Method);
        return continuation(request, context);
    }
    private void LogCall<TRequest, TResponse>(Method<TRequest, TResponse> method)
    where TRequest : class
    where TResponse : class
    {
        var initialColor = Console.ForegroundColor;
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"Starting call. Type: {method.Type}. Request: {typeof(TRequest)}. Response: {typeof(TResponse)}");
        Console.ForegroundColor = initialColor;
    }
}

在客户端上注册拦截器,修改Program代码:

class Program
{
    static async Task Main(string[] args)
    {
        var channel = GrpcChannel.ForAddress("https://localhost:5001");
        var invoker = channel.Intercept(new ClientLoggerInterceptor());
        var client = new TestServer.TestServerClient(invoker);

        var reply = await client.SayHelloAsync(
            new HelloRequest { Name = "jackletter" });
        Console.WriteLine("Greeter 服务返回数据: " + reply.Message);
        Console.WriteLine("ok");
    }
}

6.2 服务端拦截器

grpc-server1工程上新建类ServerLoggerInterceptor,如下:

public class ServerLoggerInterceptor:Interceptor
{
    private readonly ILogger<ServerLoggerInterceptor> logger;

    public ServerLoggerInterceptor(ILogger<ServerLoggerInterceptor> logger)
    {
        this.logger = logger;
    }
    public override Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
    {
        LogCall<TRequest, TResponse>(MethodType.Unary, context);
        return continuation(request, context);
    }

    private void LogCall<TRequest, TResponse>(MethodType methodType, ServerCallContext context)
    where TRequest : class
    where TResponse : class
    {
        logger.LogWarning($"Starting call. Type: {methodType}. Request: {typeof(TRequest)}. Response: {typeof(TResponse)}");
    }
}

在服务端上注册拦截器,修改StartupConfigureServices方法代码:

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<ServerLoggerInterceptor>();
    });
}

然后,编译并先后运行服务端和客户端,输出如下:

服务端:
在这里插入图片描述

客户端:
在这里插入图片描述

七、grpc流式调用

7.1 什么是grpc流?

gRPC 有四种服务类型,分别是:简单 RPC(Unary RPC)、服务端流式 RPC (Server streaming RPC)、客户端流式 RPC (Client streaming RPC)、双向流式 RPC(Bi-directional streaming RPC)。它们主要有以下特点:

  • 简单 RPC

    一般的rpc调用,传入一个请求对象,返回一个返回对象

  • 服务端流式 RPC

    传入一个请求对象,服务端可以返回多个结果对象

  • 客户端流式 RPC

    客户端传入多个请求对象,服务端返回一个结果对象

  • 双向流式 RPC

    结合客户端流式RPC和服务端流式RPC,可以传入多个请求对象,返回多个结果对象

gRPC 通信是基于 HTTP/2 实现的,它的双向流映射到 HTTP/2 流。HTTP/2 具有流的概念,流是为了实现HTTP/2的多路复用。关于http2的流概念参考:《HTTP/2笔记之流和多路复用》

7.2 写一个双向流功能

注:一开始我们写的属于简单调用。
下面我们要写一个支持双向流调用的方法。当我们调用这个方法后,我们就打开了一个双向流管道,我们可以向这个管道中写入东西,同时也可以从这个管道中读取东西(同时读写肯定需要两个线程的!)。

我们新建一个proto文件:TestStream.proto,将它放到TestServer.proto的同目录,内容如下:

syntax = "proto3";

// 命名空间
option csharp_namespace = "Protos";

package Protos;

// 服务接口
service TestStream {
  // 接口内的方法
  rpc StreamRequest (stream StreamReq) returns (stream StreamRes);
}

// 请求参数模型
message StreamReq {
  string name = 1;
}

// 响应参数模型
message StreamRes {
  string message = 1;
}

然后,我们在服务端新建类TestStream,位置如下:
在这里插入图片描述
服务类的代码如下:

namespace grpc_server1.GrpcServices
{
    public class TestStream : Protos.TestStream.TestStreamBase
    {
        private static int count = 0;
        private readonly ILogger<TestStream> logger;

        public TestStream(ILogger<TestStream> logger)
        {
            this.logger = logger;
        }

        public override async Task StreamRequest(IAsyncStreamReader<StreamReq> req, IServerStreamWriter<StreamRes> res, ServerCallContext context)
        {
            var cur = Interlocked.Increment(ref count);
            logger.LogInformation($"开始接受流式请求:{cur}");
            while (await req.MoveNext())
            {
                StreamReq currentReq = req.Current;
                logger.LogInformation($"在流式请求{cur}中,接受到了请求:{currentReq.Name}...");
                await res.WriteAsync(new StreamRes()
                {
                    Message = $"请求序列:{cur},请求参数:{currentReq.Name}"
                });
            }
            logger.LogInformation($"处理流请求{cur}完毕!");
        }
    }
}

然后,我们将这个grpc服务像TestServer一样注册进去(Startup.cs):

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
     if (env.IsDevelopment())
     {
         app.UseDeveloperExceptionPage();
     }

     app.UseRouting();

     app.UseEndpoints(endpoints =>
     {
         endpoints.MapGrpcService<TestServer>();
         endpoints.MapGrpcService<TestStream>();
         endpoints.MapGet("/", async context =>
         {
             await context.Response.WriteAsync("Hello World!");
         });
     });
 }

现在,我们修改客户端的调用代码(main方法),修改后如下:

class Program
{
    static async Task Main(string[] args)
    {
        var channel = GrpcChannel.ForAddress("https://localhost:5001");
        var invoker = channel.Intercept(new ClientLoggerInterceptor());
        //rpc简单调用=======================开始============================
        var client = new TestServer.TestServerClient(invoker);

        var reply = await client.SayHelloAsync(
            new HelloRequest { Name = "jackletter" });
        Console.WriteLine("Greeter 服务返回数据: " + reply.Message);
        //rpc简单调用========================结束============================

        //rpc双向流调用=======================开始===========================
        var streamClient = new TestStream.TestStreamClient(invoker);
        var streamCall = streamClient.StreamRequest();
        //定义流式响应处理逻辑
        var task = Task.Run(async () =>
          {
              await foreach (var resp in streamCall.ResponseStream.ReadAllAsync())
              {
                  Console.WriteLine($"收到了消息:{resp.Message}");
              }
          });
        //发送几段流式数据
        Console.WriteLine("客户端开始发送5段数据...");
        var names = new string[] { "name1", "name2", "name3", "name4", "name5" };
        for (int i = 0; i < 5; i++)
        {
            await streamCall.RequestStream.WriteAsync(new StreamReq() { Name = names[i] });
            Console.WriteLine($"客户端发送请求段完毕:{names[i]}");
            //查看控制台接受的返回
            await Task.Delay(800);
        }
        //发送完毕
        await streamCall.RequestStream.CompleteAsync();
        Console.WriteLine("客户端已发送完5段数据!");
        //等待响应结束
        await task;
        Console.WriteLine("双向流调用完成!");
        //rpc双向流调用===============================结束========================

        Console.WriteLine("ok");
        Console.ReadLine();
    }
}

7.3 测试双向流效果

编译上面两个工程,先运行服务端,然后运行客户端,效果如下图:
在这里插入图片描述

八、使用c#代码代替*.proto文件

上面的例子都是使用*.proto文件定义服务接口,这样,当我们给别人使用的时候,我们需要将*.proto文件共享给别人,然而当彼此双方都是.Neter的时候,是否直接提供给别人*.dll文件更方便呢?下面就来实验使用*.dll文件…

先准备一个空白解决方案GrpcDemo

8.1 准备接口

新建.net standard 2.1工程ShareDllInterface

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.ServiceModel.Primitives" Version="4.8.1" />
  </ItemGroup>
</Project>

定义接口如下(IHelloService.cs):

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace ShareDllInterface
{
    [ServiceContract]
    public interface IHelloService
    {
        [OperationContract]
        ResponseMessage SayHello(RequestMessage req);

        [OperationContract]
        IAsyncEnumerable<ResponseMessage> StreamDemo(IAsyncEnumerable<RequestMessage> stream);

    }

    [DataContract]
    public class RequestMessage
    {
        [DataMember(Order = 1)]
        public string Name { get; set; }
    }

    [DataContract]
    public class ResponseMessage
    {
        [DataMember(Order = 1)]
        public string Message { set; get; }
    }
}

8.2 准备grpc服务

新建asp.net core空web工程ShareDllGrpcServer,并添加到工程ShareDllInterface的引用,然后添加protobuf-net.Grpc.AspNetCore包引用,最终*.csproj文件如下:

<Project Sdk="Microsoft.NET.Sdk.Web">
	<PropertyGroup>
		<TargetFramework>netcoreapp3.1</TargetFramework>
	</PropertyGroup>
	<ItemGroup>
		<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.123" />
	</ItemGroup>
	<ItemGroup>
		<ProjectReference Include="..\ShareDllInterface\ShareDllInterface.csproj" />
	</ItemGroup>
</Project>

新建类HelloService实现IHelloService服务:

using Microsoft.Extensions.Logging;
using ShareDllInterface;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace ShareDllGrpcServer
{
    public class HelloService : IHelloService
    {
        private readonly ILogger<HelloService> logger;

        public HelloService(ILogger<HelloService> logger)
        {
            this.logger = logger;
        }
        public ResponseMessage SayHello(RequestMessage req)
        {
            logger.LogInformation($"接收到了SayHello调用:{req?.Name}");
            return new ResponseMessage()
            {
                Message = $"hello,{req?.Name}!"
            };
        }

        private int count = 0;
        public async IAsyncEnumerable<ResponseMessage> StreamDemo(IAsyncEnumerable<RequestMessage> stream)
        {

            int cur = Interlocked.Increment(ref count);
            logger.LogInformation($"接收到了StreamDemo调用:{cur}...");
            await foreach (var req in stream)
            {
                logger.LogInformation($"收到了消息:P{req.Name}");
                await Task.Delay(800);
                var res = new ResponseMessage()
                {
                    Message = $"流式调用{cur}:{req?.Name}"
                };
                yield return res;
            }
            logger.LogInformation($"客户端关闭流式调用:{cur}");
        }
    }
}

将grpc服务注入到容器中,并开启到HelloService的映射,修改Startup.cs即可,最终如下:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ProtoBuf.Grpc.Server;

namespace ShareDllGrpcServer
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            //添加Grpc服务
            services.AddCodeFirstGrpc();
        }


        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<HelloService>();
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }
    }
}

最后,不要忘了grpc采用的是http2,所以我们在appsettings.json文件中配置Kestrel

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http2"
    }
  }
}

好了,现在grpc服务就准备完了。

8.3 准备grpc客户端

新建.net core控制台工程ShareDllClient,添加如下包引用:

<ItemGroup>
	<PackageReference Include="Grpc.Net.ClientFactory" Version="2.23.2" />
	<PackageReference Include="protobuf-net.Grpc" Version="1.0.123" />
</ItemGroup>

添加到ShareDllInterface工程的引用:

<ItemGroup>
	<ProjectReference Include="..\ShareDllInterface\ShareDllInterface.csproj" />
</ItemGroup>

我们也可以通过nuget包或直接引用dll文件。

写客户端代码如下:

using Grpc.Net.Client;
using ProtoBuf.Grpc.Client;
using ShareDllInterface;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ShareDllClient
{
    class Program
    {
        static async Task Main(string[] args)
        {
            ProtoBuf.Grpc.Client.GrpcClientFactory.AllowUnencryptedHttp2 = true;

            using var http = GrpcChannel.ForAddress("http://localhost:5000");
            var client = http.CreateGrpcService<IHelloService>();

            //简单测试
            var req = new RequestMessage()
            {
                Name = "小明"
            };
            Console.WriteLine($"发送简单消息:{req.Name}");
            var res = client.SayHello(req);
            Console.WriteLine($"收到了响应:{res.Message}");

            //流式调用
            await foreach (var reply in client.StreamDemo(SendPackage()))
            {
                Console.WriteLine($"收到流式响应:{reply.Message}");
            }
            Console.WriteLine("流式调用已结束!");
            Console.ReadLine();

        }
        static int count = 1;
        private static async IAsyncEnumerable<RequestMessage> SendPackage()
        {
            for (int i = 0; i < 5; i++)
            {
                var request = new RequestMessage
                {
                    Name = "name" + count
                };
                Console.WriteLine($"发送流式消息:{request.Name}");
                yield return request;
            }
        }
    }
}

8.4 测试效果

让我们编译并先后运行ShareDllGrpcServerShareDllClient工程吧:
在这里插入图片描述

至此,我们的初体验完成!

网站文章

  • django 使用默认django数据库创建数据库

    django 使用默认django数据库创建数据库

    django有默认自带的数据库,当然也可以用其他的数据库,修改数据库的方式也很简单是需要在setting.py修改 DATABASES这里面的参数就可以了 DATABASES = { &#39;def...

    2024-04-01 00:32:21
  • centos boot dvd版本_华为LTE认证考试题库(Word版)

    centos boot dvd版本_华为LTE认证考试题库(Word版)

    资料共计129页,篇幅有限,本文只列举少部分试题。如需要原文档:请关注本公众号,并将本文分享到朋友圈或者是通信群组。分享后添加微信(微信号:tongxinzixun123)领取。这些资料是我从各大通信论坛上面所收集,版权归原作者所有,本号只提供分享,供通信人学习交流用,禁止用于商业等用途,谢谢大家的理解与支持!更多精彩内容,请点击下面链接查看并领取:4G网络优化典型案例(PPT版)5G现...

    2024-04-01 00:32:12
  • 苹果手机无法解析html,苹果手机故障全解析

    苹果手机无法解析html,苹果手机故障全解析

    苹果手机的的故障问题虽然有很多,不过依旧无法阻挡果粉们对它的爱。今天小编就为大家总结和介绍一下苹果手机都有哪些故障和解决办法!手机不像电脑是靠自身散热,大家最直观的感受就是手机操作一会发烫,怎么解决?...

    2024-04-01 00:32:04
  • 什么是美颜SDK?

    什么是美颜SDK?

    首先,美摄美颜SDK拥有丰富的美颜算法库,可以满足用户多样化的美颜需求。再者,它还具备出色的跨平台兼容性,可以轻松集成到各种设备和应用中,无论是手机、相机还是直播平台,都能发挥出其强大的功能。当然,针...

    2024-04-01 00:31:58
  • Jfinal 在tomcat启动报错!!

    看清楚,我的jfianl项目启动报错大概显示:java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to start component 什么原因呢?包也导够了,是从官方maven Demo拷贝过来的,在jetty server下可以启动,到部署到tom

    2024-04-01 00:31:30
  • ansible 流程控制

    ansible 流程控制使用when判断主机名- hosts: rsync_server tasks: - name: Install rsyncd Server yum: name: rsync state: present - name: Config rsyncd Conf copy: ...

    2024-04-01 00:31:23
  • angular路由传递参数

    angular路由传递参数

    1、 查询参数中传递数据 在a标签上添加一个参数queryParams &amp;amp;lt;a [routerLink]=&amp;quot;[&#39;/tab4&#39;]&amp;quot; [queryParams]=&amp;quot;{id:3}&amp;quot; &amp;amp;gt;tab4&amp;amp;lt;/a&amp;amp;gt; 此时此时除了rout

    2024-04-01 00:31:17
  • linux shell 除法运算符,Linux shell 基本运算符详解

    shell 支持多种运算符1.算数运算符2.关系运算符3.布尔运算符4.字符串运算符5.文件测试运算符原生bash不支持简单的数学运算,但是可以通过其他命令来实现,例如 awk 和 expr,expr...

    2024-04-01 00:30:50
  • 《奔跑吧Linux内核(第二版)》第三章笔记

    《奔跑吧Linux内核(第二版)》第三章笔记

    ARM64架构基础知识

    2024-04-01 00:30:40
  • 【网络编程】更强的IO复用模型:epoll

    【网络编程】更强的IO复用模型:epoll

    epoll可以说是和poll非常相似的一种I/O多路复用技术,epoll通过监控注册的多个描述字,来进行I/O事件的分发处理。不同于poll的是,epoll不仅提供了默认的level-triggere...

    2024-04-01 00:30:18