手把手教大家在 gRPC 中使用 JWT 完成身份校验
文章目录
- 1. JWT 介绍
- 1.1 无状态登录
- 1.1.1 什么是有状态
- 1.1.2 什么是无状态
- 1.2 如何实现无状态
- 1.3 JWT
- 1.3.1 简介
- 1.3.2 JWT数据格式
- 1.3.3 JWT 交互流程
- 1.3.4 JWT 存在的问题
- 2. 实践
- 2.1 项目创建
- 2.2 grpc_api
- 2.3 grpc_server
- 2.4 grpc_client
- 3. 小结
上篇文章松哥和小伙伴们聊了在 gRPC 中如何使用拦截器,这些拦截器有服务端拦截器也有客户端拦截器,这些拦截器的一个重要使用场景,就是可以进行身份的校验。当客户端发起请求的时候,服务端通过拦截器进行身份校验,就知道这个请求是谁发起的了。今天松哥就来通过一个具体的案例,来和小伙伴们演示一下 gRPC 如何结合 JWT 进行身份校验。
1. JWT 介绍
1.1 无状态登录
1.1.1 什么是有状态
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 Tomcat 中的 Session。例如登录:用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:
- 服务端保存大量数据,增加服务端压力
- 服务端保存用户状态,不支持集群化部署
1.1.2 什么是无状态
微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:
- 服务端不保存任何客户端请求者信息
- 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
那么这种无状态性有哪些好处呢?
- 客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器
- 服务端的集群和状态对客户端透明
- 服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)
- 减小服务端存储压力
1.2 如何实现无状态
无状态登录的流程:
- 首先客户端发送账户名/密码到服务端进行认证
- 认证通过后,服务端将用户信息加密并且编码成一个 token,返回给客户端
- 以后客户端每次发送请求,都需要携带认证的 token
- 服务端对客户端发送来的 token 进行解密,判断是否有效,并且获取用户登录信息
1.3 JWT
1.3.1 简介
JWT,全称是 Json Web Token, 是一种 JSON 风格的轻量级的授权和身份认证规范,可实现无状态、分布式的 Web 应用授权:
JWT 作为一种规范,并没有和某一种语言绑定在一起,常用的 Java 实现是 GitHub 上的开源项目 jjwt,地址如下:https://github.com/jwtk/jjwt
1.3.2 JWT数据格式
JWT 包含三部分数据:
-
Header:头部,通常头部有两部分信息:
- 声明类型,这里是JWT
- 加密算法,自定义
我们会对头部进行 Base64Url 编码(可解码),得到第一部分数据。
-
Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:
- iss (issuer):表示签发人
- exp (expiration time):表示token过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
这部分也会采用 Base64Url 编码,得到第二部分数据。
- Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过 Header 中配置的加密算法生成。用于验证整个数据完整和可靠性。
生成的数据格式如下图:
注意,这里的数据通过 .
隔开成了三部分,分别对应前面提到的三部分,另外,这里数据是不换行的,图片换行只是为了展示方便而已。
1.3.3 JWT 交互流程
流程图:
步骤翻译:
- 应用程序或客户端向授权服务器请求授权
- 获取到授权后,授权服务器会向应用程序返回访问令牌
- 应用程序使用访问令牌来访问受保护资源(如 API)
因为 JWT 签发的 token 中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了 RESTful 的无状态规范。
1.3.4 JWT 存在的问题
说了这么多,JWT 也不是天衣无缝,由客户端维护登录状态带来的一些问题在这里依然存在,举例如下:
- 续签问题,这是被很多人诟病的问题之一,传统的 cookie+session 的方案天然的支持续签,但是 jwt 由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入 redis,虽然可以解决问题,但是 jwt 也变得不伦不类了。
- 注销问题,由于服务端不再保存用户信息,所以一般可以通过修改 secret 来实现注销,服务端 secret 修改后,已经颁发的未过期的 token 就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。
- 密码重置,密码重置后,原本的 token 依然可以访问系统,这时候也需要强制修改 secret。
- 基于第 2 点和第 3 点,一般建议不同用户取不同 secret。
当然,为了解决 JWT 存在的问题,也可以将 JWT 结合 Redis 来用,服务端生成的 JWT 字符串存入到 Redis 中并设置过期时间,每次校验的时候,先看 Redis 中是否存在该 JWT 字符串,如果存在就进行后续的校验。但是这种方式有点不伦不类(又成了有状态了)。
2. 实践
我们来看下 gRPC 如何结合 JWT。
2.1 项目创建
首先我先给大家看下我的项目结构:
├── grpc_api
│ ├── pom.xml
│ └── src
├── grpc_client
│ ├── pom.xml
│ └── src
├── grpc_server
│ ├── pom.xml
│ └── src
└── pom.xml
还是跟之前文章中的一样,三个模块,grpc_api 用来存放一些公共的代码。
grpc_server 用来放服务端的代码,我这里服务端主要提供了两个接口:
- 登录接口,登录成功之后返回 JWT 字符串。
- hello 接口,客户端拿着 JWT 字符串来访问 hello 接口。
grpc_client 则是我的客户端代码。
2.2 grpc_api
我将 protocol buffers 和一些依赖都放在 grpc_api 模块中,因为将来我的 grpc_server 和 grpc_client 都将依赖 grpc_api。
我们来看下这里需要的依赖和插件:
<dependencies><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.grpc</groupId><artifactId>grpc-netty-shaded</artifactId><version>1.52.1</version></dependency><dependency><groupId>io.grpc</groupId><artifactId>grpc-protobuf</artifactId><version>1.52.1</version></dependency><dependency><groupId>io.grpc</groupId><artifactId>grpc-stub</artifactId><version>1.52.1</version></dependency><dependency><groupId>org.apache.tomcat</groupId><artifactId>annotations-api</artifactId><version>6.0.53</version><scope>provided</scope></dependency>
</dependencies>
<build><extensions><extension><groupId>kr.motd.maven</groupId><artifactId>os-maven-plugin</artifactId><version>1.6.2</version></extension></extensions><plugins><plugin><groupId>org.xolstice.maven.plugins</groupId><artifactId>protobuf-maven-plugin</artifactId><version>0.6.1</version><configuration><protocArtifact>com.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier}</protocArtifact><pluginId>grpc-java</pluginId><pluginArtifact>io.grpc:protoc-gen-grpc-java:1.51.0:exe:${os.detected.classifier}</pluginArtifact></configuration><executions><execution><goals><goal>compile</goal><goal>compile-custom</goal></goals></execution></executions></plugin></plugins>
</build>
这里的依赖和插件松哥在本系列的第一篇文章中都已经介绍过了,唯一不同的是,这里引入了 JWT 插件,JWT 我使用了比较流行的 JJWT 这个工具。JJWT 松哥在之前的文章和视频中也都有介绍过,这里就不再啰嗦了。
先来看看我的 Protocol Buffers 文件:
syntax = "proto3";option java_multiple_files = true;
option java_package = "org.javaboy.grpc.api";
option java_outer_classname = "LoginProto";
import "google/protobuf/wrappers.proto";package login;service LoginService {rpc login (LoginBody) returns (LoginResponse);
}service HelloService{rpc sayHello(google.protobuf.StringValue) returns (google.protobuf.StringValue);
}message LoginBody {string username = 1;string password = 2;
}message LoginResponse {string token = 1;
}
经过前面几篇文章的介绍,这里我就不多说啦,就是定义了两个服务:
- LoginService:这个登录服务,传入用户名密码,返回登录成功之后的令牌。
- HelloService:这个就是一个打招呼的服务,传入字符串,返回也是字符串。
定义完成之后,生成对应的代码即可。
接下来再定义一个常量类供 grpc_server 和 grcp_client 使用,如下:
public interface AuthConstant {SecretKey JWT_KEY = Keys.hmacShaKeyFor("hello_javaboy_hello_javaboy_hello_javaboy_hello_javaboy_".getBytes());Context.Key<String> AUTH_CLIENT_ID = Context.key("clientId");String AUTH_HEADER = "Authorization";String AUTH_TOKEN_TYPE = "Bearer";
}
这里的每个常量我都给大家解释下:
- JWT_KEY:这个是生成 JWT 字符串以及进行 JWT 字符串校验的密钥。
- AUTH_CLIENT_ID:这个是客户端的 ID,即客户端发送来的请求携带了 JWT 字符串,通过 JWT 字符串确认了用户身份,就存在这个变量中。
- AUTH_HEADER:这个是携带 JWT 字符串的请求头的 KEY。
- AUTH_TOKEN_TYPE:这个是携带 JWT 字符串的请求头的参数前缀,通过这个可以确认参数的类型,常见取值有 Bearer 和 Basic。
如此,我们的 gRPC_api 就定义好了。
2.3 grpc_server
接下来我们来定义 gRPC_server。
首先来定义登录服务:
public class LoginServiceImpl extends LoginServiceGrpc.LoginServiceImplBase {@Overridepublic void login(LoginBody request, StreamObserver<LoginResponse> responseObserver) {String username = request.getUsername();String password = request.getPassword();if ("javaboy".equals(username) && "123".equals(password)) {System.out.println("login success");//登录成功String jwtToken = Jwts.builder().setSubject(username).signWith(AuthConstant.JWT_KEY).compact();responseObserver.onNext(LoginResponse.newBuilder().setToken(jwtToken).build());responseObserver.onCompleted();}else{System.out.println("login error");//登录失败responseObserver.onNext(LoginResponse.newBuilder().setToken("login error").build());responseObserver.onCompleted();}}
}
省事起见,我这里没有连接数据库,用户名和密码固定为 javaboy 和 123。
登录成功之后,就生成一个 JWT 字符串返回。
登录失败,就返回一个 login error 字符串。
再来看我们的 HelloService 服务,如下:
public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {@Overridepublic void sayHello(StringValue request, StreamObserver<StringValue> responseObserver) {String clientId = AuthConstant.AUTH_CLIENT_ID.get();responseObserver.onNext(StringValue.newBuilder().setValue(clientId + " say hello:" + request.getValue()).build());responseObserver.onCompleted();}
}
这个服务就更简单了,不啰嗦。唯一值得说的是 AuthConstant.AUTH_CLIENT_ID.get();
表示获取当前访问用户的 ID,这个用户 ID 是在拦截器中存入进来的。
最后,我们来看服务端比较重要的拦截器,我们要在拦截器中从请求头中获取到 JWT 令牌并解析,如下:
public class AuthInterceptor implements ServerInterceptor {private JwtParser parser = Jwts.parser().setSigningKey(AuthConstant.JWT_KEY);@Overridepublic <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata, ServerCallHandler<ReqT, RespT> serverCallHandler) {String authorization = metadata.get(Metadata.Key.of(AuthConstant.AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER));Status status = Status.OK;if (authorization == null) {status = Status.UNAUTHENTICATED.withDescription("miss authentication token");} else if (!authorization.startsWith(AuthConstant.AUTH_TOKEN_TYPE)) {status = Status.UNAUTHENTICATED.withDescription("unknown token type");} else {Jws<Claims> claims = null;String token = authorization.substring(AuthConstant.AUTH_TOKEN_TYPE.length()).trim();try {claims = parser.parseClaimsJws(token);} catch (JwtException e) {status = Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e);}if (claims != null) {Context ctx = Context.current().withValue(AuthConstant.AUTH_CLIENT_ID, claims.getBody().getSubject());return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler);}}serverCall.close(status, new Metadata());return new ServerCall.Listener<ReqT>() {};}
}
这段代码逻辑应该好理解:
- 首先从 Metadata 中提取出当前请求所携带的 JWT 字符串(相当于从请求头中提取出来)。
- 如果第一步提取到的值为 null 或者这个值不是以指定字符 Bearer 开始的,说明这个令牌是一个非法令牌,设置对应的响应 status 即可。
- 如果令牌都没有问题的话,接下来就进行令牌的校验,校验失败,则设置相应的 status 即可。
- 校验成功的话,我们就会获取到一个 Jws 对象,从这个对象中我们可以提取出来用户名,并存入到 Context 中,将来我们在 HelloServiceImpl 中就可以获取到这里的用户名了。
- 最后,登录成功的话,
Contexts.interceptCall
方法构建监听器并返回;登录失败,则构建一个空的监听器返回。
最后,我们再来看看启动服务端:
public class LoginServer {Server server;public static void main(String[] args) throws IOException, InterruptedException {LoginServer server = new LoginServer();server.start();server.blockUntilShutdown();}public void start() throws IOException {int port = 50051;server = ServerBuilder.forPort(port).addService(new LoginServiceImpl()).addService(ServerInterceptors.intercept(new HelloServiceImpl(), new AuthInterceptor())).build().start();Runtime.getRuntime().addShutdownHook(new Thread(() -> {LoginServer.this.stop();}));}private void stop() {if (server != null) {server.shutdown();}}private void blockUntilShutdown() throws InterruptedException {if (server != null) {server.awaitTermination();}}
}
这个跟之前的相比就多加了一个 Service,添加 HelloServiceImpl 服务的时候,多加了一个拦截器,换言之,登录的时候,请求是不会被这个认证拦截器拦截的。
好啦,这样我们的 grpc_server 就开发完成了。
2.4 grpc_client
接下来我们来看 grpc_client。
先来看登录:
public class LoginClient {public static void main(String[] args) throws InterruptedException {ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051).usePlaintext().build();LoginServiceGrpc.LoginServiceStub stub = LoginServiceGrpc.newStub(channel);login(stub);}private static void login(LoginServiceGrpc.LoginServiceStub stub) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(1);stub.login(LoginBody.newBuilder().setUsername("javaboy").setPassword("123").build(), new StreamObserver<LoginResponse>() {@Overridepublic void onNext(LoginResponse loginResponse) {System.out.println("loginResponse.getToken() = " + loginResponse.getToken());}@Overridepublic void onError(Throwable throwable) {}@Overridepublic void onCompleted() {countDownLatch.countDown();}});countDownLatch.await();}
}
这个方法直接调用就行了,看过前面几篇 gRPC 文章的话,这里都很好理解。
再来看 hello 接口的调用,这个接口调用需要携带 JWT 字符串,而携带 JWT 字符串,则需要我们构建一个 CallCredentials 对象,如下:
public class JwtCredential extends CallCredentials {private String subject;public JwtCredential(String subject) {this.subject = subject;}@Overridepublic void applyRequestMetadata(RequestInfo requestInfo, Executor executor, MetadataApplier metadataApplier) {executor.execute(() -> {try {Metadata headers = new Metadata();headers.put(Metadata.Key.of(AuthConstant.AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER),String.format("%s %s", AuthConstant.AUTH_TOKEN_TYPE, subject));metadataApplier.apply(headers);} catch (Throwable e) {metadataApplier.fail(Status.UNAUTHENTICATED.withCause(e));}});}@Overridepublic void thisUsesUnstableApi() {}
}
这里就是将请求的 JWT 令牌放入到请求头中即可。
最后来看看调用:
public class LoginClient {public static void main(String[] args) throws InterruptedException {ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051).usePlaintext().build();LoginServiceGrpc.LoginServiceStub stub = LoginServiceGrpc.newStub(channel);sayHello(channel);}private static void sayHello(ManagedChannel channel) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(1);HelloServiceGrpc.HelloServiceStub helloServiceStub = HelloServiceGrpc.newStub(channel);helloServiceStub.withCallCredentials(new JwtCredential("eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJqYXZhYm95In0.IMMp7oh1dl_trUn7sn8qiv9GtO-COQyCGDz_Yy8VI4fIqUcRfwQddP45IoxNovxL")).sayHello(StringValue.newBuilder().setValue("wangwu").build(), new StreamObserver<StringValue>() {@Overridepublic void onNext(StringValue stringValue) {System.out.println("stringValue.getValue() = " + stringValue.getValue());}@Overridepublic void onError(Throwable throwable) {System.out.println("throwable.getMessage() = " + throwable.getMessage());}@Overridepublic void onCompleted() {countDownLatch.countDown();}});countDownLatch.await();}
}
这里的登录令牌就是前面调用 login 方法时获取到的令牌。
好啦,大功告成。
3. 小结
上面的登录与校验只是松哥给小伙伴们展示的一个具体案例而已,在此案例基础之上,我们还可以扩展出来更多写法,但是万变不离其宗,其他玩法就需要小伙伴们自行探索啦~
相关文章:

手把手教大家在 gRPC 中使用 JWT 完成身份校验
文章目录1. JWT 介绍1.1 无状态登录1.1.1 什么是有状态1.1.2 什么是无状态1.2 如何实现无状态1.3 JWT1.3.1 简介1.3.2 JWT数据格式1.3.3 JWT 交互流程1.3.4 JWT 存在的问题2. 实践2.1 项目创建2.2 grpc_api2.3 grpc_server2.4 grpc_client3. 小结上篇文章松哥和小伙伴们聊了在 …...

VSCode远程连接服务器
工作使用服务器的jupyter,直到有一天服务器挂了,然而,代码还没有来得及备份。o(╥﹏╥)o VScode远程连接服务器,使用服务器的资源,代码可以存在本地,可以解决上述困境。 1.官网下载VSCode.网址https://cod…...

【C++】-- 异常
目录 C语言传统的处理错误的方式 C异常概念 异常的使用 异常的抛出和捕获 自定义异常体系 异常的重新抛出 异常安全 异常规范(C期望) C标准库的异常体系 异常的优缺点 C异常的优点 C异常的缺点 总结 C语言传统的处理错误的方式 传统的错误…...

Java中的Stack与Queue
文章目录一、栈的概念及使用1.1 概念1.2 栈的使用1.3 栈的模拟实现二、队列的概念及使用2.1 概念2.2 队列的使用2.3 双端队列(Deque)三、相关OJ题3.1 用队列实现栈。3.2 用栈实现队列。总结一、栈的概念及使用 1.1 概念 栈:一种特殊的线性表,其只允许在…...

