golang netpoller揭秘

golang netpoller是网络IO模型的核心部分,利用了操作系统提供的事件通知机制,如Linux的epoll、BSD的kqueue或者windows的IOCP。这些机制允许应用程序监视多个文件描述符(在网络编程中,通常是 socket),并在其中任何一个准备好进行 I/O 操作时接收通知。

数据结构

netFD

网络连接都是基于对netFD结构的操作

// 网络文件描述符
type netFD struct {pfd poll.FD// immutable until Close// 网络协议族。比如AF_INET表示ipv4,AF_INET6表示ipv6family      int// socket类型sotype      int// 握手是否完成isConnected bool // handshake completed or use of association with peer// 网络类型。比如tcp、ipnet         string// 储存网络连接的本地地址laddr       Addr// 储存网络连接的远程地址raddr       Addr
}// 文件描述符。可表示网络连接或者系统文件
type FD struct {// 用于锁定文件描述符并序列化 Read 和 Write 方法的使用。fdmu fdMutex// 系统文件描述符。这个字段的值在 Close 方法调用之前是不变的。Sysfd int// 文件描述符的平台相关状态。SysFile// I/O 轮询器pd pollDesc// 文件关闭时发出的信号量csema uint32// 非0代表处于堵塞模式isBlocking uint32// 是否是一个流式描述符,而不是一个基于数据包的描述符,如 UDP 套接字。这个字段的值是不变的。IsStream bool// 零字节读取是否表示 EOF。对于基于消息的套接字连接,这个字段的值是 false。ZeroReadIsEOF bool// 表示这是否是一个文件,而不是一个网络套接字。isFile bool
}

pollDesc

pollDesc为底层轮询器封装

type pollDesc struct {runtimeCtx uintptr
}type pollDesc struct {_     sys.NotInHeap// 下一个pollDesclink  *pollDesc      // in pollcache, protected by pollcache.lock// 文件描述符fd    uintptr        // constant for pollDesc usage lifetime// 保护pollDesc不受过时的的影响fdseq atomic.Uintptr // protects against stale pollDesc// 保存从 closing、rd 和 wd 中获取的位,这些位只在持有锁的情况下写入,以供 netpollcheckerr 使用,netpollcheckerr 不能获取锁。在可能改变摘要的方式下锁定这些字段后,代码必须在释放锁之前调用 publishInfo。atomicInfo atomic.Uint32 // atomic pollInfo// 读取和写入的G 指针,它们是原子访问的rg atomic.Uintptr // pdReady, pdWait, G waiting for read or pdNilwg atomic.Uintptr // pdReady, pdWait, G waiting for write or pdNillock    mutex // protects the following fields// 表示是否正在关闭closing bool// 用户可设置的cookieuser    uint32    // 保护读写计时器不受过时的影响rseq    uintptr   // protects from stale read timerswseq    uintptr   // protects from stale write timers// 读截止timerrt      timer     // read deadline timer (set if rt.f != nil)// 读截止时间rd      int64     // read deadline (a nanotime in the future, -1 when expired)// 写截止timerwt      timer     // write deadline timer// 写截止时间wd      int64     // write deadline (a nanotime in the future, -1 when expired)// 指向pollDesc自身的指针,用于间接接口的储存self    *pollDesc
}
pollCache

pollCache用于缓存pollDesc结构,可以避免在每次网络IO操作时都创建新的pollDesc

type pollCache struct {// 互斥锁,用于在多个 goroutine 之间同步对 pollCache 的访问lock  mutex// 指向 pollDesc 的指针,表示缓存中的第一个 pollDesc 结构。如果缓存为空,那么这个字段的值就是nilfirst *pollDesc
}

PollDesc 对象必须是类型稳定的,因为在描述符关闭或重用后,我们可能会从 epoll/kqueue 中获取到就绪通知。这是因为在网络编程中,文件描述符(File Descriptor,简称 FD)是一个重要的资源,它在关闭后可能会被立即重用。如果一个 FD 关闭后,另一个 FD 立即重用了这个数字,那么原来的 FD 上的任何未决的事件可能会错误地通知到新的 FD 上。

