golang-bufio 缓冲读

news2024/11/24 6:42:15

缓冲 IO

在这里插入图片描述

计算机中我们常听到这样的两种程序优化方式:

  • 以时间换空间
  • 以空间换时间

今天要来看的缓冲IO就是典型的以空间换时间,它的基本原理见上图。简单的解释就是:程序不再直接去读取底层的数据源,而是通过一个缓冲区来进行读取。即先把数据从底层数据源读取到缓冲区,然后再从缓冲区进行读取。这样一分析,似乎有点多此一举了,本来一次的数据读写变成了两次?其实,不是这样的,因为这里的两个读写的成本或者说代价不同。我们都知道存储器是分级的,硬盘读写速度和内存读写速度之间的差距是巨大的:内存读写 >> 硬盘读写或者网络读写

所以缓冲区的真正作用是减少了硬盘或者网络读写的次数(读写的数据量没变)。

举一个例子:我有1024 个字节的数据,每次读取 32 个byte,那么需要访问硬盘或者网络 32次。如果一次读取 1024 个字节呢,最终只需要一次硬盘或者网络访问就够了(内存访问的次数增加,但是总的耗时是减少的)。不过,我们再扩展一点,既然内存读写这么快,我们直接把文件一次性读入内存不就好了?这样的想法是好的,但是对于大文件(GB级或者更大的),我想你是不会全部读取进内存的,因为内存资源也是有限的。

好了,前面是一些基本的概念的复习,简单回顾一下,有助于我们理解接下来的内容。今天来记录一下我阅读 golang 的 bufio 包的理解:

Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer object, creating another object (Reader or Writer) that also implements the interface but provides buffering and some help for textual I/O.

这是 bufio 包自己的介绍,从它的名字来理解就是缓冲 IO

缓冲读

因为这个 bufio.go 文件还是蛮大的,所以准备分成两个缓冲读和写部分进行阅读。接下来会介绍几个我认为比较重要的方法,会有一些自己的理解。

Buffered input 缓冲输入

const (
	defaultBufSize = 4096
)

// Buffered input.

// Reader implements buffering for an io.Reader object.
type Reader struct {
	buf          []byte
	rd           io.Reader // reader provided by the client
	r, w         int       // buf read and write positions
	err          error
	lastByte     int // last byte read for UnreadByte; -1 means invalid
	lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}

const minReadBufferSize = 16
const maxConsecutiveEmptyReads = 100

这就是缓冲输入 Reader 的结构体,它有一个 buf 字节切片(默认的长度是4096,即 4KB),并且依赖 io.Reader 实现的。这里可以看出来,所谓的缓冲区其实就是一个字节切片

在这里插入图片描述
所以如果缓冲区是空的判断条件即是:r.b == r.w

fill 填充数据

fill() 是内部用来填充缓冲区的方法,可以说能实现缓冲就是依赖它来实现的。

// fill reads a new chunk into the buffer.
func (b *Reader) fill() {
	// Slide existing data to beginning.
	if b.r > 0 {
		copy(b.buf, b.buf[b.r:b.w])
		b.w -= b.r
		b.r = 0
	}

	if b.w >= len(b.buf) {
		panic("bufio: tried to fill full buffer")
	}

	// Read new data: try a limited number of times.
	for i := maxConsecutiveEmptyReads; i > 0; i-- {
		n, err := b.rd.Read(b.buf[b.w:])
		if n < 0 {
			panic(errNegativeRead)
		}
		b.w += n
		if err != nil {
			b.err = err
			return
		}
		if n > 0 {
			return
		}
	}
	b.err = io.ErrNoProgress
}

这里是我从源码中的理解:

  1. 如果 b.r > 0,这说明缓冲区已经被读取了一些数据,所以要把后面未读取的数据搬到前面来,然后 b.w 就要减少 b.r,同时 b.r 需要置零(因为已经搬到前面了)。

  2. 如果 b.w >= len(b.buf),这种属于异常情况,会发生 panic("bufio: tried to fill full buffer")。也就是说,当你尝试向一个满的 buf 切片填充数据时,这是不允许的。这是出于程序正确性的角度考虑的。

  3. 然后就可以尝试进行填充数据了。这里很有意思,填充数据本身是在一个循环中进行的,它最多会执行 100 次([1, 100])。

