Golang流媒体实战之六:lal拉流服务源码阅读

news2025/1/16 20:17:39

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

《Golang流媒体实战》系列的链接

  1. 体验开源项目lal
  2. 回源
  3. 转推和录制
  4. lalserver的启动源码阅读
  5. Golang流媒体实战之五:lal推流服务源码阅读
  6. Golang流媒体实战之六:lal拉流服务源码阅读

本篇概览

  • 本文是《Golang流媒体实战》系列的第六篇,经过前面两篇的源码阅读后,咱们逐渐进入深入学习的状态,本篇继续阅读关键代码:拉流服务
  • 为了高效准确的阅读拉流服务源码,本篇继续使用日志结合源码的阅读方式,具体改动后面会详细说明,总的来说就是了解lal在拉流场景是如何响应每个命令,以及如何将推流端发来的流媒体数据给到拉流端

直接跳过一部分源码

  • 在拉流场景,lal与客户端的握手和chunk传输都是通用的RTMP协议,在本文这部分代码就直接跳过了,因为前文已有详细的说明

开始阅读

  • 拉流服务的入口依旧在server_session.go#RunLoop(),握手成功后由ServerSession.runReadLoop处理拉流客户端发来的消息
func (s *ServerSession) RunLoop() (err error) {
	if err = s.handshake(); err != nil {
		_ = s.dispose(err)
		return err
	}

	err = s.runReadLoop()
	_ = s.dispose(err)

	return err
}
  • 跳过处理chunk的代码chunk_composer.go#RunLoop,直接来到处理message的server_session.go#doMsg方法,如下所示,面对着各种消息类型的处理逻辑,又让人犯愁了:在拉流的时候,真实的消息顺序究竟是怎样的呢?
func (s *ServerSession) doMsg(stream *Stream) error {
	if err := s.writeAcknowledgementIfNeeded(stream); err != nil {
		return err
	}

	//log.Debugf("%d %d %v", stream.header.msgTypeId, stream.msgLen, stream.header)
	switch stream.header.MsgTypeId {
	case base.RtmpTypeIdWinAckSize:
		return s.doWinAckSize(stream)
	case base.RtmpTypeIdSetChunkSize:
		// noop
		// 因为底层的 chunk composer 已经处理过了,这里就不用处理
	case base.RtmpTypeIdCommandMessageAmf0:
		return s.doCommandMessage(stream)
	case base.RtmpTypeIdCommandMessageAmf3:
		return s.doCommandAmf3Message(stream)
	case base.RtmpTypeIdMetadata:
		return s.doDataMessageAmf0(stream)
	case base.RtmpTypeIdAck:
		return s.doAck(stream)
	case base.RtmpTypeIdUserControl:
		s.doUserControl(stream)
	case base.RtmpTypeIdAudio:
		fallthrough
	case base.RtmpTypeIdVideo:
		if s.sessionStat.BaseType() != base.SessionBaseTypePubStr {
			return nazaerrors.Wrap(base.ErrRtmpUnexpectedMsg)
		}
		s.avObserver.OnReadRtmpAvMsg(stream.toAvMsg())
	default:
		Log.Warnf("[%s] read unknown message. typeid=%d, %s", s.UniqueKey(), stream.header.MsgTypeId, stream.toDebugString())

	}
	return nil
}
  • 此刻去看下真实日志应该是个不错的方法,但是,此时lal还在处理推流请求,有大量推流相关的日志也在源源不断的输出
  • 于是,为了只看拉流先关日志,对代码做少量修改,如下图所示,修改后只有拉流才会输出日志
    在这里插入图片描述
  • 第二处改动如下,在处理amf0消息的时候,如果不是推流,就把命令打印出来
    在这里插入图片描述
  • 修改完毕再重新运行lal、推流、拉流,就能获取到修改后的日志了,用关键字pull log过滤后的日志内容如下
