live555源码学习(1)

1 基础组件

live项目主要包含了四个基础库、程序入口类(mediaServer)和测试程序(testProgs)。四个基础库是UsageEnvironment、BasicUsageEnvironment、groupsock和liveMedia

UsageEnvironment

抽象了两个类UsageEnvironment和TaskScheduler,UsageEnvironment表示整个运行环境,提供错误记录和输出的功能。TaskScheduler表示任务调度中心,用于异步事件的读取和处理。该库中还有一个抽象类HashTable,这是一个通用的HashTable,在整个项目中都可以使用它。

BasicUsageEnvironment

UsageEnvironment和TaskScheduler的具体实现类

groupsock

网络接口的封装,用于数据包的接收和发送,同时支持多播和单播。groupsock库中包括了GroupEId、Groupsock、GroupsockHelper、NetAddress、NetInterface等类。GroupsockHelper类主要用于读写Socket。

liveMedia

基于基类Medium,实现各种流媒体和编解码类型结构,定义了source(生产者)和sink(消费者)操作。

mediaServer

mediaServer下的live555MediaServer提供了main函数,DynamicRTSPServer继承了RTSPServer并重写了虚函数lookupServerMediaSession

2 主程序

2.1 基本概念和实体

MediaServer是服务器的抽象。

  • 它创建用于接受客户端连接的socket。它还是其他实体的容器。

ClientConnection是与客户端的数据连接的抽象。

  • 当客户端连接服务器时,MediaServer创建ClientConnection的实例,保存在成员fClientConnections[]中。

MediaSession是媒体的抽象。

  • 当用户请求建立媒体连接时,创建MediaSession实例,保存在MediaServer的成员fServerMediaSessions[]中。这是一个Hash表,key值是媒体的名字。

媒体中可以有多个通道,MediaSubsession是媒体通道的抽象。

  • 创建MediaSession时,同时为其中的通道创建MediaSubsession实例,保存在成员fSubsessionHead指向的链表中。

ClientSession是与客户端的对话的抽象,承载在ClientConnection上。

  • 当客户端请求开始播放时,创建ClientSession实例,保存在成员fClientSessions[]中。这是一个Hash表,key值是全局唯一的session id。

StreamState是ClientSession用于挂接到MediaSubsession的中介。

  • ClientSession要挂接到MediaSession上,获得媒体源。它的成员fStreamStates[]引用MediaSession中的MediaSubsession实例。
  • fStreamStates[]是一个数组,这里的trackNum指它的索引。

2.2 main函数

main函数创建任务调度器,创建RTSPServer实例,将它的socket置于调度器的监听下,最后运行调度器,处理socket事件。

  • 调用BasicTaskScheduler::createNew()创建任务调度器,这是一个BasicTaskScheduler实例。
  • 引用调度器实例,创建BasicUsageEnvironment实例。
  • 调用DynamicRTSPServer::createNew(),创建RTSPServer实例。
    • 调用GenericMediaServer::setUpOurSocket()创建TCP socket。createNew()的参数指定本地端口号,这里先指定端口号554。但绑定可能失败,如果失败了,main()会再次调用createNew(),用端口号8554再重试一次。
    • 创建的socket保存在GenericMediaServer的成员fServerSocket中。
    • 调用TaskSheduler::turnOnBackgroundReadHandling()将socket置于监听状态,回调函数为GenericMediaServer::incomingConnectionHandler()。
GenericMediaServer
::GenericMediaServer(UsageEnvironment& env, int ourSocketIPv4, int ourSocketIPv6, Port ourPort,unsigned reclamationSeconds): Medium(env),fServerSocketIPv4(ourSocketIPv4), fServerSocketIPv6(ourSocketIPv6),fServerPort(ourPort), fReclamationSeconds(reclamationSeconds),fServerMediaSessions(HashTable::create(STRING_HASH_KEYS)),fClientConnections(HashTable::create(ONE_WORD_HASH_KEYS)),fClientSessions(HashTable::create(STRING_HASH_KEYS)),fPreviousClientSessionId(0),fTLSCertificateFileName(NULL), fTLSPrivateKeyFileName(NULL) {ignoreSigPipeOnSocket(fServerSocketIPv4); // so that clients on the same host that are killed don't also kill usignoreSigPipeOnSocket(fServerSocketIPv6); // ditto// Arrange to handle connections from others:env.taskScheduler().turnOnBackgroundReadHandling(fServerSocketIPv4, incomingConnectionHandlerIPv4, this);env.taskScheduler().turnOnBackgroundReadHandling(fServerSocketIPv6, incomingConnectionHandlerIPv6, this);
}
  void turnOnBackgroundReadHandling(int socketNum, BackgroundHandlerProc* handlerProc, void* clientData) {setBackgroundHandling(socketNum, SOCKET_READABLE, handlerProc, clientData);}