循环体内的内容:

  1. 从底层的 reader 中读取数据,把读取的数据存入缓冲区的切片中,b.w 开始。

  2. 如果读取的 n < 0,那么会发生 panic。这种都是专门针对各种异常情况的处理,通常是不会发生的。

  3. 把数据写入缓冲区后,b.w 同时向后移动到数据的最后面。

  4. 如果读取数据时发生了错误,那么就返回。

如果 n > 0,那么就返回。这里很奇怪,为什么要单独 n > 0 时返回呢?因为,还有一种可能是 n == 0,如果出现这种情况,说明没有读取到数据。那么,怎么办呢?这就是前面循环的作用了,它会再次尝试读取,如果尝试 100 次都失败了,那么就停止读取,并且返回错误。(这种情况通常是因为一个错误的 reader 实现导致的)


这里举一个例子:测试字符串 CrazyDragon,假设初始缓冲区容量为10,现在开始第一次填充数据(r.b = r.w = 0),假设它读取了Crazy 这 5 个数据(这里的读取是从底层数据源读取,对于缓冲区来说它是指写入)。因为还没有开始读取(这里的读取指的是对缓冲区的读取),所以 r.b 为 0,r.w 为 5(这里你可以思考一下,为什么不是 4 呢?)。
在这里插入图片描述
然后的操作的是:

  1. 用户读取了 3 个数据,那么现在 r.b 移动到 3 的位置,然后再次触发填充函数。
  2. 它会首先把从 r.br.w 的数据搬到(或者说复制)缓冲切片的开头.
  3. 然后从底层数据源读取 Dragon 这 6 个数据,并将其填充进缓冲区,r.w 向后移动 6。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Peak 查看但不读取数据

注:// Tip: 以这个开头的中文注释,是我添加的理解。

// Peek returns the next n bytes without advancing the reader. The bytes stop
// being valid at the next read call. If Peek returns fewer than n bytes, it
// also returns an error explaining why the read is short. The error is
// ErrBufferFull if n is larger than b's buffer size.
//
// Calling Peek prevents a UnreadByte or UnreadRune call from succeeding
// until the next read operation.
func (b *Reader) Peek(n int) ([]byte, error) {
	if n < 0 {
		return nil, ErrNegativeCount
	}

	b.lastByte = -1
	b.lastRuneSize = -1

    // Tip:如果 n 大于缓冲区剩余的数据且缓冲区未满,会调用 fill() 将其填满
	for b.w-b.r < n && b.w-b.r < len(b.buf) && b.err == nil {
		b.fill() // b.w-b.r < len(b.buf) => buffer is not full
	}

    // Tip:如果 n 大于缓冲区的长度,会报错 ErrBufferFull
	if n > len(b.buf) {
		return b.buf[b.r:b.w], ErrBufferFull
	}

	// 0 <= n <= len(b.buf)
	var err error
    // Tip:如果最终还是剩余数据小于需要读取的数据,那么就返回剩余数据和相应的错误
	if avail := b.w - b.r; avail < n {
		// not enough data in buffer
		n = avail
		err = b.readErr()
		if err == nil {
			err = ErrBufferFull
		}
	}
    // Tip:这才是最关键的代码,只是返回缓冲区中数据的切片,其他的都错误处理相关的
	return b.buf[b.r : b.r+n], err
}

Peek 有偷看的意思,它的作用就是查看当前读取位置之后的 n 字节数据,但是不移动读取的下标。如果它返回的字节少于 n 个,它会返回一个错误来解释原因。如果 n 大于缓冲区的大小,那么会返回一个 ErrBufferFull 错误。通过调用 Peek() 可以阻止 UnreadByte 或者 UnreadRune 调用成功,直到下一次读取操作。

