热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

玩转直播:如何从0到1构建简单直播系统

一、前言随着5G时代的到来,音视频行业也可能迎来一个行业的春天,直播则是新视频行业一直以来的一个重要的产品形态,从最初的秀场直播ÿ

一、前言

随着5G时代的到来,音视频行业也可能迎来一个行业的春天,直播则是新视频行业一直以来的一个重要的产品形态,从最初的秀场直播,游戏直播,到今年由于疫情,目前比较火的在线教育直播,带货直播等,各类新的直播形式则是越来越多的展示在大众面前。

作为技术开发的我们,今天我们一起简单的了解一下,如何快速搭建一套最简单的直播系统,简单地了解一下主流直播的架构模型。

二、推拉流模型

首先我们先看一张完整的直播推拉流的模型图,我们可以很清楚地看到直播宏观上的架构模型图

图片

2.1 直播三个主要模块

推流模块

推流模块主要分为音视频数据的采集,如果是秀场类直播,可以做美颜滤镜相关功能,用来提升直播的画面品质和用户体验,最后通过编码压缩,降低音视频数据的体积,最后通过流媒体传输协议将数据按照固定格式传递到RTMP服务器,这样整个推流端的工作就完成了。

RTMP服务端模块

传统意义上的RTMP服务器其实可能就只有转码的功能,将推流端传递过来的数据,转成flv等网络格式的数据文件,方便播放端的观看,不过目前云商都提供了一整套的解决方案,例如清晰度转码,内容健康检查,直播封面的生成,数据统计,录制回放等功能,这也是在RTMP服务器的基础上,进行的业务封装,这样才能提供一整套的解决方案。

播放端模块

播放端的逻辑就相对比较简单,简而言之就是获取拉流地址,进行音视频的播放,不过在实际开发的过程中,播放端的业务工作量和技术优化点都是最多的,如上图所示的首屏秒开,解码优化,切换直播间等功能,都是需要花费大量的精力,根据业务不断地去演进优化的。

三、搭建步骤

本入门直播简单教程主要分为如下几个模块


搭建直播服务器

使用OBS进行推流

直播流如何观看

直播间消息的实现


3.1 搭建直播服务器

直播服务器实时地将推流端上传的视频流进行解析和编解码,以用于支持rtmp、hls或httpflv等直播协议的观看端进行观看。

图片

当前市面上有很多开源的直播服务器解决方案,如 livego、srs 和 nginx-rtmp ,亦或者是目前比较主流的云解决方案,目前阿里云,七牛云,腾讯云等都提供了标准的成熟的解决方案,本篇文章旨在快速地搭建一个简单的直播,所以我们可以采用livego这个开放源代码的方式去搭建推拉流服务器,livego 使用纯 go 语言编写,性能高且跨平台,安装和使用非常简单,支持常用的传输协议、文件格式和编码格式,或者安装上文所示,直接在云商开播直播服务。

安装 livego 主要有三种方式:1)直接下载二进制可运行文件;2)从Docker启动;3)从源码编译。

docker run -p 1935:1935 -p 7001:7001 -p 7002:7002 -p 8090:8090 -d gwuhaolin/livego

其中,各个端口的含义如下:


8090:HTTP 管理访问监听地址



1935:RTMP 服务监听地址



7001:HTTP-FLV 服务监听地址



7002:HLS 服务监听地址


3.2 使用OBS推流

OBS(Open Broadcaster Software)是一款开源免费的提供视频录制和直播功能的软件,去OBS官网下载对应平台的软件进行安装即可。

要想推流,首先要解决的是“推什么”的问题,也就是要明确流的来源。打开OBS,点击新建“来源”按钮,如下图中第1步所示,可以看到OBS支持的来源比较丰富,有媒体源、显示器采集、浏览器和窗口采集等等。此处用现有的mp4文件来进行循环推流,因此来源选择“媒体源”,名称用默认的就行,点击“确定”后,设置要播放的视频文件,然后点击“确定”即可。

图片

然后,要解决的就是“往哪推”的问题,也就是需要有一个可用的推流地址才行。