INFO pull log, SessionId [RTMPPUBSUB4], sub msg header {Csid:3 MsgLen:196 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0},  - server_session.go:216
INFO pull log, SessionId [RTMPPUBSUB4], cmd [connect] - server_session.go:345
INFO [RTMPPUBSUB4] < R connect('live'). tcUrl=rtmp://127.0.0.1:1935/live - server_session.go:413
INFO [RTMPPUBSUB4] > W Window Acknowledgement Size 5000000. - server_session.go:417
INFO pull log, SessionId [RTMPPUBSUB4], sub msg header {Csid:2 MsgLen:4 MsgTypeId:5 MsgStreamId:0 TimestampAbs:0},  - server_session.go:216
INFO [RTMPPUBSUB4] < R Window Acknowledgement Size: 5000000 - server_session.go:257
INFO pull log, SessionId [RTMPPUBSUB4], sub msg header {Csid:3 MsgLen:25 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0},  - server_session.go:216
INFO pull log, SessionId [RTMPPUBSUB4], cmd [createStream] - server_session.go:345
INFO [RTMPPUBSUB4] < R createStream(). - server_session.go:444
INFO [RTMPPUBSUB4] > W _result(). - server_session.go:445
INFO pull log, SessionId [RTMPPUBSUB4], sub msg header {Csid:8 MsgLen:38 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0},  - server_session.go:216
INFO pull log, SessionId [RTMPPUBSUB4], cmd [getStreamLength] - server_session.go:345
2023/04/08 10:09:06.774588 DEBUG [RTMPPUBSUB4] read command message, ignore it. cmd=getStreamLength, header={Csid:8 MsgLen:38 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0}, b=len(core)=4096, rpos=27, wpos=38, hex=00000000  05 02 00 07 74 65 73 74  31 31 31                 |....test111|
 - server_session.go:366
INFO pull log, SessionId [RTMPPUBSUB4], sub msg header {Csid:8 MsgLen:36 MsgTypeId:20 MsgStreamId:1 TimestampAbs:0},  - server_session.go:216
INFO pull log, SessionId [RTMPPUBSUB4], cmd [play] - server_session.go:345
INFO [RTMPPUBSUB4] < R play('test111'). - server_session.go:507
INFO [RTMPPUBSUB4] > W onStatus('NetStream.Play.Start'). - server_session.go:517
2023/04/08 10:09:06.774929 DEBUG [GROUP2] [RTMPPUBSUB4] add SubSession into group. - group__out_sub.go:20
INFO pull log, SessionId [RTMPPUBSUB4], sub msg header {Csid:2 MsgLen:10 MsgTypeId:4 MsgStreamId:0 TimestampAbs:1},  - server_session.go:216
  • 通过上述日志,可以看出拉流场景,lal收到的命令依次如下
connect
->
server bandwidth
->
createStream
->
getStreamLength
->
play
->
control message
  • 有了这个实际顺序,阅读源码理时就不会迷失方向了,接下来先要搞清楚一个问题:下图是刚才新增的代码,s.sesssionStat.BaseType()代表的是当前会话的类型,那么问题来了,这个会话类型是何时确定的呢?
    在这里插入图片描述

会话类型是何时确定的

  • 当lal的1935端口收到一个远程TCP连接的时候(推流或拉流都会建立TCP连接),会调用server.go#handleTcpConnect,里面会用NewServerSession穿件Session对象,即会话对象,如下图红色箭头
    在这里插入图片描述
  • 在NewServerSession方法中,调用base.NewBasicSessionStat的时候,指定了sessionType等于base.SessionTypeRtmpServerSession(注意,这时候还只知道是个TCP连接,并不清楚具体是推流还是拉流)
    在这里插入图片描述
  • 展开base.NewBasicSessionStat方法,看到了设置会话类型的代码,如下图,此时会话类型是PUBSUB,很中性,很合理,毕竟现在还不知是推流还是拉流
    在这里插入图片描述
  • 建立TCP连接后,就会陆陆续续收到拉流端侧发来的各种命令,其中有一个amf0命令名为play,看名字也知道是播放的命令,处理该命令的方法是server_session.go#doPlay,下图是其部分源码,红色箭头可见此时会话的类型被正式设置成了SUB
    在这里插入图片描述
  • 代码读到这里,我突然想到:举一反三,推流的会话类型是啥时确定的呢?应该是在收到明确的推流命令时吧
  • 打开代码,果然,在处理publish命令的时候,将推流的会话类型设置为PUB
    在这里插入图片描述
  • 终于把会话类型的问题弄明白了,接下来学习每个命令的响应

server bandwidth(5)

  • 消息类型等于5的时候,lal的处理逻辑是doWinAckSize方法,这里只是对成员变量做了设置
