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

news2024/11/27 8:27:42

今天这篇是接上上篇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.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.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 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请求的业务逻辑,它首先处理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{}) 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

如何实现的?

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)).Done
    return 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.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调用更方便。

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

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

相关文章

kali linux无法使用root打开vlc观看视频的解决办法

kali linux陆续装了几个视频播放器&#xff0c;都比较不够友好&#xff0c;无奈安装vlc,vlc安装方法就是 apt install vlc这个没什么好说的&#xff0c;多数源都集成这个著名软件了&#xff0c;kali linux打开闪退&#xff0c;终端下运行出现&#xff1a; VLC is not supposed…

【数学建模】《实战数学建模:例题与讲解》第七讲-Bootstrap方法(含Matlab代码)

【数学建模】《实战数学建模&#xff1a;例题与讲解》第七讲-Bootstrap方法&#xff08;含Matlab代码&#xff09; 基本概念习题7.31. 题目要求2.解题过程3.程序4.结果 习题7.51. 题目要求2.解题过程3.程序4.结果 如果这篇文章对你有帮助&#xff0c;欢迎点赞与收藏~ 基本概念…

软件设计师——计算机网络(二)

&#x1f4d1;前言 本文主要是【计算机网络】——软件设计师——计算机网络的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &#x1…

Unity DOTS中的baking(一) Baker简介

Unity DOTS中的baking&#xff08;一&#xff09; Baker简介 baking是DOTS ECS工作流的一环&#xff0c;大概的意思就是将原先Editor下的GameObject数据&#xff0c;全部转换为Entity数据的过程。baking是一个不可逆的过程&#xff0c;原先的GameObject在运行时不复存在&#x…

漏刻有时百度地图API实战开发(8)关键词输入检索获取经纬度坐标和地址

在百度地图中进行关键词输入检索时&#xff1a; 在地图页面顶部的搜索框中输入关键词。点击搜索按钮或按下回车键进行搜索。地图将显示与关键词相关的地点、商家、景点等信息。可以使用筛选和排序功能来缩小搜索范围或更改搜索结果的排序方式。点击搜索结果中的地点或商家&…

软件工程考试复习

第一章、软件工程概述 &#x1f31f;软件程序数据文档&#xff08;考点&#xff09; &#x1f31f;计算机程序及其说明程序的各种文档称为 &#xff08; 文件 &#xff09; 。计算任务的处理对象和处理规则的描述称为 &#xff08; 程序 &#xff09;。有关计算机程序功能、…

Spring Cloud Gateway + Nacos + LoadBalancer实现企业级网关

1. Spring Cloud Gateway 整合Nacos、LoadBalancer 实现企业级网关 前置工作&#xff1a; 创建 SpringBoot 多模块项目创建网关&#xff08;gateway-service&#xff09;、用户&#xff08;user-service&#xff09;模块用户模块添加 Nacos discovery 支持以及 Spring Web&am…

一键抠图2:C/C++实现人像抠图 (Portrait Matting)

一键抠图2&#xff1a;C/C实现人像抠图 (Portrait Matting) 目录 一键抠图2&#xff1a;C/C实现人像抠图 (Portrait Matting) 1. 前言 2. 抠图算法 3. 人像抠图算法MODNet &#xff08;1&#xff09;模型训练 &#xff08;2&#xff09;将Pytorch模型转换ONNX模型 &…

【IDEA】IntelliJ IDEA中进行Git版本控制

本篇文章主要记录一下自己在IntelliJ IDEA上使用git的操作&#xff0c;一个新项目如何使用git进行版本控制。文章使用的IDEA版本 IntelliJ IDEA Community Edition 2023.3&#xff0c;远程仓库为https://gitee.com/ 1.配置Git&#xff08;File>Settings&#xff09; 2.去Git…

Java网络编程——安全网络通信

