添加依赖

 <properties><lombok.version>1.18.16</lombok.version><javacv.version>1.5.8</javacv.version><ffmpeg.version>5.1.2-${javacv.version}</ffmpeg.version></properties><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>4.3.2</version></dependency><dependency><groupId>org.bytedeco</groupId><artifactId>javacv</artifactId><version>${javacv.version}</version></dependency><dependency><groupId>org.bytedeco</groupId><artifactId>javacpp-platform</artifactId><version>${javacv.version}</version></dependency><dependency><groupId>org.bytedeco</groupId><artifactId>ffmpeg</artifactId><version>${ffmpeg.version}</version></dependency><dependency><groupId>org.bytedeco</groupId><artifactId>ffmpeg-platform</artifactId><version>${ffmpeg.version}</version></dependency></dependencies>

创建实体

import lombok.Data;import java.text.SimpleDateFormat;@Data
public class Camera {/*** rtsp地址*/private String rtsp;/*** rtmp地址*/private String rtmp;/*** hls地址*/private String hls;/*** 最新一次打开时间*/private String openTime;/*** 同流观众数*/private int count;/*** 监控token*/private String token;/*** 熔断时间(分钟)*/private String fusingTime;/*** 抓帧器配置*/private GrabberOption grabberOption;/*** 记录器配置*/private RecorderOption recorderOption;@Datapublic static class GrabberOption {/*** rtsp转rtmp通信协议*/private String rtsp_transport;/*** 阻塞等待延迟*/private String stimeout;}@Datapublic static class RecorderOption {/*** 延迟*/private String tune;/**** 权衡quality(视频质量)和encode speed(编码速度) values(值): ** ultrafast(终极快),superfast(超级快), veryfast(非常快), faster(很快), fast(快), ** medium(中等), slow(慢), slower(很慢), veryslow(非常慢) ** ultrafast(终极快)提供最少的压缩(低编码器CPU)和最大的视频流大小;而veryslow(非常慢)提供最佳的压缩(高编码器CPU)的同时降低视频流的大小*/private String preset;/*** 画面质量参数,0~51;18~28是一个合理范围*/private String crf;}/*** 同流增加观众*/public void increase() {this.count = this.count + 1;this.openTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis());}/*** 同流减少观众*/public void reduce() {this.count = this.count - 1;}}

配置类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;/*** @author charels.teng* @date 2022/12/6* @since 1.0**/
@Data
@Primary
@Component
@ConfigurationProperties(prefix = "camera")
public class CameraProperties {/*** 熔断时间(单位:分钟)*/private String fusingTime;/*** rtmp域名*/private String rtmpDomain;/*** hls域名*/private String hlsDomain;/*** 抓帧配置*/private Camera.GrabberOption grabber;/*** 取帧配置*/private Camera.RecorderOption recorder;}

控制层

