源地址:Android WebRTC简介_CharonChui的博客-CSDN博客_android webrtc

Android WebRTC简介

WebRTC简介
WebRTC

WebRTC名称源自网页实时通信(Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的技术,是谷歌2010年以6820万美元收购Global IP Solutions公司而获得的一项技术。Google于2011年6月3日开源的即时通讯项目,旨在使其成为客户端视频通话的标准。其实在Google将WebRTC开源之前,微软和苹果各自的通讯产品已占用很大市场份额(如Skype),Google`也是为了快速扩大市场,所以将他给开源。在行业内得到了广泛的支持和应用,成为下一代视频通话的标准。更多介绍可以去官网上看。

WebRTC被誉为是web长期开源开发的一个新启元,是近年来Web开发的最重要创新。WebRTC允许Web开发者在其web应用中添加视频聊天或者点对点数据传输,不需要复杂的代码或者昂贵的配置。目前支持Chrome、Firefox和Opera,后续会支持更多的浏览器,它有能力达到数十亿的设备。

然而,WebRTC一直被误解为仅适合于浏览器。事实上,WebRTC最重要的一个特征是允许本地和web应用间的互操作,很少有人使用到这个特性。

本文主要以开源项目AndroidRTC为例。
下载后通过Android Studio打开,打开的时候可能会报错,说找不到org.json:json:20090211,这时只要将AndroidRTC/webrtc-client/build.gradle中的

dependencies {
    compile ('com.github.nkzawa:socket.io-client:0.4.1')
    compile 'io.pristine:libjingle:8871@aar'
}

修改成

dependencies {
    compile ('com.github.nkzawa:socket.io-client:0.4.1'){ // //webSocket相关
        exclude group: 'org.json', module: 'json' // 这句话是我新加的,不然会提示org.json:json:20090211找不到
    }

compile 'io.pristine:libjingle:8871@aar' // //webRTC官方aar包
}

就可以了。

运行一下你会发现进去会黑屏,这是因为需要依赖node.js服务。

WebRTC Live Streaming:

Node.js server
Desktop client
Android client
如果电脑上没有安装nodejs需要先安装nodejs,因为需要依赖node.js所以我们要先安装node.js。去nodejs官网下载安装。

然后是要本地下载ProjectRTC项目:
- git clone https://github.com/pchab/ProjectRTC.git
- cd ProjectRTC/
- npm install
- npm start
服务默认会运行在3000端口,你可以在浏览器中打开localhost:3000。

如图就表示运行成功了。

接下来我们进行打开android客户端,你会发现,还是黑屏。
这是因为我们需要在项目中的strings.xml中将配置修改成本地电脑在局域网下的ip地址和运行Node服务端的端口。如下:

<resources>
    <string name="app_name">AndroidRTC</string>
    <string name="host">172.16.55.27</string>
    <string name="port">3000</string>
    <string name="action_settings">Options</string>
</resources>

好了,现在再运行android项目,然后打开浏览器输入localhost:3000,在浏览器页面点击start,你就可以看到电脑摄像头获取的画面,如下图:

然后点击左边的call,就会去申请连手机端,接下来你就可以在浏览器和手机端看到画面了,如下图:

手机端的画面:

既然连通了,下面就仔细分析一下代码:
代码文件比较少,主要是三个类:

其中在WebRtcClient中实现的逻辑最为核心,我们就从他入手。

Android相关的API有VideoCapturerAndroid,VideoRenderer,MediaStream,PeerConnection,
和PeerConnectionFactory。下面我们将逐一讲解。

在开始之前,需要创建PeerConnectionFactory,这是Android上使用WebRTC最核心的API。

PeerConnectionFactory
Android WebRTC最核心的类。理解这个类并了解它如何创建其他任何事情是深入了解Android中WebRTC的关键。
它和我们期望的方式还是有所不同的,所以我们开始深入挖掘它。

我们先到WebRtcClient文件中找到PeerConnectionFactory初始化的地方:

initializeAndroidGlobals的参数分别是:
- context:应用上下文
- initializeAudio:是否初始化音频的布尔值。
- initializeVideo:是否初始化视频的布尔值。跳过这两个就允许跳过请求API的相关权限,例如数据通道应用。
- videoCodecHwAcceleration:是否允许硬件加速的布尔值。
- renderEGLContext:用来提供支持硬件视频解码,可以在视频解码线程中创建共享EGL上下文。
可以为空——在本文例子中硬件视频解码将产生yuv420帧而非texture帧。
- initializeAndroidGlobals也是返回布尔值,true表示一切OK,false表示有失败。

有了peerConnectionFactory实例,就可以从用户设备获取视频和音频,最终将其渲染到屏幕上。在Android中,我们需要了解VideoCapturerAndroid,VideoSource,VideoTrack和VideoRenderer。
先从VideoCapturerAndroid开始:

VideoCapturerAndroid
VideoCapturerAndroid其实是一系列Camera API的封装,为访问摄像头设备的流信息提供了方便。它允许获取多个摄像头设备信息,包括前置摄像头,或者后置摄像头。

在WebRtcClient类中的代码为:

private void setCamera(){
    localMS = factory.createLocalMediaStream("ARDAMS");
    if(pcParams.videoCallEnabled){
        MediaConstraints videoConstraints = new MediaConstraints();
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxHeight", Integer.toString(pcParams.videoHeight)));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxWidth", Integer.toString(pcParams.videoWidth)));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxFrameRate", Integer.toString(pcParams.videoFps)));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("minFrameRate", Integer.toString(pcParams.videoFps)));

videoSource = factory.createVideoSource(getVideoCapturer(), videoConstraints);
        localMS.addTrack(factory.createVideoTrack("ARDAMSv0", videoSource));
    }

AudioSource audioSource = factory.createAudioSource(new MediaConstraints());
    localMS.addTrack(factory.createAudioTrack("ARDAMSa0", audioSource));

mListener.onLocalStream(localMS);
}

private VideoCapturer getVideoCapturer() {
    // 获取前置摄像头的名称
    String frontCameraDeviceName = VideoCapturerAndroid.getNameOfFrontFacingDevice();
    // 创建前置摄像头对象
    return VideoCapturerAndroid.create(frontCameraDeviceName);
}

有了包含摄像流信息的VideoCapturerAndroid实例,就可以创建从本地设备获取到的包含视频流信息的MediaStream,从而发送给另一端。但做这些之前,我们首先研究下如何将自己的视频显示到应用上面。

VideoSource/VideoTrack
从VideoCapturer实例中获取一些有用信息,或者要达到最终目标————为连接端获取合适的媒体流,或者仅仅是将它渲染给用户,我们需要了解VideoSource和VideoTrack类。

VideoSource允许方法开启、停止设备捕获视频。这在为了延长电池寿命而禁止视频捕获的情况下比较有用。
VideoTrack是简单的添加VideoSource到MediaStream对象的一个封装。

我们通过代码看看它们是如何一起工作的。capturer是VideoCapturer的实例,videoConstraints是MediaConstraints的实例。
代码还是在上面贴出的setCamera()方法中:

private void setCamera(){
    localMS = factory.createLocalMediaStream("ARDAMS");
    if(pcParams.videoCallEnabled){
        MediaConstraints videoConstraints = new MediaConstraints();
        ......
        // 创建VideoSource
        videoSource = factory.createVideoSource(getVideoCapturer(), videoConstraints);
        // 创建VideoTrack
        localMS.addTrack(factory.createVideoTrack("ARDAMSv0", videoSource));
    }
    // 创建AudioSource
    AudioSource audioSource = factory.createAudioSource(new MediaConstraints());
    // 创建AudioSource
    localMS.addTrack(factory.createAudioTrack("ARDAMSa0", audioSource));

mListener.onLocalStream(localMS);
}

AudioSource/AudioTrack
AudioSource和AudioTrack与VideoSource和VideoTrack相似,只是不需要AudioCapturer来获取麦克风,
代码同上。

VideoRenderer
进入VideoRenderer,WebRTC库允许通过VideoRenderer.Callbacks实现自己的渲染。另外,
它提供了一种非常好的默认方式VideoRendererGui。简而言之,VideoRendererGui是一个GLSurfaceView,使用它可以绘制自己的视频流。我们通过代码看一下它是如何工作的,以及如何添加renderer到VideoTrack。
因为他是渲染的类,所以代码就不是在WebRtcClient里面了,在RtcActivity中,我们看下代码:

// Local preview screen position before call is connected.
private static final int LOCAL_X_CONNECTING = 0;
private static final int LOCAL_Y_CONNECTING = 0;
private static final int LOCAL_WIDTH_CONNECTING = 100;
private static final int LOCAL_HEIGHT_CONNECTING = 100;
// Local preview screen position after call is connected.
private static final int LOCAL_X_CONNECTED = 72;
private static final int LOCAL_Y_CONNECTED = 72;
private static final int LOCAL_WIDTH_CONNECTED = 25;
private static final int LOCAL_HEIGHT_CONNECTED = 25;
// Remote video screen position
private static final int REMOTE_X = 0;
private static final int REMOTE_Y = 0;
private static final int REMOTE_WIDTH = 100;
private static final int REMOTE_HEIGHT = 100;

private VideoRendererGui.ScalingType scalingType = VideoRendererGui.ScalingType.SCALE_ASPECT_FILL;
private GLSurfaceView vsv;
private VideoRenderer.Callbacks localRender;
private VideoRenderer.Callbacks remoteRender;
private WebRtcClient client;
private String mSocketAddress;
private String callerId;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    getWindow().addFlags(
            LayoutParams.FLAG_FULLSCREEN
                    | LayoutParams.FLAG_KEEP_SCREEN_ON
                    | LayoutParams.FLAG_DISMISS_KEYGUARD
                    | LayoutParams.FLAG_SHOW_WHEN_LOCKED
                    | LayoutParams.FLAG_TURN_SCREEN_ON);
    setContentView(R.layout.main);
    mSocketAddress = "http://" + getResources().getString(R.string.host);
    mSocketAddress += (":" + getResources().getString(R.string.port) + "/");

// 获取GLSurfaceView 
    vsv = (GLSurfaceView) findViewById(R.id.glview_call);
    vsv.setPreserveEGLContextOnPause(true);
    vsv.setKeepScreenOn(true);
    // 设置给VideoRendererGui
    VideoRendererGui.setView(vsv, new Runnable() {
        @Override
        public void run() {
            init();
        }
    });

// 创建本地和远程的Render
    // local and remote render
    remoteRender = VideoRendererGui.create(
            REMOTE_X, REMOTE_Y,
            REMOTE_WIDTH, REMOTE_HEIGHT, scalingType, false);
    localRender = VideoRendererGui.create(
            LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING,
            LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING, scalingType, true);

final Intent intent = getIntent();
    final String action = intent.getAction();

if (Intent.ACTION_VIEW.equals(action)) {
        final List<String> segments = intent.getData().getPathSegments();
        callerId = segments.get(0);
    }
}

......

public void onLocalStream(MediaStream localStream) {
    // 将Render添加到VideoTrack中
    localStream.videoTracks.get(0).addRenderer(new VideoRenderer(localRender));
    VideoRendererGui.update(localRender,
            LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING,
            LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING,
            scalingType);
}

@Override
public void onAddRemoteStream(MediaStream remoteStream, int endPoint) {
    remoteStream.videoTracks.get(0).addRenderer(new VideoRenderer(remoteRender));
    VideoRendererGui.update(remoteRender,
            REMOTE_X, REMOTE_Y,
            REMOTE_WIDTH, REMOTE_HEIGHT, scalingType);
    VideoRendererGui.update(localRender,
            LOCAL_X_CONNECTED, LOCAL_Y_CONNECTED,
            LOCAL_WIDTH_CONNECTED, LOCAL_HEIGHT_CONNECTED,
            scalingType);
}

@Override
public void onRemoveRemoteStream(int endPoint) {
    VideoRendererGui.update(localRender,
            LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING,
            LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING,
            scalingType);
}

MediaConstraints
MediaConstraints是支持不同约束的WebRTC库方式的类,可以加载到MediaStream中的音频和视频轨道。对于大多数需要MediaConstraints的方法,一个简单的MediaConstraints实例就可以做到。

private MediaConstraints pcConstraints = new MediaConstraints();
......
pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
pcConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));

......
this.pc = factory.createPeerConnection(iceServers, pcConstraints, this);

MediaStream
本地看见自己后接下来就要想办法让对方看见自己。我们需要自己创建MediaStream。
接下来我们就研究如何添加本地的VideoTrack和AudioTrack来创建一个合适的MediaStream。
代码还是在前面分析的setCamera()方法中:

private MediaStream localMS;

private void setCamera(){
    // 创建MediaStream
    localMS = factory.createLocalMediaStream("ARDAMS");
    if(pcParams.videoCallEnabled){
        MediaConstraints videoConstraints = new MediaConstraints();
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxHeight", Integer.toString(pcParams.videoHeight)));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxWidth", Integer.toString(pcParams.videoWidth)));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxFrameRate", Integer.toString(pcParams.videoFps)));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("minFrameRate", Integer.toString(pcParams.videoFps)));

videoSource = factory.createVideoSource(getVideoCapturer(), videoConstraints);
        // 添加VideoTrack
        localMS.addTrack(factory.createVideoTrack("ARDAMSv0", videoSource));
    }

AudioSource audioSource = factory.createAudioSource(new MediaConstraints());
    // 添加AudioTrack
    localMS.addTrack(factory.createAudioTrack("ARDAMSa0", audioSource));

mListener.onLocalStream(localMS);
}

我们现在有了包含视频流和音频流的MediaStream实例,而且在屏幕上显示了我们的预览画面。下面要做的就该把这些信息传送给对方了。
这篇文章不会介绍如何建立自己的信号流,我们直接介绍对应的API方法,以及它们如何与web关联的。 AppRTC使用autobahn使得WebSocket连接到信号端。

PeerConnection
现在我们有了自己的MediaStream,就可以开始连接远端了。创建PeerConnection很简单,只需要PeerConnectionFactory的协助即可。

private PeerConnection pc;
private PeerConnectionFactory factory;
private LinkedList<PeerConnection.IceServer> iceServers = new LinkedList<>();

iceServers.add(new PeerConnection.IceServer("stun:23.21.150.121"));
iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302"));

this.pc = factory.createPeerConnection(iceServers, pcConstraints, this);

参数的作用如下:

iceServers:连接到外部设备或者网络时需要用到这个参数。在这里添加STUN 和 TURN 服务器就允许进行连接,即使在网络条件很差的条件下。
constraints:前面介绍的MediaConstraints的实例,应该包含offerToRecieveAudio和offerToRecieveVideo。
observer:PeerConnection.Observer实例。

PeerConnection和web上的对应API很相似,包含了addStream、addIceCandidate、createOffer、createAnswer、getLocalDescription、setRemoteDescription和其他类似方法。为了了解如何协调所有工作在两点之间建立起通讯通道,我们先看下下面的几个类:

addStream
这个是用来将MediaStream添加到PeerConnection中的,如同它的命名一样。如果你想要对方看到你的视频、听到你的声音,就需要用到这个方法。

addIceCandidate
一旦内部IceFramework发现有candidates允许其他方连接你时,就会创建IceCandidates。当通过PeerConnectionObserver.onIceCandidate传递数据到对方时,需要通过任何一个你选择的信号通道获取到对方的IceCandidates。使用addIceCandidate添加它们到PeerConnection,以便PeerConnection可以通过已有信息试图连接对方。

createOffer/createAnswer
这两个方法用于原始通话的建立。在WebRTC中,已经有了caller和callee的概念,一个是呼叫,一个是应答。createOffer是caller使用的,它需要一个sdpObserver,它允许获取和传输会话描述协议Session Description Protocol (SDP)给对方,还需要一个MediaConstraint。一旦对方得到了这个请求,它将创建一个应答并将其传输给caller。SDP是用来给对方描述期望格式的数据(如video、formats、codecs、encryption、resolution、 size等)。一旦caller收到这个应答信息,双方就相互建立的通信需求达成了一致,如视频、音频、解码器等。

setLocalDescription/setRemoteDescription
这个是用来设置createOffer和createAnswer产生的SDP数据的,包含从远端获取到的数据。它允许内部PeerConnection配置链接以便一旦开始传输音频和视频就可以开始真正工作。

PeerConnection.Observer
这个接口提供了一种监测PeerConnection事件的方法,例如收到MediaStream时,或者发现iceCandidates时,或者需要重新建立通讯时。这个接口必须被实现,以便你可以有效处理收到的事件,例如当对方变为可见时,向他们发送信号iceCandidates。

WebRTC打开了人与人之间的通讯,对开发者免费,对终端用户免费。 它不仅仅提供了视频聊天,还有其他应用,比如健康服务、低延迟文件传输、种子下载、甚至游戏应用。
想要看到一个真正的WebRTC`应用实例,请下载appear.in。它在浏览器和本地应用间运行的相当完美,在同一个房间内最多可以8个人免费使用。不需要安装和注册。

最后总结一下:

直播用的主流协议:

RTMP: (Real Time Messaging Protocol)实时消息传送协议是Adobe Systems公司为Flash播放器和服务器之间音频、视频和数据传输 开发的开放协议。目前直播的主流平台95%都是基于该协议做的。

优点:成熟,稳定,效果好等等
缺点:有延迟,达不到实时互动等等
WebRTC: 名称源自网页实时通信(Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的技术,是谷歌2010年以6820万美元收购Global IP Solutions公司而获得的一项技术。2011年5月开放了工程的源代码,在行业内得到了广泛的支持和应用,成为下一代视频通话的标准。

优点:实时互动,开发难度小等等
缺点:太实时,如果网络不好,视频质量会严重下降,体验不好;考研服务器等等
那既然WebRTC这么好,那直接用来做直播不就好了。结果往往并没有想象中的那么完美。

WebRTC整体的技术并不适合做直播。WebRTC设计的初衷只是为了在两个浏览器/native app之间解决直接连接发送media streaming/data数据的,也就是所谓的peer to peer的通信,大多数的情况下不需要依赖于服务器的中转,因此一般在通信的逻辑上是一对一。而我们现在的直播服务大部分的情况下是一对多的通信,一个主播可能会有成千上万个接收端,这种方式用传统的P2P来实现是不可能的,所以目前直播的方案基本上都是会有直播服务器来做中央管理,主播的数据首先发送给直播服务器,直播服务器为了能够支持非常多用户的同事观看,还要通过边缘节点CDN的方式来做地域加速,所有的接收端都不会直接连接主播,而是从服务器上接收数据。

WebRtc只适合小范围(8人以内)音视频会议,不适合做直播:
1. 视频部分:vpx的编码器太弱,专利原因不能用264,做的好的都要自己改264/265代码才行。
2. 音频部分:音频只适合人声编码,对音乐和其他非人声的效果很糟糕。
3. 网络部分:对国内各种奇葩网络适应性太低,网络糟糕点或者人多点就卡。
4. 信号处理:同时用过GIPS和WebRTC进行对比,可以肯定目前开源的代码是GIPS阉割过的。
5. 使用规模:10人以内使用,超过10人就挂了,

现在的直播服务器请使用nginx rtmp-module架设,架设好了用ffmpeg命令行来测试播摄像头。主播客户端请使用rtmp进行推流给rtmp-module,粉丝请使用rtmp/flv+http stream进行观看,PC-web端的粉丝请使用Flash NetStream来观看,移动web端的粉丝请使用hls/m3u8来观看。如果你试验成功要上线了,出现压力了,那么把nginx分层(接入层+交换层),稍微改两行代码,如果资金不足以全国部署服务器,那么把nginx-rtmp-module换为cdn的标准直播服务,也可以直接调过nginx,一开始就用cdn的直播服务,比如网宿(斗鱼的直播服务提供商)。这是正道,别走弯路了。

那既然WebRTC不适合做直播,还写这么多干啥,WebRTC内部包含的技术模块是非常适合解决直播过程中存在的各种问题的,而且应该在大多数直播技术框架中都已经得到了部分应用,例如音视频数据的收发、音频处理回音消除降噪等。所以综上,可以使用WebRTC内部的技术模块来解决直播过程中存在的技术问题,但是不适合直接用WebRTC来实现直播的整体框架。

参考:

Introduction to WebRTC on Android
Android WebRTC 编译
Android WebRTC开源项目
ProjectRTC
Getting Started with WebRTC
WebRTC Github
WebRTC GoogleSource
Android WebRTC
Samples
WebRTC API
WebRTC适合做直播吗?
————————————————
版权声明:本文为CSDN博主「CharonChui」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Charon_Chui/article/details/80510945

(转)Android WebRTC简介相关推荐

  1. Android WebRTC语音视频通话demo

    Android WebRTC简介 https://blog.csdn.net/Charon_Chui/article/details/80510945?utm_term=%E6%89%8B%E6%9C ...

  2. Android WebRTC 入门教程(一) -- 使用相机

    前言,最近在搞网页投屏,发现 WebRTC 的Android 版本较少,这里的话,参考了一些优秀的博客,主要是这个大佬的 https://www.jianshu.com/p/eb5fd116e6c8 ...

  3. 视频教程-Android WebRTC 实现1V1实时音视频通信-Android

    Android WebRTC 实现1V1实时音视频通信 从2012年开始从事移动互联网方面的开发工作,曾担任去哪儿网开发工程师,搜狗高级开发工程师,拥有多年一线实战开发经验. 擅长语言:Object- ...

  4. WebRTC基础实践 - 1. WebRTC简介

    WebRTC 是一个开源的实时通信项目, 主要目标是对Web/原生App平台上的语音.视频.以及数据传输等实时通讯提供支持. WebRTC 主要包括以下 JavaScript API(点击链接可查看相 ...

  5. 【译】Android系统简介—— Activity

    续上一篇,继续介绍Android系统.上一篇: [译]Android系统简介 本文主要介绍构建Android应用的一些主要概念: Activity Activity是应用程序中一个单独的有UI的页面( ...

  6. Android ViewTreeObserver简介-------------转

    Android ViewTreeObserver简介 一.结构 public final class ViewTreeObserver extends Object java.lang.Object ...

  7. android radiooptions简介

    android radiooptions简介 RILD负责modem和RILJ端的通信,信息分两种:unsolicited和solicited,前者是由modem主动上报的,诸如时区更新.通话状态.网 ...

  8. Android 的简介和体系结构中每个层的功能。

    Android 的简介和体系结构中每个层的功能. 1.简介 Android是由Google公司和开放手机联盟领导并开发的一种基于Linux的自由且开放源代码的操作系统,主要使用于移动设备. Andro ...

  9. Android字体简介

    Android字体简介 Android系统默认支持三种字体,分别为:"sans","serif","monospace". android. ...

最新文章

  1. 音视频编解码: YUV采样格式中的YUV444,YUV422,YUV420理解
  2. 基于Java的RDMA高性能通信库(二):Java Socket Over RDMA
  3. 最大熵模型(Maximum Entropy Model)文献阅读指南
  4. mysql 取消主从复制_MySQL:第一次看到有人把MySQL主从复制讲解的这么清楚
  5. js:自动亮起100盏灯
  6. Microsoft Azure Tutorial: Build your first movie inventory web app with just a few lines of code
  7. A piecture of J2EE Core Patterns
  8. Ubuntu 14.04数据库服务器--mysql的安装和配置
  9. mysql 集群分区_mysql 集群与分区
  10. [译]机器人操作系统简介:终极机器人应用框架(上)
  11. 智慧医院建设背景下的电子病历分析利用框架
  12. git config命令入门
  13. VMware Mac 全屏问题
  14. 【机器学习】逻辑回归原理及其实现
  15. 【考研高数 武忠祥+880版 自用】高数第二章基础阶段思维导图
  16. matlab倒立摆pid仿真,一级倒立摆课程设计--倒立摆PID控制及其Matlab仿真
  17. 农业物联网行业调研报告 - 市场现状分析与发展前景预测
  18. Equinox 和 OSGI 介绍
  19. 超级干货:光纤知识总结最全的文章,盘它!
  20. HTML5的文档声明

热门文章

  1. Visual Studio 2010安装、配置及使用
  2. SPEC测试arm服务器性能,SPECJVM2008测试处理器性能_服务器评测与技术-中关村在线...
  3. ThinkPad E40无线网卡驱动安装 FOR CENTOS6.3
  4. 网络协议对应的端口号
  5. 固生堂通过港交所聆讯:上半年营收约6亿元,已实现连续盈利
  6. 万能实体类(pageDate)
  7. 国产化之银河麒麟.netcore3.1访问https服务的两个问题
  8. 外形很犀利 Win7旗舰版全新体验
  9. python3切割圆形图片
  10. 2021年中国纯电动车产量、销量及投资情况分析[图]