func (s *ServerSession) doWinAckSize(stream *Stream) error {
	if stream.msg.Len() < 4 {
		return base.NewErrRtmpShortBuffer(4, int(stream.msg.Len()), "ServerSession::doWinAckSize")
	}

	s.peerWinAckSize = int(bele.BeUint32(stream.msg.buff.Bytes()))
	Log.Infof("[%s] < R Window Acknowledgement Size: %d", s.UniqueKey(), s.peerWinAckSize)
	return nil
}

createStream

  • createStream命令的处理也很简单,没有业务逻辑,只是对客户端的回复
func (s *ServerSession) doCreateStream(tid int, stream *Stream) error {
	Log.Infof("[%s] < R createStream().", s.UniqueKey())
	Log.Infof("[%s] > W _result().", s.UniqueKey())
	if err := s.packer.writeCreateStreamResult(s.conn, tid); err != nil {
		return err
	}
	return nil
}

getStreamLength

  • 接下来的命令是getStreamLength,顾名思义,客户端想知道媒体流的长度
  • 在直播场景下,媒体流没有长度,于是,面对getStreamLength命令,lal不予理会
    在这里插入图片描述

play

  • 拉流场景中,play算是最重要的命令了,前面在分析如何设置会话类型的时候,已经对play有一些了解,接下来要细看这部分
  • play命令的处理逻辑如下,先从命令提取了流名,然后回复两个控制命令StreamIsRecorded和StreamBegin,告诉端侧播放即将开始,紧接着就是状态同步命令NetStream.Play.Start,然后设置超时时间(推流是写超时,拉流失读超时),接着是前面看过一次的代码:设置会话类型为SUB,最后是对观察者的回调
func (s *ServerSession) doPlay(tid int, stream *Stream) (err error) {
	if err = stream.msg.readNull(); err != nil {
		return err
	}
	s.streamNameWithRawQuery, err = stream.msg.readStringWithType()
	if err != nil {
		return err
	}
	ss := strings.Split(s.streamNameWithRawQuery, "?")
	s.streamName = ss[0]
	if len(ss) == 2 {
		s.rawQuery = ss[1]
	}

	s.url = fmt.Sprintf("%s/%s", s.tcUrl, s.streamNameWithRawQuery)

	Log.Infof("[%s] < R play('%s').", s.UniqueKey(), s.streamNameWithRawQuery)
	// TODO chef: start duration reset

	if err := s.packer.writeStreamIsRecorded(s.conn, Msid1); err != nil {
		return err
	}
	if err := s.packer.writeStreamBegin(s.conn, Msid1); err != nil {
		return err
	}

	Log.Infof("[%s] > W onStatus('NetStream.Play.Start').", s.UniqueKey())
	if err := s.packer.writeOnStatusPlay(s.conn, Msid1); err != nil {
		return err
	}

	// 回复完信令后修改 connection 的属性
	s.modConnProps()

	s.sessionStat.SetBaseType(base.SessionBaseTypeSubStr)
	err = s.observer.OnNewRtmpSubSession(s)
	if err != nil {
		s.DisposeByObserverFlag = true
	}
	return err
}
  • 对上述代码,有一处不理解的地方,就是根据会话类型修改连接超时时长的代码(modConnProps方法内部),这段代码执行完毕后才会设置会话类型,所以modConnProps方法中的会话类型应该是不准的,那么超时的设置也就有问题了,也许是我对代码的理解还不够深入吧
    在这里插入图片描述
  • 再来看看刚刚提到的观察者的回调,对应的是server_manager__.go#OnNewRtmpSubSession方法,主要是先鉴权,再把会话加入Group
func (sm *ServerManager) OnNewRtmpSubSession(session *rtmp.ServerSession) error {
	sm.mutex.Lock()
	defer sm.mutex.Unlock()

	info := base.Session2SubStartInfo(session)

	if err := sm.option.Authentication.OnSubStart(info); err != nil {
		return err
	}

	group := sm.getOrCreateGroup(session.AppName(), session.StreamName())
	group.AddRtmpSubSession(session)

	info.HasInSession = group.HasInSession()
	info.HasOutSession = group.HasOutSession()

	sm.option.NotifyHandler.OnSubStart(info)
	return nil
}
  • 至此,play命令的主要操作就算看完了,lal接下来收到的是Control Message(0x04)

Control Message

  • 响应Control Message的方法是doUserControl,只是个ping的响应