注:rpc框架以及返回类根据自身业务框架更改
import org.springframework.beans.factory.annotation.Autowired;import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;@Path("/camera")
public class CameraResource {@Autowiredprivate CameraService cameraService;/*** 开启视频流*/@GET@Path("/open/{param}")@Produces(MediaType.APPLICATION_JSON)public Result openVideo(@PathParam("param") String param) {return cameraService.open(param);}/*** 关闭视频流*/@GET@Path("/close/{token}")@Produces(MediaType.APPLICATION_JSON)public Result closeVideo(@PathParam("token") String token) {return cameraService.close(token);}

业务层

import cn.hutool.core.collection.CollUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.List;import static cn.facilityone.xia.stream.vedio.camera.thread.CameraThread.THREAD_MAP;/*** @author charels.teng* @date 2022/12/6* @since 1.0**/
@Service
public class CameraServiceImpl implements CameraService {private static final Logger log = LoggerFactory.getLogger(CameraServiceImpl.class);@Autowiredprivate CameraProperties cameraProperties;@Overridepublic Result open(String token) {Camera camera = new Camera();// 可可通过paramString url = iotMointros.get(0).getUrl();if (StringUtils.isEmpty(url)) {throw new BusinessException("无监控源!!!");}camera.setRtsp(url);camera.setToken(iotSn);return openCamera(camera);}private Result openCamera(Camera camera) {String openTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis());String token = camera.getToken();if (THREAD_MAP.containsKey(token)) {if (StringUtils.isNotBlank(THREAD_MAP.get(token).getCamera().getHls())) {THREAD_MAP.get(token).getCamera().increase();return new Result(new CameraDto(THREAD_MAP.get(token).getCamera()));} else {THREAD_MAP.remove(token);}}String rtsp = camera.getRtsp();String rtmp = cameraProperties.getRtmpDomain() + "/" + token;String hls = cameraProperties.getHlsDomain() + "/" + token + ".m3u8";Socket rtspSocket = new Socket();Socket rtmpSocket = new Socket();try {URI rtspUri = URI.create(rtsp);rtspSocket.connect(new InetSocketAddress(rtspUri.getHost(), rtspUri.getPort()), 1000 * 3);} catch (IOException e) {e.printStackTrace();log.error("与拉流IP{}:建立TCP连接失败!", rtsp);return new Result(Result.CODE_500, "与拉流IP建立TCP连接失败!");}try {URI rtmpUri = URI.create(rtmp);rtmpSocket.connect(new InetSocketAddress(rtmpUri.getHost(), rtmpUri.getPort()), 1000 * 3);} catch (IOException e) {e.printStackTrace();log.error("与推流IP{}:建立TCP连接失败!", rtmp);return new Result(Result.CODE_500, "与推流IP建立TCP连接失败!");}camera.setRtsp(rtsp);camera.setRtmp(rtmp);camera.setOpenTime(openTime);camera.setToken(token);camera.setCount(1);camera.setHls(hls);camera.setFusingTime(cameraProperties.getFusingTime());option(camera);CameraDto cameraDto = new CameraDto();cameraDto.setUrl(hls);cameraDto.setToken(token);CameraThread cameraThread = new CameraThread(camera);CameraThread.THREAD_POOL.execute(cameraThread);THREAD_MAP.put(token, cameraThread);return new Result(cameraDto);}private void option(Camera camera) {Camera.GrabberOption grabberOption = new Camera.GrabberOption();grabberOption.setRtsp_transport(cameraProperties.getGrabber().getRtsp_transport());grabberOption.setStimeout(cameraProperties.getGrabber().getStimeout());camera.setGrabberOption(grabberOption);Camera.RecorderOption recorderOption = new Camera.RecorderOption();recorderOption.setTune(cameraProperties.getRecorder().getTune());recorderOption.setPreset(cameraProperties.getRecorder().getPreset());recorderOption.setCrf(cameraProperties.getRecorder().getCrf());camera.setRecorderOption(recorderOption);}@Overridepublic Result close(String token) {if (StringUtils.isNotBlank(token)) {if (THREAD_MAP.containsKey(token)) {CameraThread cameraThread = THREAD_MAP.get(token);Camera camera = cameraThread.getCamera();int count = camera.getCount() - 1;camera.setCount(count);if (count > 0) {log.info("关闭成功 当前相机使用人数为{}  [rtmp:{}]",camera.getCount(), camera.getRtmp());return new Result(camera.getCount());} else {if (count == 0) {THREAD_MAP.get(token).interrupt();log.info("关闭推流成功 当前相机使用人数为{}  [rtmp:{}]",camera.getCount(), camera.getRtmp());return new Result(camera.getCount());} else {THREAD_MAP.get(token).interrupt();THREAD_MAP.remove(token);log.info("异常关闭推流 当前相机使用人数为{}  [rtmp:{}]",camera.getCount(), camera.getRtmp());return new Result(camera.getCount());}}}}return new Result("请检查该token下的视频是否开启成功").toFail();}}

推流

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import lombok.Data;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.stream.Collectors;import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;@Data
public class Pusher {private static final Logger log = LoggerFactory.getLogger(Pusher.class);private Camera camera;private FFmpegFrameGrabber grabber;private FFmpegFrameRecorder recorder;private int errorIndex = 0;private double frameRate;public Pusher(Camera camera) {this.camera = camera;}public void push() {try {//            avutil.av_log_set_level(avutil.AV_LOG_INFO);
//            FFmpegLogCallback.set();grabber = new FFmpegFrameGrabber(camera.getRtsp());grabber.setOptions(BeanUtil.beanToMap(camera.getGrabberOption()).entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, grabber -> String.valueOf(grabber.getValue()))));grabber.start();// 部分监控设备流信息里携带的帧率为9000,如出现此问题,会导致dts、pts时间戳计算失败,播放器无法播放,故出现错误的帧率时,默认为25帧if (grabber.getFrameRate() > 0 && grabber.getFrameRate() < 100) {frameRate = grabber.getFrameRate();} else {frameRate = 25.0;}int width = grabber.getImageWidth();int height = grabber.getImageHeight();// 若视频像素值为0,说明拉流异常,程序结束if (width == 0 || height == 0) {log.error(camera.getRtsp() + " 拉流异常!");grabber.stop();grabber.close();}recorder = new FFmpegFrameRecorder(camera.getRtmp(), grabber.getImageWidth(), grabber.getImageHeight());recorder.setInterleaved(true);// 关键帧间隔,一般与帧率相同或者是视频帧率的两倍recorder.setGopSize((int) (frameRate * 2));// 视频帧率(保证视频质量的情况下最低25,低于25会出现闪屏)recorder.setFrameRate(frameRate);// 设置比特率recorder.setVideoBitrate(grabber.getVideoBitrate());// 封装flv格式recorder.setFormat("flv");// h264编/解码器recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);recorder.setVideoCodec(grabber.getVideoCodec());recorder.setAudioCodec(grabber.getAudioCodec());recorder.setVideoBitrate(grabber.getVideoBitrate());recorder.setAudioBitrate(grabber.getAudioBitrate());
//            recorder.setPixelFormat(grabber.getPixelFormat());recorder.setAudioChannels(grabber.getAudioChannels());recorder.setOptions(BeanUtil.beanToMap(camera.getRecorderOption()).entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, recorder -> String.valueOf(recorder.getValue()))));AVFormatContext fc = grabber.getFormatContext();recorder.start(fc);log.debug("开始推流 [rtsp:{} rtmp:{}]", camera.getRtsp(), camera.getRtmp());// 清空探测时留下的缓存grabber.flush();AVPacket pkt = null;/*pts和dts可根据实际情况选择是否添加,部分视频容易导致无限空包导致无法观看*/long dts = 0; // 选择性注释long pts = 0; // 选择性注释int timebase = 0; // 选择性注释int no_frame_index;for (no_frame_index = 0; no_frame_index < 10 && errorIndex < 10;) {long startTime = System.currentTimeMillis();// 断流if (THREAD_MAP.containsKey(camera.getToken()) && THREAD_MAP.get(camera.getToken()).getExitCode() == 1) {break;}// 熔断DateTime earlyDate = DateUtil.parse(this.camera.getOpenTime(), "yyyy-MM-dd HH:mm:ss");DateTime latelyDate = DateUtil.parse(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(startTime), "yyyy-MM-dd HH:mm:ss");if (DateUtil.between(earlyDate, latelyDate, DateUnit.MINUTE) > Integer.parseInt(camera.getFusingTime())) {THREAD_MAP.get(this.camera.getToken()).getCamera().setCount(0);break;}pkt = grabber.grabPacket();// 空包记录次数跳过if (pkt == null || pkt.size() == 0 || pkt.data() == null) {log.warn("JavaCV 出现空包 [rtsp:{} rtmp:{}]", camera.getRtsp(), camera.getRtmp());no_frame_index++;continue;}// 过滤音频if (pkt.stream_index() == 1) {av_packet_unref(pkt);}// 矫正sdk回调数据的dts,pts每次不从0开始累加所导致的播放器无法续播问题/*pts和dts可根据实际情况选择是否添加,部分视频容易导致无限空包导致无法观看*/pkt.pts(pts); // 选择性注释pkt.dts(dts); // 选择性注释errorIndex += recorder.recordPacket(pkt) ? 0 : 1;// pts,dts累加timebase = grabber.getFormatContext().streams(pkt.stream_index()).time_base().den(); // 选择性注释pts += timebase / frameRate; // 选择性注释dts += timebase / frameRate; // 选择性注释// 将缓存空间的引用计数-1,并将Packet中的其他字段设为初始值。如果引用计数为0,自动的释放缓存空间。av_packet_unref(pkt);long endTime = System.currentTimeMillis(); // 选择性注释if ((long) (1000 /frameRate) - (endTime - startTime) > 0) { // 选择性注释Thread.sleep((long) (1000 / frameRate) - (endTime - startTime)); // 选择性注释} // 选择性注释}// 减员if (no_frame_index == 10) {this.camera.reduce();}} catch (FrameGrabber.Exception | FFmpegFrameRecorder.Exception e) {e.printStackTrace();log.error(e.getMessage());} catch (InterruptedException e) {e.printStackTrace();} finally {release();log.info("推流结束 [rtsp:{} rtmp:{}]", camera.getRtsp(), camera.getRtmp());}}public void release() {try {grabber.stop();grabber.close();if (recorder != null) {recorder.stop();recorder.close();}if (this.camera.getCount() != 0) {this.camera.setCount(0);}} catch (FrameGrabber.Exception | FrameRecorder.Exception e) {e.printStackTrace();}}
}

线程池

import cn.facilityone.xia.stream.vedio.camera.base.Camera;
import cn.facilityone.xia.stream.vedio.camera.pusher.Pusher;import java.util.Map;
import java.util.concurrent.*;public class CameraThread implements Runnable{/*** 线程维护表,可根据业务需求选择内存/数据库/redis等维护方式*/public static Map<String, CameraThread> THREAD_MAP = new ConcurrentHashMap<>();private static final int CORE = Runtime.getRuntime().availableProcessors();public static final ExecutorService THREAD_POOL = new ThreadPoolExecutor(CORE,CORE * 4,3,TimeUnit.SECONDS,new LinkedBlockingQueue<>(CORE),Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardOldestPolicy());private Camera camera;/*** 线程退出码*/private int exitCode = 0;public int getExitCode() {return exitCode;}public CameraThread(Camera camera) {this.camera = camera;}public Camera getCamera() {return this.camera;}// 中断线程public void interrupt() {this.exitCode = 1;}@Overridepublic void run() {String token = camera.getToken();try {Pusher rtmpPusher = new Pusher(camera);rtmpPusher.push();if (THREAD_MAP.get(token).getCamera().getCount() == 0) {THREAD_MAP.remove(token);}} catch (Exception e) {e.printStackTrace();THREAD_MAP.remove(token);}}
}

启动类

@Slf4j
@SpringBootApplication
@EnableScheduling
public class Application {public static void main(String[] args) {// 服务启动执行FFmpegFrameGrabber和FFmpegFrameRecorder的tryLoad(),以免导致第一次推流时耗时。try {FFmpegFrameGrabber.tryLoad();FFmpegFrameRecorder.tryLoad();} catch (Exception e) {e.printStackTrace();}SpringApplication.run(FodCameraApplication.class, args);}@PreDestroypublic void destory() {log.info("服务结束,销毁...");THREAD_MAP.clear();THREAD_POOL.shutdown();}}

Nginx

1、下载nginx-rtmp-module

进入下载地址:https://github.com/arut/nginx-rtmp-module,并下载压缩包,如下图所示:

2、安装nginx

