[JAVA版本] 最新websocket获取B站直播弹幕——非官方API

这篇具有很好参考价值的文章主要介绍了[JAVA版本] 最新websocket获取B站直播弹幕——非官方API。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、教程

如果只想要代码实现,直接看第二部分。

1、相关依赖

fastjson2用于解析JSON字符串,可自行替换成别的框架。
hutool-core用于解压zip数据,可自行替换成别的框架。

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.40</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-core -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-core</artifactId>
    <version>5.8.21</version>
</dependency>

2、获取Cookie

2023年9月B站如果不登录,获取到的弹幕消息是经过脱敏的,获取不到用户名和用户ID。
获取方式: 电脑浏览器登录B站,按F12去网络请求里把B站Cookie值全部复制出来。

3、自定义GET方法,让每次请求都带上cookie

private String get(String url) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
    String bodyStr = JSONObject.toJSONString(dataMap);
    HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
    con.setRequestMethod(method.code);
    /**----------设置请求头------------------------------------------------------------------**/
    con.setRequestProperty("User-Agent", "Mozilla/5.0");
    con.setRequestProperty("Accept", "application/json");application/json。
    con.setRequestProperty("Content-Type", "application/json");
    con.setRequestProperty("Cookie", cookie);
    // 获取响应结果
    try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))){
        // 返回响应结果
        return  bufferedReader.lines().collect(Collectors.joining("\n"));
    }
}

3、定义获取弹幕服务器信息 和 使用

该方法可以获得弹幕服务器信息检验你是否登录的token

public JSONObject getDanmuInfoData(int roomid) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
	//获取直播间真实ID ,因为存在短ID
	String result = get("https://api.live.bilibili.com/room/v1/Room/room_init?id="+roomid);
    roomid = JSONObject.parseObject(result).getJSONObject("data").getIntValue("room_id");;
	//获取弹幕服务信息
	result = get("https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?type=0&id="+roomid)return JSONObject.parseObject(getDanmuInfo(result )).getJSONObject("data");
}

使用:

//获取弹幕服务器信息
JSONObject danmuInfoData = getDanmuInfoData(直播间ID);
//获取完整弹幕信息的Token
String token = danmuInfoData.getString("token");
//服务器节点列表
JSONArray hostList = danmuInfoData.getJSONArray("host_list");
//选一个服务器节点
JSONObject host = hostList.getJSONObject(0);
//弹幕服务器地址
String wsUrl = String.format("ws://%s:%s/sub", host.getString("host"), host.getString("ws_port"));

4、定义websocket监听类,处理监听到的事件

4.1先新建几个常量,后面方便使用。

public interface Opt{
    short HEARTBEAT = 2;//	客户端发送的心跳包(30秒发送一次)
    short HEARTBEAT_REPLY = 3;//	服务器收到心跳包的回复 人气值,数据不是JSON,是4字节整数
    short SEND_SMS_REPLY = 5;//	服务器推送的弹幕消息包
    short AUTH = 7;//客户端发送的鉴权包(客户端发送的第一个包)
    short AUTH_REPLY = 8;//服务器收到鉴权包后的回复
}
public interface Version{
    short NORMAL = 0;//Body实际发送的数据——普通JSON数据
    short ZIP = 2; //Body中是经过压缩后的数据,请使用zlib解压,然后按照Proto协议去解析。
}

4.2定义WebsocketListener 监听类

定义变量cookie、roomid、token,需要用他们生成鉴权包。

@ClientEndpoint
public class WebsocketListener {
    private String cookie;
    private int roomid;
    private String token;

    public WebsocketListener(String cookie, int roomid, String token) {
        this.cookie = cookie;
        this.roomid = roomid;
        this.token = token;
    }

    private Session session;
    @OnOpen
    public void onOpen(Session session) throws IOException {
        

    }

    @OnMessage
    public void onMessage(ByteBuffer byteBuffer) {
      
    }

    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        System.out.println("服务器断开: " + closeReason);
    }

    @OnError
    public void onError(Session session, Throwable t) {
        t.printStackTrace();
    }

定义 封包 / 解包 方法

Websocket发送和接收时使用。

