gRPC的简单使用

前言

八月初的时候,在公司内部做了一个主题为《gRPC的简单使用》的分享,其实就是和小伙伴们扯扯淡,现在抽空回忆一下,也算是一个小小的总结吧。

现在市面上耳熟能详的RPC框架也很多,下面列举几个遇到比较多的。

  1. 谷歌的gRPC

  2. 推特的Thrift

  3. 阿里的Dubbo

  4. 。。。。

它们都是支持多语言的,相对来说,这三个之中,Dubbo支持的语言略微少一点。现在在一个公司内都能见到多种语言的技术栈都已经是十分常见的事了,好比我司,都有JAVA,C#,Python三种语言了,所以在多语言支持这方面,在技术选型的时候,肯定是要有所考虑的。

下面进入正式的主题,gRPC。

gRPC的简单介绍

640?wx_fmt=png

gRPC是一个现代的开源高性能RPC框架,可以在任何环境中运行。它可以高效地将数据中心内和跨数据中心的服务连接起来,并支持可插拔的负载平衡、跟踪、健康检查和身份验证。同时,它还把设备,移动应用程序和浏览器连接到后端服务的分布式计算变得很容易。

gRPC有什么优点呢?

  1. 简单的服务定义 (使用Protocol Buffers定义服务,这是一个功能强大的二进制序列化工具集和语言)

  2. 跨语言和平台工作 (在微服务式架构中有效地连接多语言服务(10+种语言支持)并能自动为各种语言和平台的服务生成惯用的客户端和服务器存根)

  3. 快速启动并扩展 (使用单行安装运行时和开发环境,并使用框架每秒扩展到数百万个RPC)

  4. 双向流媒体和集成的身份验证 (双向流媒体和集成的身份验证 基于http/2的传输的双向流和完全集成的可插拔身份验证)

gRPC在使用的时候有4种模式供我们选择

  1. 一元RPC(Unary RPCs ):这是最简单的定义,客户端发送一个请求,服务端返回一个结果

  2. 服务器流RPC(Server streaming RPCs):客户端发送一个请求,服务端返回一个流给客户端,客户从流中读取一系列消息,直到读取所有消息

  3. 客户端流RPC(Client streaming RPCs ):客户端通过流向服务端发送一系列消息,然后等待服务端读取完数据并返回处理结果

  4. 双向流RPC(Bidirectional streaming RPCs):客户端和服务端都可以独立向对方发送或接受一系列的消息。客户端和服务端读写的顺序是任意。

我们要根据具体的场景来决定选择那一种。

这里只介绍一元RPC。正常来说,一元RPC应该可以满足我们日常60~70%的需求了吧。

基本用法

gRPC的基本用法可以简单的分为三个点:

  • 服务的定义,即proto文件的编写

  • 服务端代码编写

  • 客户端代码编写

下面我们依次来看一下

服务的定义

既然要定义一个服务,肯定是知道了这个服务要完成什么事之后。

在定义之前,要对proto3和proto2有所了解。不过proto3是推荐的格式。所以我们基本上只要用proto3就可以了。

下面先来看一个后面要用到的proto文件。

syntax = "proto3";option csharp_namespace = "XXXService";package UserInfo;service UserInfoService {  rpc GetList(GetUserListRequest) returns (GetUserListReply){}  rpc GetById(GetUserByIdRequest) returns (GetUserByIdRelpy){}  rpc Save(SaveUserRequest) returns (SaveUserReply){}}message GetUserByIdRequest {    int32 id = 1;}message GetUserByIdRelpy{    int32 id = 1;    string name = 2;    int32 age = 3;    int64 create_time = 4;}message GetUserListRequest {    int32 id = 1;    string name = 2;}message GetUserListReply {  message MsgItem {    int32 id = 1;    string name = 2;    int32 age = 3;    int64 create_time = 4;   }   int32 code = 1;   string msg = 2;   repeated MsgItem data = 3;}message SaveUserRequest {    string name = 1;    int32 age = 2;}message SaveUserReply {   int32 code = 1;   string msg = 2;}

它有下面的几个部分

  1. syntax , 指定要用那个版本的语法

  2. service , 指定rpc服务的接口,简单理解成我们平时定义的接口

  3. message , 指定要传输的消息体,简单理解成我们平常用的 DTO

  4. package , 指定包名

  5. option , 可选参数的定义,不同语言有不同的选项