  1. 下载地址:http://nginx.org/en/download.html,选择需要的版本下载即可,如下图:

  2. 安装nginx前首先要确认系统中安装了gcc、pcre-devel、zlib-devel、openssl-devel,运行命令:yum -y install gcc pcre pcre-devel zlib zlib-devel openssl openssl-devel,结果如下图所示:

yum -y install gcc pcre pcre-devel zlib zlib-devel openssl openssl-devel

  1. 解压nginx
tar -xvf nginx-1.23.2.tar.gz
  1. 编译安装nginx,并指定上面下载的rtmp模块路径,命令为:

(1) 安装依赖包,依次执行以下两条命令:

yum -y install libxml2 libxml2-dev
yum -y install libxslt-devel

如果出现:Cannot prepare internal mirrorlist: No URLs in mirrorlist

则依次执行以下两条命令:

sudo sed -i -e "s|mirrorlist=|#mirrorlist=|g" /etc/yum.repos.d/CentOS-*sudo sed -i -e "s|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g" /etc/yum.repos.d/CentOS-*

(2) 编译安装nginx,并指定上面下载的rtmp模块路径,执行命令:

# nginx-rtmp-module-master路径根据自身改变
./configure --add-module=../nginx-rtmp-module-master --with-http_ssl_module --with-http_ssl_module --with-http_xslt_module --with-http_flv_module --with-debug --with-http_gzip_static_module

执行成功如下图所示:

  1. 进入nginx配置文件目录,修改nginx.conf文件,共需要添加两处配置:

(1) 第一步在文件末尾加上下面配置信息:

# http平级
rtmp {server {listen 1935;chunk_size 4096; application live {live on;record off;publish_notify on;#on_publish http://localhost:8080/newsweb/api/v1/rtmp/on_publish;#on_publish_done http://localhost:8080/newsweb/api/v1/rtmp/on_publish_done;#on_play http://localhost:8080/newsweb/api/v1/rtmp/on_play;#on_play_done http://localhost:8080/newsweb/api/v1/rtmp/on_play_done;}application hls {live on;hls on;                  #是否开启hls# 可填绝对路径,也可填相对路劲(/usr/local/nginx/)hls_path /usr/local/nginx/temp/hls;          #本地切片路径hls_fragment 8s;         #本地切片长度publish_notify on;#on_publish http://localhost:8080/newsweb/api/v1/rtmp/on_publish;#on_publish_done http://localhost:8080/newsweb/api/v1/rtmp/on_publish_done;#on_play http://localhost:8080/newsweb/api/v1/rtmp/on_play;#on_play_done http://localhost:8080/newsweb/api/v1/rtmp/on_play_done;}}
}

(2) 在http->server节点下增加推流目录的访问权限配置:

 # 推流详情页面 location /stat {rtmp_stat all;# rtmp_stat_stylesheet stat.xsl;}location /stat.xsl {root html;}#HLS配置开始,这个配置为了`客户端`能够以http协议获取HLS的拉流location /hls {  #server hls fragments  types{  application/vnd.apple.mpegurl m3u8;  video/mp2t ts;  }  # 可填绝对路径,也可填相对路劲(/usr/local/nginx/html/)alias temp/hls;  #该目录对应rtmp 配置中的hls_path即可# autoindex on;  # 开启目录文件列表# autoindex_exact_size on;  # 显示出文件的确切大小,单位是bytes# autoindex_localtime on;  # 显示的文件时间为文件的服务器时间# charset utf-8,gbk;  # 避免中文乱码expires -1;  }
  1. 执行命令:cd … && make && make install,执行成功如下图所示:
  2. 测试配置文件是否有问题:
./usr/local/nginx/nginx -t

  1. 启动:
./usr/local/nginx/nginx
  1. 在浏览器中输入linux的ip地址检查nginx启动页面是否可访问,正常访问成功如下图所示:

若配置过程中遇到问题可评论留言,我会逐一帮助解决❗❗❗

Javacv Javacv+ffmpeg+Nginx 监控/直播 学习教程相关推荐

  1. ffmpeg+nginx 的直播(2,直播摄像头和麦克风)

    假设我的服务器是centos7 192.168.139.117 分别设置 [b] 1.发送端 2.服务端 3.客户端 [/b] [b]1.发送端:测试windows[/b] 声卡 控制面板--> ...

  2. 海思开发板FFmpeg+Nginx,推流RTMP播放(优秀教程收集+实操整理)

    海思开发板FFmpeg+Nginx推流RTSP播放(优秀教程收集+实操整理) 安装FFmpeg及移植 FFmpeg编译问题收录: static declaration of 'cbrt' follow ...

  3. vlc搭建rtsp直播Demo ffmpeg + nginx + flv.js实现rtsp网页播放Demo

    文章目录 学习链接 本地视频文件作为数据源 推流步骤 拉流步骤 本地摄像头作为数据源 拉流步骤 vlc + ffmpeg + nginx + flv.js 实现网页视频直播 概括 vlc打开摄像头,提 ...

  4. 音视频开发(15)---IPC+NVR+路由器+ffmpeg+nginx实现网页/Android/IOS的HLS直播

    海康IPC+NVR+路由器+ffmpeg+nginx实现网页/Android/IOS的HLS直播 研究摄像头直播有一个月了,最终领导决定使用HLS协议进行摄像头实时预览,原因是HLS对移动端和Web端 ...

  5. spring boot 使用javaCV的FFmpeg帧捕捉器捕捉流的音频与视频帧来实现视频流下载保存

    spring boot 使用javaCV的FFmpeg帧捕捉器捕捉流的音频与视频帧来实现视频流下载保存 实现图解: 一.引入maven jar包 <dependency><group ...

  6. 使用ffmpeg+nginx将rtmp直播流转为hls直播流

    ffmpeg的安装和nginx的安装在之前的博客中有详细的讲解. mac搭建nginx+rtmp直播流 1.配置nginx (1)配置rtmp直播​ rtmp {server {listen 1935 ...

  7. 收藏:视频网站(JavaEE+FFmpeg)/Nginx+ffmpeg实现流媒体直播点播系统

    FFmpeg安装(windows环境)http://www.cnblogs.com/xiezhidong/p/6924775.html 最简单的视频网站(JavaEE+FFmpeg)http://bl ...

  8. javacv和FFmpeg实现视频播放器的制作,和视音同步操作;

    最近用javaCV的ffmpeg包的FFmpegFrameGrabber帧捕捉器对捕捉到的音频帧和视频帧做了同步的播放.采用的同步方法是视频向音频同步. 具体的思路如下: (1)首先介绍ffmpeg是 ...

  9. FFmpeg入门详解之103:FFmpeg Nginx VLC打造M3U8直播点播

    FFmpeg+Nginx+VLC打造M3U8点播 Nginx:  web服务器(win10,本地nginx) FFmpeg: m3u8切片(4.3.1) VLC: 点播客户端 切片命令行: ffmpe ...

最新文章

  1. VMware虚拟机搭MAC系统
  2. 如何实现在网页复制后加上网站的来源href
  3. [leetcode]160.相交链表
  4. C++ 异常变量的生命周期
  5. C的|、||、、、异或、~、!运算(转)
  6. libuv 原理_Nodejs的运行原理-libuv篇
  7. 详解Python的max、min和sum函数用法
  8. gdal ImportError: DLL load failed
  9. VB模拟键盘输入的N种方法
  10. android屏幕尺寸像素详解
  11. 对于数据链路层滑动窗口协议中窗口大小的总结
  12. 手把手教你搭建docker环境
  13. 花花公子发大招!一款可以“美容”的安全套,极致××体验从它出发 | 钛空舱
  14. 使用 nosqlBooster for mongoDB 连接 Mongodb
  15. python爬虫由浅入深8---基于正则表达式查询的淘宝比价定向爬虫
  16. 大军师司马懿之军师联盟
  17. BAT 字符串大小写转换
  18. 通俗易懂的csrf漏洞(token为什么能放cookie)
  19. 漂亮的Adapter模式-体会RecyclerView的设计实现
  20. java中的三目运算

热门文章

  1. Web大学生网页作业成品 基于HTML+CSS+JavaScript (刘德华9页 )
  2. python开发的炸金花小游戏来啦,从此不再无聊~
  3. Elasticsearch 脚本安全使用指南
  4. 基于 Arduino 的智能投石机制作
  5. 《心流》| 写作反熵
  6. python安装及模块安装
  7. Python基础-EMS系统
  8. 三种简单的数字倒叙方式
  9. 【数学】丑数II 和 超级丑数
  10. Word排版——毕业论文专业排版2——样式