使用 seq 变量来检测过时的通知。在改变截止日期或者描述符被重用时,seq 会增加。这是为了防止因为 FD 的重用导致的错误通知。通过在每次改变 FD 的状态时增加 seq,可以确保即使 FD 被重用,也能正确地识别出哪些通知是过时的

监听

net.Listen

调用net.Listen之后会通过系统调用socket方法创建网络socket分配给listener

// socket returns a network file descriptor that is ready for
// asynchronous I/O using the network poller.
func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlCtxFn func(context.Context, string, string, syscall.RawConn) error) (fd *netFD, err error) {// 创建系统sockets, err := sysSocket(family, sotype, proto)if err != nil {return nil, err}// 设置默认的socket选项if err = setDefaultSockopts(s, family, sotype, ipv6only); err != nil {poll.CloseFunc(s)return nil, err}// 创建新的网络文件描述符if fd, err = newFD(s, family, sotype, net); err != nil {poll.CloseFunc(s)return nil, err}// 若本地地址存在,远程地址不存在,则为监听。根据socket类型,调用网络文件描述符方法进行监听if laddr != nil && raddr == nil {switch sotype {case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET:if err := fd.listenStream(ctx, laddr, listenerBacklog(), ctrlCtxFn); err != nil {fd.Close()return nil, err}return fd, nilcase syscall.SOCK_DGRAM:if err := fd.listenDatagram(ctx, laddr, ctrlCtxFn); err != nil {fd.Close()return nil, err}return fd, nil}}// 否则,为dial请求if err := fd.dial(ctx, laddr, raddr, ctrlCtxFn); err != nil {fd.Close()return nil, err}return fd, nil
}

netFD.listenStream

netFD.listenStream创建socket,并绑定监听地址进行监听

func (fd *netFD) listenStream(ctx context.Context, laddr sockaddr, backlog int, ctrlCtxFn func(context.Context, string, string, syscall.RawConn) error) error {// 设置默认的监听socket选项var err errorif err = setDefaultListenerSockopts(fd.pfd.Sysfd); err != nil {return err}// 获取socket地址var lsa syscall.Sockaddrif lsa, err = laddr.sockaddr(fd.family); err != nil {return err}// 执行控制操作if ctrlCtxFn != nil {c := newRawConn(fd)if err := ctrlCtxFn(ctx, fd.ctrlNetwork(), laddr.String(), c); err != nil {return err}}// 将socket绑定到指定地址if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {return os.NewSyscallError("bind", err)}// 开始监听if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {return os.NewSyscallError("listen", err)}// 网络描述符初始化,注册到netpoller中if err = fd.init(); err != nil {return err}// 获取socket本地网络地址,并设置到fd本地地址中lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)fd.setAddr(fd.addrFunc()(lsa), nil)return nil
}

poll_runtime_pollOpen

在网络描述符初始化中,会调用poll_runtime_pollOpen打开文件描述符,并注册到netpoller中

func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) {// 从缓存中分配到pollDescpd := pollcache.alloc()lock(&pd.lock)// 初始化分配到的pollDescwg := pd.wg.Load()if wg != pdNil && wg != pdReady {throw("runtime: blocked write on free polldesc")}rg := pd.rg.Load()if rg != pdNil && rg != pdReady {throw("runtime: blocked read on free polldesc")}pd.fd = fdif pd.fdseq.Load() == 0 {// The value 0 is special in setEventErr, so don't use it.pd.fdseq.Store(1)}pd.closing = falsepd.setEventErr(false, 0)pd.rseq++pd.rg.Store(pdNil)pd.rd = 0pd.wseq++pd.wg.Store(pdNil)pd.wd = 0pd.self = pd// 更新pollDesc的原子信息pd.publishInfo()unlock(&pd.lock)// 将文件描述符注册到netpollererrno := netpollopen(fd, pd)if errno != 0 {pollcache.free(pd)return nil, int(errno)}return pd, 0
}

netpollopen将pollDesc注册到Linux epoll中

