gRPC-Go源码解读二 传输层数据处理流程

news2025/1/19 14:23:58

         本篇文章主要介绍gRPC Client传输层的处理流程,如有疑问,欢迎指教。

 

gRPC版本: 1.54.0-dev

      gRPC基于http2传输,传输层主要处理http2相关的内容。RFC7540制定了http2协议规范,因此,这部分代码的逻辑绝大部分是按照协议规范实现的。如初始化http2连接、维持心跳、读取/发送Http2 Frame,流量控制等等。

        具体实现上采取读写分离,由两个go协程分别负责frame读取和写入, 简单说就是建立个TCP链接,然后起两个协程分别负责读写。此外,为了提升网络传输性能,gRPC-Go还实现了BDP(Bandwidth Delay Product)采样以及流控窗口自动扩容等等。       

        在具体说明之前,先介绍两个重要的对象,loopyWriter,controlBuffer。

        loopyWriter 简称loopy,顾名思义,这个是循环写的东西。它内部维护一个controlBuffer用于接收各种控制信息,包括从读端接收到的各种控制Frame(Setting、WindowUpdate、GoAway等)、以及待发送的Data、Header Frame。此外还有一些用于维护内部状态的信息。写端循环读取controlBuffer处理,没有消息就阻塞等待。


type loopyWriter struct {
	side      side        // client or server 
	cbuf      *controlBuffer // 控制信息缓存
	sendQuota uint32 // 链路发送流控窗口额度 每次发送DataFrame前会检查是否有额度发送
	oiws      uint32 // stream 发送流控窗口额度 

	// 对于client侧, 这里的stream已经发送了header frame
	// 对于server侧, 这里的stream已经收到了header frame
	estdStreams map[uint32]*outStream // 当前连接上已建立且尚未清除的stream

    // activeStreams是个stream链表,每个stream都有发送额度并且有数据待发送,数据存在在stream自身的item列表中,如果是server侧,该列表可能还包含trailers数据(header frame)
	activeStreams *outStreamList
	framer        *framer
	hBuf          *bytes.Buffer  // The buffer for HPACK encoding.
	hEnc          *hpack.Encoder // HPACK encoder.
	bdpEst        *bdpEstimator  // dbp 估算器 用于动态窗口更新
	draining      bool          

	// Side-specific handlers
	ssGoAwayHandler func(*goAway) (bool, error)
}

controlBuffer 作用上相当于一个消息队列,生产者将消息追加到list链表中,同时检查是否有消费者在等待,如有则通过ch唤醒。

// control buffer 类似一个消息队列 主要用写端使用,业务发送消息也是先追加到这里
type controlBuffer struct {
	ch              chan struct{} 
	done            <-chan struct{}
	mu              sync.Mutex
	consumerWaiting bool      // 如写端没有消息,则阻塞等待通知 
	list            *itemList // 消息列表
	err             error

    // 统计有多少个需要响应的frame,如超过一定数量,则暂停读端写入
	transportResponseFrames int
	trfChan                 atomic.Value // chan struct{}
}

        下面两张流程图详细描述了读端和写端的处理流程。看上去流程复杂,其实就是处理不同的HTTP2 Frame而已,对于读端来说,最核心的就是处理Data Frame,收到data frame后会将之发送到对应stream的接收缓存等待读取。

        对于写端而言,核心部分就是发送DataFrame和HeaderFrame,此外,为了减少系统调用,提高发送效率,如果有多个frame,会采取批量发送的方式发送。如果单次发送数量少于一定字节数,还会让出一轮CPU时间片等待更多数据。

图一 http2Client reader 读端

图2 loopyWriter​​​​ 写端

         

下面看具体的代码,先从newHTTP2Client 新建http2client开始:

