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

這里的TT1T2能夠被encoding/gob序列化,即使使用其它的序列化框架,將來這個需求可能回被弱化。

  • 第一個參數(T1)代表調用者(client)提供的參數
  • 第二個參數(*T2)代表要返回給調用者的計算結果
  • 方法的返回值如果不為空, 那么它作為一個字符串返回給調用者(所以需要一個序列化框架)
  • 如果返回error,則reply參數不會返回給調用者

使用RPC包

簡單例子,是一個非常簡單的服務。

在這里插入圖片描述
我們在這個里面就搞112就好:

在這個例子中定義了一個簡單的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.MultiplyArith.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.

完整的例子:

  1. 創建一個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
}
  1. 創建一個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 {}
}
  1. 創建一個客戶端,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請求的業務邏輯,它首先處理httpCONNECT請求, 接收后就Hijacker這個連接conn, 然后調用ServeConn在這個連接上處理這個客戶端的請求。
    • 其實是實現了 http.Handler接口,我們一般不直接調用這個方法。
      • Server.HandleHTTP設置rpc的上下文路徑
      • rpc.HandleHTTP使用默認的上下文路徑`
      • DefaultRPCPath
      • DefaultDebugPath
    • 當你啟動一個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中處理服務調用。

總結

  1. 對象重用。 RequestResponse都是可重用的,通過Lock處理競爭。這在大并發的情況下很有效。

  2. 使用了大量的goroutine。如果使用一定數量的goroutine作為worker池去處理這個case,可能還會有些性能的提升,但是更復雜了。使用goroutine可以獲得了非常好的性能。

  3. 業務處理是異步的,服務的執行不會阻塞其它消息的讀取。

  4. 一個codec實例必然和一個connnection相關,因為它需要從connection中讀取request和發送response

go的rpc官方庫的消息(requestresponse)的定義很簡單, 就是消息頭(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

如何實現的?

DialHTTPDialHTTPPath是通過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則創建一個缺省codecglob序列化庫的客戶端

// 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的方式

客戶端的調用有兩個方法: GoCall

  • Go方法是異步的,它返回一個 Call指針對象, 它的Done是一個channel,如果服務返回,Done就可以得到返回的對象(實際是Call對象,包含Replyerror信息)。
  • 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 SerializableFSTKryoProtobufThriftHessionAvroFury
在這里插入圖片描述

Fury是最新的序列化框架:號稱比jdk 快170倍,后面會講的 支持多種語言

Go官方庫實現了JSON-RPC 1.0JSON-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調用更方便。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/210816.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/210816.shtml
英文地址,請注明出處:http://en.pswp.cn/news/210816.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

nginx配置正向代理支持https

操作系統版本&#xff1a; Alibaba Cloud Linux 3.2104 LTS 64位 nginx版本&#xff1a; nginx-1.25.3 1. 下載軟件 切換目錄 cd /server wget http://nginx.org/download/nginx-1.25.3.tar.gz 1.1解壓 tar -zxvf nginx-1.25.3.tar.gz 1.2切換到源碼所在目錄…

【探索Linux】—— 強大的命令行工具 P.21(多線程 | 線程同步 | 條件變量 | 線程安全)

閱讀導航 引言一、線程同步1. 競態條件的概念2. 線程同步的概念 二、條件變量1. 條件變量函數?使用前提&#xff08;1&#xff09;初始化條件變量&#xff08;2&#xff09;等待條件滿足&#xff08;3&#xff09;喚醒等待pthread_cond_broadcast()pthread_cond_signal() &…

JavaGUI詳解

GUI Java GUI**1、Java GUI 概述****2、容器****2、1 窗口****2、2 彈窗和對話框****對話框****自定義彈窗** **2、3 面板****普通面板****滾動面板****分隔面板****選項卡面板** **3、布局****3.1、流式布局****3.2、網格布局****3.3、邊框布局****4、組件****4.1、基本組件**…

Steampipe的安裝部署及簡單使用(附帶AWS CLI的安裝與使用)

介紹 Steampipe 將 API 和服務公開為高性能關系數據庫&#xff0c;使您能夠編寫基于 SQL 的查詢來探索動態數據。Mods 通過使用簡單 HCL 構建的儀表板、報告和控件擴展了 Steampipe 的功能。 官網&#xff1a;https://steampipe.io/ steampipe的安裝 下載腳本并執行 sudo /…

Unity優化——批處理的優勢

大家好&#xff0c;這里是七七&#xff0c;前段時間在忙一些事情&#xff0c;最近終于有空來更新優化篇了。本文本打算分為上下兩篇&#xff0c;但為了看更方便&#xff0c;就多花了幾天寫成一文發布&#xff0c;具體是介紹了圖形優化中批處理的具體效果&#xff0c;雖然本文篇…

【Linux】cat 命令使用

cat 命令 cat&#xff08;英文全拼&#xff1a;concatenate&#xff09;命令用于連接文件并打印到標準輸出設備上。 可以使用cat連接多個文件、創建新文件、將內容附加到現有文件、查看文件內容以及重定向終端或文件中的輸出。 cat可用于在不同選項的幫助下格式化文件的輸出…

LV.13 D1 嵌入式系統移植導學 學習筆記

一、嵌入式系統分層 操作系統&#xff1a;向下管理硬件、向上提供接口 操作系統為我們提供了&#xff1a; 1.進程管理 2.內存管理 3.網絡接口 4.文件系統 5.設備管理 那系統移植是干什么呢&#xff1f; 就是將Linux操作系統移植到基于ARM處理器的開發板中。 那為什么要移植系…

【calcitonin ; 降鈣素 ;降鈣素原】

Parathyroid_Hormone -甲狀旁腺激素 PTH &#xff1b; 特立帕肽&#xff1b;

『OPEN3D』1.8.2 全局ICP配準

前文提到的多種icp方式均需要初始的變換函數作為配準過程的初始值,并在該初始值上進行迭代優化得到結果;那么global icp為前面這些精配準的icp提供了初始變換函數。因此global ICP配準后可視化的點云結果可能沒有完全配準,需要再進行一次精配準操作。 global icp需要對點云提…

lightdb plorasql集合類型新增可變數組

文章目錄 背景集合類型可變數組可變數組示例 背景 在信創適配中&#xff0c;從Oracle遷移過來的存儲過程使用到可變數組。因此在LightDB-X 23.4版本中對現有的集合類型進行了增強&#xff0c;添加了可變數組類型。 集合類型 在LightDB-X 23.4版本開始plorasql支持的集合類型…

【SQL開發實戰技巧】系列(四十八):Oracle12C常用新特性?多分區操作和管理

系列文章目錄 【SQL開發實戰技巧】系列&#xff08;一&#xff09;:關于SQL不得不說的那些事 【SQL開發實戰技巧】系列&#xff08;二&#xff09;&#xff1a;簡單單表查詢 【SQL開發實戰技巧】系列&#xff08;三&#xff09;&#xff1a;SQL排序的那些事 【SQL開發實戰技巧…

K8s構建的mysql無法遠程連接

最近在寫一個老師布置的大作業&#xff0c;都是老師寫好的yaml文件&#xff0c;都是沒問題的&#xff0c;但是構建的mysql無法遠程連接。 嘗試了網上的很多方法&#xff0c;都失敗了&#xff0c;我的構建過程應該是沒什么錯誤的&#xff0c;所以網上的方法并不奏效&#xff0c…

【小白專用】Sql Server 連接Mysql 更新23.12.09

目標 已知mysql連接參數&#xff08;地址和用戶&#xff09;&#xff0c;期望通過Microsoft Sql Server Management Studio &#xff08;以下簡稱MSSSMS&#xff09;連接Mysql&#xff0c;在MSSSMS中直接查詢或修改Mysql中的數據。 一般是選最新的版本下載。 選64位還是32位&a…

C++ 對象的初始化和清理:構造函數和析構函數

目錄 構造函數和析構函數 構造函數 析構函數 構造函數的分類及調用 括號法 顯示法 隱式轉換法 拷貝構造函數的調用時機 使用一個已經創建完畢的對象來初始化一個新對象 值傳遞的方式給函數參數傳值 以值方式返回局部對象 構造函數調用規則 初始化列表 類對象作…

【Java 基礎】27 XML 解析

文章目錄 1.SAX 解析器1&#xff09;什么是 SAX2&#xff09;SAX 工作流程初始化實現事件處理類解析 3&#xff09;示例代碼 2.DOM 解析器1&#xff09;什么是 DOM2&#xff09;DOM 工作流程初始化解析 XML 文檔操作 DOM 樹 3&#xff09;示例代碼 總結 在項目開發中&#xff0…

Jupyter notebook修改背景主題

打開Anaconda Prompt&#xff0c;輸入以下內容 1. pip install --upgrade jupyterthemes 下載對應背景主題包 出現Successfully installed jupyterthemes-0.20.0 lesscpy-0.15.1時&#xff0c;說明已經下載安裝完成 2. jt -l 查看背景主題列表 3. jt -t 主題名稱&#xff08;…

【LeeCode】18.四數之和

給你一個由 n 個整數組成的數組 nums &#xff0c;和一個目標值 target 。請你找出并返回滿足下述全部條件且不重復的四元組 [nums[a], nums[b], nums[c], nums[d]] &#xff08;若兩個四元組元素一一對應&#xff0c;則認為兩個四元組重復&#xff09;&#xff1a; 0 < a, …

mysql的BIT數值類型

MySQL :: MySQL 8.2 Reference Manual :: 11.1.5 Bit-Value Type - BIT MySQL :: MySQL 8.2 Reference Manual :: 9.1.5 Bit-Value Literals BIT類型用來存放bit值&#xff0c;每一位是0或者1&#xff0c;允許1-64位。 例如&#xff0c;下面表定義了new這列的類型為8位的BIT…

NestJS的微服務實現

1.1 基本概念 微服務基本概念&#xff1a;微服務就是將一個項目拆分成多個服務。舉個簡單的例子&#xff1a;將網站的登錄功能可以拆分出來做成一個服務。 微服務分為提供者和消費者&#xff0c;如上“登錄服務”就是一個服務提供者&#xff0c;“網站服務器”就是一個服務消…

Python如何實現數據驅動的接口自動化測試

大家在接口測試的過程中&#xff0c;很多時候會用到對CSV的讀取操作&#xff0c;本文主要說明Python3對CSV的寫入和讀取。下面話不多說了&#xff0c;來一起看看詳細的介紹吧。 1、需求 某API&#xff0c;GET方法&#xff0c;token,mobile,email三個參數 token為必填項mobil…