定义WebsocketListener 封包方法
public static byte[] pack(String jsonStr, @NonNull short code) throws IOException {
    byte[] contentBytes = new byte[0];
    if(Opt.AUTH == code){
        contentBytes = jsonStr.getBytes();
    }
    try(ByteArrayOutputStream data = new ByteArrayOutputStream();
        DataOutputStream stream = new DataOutputStream(data)){
        stream.writeInt(contentBytes.length + 16);//封包总大小
        stream.writeShort(16);//头部长度 header的长度,固定为16
        stream.writeShort(Version.NORMAL);
        stream.writeInt(code);//操作码(封包类型)
        stream.writeInt(1);//保留字段,可以忽略。
        if(Opt.AUTH == code){
            stream.writeBytes(jsonStr);
        }
        return data.toByteArray();
    }
}
定义 WebsocketListener 创建鉴权包方法

用于鉴权。
这里就需要用到变量cookie、roomid、token。

public byte[] generateAuthPack(String cookie,int roomid, String token) throws IOException {
     JSONObject jo = new JSONObject();
        Arrays.stream(cookie.split(";")).forEach(c ->{
            if(c.trim().startsWith("DedeUserID=")){
                jo.put("uid", Long.valueOf(c.split("=")[1]));
            }else if(c.trim().startsWith("buvid3=")){
                jo.put("buvid", c.split("=")[1]);
            }
        });
        jo.put("roomid", roomid);
        jo.put("protover", 1);
        jo.put("platform", "web");
        jo.put("type", 2);
        jo.put("key", token);
        return pack(jo.toString(), Opt.AUTH);
 }
定义 WebsocketListener 创建心跳包方法

用于维持服务连接。

public static byte[] generateHeartBeatPack() throws IOException {
	return pack(null, Opt.HEARTBEAT);
}
定义 WebsocketListener 的解包方法

用于解析服务器返回的消息。
在方法内解析完消息后,可以在 //todo处自定义方法处理器去处理弹幕消息;

public static void unpack(ByteBuffer byteBuffer){
    int packageLen = byteBuffer.getInt();
    short headLength = byteBuffer.getShort();
    short protVer = byteBuffer.getShort();
    int optCode = byteBuffer.getInt();
    int sequence = byteBuffer.getInt();
    if(Opt.HEARTBEAT_REPLY == optCode){
        System.out.println("这是服务器心跳回复");
    }
    byte[] contentBytes = new byte[packageLen - headLength];
    byteBuffer.get(contentBytes);
    //如果是zip包就进行解包
    if(Version.ZIP == protVer){
        unpack(ByteBuffer.wrap(ZipUtil.unZlib(contentBytes)));
        return;
    }

    String content = new String(contentBytes, StandardCharsets.UTF_8);
    if(Opt.AUTH_REPLY == optCode){
        //返回{"code":0}表示成功
        System.out.println("这是鉴权回复:"+content);
    }
    //真正的弹幕消息
    if(Opt.SEND_SMS_REPLY == optCode){
        System.out.println("真正的弹幕消息:"+content);
        //todo 自定义处理

    }
    //只存在ZIP包解压时才有的情况
    //如果byteBuffer游标 小于 byteBuffer大小,那就证明还有数据
    if(byteBuffer.position() < byteBuffer.limit()){
        unpack(byteBuffer);
    }
}

4.3 实现WebsocketListener 的 onOpen方法。

连接成功后,需要做两件事:

  • 发送鉴权包
  • 发送维持服务连接的心跳包