// 基于给定地址构建http2的客户端,如成功,返回的http2client可以读写消息了。
func newHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts ConnectOptions, onClose func(GoAwayReason)) (_ *http2Client, err error) {
    // 默认http,如传递证书相关参数,使用https
	scheme := "http"
	ctx, cancel := context.WithCancel(ctx)
	defer func() {
		if err != nil {
			cancel()
		}
	}()


	// tls相关 
	connectCtx = icredentials.NewClientHandshakeInfoContext(connectCtx, credentials.ClientHandshakeInfo{Attributes: addr.Attributes})

	// 创建tcp连接
	conn, err := dial(connectCtx, opts.Dialer, addr, opts.UseProxy, opts.UserAgent)
	if err != nil {
		if opts.FailOnNonTempDialError {
			return nil, connectionErrorf(isTemporary(err), err, "transport: error while dialing: %v", err)
		}
		return nil, connectionErrorf(true, err, "transport: Error while dialing: %v", err)
	}

	// Any further errors will close the underlying connection
	defer func(conn net.Conn) {
		if err != nil {
			conn.Close()
		}
	}(conn)

	ctxMonitorDone := grpcsync.NewEvent()
	newClientCtx, newClientDone := context.WithCancel(connectCtx)
	defer func() {
		newClientDone()         // Awaken the goroutine below if connectCtx hasn't expired.
		<-ctxMonitorDone.Done() // Wait for the goroutine below to exit.
	}()
	go func(conn net.Conn) {
		defer ctxMonitorDone.Fire() // Signal this goroutine has exited.
		<-newClientCtx.Done()       // Block until connectCtx expires or the defer above executes.
		if err := connectCtx.Err(); err != nil {
			// connectCtx expired before exiting the function.  Hard close the connection.
			if logger.V(logLevel) {
				logger.Infof("newClientTransport: aborting due to connectCtx: %v", err)
			}
			conn.Close()
		}
	}(conn)

	kp := opts.KeepaliveParams
	if kp.Time == 0 { // 默认长连接
		kp.Time = defaultClientKeepaliveTime
	}
	if kp.Timeout == 0 { // 超时默认20秒
		kp.Timeout = defaultClientKeepaliveTimeout
	}
	keepaliveEnabled := false
	if kp.Time != infinity {
		// 当数据包发出去后的等待时间超过用户设置的时间时,判定连接超时
		if err = syscall.SetTCPUserTimeout(conn, kp.Timeout); err != nil {
			return nil, connectionErrorf(false, err, "transport: failed to set TCP_USER_TIMEOUT: %v", err)
		}
		keepaliveEnabled = true
	}
	// 安全相关
	var (
		isSecure bool
		authInfo credentials.AuthInfo
	)
	transportCreds := opts.TransportCredentials
	perRPCCreds := opts.PerRPCCredentials
	if b := opts.CredsBundle; b != nil {
		if t := b.TransportCredentials(); t != nil {
			transportCreds = t
		}
		if t := b.PerRPCCredentials(); t != nil {
			perRPCCreds = append(perRPCCreds, t)
		}
	}
	if transportCreds != nil {
		conn, authInfo, err = transportCreds.ClientHandshake(connectCtx, addr.ServerName, conn)
		if err != nil {
			return nil, connectionErrorf(isTemporary(err), err, "transport: authentication handshake failed: %v", err)
		}
		for _, cd := range perRPCCreds {
			if cd.RequireTransportSecurity() {
				if ci, ok := authInfo.(interface {
					GetCommonAuthInfo() credentials.CommonAuthInfo
				}); ok {
					secLevel := ci.GetCommonAuthInfo().SecurityLevel
					if secLevel != credentials.InvalidSecurityLevel && secLevel < credentials.PrivacyAndIntegrity {
						return nil, connectionErrorf(true, nil, "transport: cannot send secure credentials on an insecure connection")
					}
				}
			}
		}
		isSecure = true
		if transportCreds.Info().SecurityProtocol == "tls" {
			scheme = "https"
		}
	}

	// 默认开启动态流控窗口 除非指定有效的流控窗口参数 包括stream level or conn level
	dynamicWindow := true
	icwz := int32(initialWindowSize) // initial window size 65535
	if opts.InitialConnWindowSize >= defaultWindowSize {
		icwz = opts.InitialConnWindowSize
		dynamicWindow = false
	}

	writeBufSize := opts.WriteBufferSize
	readBufSize := opts.ReadBufferSize
	maxHeaderListSize := defaultClientMaxHeaderListSize
	if opts.MaxHeaderListSize != nil {
		maxHeaderListSize = *opts.MaxHeaderListSize
	}
	t := &http2Client{
		ctx:                   ctx,
		ctxDone:               ctx.Done(), // Cache Done chan.
		cancel:                cancel,
		userAgent:             opts.UserAgent,
		registeredCompressors: grpcutil.RegisteredCompressors(),
		address:               addr, // 对端地址
		conn:                  conn, // 底层TCP连接
		remoteAddr:            conn.RemoteAddr(),
		localAddr:             conn.LocalAddr(),
		authInfo:              authInfo,
		readerDone:            make(chan struct{}),
		writerDone:            make(chan struct{}),
		goAway:                make(chan struct{}),
		// Frame 读写
		framer: newFramer(conn, writeBufSize, readBufSize, maxHeaderListSize),
		// 输入流量窗口控制 用于控制对端发送速度
		fc:     &trInFlow{limit: uint32(icwz)},
		scheme: scheme,
		// 当前client上的stream
		activeStreams: make(map[uint32]*Stream),

        // 安全相关 
		isSecure:      isSecure,
		perRPCCreds:   perRPCCreds,
        // 保活参数
		kp:            kp,
		statsHandlers: opts.StatsHandlers,
        
 
		// 本地stream 初始化流控窗口大小
		initialWindowSize: initialWindowSize,
		// stream ID ,默认从1开始,每次新建stream都会自动+2。 http2 client streamID为奇数,server侧streamID为偶数。
		nextID: 1,
		// 最大流并发数
		maxConcurrentStreams: defaultMaxStreamsClient,
		// 可用stream额度
		streamQuota:           defaultMaxStreamsClient,
		streamsQuotaAvailable: make(chan struct{}, 1),

		// metric相关
		czData:           new(channelzData),
		keepaliveEnabled: keepaliveEnabled,
		bufferPool:       newBufferPool(),
		onClose:          onClose,
	}
	t.ctx = peer.NewContext(t.ctx, t.getPeer())
	if md, ok := addr.Metadata.(*metadata.MD); ok {
		t.md = *md
	} else if md := imetadata.Get(addr); md != nil {
		t.md = md
	}

	// control frame buffer
	t.controlBuf = newControlBuffer(t.ctxDone)

	// 和上面的conn window一样,如果设置了stream flow window,则也不能使用动态窗口机制
	if opts.InitialWindowSize >= defaultWindowSize {
		t.initialWindowSize = opts.InitialWindowSize
		dynamicWindow = false
	}

	// 动态窗口默认开启 这初始化bdp采样器
	if dynamicWindow {
		t.bdpEst = &bdpEstimator{
			bdp:               initialWindowSize,
			updateFlowControl: t.updateFlowControl,
		}
	}
	// 数据统计相关
	for _, sh := range t.statsHandlers {
		t.ctx = sh.TagConn(t.ctx, &stats.ConnTagInfo{
			RemoteAddr: t.remoteAddr,
			LocalAddr:  t.localAddr,
		})
		connBegin := &stats.ConnBegin{
			Client: true,
		}
		sh.HandleConn(t.ctx, connBegin)
	}

	// metrics
	t.channelzID, err = channelz.RegisterNormalSocket(t, opts.ChannelzParentID, fmt.Sprintf("%s -> %s", t.localAddr, t.remoteAddr))
	if err != nil {
		return nil, err
	}

	// 保活 根据配置时间定时发送 ping frame
	if t.keepaliveEnabled {
		t.kpDormancyCond = sync.NewCond(&t.mu)
		go t.keepalive()
	}

	readerErrCh := make(chan error, 1)

	// 读端 reader
	go t.reader(readerErrCh)
	defer func() {
		if err == nil {
			// 如果 server preface 读取异常则关闭连接
			err = <-readerErrCh
		}
		if err != nil {
			t.Close(err)
		}
	}()

	// 发送 http/2 connection preface, http2协议规定client和server必须发送connection preface以作为最终的协议确认,对于client侧,包含一个固定字符串以及一个或多个setting frame,对于server侧,则为一个或多个setting frame。
	n, err := t.conn.Write(clientPreface)
	if err != nil {
		err = connectionErrorf(true, err, "transport: failed to write client preface: %v", err)
		return nil, err
	}
	if n != len(clientPreface) {
		err = connectionErrorf(true, nil, "transport: preface mismatch, wrote %d bytes; want %d", n, len(clientPreface))
		return nil, err
	}
	var ss []http2.Setting

	// 和默认值不同则发一个setting frame通知对端
	if t.initialWindowSize != defaultWindowSize {
		ss = append(ss, http2.Setting{
			ID:  http2.SettingInitialWindowSize,
			Val: uint32(t.initialWindowSize),
		})
	}
    // 如有自定义参数则发一个setting frame通知对端
	if opts.MaxHeaderListSize != nil {
		ss = append(ss, http2.Setting{
			ID:  http2.SettingMaxHeaderListSize,
			Val: *opts.MaxHeaderListSize,
		})
	}
	// 继续发送 connection preface,即便设置为空也会发送一个空的setting,这是http2 connection preface要求
	err = t.framer.fr.WriteSettings(ss...)
	if err != nil {
		err = connectionErrorf(true, err, "transport: failed to write initial settings frame: %v", err)
		return nil, err
	}

	// 通知对端,conn流控窗口需要增大, 这个只能通过window-update帧来通知而不是通过setting帧,因为setting帧是用来修改stream initial window size
	if delta := uint32(icwz - defaultWindowSize); delta > 0 {
		if err := t.framer.fr.WriteWindowUpdate(0, delta); err != nil {
			err = connectionErrorf(true, err, "transport: failed to write window update: %v", err)
			return nil, err
		}
	}

	// metric 统计
	t.connectionID = atomic.AddUint64(&clientConnectionCounter, 1)

	// 清空发送缓存
	if err := t.framer.writer.Flush(); err != nil {
		return nil, err
	}

	// 写端,循环处理写事件,如心跳、控制消息、业务数据等
	go func() {
		t.loopy = newLoopyWriter(clientSide, t.framer, t.controlBuf, t.bdpEst)
		err := t.loopy.run()
		if logger.V(logLevel) {
			logger.Infof("transport: loopyWriter exited. Closing connection. Err: %v", err)
		}
		// Do not close the transport.  Let reader goroutine handle it since
		// there might be data in the buffers.
		// 关闭套接字
		t.conn.Close()
		// 通知controlBuf准备结束
		t.controlBuf.finish()
		// 关闭写信号
		close(t.writerDone)
	}()
	return t, nil
}

