编译安装 Nginx

这个时候选择编译安装的方式:nginx 的 rtmp 模块,下载地址是 github.com/arut/nginx-…nginx_mod_h264_streaming 的下载地址是:h264.code-shop.com/download/ng…nginx 源码包,下载地址是:nginx.org/download/ng…先准备好 Nginx 源码包、rtmp 模块和 nginx_mod_h264_streaming 模块,并解压:

# 在root目录下准备nginx-1.19.6.tar.gz、nginx-rtmp-module.zip、nginx_mod_h264_streaming-2.2.7.tar.gz并解压
nginx-1.19.6
nginx-1.19.6.tar.gz
nginx-rtmp-module
nginx-rtmp-module.zip
nginx_mod_h264_streaming-2.2.7
nginx_mod_h264_streaming-2.2.7.tar.gz
cd nginx-1.19.6
# 检查依赖库与环境、添加rtmp和h264_streaming模块
./configure --prefix=/usr/local/nginx --with-http_ssl_module --add-module=/root/nginx-rtmp-module --add-module=/root/nginx_mod_h264_streaming-2.2.7# 根据提示还需要安装PCRE、OpenSSL、Zlib的库:
apt install libpcre3 libpcre3-dev zlib1g-dev openssl libssl-dev# 再次检查依赖库与环境、添加rtmp和h264_streaming模块
./configure --prefix=/usr/local/nginx --with-http_ssl_module --add-module=/root/nginx-rtmp-module --add-module=/root/nginx_mod_h264_streaming-2.2.7# 编译 + 安装
make
make install
# 建立软连接
ln -s /usr/local/nginx/sbin/nginx /usr/local/bin/nginx
# 开启Nginx服务器
nginx

编译过程可能会遇到的问题:

问题 1:ngx_http_streaming_module.c:158:8: error: ngx_http_request_t has no member named zero_in_uri> 解决方案:注释掉 nginx_mod_h264_streaming-2.2.7/src/ngx_http_streaming_module.c 的 158 到 161 行即可

问题 2:error: variable ‘stream_priority’ set but not used [-Werror=unused-but-set-variable]> 解决方案:修改 nginx-1.10.2/objs/Makefile 文件第 2 行 CFLAGS 变量去掉 “-Werror” 字段

现在找一个 mp4 与 flv 文件分别放在/root/videos/mp4//root/videos/flv/下,则配置文件修改如下:

...
# 如果访问出现403,需要把user配置为root;
userroot;
...server {listen 80;server_namelocalhost;location / {root html;indexindex.html index.htm;} location~ \.mp4$ {  root /root/videos/mp4/;}location ~ \.flv$ {root /root/videos/flv/;}
}
...

然后输入 xx.xx.xx.xx/test.mp4 即可开始播放,就说明已经配置好了,现在你已经拥有了一个基本的视频点播站点,就像下面这样:对于 flv 的视频,可以使用 VLC 来播放,这是下载地址 get.videolan.org/vlc/3.0.11.…

apt 安装 ffmpeg

apt install ffmpeg

如果没有更换源的话会很慢,下面可以更新一下镜像源,再执行apt install ffmpeg1、首先备份原来的源:

cp /etc/apt/sources.list /etc/apt/sources.list.bak

2、查看本 Ubuntu 的代号

lsb_release -a

<img src=“https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2ccf2ad0fd964c989cbab5887f426c2e~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)对于我的 Ubuntu20.04 来说,代号就是 focal3、确认查看阿里云是否存在该源mirrors.aliyun.com/ubuntu/dist… 看来是存在 focal 的。4、将下面的 XXX 全部替换为系统的代号,比如我的系统代号是 foca” style=“margin: auto” />

deb http://mirrors.aliyun.com/ubuntu/ XXX main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ XXX main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ XXX-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ XXX-security main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ XXX-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ XXX-updates main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ XXX-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ XXX-proposed main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ XXX-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ XXX-backports main restricted universe multiverse

那么替换完成后就是

deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse

5、更新缓存

apt update