在网络上&#xff0c;信息在由源主机到目标主机的传输过程中会经过其他计算机。在一般情况下&#xff0c;中间的计算机不会监听路过的信息。但在使用网上银行或者进行信用卡交易时&#xff0c;网络上的信息有可能被非法分子监听&#xff0c;从而导致个人隐私的泄露。由于Intern…

adb unauthorized 踩坑记录

给Realme X7 Pro 安装Root后&#xff0c;发现adb连接设备呈现unauthorized 状态&#xff1a; 在Google以后&#xff0c;尝试了很多方案&#xff0c;均无效&#xff0c;尝试的方案如下&#xff1a; 重启手机&#xff0c;电脑。不行撤销调试授权&#xff0c;开关usb调试&#xf…

基于单片机智能安防窗户防盗系统设计

**单片机设计介绍&#xff0c;基于单片机智能安防窗户防盗系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的智能安防窗户防盗系统是一种利用单片机技术设计的窗户防盗装置。它通过传感器监测窗户状态&#xf…

vue使用echarts显示中国地图

项目引入echarts以后&#xff0c;在页面创建canvas标签 引入一个公共js文件&#xff08;下面这段代码就是china.js文件&#xff09; (function (root, factory) {if (typeof define function && define.amd) {// AMD. Register as an anonymous module.define([ex…

成都工业学院Web技术基础(WEB)实验四:CSS3布局应用

写在前面 1、基于2022级计算机大类实验指导书 2、代码仅提供参考&#xff0c;前端变化比较大&#xff0c;按照要求&#xff0c;只能做到像&#xff0c;不能做到一模一样 3、图片和文字仅为示例&#xff0c;需要自行替换 4、如果代码不满足你的要求&#xff0c;请寻求其他的…

Windows 和 MacOS 上安装配置ADB(安卓调试桥)

一、Android 调试桥 (ADB) Android 调试桥&#xff08;ADB&#xff09; 是一款多功能命令行工具&#xff0c;它让你能够更便捷地访问和管理 Android 设备。使用 ADB 命令&#xff0c;你可以轻松执行以下操作 在设备上安装、复制和删除文件&#xff1b;安装应用程序&#xff1…

多维时序 | MATLAB实现TSOA-TCN-Multihead-Attention多头注意力机制多变量时间序列预测

多维时序 | MATLAB实现TSOA-TCN-Multihead-Attention多头注意力机制多变量时间序列预测 目录 多维时序 | MATLAB实现TSOA-TCN-Multihead-Attention多头注意力机制多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现TSOA-TCN-Multihead-…

【Python网络爬虫入门教程1】成为“Spider Man”的第一课:HTML、Request库、Beautiful Soup库

Python 网络爬虫入门&#xff1a;Spider man的第一课 写在最前面背景知识介绍蛛丝发射器——Request库智能眼镜——Beautiful Soup库 第一课总结 写在最前面 有位粉丝希望学习网络爬虫的实战技巧&#xff0c;想尝试搭建自己的爬虫环境&#xff0c;从网上抓取数据。 前面有写一…

移动到末尾(蓝桥杯)

#include <stdio.h> #include <stdlib.h>#define N 1000 //双指针思想 int main(int argc, char *argv[]) {int n;int s[N];scanf("%d",&n);for(int i 0 ; i < n ; i)scanf("%d",&s[i]);int j 0; for(int i 0 ; …

SSM与SpringBoot面试题总结

什么是spring&#xff1f;谈谈你对IOC和AOP的理解。 Spring:是一个企业级java应用框架&#xff0c;他的作用主要是简化软件的开发以及配置过程&#xff0c;简化项目部署环境。 Spring的优点: 1、Spring低侵入设计&#xff0c;对业务代码的污染非常低。 2、Spring的DI机制将…

什么是神经网络的非线性

大家好啊&#xff0c;我是董董灿。 最近在写《计算机视觉入门与调优》&#xff08;右键&#xff0c;在新窗口中打开链接&#xff09;的小册&#xff0c;其中一部分说到激活函数的时候&#xff0c;谈到了神经网络的非线性问题。 今天就一起来看看&#xff0c;为什么神经网络需…