JAVA调用harbor接口实现镜像tar包上传

前言

  使用harbor过程中,一直想使用harbor的api实现镜像的上传功能,但是实际上harbor是直接调用了docker registry的api,harbor层只是做了一个透传的功能,这个可以参考《harbor权威指南》这本书,参考官网接口以及网上大佬的思想,实现了一个Java版本,主要是实现了docker daemon上传的逻辑。

docker镜像tar包结构

实现上传首先需要将镜像的tar包解压,读取目录结构,一个典型的docker镜像包(使用docker save命令)结构如下:

.
├── 1ecf8bc84a7c3d60c0a6bbdd294f12a6b0e17a8269616fc9bdbedd926f74f50c
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── 6f4ec1f3d7ea33646d491a705f94442f5a706e9ac9acbca22fa9b117094eb720.json
├── aaac5bde2c2bcb6cc28b1e6d3f29fe13efce6d6b669300cc2c6bfab96b942af4
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── b63363f0d2ac8b3dca6f903bb5a7301bf497b1e5be8dc4f57a14e4dc649ef9bb
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── c453224a84b8318b0a09a83052314dd876899d3a1a1cf2379e74bba410415059
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── dd8ef1d42fbcccc87927eee94e57519c401b84437b98fcf35505fb6b7267a375
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── manifest.json
└── repositories

清单文件manifet.json结构

[
    {
        "Config":"6f4ec1f3d7ea33646d491a705f94442f5a706e9ac9acbca22fa9b117094eb720.json",
        "RepoTags":[
            "alpine:filebeat-6.8.7-arm64"
        ],
        "Layers":[
            "aaac5bde2c2bcb6cc28b1e6d3f29fe13efce6d6b669300cc2c6bfab96b942af4/layer.tar",
            "dd8ef1d42fbcccc87927eee94e57519c401b84437b98fcf35505fb6b7267a375/layer.tar",
            "c453224a84b8318b0a09a83052314dd876899d3a1a1cf2379e74bba410415059/layer.tar",
            "b63363f0d2ac8b3dca6f903bb5a7301bf497b1e5be8dc4f57a14e4dc649ef9bb/layer.tar",
            "1ecf8bc84a7c3d60c0a6bbdd294f12a6b0e17a8269616fc9bdbedd926f74f50c/layer.tar"
        ]
    }
]

manifest.json 包含了对这个tar包的描述信息,比如image config文件地址,tags说明,镜像layer信息,在解析的时候也是根据这个文件去获取关联的文件

Doker Registry api

上传过程主要调用docker registry api</br>
参考https://docs.docker.com/registry/spec/api/#pushing-an-image

上传流程

  1. 获取鉴权信息
  2. 检查layer.tar是否已经存在
  3. 上传layer.tar
  4. 上传image config
  5. 上传manifest(非包中的manifest.json而是Manifest struct)

Java实现

  1. 项目结构如下


    项目结构.png
  2. 核心实现类DockerImageUploadBiz.java

/**
 * @author Administrator
 */
@Service
@Slf4j
public class DockerImageUploadBiz {
    
    //远程仓库
    @Value("${docker.remote.repo}")
    private String targetRepoAddress;
    
    //本地解压路径
    @Value("${docker.upload.extractPath}")
    private String tarPath;
    
    //harbor用户名
    @Value("${docker.harbor.userName}")
    private String userName;
    
    //密码
    @Value("${docker.harbor.password}")
    private String password;
    