其实看上去还是比较容易懂的。至少一眼看过去能知道是些什么意思。

如果对proto3还没有了解的,可以参考这个文档Language Guide (proto3),里面很清楚的介绍了一些数据类型和不同语言数据类型的对应关系。

这里有一个要注意的是,时间类型,在proto3中,没有datetime类型,过去很长一段时间,我们是只能用时间戳来表示时间,也就是定义一个长整型,现在是可以用timestamp表处理了。

在写服务端和客户端代码之前,我们需要根据proto文件生成对应的代码。

一个命令即可搞定。

protoc                                                                             path/to/file.proto

现在时代进步的这么快,不少语言已经有工具做了集成,可以在build项目的时候就生成对应的文件了,不需要我们再单独去执行一次上面的那个命令。

好比我们的.NET项目,可以在ItemGroup中直接指定Protobuf,然后告诉它,proto文件是那个,是要生成服务端代码还是客户端代码。

可以看看下面这个具体的例子。

<Project Sdk="Microsoft.NET.Sdk.Web">  <PropertyGroup>    <TargetFramework>netcoreapp2.1</TargetFramework>  </PropertyGroup>  <ItemGroup>    <Protobuf Include="Protos\userinfo.proto" GrpcServices="Server" />  </ItemGroup>    <ItemGroup>    <PackageReference Include="Microsoft.AspNetCore.App" />    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />    <PackageReference Include="Google.Protobuf" Version="3.8.0" />    <PackageReference Include="Grpc.Core" Version="1.22.0" />    <PackageReference Include="Grpc.Tools" Version="1.22.0">      <PrivateAssets>all</PrivateAssets>      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>    </PackageReference>  </ItemGroup></Project>

再往下,就是写代码了。

服务端代码编写

服务端代码分两部分,一部分是服务具体的实现,一部分是服务怎么起来。

先来看看服务的具体实现。

namespace MyBasedServiceA{    using Grpc.Core;    using System.Linq;    using System.Threading.Tasks;    public class UserInfoServiceImpl : UserInfoService.UserInfoServiceBase    {        public override Task<GetUserByIdRelpy> GetById(GetUserByIdRequest request, ServerCallContext context)        {            var result = new GetUserByIdRelpy();            var user = FakeUserInfoDb.GetById(request.Id);            result.Id = user.Id;            result.Name = user.Name;            result.Age = user.Age;            result.CreateTime = user.CreateTime;            return Task.FromResult(result);        }        public override Task<GetUserListReply> GetList(GetUserListRequest request, ServerCallContext context)        {            var result = new GetUserListReply();            var userList = FakeUserInfoDb.GetList(request.Id, request.Name);            result.Code = 0;            result.Msg = "成功";            result.Data.AddRange(userList.Select(x => new GetUserListReply.Types.MsgItem            {                Id = x.Id,                Age = x.Age,                CreateTime = x.CreateTime,                Name = x.Name            }));            return Task.FromResult(result);        }        public override Task<SaveUserReply> Save(SaveUserRequest request, ServerCallContext context)        {            var result = new SaveUserReply();            var flag = FakeUserInfoDb.Save(request.Name, request.Age);            result.Code = 0;            result.Msg = "成功";                        return Task.FromResult(result);        }    }}

可以看到上面的代码,我们只要继承由proto文件生成的一个基类,然后去重写它的实现,就可以认为是实现了一个服务。这个其实就是写我们具体的业务逻辑,大boss有什么需求,堆上去就好了。

然后来看第二部分,服务怎么起来。

在这里我选择的方案是使用通用主机来跑。当然也可以直接在Startup的Configure方法中去启动服务。只要能起来就行 :-D

namespace MyBasedServiceA{    using Grpc.Core;    using Microsoft.Extensions.Hosting;    using Microsoft.Extensions.Logging;    using System;    using System.Threading;    using System.Threading.Tasks;    public class MyBasedServiceAHostedService : BackgroundService    {        private readonly ILogger _logger;        private Server _server;        public MyBasedServiceAHostedService(ILoggerFactory loggerFactory)        {            this._logger = loggerFactory.CreateLogger<MyBasedServiceAHostedService>();            _server = new Server            {                Services = { UserInfoService.BindService(new UserInfoServiceImpl()) },                                                Ports = { new ServerPort("0.0.0.0", 9999, ServerCredentials.Insecure) }            };        }              protected override Task ExecuteAsync(CancellationToken stoppingToken)        {            _server.Start();            return Task.CompletedTask;        }    }}