void BasicTaskScheduler::setBackgroundHandling(int socketNum, int conditionSet, BackgroundHandlerProc* handlerProc, void* clientData) {if (socketNum < 0) return;
#if !defined(__WIN32__) && !defined(_WIN32) && defined(FD_SETSIZE)if (socketNum >= (int)(FD_SETSIZE)) return;
#endifFD_CLR((unsigned)socketNum, &fReadSet);FD_CLR((unsigned)socketNum, &fWriteSet);FD_CLR((unsigned)socketNum, &fExceptionSet);if (conditionSet == 0) {fHandlers->clearHandler(socketNum);if (socketNum+1 == fMaxNumSockets) {--fMaxNumSockets;}} else {fHandlers->assignHandler(socketNum, conditionSet, handlerProc, clientData);if (socketNum+1 > fMaxNumSockets) {fMaxNumSockets = socketNum+1;}if (conditionSet&SOCKET_READABLE) FD_SET((unsigned)socketNum, &fReadSet);if (conditionSet&SOCKET_WRITABLE) FD_SET((unsigned)socketNum, &fWriteSet);if (conditionSet&SOCKET_EXCEPTION) FD_SET((unsigned)socketNum, &fExceptionSet);}
}
  • 调用RTSPServer::rtspURLPrefix()得到URL信息,其中调用getourIPAddress()得到本地地址。
  • 调用BasicTaskScheduler0::doEventLoop()调度任务。

GenericMediaServer::setUpOurSocket()创建TCP socket。

  • 调用setupStreamSocket()创建TCP socket。
  • 调用increaseSendBufferTo()重新设置增加缓存大小。
  • 调用listen()开始监听。

BasicTaskScheduler0::doEventLoop()消息循环。

  • 循环调用BasicTaskScheduler::SingleStep()
    • 为所有需要操作的socket执行select。
    • 找到第一个socket handler执行。
    • 找到第一个响应事件执行。
    • 找到第一个延迟任务执行。

2.3 GenericMediaServer::incomingConnectionHandler()

当有新的数据连接请求时,GenericMediaServer::incomingConnectionHandler()被调用。其中调用incomingConnectionHandlerOnSocket(),参数是成员fServerSocket。

  • 调用accept(),返回新数据连接的socket,同时得到客户端的地址。
  • 调用makeSocketNonBlocking()设置socket为非阻塞模式。
  • 调用increaseSendBufferTo()增加发送缓存大小。
  • 用新连接的socket和客户端地址,调用RTSPServer::createNewClientConnection()。

在createNewClientConnection()中

  • 创建RTSPClientConnection实例。
    • ClientConnection的成员fOurSocket保存新连接的socket值,成员fClientAddr保存客户端地址。
    • GenericMediaServer的成员fClientConnections->Add保存ClientConnection的实例。
    • 调用TaskScheduler::setBackgroundHandling(),将新连接的socket置于监听状态,回调函数为ClientConnection::incomingRequestHandler()。
  • 调用RTSPClientConnection::resetRequestBuffer()复位与接收缓存有关的位置指示变量。

2.4 ClientConnection::incomingRequestHandler()

当socket有数据到达时,ClientConnection::incomingRequestHandler()被调用。

  • 调用readSocket()读取数据,这是RTSP请求字符串。接收缓存是成员fRequestBuffer[]。
  • 调用RTSPClientConnection::handleRequestBytes解析RTSP请求并处理。
    • 调用parseRTSPRequestString()解析请求字符串,得到RTSP命令字及参数,包括:cmdName请求命令字(如:OPTIONS),urlSuffix是文件名,cseq是请求序列号。
    • 后面根据cmdName,调用不同的函数处理。如handleCmd_OPTIONS()处理OPTIONS请求等。
    • 对于SETUP请求,调用GenericMediaServer::createNewClientSessionWithId创建ClientSession实例。
    • 对于SETUP之后的请求,parseRTSPRequestString()得到有效的sessionIdStr,调用GenericMediaServer::lookupClientSession()得到对应的ClientSession实例。然后调用RTSPClientSession::handleCmd_withinSession(),再调用相应的处理函数。
	  if (authenticationOK("SETUP", urlTotalSuffix, (char const*)fRequestBuffer)) {clientSession= (RTSPServer::RTSPClientSession*)fOurRTSPServer.createNewClientSessionWithId();} else {areAuthenticated = False;}
      if (requestIncludedSessionId) {clientSession= (RTSPServer::RTSPClientSession*)(fOurRTSPServer.lookupClientSession(sessionIdStr));if (clientSession != NULL) clientSession->noteLiveness();}