Discard 丢弃数据

// Discard skips the next n bytes, returning the number of bytes discarded.
//
// If Discard skips fewer than n bytes, it also returns an error.
// If 0 <= n <= b.Buffered(), Discard is guaranteed to succeed without
// reading from the underlying io.Reader.
func (b *Reader) Discard(n int) (discarded int, err error) {
	if n < 0 {
		return 0, ErrNegativeCount
	}
	if n == 0 {
		return
	}

	b.lastByte = -1
	b.lastRuneSize = -1

    // Tip:因为缓冲区里面不一定有足够的数据,所以它也是会在底层调用 fill 来填充数据的
	remain := n
	for {
		skip := b.Buffered()
		if skip == 0 {
			b.fill()
			skip = b.Buffered()
		}
		if skip > remain {
			skip = remain
		}
		b.r += skip
		remain -= skip
		if remain == 0 {
			return n, nil
		}
		if b.err != nil {
			return n - remain, b.readErr()
		}
	}
}

discard 就是丢弃的意思,这个词在 IT 领域还是很常见的,特别是和网络相关的。它的作用就是丢弃指定的 n 个字节,返回实际丢弃的字节数和相应的错误。

Read 读取数据

这个方法使用起来和正常的 IO 的 Read 是一样的,主要区别在于是先从缓冲区取数据,可能会调用一次 fill 函数填充缓存区。ReadByte 它返回下一个字节,ReadRune,它返回下一个 Unicode 字符。

UnReadByte 和 UnrReadRune 往前读

这里我把它称为往前读,因为它的作用是把读取指针向前移动。这两个方法类似,不过一个是向前移动一个字节,另一个是向前移动一个字符。

这里可以看到向前移动一个字节后,读取指针又指向了 :,本来这个字符已经被读取出来了,指针已经执行它后面的那个位置了。

package main

import (
	"bufio"
	"fmt"
	"strings"
)