newHTTP2Client 主要就是新建TCP套接字,然后按照http2协议规范初始化http2连接,然后起2个go 协程分别负责套接字的读写。

下面看看读端的代码:


// 验证server侧 connection preface,然后开始循环读数据
func (t *http2Client) reader(errCh chan<- error) {
    // 退出前关闭读 此时套接字还能继续写
	defer close(t.readerDone)

	// 读取 server 侧connection preface(由SettingFrame构成)
	if err := t.readServerPreface(); err != nil {
		errCh <- err
		return
	}
	close(errCh)

	// 更新读取时间 keepalive保活协程会用到
	if t.keepaliveEnabled {
		atomic.StoreInt64(&t.lastRead, time.Now().UnixNano())
	}

	// 循环读取消息
	for {
        // 检查写端是否有大量响应消息需要发送,如有,则等待
		t.controlBuf.throttle()
        // 接收http2 frame
		frame, err := t.framer.fr.ReadFrame()
		if t.keepaliveEnabled {
			// 更新读取时间 keepalive 保活协程会用到
			atomic.StoreInt64(&t.lastRead, time.Now().UnixNano())
		}
		if err != nil {
            // 如果是stream相关错误,则关闭对应stream,否则关闭整个链接
			if se, ok := err.(http2.StreamError); ok {
				t.mu.Lock()
				s := t.activeStreams[se.StreamID]
				t.mu.Unlock()
				if s != nil {
					code := http2ErrConvTab[se.Code]
					errorDetail := t.framer.fr.ErrorDetail()
					var msg string
					if errorDetail != nil {
						msg = errorDetail.Error()
					} else {
						msg = "received invalid frame"
					}
					// 关掉对应的stream
					t.closeStream(s, status.Error(code, msg), true, http2.ErrCodeProtocol, status.New(code, msg), nil, false)
				}
				continue
			} else {
				// 关闭整个连接
				t.Close(connectionErrorf(true, err, "error reading from server: %v", err))
				return
			}
		}
        // 根据frame 类型调用各自处理函数
		switch frame := frame.(type) {
		// Header帧
		case *http2.MetaHeadersFrame:
			t.operateHeaders(frame)
		// Data帧
		case *http2.DataFrame:
			t.handleData(frame)
		// RstStream帧
		case *http2.RSTStreamFrame:
			t.handleRSTStream(frame)
		// Setting帧
		case *http2.SettingsFrame:
			t.handleSettings(frame, false)
		// Ping帧
		case *http2.PingFrame:
			t.handlePing(frame)
		// 链接断开帧
		case *http2.GoAwayFrame:
			t.handleGoAway(frame)
		// 窗口更新帧
		case *http2.WindowUpdateFrame:
			t.handleWindowUpdate(frame)
		default:
			if logger.V(logLevel) {
				logger.Errorf("transport: http2Client.reader got unhandled frame type %v.", frame)
			}
		}
	}
}