@OnOpen
public void onOpen(Session session) throws IOException {
    this.session = session;
    RemoteEndpoint.Async remote = session.getAsyncRemote();
    //鉴权协议包
    ByteBuffer authPack = ByteBuffer.wrap(generateAuthPack());
    remote.sendBinary(authPack);
    //每30秒发送心跳包
    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
    executorService.scheduleAtFixedRate(() -> {
        try {
            ByteBuffer heartBeatPack = ByteBuffer.wrap(generateHeartBeatPack());
            remote.sendBinary(heartBeatPack);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }, 0, 30, TimeUnit.SECONDS);

}
4.4 实现WebsocketListener 的 onMessage方法,接收服务器消息

需要调用上面的解包方法。文章来源地址https://www.toymoban.com/news/detail-826222.html

@OnMessage
public void onMessage(ByteBuffer byteBuffer) {
     //解包
     unpack(byteBuffer);
 }

二、我的代码实现

1、创建BiliRequest.java

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import jakarta.websocket.ContainerProvider;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.WebSocketContainer;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.stream.Collectors;

public class BiliRequest {
    private String cookie;


    public BiliRequest(String cookie) {
        this.cookie = cookie;
    }

    /**
     * 获取直播间ID ,因为存在短ID
     */
    private String getReadRoomId(int roomid) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        return get("https://api.live.bilibili.com/room/v1/Room/room_init?id="+roomid);
    }
    /**获得弹幕服务地址信息**/
    private String getDanmuInfo(int roomid) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        return get("https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?type=0&id="+roomid);
    }

    public JSONObject getDanmuInfoData(int roomid) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        JSONObject readRoomId = JSONObject.parseObject(getReadRoomId(roomid));
        roomid = readRoomId.getJSONObject("data").getIntValue("room_id");
        return JSONObject.parseObject(getDanmuInfo(roomid)).getJSONObject("data");

    }

    enum Method{
        GET("GET"),POST("POST");
        public String code;

        Method(String code) {
            this.code = code;
        }
    }
    public String get(String url) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        return request(Method.GET,url,null);
    }

    private String request(Method method,String url, Map<String,Object> dataMap) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        String bodyStr = JSONObject.toJSONString(dataMap);
        HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
        con.setRequestMethod(method.code);
        /**----------设置请求头------------------------------------------------------------------**/
        con.setRequestProperty("User-Agent", "Mozilla/5.0");
        con.setRequestProperty("Accept", "application/json");
        con.setRequestProperty("Content-Type", "application/json");
        con.setRequestProperty("Cookie", cookie);
        // 发送 POST 请求
        if(Method.POST == method && null != dataMap && !dataMap.isEmpty()){
            con.setDoOutput(true);
            try(DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {
                wr.writeBytes(bodyStr);
                wr.flush();
            }
        }
        // 获取响应结果
        try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))){
            // 返回响应结果
            return  bufferedReader.lines().collect(Collectors.joining("\n"));
        }
    }
}

2、创建WebsocketListener.java 监听器

import cn.hutool.core.util.ZipUtil;
import com.alibaba.fastjson2.JSONObject;
import jakarta.websocket.*;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;


@ClientEndpoint
public class WebsocketListener {
    private String cookie;
    private int roomid;
    private String token;

    public WebsocketListener(String cookie, int roomid, String token) {
        this.cookie = cookie;
        this.roomid = roomid;
        this.token = token;
    }

    private Session session;
    @OnOpen
    public void onOpen(Session session) throws IOException {
        this.session = session;
        RemoteEndpoint.Async remote = session.getAsyncRemote();
        //鉴权协议包
        ByteBuffer authPack = ByteBuffer.wrap(generateAuthPack());
        remote.sendBinary(authPack);
        //每30秒发送心跳包
        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
        executorService.scheduleAtFixedRate(() -> {
            try {
                ByteBuffer heartBeatPack = ByteBuffer.wrap(generateHeartBeatPack());
                remote.sendBinary(heartBeatPack);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }, 0, 30, TimeUnit.SECONDS);

    }

    @OnMessage
    public void onMessage(ByteBuffer byteBuffer) {
        //解包
        unpack(byteBuffer);
    }

    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        System.out.println("断开连接: " + closeReason);
    }

    @OnError
    public void onError(Session session, Throwable t) {
        t.printStackTrace();
    }

    public interface Opt{
        short HEARTBEAT = 2;//	客户端发送的心跳包(30秒发送一次)
        short HEARTBEAT_REPLY = 3;//	服务器收到心跳包的回复 人气值,数据不是JSON,是4字节整数
        short SEND_SMS_REPLY = 5;//	服务器推送的弹幕消息包
        short AUTH = 7;//客户端发送的鉴权包(客户端发送的第一个包)
        short AUTH_REPLY = 8;//服务器收到鉴权包后的回复
    }
    public interface Version{
        short NORMAL = 0;//Body实际发送的数据——普通JSON数据
        short ZIP = 2; //Body中是经过压缩后的数据,请使用zlib解压,然后按照Proto协议去解析。
    }

    /**
     * 封包
     * @param jsonStr 数据
     * @param code 协议包类型
     * @return
     * @throws IOException
     */
    public static byte[] pack(String jsonStr, short code) throws IOException {
        byte[] contentBytes = new byte[0];
        if(Opt.AUTH == code){
            contentBytes = jsonStr.getBytes();
        }
        try(ByteArrayOutputStream data = new ByteArrayOutputStream();
            DataOutputStream stream = new DataOutputStream(data)){
            stream.writeInt(contentBytes.length + 16);//封包总大小
            stream.writeShort(16);//头部长度 header的长度,固定为16
            stream.writeShort(Version.NORMAL);
            stream.writeInt(code);//操作码(封包类型)
            stream.writeInt(1);//sequence,可以取常数1 .保留字段,可以忽略。
            if(Opt.AUTH == code){
                stream.writeBytes(jsonStr);
            }
            return data.toByteArray();
        }
    }