ffmpeg 安装成功后即可查看 ffmpeg 的版本:

ffmpeg

使用 ffmpeg 切割媒体文件

进入到 test.mp4 存在的目录,执行如下命令完成对 test.mp4 的切割:

cd ~/videos/mp4
ffmpeg -i test.mp4 -c:v libx264 -c:a copy -f hls -threads 8 -hls_time 30 -hls_list_size 0 test.m3u8

hls_time: 设置每片的长度,单位是秒,默认值为 2 秒。hls_list_size: 设置播放列表保存的最多条目,设置为 0 会保存有所片信息,默认值为 5。hls_wrap: 设置多少片之后开始覆盖,如果设置为 0 则不会覆盖,默认值为 0。这个选项能够避免在磁盘上存储过多的片,而且能够限制写入磁盘的最多的片的数量。start_number: 设置播放列表中 sequence number 的值为 number,默认值为 0hls_base_url: 参数用于为 M3U8 列表的文件路径设置前置基本路径参数,因为在 FFmpeg 中生成 M3U8 时写入的 TS 切片路径默认为 M3U8 生成的路径相同,但是实际上 TS 所存储的路径既可以为本地绝对路径,也可以为相对路径,还可以为网络路径,因此使用 hls_base_url 参数可以达到该效果切割完成后,可以看到文件夹下的 ts 片断和对应的 m3u8 文件:

这些 ts 片断都是可以直接播放的,而且可以看到对应的 m3u8 文件如下,关于 m3u8 文件属性的内容在《流媒体协议之 HLS》中已经介绍过了。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:36
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:35.704711,
test0.ts
#EXTINF:24.984956,
test1.ts
#EXTINF:31.450178,
test2.ts
#EXTINF:29.614889,
test3.ts
#EXTINF:29.531467,
test4.ts
#EXTINF:28.780667,
test5.ts
#EXTINF:31.992422,
test6.ts
#EXTINF:31.241622,
test7.ts
#EXTINF:28.196711,
test8.ts
#EXTINF:29.823444,
test9.ts
#EXTINF:32.451244,
test10.ts
#EXTINF:30.824511,
test11.ts
#EXTINF:26.820244,
test12.ts
#EXTINF:13.931511,
test13.ts
#EXT-X-ENDLIST

修改一下 Nginx 的配置文件

server {listen 80;server_namelocalhost;location / {root html;indexindex.html index.htm;} location~ \.mp4$ {  root /root/videos/mp4/;}location ~ \.flv$ {root /root/videos/flv/;}location /media {alias /root/videos/mp4/;add_header Cache-Control no-cache;}
}

这样通过 VLC 打开网络串流,输入 m3u8 的地址:http://172.16.26.2/media/test.m3u8 即可播放对应的媒体资源:

如果加上 hls_base_url 参数生成的 m3u8 文件如下:

ffmpeg -i test.mp4 -c:v libx264 -c:a copy -f hls -threads 8 -hls_time 30 -hls_list_size 0 -hls_base_url http://172.16.26.2/media/ test.m3u8cat test.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:36
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:35.704711,
http://172.16.26.2/media/test0.ts
#EXTINF:24.984956,
http://172.16.26.2/media/test1.ts
#EXTINF:31.450178,
http://172.16.26.2/media/test2.ts
#EXTINF:29.614889,
http://172.16.26.2/media/test3.ts
#EXTINF:29.531467,
http://172.16.26.2/media/test4.ts
#EXTINF:28.780667,
http://172.16.26.2/media/test5.ts
#EXTINF:31.992422,
http://172.16.26.2/media/test6.ts
#EXTINF:31.241622,
http://172.16.26.2/media/test7.ts
#EXTINF:28.196711,
http://172.16.26.2/media/test8.ts
#EXTINF:29.823444,
http://172.16.26.2/media/test9.ts
#EXTINF:32.451244,
http://172.16.26.2/media/test10.ts
#EXTINF:30.824511,
http://172.16.26.2/media/test11.ts
#EXTINF:26.820244,
http://172.16.26.2/media/test12.ts
#EXTINF:13.931511,
http://172.16.26.2/media/test13.ts
#EXT-X-ENDLIST