读端代码逻辑简单,就是验证过 connection preface  之后,循环读取frame处理。preface中文意思是开场白、序言。http2 协议规定,在正式进行数据交换前,client和server必须先进行connection preface以进行最终的协议确认,原文是这样:

In HTTP/2, each endpoint is required to send a connection preface as a final confirmation of the protocol in use and to establish the initial settings for the HTTP/2 connection. The client and server each send a different connection preface.

在HTTP/2中,每个端点都需要发送一个连接序言,作为使用协议的最终确认,并为HTTP/2连接建立初始设置。客户端和服务器各自发送一个不同的连接序言。

        下面是example目录中UnaryEcho 请求抓包例子,其中server服务端口是50051。可以看到,在TCP三次握手之后,client和server就开始发送connection preface,client 侧connection preface由一个magic报文(内容是一串固定字符)加上一个Setting Frame组成,server端有2个SettingFrame构成,之后便是client发送请求HeaderFrame和DataFrame, Server端收到DataFrame后立刻发了个windowUpdate等等。 

下面在看写端的代码,也是一个循环:

// 从controlBuf读取消息并处理,包括更新loopy自身状态或者发送http2 frame。loopy 将所有需要发送数据的stream放到一个active链表中,active链表中的每个stream必须满足两个条件,1,有数据发送。2,不受stream 流控限制。在运行循环的每次迭代中,除了处理传入的控制帧之外,循环调用processData,处理activeStreams链表中的stream上的发送消息队列,每次发送一个data frame或者一个data frame加上一个header frame,如还有消息,则继续加入activeStreams中等待下次处理
func (l *loopyWriter) run() (err error) {
	// 退出之前清空下发送缓存
	defer l.framer.writer.Flush()
	for {
		// 阻塞读消息(消息包括setting、header、data、ping、goaway frame)
		it, err := l.cbuf.get(true)
		if err != nil {
			return err
		}
		// 消息处理
		if err = l.handle(it); err != nil {
			return err
		}
		// 发送data frame
		if _, err = l.processData(); err != nil {
			return err
		}
		// 是否让出CPU,只会让一次
		gosched := true
	hasdata:
		for {
			// 非阻塞读消息
			it, err := l.cbuf.get(false)
			if err != nil {
				return err
			}
			if it != nil {
				// 存在消息则循环处理
				if err = l.handle(it); err != nil {
					return err
				}
				if _, err = l.processData(); err != nil {
					return err
				}
				continue hasdata
			}

			isEmpty, err := l.processData()
			if err != nil {
				return err
			}
			if !isEmpty { // 还有data待处理 则继续
				continue hasdata
			}
			if gosched {
				gosched = false
				// 批量写
				if l.framer.writer.offset < minBatchSize {
					runtime.Gosched()
					continue hasdata
				}
			}
			l.framer.writer.Flush()
			break hasdata
		}
	}
}