    /**
    * @param sourceTar 上传的镜像文件
    * @param project 项目
    */
    public void push(File sourceTar, String project) {
        if (!sourceTar.exists()) {
            log.warn("Error!file is not exist!path:{}", sourceTar);
            return;
        }
        try {
            String unTarPath = FileUtil.doUnArchiver(sourceTar, tarPath);
            String manifest = FileUtil.readJsonFile(unTarPath + File.separator + "manifest.json");
            JSONArray jsonArray = JSONObject.parseArray(manifest);
            if (Objects.isNull(jsonArray)) {
                log.warn("manifest convert error!path:{},content:{}", unTarPath + File.separator + "manifest.json", manifest);
                return;
            }
            for (Object arr : jsonArray) {
                JSONObject jsonObject = (JSONObject) arr;
                JSONArray repoTags = jsonObject.getJSONArray("RepoTags");
                for (Object repoTag : repoTags) {
                    String repo = repoTag.toString();
                    String substring = repo.substring(repo.lastIndexOf('/') + 1);
                    String[] split = substring.split(":");
                    String imageName = split[0];
                    String tag = split[1];
                    log.info("imageName:{},tag:{}", imageName, tag);
                    JSONArray layers = jsonObject.getJSONArray("Layers");
                    //1.上传layer
                    log.info("========================STEP:1/3===============================");
                    log.info("PUSHING LAYERS STARTING...");
                    List<String> layerPathList = new ArrayList<>(layers.size());
                    int i = 1;
                    for (Object layer : layers) {
                        String layerPath = unTarPath + File.separator + layer.toString();
                        log.info("PUSHING LAYER:{}-{} ...", i, layers.size());
                        layerPathList.add(layerPath);
                        pushLayer(project, layerPath, imageName);
                        i++;
                    }
                    log.info("PUSHING LAYERS ENDED...");

                    log.info("========================STEP:2/3===============================");
                    //2.上传config
                    log.info("PUSHING CONFIG STARTING...");
                    String config = jsonObject.getString("Config");
                    String configPath = unTarPath + File.separator + config;
                    pushingConfig(project, configPath, imageName);
                    log.info("PUSHING CONFIG ENDED...");

                    log.info("========================STEP:3/3===============================");
                    //3.上传manifest
                    log.info("PUSHING MANIFEST STARTING...");
                    pushingManifest(project, layerPathList, configPath, imageName, tag);
                    log.info("PUSHING MANIFEST ENDED...");
                    log.info("PUSHING {} COMPLETED!", repo);
                }
            }

        } catch (Exception e) {
            log.error("", e);
        }
    }

    /**
     * 上传镜像层
     *
     * @param layerPath 层路径
     * @param imageName 镜像名称
     */
    private void pushLayer(String project, String layerPath, String imageName) throws Exception {
        File layerFile = new File(layerPath);
        boolean layerExist = checkLayerExist(project, layerFile, imageName);
        if (layerExist) {
            log.info("LAYER ALREADY EXISTS! LAYER PATH:{}", layerPath);
            return;
        }
        String location = startingPush(project, imageName);
        chunkPush(layerFile, location);
//        monolithicPush(layerFile,location);
    }

    /**
     * 判断层是否存在
     *
     * @param layer     层
     * @param imageName 镜像名称
     * @return true:存在,false:不存在
     */
    private boolean checkLayerExist(String project, File layer, String imageName) throws Exception {
        String hash256 = FileUtil.hash256(layer);
        String url = String
                .format("%s/v2/%s/blobs/%s", targetRepoAddress, project + "/" + imageName, "sha256:" + hash256);
        Response response = OkHttpClientUtil.headOkHttp(url);
        return response.code() == HttpStatus.OK.value();
    }

    /**
     * 开始上传
     *
     * @param imageName 镜像名称
     */
    private String startingPush(String project, String imageName) throws IOException {
        String url = String.format("%s/v2/%s/blobs/uploads/", targetRepoAddress, project + "/" + imageName);
        Response response = OkHttpClientUtil.postOkHttp(url, RequestBody.create(null, ""));
        if (response.code() == HttpStatus.ACCEPTED.value()) {
            return response.header("location");
        }
        return "";
    }

    /**
     * 分块上传
     */
    private void chunkPush(File layerFile, String url) throws Exception {
        long length = layerFile.length();
        log.info("file size:{}", length);
        //10M
        int len = 1024 * 1024 * 5;
        byte[] chunk = new byte[len];
        int offset = 0;
        int index = 0;
        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
        while (true) {
            byte[] blocks = FileUtil.getBlock(offset, layerFile, chunk.length);
            if (Objects.isNull(blocks)) {
                log.warn("File block is null!");
                break;
            }
            offset += blocks.length;
            messageDigest.update(blocks);
            log.info("pushing range:[{}-{}]... {}%", index, offset, String.format("%.2f", (float) offset / (float) length * 100));
            if (offset == length) {
                String hash256 = FileUtil.byte2Hex(messageDigest.digest());
                url = String.format("%s&digest=sha256:%s", url, hash256);
                Response response = OkHttpClientUtil.putOkHttp(url, index, offset, blocks);
                if (response.code() != HttpStatus.CREATED.value()) {
                    log.error("chunk push error!code:{},digest:{},{}", response.code(), hash256, response.body().string());
                    throw new RuntimeException("chunk push error");
                }
                response.close();
                break;
            } else {
                Response response = OkHttpClientUtil.patchOkHttp(url, index, offset, blocks);
                if (response.code() != HttpStatus.ACCEPTED.value()) {
                    log.error("patch error!code:{},response:{}", response.code(), response.body().string());
                    throw new RuntimeException("patch error!");
                }
                url = response.header("location");
            }
            index = offset;
        }
    }