func netpollopen(fd uintptr, pd *pollDesc) uintptr {var ev syscall.EpollEvent// 设置事件为输入、输出、连接被对方关闭或者半关闭事件以及设置边缘触发模式ev.Events = syscall.EPOLLIN | syscall.EPOLLOUT | syscall.EPOLLRDHUP | syscall.EPOLLET// 将 pd 和 pd.fdseq.Load() 打包成一个标记指针 tp。这个标记指针被存储在 ev.Data 中tp := taggedPointerPack(unsafe.Pointer(pd), pd.fdseq.Load())*(*taggedPointer)(unsafe.Pointer(&ev.Data)) = tp// 将 fd 添加到 epoll 的实例中,并返回 syscall.EpollCtl 的结果。syscall.EpollCtl 是一个系统调用,用于控制 epoll 的行为。在这里,它的行为是添加一个新的文件描述符到 epoll 的实例中return syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, int32(fd), &ev)
}

接收

listener.Accept

listener.Accept实质上就是调用其内部的netFD的accept方法,接受网络连接,并以建立的新连接创建网络文件描述符

func (fd *netFD) accept() (netfd *netFD, err error) {// 接受新的网络连接,并返回新的系统文件描述符d和远程socket地址 rsad, rsa, errcall, err := fd.pfd.Accept()if err != nil {if errcall != "" {err = wrapSyscallError(errcall, err)}return nil, err}// 根据建立连接的系统文件描述符,创建新的网络文件描述符if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {poll.CloseFunc(d)return nil, err}// 初始化新网络文件描述符if err = netfd.init(); err != nil {netfd.Close()return nil, err}// 获取新网络文件描述符的本地socket地址,并设置到fd本地地址中lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd)netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))return netfd, nil
}

fd.Accept

fd.Accept与下面fd.Read大致流程一致,只是循环调用的是accept