写端逻辑也很简洁,就是循环读取controlBuf,轮训处理有消息发送的stream。当有stream要发送数据时,则将data写入到controlBuf待处理。

        以上就是gRPC传输层处理流程介绍。对于上层来说,建立好了http2Client就可以收发消息了,至于流控等是无需关心的。

        整个流程除了业务数据的接收和发送之外,比较值得注意的是流量控制这一块的处理,关于gRPC流程控制,已经有一篇极好的文章详细介绍了。这里我稍稍补充下关于BDP和流控窗口临时增加这一部分。

图3 BDP

        BDP(Bandwidth Delay Product 带宽延迟积) 用来衡量网络链路中可以发送多少比特数(或字节数)。它给出了发送者在任意时间在未接收到接收端确认数据之前最多可以发送的数据量。如果想最大化利用网络传输效能,接收端的接收窗口必须要大于bdp。因为如果小于bdp,则无法充分利用网络链路传输效能。        

        gRPC通过发送bdpping以及收到bdpping的响应来计算RTT,并统计在此期间收到的数据量sample来估算bdp,以此来调整流控控制窗口从而提升网络传输性能。实现细节以及理论细节可以参考贴出的参考资料。

        流控窗口临时增加是指当业务程序要求读取的数据超过当前流控窗口时,正常情况下,发送端会分多个frame多次发送。为了提升发送性能,接收端此时会发送一个windowUpdate窗口指示发送端可以发送更多数据从而绕过流控窗口的限制。这个优化在高延迟网络下可以提升10倍以上的性能。

