Socket编程之TCP套件字

news2025/12/19 8:42:43

基于的TCP套件字编程流程

1. Socket套接字

Socket是一个编程接口(网络编程接口),是一种特殊的文件描述符(write/read)。Socket并不 仅限于TCP/IP

Socket独立于具体协议的编程接口,这个接口位于TCP/IP四层模型的应用层与传输层之间

介绍一下网络通信的三种主要模式

  1. 单工 A B两个端 A只负责发送 B只负责接收
  2. 半双工 A、B都可以发送或者接受 但是同一时间点之只能发送或者接收
  3. 全双工 A、B同一时间点既可以发也可以收

不使用多线程和并发的话socket编程只能支持到半双工

1.2 Socket的类型

  • 流式套接字:(SOCK_STREAM
    • 面向字节流,针对于传输层协议为TCP协议的网络应用
  • 数据报套接字:(SOCK_DGRAM
    • 面向数据报,针对于传输层协议为UDP协议的网络应用
  • 原始套接字:(SOCK_RAW)
    • s直接跳过传输层

2.基于的TCP套件字编程流程

任何网络应用都会有通信双方:

  • Send 发送端
  • recv 接收端

TCP网络应用(C/S模型)(长连接)

  • Client 客户端(TCP
  • Server 服务端(TCP

任何的网络应用:
传输层的协议(TCP/UDP)+ 端口 + IP地址
网络地址:
任何网络应用任意一方都需要有一个网络地址 (IP+端口0)

2.1 TCP网络应用执行的过程

  • 建立连接
    • 三次握手
  • 发送/接收数据
    • 发送数据:write/send/sendto
    • 接收数据:read/recv/recvfrom
  • 关闭连接
    • 四次挥手

2.2 TCP网络应用的编程流程

2.2.1 TCP-Server服务端
2.2.1.1 建立一个套件字:(socket
SOCKET(2)                  Linux Programmer's Manual                 SOCKET(2)
 NAME
       socket - create an endpoint for communication
 SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       // windows的socket在  winsock2.h
       int socket(int domain, int type, int protocol);
        /*
            @描述:
                申请一个指定类型和指定协议的套接字
            @domain:
                指定域/协议簇。socket接口不仅不局限于TCP/IP,它可以用于Bluetooth、本地通
信...
                每一种下面都有自己的许多协议,我们把IPV4下面的所有协议都归纳到了一个域:
                    AF_INET  IPV4
                    AF_INET6 IPV6
                    AF_UNIX AF_LOCAL 本地通信
                    AF_BULETOOTH 蓝牙
                    ...
                    
            @type:
                指定要创建的套件字的类型:
                    SOCK_STREAM 流式套接字
                    SOCK_DGRAM  数据报套接字
                    SOCK_RAW    原始套接字
                    ...
                    TCP采用流式套接字,UDP采用数据报套接字
            @protocol
                协议,指定具体的应用层协议,可以指定为0:表示采用不知名的私有的应用层
            @return:
                成功返回一个套接字描述符
                失败返回-1,同时errno被设置
        */
2.2.1.2 绑定一个网络地址:(bind)
  • 并不是任意的地址都可以(需要合法且能够正常访问)
  • 把一个套接字和一个网络地址进行绑定。如果想让其他人来主动联系/连接,就需要绑定一个地

​ 址,并且需要把这个地址告诉其他人。不进行绑定,并代表套接字没有地址,不进行绑定套接字在
​ 进行通信时候,内核会动态为套接字指定一个地址。

 BIND(2)                    Linux Programmer's Manual                   
BIND(2)
 NAME
       bind - bind a name to a socket
 SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
     	#include <arpa/inet.h>
       int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
       /*
            @描述:
                用于给一个指定的套接字绑定网络地址
            @sockfd:
                需要绑定地址的套接字
            @addr:
                一个结构体类型,表示网络地址
                    socket接口不仅可以用于以太网(IPV4),也可以用于IPV6,同时也可以
用于Bluetooth,....
                    不同的协议簇,它的地址是不一样的。
                    socket编程接口,用一个通用的 “ 网络地址接口 ”
                        struct sockaddr
                        {
                            sa_family_t sin_family; // 指定协议簇
                            char sa_data[14];
                        };
                     协议地址结构:
                         struct sockaddr_in
                         {
                              sa_family_t sin_family; // 指定协议簇
                              u_int16_t sin_port;     // 端口号
                              struct in_addr sin_addr;// IP地址
                              char sin_zero[8];       // 填充8字节,为了和其他协
议簇地址结构体大小一样
                          };
                            如:
                        struct sockaddr_in sock_info;
                        sock_info.sin_family    = AF_INET; // 指定为IPV4
                        sock_info.sin_port      = htons(6666); //指定为6666端
口
                        sock_info.sin_addr.s_addr = 
inet_addr("192.168.31.1"); // 绑定ip地址
                        // inet_aton("192.168.31.1",&sock_info.sin_addr);
                        bind(sock,(struct sockaddr 
*)&sock_info,sizeof(sock_info));
              @addrlen
                    表示网络地址结构体的大小
              @return:
                    成功返回0,失败返回-1
       
       */

**struct sockaddr_inj解释一下 **这是他的头文件include <arpa/inet.h>

  • sockaddr_in 绑定网络地址

    • sock -》socket套接字

    • addr -》address 地址

    • in -》internet 网络

2.2.1.3 等待监听:(listen)
  • 让一个套接字进入一个监听状态
 LISTEN(2)                  Linux Programmer's Manual                 
LISTEN(2)
 NAME
       listen - listen for connections on a socket
       SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int listen(int sockfd, int backlog);
       /*
            @描述
                设置指定的套接字进入监听模式
            @sockfd:
                需要进入监听模式的套接字
            @backlog:
                可以处理的最大请求数目,可以理解为发起请求的客户端的队伍可以有多长
            @return:
                成功返回0,失败返回-1
       */
2.2.1.4 等待客户端的连接:(accept)
  • 等待客户端来发起连接和客户端建立TCP连接
    • 三次握手
  • 函数成功返回表示和一个客户端完成连接
  • 多次调用函数就可以与不同的客户端进行连接
 ACCEPT(2)                  Linux Programmer's Manual                 
ACCEPT(2)
 NAME
       accept, accept4 - accept a connection on a socket
 SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
        /*
            @描述:
                等待客户端连接套接字,等待客户端发起连接请求
            @sockfd:
                等待客户端连接的那个套接字
            @addr:
                网络地址结构体,用于存储连接成功的客户端信息的。
            @addrlen:
                网络地址结构体的长度指针,用来保存客户端地址结构体的长度的。
                在调用的时候addrlen指向的空间保存的是addr的结构体的最大长度。
                如果函数成功返回,addrlen指向的空间保存的是client客户端地址的结构体长
度。
            @return:
                成功返回与该客户端的连接套接字的描述符(后续服务端和客户端的数据通信,通
过该套件字通信 )
                失败返回-1,同时errno被设置。
2.2.1.5 数据的传输:读/写
  • 发送数据:write/send/sendto
  • 接收数据:read/recv/recvfrom
#include <sys/types.h>
 #include <sys/socket.h>
 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
 /*
    作用:
        往指定套接字中写入数据
    @sockfd:
        需要写入数据的套接字描述符
    @buf:
        需要写入的数据空间的指针
    @len:
        数据的长度
    @flags:
        一般给0,” 带外数据 “
    @return:
        成功返回实际发送的字节数,失败返回-1,同时...
 */
 #include <sys/types.h>
 #include <sys/socket.h>
 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
 /*
    作用:
        从指定套接字中获取数据
    @sockfd:
        需要读取数据的套接字描述符
    @buf:
        读取到的数据所要保存的空间的指针
    @len:
        需要获取的数据的长度
    @flags:
        一般给0,” 带外数据 “
    @return:
        成功返回实际获取的字节数,失败返回-1,同时...
 */
2.2.1.6 关闭套接字:(close/shutdown)
  • 四次挥手
 #include <sys/socket.h>
 int shutdown(int sockfd, int how);
 /*
    作用:
        关闭一个套接字
    @sockfd:
        需要关闭操作的套接字描述符
    @how:
        关闭方式:
 			SHUT_RD   关闭读
            SHUT_WR   关闭写
            SHUT_RDWR 关闭读写 -->close(sockfd);
 */
2.2.2 TCP-Client客户端
  • 建立一个套接字:socket

  • 绑定地址:可选

    • 可以绑定也可也不绑定(不推荐绑定,让系统分配)
  • 发起连接请求:connect

 CONNECT(2)                 Linux Programmer's Manual                
CONNECT(2)
 NAME
       connect - initiate a connection on a socket
 SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int connect(int sockfd, const struct sockaddr *addr,socklen_t 
addrlen);
        /*
            @描述:
                用指定的套接字,对指定网络地址发起连接请求
            @sockfd:
                发起连接请求的套接字
                同时这个套接字是与服务端进行数据通信的套接字
            @addr:  
                需要连接到的网络地址,目标地址
            @addrlen:
                目标地址结构体的大小
            @return:
                成功返回0,失败返回-1
        */

数据的传输:读/写

  • 发送数据:write/send/sendto
  • 接收数据:read/recv/recvfrom

关闭套接字close

示例

客户端

#include <iostream>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cstring>
using namespace std;
vector<int> clients;
// 子线程 接收客户信息
void *myClient(void *arg)
{
    int newClient = *(int *)arg;
    // 等待客户端传过来数据
    char buffer[1024] = {0};
    while (1)
    {
        if (recv(newClient, buffer, 1024, 0) < 0)
        {
            bool shouldExit = false;
            cout << "接收客户端信息失败" << endl;
            close(newClient);
               break;
           
        }
        if (strcmp(buffer, "exit") == 0)
        {
            cout << "结束通信" << endl;
            close(newClient);
           break;
        }
        // 转发给其他客户端
        for (int i = 0; i < clients.size(); i++)
        {
            if (clients[i] == newClient)
                continue;
            if (send(clients[i], buffer, 1024, 0) < 0)
            {

                cout << "转发给其他客户端失败" << endl;
            }
        }
        cout << "客户端信息:" << buffer << endl;
    }
    

    close(newClient);
    return nullptr;
}

int main()
{
    // 1. 申请一个套机字 socket
    int socket_id = socket(AF_INET, SOCK_STREAM, 0);

    if (socket_id == -1)
    {
        perror("套接字创建失败");
    }
    cout << "创建套接字成功" << endl;
    // 2.绑定一个网络地址:(bind)
    //   struct sockaddr_in
    /*
    sockaddr_in   绑定网络地址
    sock -》`socket`套接字
    addr -》`address` 地址
    in   -》`internet  `网络
    */
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;                         // 地址结构 指定为IPV4
    addr.sin_port = htons(5555);                       // 端口号
    addr.sin_addr.s_addr = inet_addr("192.168.5.128"); // IP地址
    //  C 语言允许将非 const 类型的指针隐式地转换为 const 类型的指针会隐式转换吗
    if ((bind(socket_id, (const struct sockaddr *)&addr, sizeof(addr)) == -1))
    {
        perror("绑定失败");
        return -1;
    }
    cout << "绑定成功" << endl;
    // 3. 等待监听:(listen)
    if (listen(socket_id, 5) == -1)
    {
        perror("监听失败");
        return -1;
    }
    cout << "监听成功" << endl;

    cout << "服务请求开启成功" << endl;

    // 4.  等待客户端的连接:(accept)
    while (1)// 使用标志变量控制循环
    {
        struct sockaddr_in newClentInfor;
        socklen_t addrlen = sizeof(sockaddr_in);
        int newClent = accept(socket_id, (struct sockaddr *)&newClentInfor, &addrlen);
       
        if (newClent == -1)
            continue;
        cout << "新客户端连接成功[" << inet_ntoa(newClentInfor.sin_addr) << ":" << htons(newClentInfor.sin_port) << "]" << endl;

        // 增加一个客户端
        clients.push_back(newClent);
        // 创建一个线程
        pthread_t tid;
 
        pthread_create(&tid, nullptr, myClient, (void *)&newClent);
        pthread_detach(tid); // 线程分离,避免阻塞主线程
       
    }
    
    // 关闭套接字  socket本质是文件标识符 close可以关闭
    close(socket_id);

    return 0;
}

服务端

#include <winsock2.h>
#include <windows.h>
#include <iostream>
#include <pthread.h>
using namespace std;

// 接收信息的线程
void *RecvThread(void *arg)
{
    // 套接字类型转换
    SOCKET socket_client = *(SOCKET *)arg;
    while(1)
    {
        char szMsg[256] = {0};
        int nlen =  recv(socket_client, szMsg, sizeof(szMsg), 0);
        // 清空输入缓冲区
        fflush(stdout);
        if(nlen > 0)
        {
            // 清空输入缓冲区
             fflush(stdout);
            cout << "接收到的消息:" << szMsg << endl;
        }
        else if(nlen ==  0)
        {
            cout << "服务器关闭" << endl;
            break;
        }

      
    }
      return nullptr;
}
int main()
{
    // 1. 指定网络库版本
    WSADATA waData;

    // 2. 初始化网络库  MAKEWORD(2, 2) 表示请求 Winsock 2.2 版本
    if (WSAStartup(MAKEWORD(2, 2), &waData) != 0)
    {
        perror("初始化网络库失败");
        return -1;
    }

    // 申请一个套接字
    SOCKET socket_client = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_client == INVALID_SOCKET)
    {
        perror("套接字申请失败");
        return -1;
    }

    // 配置服务器地址
    SOCKADDR_IN addrSrv;
    addrSrv.sin_family = AF_INET;   // 指定为IPV4
    addrSrv.sin_port = htons(5555); // 端口号
    addrSrv.sin_addr.s_addr = inet_addr("192.168.5.128");

    // 连接服务器
    if (connect(socket_client, (sockaddr*)&addrSrv, sizeof(addrSrv)) == SOCKET_ERROR)
    {
        cerr << "连接失败,错误码:" << WSAGetLastError() << endl;
        closesocket(socket_client);
        WSACleanup();
        return -1;
    }
    // 接收信息线程
    pthread_t tid;
    pthread_create(&tid, nullptr, RecvThread, (void*)&socket_client);


    // 数据通信
    while (1)
    {
        cout << "请输入:" << endl;
        char szMsg[256] = {0};
        cin >> szMsg;
        if (send(socket_client, szMsg, strlen(szMsg), 0) == -1)
        {
            perror("发送失败");
            return -1;
        }
        if (strcmp(szMsg, "exit") == 0)
        {
             cout << "通信结束";
            // 关闭套接字
            closesocket(socket_client);
            // 释放网络库资源
            WSACleanup();
            exit(0);
           
        }
    }
    // 关闭套接字
    closesocket(socket_client);
    // 释放网络库资源
    WSACleanup();
    return 0;
}
  • socket_id:可以理解为服务器的“门岗”,它负责监听指定的端口,等待客户端的连接请求。当有客户端请求连接时,socket_id 会接收到这个请求,并通过 accept() 函数创建一个新的套接字来处理这个客户端。
  • newClent:这是通过 accept() 函数创建的新套接字,它专门用于与连接的客户端进行通信。可以理解为服务器用来和特定客户端“对话”的通道。

socket_id 就像是一个门岗,负责接待来访的客户(监听连接请求);而 newClent 就像是服务器派去和客户具体沟通的服务人员,负责处理具体的信息交流。

不过要注意,newClent 并不是服务器本身,而是服务器为每个客户端创建的一个独立的套接字,用于和客户端进行数据传输。

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

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

相关文章

AD9268、AD9643调试过程中遇到的问题

Ad9268芯片 AD9268是一款双通道、16位、80 MSPS/105 MSPS/125 MSPS模数转换器(ADC)。AD9268旨在支持要求高性能、低成本、小尺寸和多功能的通信应用。双通道ADC内核采用多级差分流水线架构&#xff0c;集成输出纠错逻辑。每个ADC都具有宽带宽、差分采样保持模拟输入放大器&…

webpack CDN打包优化

CDN网络分发服务 请求资源时最近的服务器将缓存内容交给用户 体积较大且变动不多的文件存在CDN文件中 react react-dom资源 // 添加自定义对于webpack的配置const path require(path) const { whenProd, getPlugin, pluginByName } require(craco/craco)module.exports {//…

ARM内核一览

经常看介绍某某牛批芯片用的又是ARM什么核&#xff0c;看的云里雾里&#xff0c;所以简单整理整理。&#xff08;内容来自官网和GPT&#xff09; 1 ARM 内核总体分类 系列特点应用场景Cortex-M超低功耗、低成本、实时性嵌入式系统、微控制器、IoTCortex-R高可靠性、硬实时汽车…

Rust 和 Python 如何混合使用

Rust 与 Python 可以通过多种方式混合使用&#xff0c;如 FFI 接口、PyO3 库、CFFI、CPython API、wasm 模块嵌入等。这种混合开发模式可结合 Rust 的性能优势与 Python 的开发效率。其中&#xff0c;PyO3 是目前最受欢迎的桥接工具&#xff0c;它允许使用 Rust 编写 Python 扩…

台式电脑CPU天梯图_2025年台式电脑CPU天梯图

CPU的选择绝对是重中之重,它关乎了一台电脑性能好坏。相信不少用户,在挑选CPU的时候不知道谁强谁弱,尤其是intel和AMD两款CPU之间。下面通过2025年台式电脑CPU天梯图来了解下这两款cpu. 2025年台式电脑CPU天梯图 2025年台式电脑CPU天梯图包含了老旧型号以及12代、13代、14代…

2025年渗透测试面试题总结-匿名[校招]安全服务工程师(题目+回答)

安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 匿名[校招]安全服务工程师 一面问题与完整回答 1. 学校、专业、成绩与排名 2. 学习安全时长 3. 当前学习…

Deseq2:MAG相对丰度差异检验

首先使用代码将contigs和MAG联系起来 https://github.com/MrOlm/drep/blob/master/helper_scripts/parse_stb.py ~/parse_stb.py --reverse -f ~/bin_dir/* -o ~/bin_dir/genomes.stb # 查看第一列的contigs有没有重复&#xff08;重复的话会影响后续比对&#xff09; awk {p…

CTFHub-RCE 命令注入-过滤目录分隔符

观察源代码 代码里面可以发现过滤了目录分隔符\和/ 判断是Windows还是Linux 源代码中有 ping -c 4 说明是Linux 查看有哪些文件 127.0.0.1|ls 打开flag文件 发现存在一个flag_is_here的文件夹&#xff0c;我们需要打开这个文件夹找到目标文件我们尝试分步&#xff0c;先利…

CentOS-stream-9 Zabbix的安装与配置

一、Web环境搭建部署Zabbix时&#xff0c;选择合适的MariaDB、PHP和Nginx版本非常重要&#xff0c;以确保兼容性和最佳性能。以下是建议版本&#xff1a;Zabbix 6.4 MariaDB&#xff1a;官方文档推荐使用MariaDB 10.3或更高版本。对于CentOS Stream 9&#xff0c;建议使用Maria…

开源是什么?我们为什么要开源?

本片为故事类文章推荐听音频哦 软件自由运动的背景 梦开始的地方 20世纪70年代&#xff0c;软件行业处于早期发展阶段&#xff0c;软件通常与硬件捆绑销售&#xff0c;用户对软件的使用、修改和分发权利非常有限。随着计算机技术的发展和互联网的普及&#xff0c;越来越多的开…

【unity游戏开发——编辑器扩展】EditorApplication公共类处理编辑器生命周期事件、播放模式控制以及各种编辑器状态查询

注意&#xff1a;考虑到编辑器扩展的内容比较多&#xff0c;我将编辑器扩展的内容分开&#xff0c;并全部整合放在【unity游戏开发——编辑器扩展】专栏里&#xff0c;感兴趣的小伙伴可以前往逐一查看学习。 文章目录 前言一、监听编辑器事件1、常用编辑器事件2、示例监听播放模…

React---day3

React 2.5 jsx的本质 jsx 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。所有的jsx最终都会被转换成React.createElement的函数调用。 createElement需要传递三个参数&#xff1a; 参数一&#xff1a;type 当前ReactElement的类型&#xff1b;…

PyCharm接入DeepSeek,实现高效AI编程

介绍本土AI工具DeepSeek如何结合PyCharm同样实现该功能。 一 DeepSeek API申请 首先进入DeepSeek官网&#xff1a;DeepSeek 官网 接着点击右上角的 “API 开放平台“ 然后点击API keys 创建好的API key&#xff0c;记得复制保存好 二 pycharm 接入deepseek 首先打开PyCh…

CTFSHOW-WEB-36D杯

给你shell 这道题对我这个新手还是有难度的&#xff0c;花了不少时间。首先f12看源码&#xff0c;看到?view_source&#xff0c;点进去看源码 <?php //Its no need to use scanner. Of course if you want, but u will find nothing. error_reporting(0); include "…

RabbitMQ vs MQTT:深入比较与最新发展

RabbitMQ vs MQTT&#xff1a;深入比较与最新发展 引言 在消息队列和物联网&#xff08;IoT&#xff09;通信领域&#xff0c;RabbitMQ 和 MQTT 是两种备受瞩目的技术&#xff0c;各自针对不同的需求和场景提供了强大的解决方案。随着 2025 年的到来&#xff0c;这两项技术都…

金砖国家人工智能高级别论坛在巴西召开,华院计算应邀出席并发表主题演讲

当地时间5月20日&#xff0c;由中华人民共和国工业和信息化部&#xff0c;巴西发展、工业、贸易与服务部&#xff0c;巴西公共服务管理和创新部以及巴西科技创新部联合举办的金砖国家人工智能高级别论坛&#xff0c;在巴西首都巴西利亚举行。 中华人民共和国工业和信息化部副部…

【KWDB 创作者计划】_再热垃圾发电汽轮机仿真与监控系统:KaiwuDB 批量插入10万条数据性能优化实践

再热垃圾发电汽轮机仿真与监控系统&#xff1a;KaiwuDB 批量插入10万条数据性能优化实践 我是一台N25-3.82/390型汽轮机&#xff0c;心脏在5500转/分的轰鸣中跳动。垃圾焚烧炉是我的胃&#xff0c;将人类遗弃的残渣转化为金色蒸汽&#xff0c;沿管道涌入我的胸腔。 清晨&#x…

Android第十一次面试多线程篇

​面试官​&#xff1a; “你在项目里用过Handler吗&#xff1f;能说说它是怎么工作的吗&#xff1f;” ​候选人​&#xff1a; “当然用过&#xff01;比如之前做下载功能时&#xff0c;需要在后台线程下载文件&#xff0c;然后在主线程更新进度条。这时候就得用Handler来切…

安全,稳定可靠的政企即时通讯数字化平台

在当今数字化时代&#xff0c;政企机构面临着复杂多变的业务需求和日益增长的沟通协作挑战。BeeWorks作为一款安全&#xff0c;稳定可靠的政企即时通讯数字化平台&#xff0c;凭借其安全可靠、功能强大的特性&#xff0c;为政企提供了高效、便捷的沟通协作解决方案&#xff0c;…

LiquiGen流体导入UE

导出ABC 导出贴图 ABC导入Houdini UE安装SideFX_Labs插件 C:\Users\Star\Documents\houdini20.5\SideFXLabs\unreal\5.5 参考: LiquiGenHoudiniUE血液流程_哔哩哔哩_bilibili