func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {if err := fd.readLock(); err != nil {return -1, nil, "", err}defer fd.readUnlock()if err := fd.pd.prepareRead(fd.isFile); err != nil {return -1, nil, "", err}for {s, rsa, errcall, err := accept(fd.Sysfd)if err == nil {return s, rsa, "", err}switch err {case syscall.EINTR:continuecase syscall.EAGAIN:if fd.pd.pollable() {if err = fd.pd.waitRead(fd.isFile); err == nil {continue}}case syscall.ECONNABORTED:// This means that a socket on the listen// queue was closed before we Accept()ed it;// it's a silly error, so try again.continue}return -1, nil, errcall, err}
}

读取

fd.Read

主要就是加锁,尝试从文件描述符中读取数据,若读取不成功,就进行读取等待

func (fd *FD) Read(p []byte) (int, error) {// 获取读锁if err := fd.readLock(); err != nil {return 0, err}defer fd.readUnlock()// 若读取字节长度为0,直接返回if len(p) == 0 {return 0, nil}// 准备读取if err := fd.pd.prepareRead(fd.isFile); err != nil {return 0, err}// 若fd是流,且读取字节长度大于1<<30,则截断if fd.IsStream && len(p) > maxRW {p = p[:maxRW]}for {// 系统调用从文件描述符中读取数据n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)if err != nil {n = 0// 若读未就绪时,等待if err == syscall.EAGAIN && fd.pd.pollable() {if err = fd.pd.waitRead(fd.isFile); err == nil {continue}}}err = fd.eofError(n, err)return n, err}
}

在读取信息未到达时,waitRead方法会调用到runtime_pollWait方法,而runtime_pollWait又会调用netpollblock方法

netpollblock

netpollblock用于堵塞当前goroutine,等待网络IO事件的发生

// 对于同一个模式,不允许并发调用netpollblock,因为 pollDesc 只能为每种模式持有一个等待的 goroutine
func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {// 根据读写模式,获取状态gpp := &pd.rgif mode == 'w' {gpp = &pd.wg}// set the gpp semaphore to pdWaitfor {// 若期待的IO事件准备好了,则解除堵塞if gpp.CompareAndSwap(pdReady, pdNil) {return true}// 若没有期待的IO事件发生,则设置为等待,退出循环if gpp.CompareAndSwap(pdNil, pdWait) {break}// 防止出现意外状态,导致无限循环if v := gpp.Load(); v != pdReady && v != pdNil {throw("runtime: double wait")}}// 若设置了忽略错误等待或者无错误,则堵塞// 在将 gpp 设置为 pdWait 后需要重新检查错误状态。因为 runtime_pollUnblock、runtime_pollSetDeadline 和 deadlineimpl 这几个函数的操作顺序与此相反:它们先将状态存储到 closing/rd/wd,然后发布信息,最后加载 rg/wg。所以,为了保证状态的正确性,需要在设置 gpp 为 pdWait 后重新检查错误状态if waitio || netpollcheckerr(pd, mode) == pollNoError {gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceBlockNet, 5)}// 堵塞结束后,设置pdNil状态old := gpp.Swap(pdNil)if old > pdWait {throw("runtime: corrupted polldesc")}return old == pdReady
}

netpoll

netpoll检测所有就绪的网络连接,并返回所有可允许的goroutine。因此,通过该方法可以唤醒在网络读取、写入等待中的goroutine

func netpoll(delay int64) (gList, int32) {if epfd == -1 {return gList{}, 0}// 根据输入的延迟时间,计算epoll等待时间// 等待时间delay < 0,永远堵塞下去// 等待时间delay = 0,非堵塞,仅轮询// 等待时间delay > 0,堵塞若干时间var waitms int32if delay < 0 {waitms = -1} else if delay == 0 {waitms = 0} else if delay < 1e6 {waitms = 1} else if delay < 1e15 {waitms = int32(delay / 1e6)} else {// An arbitrary cap on how long to wait for a timer.// 1e9 ms == ~11.5 days.waitms = 1e9}// 调用epollWait,等待epoll事件var events [128]syscall.EpollEvent
retry:n, errno := syscall.EpollWait(epfd, events[:], int32(len(events)), waitms)if errno != 0 {if errno != _EINTR {println("runtime: epollwait on fd", epfd, "failed with", errno)throw("runtime: netpoll failed")}// If a timed sleep was interrupted, just return to// recalculate how long we should sleep now.if waitms > 0 {return gList{}, 0}goto retry}// 遍历事件,获取需要恢复的goroutinevar toRun gListdelta := int32(0)for i := int32(0); i < n; i++ {ev := events[i]if ev.Events == 0 {continue}// 如果设置了提前唤醒,则会跳出当前轮询。因为break事件只是表示有其他事情需要处理,并不是真正的网络事件if *(**uintptr)(unsafe.Pointer(&ev.Data)) == &netpollBreakRd {if ev.Events != syscall.EPOLLIN {println("runtime: netpoll: break fd ready for", ev.Events)throw("runtime: netpoll: break fd ready for something unexpected")}// 如果 delay 不为 0,这表示这个 "break" 事件是在一个阻塞的轮询中被检测到的。在这种情况下,代码会从 netpollBreakRd 读取数据,并将 netpollWakeSig 设置为 0。这样可以确保下一次 "break" 事件能够被正确处理。if delay != 0 {var tmp [16]byteread(int32(netpollBreakRd), noescape(unsafe.Pointer(&tmp[0])), int32(len(tmp)))netpollWakeSig.Store(0)}continue}// 判断事件发生的类型,以决定从pollDesc中wg还是rg取goroutinevar mode int32if ev.Events&(syscall.EPOLLIN|syscall.EPOLLRDHUP|syscall.EPOLLHUP|syscall.EPOLLERR) != 0 {mode += 'r'}if ev.Events&(syscall.EPOLLOUT|syscall.EPOLLHUP|syscall.EPOLLERR) != 0 {mode += 'w'}if mode != 0 {// 取出保存在事件数据中的pollDes,并添加到就绪goroutine链表tp := *(*taggedPointer)(unsafe.Pointer(&ev.Data))pd := (*pollDesc)(tp.pointer())tag := tp.tag()if pd.fdseq.Load() == tag {pd.setEventErr(ev.Events == syscall.EPOLLERR, tag)delta += netpollready(&toRun, pd, mode)}}}return toRun, delta
}

总结

netpoller依托于go调度器,提供了一种看上去同步的异步网络编程模式,显著地降低了开发难度

更重要的是,go主动挂起goroutine等待网络IO的完成,而不是被动让系统线程去挂起,这就将执行网络IO的goroutine掌控在Go运行时中

Ref

  1. https://strikefreedom.top/archives/go-netpoll-io-multiplexing-reactor

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

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

相关文章

LLM优化:开源星火13B显卡及内存占用优化

1. 背景 本qiang~这两天接了一个任务&#xff0c;部署几个开源的模型&#xff0c;并且将本地经过全量微调的模型与开源模型做一个效果对比。 部署的开源模型包括&#xff1a;星火13B&#xff0c;Baichuan2-13B, ChatGLM6B等 其他两个模型基于transformers架构封装&#xff0…

【Linux】Centos7安装部署asterisk,搭建 SIP服务器

1、安装环境依赖 yum install -y make gcc zlib-devel perl wget yum install -y gcc gcc-c autoconf libtool automake make yum install -y openssl-devel &#xff08;以上需要联网安装&#xff0c;离线安装各种依赖需要进一步研究&#xff09; openssl version Open…

表单提交出现问题却没有报错

最近搞毕设提交表单传给后台总是出现错误&#xff0c;有时候可以运行成功&#xff0c;有时候运行不了但是没有报错&#xff0c;以为是jQuery导入的问题尝试换了jQuery的其他导入方式没有解决&#xff0c;后来发现前端页面的表单要防止默认操作&#xff01;&#xff01;&#xf…

CMUS狮身人面像(六)-调整语音识别准确性

调整语音识别准确性 精度差的原因测试数据库设置运行测试 语音识别的准确性并不总是很高。 首先&#xff0c;重要的是要了解您的准确性是否只是低于预期&#xff0c;还是总体上非常低。如果总体精度非常低&#xff0c;则您很可能错误配置了解码器。如果低于预期&#xff0c;可…

qt学习篇---C++基础学习

本学习笔记学习下面视频总结&#xff0c;感兴趣可以去学习。讲的很详细 【北京迅为】嵌入式学习之QT学习篇_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1tp4y1i7EJ/?spm_id_from333.337.search-card.all.click&vd_source8827cc0da16223b9f2ad8ae7111de9e2 目录 C…

【K8s】工作以来遇到的K8s相关问题、故障

工作以来遇到的有关K8S相关问题及故障 deployments 资源 2副本情况下&#xff0c;一个springboot的pod能访问&#xff0c;一个不能&#xff08;端口不通&#xff09;在K8S运维(多人管理) 不知道谁在链路加了个跨域配置&#xff0c;导致前端打不开图片某些安全部门演练时经常在…

Linux深入理解内核 - 内存寻址

目录 引论&#xff0c;三个地址 硬件中的分段 段描述符 快速访问段描述符 分段单元 Linux GDT Linux LDT 硬件中的分页 PAE 硬件高速缓存 TLB Linux中的分页 页表类型定义pgd_t、pmd_t、pud_t和pte_t pteval_t&#xff0c;pmdval_t&#xff0c;pudval_t&#xff0…

k8s pod 镜像拉取策略

在 Kubernetes (k8s) 中&#xff0c;Pod 容器镜像的拉取策略通过 imagePullPolicy 属性来控制。这一策略决定了 kubelet 如何以及何时从容器镜像仓库中拉取镜像。以下是三种主要的镜像拉取策略及其详细说明&#xff1a; Always: 说明: 这是默认的拉取策略。当设置为 Always 时&…

PDCA循环:持续精进的工具

文章目录 一、什么是PDCA二、PDCA的应用场景三、PDCA在信息系统项目管理中的应用 一、什么是PDCA PDCA循环是由美国质量管理专家沃特阿曼德休哈特&#xff08;Walter A. Shewhart&#xff09;在20世纪30年代提出的&#xff0c;最初用于制造业的质量管理。休哈特博士在构想PDCA…

【C++题解】1418. 求一个5位数的各个位之和

问题&#xff1a;1418. 求一个5位数的各个位之和 类型&#xff1a;基本运算、拆位求解 题目描述&#xff1a; 从键盘读入一个 5 位的正整数&#xff0c;请求出这个 5 位数的各个位之和。 输入&#xff1a; 一个 5 位的正整数 n 。 输出&#xff1a; 这个 5 位数的各个位之…

2385. 感染二叉树需要的总时间

2385. 感染二叉树需要的总时间 题目链接&#xff1a;2385. 感染二叉树需要的总时间 代码如下&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr)…

Aiseesoft Blu-ray Player for Mac:蓝光播放器

Aiseesoft Blu-ray Player for Mac是一款功能强大且易于使用的蓝光播放器&#xff0c;专为Mac用户打造。它以其卓越的性能和简洁的操作界面&#xff0c;为用户带来了全新的高清蓝光播放体验。 Aiseesoft Blu-ray Player for Mac v6.6.50激活版下载 这款软件支持播放任何高质量的…

ArcGIS Pro3.0软件破解版安装教程

软件名称&#xff1a;ArcGIS Pro 3.0 安装环境&#xff1a;Windows 软件大小&#xff1a;7.3GB 硬件要求&#xff1a;CPU2GHz&#xff0c;内存4G(或更高) 百度云下载链接 &#xff1a; https://pan.baidu.com/s/1CXy1MSwdQXdVnJoV2X422A 提 取 码 &#xff1a;r0w1 教学内…

AI图书推荐:ChatGPT写论文的流程与策略

论文一直是任何学术学位的顶峰。它展示了学生在研究领域的兴趣和专业知识。撰写论文也是一个学习经验&#xff0c;为学术工作以及专业研究角色做好准备。但是&#xff0c;论文工作总是艰苦的&#xff0c;通常是充满乐趣和创造性的&#xff0c;但有时也是乏味和无聊的。生成式人…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-6.4

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

采用前后端分离Vue,Ant-Design技术开发的(手麻系统成品源码)适用于三甲医院

开发环境 技术架构&#xff1a;前后端分离 开发语言&#xff1a;C#.net6.0 开发工具&#xff1a;vs2022,vscode 前端框架&#xff1a;Vue,Ant-Design 后端框架&#xff1a;百小僧开源框架 数 据 库&#xff1a;sqlserver2019 系统特性 麻zui、护理、PACU等围术期业务全覆…

FreeRTOS学习——FreeRTOS队列(上)

本篇文章记录我学习FreeRTOS队列的相关知识&#xff0c;主要包括队列简介、队列的结构体、队列创建等知识。 队列是为了任务与任务、任务与中断之间的通信而准备的&#xff0c;可以在任务与任务、任务与中断之间传递消息&#xff0c;队列中可以存储有限的、大小固定的数据项目。…

Android 在attrs.xml添加属性时出现 Found item Attr/****** more than one time

Android 在attrs.xml添加属性时出现 Found item Attr/****** more than one time 问题描述解决办法方式一方式二 小结 问题描述 在Android应用开发过程中&#xff0c;经常需要自定义控件&#xff0c;并且定义控件的属性&#xff0c;方便灵活的修改控件的显示样式&#xff0c;提…

HCIP-Datacom-ARST必选题库_无线【道题】

单选题 26/1327、 如图所示,漫游前数据流显的转发为STA-HAP-上层网络,转发方式为直接转发,并且在AP上配置了家乡代理经过三层漫游 以后,数据的转发流量走向是? STA-FAP-FAC-HAC-HAP-上层网络 BSTA-FAP-FAC-HAC-上层网络 STA-FAP-FAC-上层网络 STA-FAP-FAC-HAC-HAP-HAC-上…

IT廉连看——UniApp——样式绑定

IT廉连看——UniApp——样式绑定 一、样式绑定 两种添加样式的方法&#xff1a; 1、第一种写法 写一个class属性&#xff0c;然后将css样式写在style中。 2、第二种写法 直接把style写在class后面 添加一些效果&#xff1a;字体大小 查看效果 证明这样添加样式是没有问题的…