使用 ffmpeg 合并 ts 文件

使用 ffmpeg 也可以通过 m3u8 索引文件把所有的 ts 片段文件合并:

ffmpeg -i ./test.m3u8 -acodec copy -vcodec copy output.mp4

如果是网络上的 m3u8 点播列表,也可以下载并合并到 mp4 中:

ffmpeg -i "http://xxx.com/media/test.m3u8" "save_video.mp4"

ffmpeg 切割并加密媒体文件

将一个 mp4 视频文件切割为多个 ts 片段,并在切割过程中对每一个片段使用 AES-128 加密,最后生成一个 m3u8 的视频索引文件:

加密用的 key,通过 OpenSSL 生成一个 enc.key 文件

openssl rand16 > enc.key

另一个是 iv

openssl rand -hex 16

这里生成的 IV 是ef157287b9fc922ed1cc101a09e742b3

新建一个文件 enc.keyinfo 内容格式如下:

http://172.16.26.2/media/enc.key
enc.key
ef157287b9fc922ed1cc101a09e742b3

因为 enc.key 直接放在了/root/vides/mp4/目录下,所以通过http://172.16.26.2/media/enc.key这个地址完全可以访问到这个 enc.key 文件。

-y \

ffmpeg -y -i test.mp4 -hls_time 30 -hls_key_info_file enc.keyinfo -hls_playlist_type vod -hls_segment_filename "file%d.ts" -hls_base_url http://172.16.26.2/media/ test.m3u8

上述命令中-hls_time 30 即每个片段 30s,-hls_playlist_type vod 表示这是一个点播播放列表,hls_segment_filename "file%d.ts"规定了片断的文件名。生成的 m3u8 文件如下:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:36
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-KEY:METHOD=AES-128,URI="http://172.16.26.2/media/enc.key",IV=0xef157287b9fc922ed1cc101a09e742b3
#EXTINF:35.704711,
http://172.16.26.2/media/file0.ts
#EXTINF:24.984956,
http://172.16.26.2/media/file1.ts
#EXTINF:31.450178,
http://172.16.26.2/media/file2.ts
#EXTINF:29.614889,
http://172.16.26.2/media/file3.ts
#EXTINF:29.531467,
http://172.16.26.2/media/file4.ts
#EXTINF:28.780667,
http://172.16.26.2/media/file5.ts
#EXTINF:31.992422,
http://172.16.26.2/media/file6.ts
#EXTINF:31.241622,
http://172.16.26.2/media/file7.ts
#EXTINF:28.196711,
http://172.16.26.2/media/file8.ts
#EXTINF:29.823444,
http://172.16.26.2/media/file9.ts
#EXTINF:32.451244,
http://172.16.26.2/media/file10.ts
#EXTINF:30.824511,
http://172.16.26.2/media/file11.ts
#EXTINF:26.820244,
http://172.16.26.2/media/file12.ts
#EXTINF:13.931511,
http://172.16.26.2/media/file13.ts
#EXT-X-ENDLIST

这样通过加密生成的每个 ts 片断都需要解密才能播放。HTTP Live Streaming 中内容加密有两种,一种是对 TS 切片文件直接加密;另一种是对 H.264 编码文件中类型为 1 和 5 的 NAL 单元进行加密,其它类型的 NAL 单元不加密。HLS 中媒体分块如果是加密的,其加密密钥通过 M3U8 文件中的#EXT-X-KEY来指定,密钥文件由客户端从服务器请求认证获得。一个播放列表可以有一个以上的#EXT-X-KEY,同一个媒体段也可以有多个不同 KEYFORMAT 属性值的#EXT-X-KEY,在本例中使用的是对每个 TS 片断进行加密。