func (s *ServerSession) doUserControl(stream *Stream) error {
	// TODO(chef): 检查buff长度有效性 202301
	userControlType := bele.BeUint16(stream.msg.buff.Bytes())
	if userControlType == uint16(base.RtmpUserControlPingRequest) {
		stream.msg.buff.Skip(2)
		timestamp := bele.BeUint32(stream.msg.buff.Bytes())
		return s.packer.writePingResponse(s.conn, timestamp)
	}
	return nil
}
  • 代码读到此,lal处理拉流客户端命令的逻辑算是看完了,可见主要是RTMP协议的实现、会话对象维护、还有就是根据流名加入Group
  • 其实到现在咱们还只是看了lal与拉流客户端正式建立联系的代码,真正的流传输还没看到,这也是接下来的任务:拉流动作的具体实现代码

拉流动作

  • 如果您看过了前文的推流代码,此刻应该是胸有成竹了,关键代码前面已经看过,现在无非是从拉流的视角再去温习一遍而已
  • 拉流对应的具体动作,其实是推流的逻辑触发的,简单的说:lal收到推流端发来的媒体流数据时,就会将数据写入拉流的TCP连接中
  • 咱们来看代码
  • lal收到推流端发来的媒体流消息时,会执行group__core_streaming.go#broadcastByRtmpMsg,下面是其中的一段代码,遍历该流名的group下的所有拉流会话,逐一处理,这部分代码中,针对刚刚加入的会话有特别处理,首先要把媒体流的meta信息给拉流端,其次要将缓存的关键帧推给拉流侧,这样拉流侧就能快速播放了,而无需等到推流端推来的关键帧(一个GOP可能长达数秒,不用缓存的话可能要等数秒才有关键帧,图像才能显示)
	for session := range group.rtmpSubSessionSet {
		if session.IsFresh {
			// TODO chef: 头信息和full gop也可以在SubSession刚加入时发送
			if group.rtmpGopCache.MetadataEnsureWithoutSetDataFrame != nil {
				Log.Debugf("[%s] [%s] write metadata", group.UniqueKey, session.UniqueKey())
				_ = session.Write(group.rtmpGopCache.MetadataEnsureWithoutSetDataFrame)
			}
			if group.rtmpGopCache.VideoSeqHeader != nil {
				Log.Debugf("[%s] [%s] write vsh", group.UniqueKey, session.UniqueKey())
				_ = session.Write(group.rtmpGopCache.VideoSeqHeader)
			}
			if group.rtmpGopCache.AacSeqHeader != nil {
				Log.Debugf("[%s] [%s] write ash", group.UniqueKey, session.UniqueKey())
				_ = session.Write(group.rtmpGopCache.AacSeqHeader)
			}
			gopCount := group.rtmpGopCache.GetGopCount()
			if gopCount > 0 {
				// GOP缓存中肯定包含了关键帧
				session.ShouldWaitVideoKeyFrame = false

				Log.Debugf("[%s] [%s] write gop cache. gop num=%d", group.UniqueKey, session.UniqueKey(), gopCount)
			}
			for i := 0; i < gopCount; i++ {
				for _, item := range group.rtmpGopCache.GetGopDataAt(i) {
					_ = session.Write(item)
				}
			}

			// 有新加入的sub session(本次循环的第一个新加入的sub session),把rtmp buf writer中的缓存数据全部广播发送给老的sub session
			// 从而确保新加入的sub session不会发送这部分脏的数据
			// 注意,此处可能被调用多次,但是只有第一次会实际flush缓存数据
			if group.rtmpMergeWriter != nil {
				group.rtmpMergeWriter.Flush()
			}

			session.IsFresh = false
		}

		if session.ShouldWaitVideoKeyFrame && msg.IsVideoKeyNalu() {
			// 有sub session在等待关键帧,并且当前是关键帧
			// 把rtmp buf writer中的缓存数据全部广播发送给老的sub session
			// 并且修改这个sub session的标志
			// 让rtmp buf writer来发送这个关键帧
			if group.rtmpMergeWriter != nil {
				group.rtmpMergeWriter.Flush()
			}
			session.ShouldWaitVideoKeyFrame = false
		}
	} 
  • 然后才是关键代码,就是这段
	if len(group.rtmpSubSessionSet) > 0 {
		if group.rtmpMergeWriter == nil {
			group.write2RtmpSubSessions(lazyRtmpChunkDivider.GetEnsureWithoutSdf())
		} else {
			group.rtmpMergeWriter.Write(lazyRtmpChunkDivider.GetEnsureWithoutSdf())
		}
	}
  • 真正执行的是write2RtmpSubSessions方法,如下所示,遍历所有拉流的session,把流媒体消息通过TCP连接写入(session.Write方法)