    /**
     * 整块上传
     *
     * @param layer
     */
    private void monolithicPush(File layer, String url) throws Exception {
        byte[] contents = FileUtils.readFileToByteArray(layer);
        String hash256 = FileUtil.hash256(layer);
        url = url + "&digest=sha256:" + hash256;
        Response response = OkHttpClientUtil.putOkHttp(url, contents);
        if (response.code() != HttpStatus.CREATED.value()) {
            log.error("monolithicPush error!code:{},{}", response.code(), response.body().string());
            throw new RuntimeException("monolithicPush error!");
        }
    }

    /**
     * 上传config
     *
     * @param configPath 路径
     * @param imageName  镜像名称
     * @throws Exception 异常
     */
    private void pushingConfig(String project, String configPath, String imageName) throws Exception {
        File file = new File(configPath);
        if (checkLayerExist(project, file, imageName)) {
            log.warn("{} exists!", configPath);
            return;
        }
        log.info("start pushing config...");
        String url = startingPush(project, imageName);
        monolithicPush(file, url);
        log.info("config:{} upload success!", configPath);
    }

    /**
     * 上传manifest清单
     *
     * @param layerArrays
     * @param configPath
     * @param tag
     * @throws Exception
     */
    private void pushingManifest(String project, List<String> layerArrays, String configPath, String imageName, String tag) throws Exception {
        ManifestV2 manifestV2 = new ManifestV2()
                .setMediaType("application/vnd.docker.distribution.manifest.v2+json")
                .setSchemaVersion(2);
        File configFile = new File(configPath);
        String hash256 = FileUtil.hash256(configFile);
        Config config = new Config()
                .setMediaType("application/vnd.docker.container.image.v1+json")
                .setDigest("sha256:" + hash256)
                .setSize((int) configFile.length());
        manifestV2.setConfig(config);
        List<Layer> layers = layerArrays.stream()
                .map(layerPath -> {
                    File layerFile = new File(layerPath);
                    Layer layer = new Layer();
                    String hash2561 = FileUtil.hash256(layerFile);
                    layer.setDigest("sha256:" + hash2561);
                    layer.setMediaType("application/vnd.docker.image.rootfs.diff.tar");
                    layer.setSize((int) layerFile.length());
                    return layer;
                }).collect(Collectors.toList());
        manifestV2.setLayers(layers);
        String manifestStr = JSON.toJSONString(manifestV2);
//        System.out.println(manifestStr);
        String url = String.format("%s/v2/%s/manifests/%s", targetRepoAddress, project + "/" + imageName, tag);
        Response response = OkHttpClientUtil.putManifestOkHttp(url, manifestStr.getBytes(StandardCharsets.UTF_8));
        if (response.code() != HttpStatus.CREATED.value()) {
            log.error("upload manifest error!,code:{},response:{}", response.code(), response.body().string());
            return;
        }
//        response.close();
        log.info("manifest upload success!");
    }
}

  1. FileUtil.java实现了文件的解压以及获取文件sha256等方法
/**
 * @author Administrator
 */
public class FileUtil {

    /**
     * 解压tar
     *
     * @param sourceFile
     * @param destPath
     * @throws Exception
     */
    public static String doUnArchiver(File sourceFile, String destPath)
            throws Exception {
        byte[] buf = new byte[1024];
        FileInputStream fis = new FileInputStream(sourceFile);
        BufferedInputStream bis = new BufferedInputStream(fis);
        TarArchiveInputStream tais = new TarArchiveInputStream(bis);
        TarArchiveEntry tae = null;
        destPath = createTempDirIfNotExist(sourceFile.getName(), destPath);
        while ((tae = tais.getNextTarEntry()) != null) {
            File f = new File(destPath + "/" + tae.getName());
            if (tae.isDirectory()) {
                f.mkdirs();
            } else {
                /*
                 * 父目录不存在则创建
                 */
                File parent = f.getParentFile();
                if (!parent.exists()) {
                    parent.mkdirs();
                }

                FileOutputStream fos = new FileOutputStream(f);
                BufferedOutputStream bos = new BufferedOutputStream(fos);
                int len;
                while ((len = tais.read(buf)) != -1) {
                    bos.write(buf, 0, len);
                }
                bos.flush();
                bos.close();
            }
        }
        tais.close();
        return destPath;
    }

