GoLong的学习之路,进阶,微服务之使用,RPC包(包括源码分析)

2023-12-16

今天这篇是接上上篇RPC原理之后这篇是讲如何使用go本身自带的标准库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.B
    return 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.B
    quo.Rem = args.A % args.B
    return 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.

完整的例子:

  1. 创建一个 service.go 的文件用来保存结构体对象以及方法
package main

import "errors"

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	return 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.B
	quo.Rem = args.A % args.B
	return nil
}
  1. 创建一个 RPC 服务端, server.go
package main

import (
	"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 main

import (
	"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 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)
	// 异步调用
	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 使用默认的上下文路径`
      • 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. 对象重用。 Request Response 都是可重用的,通过 Lock 处理竞争。这在 大并发 的情况下很有效。

  2. 使用了大量的 goroutine 。如果使用一定数量的 goroutine 作为 worker池 去处理这个case,可能还会有些性能的提升,但是更复杂了。使用 goroutine 可以获得了非常好的性能。

  3. 业务处理是异步的,服务的执行不会阻塞其它消息的读取。

  4. 一个 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{}) error
    func (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) *Client
    func 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)).Done
    return 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.Method
    c.mutex.Lock()
    c.seq++
    c.pending[c.seq] = c.req.Id
    c.req.Id = nil
    r.Seq = c.seq
    c.mutex.Unlock()
    return nil
}

使用JSON协议的RPC

rpc 包默认使用的是 gob 协议 对传输数据进行 序列化/反序列化 ,比较有 局限性

将例子进行修改:
服务器端:

package main

import (
	"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 main

import (
	"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 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)
	// 异步调用
	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调用更方便。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

GoLong的学习之路,进阶,微服务之使用,RPC包(包括源码分析) 的相关文章

  • 软件测试/人工智能丨利用 ChatGPT 编写测试报告

    以百度为例 如何使用ChatGPT编写测试报告 测试报告 项目信息 项目名称 百度搜索引擎测试 测试阶段 总体功能测试 测试日期 MM DD YYYY MM DD YYYY 测试人员 测试团队成员名单 测试目标 验证百度搜索引擎的核心功能
  • 【UE5】初识MetaHuman 创建虚拟角色

    步骤 在UE5工程中启用 Quixel Bridge 插件 打开 Quixel Bridge 点击 MetaHumans MetaHuman Presets UE5 点击 START MHC 在弹出的网页中选择一个虚幻引擎版本 然后点击 启

随机推荐

  • 【UE5】监控摄像头效果(下)

    目录 效果 步骤 一 多摄像机视角切换 二 摄像头自动旋转巡视 三 摄像头跟踪拍摄 效果 步骤 一 多摄像机视角切换 1 打开玩家控制器 MyPlayerController 添加一个变量 命名为 BP SecurityCameraArra
  • 【UE5.2】通过Water插件使物体漂浮在水面上

    效果 步骤 1 新建一个工程 创建一个Basic关卡 添加初学者内容包到内容浏览器 2 在插件中启用 Water 插件 然后重启工程 3 重启后提示 碰撞描述文件设置不包括水体碰撞描述文件的条目 水碰撞必须使用该描述文件才能正常工作 将条目
  • 软件测试/测试开发丨人工智能算法的基本原理,如何解决实际的问题

    人工智能 AI 算法的基本原理涉及模仿人类智能行为的计算机程序和模型 这些算法通常通过学习和适应从数据中提取规律来解决实际问题 以下是一些常见的人工智能算法以及它们的基本原理 监督学习算法 图像识别 问题 识别图像中的数字 算法 使用卷积神
  • 【UE】在蓝图中修改材质实例的参数的两种方式

    目录 方式一 通过 在材质上设置标量 向量参数值 节点实现 方式二 通过 设置标量 向量参数值 节点实现 方式一 通过 在材质上设置标量 向量参数值 节点实现 1 在材质中设置了两个参数 2 创建材质实例 3 创建一个蓝图 对静态网格体赋予
  • 【UE】制作物体逐渐溶解消失并且可以复原的效果

    效果 步骤 1 新建一个工程 创建一个Basic关卡 添加第三人称游戏和初学者内容包资源到内容浏览器 2 找到并打开初学者内容包中椅子的材质 M Chair 将混合模式改为 已遮罩 在材质图表中添加如下节点 此时我们就可以通过参数 Fade
  • 【UE 材质】角色触碰空气墙效果

    效果 步骤 1 新建一个工程 创建一个Basic关卡 添加一个第三人称游戏资源到内容浏览器 2 新建一个材质参数集 这里命名为 MPC Vector 打开 MPC Vector 添加一个向量参数 3 新建一个材质 这里命名为 M Wall
  • 【UE5.1 MetaHuman】使用mixamo_converter把Mixamo的动画重定向给MetaHuman使用

    目录 前言 效果 步骤 一 下载mixamo converter软件 二 Mixamo动画重定向 三 导入UE 四 动画重定向 五 使用重定向后的动画 前言 上一篇 UE5 初识MetaHuman 创建虚拟角色 中我们已经制作了一个Meta
  • 【UE】制作熔岩星球材质

    效果 步骤 1 新建一个工程 创建一个Basic关卡 添加第三人称游戏和初学者内容包资源到内容浏览器 2 新建一个材质 这里命名为 M Sun 打开 M Sun 添加两个Texture节点 纹理分别为 T Rock Basalt N 和 T
  • 【UE5】监控摄像头效果(上)

    目录 效果 步骤 一 视角切换 二 摄像头画面后期处理 三 在场景中显示摄像头画面 效果 步骤 一 视角切换 1 新建一个Basic关卡 添加第三人称游戏资源到项目浏览器 2 新建一个Actor蓝图 这里命名为 BP SecurityCam
  • pico示波器使用

    文章目录 Pico示波器保存波形 Pico示波器录制数据 Pico示波器解析CAN报文 Pico示波器保存波形 Pico示波器可以通过以下步骤保存波形 在示波器上选择要保存的波形 连接示波器到计算机上 可以使用USB或者Ethernet连接
  • 头歌——HBase 开发:使用Java操作HBase

    第1关 创建表 题目 任务描述 本关任务 使用 Java 代码在 HBase 中创建表 相关知识 为了完成本关任务 你需要掌握 1 如何使用 Java 连接 HBase 数据库 2 如何使用 Java 代码在 HBase 中创建表 如何使用
  • 【UE5】瞬移+马赛克过渡效果

    效果 步骤 1 新建一个工程 创建一个Basic关卡 2 添加第三人称游戏资源到内容浏览器 3 新建一个材质 这里命名为 M Pixel 打开 M Pixel 设置材质域为 后期处理 在材质图表中添加如下节点 此时效果如下 已经有马赛克的效
  • 【3DsMax】制作简单的骨骼动画

    效果 步骤 首先准备4个板子模型展开放置好 添加一个4段的骨骼 选中其中的一块板子添加蒙皮命令 在蒙皮的参数面板中 设置每块板子对应哪块骨骼 设置好后你可以发现此时就已经可以通过骨骼来控制模型了 接下来就可以制作动画 点击左下角 时间配置
  • 【UE】制作地月全息投影

    效果 步骤 1 在必应国际版上搜索 purlin noise 下载如下所示图片 再搜索 Earth Map 下载如下所示图片 再搜索 Moon 360 下载如下所示图片 这三张图片的资源链接如下 链接 https pan baidu com
  • python在车载电子测试方面的应用笔记【1】

    文章目录 在DataFrame中某列插入数据 并根据另一列查找是否存在某个字符串完全一样 在另一列插入对应数据的功能 删除DataFrame某列数据长度大于6的数据 使用 PyInstaller 打包成一个独立的 exe 文件 通过检索空格
  • 通过kubeadm方式安装k8s

    虚拟机最少是 2 core master内存最小3G node内存最小2G 要求的Docker版本是18 03 如果不是安装的docker ce 版本是过旧的 可以选择删除后重新安装 也可以重新创建一个虚拟机执行以下命令 简单方法 使用ma
  • Docker build 无法解析域名

    报错 Docker build 无法解析域名 报错 ERROR 2 12 RUN curl o etc yum repos d CentOS Base repo https mirrors aliyun com repo Centos 7
  • 安装 运行 gemmini 和chipyard

    安装gemmini 和chipyard过程 安装版本 chipyard 版本是1 8 1 gemmini版本0 7 0 tip 如果在base里安装conda lock觉得缓慢 可以新建新的环境时就指定安装conda lock conda
  • GoLong的学习之路,进阶,微服务之序列化协议,Protocol Buffers V3

    这章是接上一章 使用 RPC包 序列化中没有详细去讲 因为这一块需要看的和学习的地方很多 并且这一块是RPC中可以说是最重要的一块 也是性能的重要影响因子 今天这篇主要会讲其使用方式 文章目录 Protocol Buffers V3 背景以
  • GoLong的学习之路,进阶,微服务之使用,RPC包(包括源码分析)

    今天这篇是接上上篇RPC原理之后这篇是讲如何使用go本身自带的标准库RPC 这篇篇幅会比较短 重点在于上一章对的补充 文章目录 RPC包的概念 使用RPC包 服务器代码分析 如何实现的 总结 Server还提供了两个注册服务的方法