func (group *Group) write2RtmpSubSessions(b []byte) {
	for session := range group.rtmpSubSessionSet {
		if session.IsFresh || session.ShouldWaitVideoKeyFrame {
			continue
		}
		_ = session.Write(b)
	}
}
  • 至此,拉流源码阅读完成,除了对基础知识的掌握,相信您对lal作者的源码风格也逐渐熟悉了吧:简洁明了,关键位置有注释,这样的代码读起来真是一种享受,接下来的学习之旅,一定有有更多精彩等着我们

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

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

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

相关文章

大数据3 -Hadoop HDFS-分布式文件系统

目录 1.为什么需要分布式存储&#xff1f; 2. HDFS的基础架构 3. HDFS存储原理 4. NameNode是如何管理Block块的 5. HDFS数据的读写流程 1.为什么需要分布式存储&#xff1f; •数据量太大&#xff0c;单机存储能力有上限&#xff0c;需要靠数量来解决问题•数量的提升带…

【区块链】走进web3的世界-合约交互中的异常/边界处理

在以太坊智能合约中&#xff0c;异常处理是一个非常重要的问题&#xff0c;因为任何一个函数调用都有可能导致异常。常见的异常包括函数调用失败、无效参数、内部错误等。 在 Solidity 中&#xff0c;可以使用 require、assert 和 revert 等关键字来处理异常。这些关键字可以用…

第一章 序言:Pytorch在自然语言处理中的应用

01 序言&#xff1a;Pytorch在自然语言处理中的应用 目录01 序言&#xff1a;Pytorch在自然语言处理中的应用1. PyTorch简介2. 自然语言处理3. PyTorch在自然语言处理中的应用3.1 文本分类3.2 情感分析3.3 机器翻译4. 结论1. PyTorch简介 首先&#xff0c;我们需要介绍一下PyT…

WINDOWS消息

WINDOWS消息 Unit01消息队列 01消息队列概念 消息队列是用于存放消息的队列消息在队列中先进先出所有窗口程序都有消息队列程序&#xff08;GetMessage&#xff09;可以从队列中获消息 02消息队列分类 系统消息队列&#xff1a;由系统维护的消息队列&#xff08;这个队列非…

Qt的内存管理机制

QObject的parent设置为null 1.如果构造时直接指定了null&#xff0c;当前实例不会有父对象存在&#xff0c;Qt也不能自动析构该实例&#xff0c;除非实例超出作用域导致析构函数被调用&#xff0c;使用deletelater()函数&#xff0c;不建议使用delete 2.如果指定了parent&#…

关于电商商品数据API接口列表,你想知道的(详情页、Sku信息、商品描述、评论问答列表)

目录 一、商品数据API接口列表 二、商品详情数据API调用代码item_get 三、获取sku详细信息item_sku 四、获得淘宝商品评论item_review 五、数据说明文档 进入 一、商品数据API接口列表 二、商品详情数据API调用代码item_get <?php// 请求示例 url 默认请求参数已经URL…

数据结构-插入排序

一.概要 插入排序是一种基于比较的排序算法&#xff0c;其基本思想是将待排序的元素插入到已排序的序列中&#xff0c;形成新的有序序列。 插入排序算法的过程如下&#xff1a; 将待排序序列分为两部分&#xff1a;已排序部分和未排序部分&#xff1b; 初始时&#xff0c;已…

C++string类的详细使用方法

String类的详细使用 文章目录String类的详细使用初始化扩容空间resize与reserve扩容长度获取插入与删除函数运算符插入append插入assign字符串截取push_back尾插erase删除replase替换swap交换pop_back尾删substr截断字符串功能copy拷贝find查找rfind反向查找find_first_of匹配查…

三路快排(基于三指针单趟排序的快速排序)+快排时间复杂度再分析

目录 一.前言 二. 三路快排 &#x1f60d;算法思想: &#x1f60d;算法实现步骤: &#x1f60d;三指针单趟排序的实现:​ &#x1f60d;非递归快排完全体: &#x1f914;与C标准库里的快排进行对比测试: 三.快排时间复杂度再分析 一.前言 http://t.csdn.cn/mz8dghttp://…