    /**
     * 生成认证包
     * @return
     */
    public byte[] generateAuthPack(String jsonStr) throws IOException {
        return pack(jsonStr, Opt.AUTH);
    }/**
     * 生成认证包-用于非官方开放API
     * @return
     */
     public byte[] generateAuthPack() throws IOException {
        JSONObject jo = new JSONObject();
        Arrays.stream(cookie.split(";")).forEach(c ->{
            if(c.trim().startsWith("DedeUserID=")){
                jo.put("uid", c.split("=")[1]);
            }else if(c.trim().startsWith("buvid3=")){
                jo.put("buvid", c.split("=")[1]);
            }
        });
        jo.put("roomid", String.valueOf(roomid));
        jo.put("protover", Version.NORMAL);
        jo.put("platform", "web");
        jo.put("type", 2);
        jo.put("key", token);
        return pack(jo.toString(), Opt.AUTH);
    }

    /**
     * 生成心跳包
     * @return
     */
    public static byte[] generateHeartBeatPack() throws IOException {
        return pack(null, Opt.HEARTBEAT);
    }


    /**
     * 解包
     * @param byteBuffer
     * @return
     */
    public static void unpack(ByteBuffer byteBuffer){
        int packageLen = byteBuffer.getInt();
        short headLength = byteBuffer.getShort();
        short protVer = byteBuffer.getShort();
        int optCode = byteBuffer.getInt();
        int sequence = byteBuffer.getInt();
        if(Opt.HEARTBEAT_REPLY == optCode){
            System.out.println("这是服务器心跳回复");
        }
        byte[] contentBytes = new byte[packageLen - headLength];
        byteBuffer.get(contentBytes);
        //如果是zip包就进行解包
        if(Version.ZIP == protVer){
            unpack(ByteBuffer.wrap(ZipUtil.unZlib(contentBytes)));
            return;
        }

        String content = new String(contentBytes, StandardCharsets.UTF_8);
        if(Opt.AUTH_REPLY == optCode){
            //返回{"code":0}表示成功
            System.out.println("这是鉴权回复:"+content);
        }
        //真正的弹幕消息
        if(Opt.SEND_SMS_REPLY == optCode){
            System.out.println("真正的弹幕消息:"+content);
            // todo 自定义处理

        }
        //只存在ZIP包解压时才有的情况
        //如果byteBuffer游标 小于 byteBuffer大小,那就证明还有数据
        if(byteBuffer.position() < byteBuffer.limit()){
            unpack(byteBuffer);
        }
    }
}

3、使用


public class App {
    public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeyException, URISyntaxException, DeploymentException {
        openLiveRoom(直播间ID, 你的Cookie);
    }
	public static void openLiveRoom(int roomId,String cookie) throws IOException, NoSuchAlgorithmException, InvalidKeyException, URISyntaxException, DeploymentException {
		BiliRequest biliReqest = new BiliRequest(cookie);
	    JSONObject danmuInfoData = biliReqest.getDanmuInfoData(roomId);
	    //登录Token
	    String token = danmuInfoData.getString("token");
	    //选一个服务器节点
	    JSONArray hostList = danmuInfoData.getJSONArray("host_list");
	    JSONObject host = hostList.getJSONObject(0);
	    String wsUrl = String.format("ws://%s:%s/sub", host.getString("host"), host.getString("ws_port"));
	    //创建Websocket并连接
	    WebSocketContainer container = ContainerProvider.getWebSocketContainer();
	    container.connectToServer(new WebsocketListener(cookie, roomId, token), new URI(wsUrl)); // 连接到WebSocket服务器
	}
}