在上面的示例 m3u8 文件中,#EXT-X-KEY有一个属性 URI,其实这个 URI 就是秘钥的地址,在实际音视频版权保护的案例中,TS 切片文件的加解密是非常重要的一环,因为客户端只有在拿到了 key 文件之后才能对 TS 切片文件进行解密,所以在 URI 上面做文章就很关键,这里只是用了一个简单的 HTTP URL 表示了 key 文件的地址,实际场景中需要配合用户 Token 等一系列校验过程才能使客户端拿到真正的 key,另外如果 key 文件本身也是加密的话还需要对 Key 文件本身进行解密,如果把解密的代码放到 SO 库里(也就是 C/C++ 编写的库),那么要破译 Key 就更难了。所以为了防盗链还是有很多的方法流程的。

代码中解密 TS 文件

在很多播放器内就内置了解密 m3u8 文件的功能,但是必须是在本例中这样直接给出 key 的 URL 才可以。对于这样的直接给出 Key 的地址的情况,只需要根据对应的 Key 做解密操作就行了。上面的每一个 TS 文件未解密都不能播放,因此每个 TS 文件都需要进行解密。下面是我写的关于 TS 文件 AES128 加解密的代码:

先引入 Java 实现 AES 加密模块的依赖

implementation group: 'org.bouncycastle', name: 'bcprov-jdk16', version: '1.46'

AES128Utils.java