SolidWorks2020安装教程

破解文件及步骤 和 安装包 hf&#xff1a;SolidWorks2020 即可 &#xff08;我的推广 共中号&#xff09; Before installation, block the outgoing Internet access by means of Windows Firewall or cord plug. Check .NET Framework 3.5 and 4.0 are installed. If .NET …

Hive安装与操作

目录 环境 数据 实验步骤与结果 &#xff08;1&#xff09;环境启动 &#xff08;2&#xff09;Hive基本操作 环境 Hadoop集群开发环境、mysql、Hive环境 数据 course.txt、sc.txt、student.txt 实验步骤与结果 &#xff08;1&#xff09;环境启动 ①执行命令&#xf…

JVM的内存结构(超详细附加大厂面试题)

内存结构 1、什么是 JVM &#xff1f; 1&#xff09;定义 Java Virtual Machine &#xff0c;Java 程序的运行环境&#xff08;Java 二进制字节码的运行环境&#xff09;。 2&#xff09;好处 一次编译&#xff0c;处处执行 自动的内存管理&#xff0c;垃圾回收机制 数组下…

结构重参数化宇宙(Re-parameterization Universe)

文章目录0. 前言1. Re-parameterization Universe1.1 RepVGG1.2. RepOptimizer2. 应用参考资料0. 前言 一方面&#xff0c;大量研究表明&#xff0c;多分支网络架构的性能普遍优于单分支架构&#xff1b;另一方面&#xff0c;相比多分支架构&#xff0c;单分支架构更有利于部署…

windows系统管理_windows server 2016 用户管理

用户账户的概述 **计算机用户账户&#xff1a;**由将用户定义到某一系统的所有信息组成的记录,账户为用户或计算机提供安 全凭证&#xff0c;包括用户名和用户登陆所需要的密码&#xff0c;以及用户使用以便用户和计算机能够登录到网络并 访问域资源的权利和权限。不同的身份拥…

自动控制原理模拟卷2

自动控制原理模拟题二 Question1 电炉温度控制系统原理如下图所示,分析系统保持电炉温度恒定的工作过程,指出系统的被控对象、被控量及各部件的作用,并画出系统方块图。 解: 电炉使用电阻丝加热,并要求保持炉温恒定,图中采用热电偶来测量电炉温并将其转换为电压信号,将…

Android 新版 Logcat 操作小技巧

新版的Android Studio中启用了新的 Logcat&#xff0c;有些小技巧这里介绍一下&#xff1a; 文章目录1. Logcat启动2. Logcat 搜索1. 搜索当前包名下的日志&#xff1a;2. 添加日志级别3. 添加标签4. 标签字段5. 排除字段6. 使用正则表达式7. 使用正则表达式排除8. 使用age截取…

初识C语言 ——“C Primer Plus”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰的内容是读一本好书&#xff0c;这一本书的名字就叫做《C Primer Plus》&#xff0c;那么&#xff0c;又回到了我们的初识C语言阶段啦&#xff0c;保证零基础都能看懂噢&#xff0c;下面&#xff0c;让我们进入C语言的…

app抓包实战

文章目录一、抓包原理二、常用应用场景三、过滤四、重发五、修改请求六、断点&#xff08;BreakPoint&#xff09;一、抓包原理 二、常用应用场景 解决移动端接口测试 解决接口测试过程中检查传参错误问题 mock测试&#xff08;虚拟的对象代替正常的数据、后端接口没有开发完成…

XXL-JOB分布式任务调度平台搭建以及和SpringBoot整合应用

1 前言 XXL-JOB 是一个轻量级分布式任务调度平台&#xff0c;其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线&#xff0c;开箱即用。 可以前往 Gitee 地址进行下载使用&#xff1a; https://gitee.com/xuxueli0323/xxl-job.g…

计算机网络考试复习——第三章 3.1 3.2

本章考试范围为3.1 3.2 3.3 3.4 首先明确数据链路层在网络中是在物理层的上面从下网上看是在第二层&#xff0c;在数据链路层传输的单位是帧。 网络中的主机、路由器等都必须实现数据链路层&#xff0c;局域网中的主机、交换机等都必须实现数据链路层。 数据链路层的地位&am…