GoLong的学习之路,进阶,微服务之使用,RPC包(包括源码分析)
今天这篇是接上上篇RPC原理之后这篇是讲如何使用go本身自带的标准库RPC。这篇篇幅会比较短。重点在于上一章对的补充。
文章目录
- RPC包的概念
- 使用RPC包
- 服务器代码分析
- 如何实现的?
- 总结
- Server还提供了两个注册服务的方法
- 客户端代码分析
- 如何实现的?
- 如何异步编程同步?
- 总结
- codec/序列化框架
- 使用JSON协议的RPC
RPC包的概念
回顾RPC原理
看完回顾后其实就可以继续需了解并使用go中所提供的包。
Go语言的 rpc 包提供对通过网络或其他i/o连接导出的对象方法的访问,服务器注册一个对象,并把它作为服务对外可见(服务名称就是类型名称)。
注册后,对象的导出方法将支持远程访问。服务器可以注册不同类型的多个对象(服务) ,但是不支持注册同一类型的多个对象。
Go官方提供了一个RPC库: net/rpc。
包rpc提供了通过网络访问一个对象的输出方法的能力。
服务器需要注册对象,通过对象的类型名暴露这个服务。
注册后这个对象的输出方法就可以远程调用,这个库封装了底层传输的细节,包括序列化(默认GOB序列化器)。
对象的方法要能远程访问,它们必须满足一定的条件,否则这个对象的方法会被忽略:
- 方法的
类型是可输出的 - 方法
本身是可输出的 - 方法必须由两个参数,必须是输出类型或者是内建类型
- 方法的第二个参数必须是
指针类型 - 方法返回类型为
error
所以一个输出方法的格式如下:
func (t *T) MethodName(argType T1, replyType *T2) error
这里的T、T1、T2能够被encoding/gob序列化,即使使用其它的序列化框架,将来这个需求可能回被弱化。
- 第一个参数
(T1)代表调用者(client)提供的参数 - 第二个参数
(*T2)代表要返回给调用者的计算结果 - 方法的返回值如果不为空, 那么它作为一个
字符串返回给调用者(所以需要一个序列化框架) - 如果返回
error,则reply参数不会返回给调用者
使用RPC包
简单例子,是一个非常简单的服务。