public class AES128Utils {static {Security.addProvider(new BouncyCastleProvider());}/** * 序号格式化为32字节长度字符串 * @param index 片断序号 * @return 32字节长度序号 */public static String getIvValue(int index){return String.format("%032x", index);}/** * 加密的TS文件加密为字节数组 * @param srcTsFileBytes 加密的TS文件字节数组 * @param keyBytes key文件的字节数组 * @param iv iv偏移量(m3u8文件中) * @return 解密后的字节数组 * @throws Exception 编解码、IO异常 */public static byte[] decryptTsFile(byte[] srcTsFileBytes, byte[] keyBytes, String iv) throws Exception{Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");byte[] ivByte = iv.getBytes();if (ivByte.length != 16) ivByte = new byte[16];AlgorithmParameterSpec paramSpec = new IvParameterSpec(ivByte);cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);return cipher.doFinal(srcTsFileBytes, 0, srcTsFileBytes.length);}/** * 加密的TS文件加密为字节数组 * @param srcTsFile 加密的TS文件 * @param key key文件的字节数组 * @param iv iv偏移量(m3u8文件中) * @return 解密后的字节数组 * @throws Exception 编解码、IO异常 */public static byte[] decryptTsFile(File srcTsFile, String key, String iv) throws Exception {return decryptTsFile(IOUtils.fileToByteArray(srcTsFile), key.getBytes(), iv);}/** * 加密的TS文件加密为字节数组 * @param srcTsFile 加密的TS文件 * @param keyFile key文件 * @param iv iv偏移量(m3u8文件中) * @return 解密后的字节数组 * @throws Exception 编解码、IO异常 */public static byte[] decryptTsFile(File srcTsFile, File keyFile, String iv) throws Exception {return decryptTsFile(IOUtils.fileToByteArray(srcTsFile), IOUtils.fileToByteArray(keyFile), iv);}
}

IOUtils.java

public class IOUtils {private static final String TAG = "IOUtils";/** * 合并Ts片断文件 * @param tsFiles Ts文件集合 * @param descFile 目标文件 * @throws IOException IOException */public static void mergeTsFiles(Map<String, File> tsFiles, List<String> tsList,File descFile, boolean deleteSrcFile) throws IOException{FileOutputStream fileOutputStream = new FileOutputStream(descFile);for(String name: tsList){File file = tsFiles.get(name);Log.i(TAG, "mergeTsFiles: key = " + name + ", path = "+ file.getAbsolutePath());fileOutputStream.write(IOUtils.fileToByteArray(file));fileOutputStream.flush();if(deleteSrcFile) file.delete();}fileOutputStream.close();}/** * 文件转字节数组 * @param srcFile 源文件 * @return 字节数组 * @throws IOException IO */public static byte[] fileToByteArray(File srcFile) throws IOException {FileInputStream inputStream = new FileInputStream(srcFile);byte[] buffer = new byte[4096];ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();int read;while ((read = inputStream.read(buffer)) != -1){byteArrayOutputStream.write(buffer, 0, read);}inputStream.close();byteArrayOutputStream.close();return byteArrayOutputStream.toByteArray();}/** * 字节数组写入文件 * @param srcByte 组接数组 * @param descFile 目标文件 * @throws IOException IO */public static void byteArrayToFile(byte[] srcByte, File descFile) throws IOException {FileOutputStream os = new FileOutputStream(descFile);os.write(srcByte);os.close();}
}

M3u8Parser.java

public class M3u8Parser {private String baseUrl;private static final String TAG = "M3u8Parser";private final File m3u8File;List<String> tsList = new ArrayList<>();private Activity context;private File cacheDir;private File keyFile;public M3u8Parser(String baseUrl, File m3u8File, Activity context) {this.baseUrl = baseUrl;this.m3u8File = m3u8File;this.context = context;cacheDir = context.getExternalCacheDir();}public void initParser() throws IOException {BufferedReader bufferedReader = new BufferedReader(new FileReader(m3u8File));String line;while((line = bufferedReader.readLine()) != null){if(line.startsWith("#EXT-X-KEY")){String[] split = line.split(",");if(split.length == 3){String keyUrl = split[1].substring(5, split[1].length() - 1);System.out.println(keyUrl);NetWorkUtils.doGet(keyUrl, new NetWorkUtils.ResultListener() {@Overridepublic void success(File keyFile) {Log.i(TAG, "success: " + keyFile.getAbsolutePath());M3u8Parser.this.keyFile = keyFile;}@Overridepublic void failed(IOException e) {Log.e(TAG, "failed: ", e);}});}}else if(!line.startsWith("#")){tsList.add(line);}}}public void startDownload(DownloadListener downloadListener) {CountDownLatch countDownLatch = new CountDownLatch(tsList.size());Map<String, File> downloadTsFiles = new HashMap<>();int index = 0;for(String ts: tsList){String url = baseUrl + ts;Log.i(TAG, "startDownload: ts = " + ts);Log.i(TAG, "startDownload: url = " + url);int finalIndex = index;NetWorkUtils.doGet(url, new NetWorkUtils.ResultListener() {@Overridepublic void success(File downloadFile) {String iv = AES128Utils.getIvValue(finalIndex);try {// 下载后直接解码byte[] bytes = AES128Utils.decryptTsFile(downloadFile, keyFile, iv);IOUtils.byteArrayToFile(bytes, downloadFile);} catch (Exception e) {e.printStackTrace();}downloadTsFiles.put(ts, downloadFile);countDownLatch.countDown();}@Overridepublic void failed(IOException e) {Log.e(TAG, "TS文件下载失败", e);}});index++;}try {countDownLatch.await(1200, TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}try {File descFile = new File(cacheDir, "main.ts");IOUtils.mergeTsFiles(downloadTsFiles, tsList, descFile, false);context.runOnUiThread(()->{Toast.makeText(context, "缓存完成:" + descFile.getAbsolutePath(), Toast.LENGTH_SHORT).show();});downloadListener.finishDownload();} catch (IOException e) {e.printStackTrace();}}public interface DownloadListener {void finishDownload();}
}

HLS 流媒体服务与加解密相关推荐

  1. ffmpeg-简单AES加解密记录

    分享一下视频分段缓存技术之中的一种格式m3u8.据说是苹果开发的,前途无量. 使用起来确实蛮方便,可以自己集成做成播放器.本文暂时只记录简单的加解密和播放. 观摩这项技术时参考的几篇文章分享如下: 使 ...

  2. HLS/m3u8之sample-aes解密及软件开发

    相关原创文章: hls加密流生成之sample-aes加密 hls加密流生成之aes-128加密 1.HLS HLS,Http Live Streaming 是由Apple公司定义的用于实时流传输的协 ...

  3. 阿里云视频加解密VOD开发

    VOD使用开发 阿里云视频点播官方文档 阿里云视频点播(ApsaraVideo VoD)是集音视频采集.编辑.上传.自动化转码处理.媒体资源管理.高效云剪辑处理.分发加速.视频播放于一体的一站式音视频 ...

  4. 使用Shaka-packager进行加解密的简单实例

    文章目录 1.环境 2.获取工具 3.准备视频文件 4.DASH加密 5.DASH解密 6.HLS的加解密 Shaka Packager是用于DASH和HLS打包和加密的工具和媒体打包SDK. 支持W ...

  5. 加解密基础——(对称加密、非对称加密和混合加密)

    本文对之前学习过的加解密相关知识做一简单总结,以备后用. 1. 基本概念 加密算法 通常是复杂的数学公式,这些公式确定如何将明文转化为密文的过程和规则. 密钥 是一串被加入到算法中的随机比特. 待续 ...

  6. C语言实现AES加解密

    C语言实现AES加解密 AES算法 具体代码 AES算法 (AES)RIJNDAEL算法是一个数据块长度盒密钥长度都可变的分组加密算法,其数据块长度和密钥长度都可独立地选定为大于等于128位且小于等于 ...

  7. delphi7aes加密解密与java互转_惊呆了!不改一行Java代码竟然就能轻松解决敏感信息加解密|原创

    前言 出于安全考虑,现需要将数据库的中敏感信息加密存储到数据库中,但是正常业务交互还是需要使用明文数据,所以查询返回我们还需要经过相应的解密才能返回给调用方. ❝ ps:日常开发中,我们要有一定的安全 ...

  8. boot数据加解密 spring_SpringBoot 集成 Jasypt 对数据库加密以及踩坑

    前言 密码安全是非常重要的,因此我们在代码中往往需要对密码进行加密,以此保证密码的安全 加依赖 <!-- jasypt --> <dependency><groupId& ...

  9. 一个java的DES加解密类转换成C#

    原文:一个java的DES加解密类转换成C# 一个java的des加密解密代码如下: //package com.visionsky.util;import java.security.*; //im ...

最新文章

  1. 工业机器人应用行业大盘点
  2. Eclipse中新建jsp文件访问页面时乱码问题
  3. 4.6.3 内表数据处理
  4. OSO.EXE病毒专杀工具
  5. 2010年 我的齐鲁软件大赛作品
  6. [f]class获取元素函数
  7. 【昊鼎王五】Windows的Git客户端安装步骤
  8. 斐波那契数列c语言编程递归,C语言实现Fibonacci数列递归
  9. 无线键鼠接收器配对怎么就那么难?简直就是浪费
  10. 【蓝桥杯单片机学习记录4】小蜜蜂老师的工厂灯光设计程序代码赏析——博采众长
  11. Git篇:使用Git将代码库更新到本地(完整版)
  12. Unity引擎UI模块知识Tree
  13. 计算机硬件交通灯课程设计,交通灯计算机硬件课程设计(附件).doc
  14. Python3多线程_thread模块的应用
  15. 让ChatGPT干正事,如何查找靠谱的真文献写论文
  16. Vim配置文件以及Vim插件
  17. 多功能大厅的椅子应该是什么样子的?
  18. Shopee发布Apple(苹果)品牌限售政策
  19. 【单片机毕业设计】【mcuclub-jj-012】基于单片机的晾衣架的设计
  20. SQL:ERROR: more than one row returned by a subquery used as an expression

热门文章

  1. SOCKS5实现(一)
  2. 大神偷偷收藏的7个自学网站,质量高且免费,请低调使用
  3. 如何创建 CAB 文件和如何从文件、内存和资源中解压缩 CAB 文件
  4. 大数据杂谈篇:认识大数据生态(个人心得分享)
  5. NOIP 2004 合唱队形
  6. 【记录】嵌入式经典通信UART理解
  7. 全排列Permutation
  8. GDB多线程调试(调试命令+调试演示)
  9. 分子动力学系综小结 (转)
  10. eclipse 下载和安装教程(初学者,2022最新版)