然后是Program中的代码。

namespace MyBasedServiceA{    using Microsoft.AspNetCore.Builder;    using Microsoft.AspNetCore.Hosting;    using Microsoft.Extensions.DependencyInjection;    using Microsoft.Extensions.Hosting;    using Microsoft.Extensions.Logging;    public class Program    {        public static void Main(string[] args)        {            var host = new HostBuilder()                .ConfigureLogging((hostContext, configLogging) =>                {                    configLogging.AddConsole();                    configLogging.AddDebug();                })                .ConfigureServices((hostContext, services) =>                {                    services.AddHostedService<MyBasedServiceAHostedService>();                })                .Build();            host.Run();        }    }}

到这里,服务端已经可以了。

下面就是客户端了。

客户端代码编写

在这里客户端,我们写两个,一个基于C#(.net core), 一个基于python。刚好也验证一下gRPC的多语言。

正常来说,我们所说的客户端可能很大一部分是对外的WEB API了,就是说api的内部实现,是rpc的调用,而对外的是常见的返回JSON的rest api。

我们先通过控制台来体验一下它的客户端调用。

C#(.net core)客户端

class Program{    static void Main(string[] args)    {        var channel = new Channel("localhost:9999", ChannelCredentials.Insecure);        var client = new UserInfoService.UserInfoServiceClient(channel);        var saveResponse = client.Save(new SaveUserRequest { Age = 99, Name = "c#name" });        Console.WriteLine($"Save received: code = {saveResponse.Code} ,  msg = {saveResponse.Msg}");        var getListResponse = client.GetList(new GetUserListRequest { });        Console.WriteLine($"GetList received: code =  {getListResponse.Code} ,  msg = {getListResponse.Msg}");        foreach (var item in getListResponse.Data)        {            Console.WriteLine(item.Name);        }        Console.ReadKey();    }}

其实这种方式我们很容易联想到WCF,都是生成代码,可以直接点出来的方法,强类型的使用体验。不过我是基本没有用过WCF的,貌似暴露了年龄了,逃~~

python客户端

python要想运行gRPC相关的,要先安装 grpcio-tools,然后再用命令生成相应的文件。

# 安装pip install grpcio-tools# 生成python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./userinfo.proto

具体实现

import grpcimport userinfo_pb2, userinfo_pb2_grpc_HOST = 'localhost'_PORT = '9999'def run():    conn = grpc.insecure_channel(_HOST + ':' + _PORT)    client = userinfo_pb2_grpc.UserInfoServiceStub(channel=conn)    saveResponse = client.Save(userinfo_pb2.SaveUserRequest(name="pyname", age=39))    print("Save received: code = " + str(saveResponse.code) + ", msg = "+ saveResponse.msg)    getListResponse = client.GetList(userinfo_pb2.GetUserListRequest())    print("GetList received: code = " + str(getListResponse.code) + ", msg = "+ getListResponse.msg)    for d in getListResponse.data:        print(d.name)if __name__ == '__main__':    run()

同样也是很简洁。

运行效果

在服务端起来的情况下,先运行.net core的客户端,然后再运行python的客户端,结果大致如下。

640?wx_fmt=png

注: 在调用的时候,有几个概念要知道!!

  1. gRPC中没有采用传统的timeout方式去处理,而是采用了Deadline机制,觉得这个机制和我们的CancellationToken很相似

  2. 无论是客户端还是服务端,都可以随时取消RPC

可以看到我们现在的地址都是硬编码的,因为只有一个节点,然后在线上环境,都会是多节点的,所以我们需要有服务注册和服务发现,下面我们就结合consul来完成服务注册与发现。

服务治理(注册与发现)

当然现在可选的工具还是有很多的,consul,etcd,eureka等,当然最好的还是直接上K8S,不过我们公司还有很长的一段路才能上,所以我们就怎么简单怎么来了。

下面我们调整一下服务端的代码,让gRPC的服务可以注册到consul上面。