参考资料:

1. gRPC 流量控制详解 - 掘金

2.   gRPC性能优化(BDP & 流控窗口临时增加) 

3. 再谈 gRPC 的 Trailers 设计 

4. grpc/PROTOCOL-HTTP2.md at master · grpc/grpc · GitHub 

5. http2 接收窗口自动调整

6. RFC 7540 - Hypertext Transfer Protocol Version 2 (HTTP/2)

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

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

相关文章

科普|FCC的卫星标准 为什么又说是FCC Part25呢?

我们今天介绍的FCC的卫星标准&#xff0c;在美国是作为一种法律规定&#xff0c;具有法律效力的标准&#xff0c;通常又称为法规文件。 01 — FCC Part 25 我们先从CFR说起&#xff0c;《美国联邦法规》&#xff08; Code of Federal Regulations &#xff0c;简称CFR&#…

【JAVAEE】网络原理之网络发展史

目录 &#x1f381;1. 独立模式 &#x1f383;2. 网络互连 &#x1f388;2.1 局域网 LAN ✨2.1.1 基于网线直连 &#x1f451;2.2.2 基于集线器组建 &#x1f48b;2.2.3 基于交换机组建 &#x1f457;2.2.4 基于交换机与路由器组建 &#x1f388;2.2 广域网 21世纪是一…