我们在这个里面就搞1和12就好:
在这个例子中定义了一个简单的RPC服务器和客户端,使用的方法是一个
第一步需要定义传入参数和返回参数的数据结构
type Args struct {A, B int
}
type Quotient struct {Quo, Rem int
}
第二步定义一个服务对象,这个服务对象可以很简单。
比如类型是int或者是interface{},重要的是它输出的方法。
type Arith int
第三步实现这个类型的两个方法, 乘法和除法:
func (t *Arith) Multiply(args *Args, reply *int) error {*reply = args.A * args.Breturn nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {if args.B == 0 {return errors.New("divide by zero")}quo.Quo = args.A / args.Bquo.Rem = args.A % args.Breturn nil
}
第四步实现RPC服务器: 基于tcp实现
生成了一个Arith对象,并使用rpc.Register注册这个服务,然后通过HTTP暴露出来
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
l, e := net.Listen("tcp", ":9091")
if e != nil {log.Fatal("listen error:", e)
}
go http.Serve(l, nil)
select{
}
客户端可以看到服务Arith以及它的两个方法Arith.Multiply和Arith.Divide
第五步创建一个客户端,建立客户端和服务器端的连接: 分为同步调用和异步调用(都是远程调用)
同步调用:
client, err := rpc.DialHTTP("tcp", "127.0.0.1:9091")
if err != nil {log.Fatal("dialing:", err)
}args := &server.Args{7,8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d", args.A, args.B, reply)
异步调用:
client, err := rpc.DialHTTP("tcp", "127.0.0.1:9091")
if err != nil {log.Fatal("dialing:", err)
}
quotient := new(Quotient)
divCall := client.Go("Arith.Divide", args, quotient, nil)
replyCall := <-divCall.Done // will be equal to divCall
// check errors, print, etc.
完整的例子:
- 创建一个
service.go的文件用来保存结构体对象以及方法
package mainimport "errors"type Args struct {A, B int
}type Quotient struct {Quo, Rem int
}type Arith intfunc (t *Arith) Multiply(args *Args, reply *int) error {*reply = args.A * args.Breturn nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {if args.B == 0 {return errors.New("divide by zero")}quo.Quo = args.A / args.Bquo.Rem = args.A % args.Breturn nil
}
- 创建一个
RPC服务端,server.go
package mainimport ("log""net""net/http""net/rpc"
)func main() {arith := new(Arith)rpc.Register(arith)rpc.HandleHTTP()l, e := net.Listen("tcp", ":9091")if e != nil {log.Fatal("listen error:", e)}go http.Serve(l, nil)select {}
}
- 创建一个客户端,
client.go
package mainimport ("fmt""log""net/rpc"
)func main() {// 建立HTTP连接client, err := rpc.DialHTTP("tcp", "127.0.0.1:9091")if err != nil {log.Fatal("dialing:", err)}// 同步调用args := &Args{7, 8}var reply interr = client.Call("Arith.Multiply", args, &reply)if err != nil {log.Fatal("arith error:", err)}fmt.Printf("Arith: %d*%d=%d", args.A, args.B, reply)// 异步调用quotient := new(Quotient)divCall := client.Go("Arith.Divide", args, quotient, nil)replyCall := <-divCall.Done // will be equal to divCall// check errors, print, etc.fmt.Println(replyCall.Error)fmt.Println(quotient)
}
打开终端:
先启动服务器:
go run server.go service.go
在打开一个终端:
最后启动一个客户端:
go run client.go service.go
结果为:

服务器代码分析
Server的很多方法你可以直接调用,这对于一个简单的Server的实现更方便,但是你如果需要配置不同的Server,
比如不同的监听地址或端口,就需要自己生成Server:
var DefaultServer = NewServer()
Server有多种Socket监听的方式:
func (server *Server) Accept(lis net.Listener)func (server *Server) HandleHTTP(rpcPath, debugPath string)func (server *Server) ServeCodec(codec ServerCodec)func (server *Server) ServeConn(conn io.ReadWriteCloser)func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request)func (server *Server) ServeRequest(codec ServerCodec) error
ServeHTTP: 实现了处理http请求的业务逻辑,它首先处理http的CONNECT请求, 接收后就Hijacker这个连接conn, 然后调用ServeConn在这个连接上处理这个客户端的请求。- 其实是实现了
http.Handler接口,我们一般不直接调用这个方法。Server.HandleHTTP设置rpc的上下文路径rpc.HandleHTTP使用默认的上下文路径`DefaultRPCPathDefaultDebugPath
- 当你启动一个
http server的时候http.ListenAndServe,面设置的上下文将用作RPC传输,这个上下文的请求会教给ServeHTTP来处理 - 以上是
RPC over http的实现,可以看出net/rpc只是利用http CONNECT建立连接,这和普通的RESTful api还是不一样的。 - (源码)
- 其实是实现了
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {if req.Method != "CONNECT" {w.Header().Set("Content-Type", "text/plain; charset=utf-8")w.WriteHeader(http.StatusMethodNotAllowed)io.WriteString(w, "405 must CONNECT\n")return}conn, _, err := w.(http.Hijacker).Hijack()if err != nil {log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error())return}io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")server.ServeConn(conn)
}
如何实现的?
Accept用来处理一个监听器,一直在监听客户端的连接,一旦监听器接收了一个连接,则还是交给ServeConn在另外一个goroutine中去处理:(源码)
//Accept接受侦听器上的连接并提供请求
//每个传入连接。接受阻塞,直到监听器
//返回非nil错误。对象中调用Accept
//go语句
func (server *Server) Accept(lis net.Listener) {for {conn, err := lis.Accept()if err != nil {log.Print("rpc.Serve: accept:", err.Error())return}go server.ServeConn(conn)}
}
协程进入ServeConn可以看出,很重要的一个方法就是ServeConn
// ServeConn在单连接上运行服务器。
// ServeConn阻塞,服务连接,直到客户端挂起。
//调用者通常在go语句中调用ServeConn。
// ServeConn使用gob连接格式(参见包gob)
//连接。要使用备用编解码器,请使用ServeCodec。
//有关并发访问的信息,请参阅NewClient的注释。.
func (server *Server) ServeConn(conn io.ReadWriteCloser) {buf := bufio.NewWriter(conn)srv := &gobServerCodec{rwc: conn,dec: gob.NewDecoder(conn),enc: gob.NewEncoder(buf),encBuf: buf,}server.ServeCodec(srv)
}
连接其实是交给一个ServerCodec去处理,这里默认使用gobServerCodec去处理,这是一个未输出默认的编解码器,可以使用其它的编解码器。
// ServeCodec类似于ServeConn,但使用指定的编解码器来
//解码请求和编码响应。
func (server *Server) ServeCodec(codec ServerCodec) {sending := new(sync.Mutex)wg := new(sync.WaitGroup)for {service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec)if err != nil {if debugLog && err != io.EOF {log.Println("rpc:", err)}if !keepReading {break}// send a response if we actually managed to read a header.if req != nil {server.sendResponse(sending, req, invalidRequest, codec, err.Error())server.freeRequest(req)}continue}wg.Add(1)go service.call(server, sending, wg, mtype, req, argv, replyv, codec)}//我们已经看到没有更多的请求。//在关闭编解码器之前等待响应。wg.Wait()codec.Close()
}
它其实一直从连接中读取请求,然后调用go service.call在另外的goroutine中处理服务调用。
总结
-
对象重用。
Request和Response都是可重用的,通过Lock处理竞争。这在大并发的情况下很有效。 -
使用了大量的
goroutine。如果使用一定数量的goroutine作为worker池去处理这个case,可能还会有些性能的提升,但是更复杂了。使用goroutine可以获得了非常好的性能。 -
业务处理是异步的,服务的执行不会阻塞其它消息的读取。
-
一个
codec实例必然和一个connnection相关,因为它需要从connection中读取request和发送response。
go的rpc官方库的消息(request和response)的定义很简单, 就是消息头(header)+内容体(body)。
消息体是reply类型的序列化后的值。
type Request struct {ServiceMethod string // format: "Service.Method"Seq uint64 // 客户端选择的序列号// 包含过滤或未导出的字段
}
Server还提供了两个注册服务的方法
第二个方法为服务起一个别名,否则服务名已它的类型命名
func (server *Server) Register(rcvr interface{}) errorfunc (server *Server) RegisterName(name string, rcvr interface{}) error
它们俩底层调用register进行服务的注册(这里的源码太多就不放了)
func (server *Server) register(rcvr interface{}, name string, useName bool) error
受限于Go语言的特点,我们不可能在接到客户端的请求的时候,根据反射动态的创建一个对象,就是Java那样。
因此在Go语言中,我们需要预先创建一个服务map这是在编译的时候完成的
说白了这里需要建立一个注册名与服务之间的映射关系
server.serviceMap = make(map[string]*service)
同时每个服务还有一个方法map: map[string]*methodType,通过suitableMethods建立映射:
func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType
这样rpc在读取请求header,通过查找这两个map,就可以得到要调用的服务及它的对应方法了。
func (s *service) call(server *Server, sending *sync.Mutex, wg *sync.WaitGroup, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) {if wg != nil {defer wg.Done()}mtype.Lock()mtype.numCalls++mtype.Unlock()function := mtype.method.Func// 调用该方法,为应答提供一个新值。returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv})// 该方法的返回值是一个错误。.errInter := returnValues[0].Interface()errmsg := ""if errInter != nil {errmsg = errInter.(error).Error()}server.sendResponse(sending, req, replyv.Interface(), codec, errmsg)server.freeRequest(req)
}
客户端代码分析
客户端要建立和服务器的连接
func Dial(network, address string) (*Client, error)func DialHTTP(network, address string) (*Client, error)func DialHTTPPath(network, address, path string) (*Client, error)func NewClient(conn io.ReadWriteCloser) *Clientfunc NewClientWithCodec(codec ClientCodec) *Client
如何实现的?
DialHTTP 和 DialHTTPPath是通过HTTP的方式和服务器建立连接,他俩的区别之在于是否设置上下文路径:
// DialHTTPPath连接HTTP RPC服务器在指定的网络地址和路径上
func DialHTTPPath(network, address, path string) (*Client, error) {conn, err := net.Dial(network, address)if err != nil {return nil, err}io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\n\n")// 在切换到RPC协议之前,需要成功的HTTP响应resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})if err == nil && resp.Status == connected {return NewClient(conn), nil}if err == nil {err = errors.New("unexpected HTTP response: " + resp.Status)}conn.Close()return nil, &net.OpError{Op: "dial-http",Net: network + " " + address,Addr: nil,Err: err,}
}
首先发送 CONNECT 请求,如果连接成功则通过NewClient(conn)创建client。
Dial则通过TCP直接连接服务器
// Dial连接到指定网络地址的RPC服务器
func Dial(network, address string) (*Client, error) {conn, err := net.Dial(network, address)if err != nil {return nil, err}return NewClient(conn), nil
}
注意:根据服务是over HTTP还是 over TCP选择合适的连接方式
NewClient则创建一个缺省codec为glob序列化库的客户端
// NewClient返回一个新的Client来处理到连接另一端的服务集合。
//在连接的写端添加一个缓冲区,报头和有效载荷作为一个单元发送。
//
//连接的读写部分是独立序列化的,不需要联锁。然而,每一半都可以访问并发,所以conn的实现应该防止,并发读或并发写。
func NewClient(conn io.ReadWriteCloser) *Client {encBuf := bufio.NewWriter(conn)client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf}return NewClientWithCodec(client)
}
如果想用其它的序列化库,你可以调用NewClientWithCodec方法 一般用来做RPC框架的
// NewClientWithCodec类似于NewClient,但使用指定的编码请求和解码响应。
func NewClientWithCodec(codec ClientCodec) *Client {client := &Client{codec: codec,pending: make(map[uint64]*Call),}go client.input()return client
}
重要的是input方法,它以一个死循环的方式不断地从连接中读取response,然后调用map中读取等待的Call.Done channel通知完成。(这个其实有点令牌扫描的作用,消息队列中有说)
消息的结构和服务器一致,都是Header+Body的方式
客户端的调用有两个方法: Go 和 Call
Go方法是异步的,它返回一个Call指针对象, 它的Done是一个channel,如果服务返回,Done就可以得到返回的对象(实际是Call对象,包含Reply和error信息)。Call 方法是同步的,它实际是调用Go实现的。
如何异步编程同步?
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Donereturn call.Error
}
从一个Channel中读取对象会被阻塞住,直到有对象可以读取,这种实现很简单,也很方便。
总结
从源码中:我们还可以学到锁Lock的一种实用方式,也就是尽快的释放锁,而不是defer mu.Unlock直到函数执行到最后才释放,那样锁占用的时间太长了。
codec/序列化框架
rpc框架默认使用gob序列化库,很多情况下我们追求更好的效率的情况下,或者追求更通用的序列化格式,我们可能采用其它的序列化方式, 比如protobuf, json, xml等。
市面上也有许多序列化框架。速度快而且非常好用。gRPC是互联网后台常用的RPC框架,其内部是由protobuf协议完成通讯。这个后面再讲。
(JDK Serializable、FST、Kryo、Protobuf、Thrift、Hession和Avro,Fury)

Fury是最新的序列化框架:号称比jdk 快170倍,后面会讲的 支持多种语言
Go官方库实现了JSON-RPC 1.0。JSON-RPC是一个通过JSON格式进行消息传输的RPC规范,因此可以进行跨语言的调用。
Go的net/rpc/jsonrpc库可以将JSON-RPC的请求转换成自己内部的格式:
func (c *serverCodec) ReadRequestHeader(r *rpc.Request) error {c.req.reset()if err := c.dec.Decode(&c.req); err != nil {return err}r.ServiceMethod = c.req.Methodc.mutex.Lock()c.seq++c.pending[c.seq] = c.req.Idc.req.Id = nilr.Seq = c.seqc.mutex.Unlock()return nil
}
使用JSON协议的RPC
rpc 包默认使用的是 gob 协议对传输数据进行序列化/反序列化,比较有局限性。
将例子进行修改:
服务器端:
package mainimport ("log""net""net/rpc""net/rpc/jsonrpc"
)func main() {arith := new(Arith)rpc.Register(arith)l, e := net.Listen("tcp", ":9091")if e != nil {log.Fatal("listen error:", e)}for {conn, _ := l.Accept()// 使用JSON协议rpc.ServeCodec(jsonrpc.NewServerCodec(conn))}
}
客户端:
package mainimport ("fmt""log""net""net/rpc""net/rpc/jsonrpc"
)func main() {// 建立HTTP连接conn, err := net.Dial("tcp", "127.0.0.1:9091")if err != nil {log.Fatal("dialing:", err)}client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))// 同步调用args := &Args{7, 8}var reply interr = client.Call("Arith.Multiply", args, &reply)if err != nil {log.Fatal("arith error:", err)}fmt.Printf("Arith: %d*%d=%d", args.A, args.B, reply)// 异步调用quotient := new(Quotient)divCall := client.Go("Arith.Divide", args, quotient, nil)replyCall := <-divCall.Done // will be equal to divCall// check errors, print, etc.fmt.Println(replyCall.Error)fmt.Println(quotient)
}
如何使用与上面的例子一致。
社区中各式RPC框架(grpc、thrift等)就是为了让RPC调用更方便。
相关文章:
GoLong的学习之路,进阶,微服务之使用,RPC包(包括源码分析)
今天这篇是接上上篇RPC原理之后这篇是讲如何使用go本身自带的标准库RPC。这篇篇幅会比较短。重点在于上一章对的补充。 文章目录 RPC包的概念使用RPC包服务器代码分析如何实现的?总结Server还提供了两个注册服务的方法 客户端代码分析如何实现的?如何异步…...
uniapp x 相比于其他的开发系统框架怎么样?
首先我们要知道niapp这是一种基于Vue.js开发的跨平台应用框架,可以将同一套代码同时运行在多个平台上,包括iOS、Android、H5等。相比其他开发系统框架,他有什么优点呢?让我们共同探讨一下吧! 图片来源:unia…...
2024最新独立站建站教程!WordPress 搭建独立站的方法和步骤
不知道大家是否听说过 WordPress ?最近有个国外博主分享,她60岁的奶奶居然用WordPress建了个关于她宠物日常的小博客,看来 WordPress 在国外真的是很普及。其实,国外很多商家还利用 WordPress 搭建自己的电商网站,那说…...
深入React Flow Renderer(二):构建拖动操作栏
在上一篇博客中,我们介绍了如何启动React Flow Renderer并创建一个基本的工作流界面。本文将进一步深入,着重讨论如何构建一个可拖动的操作栏,它是用户与工作流交互的入口之一。 引言 操作栏是工作流界面的一部分,通常位于界面的…...
Java项目学生管理系统六后端补充
班级管理 1 班级列表:后端 编写JavaBean【已有】编写Mapper【已有】编写Service编写controller 编写Service 接口 package com.czxy.service;import com.czxy.domain.Classes;import java.util.List;/*** author 桐叔* email liangtongitcast.cn* description*/ p…...
PDF控件Spire.PDF for .NET【转换】演示:将 PDF 转换为线性化
PDF 线性化,也称为“快速 Web 查看”,是一种优化 PDF 文件的方法。通常,只有当用户的网络浏览器从服务器下载了所有页面后,用户才能在线查看多页 PDF 文件。然而,如果 PDF 文件是线性化的,即使完整下载尚未…...
猫头虎博主深度探索:Amazon Q——2023 re:Invent大会的AI革新之星
猫头虎博主深度探索:Amazon Q——2023 re:Invent大会的AI革新之星 授权说明:本篇文章授权活动官方亚马逊云科技文章转发、改写权,包括不限于在 亚马逊云科技开发者社区, 知乎,自媒体平台,第三方开发者媒体等亚马逊云科…...
Spring框架-GOF代理模式之JDK动态代理
我们可以分成三步来完成jdk动态代理的实现 第一步:创建目标对象 第二步:创建代理对象 第三步:调用代理对象的代理方法 public class Client {public static void main(String[] args) {//创建目标对象final OrderService target new OrderS…...
基于JAVAEE技术校园车辆管理系统论文
摘 要 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本校园车辆管理系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息…...
基于FFmpeg,实现播放器功能
一、客户端选择音视频文件 MainActivity package com.anniljing.ffmpegnative;import android.Manifest; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Ur…...
利用tf-idf对特征进行提取
TF-IDF是一种文本特征提取的方法,用于评估一个词在一组文档中的重要性。 一、代码 from sklearn.feature_extraction.text import TfidfVectorizer import numpy as npdef print_tfidf_words(documents):"""打印TF-IDF矩阵中每个文档中非零值对应…...
遇到运维故障,有没有排查和解决故障的正确流程?
稳定是偶然,异常才是常态,用来标注IT运维工作再适合不过。 因为对于IT运维来说,工作最常遇到的就是不稳定性带来的各种故障,经常围绕发现故障、响应故障、定位故障、恢复故障这四大步。 故障处理是最心跳的事情,没有…...
javaWebssh汽车销售管理系统myeclipse开发mysql数据库MVC模式java编程计算机网页设计
一、源码特点 java ssh汽车销售管理系统是一套完善的web设计系统(系统采用ssh框架进行设计开发),对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用 B/S模式开发。开发环境为TOMCAT7.…...
基于pandoraNext使用chatgpt4
1.登陆GitHub 获取pandoraNext项目GitHub - pandora-next/deploy: Pandora Cloud Pandora Server Shared Chat BackendAPI Proxy Chat2API Signup Free PandoraNext. New GPTs(Gizmo) UI, All in one! 在release中选择相应版本操作系统的安装包进行下载 2.获取license_…...
12.视图
目录 1.视图的含义与作用 2.视图的创建与查看 1.创建视图的语法形式 2、查看视图: 1.使用DESCRIBE语句查看视图基本信息 2.使用SHOW TABLE STATUS语查看视图基本信息查看视图的信息 3.使用SHOW CREATE VIEW语查看视图详细信息 4.在views表中查看视图详细信息…...
Leetcode69 x的平方根
x的平方根 题解1 袖珍计算器算法题解2 二分查找题解3 牛顿迭代 给你一个非负整数 x ,计算并返回 x 的 算术平方根 。 由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。 注意:不允许使用任何内置指数函数和算符&…...
在Linux上安装配置Nginx高性能Web服务器
1 前言 Nginx是一个高性能的开源Web服务器,同时也可以作为反向代理服务器、负载均衡器、HTTP缓存以及作为一个邮件代理服务器。它以其出色的性能和灵活性而闻名,被广泛用于处理高流量的网站和应用程序。本文将介绍在Linux环境中安装Nginx的步骤…...
LeetCode 每日一题 Day 11||贪心
2697. 字典序最小回文串 给你一个由 小写英文字母 组成的字符串 s ,你可以对其执行一些操作。在一步操作中,你可以用其他小写英文字母 替换 s 中的一个字符。 请你执行 尽可能少的操作 ,使 s 变成一个 回文串 。如果执行 最少 操作次数的方…...
ocr表格文字识别软件怎么使用?
现在的OCR软件几乎是傻瓜式的设计,操作很简单,像金鸣识别的软件无论是网页版还是电脑客户端又或是小程序,界面都简单明了,用户只需提交待识别的图片,然后点击提交识别,等识别完成就直接打开或下载打开就行了…...
【QT 5 调试软件+Linux下调用脚本shell-经验总结+初步调试+基础样例】
【QT 5 调试软件Linux下调用脚本shell-经验总结初步调试基础样例】 1、前言2、实验环境3、自我总结4、实验过程(1)准备工作-脚本1)、准备工作-编写运行脚本文件2)、给权限3)、运行脚本 (2)进入q…...
iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版分享
平时用 iPhone 的时候,难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵,或者买了二手 iPhone 却被原来的 iCloud 账号锁住,这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院挂号小程序
一、开发准备 环境搭建: 安装DevEco Studio 3.0或更高版本配置HarmonyOS SDK申请开发者账号 项目创建: File > New > Create Project > Application (选择"Empty Ability") 二、核心功能实现 1. 医院科室展示 /…...
Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
JVM虚拟机:内存结构、垃圾回收、性能优化
1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...
【从零学习JVM|第三篇】类的生命周期(高频面试题)
前言: 在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期,让读者对此有深刻印象。 目录 …...
Linux系统部署KES
1、安装准备 1.版本说明V008R006C009B0014 V008:是version产品的大版本。 R006:是release产品特性版本。 C009:是通用版 B0014:是build开发过程中的构建版本2.硬件要求 #安全版和企业版 内存:1GB 以上 硬盘…...
GraphQL 实战篇:Apollo Client 配置与缓存
GraphQL 实战篇:Apollo Client 配置与缓存 上一篇:GraphQL 入门篇:基础查询语法 依旧和上一篇的笔记一样,主实操,没啥过多的细节讲解,代码具体在: https://github.com/GoldenaArcher/graphql…...