public class MyBasedServiceAHostedService : BackgroundService{    private readonly Microsoft.Extensions.Logging.ILogger _logger;    private readonly IConfiguration _configuration;    private readonly IConsulClient _consulClient;    private Server _server;    private AgentServiceRegistration registration;    public MyBasedServiceAHostedService(ILoggerFactory loggerFactory, IConfiguration configuration, IConsulClient consulClient, IHostingEnvironment environment)    {        this._logger = loggerFactory.CreateLogger<MyBasedServiceAHostedService>();        this._configuration = configuration;        this._consulClient = consulClient;        var port = _configuration.GetValue<int>("AppSettings:Port");        _logger.LogInformation($"{environment.EnvironmentName} Current Port is : {port}");                GrpcEnvironment.SetLogger(new GrpcAdapterLogger(loggerFactory));        var address = GetLocalIP();        _logger.LogInformation($"{environment.EnvironmentName} Current IP is : {address}");        registration = new AgentServiceRegistration()        {            ID = $"MyBasedServiceA-{Guid.NewGuid().ToString("N")}",            Name = "MyBasedServiceA",            Address = address,            Port = port,            Check = new AgentServiceCheck            {                TCP = $"{address}:{port}",                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),                Interval = TimeSpan.FromSeconds(10),                Timeout = TimeSpan.FromSeconds(5)            }        };        _server = new Server        {            Ports = { new ServerPort("0.0.0.0", port, ServerCredentials.Insecure) }        };                if (!environment.IsProduction())        {            _server.Services.Add(UserInfoService.BindService(new UserInfoServiceImpl()).Intercept(new AccessLogInterceptor(loggerFactory)));        }        else        {            _server.Services.Add(UserInfoService.BindService(new UserInfoServiceImpl()));        }    }      protected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        await _consulClient.Agent.ServiceDeregister(registration.ID);        await _consulClient.Agent.ServiceRegister(registration);        _logger.LogInformation($"Registering with Consul {registration.ID} OK");        _server.Start();    }    public override async Task StopAsync(CancellationToken cancellationToken)    {        _logger.LogInformation("Unregistering from Consul");        await _consulClient.Agent.ServiceDeregister(registration.ID);        await _server.KillAsync();        await base.StopAsync(cancellationToken);    }    private string GetLocalIP()    {        try        {            string hostName = Dns.GetHostName();            IPHostEntry ipEntry = Dns.GetHostEntry(hostName);            for (int i = 0; i < ipEntry.AddressList.Length; i++)            {                if (ipEntry.AddressList[i].AddressFamily == AddressFamily.InterNetwork)                {                    return ipEntry.AddressList[i].ToString();                }            }            return "127.0.0.1";        }        catch (Exception ex)        {            _logger.LogError(ex, "Get Local Ip error");            return "127.0.0.1";        }    }}

然后客户端要定义一个从consul中读取实例的方法。

从consul中读取对应service的健康实例,正常会用定时轮训的方式去读取,或者写入短时间的缓存。

public class FindService : IFindService {    private readonly ILogger _logger;    private readonly IConsulClient _consulClient;    private readonly ConcurrentDictionary<string, (List<string> List, DateTimeOffset Expiration)> _dict;    public FindService(ILoggerFactory loggerFactory, IConsulClient consulClient)    {        _logger = loggerFactory.CreateLogger<FindService>();        _consulClient = consulClient;        _dict = new ConcurrentDictionary<string, (List<string> List, DateTimeOffset Expiration)>();    }    public async Task<string> FindServiceAsync(string serviceName)    {        var key = $"SD:{serviceName}";        if (_dict.TryGetValue(key, out var item) && item.Expiration > DateTimeOffset.UtcNow)        {            _logger.LogInformation($"Read from cache");            return item.List[new Random().Next(0, item.List.Count)];        }        else        {            var queryResult = await _consulClient.Health.Service(serviceName, string.Empty, true);            var result = new List<string>();            foreach (var serviceEntry in queryResult.Response)            {                result.Add(serviceEntry.Service.Address + ":" + serviceEntry.Service.Port);            }            _logger.LogInformation($"Read from consul : {string.Join(",", result)}");            if (result != null && result.Any())            {                                var val = (result, DateTimeOffset.UtcNow.AddSeconds(600));                _dict.AddOrUpdate(key, val, (x, y) => val);                var count = result.Count;                return result[new Random().Next(0, count)];            }            return "";        }    }}

调用的时候。