到了这里,关于[JAVA版本] 最新websocket获取B站直播弹幕——非官方API的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 抖音直播间websocket礼物和弹幕消息推送可能出现重复的情况,解决办法

    在抖音直播间里,通过websocket收到的礼物消息数据格式如下: 根据字段名称可以看到送礼物的人和送的礼物是什么,并且这个礼物的traceId是唯一的,所以可以通过这个traceId进行去重。 判断这个礼物是否在监控列表中并且是否已经在全局id中: 消息和礼物等数据也有可能会出

    2024年02月01日
    浏览(236)
  • 极简式 Unity 获取 bilibili 直播弹幕、SC、上舰、礼物等 插件

    极简式 Unity 获取 bilibili 直播弹幕、SC、上舰、礼物等 1. 声明 下载链接 软件均仅用于学习交流,请勿用于任何商业用途! 2. 介绍 该项目为Unity实时爬取B站直播弹幕。 项目介绍:通过传入B站直播间账号,实现监控B站直播弹幕、SC、上舰、礼物等。 运行方式:下载后将文件夹

    2024年02月05日
    浏览(35)
  • 视频号直播弹幕采集

    训练地址:https://www.qiulianmao.com websocket逆向 http拦截 websocket拦截 视频号直播弹幕采集 实战一:Http轮询弹幕拦截 更新中

    2024年02月06日
    浏览(49)
  • 抖音直播间弹幕rpc学习

    目标url 随便找个直播间即可。 https://live.douyin.com/198986091107 接口分析 首先并没有在xhr下找到对应的接口 因为采用了websocket来传输信息。切换到ws即可看到 消息下,可以看到16进制的数据在源源不断地增加。 那么我们只要找到反序列化后的数据,再发送到本地的socket服务,就

    2023年04月22日
    浏览(79)
  • 「GPT虚拟直播」实战篇|GPT接入虚拟人实现直播间弹幕回复

    ChatGPT和元宇宙都是当前数字化领域中非常热门的技术和应用。结合两者的优势和特点,可以探索出更多的应用场景和商业模式。例如,在元宇宙中使用ChatGPT进行自然语言交互,可以为用户提供更加智能化、个性化的服务和支持;在ChatGPT中使用元宇宙进行虚拟现实体验,可以

    2024年02月06日
    浏览(55)
  • 直播弹幕系统(二)- 整合RabbitMQ进行消息广播和异步处理

    上一篇文章 SpringCloud网关对WebSocket链接进行负载均衡 中把主要的架子搭建好了,这一篇文章就要开始写业务逻辑了。在分布式系统下,如何达到SpringBoot - WebSocket的使用和聊天室练习的效果。 我们页面上,通过 WebSocket 发送弹幕信息的时候,后端通过 @OnMessage 注解修饰的函数

    2023年04月08日
    浏览(38)
  • 植物大战僵尸小游戏抖音快手直播搭建弹幕插件教程

    植物大战弹幕插件功能介绍 该插件由梦歌技术部团队支持开发,本插件软件通过监测抖音弹幕信息,获取礼物数据触发脚本插件对应的功能; 功能目前基本上已经完善,后期功能会陆续上线支持更新,全新的脚本监测稳定方便实用! 1.打开植物大战僵尸游戏 2.打开弹幕插件

    2024年02月02日
    浏览(351)
  • Python制作一个自动发送弹幕的工具,让你看直播不冷场

    前言 嗨喽,大家好呀~这里是爱看美女的茜茜呐 让我们先看看效果: 名字我就打码了,当然名字不是关键,我直接截图展示算了,GIF的话,太麻烦了。 环境使用: Python 3.8 / 编译器 Pycharm 2021.2版本 / 编辑器 素材准备 接下来我们要准备好你想发送的弹幕内容 这个我都是随便打

    2023年04月27日
    浏览(44)
  • 华为云Moderation案例分享—AI是如何代替人工审核直播及弹幕

    云服务、API、SDK,调试,查看,我都行 阅读短文您可以学习到:人工智能AI之Moderation的直播审核、文本审核、清晰度检测、供应链扭曲矫正 classroom是基于华为云的云上软件教学服务,支持初级开发者和高校师生实现备课、上课、作业、考试、实验、实训等全教学流程的线上

    2024年01月16日
    浏览(45)
  • 抖音直播间弹幕解析:点赞,评论,送礼,进入提示等(2:解析protobuf代码)

    抖音直播间数据抓取打印效果演示 上一章中说了弹幕解析需要了解的知识点以及环境的搭建,本章中深入到代码中去,了解项目的架构和原理以及protobuf 解析实战代码。 现在说一下项目的思路吧: 1. 谷歌浏览器打开live直播间 2. mitmproxy 捕获live.douyin.com http请求并保存响应为指

    2024年02月21日
    浏览(57)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包