前面我们已经搭建好了livego直播服务器,它提供了一个默认推流地址:rtmp://localhost:1935/live,一个标准的RTMP服务器的推流URL类似这种格式:rtmp://domain/AppName/StreamName,但是要想使用该推流地址,需要有授权的 channelkey 才行。

通过访问 http://localhost:8090/control/get?room=movie 就可以获取用于推流的 channelkey,如下所示,其中 data 字段就是此次获取到的 channelkey。​​​​​​​

{ "status": 200, "data": "rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk"}

到现在,推流地址和 channelkey 都有了,只需要在OBS里面进行相关设置就可以进行推流。首先点击“控件”的“设置”按钮,进入设置面板。

图片

然后,选择“推流”选项。服务选择“自定义”,

服务器设置为:rtmp://localhost:1935/live,串流密钥设置为前面获取到的 channelkey:rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk 。设置好后,点击“控件”的“开始推流”按钮,就可以进行推流了。

图片

一般情况下,默认的输出配置就足以应付大多数场景了,但是要想获得更适合自己想要的的直播效果的话,可以在“输出”选项里设置“高级”输出模式,对此无需求的话可以直接跳过本部分。如下图所示,在高级输出设置界面,可以对串流、录像、音频和回放缓存进行配置,其中,最重要的就是对串流的设置。编码器软件可以选择 x264 和 QuickSync H.264,使用强大的 x264就可以。“重新缩放输出”可以设置输出的分辨率,默认使用原视频的分辨率。

图片

比特率(码率)的含义是视频经过压缩编码后每秒的数据量的大小,单位是 Kbps,此处 K=1000。该值越大,每秒推送的视频数据流就越大,视频质量也越高,但是占用的带宽也更多,可以根据需要进行调整,一般秀场直播常用2000~2500Kbps就可,游戏直播可能对码率的要求比较高一点,可以做对应的调整。

直播推流时,可以使用多种码率控制方式,主要有CBR、ABR、VBR和CRF。


CBR(Constant Bitrate)恒定码率,一定时间范围内比特率基本保持恒定。使用该模式时,在视频动态画面较多的场景下,图像质量会变差,而在静态画面较多的场景下,图像质量又会变好。



VBR(Variable Bitrate)可变码率,其码率可以随着图像的复杂程度的不同而变化。使用该模式时,在图像内容比较简单的场景下,分配较少的码率,而在图像内容复杂的场景下,则分配较多的码率。这样既保证了质量,又兼顾到带宽限制,优先考虑到图像质量。



ABR(Average Bitrate)平均比特率,是VBR的一种插值参数。简单场景分配较低码率,复杂场景分配足够码率,这一点类似VBR。同时,一定时间内平均码率又接近设置的目标码率,这一点又类似CBR。可以认为ABR是CBR和VBR的折中方案。



CRF(Constant Rate Factor)恒定码率系数。CRF值可以理解为对视频的清晰度和流畅度期望的一个固定输出值,即无论是在复杂场景还是在简单场景下,都希望有一个稳定的主观视频质量。


关键帧间隔(Group of Pictures,GOP)指的是一组由一个I帧、多个P帧和B帧组成的一个帧序列。一帧就是视频中的一个画面,其中:


I帧(intra coded picture):最完整的画面,自带全部信息,无需参考其他帧即可解码,每个GOP都是以I帧开始;

P帧(predictive coded picture):帧间预测编码帧,需要参考前面的I帧或P帧,才能进行解码,压缩率较高;

B帧(bipredictive coded picture):双向预测编码帧,以前帧后帧作为参考帧,压缩率最高。


对于普通视频,加大GOP长度有利于减小视频体积,但是在直播场景下,GOP过大会导致客户端的首屏播放时间变长。GOP越小图片质量越高,建议设为2秒,最长不要超过4秒。

3.3 直播流观看

我们刚刚已经搭建完成了RTMP服务器,并且使用目前比较成熟,功能比较丰富的推流工具OBS进行推流,接下来我们就要解决如何在用户终端进行观看了的问题。

FLV(Flash Video)是一种网络视频格式,是一种流媒体格式,目前主流的一些直播网络使用的流媒体格式比较多的都是flv,它能够不需要安装任何插件即可进行播放。

3.3.1 小试牛刀:使用VLC工具观看