我的第一台电脑------计算机类专业学生购置电脑的一些个人心得

⬜⬜⬜ &#x1f430;&#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;(*^▽^*)欢迎光临 &#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;&#x1f430;⬜⬜⬜ ✏️write in front✏️ &#x1f4dd;个人主页&#xff1a;陈丹宇jmu &am…

Web 攻防之业务安全:接口参数账号篡改测试(修改别人邮箱 || 手机号为自己的)

Web 攻防之业务安全&#xff1a;接口参数账号篡改测试. 业务安全是指保护业务系统免受安全威胁的措施或手段。广义的业务安全应包括业务运行的软硬件平台&#xff08;操作系统、数据库&#xff0c;中间件等&#xff09;、业务系统自身&#xff08;软件或设备&#xff09;、业务…

HCIP之LSP静态搭建实验

目录 HCIP之LSP静态搭建实验 实验图 基本配置 R1 R2 R3 R4 配置方法 搭建从1.0 - 4.0 网段的LSP 搭建静态路由 配置MPLS 配置LSR - ID 激活MPLS 全局激活 接口激活 搭建静态LSP 搭建入站LSR R1配置 搭建中转LSR R2配置 R3配置 搭建出站LSR R4配置 搭建从…

Java语言-----泛型的认识

目录 一.什么是泛型 二.泛型类的使用 2.1泛型类的定义 2.2泛型类的数组使用 三.泛型的上界 四.泛型的方法 五.泛型与集合 &#x1f63d;个人主页&#xff1a; tq02的博客_CSDN博客-C语言,Java领域博主 &#x1f308;梦的目标&#xff1a;努力学习&#xff0c;向Java进发…

八大数据库全面对比,让你明确数据库怎么去选!

随着互联网和大数据时代的到来&#xff0c;各种数据管理技术也在迅猛发展。而在数据管理技术中&#xff0c;数据库无疑是最重要的一环。现今市场上涌现出了众多数据库产品&#xff0c;不同的数据库产品针对不同的业务需求和应用场景&#xff0c;有着不同的特点和优势。本文将介…

【双碳系列】LEAP碳排放预测、LCA生命周期、GAMS电力、CGE一般均衡模型

本文围绕双碳专题分为五大内容&#xff0c;分别为&#xff1a; 基于LEAP模型的能源环境发展、碳排放建模预测及不确定性分析实践应用 (qq.com) 双碳目标下农田温室气体排放模拟实践技术应用 (qq.com) 环境影响与碳排放生命周期评估应用及案例分析 (qq.com) “双碳”目标下资…

如何实现一个可靠的 UDP

QUIC是如何实现可靠传输的&#xff1f; 市面上的基于UDP协议实现的可靠传输协议的成熟方案&#xff0c;应用在HTTP/3上。 UDP报文头部和TCP报文头部夹着三层头部 Packet Header Packet Header细分这两种&#xff1a; Long Packet Header 用于首次建立连接Short Packet Hea…

深元ai智慧工地视频分析盒子提高建筑施工现场安全效率

随着社会的快速发展&#xff0c;建筑行业安全问题日益受到重视。为了解决传统人工巡查的诸多问题&#xff0c;AI智慧工地视频分析盒子应运而生&#xff0c;通过人工智能技术&#xff0c;全面提高建筑施工现场的安全工作效率。 一、AI智慧工地视频分析盒子解决传统巡查的痛点 …

【产品设计】Android 和 IOS 的交互设计对垒

在手机操作系统百花齐放的年代&#xff0c;也是产品经理最头疼的年代&#xff0c;因为需要根据不同的操作系统做出不同的设计。而如今&#xff0c;手机操作系统基本只剩下安卓和IOS两大阵营&#xff0c;只需处理好安卓和IOS交互上的差异部分就可以做好产品设计了。 手机操作系统…