private async Task<(UserInfoService.UserInfoServiceClient Client, string Msg)> GetClientAsync(string name){    var target = await _findService.FindServiceAsync(name);    _logger.LogInformation($"Current target = {target}");    if (string.IsNullOrWhiteSpace(target))    {        return (null, "can not find a service");    }    else    {        var channel = new Channel(target, ChannelCredentials.Insecure);        var client = new UserInfoService.UserInfoServiceClient(channel);        return (client, string.Empty);    }}

然后我们编写docker-compose.yml, 让它在docker中跑

version: '3.4'services:  xxxserver1:    image: ${DOCKER_REGISTRY-}xxxserver    build:      context: .      dockerfile: MyBasedServiceA/Dockerfile    ports:      - "9999:9999"      depends_on:      - consuldev          networks:        backend:  xxxserver2:    image: ${DOCKER_REGISTRY-}xxxserver    build:      context: .      dockerfile: MyBasedServiceA/Dockerfile    ports:      - "9995:9999"       depends_on:      - consuldev          networks:        backend:              xxxclient:    image: ${DOCKER_REGISTRY-}xxxclient    build:      context: .      dockerfile: XXXService/Dockerfile    ports:      - "9000:80"    depends_on:      - consuldev          - xxxserver1      - xxxserver2    networks:        backend:  consuldev:    image: consul:latest        ports:      - "8300:8300"      - "8400:8400"      - "8500:8500"        networks:        backend:networks:    backend:          driver: bridge

运行结果如下:

当用 docker 把这几个服务都跑起来之后, 可以看到类似下面的输出

640?wx_fmt=png

也可以用docker ps命令来看一下那几个服务是不是真的在运行。

640?wx_fmt=png

同时,我们打开consul的UI界面,可以看到我们服务端的两个实例已经注册上来了。

640?wx_fmt=png

当我们用客户端去访问的时候,可以发现,第一次它是从consul中取下来了两个ip,然后随机选了一个进行访问。

640?wx_fmt=png

我们把其中一个服务端(0.4)stop,用来模拟某个节点出现异常,被剔除的情况 ,可以发现consul上面已经看不到了,只剩下0.3这个节点了。

640?wx_fmt=png

如果我们的调度策略没有及时将"死掉"的节点剔除,就会出现下面的这种情况。

640?wx_fmt=png

最后,把stop的服务端启动,模拟恢复正常,这个时候可以发现无论调度到那个节点都可以正常访问了。

640?wx_fmt=png

.NET Core 2.x 和 .NET Core 3.0的细微区别

在.NET Core 2.x中,我们的Server,是需要手动控制的,在.NET Core 3.0中,可以认为它已经和Kestrel融为一体了,不再需要我们再手动去Start了。

同样的,服务的实现,也和Endpoint Routing紧密的结合在一起了,不再和之前一样了。

可以看看下面的例子,可能会发现,这是一种熟悉的不能再熟悉的感觉。