    /**
     * 创建临时目录
     *
     * @param pathName
     * @param basePath
     */
    private static synchronized String createTempDirIfNotExist(String pathName, String basePath) {
        String dir;
        if (pathName.contains(".")) {
            String[] split = pathName.split("\\.");
            dir = basePath + File.separator + split[0];
        } else {
            dir = basePath + File.separator + pathName;
        }
        File file = new File(dir);
        if (!file.exists()) {
            file.mkdirs();
        }
        return dir;
    }

    /**
     * 读取json文件
     *
     * @param fileName
     * @return
     */
    public static String readJsonFile(String fileName) {
        try {
            File jsonFile = new File(fileName);
            Reader reader = new InputStreamReader(new FileInputStream(jsonFile), StandardCharsets.UTF_8);
            return IOUtils.toString(reader);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String hash256(File file) {

        try (InputStream fis = new FileInputStream(file)) {
            byte[] buffer = new byte[4096];
            MessageDigest md5 = MessageDigest.getInstance("SHA-256");
            for (int numRead = 0; (numRead = fis.read(buffer)) > 0; ) {
                md5.update(buffer, 0, numRead);
            }
            return byte2Hex(md5.digest());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 将byte转为16进制
     *
     * @param bytes 要转换的bytes
     * @return 16进制String
     */
    public static String byte2Hex(byte[] bytes) {
        StringBuilder stringBuffer = new StringBuilder();
        String temp;
        for (byte b : bytes) {
            temp = Integer.toHexString(b & 0xFF);
            if (temp.length() == 1) {
                // 1得到一位的进行补0操作
                stringBuffer.append("0");
            }
            stringBuffer.append(temp);
        }
        return stringBuffer.toString();
    }

    /**
     * 文件分块工具
     *
     * @param offset    起始偏移位置
     * @param file      文件
     * @param blockSize 分块大小
     * @return 分块数据
     */

    public static byte[] getBlock(long offset, File file, int blockSize) {
        byte[] result = new byte[blockSize];
        try (RandomAccessFile accessFile = new RandomAccessFile(file, "r")) {
            accessFile.seek(offset);
            int readSize = accessFile.read(result);
            if (readSize == -1) {
                return null;
            } else if (readSize == blockSize) {
                return result;
            } else {
                byte[] tmpByte = new byte[readSize];
                System.arraycopy(result, 0, tmpByte, 0, readSize);
                return tmpByte;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

  1. OkHttpClientUtil.java实现了http相关方法
/**
 * @author Administrator
 */
@Slf4j
public class OkHttpClientUtil {

    private static final TrustAllManager trustAllManager = new TrustAllManager();

    private static final OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .authenticator((route, response) -> {
                //用户名、密码
                String credential = Credentials.basic("admin", "password", StandardCharsets.UTF_8);
                return response.request().newBuilder()
                        .header("Authorization", credential)
                        .build();
            })
            .connectionPool(new ConnectionPool(10,1, TimeUnit.MINUTES))
            .sslSocketFactory(createTrustAllSSLFactory(trustAllManager),trustAllManager)
//            .proxy(new Proxy(Proxy.Type.HTTP,new InetSocketAddress("127.0.0.1",8888)))
            .writeTimeout(60,TimeUnit.MINUTES)
            .build();

    protected static SSLSocketFactory createTrustAllSSLFactory(TrustAllManager trustAllManager) {
        SSLSocketFactory ssfFactory = null;
        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, new TrustManager[]{trustAllManager}, new SecureRandom());
            ssfFactory = sc.getSocketFactory();
        } catch (Exception ignored) {
            ignored.printStackTrace();
        }

        return ssfFactory;
    }

    /**
     * get请求
     *
     * @param url
     */
    public static Response getOkHttp(String url) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .get()
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * get请求
     *
     * @param url
     */
    public static Response headOkHttp(String url) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .head()
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * post请求
     *
     * @param url
     * @param body
     */
    public static Response postOkHttp(String url, RequestBody body)
            throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * patch方式
     *
     * @param url
     * @return
     * @throws IOException
     */
    public static Response patchOkHttp(String url, int index, int offset, byte[] buffer)
            throws IOException {
        MediaType mediaType = MediaType.parse("application/octet-stream");
        RequestBody body = RequestBody.create(mediaType, buffer);
        Request request = new Builder()
                .url(url)
                .patch(body)
                .header("Content-Type", "application/octet-stream")
                .header("Content-Length", String.valueOf(buffer.length))
                .header("Content-Range", String.format("%s-%s", index, offset))
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * put方式
     *
     * @param url
     * @param index
     * @param offset
     * @param buffer
     * @return
     * @throws IOException
     */
    public static Response putOkHttp(String url, int index, int offset, byte[] buffer)
            throws IOException {
        MediaType mediaType = MediaType.parse("application/octet-stream");
        RequestBody body = RequestBody.create(mediaType, buffer);
        Request request = new Builder()
                .url(url)
                .put(body)
                .header("Content-Type", "application/octet-stream")
                .header("Content-Length", String.valueOf(buffer.length))
                .header("Content-Range", String.format("%s-%s", index, offset))
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * put方式
     *
     * @param url
     * @param buffer
     * @return
     * @throws IOException
     */
    public static Response putOkHttp(String url, byte[] buffer)
            throws IOException {
        MediaType mediaType = MediaType.parse("application/octet-stream");
        RequestBody body = RequestBody.create(mediaType, buffer);
        Request request = new Builder()
                .url(url)
                .put(body)
                .header("Content-Type", "application/octet-stream")
                .header("Content-Length", String.valueOf(buffer.length))
                .build();
        return okHttpClient.newCall(request).execute();
    }

    /**
     * put方式
     *
     * @param url
     * @param buffers
     * @return
     * @throws IOException
     */
    public static Response putManifestOkHttp(String url, byte[] buffers)
            throws IOException {
        MediaType mediaType = MediaType.parse("application/vnd.docker.distribution.manifest.v2+json");
        RequestBody body = RequestBody.create(mediaType, buffers);
        Request request = new Builder()
                .url(url)
                .put(body)
                .header("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
                .build();
        return okHttpClient.newCall(request).execute();
    }
}

class TrustAllManager implements X509TrustManager{
    @Override
    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

    }

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
}

  1. 清单文件结构ManifestV2.java
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
public class ManifestV2 {
    private Integer schemaVersion;
    private String mediaType;
    private Config config;
    private List<Layer> layers;
}

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
public class Config {
    private String mediaType;
    private Integer size;
    private String digest;
}

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Layer {
    private String mediaType;
    private Integer size;
    private String digest;
}
  1. controller层
@PostMapping("fileUpload")
    public String fileUpload2(@RequestParam("file") MultipartFile file, String project) throws IOException {
        long startTime = System.currentTimeMillis();
        String path = "C:\\test" + File.separator + file.getOriginalFilename();

        File newFile = new File(path);
        //通过CommonsMultipartFile的方法直接写文件(注意这个时候)
        file.transferTo(newFile);
        long endTime = System.currentTimeMillis();
        log.info("file:{} upload success,size:{} byte,spend time:{} ms", newFile.getName(), newFile.length(), endTime - startTime);
        log.info("start to push registry...");
        dockerImageUploadBiz.push(newFile, project);
        return "/success";
    }
  1. 调用方式--postman


    postman.png
  2. pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>docker_images_push</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
    <relativePath/>
  </parent>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-compress</artifactId>
      <version>1.20</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.54</version>
    </dependency>
    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp</artifactId>
      <version>3.8.1</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.6</version>
    </dependency>
    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.4</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>2.1.6.RELEASE</version>
      </plugin>
    </plugins>
  </build>

</project>

以上。
引用:

  1. https://jishuin.proginn.com/p/763bfbd29bd2
  2. https://docs.docker.com/registry/spec/api/#pushing-an-image
  3. https://github.com/silenceper/docker-tar-push
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容