不良事件上报系统源码 有演示,已在多家医院运营多年

不良事件上报系统源码&#xff0c;医院安全不良事件管理系统源码 技术架构&#xff1a;前后端分离&#xff0c;仓储模式&#xff0c;BS架构&#xff0c;有演示&#xff0c;已在多家医院完美运营。 相关技术&#xff1a;PHPvscodevue2elementlaravel8mysql5.7 文末获取联系&am…

【每日一练】基础题目练习

1、打印1-100之间所有的素数 素数&#xff1a;(也说质数) 数学上指在大于1的整数中只能被1和它本身整除的数。如2、3、5、7、11、43、109。过去。 方法一&#xff1a; 如果数据i能够被[2 &#xff0c;qsrt(i)]之间的数整除&#xff0c;则表示这个数不是素数。 当一个数na*b时&a…

CVE漏洞复现-CVE-2022-22947-Spring Cloud Gateway RCE

CVE-2022-22947-Spring Cloud Gateway RCE 基本介绍 微服务架构与Spring Cloud 最开始时&#xff0c;我们开发java项目时&#xff0c;所有的代码都在一个工程里&#xff0c;我们把它称为单体架构。当我们的项目的代码量越来越大时&#xff0c;开发的成员越来越多时&#xff…

Vivdao FFT IP核调试记录

最近一时兴起&#xff0c;看了下Vivado版本下的FFT IP核&#xff0c;发现和ISE版本下的FFT IP核有一些差别&#xff0c;貌似还不小。做了个简单的仿真&#xff0c;Vivado仿真结果竟然和Matlab仿真结果对不上&#xff0c;废了九牛二虎之力研究datasheet、做仿真&#xff0c;终于…

SpringBoot JSON全局日期格式转换器

参考资料 SpringBoot日期格式转换&#xff0c;SpringBoot配置全局日期格式转换器在Spring Boot中定制Jackson ObjectMapper详解SpringBoot中jackson日期格式化问题(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS not turning off timestamps) 目录需求分析一. 前期准备1.1 …

ARM简单程序设计【嵌入式系统】

ARM简单程序设计【嵌入式系统】前言推荐ARM简单程序设计创建项目注意事项顺序结构程序两数之和分支结构程序符号函数循环结构程序已知循环次数未知循环次数两重循环冒泡排序子程序设计①寄存器传递参数方式②存储区域传递参数方式③ 堆栈传递参数方式最后前言 2023-4-6 20:26:…

一文看懂多模态大型语言模型GPT-4

文章目录前言什么是GPT-4GPT-4 VS GPT-3.5GPT-4与其他模型对比GPT-4视觉输入GPT-4局限性写在最后前言 近日&#xff0c;OpenAI发布了最新版的生成预训练模型GPT-4。据官方介绍&#xff0c;最新一代的模型是一个大模型&#xff0c;性能比CPT-3.5强悍很多&#xff0c;不仅仅是接…

泛微数字化安全管理,实现标准化、智能化管理,数据可视化分析

企业安全管理需求提升&#xff1a; 随着国家政策与技术的双重驱动&#xff0c;企业当前的安全管理需求&#xff0c;从标准化管理&#xff0c;逐步发展到智能、可视、可分析的全程数字化安全管理&#xff0c;落地风险分级管控、隐患排查治理的双重预防机制。 国家发布的《企业…

腾讯云轻量级云服务器Centos7防火墙开放8080端口

腾讯云轻量级云服务器Centos7防火墙开放8080端口 一、centos7防火墙打开端口 因为Centos7以上用firewalld代替了iptables,也就是说firewalld开通了8080端口应该就行了 1.查看8080是否已经放开 sudo firewall-cmd --permanent --zonepublic --list-ports2.查看防火墙状态 s…