xilinx FPGA在线调试方法总结(vivado+ila+vio)
本文主要介绍xilinx FPGA开发过程中常用的调试方法,包括ILA、VIO和TCL命令等等,详细介绍了如何使用。一、FPGA调试基本原则根据实际的输出结果表现,来推测可能的原因,再在模块中加ILA信号,设置抓信号条件,逐…...

自动化测试——css元素定位
文章目录一、css定位场景二、css相对定位的优点三、css的调试方法1、表达式中含有字符串:表达式中的引号一定和外面字符串的引号相反四、css基础语法1、标签定位2、class定位特别注意:当class类型的属性值包含多个分割值,$(.s_tab s_tab_1z9n…...

ChatGPT可能马上取代你,这是它能做的十个工作
ChatGPT 的横空出世,在业界掀起了惊涛骇浪。专家表示,ChatGPT 和相关人工智能技术可能会威胁到一些工作岗位,尤其是白领工作。 自去年11月发布以来,新型聊天机器人模型 ChatGPT 已经被用于各种各样的工作:撰写求职信、编写儿童读物,甚至帮助学生在论文中作弊。谷歌公司发…...

ubuntu转储coredump
方法一: 输入以下命令即可,其中${USER}为自己电脑的用户名: ulimit -c unlimited echo "/home/${USER}/core.%p" > /proc/sys/kernel/core_pattern 方法二: Disable apport : sudo systemctl stop apport.servicesudo system…...

基于单片机的毕业设计推荐
** 2023基于单片机的毕业设计推荐: ** 1、基于51单片机的多功能门禁系统(低端、功能限制较大)。 2、基于单片机的多功能实时时钟。 3、基于单片机的音乐播放器。 4、基于STM32单片机的多功能门禁系统(高端、没有限制)…...

APP测试中ios和androis的区别,有哪些注意点
目录 一、运行机制不同 二、对app内存消耗处理方式不同 三、后台制度不同 四、最高权限指令不同 五、推送机制不同 六、抓取方式不同 七、灰度发版机制不同 八、审核机制不同 总结感谢每一个认真阅读我文章的人!!! 重点:…...

使用 Xcode 创建第一个 Objective-C 命令行程序 HelloWorld
总目录 iOS开发笔记目录 从一无所知到入门 文章目录创建项目运行项目,查看日志输出同一项目下新增子目录,切换要运行的 Target创建项目 打开 Xcode ,Create a new Xcode project 接下来的默认界面: 切换到 macOS 下ÿ…...

【蓝桥杯集训8】哈希表专题(3 / 3)
目录 手写哈希表 1、开放寻址法 2、拉链法 字符串前缀哈希表法 2058. 笨拙的手指 - 哈希表 秦九韶算法(进制转换) 枚举 秦九韶算法——将x进制数转化为十进制数 手写哈希表 活动 - AcWing 1、开放寻址法 设 h(x)k,也就是 x 的哈希值…...

Java Scanner 类,超详细整理,适合新手入门
目录 一、什么是 Java Scanner 类? 二、引用数据类型 1、引用数据类型的定义 三、Scanner 类有哪些常用方法? hasNext()用法 四、next() 与 nextLine() 区别 next(): nextLine(): 五、使用 next 方法 五、使用 nextLine方法 一、什…...

干货 | 中小企业选型 Elasticsearch 避坑指南
1、线上常见问题在我线下对接企业或线上交流的时候,经常会遇到各种业务场景不同的问题。比如,常见问题归类如下:常见问题1:ES 适合场景及架构选型问题。公司的核心业务是做企业员工健康管理,数据来自电子化后的员工体检…...

全局组件和局部组件
全局组件第一种定义方法:A、创建自己的组件:Loading.vueB、在main.js文件中引入组件并注册import Vue from vue import App from ./App.vue import * as filters from ./filterimport quanjuzujian from ./components/quanjuzujian.vueVue.component(qua…...

提取括号中的内容
正则能解决不嵌套的括号内容提取问题遇到一个问题,就是需要提取字符串中每一个中括号里的内容,在网上搜了一下,发现用正则表达式(\[[^\]]*\])可以提取中括号中的内容,以下面文本为匹配对象:PerformanceManager[第1个中…...

数据结构-算法的空间复杂度(1.2)
目录 1.空间复杂度 1.1 例子 1.2 空间的特殊性质 写在最后: 1.空间复杂度 空间复杂度也是一个数学表达式, 是对一个算法在运行过程中临时占用存储空间大小的量度。 他也是用大O渐进表示法。 1.1 例子 例1: 冒泡排序: v…...

【总结】python3启动web服务引发的一系列问题
背景 在某行的实施项目,需要使用python3环境运行某些py脚本。 由于行内交付的机器已自带python3 ,没有采取自行安装python3,但是运行python脚本时报没有tornado module。 错误信息 ModuleNotFoundError:No module named ‘torn…...

Linux:基于libevent读写管道代码,改进一下上一篇变成可以接收键盘输入
对上一篇进行改进,变成可以接收键盘输入,然后写入管道: 读端代码: #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <s…...

C语言格式化输出总结:%d,%c,%s,%f, %lf,%m.nd,%m.nf,%m.ns 以及sprintf函数
凡事发生必将有益于我,高手,从来都不仅仅是具备某种思维的人,而是那些具备良好学习习惯的人,成为高手,无他,手熟尔!加油在最近的学习之中,对于格式化输出这个知识点,这里…...

Nginx之反向代理、负载均衡、动静分离。
Nginx之反向代理、负载均衡、动静分离。 1、Nginx是啥? 轻量级Web服务器、反向代理服务器、电子邮件(IMAP/POP3)代理服务器 在 BSD-like 协议下发行、占内存少、并发高(同时处理请求能力)。 2、安装 官网…...

0401不定积分的概念和性质-不定积分
文章目录1 原函数与不定积分的概念1.1 原函数1.2 原函数存在定理1.3 不定积分2 不定积分的性质3 基本积分表4 例题后记1 原函数与不定积分的概念 1.1 原函数 定义1 如果在区间I上,可导函数F(x)的导航为f(x),即对任一x∈Ix\in Ix∈I,都有 F′…...

数组中的各种迭代API方法手写
js的数组上有很多实用的方法,不论是在遍历数组上,还是在操作数组内元素上,它有许多不同的遍历数组的方法,同时它还有着可以直接操作数组中间元素的方法。 接下来,我来带大家手写数组里的 遍历方法 。 Array.forEach(…...

详解量子计算:相位反冲与相位反转
前言 本文需要对量子计算有一定的了解。需要的请翻阅我的量子专栏,这里不再涉及基础知识的科普。 量子相位反冲是什么? 相位反转(phase kickback)是量子计算中的一种现象,通常在量子算法中使用,例如量子…...

C++——C++11第三篇
目录 包装器 function包装器 bind 包装器 function包装器 function包装器 也叫作适配器。C中的function本质是一个类模板,也是一个包装器。 上面的程序验证,我们会发现useF函数模板实例化了三份。 包装器可以很好的解决上面的问题 ,让它只实…...

180 2 22222
选择题(共180题,合计180.0分) 1. 在项目开工会议期间,项目发起人告诉产品负责人和团队项目章程即将完成。然而,由于存在在紧迫的期限内满足政府监管要求的压力,发起人希望立即开始工作。产品负责人下一步应该做什么? A 告诉发起人…...

成人高考初中毕业能报名吗 需要什么条件
初中学历的人员不能直接报名成人高考,考生需要有普通高中,职业高中,中专毕业证等高中同等学力就可以进行报名,在报名期间登陆所在省的教育考试院的成人高考报名入口进行报考。成人高考报名条件是什么1、遵守宪法和法律。2、国家承…...

ChatGPT初体验
ChatGPT初体验 前言 嘿嘿,最近啊AI ChatGPT刷新各大网站,对于我们国人而将很不友好,真的太不友好了。我呢在去年open AI发布的时候就有所关注,那个时候还没有像现在这样火热。谁知道短短几个月便传遍大街小巷。 一、什么是chatG…...

ChatGPT概念狂飙!究竟魅力何在?
原文:http://www.btcwbo.com/6988.html 近期,ChatGPT引领的人工智能概念在资本市场一路狂飙,AIGC题材持续发酵。截至2月7日,Wind ChatGPT指数今年以来累计上涨超50%,汉王科技、海天瑞声、云从科技等概念股股价已经翻倍…...

如何下载阅读Spring源码-全过程详解
这篇文章记录了下载spring源码和在IDEA中打开运行的全过程,并且记录了过程中遇到的问题和解决方案,适合需要学习spring源码的同学阅读。 1.spring源码下载地址 通过Git下载spring-framework项目源码: git clone https://github.com/spring…...