VLC 是一款音视频播放器,可以播放本地媒体,也可以播放网络上的媒体,到官网 

https://www.videolan.org/index.zh.html 下载对应的安装包安装即可。

点击“媒体”tab下的“打开网络串流”选项,然后网络地址设置为:

rtmp://localhost:1935/live/movie ,点击“确定”后就可以看到OBS推流的视频啦。

图片

图片

使用VLC主要是方便开发同学进行观看测试,例如观看卡顿的问题,分辨率查看,时延问题的定位,VLC算是一个比较专业的工具,能够方便我们去定位问题和解决问题的

3.3.2 使用flv.js进行浏览器端的观看

flv.js是目标最为流行的html5的纯的Javascript,也是目前国内比较主流的浏览器终端播放flv格式的解决方案,本小节我们就使用flv.js进行简单的播放,打开如下的网址:http://bilibili.github.io/flv.js/demo/

图片

可以看到如图所示的,将如下streamURL的输入框输入http://127.0.0.1:7001/live/movie.flv 后,点击switch to MediaDataSource后Load即可播放如下的画面。

图片

3.3.3 直播协议的简单介绍

图片

到目前为止,我们已经成功的搭建了RTMP小框架,了解了整个推拉流的完整过程,接下来我们就需要对与RTMP协议几个强相关的直播网络传输协议有一个入门的了解。

国内常见的直播协议有几个:


RTMP



HLS



HTTP-FLV


HLS全称是 HTTP Live Streaming。这是 Apple 提出的直播流协议。目前,IOS 和 高版本 Android 都支持 HLS,HLS 主要的两块内容是 .m3u8 文件和 .ts 播放文件。接收服务器会将接收到的视频流进行缓存,然后缓存到一定程度后,会将这些视频流进行编码格式化,同时会生成一份 .m3u8 文件和其它很多的 .ts 文件,HLS的优点是跨平台性比较好,HTML5可以直接打开播放,移动端兼容性良好,缺点也是比较明显,就是时延比较高,如果有些直播,例如互动性不高的直播,可以使用该协议,HLS网络传输格式是非常适合用于点播的场景。

RTMP全称 Real Time Messaging Protocol,即实时消息传送协议,对于开发者来说,我们先明确RTMP是应用层协议,底层是使用的TCP传输协议,这边我们知道RTMP是音视频相关领域的协议,所以这块使用TCP作为主要的传输层协议也给后续RTMP关于网络的各种各样的演进,留下了很多的空间,在直播行业,特别是在推流端,RTMP协议是名副其实的霸主,基本上所有主流的直播网站都是支持rtmp协议进行推流的,关于RTMP的具体协议细节,后续文章有具体的分析。

FLV(Flash Video)是 Adobe 公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个 FLV 由 The FLV Header, The FLV Body 以及其它 Tag 组成。因此加载速度极快。采用 FLV 格式封装的文件后缀为 .flv。

图片

流媒体协议 RTMP, HTTP-FLV, HLS 简单对比:

图片

3.3.4 直播中的消息

在秀场直播系统中,如果说音视频功能的实现,是给直播装扮上了华丽的新装外表的话,那么直播系统中消息系统的实现,则是整个直播华丽新装下的灵魂,如何搭建高可用的直播间消息系统,也是每一个直播系统必须要解决的问题。

在设计秀场直播的消息系统之前,我们需要简单地梳理一下直播间的消息类型。

通知类消息

例如送礼、弹幕、进场、榜单变化、等级变化等等消息。他们的特征是通知用户直播间的事件,营造直播间氛围,提升用户观看直播的体验。

功能类消息

例如踢人、反垃圾审核、红包、PK消息等等。这类消息的特征是辅助直播业务开展,在流程上串联开播端、观看端、服务端三个角色。

我们可以从业务角度中,分析出直播间的各类消息虽然因为业务形态各式各样,最终呈现的形式也是多彩绚丽,但是我们可以从各类的消息展现形式可以分析出,消息从开发的角度,有如下几个特性,我们按照消息是否可丢弃,和实时性划分,我们可以把所有的业务消息归为如下几类:

图片