else if (strcmp(cmdName, "TEARDOWN") == 0|| strcmp(cmdName, "PLAY") == 0|| strcmp(cmdName, "PAUSE") == 0|| strcmp(cmdName, "GET_PARAMETER") == 0|| strcmp(cmdName, "SET_PARAMETER") == 0) {if (clientSession != NULL) {clientSession->handleCmd_withinSession(this, cmdName, urlPreSuffix, urlSuffix, (char const*)fRequestBuffer);}

2.5 RTSPClientConnection::handleCmd_OPTIONS

填充回复字符串,告诉客户端支持哪些请求

void RTSPServer::RTSPClientConnection::handleCmd_OPTIONS() {snprintf((char*)fResponseBuffer, sizeof fResponseBuffer,"RTSP/1.0 200 OK\r\nCSeq: %s\r\n%sPublic: %s\r\n\r\n",fCurrentCSeq, dateHeader(), fOurRTSPServer.allowedCommandNames());
}

2.6 RTSPClientConnection::handleCmd_DESCRIBE

  • 调用DynamicRTSPServer::lookupServerMediaSession()查找ServerMediaSession实例,因为这时实例还不存在,所以这里会先创建一个。执行回调RTSPClientConnection::handleCmd_DESCRIBE_afterLookup
  • 用这个ServerMediaSeesin实例调用generateSDPDescription()。
    • 调用ourIPAddress()得到本地地址。
    • 构建会话级sdp字符串,本地地址是它的一个组成部分。
    • 遍历ServerMediaSession的成员fSubsessionHead,对其中的subsession,调用OnDemandServerMediaSubsession::sdpLines()得到媒体级sdp字符串。
    • 连接会话级sdp字符串和媒体级sdp字符串。
  • 调用RTSPServer::rtspURL()得到URL信息。这是头部字符串的一部分。

DynamicRTSPServer::lookupServerMediaSession()根据key值,在GenericMediaServer的成员fSerMediaSessions中查找对应的实例。fServerMediaSessesions是一个Hash表。lookupServerMediaSession()实现:

  • fopen打开文件,如test.264
  • 实例不存在调用全局函数createNewSMS()创建新的MediaSession实例。并调用GenericMediaServer::addServerMediaSession()将它加入成员fServerMediaSessions。

在全局函数createSMS()中

  • 从文件名test.264中,解析出扩展名“.264”。根据扩展名做不同处理。 对于扩展名“.264”,NEW_SMS("H.264 Video"),创建ServerMediaSession实例。
  • 调用H264VideoFileServerMediaSubsession::createNew(),它创建ServerMediaSubsession实例,文件名保存在成员fFileName中。后面创建FileSource实例时将用到它。
  • 调用ServerMediaSession::addSubsession把这个ServerMediaSubsession加入ServerMediaSession的子session表。ServerMediaSession的成员fSubsesisionHead和fSubsessionTail指向这个表的首部和尾部。

OnDemandServerMediaSubsession::sdpLines()得到子session的sdp字符串。sdpLines实现:

  • 调用createNewStreamSource()创建FramedSource实例
  • 调用createGroupsock()创建Groupsock实例
  • 基于FramedSource和Groupsock实例,调用createNewRTPSink()创建RTPSink实例。
  • 调用setSDPLinesFromRTPSink()。其中调用RTPSink的接口构建sdp字符串。
  • 清理掉以上创建的FramedSource、Groupsock、和RTPSink实例。这时只有ServerMediaSession和ServerMediaSubsession的实例保留下来。

2.7 RTSPClientSession::handleCmd_SETUP

先调用createNewClientSessionWithId()创建ClientSession实例,再调用handleCmd_SETUP()处理。注意handleCmd_SETUP是RTSPClientSession的成员函数,handleCmd_DESCRIBE是RTSPClientConnection的成员函数。

createNewClientSessionWithId()实现:

  • 调用our_random32()生成sessionid。
  • 用sessionid调用RTSPServer::createNewClientSession()创建RTSPClientSession实例。
    • ClientConnection的构造函数调用TaskScheduler::resheduleDelayedTask()设置延时任务,回调函数是ClientSession::LivenessTimeoutTask()。这个函数的作用是在这个Client session实例长时间不工作时,删除它。
  • 将这个实例加入GenericMediaServer的成员fClientSessions,这是一个ClientSession实例的Hash表。

RTSPClientSession::handleCmd_SETUP()实现:

  • 调用lookupServerMediaSession(),找到对应的ServerMediaSession实例。这个实例在handleCmd_DESCRIBE()中已经创建好了。
  • 调用numSubsessions()得到ServerMediaSession中的subsession数量。
  • 新建streamState数组fStreamStates[],长度为subsession数量。fStreamStates[]引用ServerMediaSubsession对象,这些对象保存在fSubsessionsHead链表中
  • 比对subsession的trackId成员和请求串中的trackId(如"track1"),相同则纪录trackNum。
  • 调用parseTransportHeader()从请求串中解析得到传输头中包括的数据流配置信息。如请求串"RTP/AVP;unicast;client_port=65512-65513",代表单播UDP,RTP连接的客户端端口为65512,RTCP连接的客户端端口为65513。
  • 调用parseRangeHeader()解析得到点播范围。如果失败,则再调用parsePlayNowHeader()看能不能找到。没有设置点播范围两个函数都返回失败,则从头开始播到尾。
  • 用前面找到的trackNum对应的subsession,调用OnDemandServerMediaSubsession::getStreamParameters()创建数据流通道。
    • 与subsesssion对应的streamState,它的成员streamToken,是这个函数的一个参数。OnDemandServerMediaSubsession实例将它当做辅助数据结构,它被定义为类StreamState,与streamState只是首字母不同。
  • 构造回复字符串。

getStreamParameters()实现:

  • 它的参数clientAddress指定了目标地址。这里因为请求中没有包括“destination=xxx”,所以这个地址是0。这时将目标地址指定ClientConnection的客户端地址。
  • 调用createNewStreamSource()创建FramedSource实例。
  • 创建用于RTP连接和RTCP连接的Groupsock实例。
    • 从成员fInitialPortNum指定的起始端口开始,尝试可用的本地端口,作为RTP连接的本地端口。fInitialPortNum在OnDemandServerMediaSubsession的构造函数中,指定缺省值为6970。
    • 调用createGroupsock()创建RTP连接的Groupsock实例。
    • 如果不复用RTCP连接和RTP连接,则将RTCP连接的端口加1,创建RTCP连接的Groupsock实例。
    • 为RTP连接,调用createNewRTPSink()创建RTPSink实例。如:.264格式文件调用H264VideoFileServerMediaSubsession::createNewRTPSink()。
    • 基于上述Groupsock和RTPSink实例,创建StreamState实例。
    • 创建Destinations实例,保存客户端地址和端口,将Destination实例加入成员fDestinationHashTable中。

2.8 RTSPClientSession::handleCmd_PLAY

实现:

  • 调用parseScaleHeader()和parseRangeHeader(),得到播放的起点和终点。
  • 遍历成员fNumStreamStates,streamState的成员subsession,调用OnDemandServerMediaSubsession::seekStream(),调整播放位置。
  • 遍历成员fNumStreamStates,streamState的成员subsession,调用OnDemandServerMediaSubsession::startStream(),开始播放。
  • 构造回复字符串

OnDemandServrMediaSubsession::startStream()实现:

  • 根据clientSessionId在成员fDestinationHashTable中查找对应的Destinations实例,其中保存了目标地址。
  • 调用StreamState::startPlaying()开始播放。
    • 这个StreamState实例是在处理SETUP请求时创建的, streamState结构体的成员streamToken保存了这个实例。
    • 调用OnDemandServerMediaSubsession::createRTCP()创建RTCPInstance实例。RTPSink实例和RTCP连接的Groupsock实例作为参数。并调用setAppHandler()和setSpecificRRHandler()设置回调函数。
    • 调用MediaSink::startPlaying(),开始解析媒体帧并通过RTP连接发送,同时调用RTCPInstance::sendReport()发送RTCP信息。

接收RTCP信息实现:

  • 调用RTCPInstance::setByeHandler设置一个回调函数,当收到bye消息时,该回调函数被调用。回调函数保存在成员fByeHandlerTask。
  • 在RTCPInstance构造函数中,调用RTCPInterface::startNetworkReading(),将RTCPInstance::incomingReportHandler()设置为数据可读时的回调函数。

在RTCPInstance::incomingReportHandler()中

  • 调用RTCPInterface::handleRead()读取数据到本地缓存。
  • 调用processIncomingReport()处理缓存。如果是bye消息,调用fByeHandlerTask()进行处理。

MediaSink::startPlaying()实现:

  • 调用MultiFramedRTPSink::continuePlaying(),其实是调用MultiFramedRTPSink::buildAndSendPacket()。
    • buildAndSendPacket封装RTP头,调用packFrame()打包帧数据
      • packFrame()从source文件读取一帧数据,通过回调afterGettingFrame()返回给sink,其实是调用afterGettingFrame1()。
        • afterGettingFrame1()向包中打帧数据,包完成则调用sendPacketIfNecessary()发送包,反之则调用packFrame()从source获取帧数据打包。

MultiFramedRTPSink中的帧数据和包缓冲区共用一个,只是用一些额外的变量指明缓冲区中属于包的部分以及属于帧数据的部分(包以外的数据叫做overflow data)。

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

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

相关文章

力扣hot5---双指针

题目&#xff1a; 解决方案&#xff1a;双指针 指针 i 指向最左侧&#xff0c;指针 j 指向最右侧。此时在宽度上达到了最大值&#xff0c;那么哪个柱子更矮&#xff0c;哪个柱子向内部移动&#xff0c;知道 i 与 j 相遇。为什么呢&#xff1f; 如果哪个哪个柱子更矮&#xff0c…

代码随想录算法训练营第四十一天|198.打家劫舍,213.打家劫舍II,337.打家劫舍III

系列文章目录 代码随想录算法训练营第一天|数组理论基础&#xff0c;704. 二分查找&#xff0c;27. 移除元素 代码随想录算法训练营第二天|977.有序数组的平方 &#xff0c;209.长度最小的子数组 &#xff0c;59.螺旋矩阵II 代码随想录算法训练营第三天|链表理论基础&#xff…

Node.js基础---模块化

基本概念 模块化 模块化是指解决一个复杂问题时&#xff0c;自上向下逐层把系统划分成若干模块的过程&#xff0c;对于整个系统来说&#xff0c;模块是可组合&#xff0c;分解和更换的单元 遵守固定规则&#xff0c;把大文件拆分成独立并互相依赖的多个小模块 好处&#xff1a…

【计算机毕业设计】208基于SSM的在线教育网站

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

OLLAMA:如何像专业人士一样运行本地语言模型

原文 https://cheatsheet.md/llm-leaderboard/ollama.en简介&#xff1a;揭示 OLLAMA 对本地语言模型的强大功能 您是否曾经发现自己陷入了基于云的语言模型网络中&#xff0c;渴望获得更本地化、更具成本效益的解决方案&#xff1f;好吧&#xff0c;您的搜索到此结束。欢迎来…

逆向案例四、进阶,爬取精灵数据咨询前五十页数据

python代码示例: import csv import execjs import requests f open(精灵数据.csv,w,encodingutf-8,newline) csv_writer csv.DictWriter(f,fieldnames[标题,发布时间,新闻来源,详情页链接,转自,点击量,新闻作者,发布时间小时,]) csv_writer.writeheader() data [] for pa…

【Ansys Fluent Web 】全新用户界面支持访问大规模多GPU CFD仿真

基于Web的技术将释放云计算的强大功能&#xff0c;加速CFD仿真&#xff0c;从而减少对硬件资源的依赖。 主要亮点 ✔ 使用Ansys Fluent Web用户界面™&#xff08;UI&#xff09;&#xff0c;用户可通过任何设备与云端运行的仿真进行远程交互 ✔ 该界面通过利用多GPU和云计算功…

理解python3中的回调函数

百度百科说&#xff1a;回调函数就是一个通过函数指针调用的函数。如果你把函数的指针&#xff08;地址&#xff09;作为参数传递给另一个函数&#xff0c;当这个指针被用来调用其所指向的函数时&#xff0c;我们就说这是回调函数。回调函数不是由该函数的实现方直接调用&#…

Sqli-labs靶场第13关详解[Sqli-labs-less-13]

Sqli-labs-Less-13 #手工注入 post传参了 根据题目看&#xff0c;像一个登录页面&#xff0c;尝试使用布尔型盲注测试能否登录网站 1. Username输入a 测试是否会有报错&#xff0c;burp抓包 报错&#xff1a;syntax to use near a) and password() LIMIT 0,1 at line 1 分…

[python] `json.dumps()` TypeError: Object of type set is not JSON serializable

在Python中&#xff0c;当你尝试将一个集合&#xff08;set&#xff09;类型的对象转换为JSON格式时&#xff0c;可能会遇到“TypeError: Object of type set is not JSON serializable”的错误。这是因为标准的JSON格式不支持Python中的集合类型&#xff0c;JSON格式支持的数据…

【04】C语言括号匹配问题

欢迎来到土土的博客~&#x1f973;&#x1f973;&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#xff1a;大耳朵土土垚的博客 &#x1f4a5; 所属专栏&#xff1a;C语言系列函数实现 题目描述&#xff1a; 给定一个只包括 ‘(’&#xff0c;‘)’&#xf…

加密隧道技术

在现在的互联网上传输数据&#xff0c;首要考虑的就是安全。这关乎到你的隐私&#xff0c;个人信息&#xff0c;财产安全等等重大问题。如果你的程序本身传输的信息没有加密&#xff0c;也可以通过其他辅助方式让你的通信加密。一些工具的就是为了解决这样的场景的&#xff0c;…

之前续写抖音开发者接入字节小游戏的缓存一下,现在说一下在 Windows 或者 Mac 如何用终端更换路径?

window: 比方说你的 window 目录下是这个路径: 第一:E:\project\Q1\trunk\client\src,然后你想切换到下一个路径的话,你可以这样子操作: 第二:E:\project\Q1\trunk\client\src> cd .\usersetting 然后回车,这里不会计较大小写 第三:你就可以在这个目录下执行你的脚本:E:…

学习大数据,所必需的java基础(7)

文章目录 File类File 的静态成员File的构造方法File的获取方法相对路径和绝对路径File的创建方法File类中的删除方法File的遍历方法 字节流IO流介绍以及输入输出以及流向的介绍IO流的流向IO流分类IO流分类 OutputStream中的子类FileOutoutStream的介绍以及方法的简单介绍InputS…

服务器中如何检查端口是否开放

有多种方法可以检测服务器端口是否开放。以下是一些常用的方法&#xff1a; 1. Telnet 命令&#xff1a; 使用 Telnet 命令来测试端口的可达性。在命令提示符或终端中执行以下命令&#xff1a; telnet your_server_ip your_port_number 如果连接成功&#xff0c;表示端口是…

C++ //练习 10.22 重写统计长度小于等于6 的单词数量的程序,使用函数代替lambda。

C Primer&#xff08;第5版&#xff09; 练习 10.22 练习 10.22 重写统计长度小于等于6 的单词数量的程序&#xff0c;使用函数代替lambda。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块 /********************************…

PDF标准详解(二)——PDF 对象

上一篇文章我们介绍了一个PDF文档应该包含的最基本的结构&#xff0c;并且手写了一个最简单的 “Hello World” 的PDF文档。后面我们介绍新的PDF标准给出示例时将以这个文档为基础&#xff0c;而不再给出完整的文档示例&#xff0c;小伙伴想自己测试可以根据上一节的文档来进行…

分布式ID选型对比(3)

redis自增 一, 引入依赖: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.6.5</version> </dependency> 二, 配置信息: spring:redis:# 地…

YOLOv8有效涨点,添加GAM注意力机制,使用Wise-IoU有效提升目标检测效果

目录 摘要 基本原理 通道注意力机制 空间注意力机制 GAM代码实现 Wise-IoU WIoU代码实现 yaml文件编写 完整代码分享&#xff08;含多种注意力机制&#xff09; 摘要 人们已经研究了各种注意力机制来提高各种计算机视觉任务的性能。然而&#xff0c;现有方法忽视了…

【C/C++随笔】static 的用法和作用

「前言」所有文章已经分类好&#xff0c;放心食用 「归属专栏」C语言 | C嘎嘎 「主页链接」个人主页 「笔者」枫叶先生(fy) static 的用法和作用&#xff1f;&#xff1f;&#xff1f; static作用&#xff1a; 作用1修改存储方式&#xff1a;用 static 修饰的变量存储在静态区…