func main() {
	reader := strings.NewReader("CrazyDragon: I love you yesterday and today.")
	bufReader := bufio.NewReaderSize(reader, 16)

	// 长度设置为 12,正好读取到 CrazyDragon:
	line := make([]byte, 12)
	n, err := bufReader.Read(line)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(string(line[:n]))
	// 缓冲区大小是 16,读取 12,还剩下 4
	fmt.Println("剩余缓冲数据:", bufReader.Buffered())
	// 调用 UnreadByte,读取指针前移一位,此时缓冲区还剩下 5
	bufReader.UnreadByte()
	fmt.Println(bufReader.Buffered())
	n, err = bufReader.Read(line[:1])
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(string(line[:n]))

在这里插入图片描述

ReadSlice 读取一个切片

// ReadSlice reads until the first occurrence of delim in the input,
// returning a slice pointing at the bytes in the buffer.
// The bytes stop being valid at the next read.
// If ReadSlice encounters an error before finding a delimiter,
// it returns all the data in the buffer and the error itself (often io.EOF).
// ReadSlice fails with error ErrBufferFull if the buffer fills without a delim.
// Because the data returned from ReadSlice will be overwritten
// by the next I/O operation, most clients should use
// ReadBytes or ReadString instead.
// ReadSlice returns err != nil if and only if line does not end in delim.
func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {
	s := 0 // search start index
	for {
		// Search buffer.
		if i := bytes.IndexByte(b.buf[b.r+s:b.w], delim); i >= 0 {
			i += s
			line = b.buf[b.r : b.r+i+1]
			b.r += i + 1
			break
		}

		// Pending error?
		if b.err != nil {
			line = b.buf[b.r:b.w]
			b.r = b.w
			err = b.readErr()
			break
		}

		// Buffer full?
		if b.Buffered() >= len(b.buf) {
			b.r = b.w
			line = b.buf
			err = ErrBufferFull
			break
		}

		s = b.w - b.r // do not rescan area we scanned before

		b.fill() // buffer is not full
	}

	// Handle last byte, if any.
	if i := len(line) - 1; i >= 0 {
		b.lastByte = int(line[i])
		b.lastRuneSize = -1
	}

	return
}

这个方法的作用是从缓冲区中读取数据,在第一次遇到指定的分隔符后停止,并返回一个指向缓冲区中这些字节的切片。ReadSlice 如果缓冲区填满数据但是没有一个分隔符,它会返回一个 ErrBufferFull 错误。

因为从 ReadSlice 返回的数据会被下一次的 IO 操作覆盖,所以大多数客户端应该使用 ReadBytes 或者 ReadString 来替代。

ReadSlice 只有在 line 没有结束在分隔符时,才会返回 err != nil

这里来做一个实验:测试数据为:CrazyDragon: I love you yesterday and today,因为默认的缓冲区大小是 4096 字节,这个对于测试代码太大了。但是,它也是可以设置的,最小值是 16 字节,这里我就设置成最小值来测试。

package main

import (
	"bufio"
	"fmt"
	"strings"
)

func main() {
	reader := strings.NewReader("CrazyDragon: I love you yesterday and today.")
	bufReader := bufio.NewReaderSize(reader, 16)
    // 此时还没有开始填充,如果缓冲区是空的,输出是 0
	fmt.Println("Buffered: ", bufReader.Buffered())
    // 开始从缓冲区读取,底层会调用 fill() 函数进行填充的
	line, err := bufReader.ReadSlice(':')
	if err != nil {
		fmt.Println(err)
	}
    // 这时 r.b 已经移动到相应的位置了,此时缓冲区的长度为 4
	fmt.Println("Buffered: ", bufReader.Buffered())
	fmt.Println(string(line))
    // 再次尝试读取,因为找不到分隔符,所以报错了
	_, err = bufReader.ReadSlice(':')
	if err != nil {
		fmt.Println("Error: ", err)
	}
    // 查看此时整个缓冲区的数据
    line, err = bufReader.Peek(16)
	if err != nil {
		fmt.Println("Error: ", err)
	}
	fmt.Printf("len(line) = %d, line = %s", len(line), string(line))
}

在这里插入图片描述

所以,可见 Buffered() 输出结果是 4,r.w 为 16,r.b 为 12。不过,这种情况就是从头开始读取一个满的缓冲区,如果缓冲区本身是未满的,那么它会将缓冲区填满(不一定是一次填满,可能会多次执行 fill(),这里的情况还是很复杂的,对于网络流来说,每次你都不一定可以读取到数据),但是如果缓存区满了还是没有发现分隔符,那么就会报错。
在这里插入图片描述

在这里插入图片描述

ReadLine 读取一行

ReadLine 是一个低级的读取行方法。大多数调用者应该使用 ReadBytes('\n') 或者 ReadString('\n') 替代或者使用扫描器 Scanner。实际上,它在内部是这样调用的:line, err = b.ReadSlice('\n')。 不过也要注意它的不同点,它是不会返回行结束符字节的。也就是说: ReadSlice('\n') 返回的数据是包含 \n,但是 ReadLine() 是不会包含的。

如果遇到了 ErrBufferFull 错误,说明整个缓存区都没有遇到 \n。但是它还会考虑是否存在 \r\n 横跨缓冲区的情况,即 \r 是缓冲区的最后一个字节,然后 \n 还没有到来,不过这只能等待下一次调用取检查了,可以通过第二个返回值来判断是否是这种情况。

这里来测试一下:

func ReadLineTest(s string) {
	reader := strings.NewReader(s)
	bufReader := bufio.NewReaderSize(reader, 16)

	line, isPrefix, err := bufReader.ReadLine()
	if err != nil {
		fmt.Println(err)
	}
	fmt.Printf("line = %s, isPrefix = %v\n", string(line), isPrefix)
}

ReadLineTest("CrazyDragonKing\r\n")
ReadLineTest("CrazyDragonKing\r")
ReadLineTest("CrazyDragonKing\n")
ReadLineTest("CrazyDragonKingCrazyDragonKingCrazyDragonKing")

在这里插入图片描述

这里来总结一下,各种可能的情况(注:缓冲区大小为 16,CrazyDragonKing 长度是 15)

"CrazyDragonKing\r\n" => "CrazyDragonKing", true, nil
"CrazyDragonKing\r"   => "CrazyDragonKing", true, nil
"CrazyDragonKing\n"   => "CrazyDragonKing", false, nil
"CrazyDragonKingCrazyDragonKingCrazyDragonKing" => CrazyDragonKingC, true, nil

ReadBytesReadString 读取字节切片和读取字符串

ReadSlice 是当遇到分隔符之后返回字节切片(包含分隔符本身),它会尝试填满缓冲区,如果没有遇到分隔符,会返回 ErrBufferFull 错误。

ReadLine 内部依赖 ReadSlice 实现,它返回的也是字节切片(不包含分隔符本身)。

ReadBytesReadString 它们两个一个返回字节切片,一个返回字符串。不同的地方在于,如果一个缓冲区没有找到,它们不会返回 ErrBufferFull 错误。而是继续读取底层的数据源,直到遇到分隔符或者文件结束。

这里来做一个实验:

func ReadStringTest(s string) {
	reader := strings.NewReader(s)
	bufReader := bufio.NewReaderSize(reader, 16)

	line, err := bufReader.ReadString('\n')
	if err != nil {
		fmt.Println(err)
	}
	fmt.Printf("line = %s\n", line)
}

ReadLineTest("CrazyDragonKingCrazyDragonKingCrazyDragonKing")
ReadStringTest("CrazyDragonKingCrazyDragonKingCrazyDragonKing\nCrazyDragonKing\n")

在这里插入图片描述

collectFragments 收集片段

// collectFragments reads until the first occurrence of delim in the input. It
// returns (slice of full buffers, remaining bytes before delim, total number
// of bytes in the combined first two elements, error).
// The complete result is equal to
// `bytes.Join(append(fullBuffers, finalFragment), nil)`, which has a
// length of `totalLen`. The result is structured in this way to allow callers
// to minimize allocations and copies.
func (b *Reader) collectFragments(delim byte) (fullBuffers [][]byte, finalFragment []byte, totalLen int, err error) {
	var frag []byte
	// Use ReadSlice to look for delim, accumulating full buffers.
	for {
		var e error
		frag, e = b.ReadSlice(delim)
		if e == nil { // got final fragment
			break
		}
		if e != ErrBufferFull { // unexpected error
			err = e
			break
		}

		// Make a copy of the buffer.
		buf := bytes.Clone(frag)
		fullBuffers = append(fullBuffers, buf)
		totalLen += len(buf)
	}

	totalLen += len(frag)
	return fullBuffers, frag, totalLen, err
}

这个方法初看起来还是蛮复杂的,特别是它返回值有 4 个,不过它这样的目的是为了调用者减少内存分配和拷贝。它的内部是依赖 ReadSlice 实现的,如果直接读取到了,那么就直接返回了。否则,下面就是不同的地方了。通常,如果返回 ErrBufferFull 是说明整个缓冲区没有遇到分隔符(如果不是,说明遇到了非预期错误,直接返回),它会保存已经读取的数据,然后再调用 ReadSlice,直到遇到分隔符或者其他错误。

对比上面这行代码的执行结果:
ReadStringTest("CrazyDragonKingCrazyDragonKingCrazyDragonKing\nCrazyDragonKing\n")

总结

好了,到这里就结束了。这里至少 Reader 相关的部分,后面还会继续 Writer 相关的部分。不过,目前还没有特别好的记录方式,只是大致挑几个代码贴出来,写一些自己的理解,然后配一些示意图,再加上一些测试代码。这篇博客不是为了说明这个包是怎么使用的,而是为什么要用它,以及它的一些实现的细节。现这样写吧,以后再探索是不是有更好的方式。

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

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

相关文章

SPI3+DMA外设驱动-TFTLCD初始化

前言 &#xff08;1&#xff09;本系列是基于STM32的项目笔记&#xff0c;内容涵盖了STM32各种外设的使用&#xff0c;由浅入深。 &#xff08;2&#xff09;小编使用的单片机是STM32F105RCT6&#xff0c;项目笔记基于小编的实际项目&#xff0c;但是博客中的内容适用于各种单片…

算法leetcode|74. 搜索二维矩阵(rust重拳出击)

文章目录 74. 搜索二维矩阵&#xff1a;样例 1&#xff1a;样例 2&#xff1a;提示&#xff1a; 分析&#xff1a;题解&#xff1a;rust&#xff1a;go&#xff1a;c&#xff1a;python&#xff1a;java&#xff1a; 74. 搜索二维矩阵&#xff1a; 给你一个满足下述两条属性的…

新手将最简单的springboot部署上tomcat出现的意外问题

现阶段springboot部署到tomcat的文章一抓一大把且都相同,便贴一个地址以展示流程: SpringBoot打war包部署Tomcat(最全)_spring boot war 部署tomcat_聊Java的博客-CSDN博客 那么就说一下我出现的问题: 在完整复现流程且确认代码无误的情况下,部署到tomcat,此时问题出现了:启动…

vue中html引入使用<%= BASE_URL %>变量

首先使用src相对路径引入 注意&#xff1a; js 文件放在public文件下 不要放在assets静态资源文件下 否则 可能会报错 GET http://192.168.0.113:8080/src/assets/js/websockets.js net::ERR_ABORTED 500 (Internal Server Error) 正确使用如下&#xff1a;eg // html中引…

Midjourney学习(二)参数的基础

prompt的组成 prompt 可以由三部分组成&#xff0c; 第一部分是垫图部分&#xff0c;也就是一张网络图片 第二部分是文本描述内容 第三部分则是参数 参数列表 --aspect <value> 或者 --ar <value> 控制画面的比例&#xff0c;横竖比例 --version <value> -…

复数的四则运算(java版)

复数的四则运算&#xff08;java版&#xff09; 目录 复数的四则运算&#xff08;java版&#xff09;介绍复数的四则运算实现思路代码1、封装复数类2、测试复数类3、代码测试结果 介绍 复数&#xff0c;为实数的延伸&#xff0c;它使任一多项式方程都有根。复数当中有个“虚数单…

攻击与防御实战经验分享:分析真实的攻击事件和入侵行为,讨论防御方法和实践经验

章节 1: 前言 作为IT领域的从业者&#xff0c;我们时刻都面临着网络安全的挑战。攻击者不断寻找漏洞&#xff0c;而防御者则需要时刻保持警惕&#xff0c;采取最佳实践来保护系统和数据。在本文中&#xff0c;我们将分享一些真实的攻击事件和入侵行为&#xff0c;并探讨针对这…

JVM 垃圾收集

垃圾收集 分代理论Java 堆的内存分区不同分代收集垃圾收集算法 分代理论 弱分代假说&#xff1a;绝大多数对象都是朝生夕灭&#xff0c;即绝大多数对象都是用完很快需要销毁的。强分代假说&#xff1a;熬过多次垃圾收集过程的对象就越难以消亡&#xff0c;即如果对象经过多次垃…

6. vue-element-admin 二次开发避坑指南

vue-element-admin 二次开发避坑指南 1.1 前言1.1.1 切换标签时未保存页面的操作内容1.1.2 markdown 样式乱码1.1.3 修改默认尺寸1.1.4 当后端服务器宕机情况下页面加载层一直转圈无法停止&#xff0c;只能关闭页面1.1.5 隐藏齿轮 1.1 前言 上一篇博文&#xff0c;我们分享了vu…

HarmonyOS Codelab 优秀样例——购物应用,体验一次开发多端部署魅力

一. 样例介绍 本篇Codelab基于自适应布局和响应式布局&#xff0c;实现购物应用在手机、折叠屏、平板不同屏幕尺寸设备上按不同设计显示。通过三层工程结构组织代码&#xff0c;实现一次开发&#xff0c;多端部署 。 手机运行效果如图所示&#xff1a; 折叠屏运行效果图&#x…

Thymeleaf解析表达式

首先springThymeleaf必过的点 org.springframework.web.servlet.DispatcherServlet#doDispatch那么先跟入handle()方法 然后跟进handleInternal方法 可以看到mav的获取方法&#xff0c;继续跟进invokeHandlerMethod 继续跟进invokeAndHandle 这里判断returnvalue是否有值…

智能工厂移动式作业轻薄加固三防平板数据采集终端

在这个高度自动化和数字化的环境中&#xff0c;数据采集变得尤为重要。为了满足这个需求&#xff0c;工业三防平板数据采集终端应运而生。工业三防平板数据采集终端采用了轻量级高强度镁合金材质&#xff0c;这使得它在保持轻薄的同时具有更强的坚固性。这种材质还具有耐磨防损…

XML—DTD、 Schema

目录 DTD是什么&#xff1f; DTD有什么用途&#xff1f; DTD与XML有什么联系&#xff1f; DTD原理图 外部DTD DTD文件book.dtd: 使用外部DTD文件的XML文件 PCDATA XML 文档构建模块 一、元素 1、元素声明 ①、有元素&#xff1a; ②、空元素&#xff1a; ③、ANY…

Kubernetes(K8s 1.28.x)部署---超详细

目录 一、基础环境配置&#xff08;所有主机均要配置&#xff09; 1、配置IP地址和主机名、hosts解析 2、关闭防火墙、禁用SELinux 3、安装常用软件 4、配置时间同步 5、禁用Swap分区 6、修改linux的内核参数 7、配置ipvs功能 二、容器环境操作 1、定制软件源 2、安…

MySQL select语句的执行流程

MYSQL基础架构 连接器查询缓存分析器优化器执行器存储引擎 比如说&#xff0c;我们经常能够在mysql的查询语句中看到如下的查询语句&#xff1a; mysql> select * from T where ID10&#xff1b;在了解它在mysql中的执行流程之前&#xff0c;首先我们来看一下mysql中的基本…

Qt之进程通信-IPC(QLocalServer,QLocalSocket 含源码+注释)

文章目录 一、IPC通信示例图1.1 设置关键字并连接的示例图1.2 进程间简单的数据通信示例图1.3 断开连接的示例图1.3.1 由Server主动断开连接1.3.2 由Socket主动断开连接 1.4 Server停止监听后的效果二、个人理解与一些心得三、一些疑问&#xff08;求教 家人们&#x1f602;&am…

VueRouter的基本使用

路由的基本使用 文章目录 路由的基本使用01-VueRouterVueRouter的使用 &#xff08; 5 2&#xff09;综合代码 拓展&#xff1a;组件存放问题 什么是路由呢&#xff1f; 在生活中的路由&#xff1a;设备和IP的映射关系 在Vue中&#xff1a;路径 和 组件 的 映射 关系。 01-Vu…

与面试官互动:建立积极的技术讨论氛围

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

OpenCV简介

OpenCV简介 OpenCV&#xff08;开源计算机视觉库&#xff1a;http://opencv.org&#xff09;是一个开源库&#xff0c;包含数百种计算机视觉算法。OpenCV 具有模块化结构&#xff0c;主要包括下列模块&#xff1a; 核心功能&#xff08;core&#xff09; - 定义基本数据结构的…

3、监测数据采集物联网应用开发步骤(3)

监测数据采集物联网应用开发步骤(2) 系统整体结构搭建 新建项目 输入项目名称&#xff1a;MonitorData 所谓兵马未动粮草先行&#xff0c;按下图创建好对应的模块备用&#xff1a; com.plugins 业务插件模块 com.zxy.adminlog 日志或文本文…