在直播系统中,秀场直播,带货直播的直播间消息信令通信是比较偏多的,主要是因为业务性质所决定的,秀场直播和带货直播这两类直播的互动性相对比较强,玩法也比较多样,按照我们上图的分类,每一个业务的消息的可丢弃性和实时性要求都不一样,所以在开发消息系统的时候,也需要对消息进行优先级排序,对消息分发的实时性也要有业务性能考量。

刚刚针对直播间消息实时性和不可丢弃性这两个属性做了业务上相关的阐述,不过对于直播消息而言,第一要素是稳定性,消息如何准确稳定地分发到指定的直播间,也是我们需要考虑的问题之一,直播消息的分发实现,从总体上说可以分为两种实现方式,第一是依靠直播间的实时通讯(Instant Messaging),也就是我们常说的IM消息系统,第二个是依靠http短轮询,例如客户端每隔1秒来请求一次服务器,服务器返回这一秒内发生的增量消息信息,客户端获取到这些增量信息,再根据具体的消息业务类型,再进行相对业务的页面UI渲染,这样就可以了,从技术上说,一个是“推”模型,一个是“拉”模型,今天我们因为搭建一个简单的直播间消息系统,我们先用一个简单的"拉"模型进行简单的实现。

基本实现思路:客户端每隔一个极短的时间,例如1秒亦或者更短的时间,根据直播间的id来调用服务端的接口,轮询该直播间发生的消息,服务端这边我们使用redis的SortedSet的数据结构来存储消息,其中key是直播间的房间id,score是服务器接收到该消息事件生成的时间戳,value可以简单地直接存储该消息序列化后的字符串,这样可以按照时间顺序地去存储消息,并且配置过期消息的删除逻辑,整个消息的存储就可以简单地搭建起来。

消息存储用java的伪代码所示:​​​​​​​