public class Startup{    public void ConfigureServices(IServiceCollection services)    {        services.AddGrpc(x=>        {            x.EnableDetailedErrors = true;            x.Interceptors.Add<AccessLogInterceptor>();        });    }      public void Configure(IApplicationBuilder app, IWebHostEnvironment env)    {        if (env.IsDevelopment())        {            app.UseDeveloperExceptionPage();        }        app.UseRouting();        app.UseEndpoints(endpoints =>        {            endpoints.MapGrpcService<GreeterService>();            endpoints.MapGrpcService<UserService>();            endpoints.MapGet("/", async context =>            {                await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");            });        });    }}

客户端的用法也和以前不一样了,直接看一个例子就很清晰了。

从它的用法上,我们也有熟悉的面孔 HttpClient

虽然gRPC是基于HTTP/2的,但是可以看到我们上面小节的例子中,还是能够指定不使用的。然而到.NET Core 3.0之后,我们就必须要使用https了,不然客户端就是调不通的。同样的,我们也可以在grpc-dotnet的仓库上面看到,如果想不使用HTTP/2,就让我们用回之前的老库,不要用新库,James Newton-King就是这么直接。

https://github.com/grpc/grpc-dotnet/issues/277

https://github.com/grpc/grpc-dotnet/issues/405

https://github.com/grpc/grpc-dotnet/issues/431

扩展阅读

  • 思考gRPC :为什么是HTTP/2

  • Grpc中Deadline分析

  • protocol-buffers官网

  • Using CancellationTokens in ASP.NET Core MVC controllers

文中出现的示例代码都可以在下面这个仓库找到

catcherwong-archive/2019

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/314465.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

生命周期结束,Spring Boot 1.x退役

一年前 Spring 官方宣布 Spring Boot 1.x 生命周期将于今年 8 月 1 日结束&#xff0c;如今时间已到&#xff0c;在发布 Spring Boot 1.5.22 的同时&#xff0c;Spring 确认将不再为 1.x 系列发布维护版本。官方希望用户尽快迁移到 Spring Boot 2.x 上&#xff0c;为此还制作了…

Apollo 配置中心:分布式部署

Apollo&#xff08;阿波罗&#xff09;是携程框架部门研发的分布式配置中心&#xff0c;能够集中化管理应用不同环境、不同集群的配置&#xff0c;配置修改后能够实时推送到应用端&#xff0c;并且具备规范的权限、流程治理等特性&#xff0c;适用于微服务配置管理场景。服务端…

使用Redis实现最近N条数据的决策

前言很多时候&#xff0c;我们会根据用户最近一段时间的行为&#xff0c;做出一些相应的策略&#xff0c;从而改变系统的运动轨迹。举个简单的例子来说明一下&#xff1a;假设A公司现在有两个合作伙伴(B和C)&#xff0c;B和C都是提供天气数据的&#xff0c;现在A公司做了一个聚…

为什么我不喜欢数据库三范式

插曲最近&#xff0c;一个远房亲戚的小表弟准备选修专业找到我问&#xff1a;"哥&#xff0c;现在学数据库有没有前途阿?""当然有啊&#xff0c;前途大大的呢""那我现在开始学数据库&#xff0c;需要先从什么开始呢?""学课程的话&#xf…

硬货 - 技术人也能轻松玩转公众号?正确姿势竟然是...

最近在知乎上看到关于「公众号是否有“前”途」的相关问题... 问题下面有些精华回答~微信公众号还有“前”途吗&#xff1f; - 知乎https://www.zhihu.com/question/324575670很好的问题&#xff01;作为一个技术人&#xff0c;我决定将此问题和自身情况结合起来&#xff0c;于…

你必须知道的Dockerfile

本篇已加入《.NET Core on K8S学习实践系列文章索引》&#xff0c;可以点击查看更多容器化技术相关系列文章。本文预计阅读时间为5分钟。01—关于Dockerfile在Docker中创建镜像最常用的方式&#xff0c;就是使用Dockerfile。Dockerfile是一个Docker镜像的描述文件&#xff0c;我…

RabbitMQ 死信/死信队列

一、RabbitMQ 死信/死信队列1、DLXDead Letter Exchange 的缩写DLX&#xff08;Dead Letter Exchanges&#xff09;死信交换&#xff0c;死信队列本身也是一个普通的消息队列&#xff0c;在创建队列的时候&#xff0c;通过设置一些关键参数&#xff0c;可以将一个普通的消息队列…

centos7 rabbitmq安装/配置

一、RabbitMQ简单介绍RabbitMQ就是当前最主流的消息中间件之一。RabbitMQ是一个开源的AMQP实现&#xff0c;服务器端用Erlang语言编写&#xff0c;支持多种客户端&#xff0c;如&#xff1a;Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等&#xff0c;支…

Hyper-V + CentOS7 安装视频教程

一、前言本文使用图文视频的方式展示安装Centos7&#xff0c;【喜欢看视频学习的童靴请拖至文尾观看视频】二、虚拟机配置指定虚拟机名称&安装位置选择虚拟机代数 第一代虚拟机&#xff08;例如Server 2008等平台技术&#xff0c;支持Vista、Win7&#xff09; 第二代虚拟机…

程序员修神之路--用NOSql给高并发系统加速

领取福利记得长按&#xff0c;领取技术书籍哦随着互联网大潮的到来&#xff0c;越来越多网站&#xff0c;应用系统需要海量数据的支撑&#xff0c;高并发、低延迟、高可用、高扩展等要求在传统的关系型数据库中已经得不到满足&#xff0c;或者说关系型数据库应对这些需求已经显…

限时团购,6.5折:《C# 7.0 核心技术指南》

大家好&#xff0c;经过近两年的翻译&#xff0c;《C# 7.0 核心技术指南》终于和大家见面了。全书由 ThoughtWorks 高级咨询师&#xff0c;资深 .NET 专家刘夏翻译。作为一本第七次再版的图书&#xff0c;此次翻译对书中的字句进行了重新整理。期间和图书的原作者 Joe Albahari…

Azure 命令行工具大混战,都是什么,该选哪个?

点击上方蓝字关注“汪宇杰博客”导语最近在学习 Azure 的命令行玩法&#xff0c;发现官方有不止一种命令行工具&#xff0c;容易对新手产生混淆&#xff0c;本文将介绍各种工具都是干啥的&#xff0c;以及如何选择。目前&#xff0c;微软官方有3个Azure命令行工具&#xff0c;分…

揭秘鸿蒙生态背后的DevOps实践

&#xff08;图片来源于网络&#xff09;8月9日&#xff0c;华为发布了鸿蒙操作系统&#xff0c;在发布会上我们看到了鸿蒙系统的研发历程&#xff1a;2017年&#xff0c;鸿蒙内核1.0完成技术验证&#xff1b;2018年&#xff0c;鸿蒙内核2.0用于终端TEE&#xff1b;2019年&…

AT3557 Four Coloring 切比雪夫距离 + 四色构造

传送门 由于曼哈顿距离在图上显示的是一个棱形&#xff0c;并不是很好看&#xff0c;所以我们将其旋转454545&#xff0c;转换成切比雪夫距离&#xff0c;这样就变成了一个正方形&#xff0c;正方形内部的点距离都不超过ddd&#xff0c;此时可以将正方形内部的点看成一个整体&a…

SonarQube系列一、Linux安装与部署

来源&#xff1a;https://www.cnblogs.com/7tiny/p/11269774.html【前言】随着项目团队规模日益壮大&#xff0c;项目代码量也越来越多。且不说团队成员编码水平层次不齐&#xff0c;即便是老手&#xff0c;也难免因为代码量的增加和任务的繁重而忽略代码的质量&#xff0c;最终…

P2906 [USACO08OPEN]Cow Neighborhoods G 切比雪夫距离 + 并查集 + set

传送门 考虑将曼哈顿距离转换成切比雪夫距离&#xff0c;这样问题就变成了max(∣x1−x2∣,∣y1−y2∣)≤dmax(|x_1-x_2|,|y_1-y_2|)\le dmax(∣x1​−x2​∣,∣y1​−y2​∣)≤d&#xff0c;这个式子就很好看了&#xff0c;我们首先按照(x,y)(x,y)(x,y)排序&#xff0c;让后我…

2018-2019 ACM-ICPC, Asia Shenyang Regional Contest E. The Kouga Ninja Scrolls 切比雪夫距离 +线段树

传送门 将曼哈顿距离转换成切比雪夫距离&#xff0c;现在就是求max(∣x1−x2∣,∣y1−y2∣)max(|x_1-x_2|,|y_1-y_2|)max(∣x1​−x2​∣,∣y1​−y2​∣)&#xff0c;显然我们可以将x,yx,yx,y分开考虑&#xff0c;下面以xxx为例。 考虑一段区间内不同门派的最大值和最小值&am…

ASP.NET Core 框架本质学习

本文作为学习过程中的一个记录。学习文章地址&#xff1a;https://www.cnblogs.com/artech/p/inside-asp-net-core-framework.html一. ASP.NET Core 框架上的 Hello World程序public class Program{public static void Main()> new WebHostBuilder() .UseKestrel() …

基于C#实现的轻量级多线程队列

工作中我们经常会遇到一些一些功能需要实现造作日志&#xff0c;数据修改日志&#xff0c;对于这种业务需求如果我们以同步的方式实现&#xff0c;难免会影响到系统的性能。如下我列出集中解决方案。使用Thread异步处理。使用线程池或Task异步处理。以上两种方案确实能解决我们…

【活动】厦门.NET俱乐部 省上云开发者专场

十年磨一剑&#xff0c;厦门.NET俱乐部诚挚邀请您相约软件园二期创驿站&#xff0c;参加云重启|厦门.NET俱乐部省上云开发者专场。活动干货满满&#xff0c;更有精美礼品&#xff0c;厦门.NET俱乐部期待与您“厦门论剑”。详情请点击图片或直接阅读原文报名