作者: 大嘴巴小牙齿

  • webRTC(Golang)

    在视频监控、工业巡检以及边缘计算等场景中,常见的一个现实问题是协议割裂:前端设备通常通过RTSP推流,而浏览器原生并不支持直接播放RTSP。这就带来了一个工程上的关键挑战——如何在不引入高延迟和复杂转码的前提下,将设备侧的视频流高效地分发到浏览器端。

    一种更直接且高效的思路,是利用WebRTC作为浏览器侧的实时传输协议,同时在服务端完成协议层的桥接,将RTSP流转换为WebRTC可消费的RTP数据流。这种方式避免了传统转码链路(例如FFMPEG+纯接口转发)带来的性能损耗,同时保留了实时性的优势。

    通过golang程序实现一个典型的视频桥接架构:上游通过RTSP拉流(通常来自摄像头或推流工具),服务端将RTP包转发到WebRTC PeerConnection,下游浏览器通过 WebRTC 实时播放视频。整体链路为:RTSP → RTP → Go服务 → WebRTC → 浏览器。


    零、在本机启一个mediamtx作为RTSP服务端,再通过ffmpeg把本机摄像头推流到服务器来模拟摄像头的RTSP流

    ./mediamtx &
    ./ffmpeg -f v4l2 -i /dev/video0 \
    -vcodec libx264 -preset veryfast -tune zerolatency \
    -f rtsp -rtsp_transport tcp \
    rtsp://test:123456@127.0.0.1:8554/test

    一、golang程序入口main中首先确定RTSP地址,并开一个Gin HTTP服务,同时维护一个clients映射,用于保存每个WebRTC客户端对应的Track:

    每个客户端并不是单独拉流,而是共享同一 RTSP输入流,服务端通过fan-out(扇出)机制将RTP包写入多个WebRTC Track,从而实现“一路输入,多路输出”

    import (
    	"github.com/bluenviron/gortsplib/v5"
    	"github.com/bluenviron/gortsplib/v5/pkg/base"
    	"github.com/bluenviron/gortsplib/v5/pkg/description"
    	"github.com/bluenviron/gortsplib/v5/pkg/format"
    	"github.com/gin-gonic/gin"
    	"github.com/pion/rtp"
    	"github.com/pion/webrtc/v3"
    )
    
    rtspURL := os.Getenv("RTSP_URL")
    if rtspURL == "" {
    	rtspURL = "rtsp://test:123456@127.0.0.1:8554/test"
    }
    
    var clientsMu sync.Mutex
    clients := map[string]*webrtc.TrackLocalStaticRTP{}

    二、HTTP部分分为三个路由:

    1)“/”路由返回播放器页面
    2)“/offer”路由处理WebRTC SDP信令交换
    3)“/stats”路由返回实时码率与包速率

    前端页面核心逻辑如下:

    浏览器创建RTCPeerConnection → 生成Offer(SDP)→ 发送给服务端 → 服务端生成Answer → 浏览器设置远端描述。需要注意的是,这里没有使用STUN/TURN(iceServers 为空),意味着该方案默认运行在内网或可直连环境,否则无法穿透NAT。

    pc=new RTCPeerConnection({iceServers:[]});
    pc.addTransceiver('video',{direction:'recvonly'});
    
    const offer=await pc.createOffer();
    await pc.setLocalDescription(offer);
    
    const resp=await fetch('/offer',{
      method:'POST',
      headers:{'Content-Type':'application/json'},
      body:JSON.stringify({sdp:offer.sdp,type:offer.type})
    });
    
    const answer=await resp.json();
    await pc.setRemoteDescription({type:answer.type,sdp:answer.sdp});

    服务端“/offer”路由处理逻辑是WebRTC的核心:

    这里做了两件关键事情:
    1)注册编解码器(H264/H265)
    2)创建 PeerConnection

    WebRTC本质上是RTP的增强版,但浏览器对编码格式要求严格,因此必须显式注册codec,否则SDP协商无法匹配。

    m := webrtc.MediaEngine{}
    _ = m.RegisterCodec(webrtc.RTPCodecParameters{
    	RTPCodecCapability: webrtc.RTPCodecCapability{
    		MimeType: webrtc.MimeTypeH264,
    		ClockRate: 90000,
    		SDPFmtpLine: "packetization-mode=1;profile-level-id=42e01f",
    	},
    	PayloadType: 96,
    }, webrtc.RTPCodecTypeVideo)
    
    api := webrtc.NewAPI(webrtc.WithMediaEngine(&m))
    pc, _ := api.NewPeerConnection(webrtc.Configuration{})

    创建Track,并绑定到PeerConnection:

    Track是WebRTC中媒体发送的抽象,本质上是RTP流的出口。这里使用TrackLocalStaticRTP,意味着可以手动写入RTP包

    track, _ := webrtc.NewTrackLocalStaticRTP(
    	webrtc.RTPCodecCapability{
    		MimeType: webrtc.MimeTypeH264,
    		ClockRate: 90000,
    	},
    	"video", "pion",
    )
    pc.AddTrack(track)

    信令交换部分:

    接收浏览器Offer → 设置远端 SDP → 生成 Answer → 返回给浏览器。GatheringCompletePromise用于等待ICE candidate收集完成,否则SDP不完整。

    offer := webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: req.SDP}
    pc.SetRemoteDescription(offer)
    
    ans, _ := pc.CreateAnswer(nil)
    pc.SetLocalDescription(ans)
    
    <-webrtc.GatheringCompletePromise(pc)

    将Track存入clients,用于后续RTP分发:

    这里通过DESCRIBE + SETUP建立RTSP会话,并解析媒体格式(H264/H265)。

    clientsMu.Lock()
    clients[c.ClientIP()+"_"+time.Now().Format("150405")] = track
    clientsMu.Unlock()

    核心转发逻辑在OnPacketRTP:

    RTSP → 解复用 → 得到RTP包 → 广播写入所有WebRTC Track
    服务端本质上只是一个RTP转发器,并不进行转码,这带来两个重要特性:
    优点:延迟极低(基本无编码延迟)
    CPU占用极小
    架构简单稳定
    限制:浏览器必须支持该编码(通常是H264)
    RTSP输入必须与WebRTC codec兼容

    cli.OnPacketRTP(media, formaH264, func(pkt *rtp.Packet) {
    	clientsMu.Lock()
    	totalBytes += uint64(len(pkt.Payload))
    	totalPackets++
    
    	activeClients := make([]*webrtc.TrackLocalStaticRTP, 0, len(clients))
    	for _, t := range clients {
    		activeClients = append(activeClients, t)
    	}
    	clientsMu.Unlock()
    
    	for _, t := range activeClients {
    		t.WriteRTP(pkt)
    	}
    })

    统计接口“/stats”路由通过简单计数实现码率计算:

    前端每秒拉取一次,实现实时监控。

    kbps := uint64(float64(totalBytes*8) / 1024 / duration)
    pktps := uint64(float64(totalPackets) / duration)

    第一,WebRTC并不负责“获取视频”,它只负责“传输媒体流”。视频源可以来自 RTSP、文件、摄像头等。

    第二,WebRTC的关键不是API,而是:

    • SDP协商
    • ICE建连
    • RTP收发

    第三,这种架构属于典型的“边缘网关模式”:

    RTSP(设备侧协议) → WebRTC(浏览器协议)

    在工业监控、视频巡检、边缘计算中非常常见

  • 存取qDrant向量(Golang)

    检索增强生成(Retrieval-Augmented Generation, RAG)成为解决模型知识局限性和幻觉问题的有效手段。在RAG架构中,向量数据库负责存储和检索文本的向量表示,而qDrant作为高性能的向量数据库,常被选为底层存储。本文将结合Go语言代码示例,详细介绍如何通过langchain-go和qdrant-go库存取qDrant向量,并实现一个完整的RAG流程。

    环境:

    • qDrant 服务(本地或远程运行)
    • Ollama 服务(用于Embedding和文本生成)
      • 准备nomic-embed-text模型
      • 准备任意支持text的大模型
    • Go语言环境及以下库:
      • github.com/tmc/langchaingo(包含向量存储、文档加载器、文本分割器等)
      • github.com/qdrant/go-client(qDrant官方Go客户端)

    新建文件夹config/
    config文件含config.go,管理配置信息。包含qDrant地址、集合名称、Ollama URL、Embedding模型名和生成模型名等。

    1. 集合管理:EnsureCollection 和 resetCollection

    确保指定的集合存在,若不存在则创建。向量维度(768)与Embedding模型(如nomic-embed-text)匹配,距离度量采用余弦相似度。创建集合时指定的向量维度(768)必须与Embedding模型输出的维度完全一致。本例使用nomic-embed-text(维度768),若换用其他模型(如all-MiniLM-L6-v2维度384),需相应调整。
    resetCollection:先删除指定集合,再重新创建,用于重置整个知识库。

    func EnsureCollection(urlStr string, collectionName string) {
    	myclient, err := qdrant.NewClient(&qdrant.Config{
    		Host:   config.QdrantIP,
    		Port:   6334,
    		UseTLS: false,
    	})
    	if err != nil {
    		log.Fatal("连接Qdrant失败:", err)
    	}
    	defer myclient.Close()
    	ctx := context.Background()
    	exists, err := myclient.CollectionExists(ctx, collectionName)
    	if err != nil {
    		log.Fatal("查询集合状态失败:", err)
    	}
    	if !exists {
    		fmt.Printf("正在创建集合: %s...\n", collectionName)
    		err = myclient.CreateCollection(ctx, &qdrant.CreateCollection{
    			CollectionName: collectionName,
    			VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
    				Size:     768, // nomic-embed-text 维度是 768:必须与 Embedding 模型一致
    				Distance: qdrant.Distance_Cosine,
    			}),
    		})
    		if err != nil {
    			log.Fatal("创建集合失败:", err)
    		}
    		fmt.Println("集合创建成功")
    	}
    }
    func resetCollection(name string) {
    	myclient, _ := qdrant.NewClient(&qdrant.Config{
    		Host: config.QdrantIP, Port: 6334, UseTLS: false,
    	})
    	defer myclient.Close()
    	ctx := context.Background()
    	_ = myclient.DeleteCollection(ctx, name)
    	EnsureCollection(config.QdrantIP, name)
    	fmt.Println("集合已重置,准备重新导入...")
    }

    2. 数据导入:IngestKnowledge

    分块策略直接影响检索效果,需根据文档类型和任务调整块大小和重叠。

    func IngestKnowledge(filePath string, shouldReset bool) {
        // 1. 重置或确保集合存在
        if shouldReset {
            resetCollection(config.Collection)
        } else {
            EnsureCollection(config.QdrantIP, config.Collection)
        }
    
        // 2. 根据文件类型选择加载器(PDF或文本)
        f, _ := os.Open(filePath)
        defer f.Close()
        var loader documentloaders.Loader
        if strings.HasSuffix(filePath, ".pdf") {
            loader = documentloaders.NewPDF(f, fileSize)
        } else {
            loader = documentloaders.NewText(f)
        }
    
        // 3. 加载并分割文档
        docs, _ := loader.LoadAndSplit(ctx, textsplitter.NewRecursiveCharacter(
            textsplitter.WithChunkSize(300),
            textsplitter.WithChunkOverlap(100),
        ))
    
        // 4. 初始化Embedder和向量存储
        embedLLM, _ := ollama.New(ollama.WithModel(config.EmbedModel), ollama.WithServerURL(config.OllamaURL))
        embedder, _ := embeddings.NewEmbedder(embedLLM)
        store, _ := qdrantl.New(
            qdrantl.WithURL(url.URL{Scheme: "http", Host: config.QdrantURL}),
            qdrantl.WithCollectionName(config.Collection),
            qdrantl.WithEmbedder(embedder),
        )
    
        _, err = store.AddDocuments(ctx, docs) // 5. 添加文档到qDrant
    }

    3. 查询与生成:UpdateRAG

    确保集合存在,初始化Embedder和向量存储(与导入时相同)。向集合中添加几个示例文档块(演示用)。执行相似性搜索:将查询文本向量化,在qDrant中检索最相似的3个文档块。将检索结果拼接为上下文,构造提示词,调用生成模型(Ollama)得到最终回答。

    注意:实际应用中,文档导入和查询生成通常是分开的步骤,此处合并仅为展示完整流程。

    func UpdateRAG() {
        // 1. 确保集合存在,初始化Embedder和向量存储
        EnsureCollection(config.QdrantIP, config.Collection)
        embedLLM, _ := ollama.New(ollama.WithModel(config.EmbedModel), ollama.WithServerURL(config.OllamaURL))
        embedder, _ := embeddings.NewEmbedder(embedLLM)
        store, _ := qdrantl.New(
            qdrantl.WithURL(url.URL{Scheme: "http", Host: config.QdrantURL}),
            qdrantl.WithCollectionName(config.Collection),
            qdrantl.WithEmbedder(embedder),
        )
    
        // 2. 准备示例文档并分割(此处仅为演示,实际可从文件加载)
        docs := []schema.Document{ ... }
        splitter := textsplitter.NewRecursiveCharacter(textsplitter.WithChunkSize(200), textsplitter.WithChunkOverlap(20))
        var chunks []schema.Document
        for _, doc := range docs {
            texts, _ := splitter.SplitText(doc.PageContent)
            for _, t := range texts {
                chunks = append(chunks, schema.Document{PageContent: t, Metadata: doc.Metadata})
            }
        }
        store.AddDocuments(ctx, chunks)
    
        // 3. 执行相似性搜索
        query := "Qdrant 适合用在什么场景?"
        results, _ := store.SimilaritySearch(ctx, query, 3)
    
        // 4. 构建提示词并调用LLM生成回答
        contextText := ""
        for _, doc := range results {
            contextText += doc.PageContent + "\n"
        }
        llm, _ := ollama.New(ollama.WithModel(config.GenerateModel), ollama.WithServerURL(config.OllamaURL))
        prompt := fmt.Sprintf(`
    你是一个基于知识库回答问题的助手。
    只能根据【知识库】内容作答,如果无法得到答案,请回答“不知道”。
    【知识库】
    %s
    【问题】
    %s
    `, contextText, query)
        answer, _ := llms.GenerateFromSinglePrompt(ctx, llm, prompt)
        fmt.Println(answer)
    }

    4. 运行示例

    func main() {
    	IngestKnowledge("./xxix.pdf", true) //指向需要索引的知识文件
            UpdateRAG()
    }
    输出:
    文档已成功写入 Qdrant
    
    Model回答:
    Qdrant 是一个高性能的向量数据库,常用于 RAG(检索增强生成)系统,因此适合用于构建基于语义搜索、推荐系统、异常检测等需要向量相似性检索的场景。
  • Load AVG

    Load Average】:代表机器在某一时间段内,处于“可运行状态”或“不可中断等待状态”的进程平均数量。

    三个数值统计跨度分别为:1分钟平均 5分钟平均 15分钟平均


    一台4核心的处理器的Load为 1.21 1.34 1.12

    1、CPU压力为 1.21 / 4 ≈ 0.30

    2、CPU压力为 1.34 / 4 ≈ 0.34

    3、CPU压力为 1.12 / 4 ≈ 0.28

    平均只有约1个任务在运行或等待CPU,而系统有4核心,完美胜任服务


    在Linux中,平均负载并非在每个时钟滴答时计算,而是由一个基于HZ频率设置的变量值驱动,并在每个时钟滴答时进行检测。该设置定义了内核时钟滴答速率(单位:赫兹,即每秒次数),默认值为100,对应10毫秒的滴答间隔。内核活动使用这些滴答数进行计时。具体来说,calc_load() 函数(位于loadavg.h文件,原为 sched.h)负责计算平均负载,它大约每LOAD_FREQ(5*HZ+1)个滴答运行一次,即略多于5秒。

  • TOPS和TFLOPS

    选择


    TOPS和TFLOPS代表了计算系统中不同的硬件性能。
    TOPS代表 Tera Operations Per Second
    TFLOPS 代表 Tera Floating Point Operations Per Second

    TOPS衡量芯片每秒能完成多少万亿次整数运算(整数的加法/乘法)。这对于吞吐量比最终精度更重要的任务尤为关键——例如,自动驾驶车辆中的神经处理单元(NPU)或RTX 5070显卡提供数千个TOPS以快速处理传感器数据。 相比之下,TFLOPS计算的是每秒可执行的万亿次浮点(十进制)计算。 浮点数学对于高精度工作至关重要,比如训练神经网络或科学模拟。


    如果工作目标是AI训练或任何需要高精度的任务,选择TFLOPS评分很高的GPU。
    如果是边缘或移动端的实时AI推理,选择高TOPS的NPU或GPU。

    关键区别


    精度与速度:浮点运算涉及小数且精度更高,因此优化TFLOPS(每秒万亿次浮点运算)的硬件用于对精度敏感的任务(如图形处理或气候模型)。整数运算(以TOPS,即每秒万亿次运算衡量)使用整数,更简单快速。正如资料所示,浮点运算“涉及小数点”,适用于高精度场景;而整数运算用于更简单的任务。实践中,GPU(英伟达、AMD独显等)为复杂计算强调TFLOPS,而NPU(神经网络处理器)和数字信号处理单元则强调TOPS以快速处理大量推理任务。

    硬件效率:整数运算比浮点运算简单,硬件通常能实现更高吞吐量。这就是NPU和其他推理加速器宣传极高TOPS值的原因。例如,现代PC CPU如英特尔酷睿Ultra系列或高通骁龙芯片集成了擅长整数运算的NPU,能以低功耗实现每秒数万亿次运算。相比之下,在相同制程下,GPU的浮点单元每秒运算次数少于NPU的整数单元,因为浮点运算更复杂。

    应用适用性:根据工作负载选择合适的指标。若进行AI训练或重型计算,需要高TFLOPS。数据中心GPU和加速器(英伟达A100/H100、AMD Instinct、谷歌TPU)使用FP32、FP16或BF16精度提供巨大的TFLOPS(通常达数十或数百)。TOPS与TFLOPS数值不能直接等同比较。例如,NPU的“130 TOPS”并不天然优于或劣于GPU的“65 TFLOPS”——它们反映不同类型的吞吐能力。TOPS反映通用(通常为低精度)运算,而TFLOPS反映十进制精度运算。高TOPS芯片可能擅长实时运行图像分类器,但在训练需要高浮点精度的模型时可能吃力。反之,高TFLOPS芯片能训练庞大模型,但在相同推理任务上可能功耗更高、成本更大。

  • 对象存储(Object Storage Service)

    OSS(对象存储服务)以存储空间(Bucket)作为数据的逻辑容器,用来统一管理对象。每个Bucket都具有唯一性,并绑定了固定的存储类别、访问权限策略以及所属地域,用户可以通过Bucket对应的访问域名在互联网上定位并访问其中的数据。

    在OSS中,对象(Object)是最基本的数据存储单元,可以理解为“文件 + 属性信息”的整体。一个对象由Key、Metadata和Data三部分组成。Key即对象的唯一名称,是经过UTF-8编码的字符串,在同一个Bucket内不能重复;Metadata是对象的描述信息,以键值对形式存在,用于说明对象的各种属性;Data则是对象实际存储的二进制数据内容。Metadata又分为系统元数据和用户元数据,其中系统元数据由OSS自动维护,用于对象管理和传输,例如内容长度、最后修改时间、ETag等;用户元数据则由用户在上传时自定义,用来补充描述对象的业务属性。

    OSS根据数据访问频率和成本需求提供了多种存储类别。标准存储具备高可靠性、高可用性和高性能,访问时延低、吞吐能力强,适合需要频繁访问的热点数据场景,如网站图片、音视频内容和移动应用资源。低频存储面向访问次数较少但仍需实时读取的数据,存储成本低于标准存储,适合长期备份类数据,但对象存储时间不足30天提前删除会产生费用。归档存储则以最低的存储成本支持长期保存的数据,适合日志、档案和影视素材等几乎不访问的数据,取回时需要等待一定时间,并且对最短存储周期有更严格的限制。

    在底层能力上,OSS通过多副本和纠删码等冗余机制,将数据分布在跨可用区甚至跨设备的存储节点上,即使多台设备同时发生故障,也能保证数据不丢失、业务不中断,并自动完成冗余修复。同时,系统会周期性校验数据完整性,发现异常后利用冗余数据进行重建,进一步保障数据可靠性。

    OSS还提供对象级别的强一致性保障。所有对象操作都是原子的,不存在中间状态:操作要么成功,要么失败。只要用户收到成功响应,数据就已经处于可用状态。例如对象上传完成后即可立即读取,更新对象时,其他并发读请求只会读到旧数据,而不会读到部分或损坏的数据。这种强一致性特性使得OSS在使用方式上更接近传统存储系统,简化了应用架构设计。

    在数据组织方式上,OSS采用Key-Value模型,而不是传统文件系统的目录树结构。对象的Key是访问数据的唯一标识,即使Key看起来像“FolderA/FolderB/file.jpg”,在OSS内部也只是一个普通字符串,并不代表真实存在的层级目录。因此,不同Key的访问效率基本一致,不会因为“目录层级”变深而降低性能。所谓的目录操作,本质上是对前缀匹配的一批对象进行处理,成本较高,也不被推荐频繁使用。

    由于OSS不支持对象的在线修改,即使只改动一个字节,也需要重新上传整个对象,因此它天然适合“一次写入,多次读取”的业务模型。依托分布式架构和Key-Value存储方式,OSS能够支持海量数据和高并发访问,非常适合静态资源分发、备份归档等场景。

    在访问和管理方式上,用户可以通过网页形式的管理控制台直观地创建Bucket、上传下载文件并配置权限;也可以使用支持S3接口的客户端工具(如S3Browser)进行本地化管理;对于开发场景,OSS提供了封装良好的SDK,方便应用程序直接调用对象存储能力;同时还提供基于HTTPS、数据格式为JSON的RESTful API,满足更底层或统计类的接口调用需求。

  • 工具iproute2命令

    Iproute2是一组用于控制Linux中TCP/IP网络和流量控制的工具

    通过ip命令使用,搭配以下选项

    link — 逻辑网络设备

    address — 协议,设备上的(IPv4或IPv6)地址

    neighbour — ARP或NDISC缓存条目

    route — 路由表入口

    rule — 规则数据库

    maddress — 组播地址

    mroute — 路由缓存条目

    tunnel — IP协议通道


    使用

    ip neigh 
    //邻居表对象建立协议地址与链路层之间的绑定;为了共享同一物理链路的主机建立地址。邻居对象条目组织成表。IPv4邻居对象表也被称为ARP表。查看邻居表的绑定及其属性,添加新的邻居表条目,并删除旧的。
    ip link
    // 链路指的是网络设备。ip link对象及其对应的命令集允许查看并作网络设备的状态。
    ip addr
    // 地址指的是连接到网络设备上的协议(IP或IPv6)地址。每个设备必须至少有一个地址,才能使用对应的协议。可以有多个不同的地址连接到一个设备。这些地址在协议结构中并不被区分,因此“别名”一词并不完全适用于此类多地址。
    ip route
    // 管理内核路由表中的路由条目。内核路由表保存协议路径与其他网络节点的信息。每个路由条目都有一个密钥,由协议前缀组成,即网络地址和网络掩码长度的配对,以及可选的类型服务(TOS)值。如果IP数据包的目的地址的最高位数等于路由前缀至少是前缀长度,且路由的TOS为零或等于数据包的TOS。
    ss -tlpn
    // -t tcp连接 -l监听 -p进程 -n不解析服务名
    tc qdisc add dev eth0 root tbf rate 10mbit burst 32kbit latency 400ms
    // 限速eth0网卡下行10mbps
    
    tc qdisc del dev eth0 root
    // 解除eth0网卡限速
    ip rule
    // 路由策略中的规则数据库控制路由选择算法。经典路由互联网中使用的算法仅基于数据包的目的地址,理论上,但实际上不基于TOS字段。在某些情况下,我们希望路由数据包的方式不同,不仅取决于目的地址,还包括其他数据包字段,如源地址、IP协议,传输协议端口甚至数据包有效载荷。这项任务称为“策略路由”。

  • WebSocket和HTTP Upgrade机制

    HTTP协议用于浏览器和服务器之间传输数据。每次请求是独立的,不记历史,HTTP来轮询服务器更新增加性能支出。

    WebSocket协议旨在取代现有利用HTTP作为传输层的双向通信技术,从而利用现有基础设施(代理、过滤、认证)。但WS是基于HTTP层,而不是为了取代HTTP。

    在HTTP协议中,“Upgrade”机制允许客户端告诉服务器把当前的HTTP连接升级到另一种协议。(RFC 2616)。

    WebSocket协议由开启握手和基础消息框架组成,分层叠加在TCP之上, 该技术的目标是为需要与服务器双向通信的浏览器应用提供一种机制,无需开启多个HTTP连接。WS通过HTTP升级协议握手。一条向服务器的WS握手:

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Origin: http://example.com
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13

    一条向客户端的WS握手:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    Sec-WebSocket-Protocol: chat

    一旦客户端和服务器都发送了握手,如果握手成功,数据传输阶段才开始。这是一个双向通信通道,双方可以独立地随意发送数据。成功握手后,客户端和服务器以本规范中称为“消息”的概念单元进行数据传递。 在线路上,一条消息由一个或多个组成框架。 WebSocket消息不一定对应于特定的网络层框架,因为分段消息可能被中介合并或拆分。

    Sec-WebSocket-Protocol: chat
    用于防止脚本在网页浏览器中使用 WebSocket API 时未经授权交叉使用该服务器。 服务器会被告知生成WebSocket连接请求的脚本来源。 如果服务器不愿意接受来自该源的连接,可以通过发送相应的HTTP错误代码来拒绝连接。 该头字段由浏览器客户端发送;对于非浏览器客户端,如果在客户端上下文中合理,可能会发送该头字段。

    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    对于该头部字段,服务器必须将头部字段中的值与全局唯一标识符字符串形式“258EAFA5-E914-47DA- 95CA-C5AB0DC85B11”串联起来,这通常不会被不理解WebSocket协议的网络端点使用。 该连接的SHA-1哈希值(160位),以base64编码,随后在服务器握手中返回。

    HTTP/1.1 101 Switching Protocols
    除了101以外的任何状态码,都表示WebSocket握手尚未完成,HTTP的语义仍然适用。头部紧跟状态码。

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

    WebSocket客户端会检查这些字段以查找脚本页面。如果 |Sec-WebSocket-Accept|如果缺少头字段,或者 HTTP 状态码不是 101,则连接无法建立,WebSocket帧也不会发送。


    通过升级器,http服务被升级为websocket服务,除非得到websocket握手,否则连接失败:

    #Go
    
    #An upgrader
    var upgrader = websocket.Upgrader{
    	CheckOrigin: func(r *http.Request) bool {
    		return true
    	},
    }
    
    var wsClients   = make(map[*websocket.Conn]bool)
    
    func wsHandler(c *gin.Context) {
    	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    	if err != nil {
    		return
    	}
    	defer conn.Close()
    	wsClients[conn] = true
    	for {
    		_, _, err := conn.ReadMessage()
    		if err != nil {
    			delete(wsClients, conn)
    			break
    		}
    	}
    }
    
    func main() {
            r := gin.Default()
            r.GET("/ws", wsHandler)
    }
  • CRC(cylic redundancy check)数据帧校验

    crc用于校验数据传输过程的完整性,crc码通常位于数据帧尾部,与数据帧同时传输。

    计算数据帧的crc码与接收的crc码来确认数据完整性:

    初始化一个寄存器为0xFFFF,逐步与每个数据帧进行异或操作。

    # golang
    func CalculateCRC(data []byte) uint16 {
    	crc := uint16(0xFFFF)
    	for _, b := range data {
    		crc ^= uint16(b)
    		for i := 0; i < 8; i++ {
    			if crc&0x0001 != 0 {
    				crc >>= 1
    				crc ^= 0xA001
    			} else {
    				crc >>= 1
    			}
    		}
    	}
    	return crc
    }
  • RandKey-QT 生成随机密码 桌面端工具

    RandKey-QT -> Github Page

    解决需要生成随机字符的燃眉之急

    Solve the urgent need to generate random characters && numbers temporatily

    ScreenShot ScreenShot

    用法 Usage

    确保用的是Qt 6.8。 make sure Qt version is 6.8.

    • 下载二进制 Download binary, 直接运行 run directly.

    • 或者克隆这个文件夹,然后在Qt craetor打开。 or clone this repository, open in Qt craetor.

    国际化 International

    1. lupdate . RandKey_QT_en_GB.ts

    2. linguist RandKey_QT_en_GB.ts

    或者使用以下脚本 or use the below Shell script:

    TS_DIR="./RandKey-QT" # .ts file
    QM_DIR="$TS_DIR/translations" # output .qm dir
    
    for tsfile in "$TS_DIR"/*.ts; do
        if [ -f "$tsfile" ]; then
            echo "Updating $tsfile "
            lupdate . -ts "$tsfile" -no-obsolete
        fi
    done
    this
    for tsfile in "$TS_DIR"/*.ts; do
        filename=$(basename "$tsfile" .ts)   
        qmfile="$QM_DIR/$filename.qm"      
        echo "Generating $qmfile "
        lrelease "$tsfile" -qm "$qmfile"
    done
    
  • Golang Range

    Golang的range可以用来遍历array,slice,string,map或channel


    range array:

    func main() {
    	animals := [3]string{}
    	animals[0] = "tiger"
    	animals[1] = "cat"
    	animals[2] = "lion"
    	for index, animal := range animals {
    		fmt.Println(index, animal)
    	}
    }
    
    //output:
    //0 tiger
    //1 cat
    //2 lion

    range slice:

    func main() {
    	urls := []string{"ziyaun.work", "https://ziyuan.work"}
    	for index, url := range urls {
    		fmt.Println(index, url)
    	}
    }
    
    //output:
    //0 ziyaun.work
    //1 https://ziyuan.work

    range string:

    func main() {
    	url := "ziyuan.work"
    	for i, v := range url {
    		fmt.Println(i, v)
    	}
    }
    
    //output:
    //0 122
    //1 105
    //2 121
    //3 117
    //4 97
    //5 110
    //6 46
    //7 119
    //8 111
    //9 114
    //10 107

    range map:

    func main() {
    	userList := make(map[string]string)
    	userList["url"] = "ziyuan.work"
    	for key, value := range userList {
    		value = "I change myself" //改变的只是提取item的值,userlist不受影响
    		fmt.Println(userList[key])
    		fmt.Println(value)
    	}
    }
    
    // output:
    //ziyuan.work
    //I change myself

    range channel:

    func main() {
    	wg := sync.WaitGroup{}
    	wg.Add(1)
    
    	c := make(chan int)
    	go func() {
    		defer wg.Done()
    		for i := 0; i < 10; i++ {
    			c <- i
    		}
    	}()
    
    	go func() {
    		wg.Wait()
    		close(c)
    	}() // 当使用range对channel操作时,会无限遍历,最终变成死锁,因此需要关闭channel
    
    	for value := range c {
    		log.Println(value)
    	}
    }
    
    //output:
    //0
    //1
    //2
    //3
    //4
    //5
    //6
    //7
    //8
    //9