long time = new Date().getTime(); try { // redis中插入消息数据 jedisTemplate.zadd(V_UNIQUE_ROOM_ID, time, JSON.toJSONString(roomMessage)); // 按照概率性的去删除redis中过期的消息数据 if (probability()) { deleteOverTimeCache(V_UNIQUE_ROOM_ID); } } catch (Exception e) { log.error("message save error", e); }

可以看到消息存储,如果使用redis的sortedSet进行存储还是比较方便的,接下来我们需要处理就是redis中过期消息的删除,因为无效的过期消息是没有价值的(所有的消息可以做持久化存储),redis中如果单一的key存储的消息过多,也会导致消息的慢查,和内存的使用量不断增大,这是我们不想看到的,这边因为是示例代码,所以简单地处理一下删除逻辑。

private void deleteOverTimeCache(String roomId) { Long totalCount = jedisTemplate.zcard(roomId); log.info("deleteOldTimeCache size is {}", totalCount); if (totalCount <600) { return; } // 倒序删除过期数据 Set tuples = jedisTemplate.zrangeWithScores(roomId, -601, -1); if (CollectionUtils.isNotEmpty(tuples)) { for (Tuple tuple : tuples) { // 这是第一个-600条的那个score double score = tuple.getScore(); jedisTemplate.zremrangeByScore(roomId, 0d, score); break; } } }

上面的伪代码probability()首先先做一个概率性的判断,例如我们做百分之一的随机判断,判断该次请求是否要进行消息的删除(请注意我们删除的逻辑是放在插入的逻辑之中的。如果每一次插入都需要判断是否要删除过期数据,会影响插入的性能)。如果通过概率性判断后,我们就优先判断某一个直播间的消息个数,如果消息个数还是比较少的话,则退出删除逻辑,如果超过消息阀值,则按照时间倒序删除已经过期的消息。

说完了http短轮询消息的存储后,我们最后再简单地说一下客户端消息查询实现逻辑。客户端通过直播间id和时间戳两个字段来请求服务端以查询直播间消息,其中"时间戳"是每一次服务端返回的,这个时间戳是渐进式的,当下一次客户端来请求服务端的数据的时候,都会带来上次服务端返回的时间戳,伪代码如下:​​​​​​​

@Override public RoomMessage queryRoomMessages(MessageMessageReq messageMessageReq) { RoomMessage result = new RoomMessage(); long timestamp = messageMessageReq.getTimestamp(); Set tuples = null; if (timestamp == 0) { // 如果传递是0,说明这个客户端终端是第一次来轮询,我们只要返回一个最近最新的消息返回即可 tuples = jedisTemplate.zrevrangeWithScores(UNIQUE_ROOM_ID, 0, 0); } else // 加上一毫秒,返回后续的消息,每次返回5个,防止客户端因为低端手机原因,过多的消息渲染不出来 tuples = jedisTemplate.zrangeByScoreWithScores(UNIQUE_ROOM_ID, timestamp + 1, System.currentTimeMillis(), 0, 5); } List eachRoomMessages = new ArrayList<>(); long lastTimestamp = 0L; if (!CollectionUtils.isEmpty(tuples)) { for (Tuple tuple : tuples) { //最后一次循环后,会把最后一条消息产生的时间戳,返回给客户端,这样下次客户端就可以拿着这个时间戳来进行查询 lastTimestamp = new Double(tuple.getScore()).longValue(); eachRoomMessages.add(JSON.parseObject(tuple.getElement(), EachRoomMessage.class)); } } result.setTimestamp(lastTimestamp); result.setEachRoomMessages(eachRoomMessages); return result; }

上述三段比较完整地代码主要陈述了一个依赖http短轮询这种方式快速实现的直播间的能力,这种方式是比较粗糙的,不过却是一个很好的实现思路,目前我们线上部分业务也是根据这个轮询的思想进行部分模块的实现。

这样实现的思路也有一个小坑,如果有采用该思路去实现的,可以尝试去规避。如果Android客户端断网的情况下,轮询的线程是不会停止的,例如是晚上8点整断网的,8点01分恢复网络的,当网络恢复的时候,第一次轮询就会导致服务端返回大量的消息,这边是需要进行处理的,否则会返回过多的消息,服务端也会出现慢查,客户端因为渲染过期的消息也会出现部分消息展示区间出现闪跳。例如公屏区可能会"发疯"般的出现各类消息,这些可以通过客户端和服务端的双方约定进行规避,例如客户端当出现网络问题的时候,在超过5秒以上,可以把时间戳置为0,要求服务端返回最新的直播间消息即可,中间丢失掉的消息,可以在业务返回内的进行丢弃。

四、小结

本文主要是想让大家对直播有一个初步的了解,了解直播基本的概念模型,一些基础的概念,后续我们会深入直播具体的模块的学习,进一步去了解直播的原理,也能够帮助我们更好的做好直播的业务。

参考阅读:


  • 聊聊我对 GraphQL 的一些认知

  • 如何保证MySQL和Redis的数据一致性?10张图带你搞定!

  • 保姆级教程,从概念到实践帮你快速上手 Apache APISIX Ingress

  • 云原生消息、事件、流超融合平台——RocketMQ 5.0 初探

  • WebRTC,音视频会议底层支撑技术解读

转自玩转直播:如何从 0 到 1 构建简单直播系统 


推荐阅读
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 本文介绍了如何在Mac上使用Pillow库加载不同于默认字体和大小的字体,并提供了一个简单的示例代码。通过该示例,读者可以了解如何在Python中使用Pillow库来写入不同字体的文本。同时,本文也解决了在Mac上使用Pillow库加载字体时可能遇到的问题。读者可以根据本文提供的示例代码,轻松实现在Mac上使用Pillow库加载不同字体的功能。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 网卡工作原理及网络知识分享
    本文介绍了网卡的工作原理,包括CSMA/CD、ARP欺骗等网络知识。网卡是负责整台计算机的网络通信,没有它,计算机将成为信息孤岛。文章通过一个对话的形式,生动形象地讲述了网卡的工作原理,并介绍了集线器Hub时代的网络构成。对于想学习网络知识的读者来说,本文是一篇不错的参考资料。 ... [详细]
  • 本文介绍了使用Python编写购物程序的实现步骤和代码示例。程序启动后,用户需要输入工资,并打印商品列表。用户可以根据商品编号选择购买商品,程序会检测余额是否充足,如果充足则直接扣款,否则提醒用户。用户可以随时退出程序,在退出时打印已购买商品的数量和余额。附带了完整的代码示例。 ... [详细]
author-avatar
b87968557
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有