WebRPC开发基础流程

一、WebRTC 使用入门

WebRTC(全称 Web Real-Time Communication),即网页即时通信。 是一个支持网页浏览器进行实时语音对话或视频对话的技术方案。从前端技术开发的视角来看,是一组可调用的API标准。

 WebRTC API

官网:WebRTC samples

WebRTC 标准概括介绍了两种不同的技术:媒体捕获设备点对点连接

媒体捕获设备包括摄像机和麦克风,还包括屏幕捕获设备。对于摄像头和麦克风,我们使用 `navigator.mediaDevices.getUserMedia()` 来捕获 `MediaStreams`。对于屏幕录制,我们改为使用 `navigator.mediaDevices.getDisplayMedia()`。

点对点连接由 `RTCPeerConnection` 接口处理。这是在 WebRTC 中两个对等方之间建立和控制连接的中心点。

WebRPC 需要做以下的几件事:
  • - 获取音频,视频或者其他数据
  • - 获取网络信息比如IP地址,端口,并与其他的WebRTC客户端进行交换,穿过NAT合防火墙进行连接.
  • - 处理信号以便发起请求报告错误或者关闭会话
  • - 交换客户端支持的媒体信息,比如分辨率,解码器
  • - 传输音频视频流或者数据
webrtc工作流程

WebRPC开发基础流程_第1张图片

媒体设备使用入门

针对 Web 开发时,WebRTC 标准提供了用于访问连接到计算机或智能手机的相机和麦克风的 API。这些设备通常称为媒体设备,可以通过实现 `MediaDevices` 接口的 `navigator.mediaDevices` 对象使用 JavaScript 进行访问。通过此对象,我们可以枚举所有已连接的设备,监听设备的变化(设备连接或断开连接时)以及打开设备以检索媒体流(见下文)。

其最常见的方式是通过 `getUserMedia()` 函数,该函数会返回一个解析为匹配媒体设备的 `MediaStream` 的 promise。此函数采用单个 `MediaStreamConstraints` 对象,用于指定我们的要求。例如,要简单地打开默认麦克风和摄像头,请执行以下操作。

// 使用promise
const constraints = {
    'video': true,
    'audio': true
}
navigator.mediaDevices.getUserMedia(constraints)
    .then(stream => {
        console.log('Got MediaStream:', stream);
    })
    .catch(error => {
        console.error('Error accessing media devices.', error);
    });

// 使用await/async
const openMediaDevices = async (constraints) => {
    return await navigator.mediaDevices.getUserMedia(constraints);
}

try {
    const stream = openMediaDevices({'video':true,'audio':true});
    console.log('Got MediaStream:', stream);
} catch(error) {
    console.error('Error accessing media devices.', error);
}

调用 `getUserMedia()` 将触发权限请求。如果用户接受该权限,系统会使用包含一个视频和一个音轨的 `MediaStream` 解析该 promise。如果权限遭拒,系统会抛出 `PermissionDeniedError`。如果没有连接任何匹配的设备,则会抛出 `NotFoundError`。

查询媒体设备

在更复杂的应用中,我们很可能需要检查所有连接的摄像头和麦克风,并向用户提供相应的反馈。这可以通过调用 `enumerateDevices()` 函数来实现。这将返回一个 promise,它可以解析为描述每个已知媒体设备的 `MediaDevicesInfo` 数组。我们可以用它来呈现界面,让用户选择他们喜欢的那个。每个 `MediaDevicesInfo` 都包含一个名为 `kind` 的属性,其值为 `audioinput`、`audiooutput` 或 `videoinput`,指示它是哪种类型的媒体设备。

// promise
function getConnectedDevices(type, callback) {
    navigator.mediaDevices.enumerateDevices()
        .then(devices => {
            const filtered = devices.filter(device => device.kind === type);
            callback(filtered);
        });
}

getConnectedDevices('videoinput', cameras => console.log('Cameras found', cameras));

// async await
async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

const videoCameras = getConnectedDevices('videoinput');
console.log('Cameras found:', videoCameras);
监听设备更改

大多数计算机都支持在运行时插入各种设备。它可能是通过 USB 连接的摄像头、蓝牙耳机或一组外部扬声器。为了正确支持这一点,Web 应用应监听媒体设备的变化。这可以通过为 `devicechange` 事件的 `navigator.mediaDevices` 添加监听器来实现。

// Updates the select element with the provided set of cameras
function updateCameraList(cameras) {
    const listElement = document.querySelector('select#availableCameras');
    listElement.innerHTML = '';
    cameras.map(camera => {
        const cameraOption = document.createElement('option');
        cameraOption.label = camera.label;
        cameraOption.value = camera.deviceId;
    }).forEach(cameraOption => listElement.add(cameraOption));
}

// Fetch an array of devices of a certain type
async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

// Get the initial set of cameras connected
const videoCameras = getConnectedDevices('videoinput');
updateCameraList(videoCameras);

// Listen for changes to media devices and update the list accordingly
navigator.mediaDevices.addEventListener('devicechange', event => {
    const newCameraList = getConnectedDevices('video');
    updateCameraList(newCameraList);
});
媒体限制

如果约束对象必须实现 `MediaStreamConstraints` 接口并将其作为参数传递给 `getUserMedia()`,我们就可以打开符合特定要求的媒体设备。此要求可以非常宽泛(音频和/或视频),也可以非常具体(最低相机分辨率或确切设备 ID)。建议使用 `getUserMedia()` API 的应用先检查现有设备,然后使用 `deviceId` 限制条件指定与设备完全匹配的限制条件。如果可能,设备还会根据限制条件进行配置。我们可以对麦克风启用回声消除功能,也可以从摄像头设置视频的特定或最小宽度和高度。

async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

// Open camera with at least minWidth and minHeight capabilities
async function openCamera(cameraId, minWidth, minHeight) {
    const constraints = {
        'audio': {'echoCancellation': true},
        'video': {
            'deviceId': cameraId,
            'width': {'min': minWidth},
            'height': {'min': minHeight}
            }
        }

    return await navigator.mediaDevices.getUserMedia(constraints);
}

const cameras = getConnectedDevices('videoinput');
if (cameras && cameras.length > 0) {
    // Open first available video camera with a resolution of 1280x720 pixels
    const stream = openCamera(cameras[0].deviceId, 1280, 720);
}
本地播放(拉流)

媒体设备打开后,如果有 MediaStream,我们可以将其分配给视频或音频元素,以在本地播放流。

async function playVideoFromCamera() {
    try {
        const constraints = {'video': true, 'audio': true};
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        const videoElement = document.querySelector('video#localVideo');
        videoElement.srcObject = stream;
    } catch(error) {
        console.error('Error opening video camera.', error);
    }
}

与 `getUserMedia()` 一起使用的典型视频元素所需的 HTML 通常具有 `autoplay` 和 `playsinline` 属性。`autoplay` 属性将使分配给元素的新数据流自动播放。`playsinline` 属性允许视频在特定移动浏览器中内嵌播放,而不仅仅是全屏播放。此外,我们还建议对直播使用 `controls="false"`,除非用户应能够暂停这些直播。


Local video playback</video></head>
<body>
    <video id="localVideo" autoplay playsinline controls="false"/>
</body>
</html></code></pre> 
  <h5>二、媒体捕获和约束</h5> 
  <p>WebRTC 的媒体部分介绍了如何使用能够捕捉视频和音频的硬件(例如相机和麦克风),以及媒体流的工作原理。此外,还介绍了显示媒体,这是应用可执行屏幕捕获的方式。</p> 
  <h6>媒体设备</h6> 
  <p>您可以通过 `navigator.mediaDevices` 对象访问和管理浏览器支持的所有摄像头和麦克风。应用可以检索已连接设备的最新列表并监听变化,因为许多相机和微型麦克风可通过 USB 连接,并且可以在应用生命周期内连接和断开连接。由于媒体设备的状态可能会随时发生变化,因此建议应用注册设备更改,以便正确处理更改。</p> 
  <h6>采集音视频</h6> 
  <p>访问媒体设备时,建议您提供尽可能详细的限制条件。虽然可以通过简单的约束条件打开默认摄像头和麦克风,但其提供的媒体流可能明显优于应用的最佳流。</p> 
  <p>具体的约束条件在 `MediaTrackConstraint` 对象中定义,一个针对音频,另一个针对视频。此对象中的特性类型为 `ConstraintLong`、`ConstraintBoolean`、`ConstraintDouble` 或 `ConstraintDOMString`。这些对象可以是特定值(例如数字、布尔值或字符串)、范围(具有最小值和最大值的 `LongRange` 或 `DoubleRange`)或具有 `ideal` 或 `exact` 定义的对象。对于特定值,浏览器将尝试选择尽可能接近的值。对于某个范围,将使用该范围内的最佳值。指定 `exact` 后,系统将仅返回与约束条件完全匹配的媒体流。</p> 
  <pre><code class="hljs">// Camera with a resolution as close to 640x480 as possible
{
    "video": {
        "width": 640,
        "height": 480
    }
}</code></pre> 
  <pre><code class="hljs">// Camera with a resolution in the range 640x480 to 1024x768
{
    "video": {
        "width": {
            "min": 640,
            "max": 1024
        },
        "height": {
            "min": 480,
            "max": 768
        }
    }
}</code></pre> 
  <pre><code class="hljs">// Camera with the exact resolution of 1024x768
{
    "video": {
        "width": {
            "exact": 1024
        },
        "height": {
            "exact": 768
        }
    }
}</code></pre> 
  <p>为了确定某个媒体流的特定轨道的实际配置,我们可以调用 `MediaStreamTrack.getSettings()`,它会返回当前应用的 `MediaTrackSettings`。</p> 
  <p>此外,也可以通过对媒体轨道上调用 `applyConstraints()` 来更新已打开的媒体设备上的轨道约束条件。这样,应用无需重新关闭现有音频流,即可重新配置媒体设备。</p> 
  <h6>显示媒体</h6> 
  <p>想要能够截取和录制屏幕的应用必须使用 Display Media API。函数 `getDisplayMedia()`(属于 `navigator.mediaDevices` 的一部分)与 `getUserMedia()` 类似,用于打开显示内容(或部分内容,如窗口)。返回的 `MediaStream` 与使用 `getUserMedia()` 时相同。</p> 
  <p>`getDisplayMedia()` 的约束条件与常规视频或音频输入资源的限制不同。</p> 
  <pre><code class="hljs">{
    video: {
        cursor: 'always' | 'motion' | 'never',
        displaySurface: 'application' | 'browser' | 'monitor' | 'window'
    }
}</code></pre> 
  <p>上述代码片段展示了屏幕录制的特殊限制的工作原理。请注意,并非所有支持显示媒体支持的浏览器都支持这些属性。</p> 
  <h6>帧率降噪功能配置</h6> 
  <ul> 
   <li> <p>frameRate:可以配置视频帧率</p> </li> 
   <li> <p>width:设置视频宽度,ideal代表理想宽度</p> </li> 
   <li> <p>height:设置视频高度,ideal代表理想高度</p> </li> 
   <li> <p>aspectRatio:代表宽高比</p> </li> 
   <li> <p>对于音频则是开启回音消除、降噪、自动增益等操作</p> </li> 
  </ul> 
  <pre><code class="hljs">const mediaStreamContrains = {
 video: {
 		frameRate: {min: 20},
 		width: {min: 640, ideal: 1280},
 		height: {min: 360, ideal: 720},
		aspectRatio: 16/9
 },
 audio: {
 		echoCancellation: true, // 开启回音消除
 		noiseSuppression: true, // 降噪
 		autoGainControl: true // 自动增益
 }
};

var promise = navigator.mediaDevices.getUserMedia(mediaStreamContrains);</code></pre> 
  <h6>采集视频数据</h6> 
  <ul> 
   <li> <p>采集摄像头的内容并在浏览器上播放</p> </li> 
   <li> <p>需要注意的是,一定要在https协议或者本地localhost域名下才可以调用</p> </li> 
   <li> <p>我们通过调用 <code>getUserMedia</code> 方法,将视频数据加载到 <code>video</code> 标签中进行播放</p> </li> 
   <li> <p>如果video标签想要播放流媒体数据,需要将数据挂在到 <code>srcObject</code>属性上,该属性和普通的 <code>src</code> 属性互斥</p> </li> 
   <li> <p>如果是第一次请求 Camera,浏览器会向用户弹出提示窗口,让用户决定是否可以访问摄像头</p> </li> 
   <li> <p>如果用户允许访问,且设备可用,则调用 <code>gotLocalMediaStream</code> 方法</p> </li> 
  </ul> 
  <h6>获取浏览器设备信息</h6> 
  <ul> 
   <li> <p>以手机为例,它一般会包括前置摄像头和后置摄像头麦克风、相机、耳机等。我们可以根据自己的需要,选择打开不同的设备</p> </li> 
   <li> <p>WebRTC 是否提供了的 <code>enumerateDevices</code> 接口,可以查询自己机子上都有哪些音视频设备</p> </li> 
   <li> <p>deviceInfo中有三个比较重要的属性</p> </li> 
   <li> <p>deviceID:设备的唯一标识</p> </li> 
   <li> <p>label:设备名称,用户已被授予访问媒体设备的权限(要想授予权限需要使用 HTTPS 请求),否则 label 字段始终为空。</p> </li> 
   <li> <p>kind:设备种类,可用于识别出是音频设备还是视频设备,是输入设备还是输出设备</p> </li> 
  </ul> 
  <pre><code class="hljs">// 判断浏览器是否支持这些 API
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
    console.log("enumerateDevices() not supported.");
    return;
}

// 枚举 cameras and microphones.
navigator.mediaDevices.enumerateDevices()
    .then(function (deviceInfos) {
        // 打印出每一个设备的信息
        deviceInfos.forEach(function (deviceInfo) {
            console.log(deviceInfo.kind + ": " + deviceInfo.label +
                " id = " + deviceInfo.deviceId);
        });
    })
    .catch(function (err) {
        console.log(err.name + ": " + err.message);
    });</code></pre> 
  <h6>方法 `getUserMedia` 的配置参数</h6> 
  <ul> 
   <li>facingMode: ‘user’ , ‘environment’ 代表前后置。</li> 
   <li>sampleRate:指定采样率。</li> 
   <li>sampleSize:每个采样点大小的位数</li> 
   <li>volume:从0(静音)到1(最大)取值</li> 
   <li>echoCancellation:是否使用回声消除来尝试去除通过麦克风回传到扬声器的音频</li> 
   <li>autoGainControl:是否要修改麦克风的输入音量</li> 
   <li>noiseSuppression:是否尝试去除音频信号中的背景噪声</li> 
   <li>latency:以秒为单位,控制开始处理声音和下一步可以使用数据之间的时间,不是很确定为什么要设更高的延迟,但是音频编解码器的延时确实有所不同。</li> 
   <li>channelCount:规定了单声道的时候为1,立体声的时候为2。<br>  </li> 
  </ul> 
  <h6>数据流和轨道</h6> 
  <p>`MediaStream` 表示媒体内容流,由音频和视频轨道 (`MediaStreamTrack`) 组成。您可以通过调用 `MediaStream.getTracks()` 从 `MediaStream` 检索所有轨道,该方法会返回一组 `MediaStreamTrack` 对象。</p> 
  <h6>媒体流跟踪</h6> 
  <p>`MediaStreamTrack` 具有的 `kind` 属性为 `audio` 或 `video`,用于表示其表示的媒体类型。您可以通过切换其 `enabled` 属性将各个轨道静音。轨道具有布尔属性 `remote`,它会指示它来自 `RTCPeerConnection` 而来自远程对等设备。</p> 
  <h6>对等连接</h6> 
  <p><strong>点对点连</strong>接是 WebRTC 规范的一部分,该规范旨在对点一台计算机上的两台应用进行连接,以使用点对点协议进行通信。对等设备之间的通信可以是视频、音频或任意二进制数据(适用于支持 `RTCDataChannel` API 的客户端)。为了发现两个对等端如何连接,两个客户端都需要提供 ICE Server 配置。这是 STUN 或 TURN 服务器,其作用是向每个客户端提供 ICE 候选对象,然后这些客户端将被传输到远程对等方。这种转移 ICE 候选对象的方式通常称为信号。</p> 
  <h6>信令</h6> 
  <p>WebRTC 规范包含用于与 ICE(互联网连接建立)服务器通信的 API,但**信令组件**并不属于该组件。需要发出信号才能让两个对等网络共享它们之间的连接方式。这通常可以通过基于 HTTP 的常规 Web API(即 REST 服务或其他 RPC 机制)解决,在此过程中,网络应用可在发起对等连接之前中继必要的信息。</p> 
  <pre><code class="hljs">// Set up an asynchronous communication channel that will be
// used during the peer connection setup
const signalingChannel = new SignalingChannel(remoteClientId);
signalingChannel.addEventListener('message', message => {
    // New message from remote client received
});

// Send an asynchronous message to the remote client
signalingChannel.send('Hello!');</code></pre> 
  <p>信令可以通过许多不同的方式实现,WebRTC 规范不偏好任何特定的解决方案。(前端程序员,可以使用nodejs,websocket技术实现)</p> 
  <h6>启动对等连接</h6> 
  <p>每个对等连接都由一个 `RTCPeerConnection` 对象处理。此类的构造函数接受单个 `RTCConfiguration` 对象作为其参数。此对象定义对等连接的设置方式,**应包含关于要使用的 ICE 服务器的信息**。</p> 
  <p>每个对等连接都由一个RTCPeerconnection对象处理。此类的构造函数将单个RTCConfiguration对象作为其参数。此对象定义了对等连接的设置方式,并应包含有关要使用的ICE服务器的信息。</p> 
  <p>一旦创建了RTCPeerConnection连接,我们需要创建**SDP提供**或**应答**,这取决于我们是主叫对等体还是接收对等体。一旦创建了SDP提供或应答,就必须通过不同的信道将其发送到远程对等端。将SDP对象传递给远程对等方称为**信令**,不在Web RTC规范的范围内。</p> 
  <p>为了从调用端启动对等连接设置,我们创建了一个RTCPeerconnection对象,然后调用createOffer()来创建一个RTCSessionDescription对象。使用setLocalDescription()将此会话描述设置为本地描述,然后通过我们的信令信道发送到接收方。我们还为我们的信号通道设置了一个监听器,以便在从接收端接收到对我们提供的会话描述的回答时使用。</p> 
  <pre><code class="hljs">async function makeCall() {
    const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
    const peerConnection = new RTCPeerConnection(configuration);
    signalingChannel.addEventListener('message', async message => {
        if (message.answer) {
            const remoteDesc = new RTCSessionDescription(message.answer);
            await peerConnection.setRemoteDescription(remoteDesc);
        }
    });
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);
    signalingChannel.send({'offer': offer});
}</code></pre> 
  <p>RTCPeerConnection.createOffer():RTCPeerConnection接口的 createOffer() 方法启动创建一个[SDP](https://developer.mozilla.org/zh-CN/docs/Glossary/SDP) offer,目的是启动一个新的 WebRTC 去连接远程端点。SDP offer 包含有关已附加到 WebRTC 会话,浏览器支持的编解码器和选项的所有[`MediaStreamTrack`](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaStreamTrack)s 信息,以及[ICE](https://developer.mozilla.org/zh-CN/docs/Glossary/ICE) 代理,目的是通过信令信道发送给潜在远程端点,以请求连接或更新现有连接的配置。返回值是一个[`Promise` (en-US)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise),创建 offer 后,将使用包含新创建的要约的[`RTCSessionDescription`](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCSessionDescription)对象来解析该返回值。</p> 
  <p>在接收端,我们会等待传入的回应,然后再创建 `RTCPeerConnection` 实例。完成后,我们使用 `setRemoteDescription()` 设置收到的回应。接下来,我们调用 `createAnswer()` 为收到的优惠创建答案。系统会使用 `setLocalDescription()` 将此答案设置为本地说明,然后通过我们的信令服务器将其发送至发起调用的一方。</p> 
  <pre><code class="hljs">const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
    if (message.offer) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);
        signalingChannel.send({'answer': answer});
    }
});</code></pre> 
  <p>两个对等方同时设置了本地和远程会话说明之后,他们就会了解远程对等方的功能。这并不意味着对等设备之间的连接已准备就绪。为此,我们需要在每个对等端收集 ICE 候选项,并通过信令通道传输给另一个对等方。</p> 
  <h6>ICE</h6> 
  <p>创建 `RTCPeerConnection` 对象后,底层框架会使用提供的 ICE 服务器收集连接建立的候选对象(ICE 候选对象)。`RTCPeerConnection` 上的事件 `icegatheringstatechange` 会指示 ICE 收集的状态为(`new`、`gathering` 或 `complete`)。</p> 
  <p>虽然对等设备可以等待 ICE 收集完成,但通常要高效地使用“滚动冰”技术,并在发现每个 ICE 候选设备后将其传输到远程对等设备。这将大大缩短对等连接的设置时间,并允许视频通话以更低的延迟开始。</p> 
  <p>要收集 ICE 候选对象,只需为 `icecandidate` 事件添加监听器即可。针对该监听器发出的 `RTCPeerConnectionIceEvent` 将包含 `candidate` 属性,该属性表示应发送到远程对等端的新候选音频(请参阅信号)。</p> 
  <pre><code class="hljs">// Listen for local ICE candidates on the local RTCPeerConnection
peerConnection.addEventListener('icecandidate', event => {
    if (event.candidate) {
        signalingChannel.send({'new-ice-candidate': event.candidate});
    }
});

// Listen for remote ICE candidates and add them to the local RTCPeerConnection
signalingChannel.addEventListener('message', async message => {
    if (message.iceCandidate) {
        try {
            await peerConnection.addIceCandidate(message.iceCandidate);
        } catch (e) {
            console.error('Error adding received ice candidate', e);
        }
    }
});</code></pre> 
  <h6>已建立连接</h6> 
  <p>收到 ICE 候选对象后,我们的对等连接状态最终会变为已连接状态。为了检测这一点,我们在 `RTCPeerConnection` 中添加一个监听器,用于监听 `connectionstatechange` 事件。</p> 
  <pre><code class="hljs">// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
    if (peerConnection.connectionState === 'connected') {
        // Peers connected!
    }
});</code></pre> 
  <h6>远程数据流使用入门</h6> 
  <p>`RTCPeerConnection` 连接到远程对等设备后,就可以在它们之间流式传输音频和视频。此时,我们会将从 `getUserMedia()` 收到的数据流连接到 `RTCPeerConnection`。媒体流包含至少一个媒体轨道,当我们想将媒体传输到远程对等设备时,它们会分别添加到 `RTCPeerConnection` 中。</p> 
  <pre><code class="hljs">const localStream = await getUserMedia({vide: true, audio: true});
const peerConnection = new RTCPeerConnection(iceConfig);
localStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, localStream);
});</code></pre> 
  <p>轨道可以在连接到远程对等方之前添加到 `RTCPeerConnection`,因此最好尽早执行此设置,而不是等待连接完成。</p> 
  <h6> 添加远程轨道</h6> 
  <p>为了接收由另一个对等方添加的远程轨道,我们会在本地 `RTCPeerConnection` 上注册一个监听器,用于监听 `track` 事件。`RTCTrackEvent` 包含一个 `MediaStream` 对象数组,这些对象与对等项的相应本地数据流具有相同的 `MediaStream.id` 值。在我们的示例中,每个轨道仅与单个数据流相关联。</p> 
  <pre><code class="hljs">const remoteVideo = document.querySelector('#remoteVideo');

peerConnection.addEventListener('track', async (event) => {
    const [remoteStream] = event.streams;
    remoteVideo.srcObject = remoteStream;
});</code></pre> 
  <h6>数据通道</h6> 
  <p>WebRTC 标准还涵盖用于通过 `RTCPeerConnection` 发送任意数据的 API。可通过对 `RTCPeerConnection` 对象调用 `createDataChannel()` 来完成此操作,该方法会返回 `RTCDataChannel` 对象。</p> 
  <pre><code class="hljs">const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel();</code></pre> 
  <p>远程对等端可以通过监听 `RTCPeerConnection` 对象的 `datachannel` 事件来接收数据通道。收到的事件是 `RTCDataChannelEvent` 类型,包含一个 `channel` 属性,该属性表示在对等方之间连接的 `RTCDataChannel`。</p> 
  <pre><code class="hljs">const peerConnection = new RTCPeerConnection(configuration);
peerConnection.addEventListener('datachannel', event => {
    const dataChannel = event.channel;
});</code></pre> 
  <h6>打开和关闭事件</h6> 
  <p>在使用数据通道发送数据之前,客户端需要等到数据通道打开后才能使用它。具体方法是监听 `open` 事件。同样,当任意一侧关闭频道时,也会发生 `close` 事件。</p> 
  <pre><code class="hljs">const messageBox = document.querySelector('#messageBox');
const sendButton = document.querySelector('#sendButton');
const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel();

// Enable textarea and button when opened
dataChannel.addEventListener('open', event => {
    messageBox.disabled = false;
    messageBox.focus();
    sendButton.disabled = false;
});

// Disable input when closed
dataChannel.addEventListener('close', event => {
    messageBox.disabled = false;
    sendButton.disabled = false;
});</code></pre> 
  <h6>信息</h6> 
  <p>如需在 `RTCDataChannel` 上发送消息,请使用要发送的数据调用 `send()` 函数。此函数的 `data` 参数可以是字符串、`Blob`、`ArrayBuffer` 或 `ArrayBufferView`。</p> 
  <pre><code class="hljs">const messageBox = document.querySelector('#messageBox');
const sendButton = document.querySelector('#sendButton');

// Send a simple text message when we click the button
sendButton.addEventListener('click', event => {
    const message = messageBox.textContent;
    dataChannel.send(message);
})</code></pre> 
  <p>远程对等端将通过监听 `message` 事件来接收 `RTCDataChannel` 上发送的消息。</p> 
  <pre><code class="hljs">const incomingMessages = document.querySelector('#incomingMessages');

const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel();

// Append new messages to the box of incoming messages
dataChannel.addEventListener('message', event => {
    const message = event.data;
    incomingMessages.textContent += message + '\n';
});</code></pre> 
  <h5>代码流程实例</h5> 
  <p>webrtc.js</p> 
  <pre><code class="hljs">export default {
  data() {
    return {
      stream: null,
    }
  },
  methods: {
    // 推流
    async pullPlayer() {
      try {
        // 1.获取本地音视频流
        // 调用 getUserMedia API 获取音视频流
        this.stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
          video: true,
        })
        // 拉流
        this.localVideo = document.getElementById('localVideo')
        console.log('Received local stream', this.stream, this.localVideo.srcObject)
        // this.desc = '11111'
        this.localVideo.srcObject = this.stream
        this.localStream = this.stream
      } catch (e) {
        console.log(`getUserMedia() error: ${e}`)
      }
    },
    // 创建RTCPeerConnection连接:发送方
    createRTCPeer() {
      this.configuration = {
        iceServers: [
          {
            urls: 'stun:stun.l.google.com:19302',
          },
        ],
      }
      // 源连接
      this.pc1 = new RTCPeerConnection(this.configuration)

      // 监听返回的 Candidate
      // 当ice准备好后,加到目标源中
      this.pc1.addEventListener('icecandidate', e => this.onIceCandidate(this.pc1, e))
      this.pc1.addEventListener('iceconnectionstatechange', e => this.onIceStateChange(this.pc1, e))
      //把localStream的音视频,放到源中
      this.getTracksStreams()
    },
    accceptRTCPeer() {
      //目标
      this.pc2 = new RTCPeerConnection(this.configuration)
      // 当ice准备好后,加到目标源中
      this.pc2.addEventListener('icecandidate', e => this.onIceCandidate(this.pc2, e))
      this.pc2.addEventListener('iceconnectionstatechange', e => this.onIceStateChange(this.pc2, e))
      //等待源发来的流
      this.pc2.addEventListener('track', this.gotRemoteStream)
    },

    //把localStream的音视频,放到源中
    getTracksStreams() {
      // 遍历本地流的所有轨道
      this.localStream.getTracks().forEach(track => this.pc1.addTrack(track, this.localStream))
    },
    // 添加 iceCandidate 时调用的方法
    async onIceCandidate(pc, event) {
      try {
        // 源发来的ice,加入到目标中
        await this.getOtherPc(pc).addIceCandidate(event.candidate)
        // 添加成功
        this.onAddIceCandidateSuccess(pc)
      } catch (e) {
        // 添加失败
        this.onAddIceCandidateError(pc, e)
      }
      console.log(
        `${this.getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`,
      )
    },
    // 拉流:将发送来的轨道数据赋值
    gotRemoteStream(e) {
      if (this.remoteVideo.srcObject !== e.streams[0]) {
        // getUserMedia 获得流后,将音视频流展示并保存到 localStream
        this.remoteVideo.srcObject = e.streams[0]
        console.log('pc2 received remote stream', e.streams[0])
      }
    },
    // 判断是发送方还是接收方
    getOtherPc(pc) {
      return pc === this.pc1 ? this.pc2 : this.pc1
    },
    getName(pc) {
      return pc === this.pc1 ? 'pc1' : 'pc2'
    },
    onIceStateChange(pc, event) {
      if (pc) {
        console.log(`${this.getName(pc)} ICE state: ${pc.iceConnectionState}`)
        console.log('ICE state change event: ', event)
      }
    },

    // 创建和设置连接描述
    async createOffers() {
      try {
        console.log('pc1 createOffer start')
        this.offerOptions = {
          offerToReceiveAudio: 1,
          offerToReceiveVideo: 1,
        }
        // 交换媒体描述信息
        const offer = await this.pc1.createOffer(this.offerOptions)
        await this.onCreateOfferSuccess(offer)
      } catch (e) {
        this.onCreateSessionDescriptionError(e)
      }
    },
    async onCreateOfferSuccess(desc) {
      // 发送端创建连接描述
      try {
        // 本地设置描述并将它发送给远端
        // 将 offer 保存到本地
        await this.pc1.setLocalDescription(desc)
        this.onSetLocalSuccess(this.pc1)
      } catch (e) {
        this.onSetSessionDescriptionError()
      }
      // 接收端创建连接描述
      try {
        // 远端将本地给它的描述设置为远端描述
        // 远端将 offer 保存
        await this.pc2.setRemoteDescription(desc)
        this.onSetRemoteSuccess(this.pc2)
      } catch (e) {
        this.onSetSessionDescriptionError()
      }
      // 目标 拿到源的连接描述后,给自己,并生成自己的连接描述
      try {
        // 远端创建应答 answer
        const answer = await this.pc2.createAnswer()
        await this.onCreateAnswerSuccess(answer)
      } catch (e) {
        this.onCreateSessionDescriptionError(e)
      }
    },
    // 本地描述创建成功
    onSetLocalSuccess(pc) {
      console.log(`${this.getName(pc)} setLocalDescription complete`)
    },
    // 本地描述创建失败
    onSetSessionDescriptionError(error) {
      console.log(`Failed to set session description: ${error.toString()}`)
    },
    // 接收描述创建成功
    onSetRemoteSuccess(pc) {
      console.log(`${this.getName(pc)} setRemoteDescription complete`)
    },
    // 接收描述创建失败
    onCreateSessionDescriptionError(error) {
      console.log(`Failed to create session description: ${error.toString()}`)
    },
    // 接收端:生成自己的连接描述
    async onCreateAnswerSuccess(desc) {
      try {
        // 远端设置本地描述并将它发给本地
        // 远端保存 answer
        await this.pc2.setLocalDescription(desc)
        this.onSetLocalSuccess(this.pc2)
      } catch (e) {
        this.onSetSessionDescriptionError(e)
      }
      console.log('pc1 setRemoteDescription start')
      try {
        // 本地将远端的应答描述设置为远端描述
        // 本地保存 answer
        await this.pc1.setRemoteDescription(desc)
        this.onSetRemoteSuccess(this.pc1)
      } catch (e) {
        this.onSetSessionDescriptionError(e)
      }
    },
    // 接收端创建本地描述成功
    onSetLocalSuccess(pc) {
      console.log(`${this.getName(pc)} setLocalDescription complete`)
    },
    // 3.端与端建立连接
    handleConnection(event) {
      // 获取到触发 icecandidate 事件的 RTCPeerConnection 对象
      // 获取到具体的Candidate
      const peerConnection = event.target
      const iceCandidate = event.candidate

      if (iceCandidate) {
        // 创建 RTCIceCandidate 对象
        const newIceCandidate = new RTCIceCandidate(iceCandidate)
        // 得到对端的 RTCPeerConnection
        const otherPeer = getOtherPeer(peerConnection)

        // 将本地获得的 Candidate 添加到远端的 RTCPeerConnection 对象中
        // 为了简单,这里并没有通过信令服务器来发送 Candidate,直接通过 addIceCandidate 来达到互换 Candidate 信息的目的
        otherPeer
          .addIceCandidate(newIceCandidate)
          .then(() => {
            handleConnectionSuccess(peerConnection)
          })
          .catch(error => {
            handleConnectionFailure(peerConnection, error)
          })
      }
    }, // 4.显示远端媒体流
    gotRemoteMediaStream(event) {
      if (remoteVideo.srcObject !== event.streams[0]) {
        remoteVideo.srcObject = event.streams[0]
        remoteStream = event.streams[0]
        console.log('remote 开始接受远端流')
      }
    },
  },
}
</code></pre> 
  <p>player.vue</p> 
  <pre><code class="hljs"><template>
  <div class="video-window">
    <video id="localVideo" playsinline autoplay muted></video>
    <video id="remoteVideo" playsinline autoplay></video>

    <div class="box">
      <button id="startButton" @click="start">Start</button>
      <button id="callButton" @click="call">Call</button>
      <button id="hangupButton" @click="hangup">Hang Up</button>
    </div>
    <div>{{ desc }}</div>
    <button class="btn" @click="ToIndex1">001</button>
  </div>
</template>
<script>
import wx from 'weixin-js-sdk'
import webrtc from '../mixins/webrtc'
export default {
  data() {
    return {
      startButton: null,
      callButton: null,
      hangupButton: null,
      localVideo: null,
      remoteVideo: null,
      startTime: null,

      pc1: null,
      pc2: null,
      desc: '',
    }
  },
  mixins: [webrtc],
  created() {
    this.roomId = this.$route.query.roomId ? this.$route.query.roomId : '001'
  },
  mounted() {
    this.startButton = document.getElementById('startButton')
    this.callButton = document.getElementById('callButton')
    this.hangupButton = document.getElementById('hangupButton')
    this.callButton.disabled = true
    this.hangupButton.disabled = true

    this.remoteVideo = document.getElementById('remoteVideo')
  },
  methods: {
    async start() {
      try {
        await this.pullPlayer()
        this.startButton.disabled = true
        this.callButton.disabled = false
      } catch (error) {
        this.desc = e
        this.startButton.disabled = false
      }
    },
    // 拉流
    async call() {
      this.callButton.disabled = true
      this.hangupButton.disabled = false
      console.log('Starting call')
      this.startTime = window.performance.now()
      // 视频轨道
      const videoTracks = this.localStream.getVideoTracks()
      // 音频轨道
      const audioTracks = this.localStream.getAudioTracks()

      // 判断视频轨道是否有值
      if (videoTracks.length > 0) {
        console.log(`Using video device: ${videoTracks[0].label}`)
      }
      // 判断音频轨道是否有值
      if (audioTracks.length > 0) {
        console.log(`Using audio device: ${audioTracks[0].label}`)
      }

      await this.createRTCPeer()
      await this.accceptRTCPeer()
      await this.createOffers()
    },

    // 断链
    hangup() {
      console.log('Ending call')
      this.pc1.close()
      this.pc2.close()
      this.pc1 = null
      this.pc2 = null
      this.hangupButton.disabled = true
      this.callButton.disabled = false
    },

    ToIndex() {
      wx.miniProgram.navigateTo({
        url: '/pages/index/index', //小程序地址
      })
    },

    ToIndex1() {
      this.$router.push({
        path: 'index',
      })
    },
  },
}
</script>
<style scoped>
.video-window {
  width: 100%;
  height: 100%;
}
/* .video-window video {
  width: 100%;
  height: 100%;
  margin: 12px 12px 0;
} */
video {
  width: calc(100% - 48px);
  height: 200px;
  background: #000;
  margin: 24px 24px 0;
}
.btn {
  padding: 12px;
  position: absolute;
  top: 12px;
  right: 12px;
}
.box {
  width: 100%;
  height: 42px;
  text-align: center;
}
.box button {
  padding: 8px 12px;
  margin: 12px 4px;
}
</style>
</code></pre> 
  <h5 style="background-color:transparent;">WEBRTC能力测试</h5> 
  <p>TRTC 能力检测</p> 
  <h6>页面准备</h6> 
  <p>视频播放的媒介是 H5 提供的 Video(音视频)和 Audio(纯音频)。<br>  </p> 
  <pre><code class="hljs"><body >
<!-- 音视频 -->
<!--
本地视频流
muted:
本地视频流的video必须置为静音(muted),否则会出现啸叫/回声等问题
Mac / iPhone / iPad 需要用js设置muted属性
autoplay:必须为激活状态
playsinline:保证在ios safari中不全屏播放
-->
<video id="localVideo" muted autoplay playsinline></video>
<!-- 远端视频流 -->
<video id="remoteVideo" autoplay playsinline></video>
<!-- 纯音频 -->
<!-- 本地音频流 / 这种场景下,localaudio 其实没有播放的必要了,可以用来调试 -->
<!-- <audio id="localAudioMedia" muted autoplay></audio> -->
<!-- 远端音频流 -->
<!-- <audio id="remoteAudioMedia" autoplay ></audio> -->
<script src="https://sqimg.qq.com/expert_qq/webrtc/3.0/WebRTCAPI.min.js"></script>
</body></code></pre> 
  <h6>H5 支持的平台</h6> 
  <table> 
   <thead> 
    <tr> 
     <th>操作系统平台</th> 
     <th>浏览器/webview</th> 
     <th>版本要求</th> 
     <th>备注</th> 
    </tr> 
   </thead> 
   <tbody> 
    <tr> 
     <td>iOS</td> 
     <td>Safari ( 只支持Safari )</td> 
     <td>11.1.2</td> 
     <td>由于苹果 Safari 仍有偶现的 bug,产品化方案建议先规避,待苹果解决后再使用对于iOS可以考虑使用我们的小程序解决方案</td> 
    </tr> 
    <tr> 
     <td>Android</td> 
     <td>TBS (微信和手机QQ的默认Webview)</td> 
     <td>43600</td> 
     <td>微信和手机QQ默认内置的浏览器内核为TBS。TBS 介绍</td> 
    </tr> 
    <tr> 
     <td>Android</td> 
     <td>Chrome</td> 
     <td>60+</td> 
     <td>需要支持 H264</td> 
    </tr> 
    <tr> 
     <td>Mac</td> 
     <td>Chrome</td> 
     <td>47+</td> 
     <td></td> 
    </tr> 
    <tr> 
     <td>Mac</td> 
     <td>Safari</td> 
     <td>11+</td> 
     <td></td> 
    </tr> 
    <tr> 
     <td>Windows(PC)</td> 
     <td>Chrome</td> 
     <td>52+</td> 
     <td></td> 
    </tr> 
    <tr> 
     <td>Windows(PC)</td> 
     <td>QQ浏览器</td> 
     <td>10.2</td> 
     <td></td> 
    </tr> 
   </tbody> 
  </table> 
  <pre><code class="hljs">function checkTBSVersion(ua) {
//ua = "Mozilla/5.0 (Linux; Android 7.1.1; vivo X9 Build/NMF26F; wv) 
//AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.49 
//Mobile MQQBrowser/6.2 TBS/043501 Safari/537.36 
//MicroMessenger/6.5.13.1100 NetType/WIFI Language/zh_CN";
    var list = ua.split(" ");
    for (var i = 0; i < list.length; i++) {
        var item = list[i];
        if (item.indexOf("TBS") !== -1 || item.indexOf("tbs") !== -1) {
            var versionStr = item.split("/")[1];
            var version = parseInt(versionStr) || 0;
            if (version <= 43600) {
                alert("您的TBS版本号(" + versionStr + ")过低,不支持WebRTC,请升级!");
            }
        }
    }
}</code></pre> 
  <p></p> 
  <p></p> 
 </div> 
</div>
                            </div>
                        </div>
                    </div>
                    <!--PC和WAP自适应版-->
                    <div id="SOHUCS" sid="1738149596758073344"></div>
                    <script type="text/javascript" src="/views/front/js/chanyan.js"></script>
                    <!-- 文章页-底部 动态广告位 -->
                    <div class="youdao-fixed-ad" id="detail_ad_bottom"></div>
                </div>
                <div class="col-md-3">
                    <div class="row" id="ad">
                        <!-- 文章页-右侧1 动态广告位 -->
                        <div id="right-1" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad">
                            <div class="youdao-fixed-ad" id="detail_ad_1"> </div>
                        </div>
                        <!-- 文章页-右侧2 动态广告位 -->
                        <div id="right-2" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad">
                            <div class="youdao-fixed-ad" id="detail_ad_2"></div>
                        </div>
                        <!-- 文章页-右侧3 动态广告位 -->
                        <div id="right-3" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad">
                            <div class="youdao-fixed-ad" id="detail_ad_3"></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="container">
        <h4 class="pt20 mb15 mt0 border-top">你可能感兴趣的:(前端,webrtc)</h4>
        <div id="paradigm-article-related">
            <div class="recommend-post mb30">
                <ul class="widget-links">
                    <li><a href="/article/1773504513622212608.htm"
                           title="大前端-postcss安装使用指南" target="_blank">大前端-postcss安装使用指南</a>
                        <span class="text-muted">黑夜照亮前行的路</span>
<a class="tag" taget="_blank" href="/search/postcss/1.htm">postcss</a>
                        <div>PostCSS是一款强大的CSS处理工具,可以用来自动添加浏览器前缀、代码合并、代码压缩等,提升代码的可读性,并支持使用最新的CSS语法。以下是一份简化的PostCSS安装使用指南:一、安装PostCSS在你的项目目录中,通过npm(NodePackageManager)来安装PostCSS。打开命令行窗口,输入以下命令:bash复制代码npminstallpostcss--save-dev这将把</div>
                    </li>
                    <li><a href="/article/1773504261557125120.htm"
                           title="谷歌浏览器驱动Chromedriver(114-120版本)文件以及驱动下载教程" target="_blank">谷歌浏览器驱动Chromedriver(114-120版本)文件以及驱动下载教程</a>
                        <span class="text-muted">pigerr杨</span>
<a class="tag" taget="_blank" href="/search/Python/1.htm">Python</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/chrome/1.htm">chrome</a><a class="tag" taget="_blank" href="/search/drivers/1.htm">drivers</a>
                        <div>ChromeDriver官方网站GitHub||GoogleChromeLabs/chrome-for-testingChromeDriver113-125_JSONChromeforTestingavailability123-125zip白月黑羽Python基础|进阶|Qt图形界面|Django|自动化测试|性能测试|JS语言|JS前端|原理与安装</div>
                    </li>
                    <li><a href="/article/1773501994674225152.htm"
                           title="虚拟 DOM 的优缺点有哪些" target="_blank">虚拟 DOM 的优缺点有哪些</a>
                        <span class="text-muted">咕噜签名分发</span>
<a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a>
                        <div>虚拟DOM(VirtualDOM)技术作为现代前端开发中的重要组成部分,已经成为了众多流行前端框架的核心特性。它的引入为前端开发带来了诸多优势,同时也需要我们认真思考其潜在的考量。下面简单的介绍一下虚拟DOM技术的优势与缺点,深入探讨其在实际应用中的影响。提升性能虚拟DOM的最大优势之一是提升页面性能。通过比较前后两次虚拟DOM树的差异,最小化实际DOM操作,从而减少页面重渲染时的性能消耗。这种优</div>
                    </li>
                    <li><a href="/article/1773495574226599936.htm"
                           title="3、JavaWeb-Ajax/Axios-前端工程化-Element" target="_blank">3、JavaWeb-Ajax/Axios-前端工程化-Element</a>
                        <span class="text-muted">所谓远行Misnearch</span>
<a class="tag" taget="_blank" href="/search/%23/1.htm">#</a><a class="tag" taget="_blank" href="/search/JavaWeb/1.htm">JavaWeb</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/ajax/1.htm">ajax</a><a class="tag" taget="_blank" href="/search/elementui/1.htm">elementui</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF%E6%A1%86%E6%9E%B6/1.htm">前端框架</a>
                        <div>P34Ajax介绍Ajax:AsynchroousJavaScriptAndXML,异步的JS和XMLJS网页动作,XML一种标记语言,存储数据,作用:数据交换:通过Ajax给服务器发送请求,并获取服务器响应的数据异步交互:在不重新加载整个页面的情况下,与服务器交换数据并实现更新部分网页的技术,例如:搜索联想、用户名是否可用的校验等等。同步与异步:同步:服务器在处理中客户端要处于等待状态,输入域名</div>
                    </li>
                    <li><a href="/article/1773382031552610304.htm"
                           title="java实体中返回前端的double类型四舍五入(格式化)" target="_blank">java实体中返回前端的double类型四舍五入(格式化)</a>
                        <span class="text-muted">婲落ヽ紅顏誶</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a>
                        <div>根据业务,需要通过后端给前端返回部分double类型的数值,一般需要保留两位小数,使用jackson转换对象packagecom.ruoyi.common.core.config;importcom.fasterxml.jackson.core.JsonGenerator;importcom.fasterxml.jackson.databind.JsonSerializer;importcom.f</div>
                    </li>
                    <li><a href="/article/1773360885226602496.htm"
                           title="Django forms组件" target="_blank">Django forms组件</a>
                        <span class="text-muted">在飞行-米龙</span>
<a class="tag" taget="_blank" href="/search/Django/1.htm">Django</a><a class="tag" taget="_blank" href="/search/django/1.htm">django</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/%E5%90%8E%E7%AB%AF/1.htm">后端</a>
                        <div>【一】引入【1】实现登陆验证功能(1)需求分析登陆验证需要前后端交互,采用form表单提交数据对数据进行校验用户名必须以英文大写字母开头密码必须大于三位数反馈给用户错误的信息除了反馈错误的信息还有保留原始输入内容(2)后端代码使用user_info_dict字典每次刷新存储存储前端发送的信息存储后端进行验证的信息defhome(request):#每次后刷新这个信息字典user_info_dict</div>
                    </li>
                    <li><a href="/article/1773308900838277120.htm"
                           title="Web前端Html的表单" target="_blank">Web前端Html的表单</a>
                        <span class="text-muted">任家伟</span>
<a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/html/1.htm">html</a>
                        <div>表单的关键字:form标签表示一个表单区域action=“后端地址”method=“提交数据方式:get/post”input单行输入框type=“text”文本name=“定义名称名字自定义”向后端提交的键readonly=“readonly”只读,不可修改,但是可以提交disabled=“disabled”禁用组件不可修改,不能提交type=“password”密码框type=“radio”单</div>
                    </li>
                    <li><a href="/article/1773279695408791552.htm"
                           title="Thinkphp - 详细实现网站系统登录功能,附带 Mysql 数据库设置、Web 前端展示界面、信息校验等(详细代码,即设计过程)" target="_blank">Thinkphp - 详细实现网站系统登录功能,附带 Mysql 数据库设置、Web 前端展示界面、信息校验等(详细代码,即设计过程)</a>
                        <span class="text-muted">王佳斌</span>
<a class="tag" taget="_blank" href="/search/%2B/1.htm">+</a><a class="tag" taget="_blank" href="/search/Thinkphp/1.htm">Thinkphp</a><a class="tag" taget="_blank" href="/search/mysql/1.htm">mysql</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">数据库</a>
                        <div>前言登录功能,是我们几乎开发每个系统都必须的模块。登录功能设计思路,主要包括几个方面。用户输入网址展示登录页面用户输入用户名,密码等点击登录进行信息校验校验通过之后,记录用户登录信息,跳转指定页面用户校验失败,提示失败信息页面目录具体功能实现为了快速搭建可用、美观的页面,我们采用一个比较成熟的前端框架Bootstrap。下面我们到Bootstrap的官网Bootsrap官网下载bootstrap。</div>
                    </li>
                    <li><a href="/article/1772795036136701952.htm"
                           title="程序员开发技术整理" target="_blank">程序员开发技术整理</a>
                        <span class="text-muted">laizhixue</span>
<a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF%E6%A1%86%E6%9E%B6/1.htm">前端框架</a>
                        <div>前端技术:vue-前端框架element-前端框架bootstrap-前端框架echarts-图标组件C#后端技术:webservice:soap架构:简单的通信协议,用于服务通信ORM框架:对象关系映射,如EF:对象实体模型,是ado.net中的应用技术soap服务通讯:xml通讯ado.net:OAuth2:登录授权认证:Token认证:JWT:jsonwebtokenJava后端技术:便捷工</div>
                    </li>
                    <li><a href="/article/1772773132000624640.htm"
                           title="【前端学习——js篇】7.函数缓存" target="_blank">【前端学习——js篇】7.函数缓存</a>
                        <span class="text-muted">笔下无竹墨下有鱼</span>
<a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF%E5%AD%A6%E4%B9%A0/1.htm">前端学习</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a>
                        <div>具体见:https://github.com/febobo/web-interview7.函数缓存函数缓存,就是将函数运算过的结果进行缓存本质上就是用空间(缓存存储)换时间(计算过程)常用于缓存数据计算结果和缓存对象。其实现主要通过闭包、柯里化和高阶函数。下面主要介绍下柯里化:①柯里化柯里化(currying)是一种函数式编程的概念,指的是将一个带有多个参数的函数转换成一系列只接受一个参数的函数的</div>
                    </li>
                    <li><a href="/article/1772673815097180160.htm"
                           title="Websocket服务监听收发消息" target="_blank">Websocket服务监听收发消息</a>
                        <span class="text-muted">beiback</span>
<a class="tag" taget="_blank" href="/search/Java/1.htm">Java</a><a class="tag" taget="_blank" href="/search/%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%97%AE%E9%A2%98/1.htm">服务器问题</a><a class="tag" taget="_blank" href="/search/websocket/1.htm">websocket</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/1.htm">网络协议</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E7%BB%9C/1.htm">网络</a>
                        <div>目录1.pom依赖坐标2.项目配置端口和项目包名2.创建处理器3.注册处理器4.前端页面1.pom依赖坐标org.springframework.bootspring-boot-starter-websocket2.项目配置端口和项目包名application.propertiesserver.port=8088//路径规范:为应用的所有servlet提供一个统一的前缀,使URL结构更加清晰和一致</div>
                    </li>
                    <li><a href="/article/1772673816238030848.htm"
                           title="Netty服务器结合WebSocke协议监听和接收数据" target="_blank">Netty服务器结合WebSocke协议监听和接收数据</a>
                        <span class="text-muted">beiback</span>
<a class="tag" taget="_blank" href="/search/%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%97%AE%E9%A2%98/1.htm">服务器问题</a><a class="tag" taget="_blank" href="/search/Java/1.htm">Java</a><a class="tag" taget="_blank" href="/search/%E6%9C%8D%E5%8A%A1%E5%99%A8/1.htm">服务器</a><a class="tag" taget="_blank" href="/search/%E8%BF%90%E7%BB%B4/1.htm">运维</a><a class="tag" taget="_blank" href="/search/netty/1.htm">netty</a>
                        <div>目录1.pom依赖2.配置属性3.创建netty服务器4.建立监听和响应5.创建启动器6.前端static下页面7.前端js8.注意异常问题9.创建netty服务器--使用守护线程1.pom依赖io.nettynetty-all4.1.86.Final2.配置属性application.properties#启动端口server.port=8088server.servlet.context-pa</div>
                    </li>
                    <li><a href="/article/1772654174425645056.htm"
                           title="基于SSM+Vue企业销售培训系统 企业人才培训系统 企业课程培训管理系统 企业文化培训班系统Java" target="_blank">基于SSM+Vue企业销售培训系统 企业人才培训系统 企业课程培训管理系统 企业文化培训班系统Java</a>
                        <span class="text-muted">计算机程序老哥</span>

                        <div>作者主页:计算机毕业设计老哥有问题可以主页问我一、开发介绍1.1开发环境开发语言:Java数据库:MySQL系统架构:B/S后端:SSM(Spring+SpringMVC+Mybatis)前端:Vue工具:IDEA或者Eclipse,JDK1.8,Maven二、系统介绍2.1图片展示注册登录页面:登陆.png前端页面功能:首页、培训班、在线学习、企业文化、交流论坛、试卷列表、系统公告、留言反馈、个</div>
                    </li>
                    <li><a href="/article/1772631263593693184.htm"
                           title="javascript实现SM2加密解密" target="_blank">javascript实现SM2加密解密</a>
                        <span class="text-muted">人生在勤,不索何获</span>
<a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/jquery/1.htm">jquery</a>
                        <div>前提JavaWeb环境前端代码window.sm2=function(t){functioni(e){if(r[e])returnr[e].exports;varn=r[e]={i:e,l:!1,exports:{}};returnt[e].call(n.exports,n,n.exports,i),n.l=!0,n.exports}varr={};returni.m=t,i.c=r,i.d=fu</div>
                    </li>
                    <li><a href="/article/1772541266194661376.htm"
                           title="前端埋点解决方案" target="_blank">前端埋点解决方案</a>
                        <span class="text-muted">zhu_zhu_xia</span>
<a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a>
                        <div>一、前言:基于神策数据的前端埋点解决方案JavaScript快速使用·神策分析使用手册[预览版]二、sdkgitlab下载地址https://github.com/sensorsdata/sa-sdk-javascript/releases或者npm安装npmisa-sdk-javascript三、入门3.1接入sdk以及配置(version1.17.2),入口文件接入sdk以及添加配置(func</div>
                    </li>
                    <li><a href="/article/1772399131164213248.htm"
                           title="如何提出令人爱回答的好问题?" target="_blank">如何提出令人爱回答的好问题?</a>
                        <span class="text-muted">兮若耶</span>

                        <div>我们经常会遇到这样的问题,如我适合做什么?这个名词怎么解释?大部分人面对这样的问题时,要么答非所问,要么无从下手。现在的很多事物都是速成的,只是好的问题并没有那么容易被提出来。而提不好的问题,可能会拿不到想要的信息等等。所以提出一个好问题很重要。01提问的功能我们参加各种聚会、会议时,能听到很多的比喻和新观点,而这些是在书上和网上找不到的。这些新的有用的东西,都在前端被实践着,暂时来不及把知识系统</div>
                    </li>
                    <li><a href="/article/1772378002940821504.htm"
                           title="谈谈对前端性能监控的理解和实践" target="_blank">谈谈对前端性能监控的理解和实践</a>
                        <span class="text-muted">Layla_c</span>
<a class="tag" taget="_blank" href="/search/web/1.htm">web</a><a class="tag" taget="_blank" href="/search/jave/1.htm">jave</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a>
                        <div>一、谈谈对前端性能监控的理解和实践前端性能监控是确保网页或应用高效、稳定运行的关键环节,它涉及对前端页面加载速度、资源消耗、错误率等指标的实时监控和预警。通过前端性能监控,开发者和运维团队能够及时发现并解决性能瓶颈,从而提升用户体验和系统稳定性。理解前端性能监控,首先要明确其重要性。在移动互联网时代,用户对网页和应用的响应速度有着极高的要求。如果页面加载缓慢或出现卡顿,用户可能会选择离开,这对企业</div>
                    </li>
                    <li><a href="/article/1772365540988354560.htm"
                           title="mineadmin使用docker启动方式" target="_blank">mineadmin使用docker启动方式</a>
                        <span class="text-muted">qq_38812523</span>
<a class="tag" taget="_blank" href="/search/docker/1.htm">docker</a><a class="tag" taget="_blank" href="/search/php/1.htm">php</a><a class="tag" taget="_blank" href="/search/%E5%AE%B9%E5%99%A8/1.htm">容器</a>
                        <div>找个目录,git下来mineadmin代码,在根目录,创建文件名docker-compose.yml然后复制下面代码version:'3'services:#首先下载前端,https://gitee.com/mineadmin/mineadmin-vue#在后端根目录建立mine-ui目录,把前端文件复制过来。#容器内访问宿主机的地址用:host.docker.internal#宿主机也可以在ho</div>
                    </li>
                    <li><a href="/article/1772335078576291840.htm"
                           title="为什么需要使用版本控制工具(如Git)?它如何帮助管理前端开发项目?" target="_blank">为什么需要使用版本控制工具(如Git)?它如何帮助管理前端开发项目?</a>
                        <span class="text-muted">智伴科技</span>
<a class="tag" taget="_blank" href="/search/git/1.htm">git</a>
                        <div>版本控制工具(如Git)在前端开发项目中扮演着重要的角色,主要有以下几方面的作用:1.**版本管理**:版本控制工具可以帮助开发团队管理项目的不同版本,记录每次代码变动的历史记录,方便追踪和回溯。开发人员可以通过版本控制工具轻松地查看、对比和恢复以前的版本。2.**协同合作**:多人开发同一个项目时,版本控制工具可以协助团队成员协同工作,避免代码冲突、重复工作和混乱。开发人员可以通过版本控制工具共</div>
                    </li>
                    <li><a href="/article/1772307893329133568.htm"
                           title="低代码与前端开发架构:重塑软件开发的未来" target="_blank">低代码与前端开发架构:重塑软件开发的未来</a>
                        <span class="text-muted">快乐非自愿</span>
<a class="tag" taget="_blank" href="/search/%E4%BD%8E%E4%BB%A3%E7%A0%81/1.htm">低代码</a><a class="tag" taget="_blank" href="/search/%E6%9E%B6%E6%9E%84/1.htm">架构</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a>
                        <div>随着技术的不断进步和数字化转型的深入,软件开发领域正经历着一场革命性的变革。在这场变革中,低代码开发平台和前端开发架构扮演着越来越重要的角色。本文将探讨低代码与前端开发架构之间的关系,并分析它们如何共同推动软件开发的创新与发展。低代码开发平台的崛起低代码开发平台(Low-CodeDevelopmentPlatform,LCDP)是一种新型的软件开发方式,它允许开发者通过图形化界面、预构建的模块和模</div>
                    </li>
                    <li><a href="/article/1772083971480420352.htm"
                           title="接口测试之测试原则、测试用例、测试流程......" target="_blank">接口测试之测试原则、测试用例、测试流程......</a>
                        <span class="text-muted">程序员老鹰</span>
<a class="tag" taget="_blank" href="/search/%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7/1.htm">测试工具</a><a class="tag" taget="_blank" href="/search/%E5%8A%9F%E8%83%BD%E6%B5%8B%E8%AF%95/1.htm">功能测试</a><a class="tag" taget="_blank" href="/search/%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B/1.htm">测试用例</a><a class="tag" taget="_blank" href="/search/%E6%B5%8B%E8%AF%95%E8%A6%86%E7%9B%96%E7%8E%87/1.htm">测试覆盖率</a><a class="tag" taget="_blank" href="/search/%E7%B3%BB%E7%BB%9F%E5%AE%89%E5%85%A8/1.htm">系统安全</a>
                        <div>一、接口的介绍软件测试中,常说的接口有两种:图形用户接口(GUI,人与程序的接口)、应用程序编程接口(API)。接口(API)是系统与系统之间,模块与模块之间或者服务与服务之间相互调用的入口。它的本质:其实就是一种约定,在开发前期,我们约定接口会接收什么数据;在处理完成后,它又会返回什么数据。开发岗位分为前端和后端,他们相互配合完成工作,会协商接口的定义方法。一般后端定义接口,前端调用接口。前后端</div>
                    </li>
                    <li><a href="/article/1772015996245180416.htm"
                           title="【前端】CommonJS和ES Module" target="_blank">【前端】CommonJS和ES Module</a>
                        <span class="text-muted">Lucky小维</span>
<a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a>
                        <div>区别语法差异:CommonJS:使用require()导入模块,使用module.exports或exports导出模块。ESModule:使用import导入模块,使用export导出模块。编译时vs运行时:CommonJS是在运行时加载模块,模块代码是动态执行的。ESModule是在编译时静态解析模块依赖关系,以便更好地进行优化和静态分析。异步加载:CommonJS不支持异步加载模块,只能同步</div>
                    </li>
                    <li><a href="/article/1771980751349284864.htm"
                           title="基于python+vue高校毕业生离校管理系统flask-django-php-nodejs" target="_blank">基于python+vue高校毕业生离校管理系统flask-django-php-nodejs</a>
                        <span class="text-muted">QQ511008285</span>
<a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/vue.js/1.htm">vue.js</a><a class="tag" taget="_blank" href="/search/flask/1.htm">flask</a><a class="tag" taget="_blank" href="/search/django/1.htm">django</a><a class="tag" taget="_blank" href="/search/node.js/1.htm">node.js</a><a class="tag" taget="_blank" href="/search/php/1.htm">php</a>
                        <div>课题主要采用Uni-weixin、django架构技术,前端以小程序页面呈现给用户,结合后台java语言使页面更加完善,后台使用MySQL数据库进行数据存储。微信小程序主要包括学生、教务人员、宿管员、图书管理员、财务人员、离校申请、物流信息、钥匙归还、图片归还、欠费信息、催缴信息等功能,从而实现智能化的管理方式,提高工作效率。关键字:高校毕业生离校管理系统;django框架;MySQL数据库语言:</div>
                    </li>
                    <li><a href="/article/1771974961003560960.htm"
                           title="Vue项目使用process.env关键字及Vue.config.js配置解决前端跨域问题" target="_blank">Vue项目使用process.env关键字及Vue.config.js配置解决前端跨域问题</a>
                        <span class="text-muted">百思不得小李</span>
<a class="tag" taget="_blank" href="/search/JS%E5%AE%9E%E6%88%98%E8%AE%B0%E5%BD%95/1.htm">JS实战记录</a><a class="tag" taget="_blank" href="/search/vue2%E5%AE%9E%E6%88%98%E8%AE%B0%E5%BD%95/1.htm">vue2实战记录</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/vue.js/1.htm">vue.js</a>
                        <div>1.process.env是Node.js中的一个环境1.打开命令行查看环境:2.process.env与VueCLI项目VueCli有以下三种运行模式development模式用于vue-cli-serviceservetest模式用于vue-cli-servicetest:unitproduction模式用于vue-cli-servicebuild和vue-cli-servicetest:e2</div>
                    </li>
                    <li><a href="/article/1771963631177629696.htm"
                           title="浅谈前端路由history和hash的理解" target="_blank">浅谈前端路由history和hash的理解</a>
                        <span class="text-muted">怂怂敲代码</span>
<a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/%E5%93%88%E5%B8%8C%E7%AE%97%E6%B3%95/1.htm">哈希算法</a><a class="tag" taget="_blank" href="/search/%E7%AE%97%E6%B3%95/1.htm">算法</a>
                        <div>hash和history在前端面试中是很常考的一道题目,可能很多人对于history和hash的理解差异性就是,他们两者的url字段一个没有#号一个有#号,但是有没有想过为什么这样呢,有无#号又有什么差异呢,这篇文章谈一谈我对前端路由history和hash的理解。hash和history都可以用于前后端分离项目,且两者有各自的特点和各自的使用场景。一、前端路由原理1、SPASPA,即单页面应用(</div>
                    </li>
                    <li><a href="/article/1771630580518158336.htm"
                           title="Yarn 管理的前端项目转换为使用 npm" target="_blank">Yarn 管理的前端项目转换为使用 npm</a>
                        <span class="text-muted">577wq</span>
<a class="tag" taget="_blank" href="/search/node.js/1.htm">node.js</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/npm/1.htm">npm</a><a class="tag" taget="_blank" href="/search/node.js/1.htm">node.js</a>
                        <div>如果你想将一个使用Yarn管理的前端项目转换为使用npm,你需要执行一些步骤来确保成功迁移。以下是一种可能的方法:步骤:备份项目:在执行任何更改之前,确保你对项目进行了备份。这样可以防止意外的数据丢失。删除yarn.lock文件:在项目根目录中,删除yarn.lock文件。这个文件记录了使用Yarn安装的确切的依赖版本信息。修改package.json文件:打开项目的package.json文件,</div>
                    </li>
                    <li><a href="/article/1771628691311362048.htm"
                           title="accessToken" target="_blank">accessToken</a>
                        <span class="text-muted">星梦清河</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/spring/1.htm">spring</a><a class="tag" taget="_blank" href="/search/boot/1.htm">boot</a><a class="tag" taget="_blank" href="/search/%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB/1.htm">经验分享</a><a class="tag" taget="_blank" href="/search/redis/1.htm">redis</a>
                        <div>1、介绍accessToken,通常是用于身份验证和授权的令牌,它可以用于前端和后端,具体使用方式取决于应用程序的架构和需求。前端应用accessToken通常用于向后端API发送请求时进行身份验证和授权。(1)前端应用程序会在用户登录成功后获取accessToken;(2)并将accessToken存储在本地;(3)然后在每次请求API时,将accessToken作为请求头或参数发送给后端;(4</div>
                    </li>
                    <li><a href="/article/1771565373594861568.htm"
                           title="python社区垃圾分类管理平台的设计与实现flask-django-php-nodejs" target="_blank">python社区垃圾分类管理平台的设计与实现flask-django-php-nodejs</a>
                        <span class="text-muted">QQ_511008285</span>
<a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/flask/1.htm">flask</a><a class="tag" taget="_blank" href="/search/django/1.htm">django</a><a class="tag" taget="_blank" href="/search/vue.js/1.htm">vue.js</a><a class="tag" taget="_blank" href="/search/php/1.htm">php</a><a class="tag" taget="_blank" href="/search/node.js/1.htm">node.js</a>
                        <div>近些年来,随着科技的飞速发展,互联网的普及逐渐延伸到各行各业中,给人们生活带来了十分的便利,社区垃圾分类管理平台利用计算机网络实现信息化管理,使整个社区垃圾分类管理的发展和服务水平有显著提升。语言:Python框架:django/flask软件版本:python3.7.7数据库:mysql数据库工具:Navicat前端框架:vue.js通过比较两个不同因素的框架,可以看出Flask和Django不</div>
                    </li>
                    <li><a href="/article/1770965590861742080.htm"
                           title="springboot项目学习-瑞吉外卖(1)" target="_blank">springboot项目学习-瑞吉外卖(1)</a>
                        <span class="text-muted">两仪式quq</span>
<a class="tag" taget="_blank" href="/search/spring/1.htm">spring</a><a class="tag" taget="_blank" href="/search/boot/1.htm">boot</a><a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E7%91%9E%E5%90%89%E5%A4%96%E5%8D%96/1.htm">瑞吉外卖</a>
                        <div>第一天任务如下:建立基本架构完成登录、退出功能注意:本博客没有使用网上教程里的mybatis-plus,使用的是mybatis;数据库连接池也没有使用教程里的druid,使用的是spring自带的连接池基本架构common包:存放的通用类R,用来给前端返回Json格式的数据config包:存放配置类,在今天的任务中主要解决静态资源路径映射问题controller包entity包:存放实体类mapp</div>
                    </li>
                    <li><a href="/article/1770938278791413760.htm"
                           title="前端测试方法" target="_blank">前端测试方法</a>
                        <span class="text-muted">gyqJulius_Caesar</span>
<a class="tag" taget="_blank" href="/search/C%2FC%2B%2B%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/1.htm">C/C++程序设计</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/%E5%8F%AF%E7%94%A8%E6%80%A7%E6%B5%8B%E8%AF%95/1.htm">可用性测试</a><a class="tag" taget="_blank" href="/search/%E5%8E%8B%E5%8A%9B%E6%B5%8B%E8%AF%95/1.htm">压力测试</a><a class="tag" taget="_blank" href="/search/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/1.htm">单元测试</a><a class="tag" taget="_blank" href="/search/%E5%8A%9F%E8%83%BD%E6%B5%8B%E8%AF%95/1.htm">功能测试</a><a class="tag" taget="_blank" href="/search/%E6%A8%A1%E5%9D%97%E6%B5%8B%E8%AF%95/1.htm">模块测试</a><a class="tag" taget="_blank" href="/search/%E9%9B%86%E6%88%90%E6%B5%8B%E8%AF%95/1.htm">集成测试</a>
                        <div>记录一些测试方法。对于前端工程师,测试是开发过程中不可或缺的环节。而其中两种最基本、最常见的测试类型就是“冒烟测试”和“全量测试”。1.冒烟测试众所周知,冒烟这个词是源自汽车行业的。新造出来的车辆要经过“冒烟测试”,以确保所有系统(包括电池、传动系统、制动系统等)都正常运转,或者说没有什么大问题。同样地,软件开发也会进行类似的测试。在软件开发中,“冒烟测试”通常是指对代码库中的主要功能点进行快速测</div>
                    </li>
                                <li><a href="/article/16.htm"
                                       title="深入浅出Java Annotation(元注解和自定义注解)" target="_blank">深入浅出Java Annotation(元注解和自定义注解)</a>
                                    <span class="text-muted">Josh_Persistence</span>
<a class="tag" taget="_blank" href="/search/Java+Annotation/1.htm">Java Annotation</a><a class="tag" taget="_blank" href="/search/%E5%85%83%E6%B3%A8%E8%A7%A3/1.htm">元注解</a><a class="tag" taget="_blank" href="/search/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3/1.htm">自定义注解</a>
                                    <div>一、基本概述 
  
  
   Annontation是Java5开始引入的新特征。中文名称一般叫注解。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。 
  
  更通俗的意思是为程序的元素(类、方法、成员变量)加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且是供指定的工具或</div>
                                </li>
                                <li><a href="/article/143.htm"
                                       title="mysql优化特定类型的查询" target="_blank">mysql优化特定类型的查询</a>
                                    <span class="text-muted">annan211</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E5%B7%A5%E4%BD%9C/1.htm">工作</a><a class="tag" taget="_blank" href="/search/mysql/1.htm">mysql</a>
                                    <div>

本节所介绍的查询优化的技巧都是和特定版本相关的,所以对于未来mysql的版本未必适用。

1 优化count查询
  对于count这个函数的网上的大部分资料都是错误的或者是理解的都是一知半解的。在做优化之前我们先来看看
  真正的count()函数的作用到底是什么。
  count()是一个特殊的函数,有两种非常不同的作用,他可以统计某个列值的数量,也可以统计行数。
  在统</div>
                                </li>
                                <li><a href="/article/270.htm"
                                       title="MAC下安装多版本JDK和切换几种方式" target="_blank">MAC下安装多版本JDK和切换几种方式</a>
                                    <span class="text-muted">棋子chessman</span>
<a class="tag" taget="_blank" href="/search/jdk/1.htm">jdk</a>
                                    <div>环境: 
MAC AIR,OS X 10.10,64位 
  
历史: 
过去 Mac 上的 Java 都是由 Apple 自己提供,只支持到 Java 6,并且OS X 10.7 开始系统并不自带(而是可选安装)(原自带的是1.6)。 
后来 Apple 加入 OpenJDK 继续支持 Java 6,而 Java 7 将由 Oracle 负责提供。 
  
在终端中输入jav</div>
                                </li>
                                <li><a href="/article/397.htm"
                                       title="javaScript (1)" target="_blank">javaScript (1)</a>
                                    <span class="text-muted">Array_06</span>
<a class="tag" taget="_blank" href="/search/JavaScript/1.htm">JavaScript</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E6%B5%8F%E8%A7%88%E5%99%A8/1.htm">浏览器</a>
                                    <div>JavaScript 
 
1、运算符 
  运算符就是完成操作的一系列符号,它有七类:   赋值运算符(=,+=,-=,*=,/=,%=,<<=,>>=,|=,&=)、算术运算符(+,-,*,/,++,--,%)、比较运算符(>,<,<=,>=,==,===,!=,!==)、逻辑运算符(||,&&,!)、条件运算(?:)、位</div>
                                </li>
                                <li><a href="/article/524.htm"
                                       title="国内顶级代码分享网站" target="_blank">国内顶级代码分享网站</a>
                                    <span class="text-muted">袁潇含</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/jdk/1.htm">jdk</a><a class="tag" taget="_blank" href="/search/oracle/1.htm">oracle</a><a class="tag" taget="_blank" href="/search/.net/1.htm">.net</a><a class="tag" taget="_blank" href="/search/PHP/1.htm">PHP</a>
                                    <div>       现在国内很多开源网站感觉都是为了利益而做的 
  
        
       当然利益是肯定的,否则谁也不会免费的去做网站 
  
   &</div>
                                </li>
                                <li><a href="/article/651.htm"
                                       title="Elasticsearch、MongoDB和Hadoop比较" target="_blank">Elasticsearch、MongoDB和Hadoop比较</a>
                                    <span class="text-muted">随意而生</span>
<a class="tag" taget="_blank" href="/search/mongodb/1.htm">mongodb</a><a class="tag" taget="_blank" href="/search/hadoop/1.htm">hadoop</a><a class="tag" taget="_blank" href="/search/%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E/1.htm">搜索引擎</a>
                                    <div>    
IT界在过去几年中出现了一个有趣的现象。很多新的技术出现并立即拥抱了“大数据”。稍微老一点的技术也会将大数据添进自己的特性,避免落大部队太远,我们看到了不同技术之间的边际的模糊化。假如你有诸如Elasticsearch或者Solr这样的搜索引擎,它们存储着JSON文档,MongoDB存着JSON文档,或者一堆JSON文档存放在一个Hadoop集群的HDFS中。你可以使用这三种配</div>
                                </li>
                                <li><a href="/article/778.htm"
                                       title="mac os 系统科研软件总结" target="_blank">mac os 系统科研软件总结</a>
                                    <span class="text-muted">张亚雄</span>
<a class="tag" taget="_blank" href="/search/mac+os/1.htm">mac os</a>
                                    <div>1.1 Microsoft Office for Mac 2011 
     大客户版,自行搜索。 
     1.2 Latex (MacTex): 
     系统环境:https://tug.org/mactex/ 
    &nb</div>
                                </li>
                                <li><a href="/article/905.htm"
                                       title="Maven实战(四)生命周期" target="_blank">Maven实战(四)生命周期</a>
                                    <span class="text-muted">AdyZhang</span>
<a class="tag" taget="_blank" href="/search/maven/1.htm">maven</a>
                                    <div>1. 三套生命周期     Maven拥有三套相互独立的生命周期,它们分别为clean,default和site。 每个生命周期包含一些阶段,这些阶段是有顺序的,并且后面的阶段依赖于前面的阶段,用户和Maven最直接的交互方式就是调用这些生命周期阶段。 以clean生命周期为例,它包含的阶段有pre-clean, clean 和 post</div>
                                </li>
                                <li><a href="/article/1032.htm"
                                       title="Linux下Jenkins迁移" target="_blank">Linux下Jenkins迁移</a>
                                    <span class="text-muted">aijuans</span>
<a class="tag" taget="_blank" href="/search/Jenkins/1.htm">Jenkins</a>
                                    <div>1. 将Jenkins程序目录copy过去       源程序在/export/data/tomcatRoot/ofctest-jenkins.jd.com下面                tar -cvzf jenkins.tar.gz ofctest-jenkins.jd.com &</div>
                                </li>
                                <li><a href="/article/1159.htm"
                                       title="request.getInputStream()只能获取一次的问题" target="_blank">request.getInputStream()只能获取一次的问题</a>
                                    <span class="text-muted">ayaoxinchao</span>
<a class="tag" taget="_blank" href="/search/request/1.htm">request</a><a class="tag" taget="_blank" href="/search/Inputstream/1.htm">Inputstream</a>
                                    <div>问题:在使用HTTP协议实现应用间接口通信时,服务端读取客户端请求过来的数据,会用到request.getInputStream(),第一次读取的时候可以读取到数据,但是接下来的读取操作都读取不到数据        
原因:   1. 一个InputStream对象在被读取完成后,将无法被再次读取,始终返回-1;   2. InputStream并没有实现reset方法(可以重</div>
                                </li>
                                <li><a href="/article/1286.htm"
                                       title="数据库SQL优化大总结之 百万级数据库优化方案" target="_blank">数据库SQL优化大总结之 百万级数据库优化方案</a>
                                    <span class="text-muted">BigBird2012</span>
<a class="tag" taget="_blank" href="/search/SQL%E4%BC%98%E5%8C%96/1.htm">SQL优化</a>
                                    <div>网上关于SQL优化的教程很多,但是比较杂乱。近日有空整理了一下,写出来跟大家分享一下,其中有错误和不足的地方,还请大家纠正补充。 
这篇文章我花费了大量的时间查找资料、修改、排版,希望大家阅读之后,感觉好的话推荐给更多的人,让更多的人看到、纠正以及补充。 
1.对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。 
2.应尽量避免在 where </div>
                                </li>
                                <li><a href="/article/1413.htm"
                                       title="jsonObject的使用" target="_blank">jsonObject的使用</a>
                                    <span class="text-muted">bijian1013</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/json/1.htm">json</a>
                                    <div>        在项目中难免会用java处理json格式的数据,因此封装了一个JSONUtil工具类。 
JSONUtil.java 
package com.bijian.json.study;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;</div>
                                </li>
                                <li><a href="/article/1540.htm"
                                       title="[Zookeeper学习笔记之六]Zookeeper源代码分析之Zookeeper.WatchRegistration" target="_blank">[Zookeeper学习笔记之六]Zookeeper源代码分析之Zookeeper.WatchRegistration</a>
                                    <span class="text-muted">bit1129</span>
<a class="tag" taget="_blank" href="/search/zookeeper/1.htm">zookeeper</a>
                                    <div>Zookeeper类是Zookeeper提供给用户访问Zookeeper service的主要API,它包含了如下几个内部类 
  
  
首先分析它的内部类,从WatchRegistration开始,为指定的znode path注册一个Watcher, 
  
    /**
     * Register a watcher for a particular p</div>
                                </li>
                                <li><a href="/article/1667.htm"
                                       title="【Scala十三】Scala核心七:部分应用函数" target="_blank">【Scala十三】Scala核心七:部分应用函数</a>
                                    <span class="text-muted">bit1129</span>
<a class="tag" taget="_blank" href="/search/scala/1.htm">scala</a>
                                    <div>何为部分应用函数? 
Partially applied function: A function that’s used in an expression and that misses some of its arguments.For instance, if function f has type Int => Int => Int, then f and f(1) are p</div>
                                </li>
                                <li><a href="/article/1794.htm"
                                       title="Tomcat Error listenerStart 终极大法" target="_blank">Tomcat Error listenerStart 终极大法</a>
                                    <span class="text-muted">ronin47</span>
<a class="tag" taget="_blank" href="/search/tomcat/1.htm">tomcat</a>
                                    <div>Tomcat报的错太含糊了,什么错都没报出来,只提示了Error listenerStart。为了调试,我们要获得更详细的日志。可以在WEB-INF/classes目录下新建一个文件叫logging.properties,内容如下 
 
Java代码  
handlers = org.apache.juli.FileHandler, java.util.logging.ConsoleHa</div>
                                </li>
                                <li><a href="/article/1921.htm"
                                       title="不用加减符号实现加减法" target="_blank">不用加减符号实现加减法</a>
                                    <span class="text-muted">BrokenDreams</span>
<a class="tag" taget="_blank" href="/search/%E5%AE%9E%E7%8E%B0/1.htm">实现</a>
                                    <div>        今天有群友发了一个问题,要求不用加减符号(包括负号)来实现加减法。 
        分析一下,先看最简单的情况,假设1+1,按二进制算的话结果是10,可以看到从右往左的第一位变为0,第二位由于进位变为1。 
   </div>
                                </li>
                                <li><a href="/article/2048.htm"
                                       title="读《研磨设计模式》-代码笔记-状态模式-State" target="_blank">读《研磨设计模式》-代码笔记-状态模式-State</a>
                                    <span class="text-muted">bylijinnan</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/1.htm">设计模式</a>
                                    <div>声明: 本文只为方便我个人查阅和理解,详细的分析以及源代码请移步 原作者的博客http://chjavach.iteye.com/ 
 
 




/*

当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类
状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况
把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化

如果在</div>
                                </li>
                                <li><a href="/article/2175.htm"
                                       title="CUDA程序block和thread超出硬件允许值时的异常" target="_blank">CUDA程序block和thread超出硬件允许值时的异常</a>
                                    <span class="text-muted">cherishLC</span>
<a class="tag" taget="_blank" href="/search/CUDA/1.htm">CUDA</a>
                                    <div>调用CUDA的核函数时指定block 和 thread大小,该大小可以是dim3类型的(三维数组),只用一维时可以是usigned int型的。 
以下程序验证了当block或thread大小超出硬件允许值时会产生异常!!!GPU根本不会执行运算!!! 
所以验证结果的正确性很重要!!! 
在VS中创建CUDA项目会有一个模板,里面有更详细的状态验证。 
 
 
以下程序在K5000GPU上跑的。</div>
                                </li>
                                <li><a href="/article/2302.htm"
                                       title="诡异的超长时间GC问题定位" target="_blank">诡异的超长时间GC问题定位</a>
                                    <span class="text-muted">chenchao051</span>
<a class="tag" taget="_blank" href="/search/jvm/1.htm">jvm</a><a class="tag" taget="_blank" href="/search/cms/1.htm">cms</a><a class="tag" taget="_blank" href="/search/GC/1.htm">GC</a><a class="tag" taget="_blank" href="/search/hbase/1.htm">hbase</a><a class="tag" taget="_blank" href="/search/swap/1.htm">swap</a>
                                    <div>HBase的GC策略采用PawNew+CMS, 这是大众化的配置,ParNew经常会出现停顿时间特别长的情况,有时候甚至长到令人发指的地步,例如请看如下日志: 
2012-10-17T05:54:54.293+0800: 739594.224: [GC 739606.508: [ParNew: 996800K->110720K(996800K), 178.8826900 secs] 3700</div>
                                </li>
                                <li><a href="/article/2429.htm"
                                       title="maven环境快速搭建" target="_blank">maven环境快速搭建</a>
                                    <span class="text-muted">daizj</span>
<a class="tag" taget="_blank" href="/search/%E5%AE%89%E8%A3%85/1.htm">安装</a><a class="tag" taget="_blank" href="/search/mavne/1.htm">mavne</a><a class="tag" taget="_blank" href="/search/%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/1.htm">环境配置</a>
                                    <div>一 下载maven 
 
安装maven之前,要先安装jdk及配置JAVA_HOME环境变量。这个安装和配置java环境不用多说。 
 
maven下载地址:http://maven.apache.org/download.html,目前最新的是这个apache-maven-3.2.5-bin.zip,然后解压在任意位置,最好地址中不要带中文字符,这个做java 的都知道,地址中出现中文会出现很多</div>
                                </li>
                                <li><a href="/article/2556.htm"
                                       title="PHP网站安全,避免PHP网站受到攻击的方法" target="_blank">PHP网站安全,避免PHP网站受到攻击的方法</a>
                                    <span class="text-muted">dcj3sjt126com</span>
<a class="tag" taget="_blank" href="/search/PHP/1.htm">PHP</a>
                                    <div>  
对于PHP网站安全主要存在这样几种攻击方式:1、命令注入(Command Injection)2、eval注入(Eval Injection)3、客户端脚本攻击(Script Insertion)4、跨网站脚本攻击(Cross Site Scripting, XSS)5、SQL注入攻击(SQL injection)6、跨网站请求伪造攻击(Cross Site Request Forgerie</div>
                                </li>
                                <li><a href="/article/2683.htm"
                                       title="yii中给CGridView设置默认的排序根据时间倒序的方法" target="_blank">yii中给CGridView设置默认的排序根据时间倒序的方法</a>
                                    <span class="text-muted">dcj3sjt126com</span>
<a class="tag" taget="_blank" href="/search/GridView/1.htm">GridView</a>
                                    <div>public function searchWithRelated() { 
        $criteria = new CDbCriteria; 
 
        $criteria->together = true; //without th</div>
                                </li>
                                <li><a href="/article/2810.htm"
                                       title="Java集合对象和数组对象的转换" target="_blank">Java集合对象和数组对象的转换</a>
                                    <span class="text-muted">dyy_gusi</span>
<a class="tag" taget="_blank" href="/search/java%E9%9B%86%E5%90%88/1.htm">java集合</a>
                                    <div>    在开发中,我们经常需要将集合对象(List,Set)转换为数组对象,或者将数组对象转换为集合对象。Java提供了相互转换的工具,但是我们使用的时候需要注意,不能乱用滥用。 
1、数组对象转换为集合对象 
    最暴力的方式是new一个集合对象,然后遍历数组,依次将数组中的元素放入到新的集合中,但是这样做显然过</div>
                                </li>
                                <li><a href="/article/2937.htm"
                                       title="nginx同一主机部署多个应用" target="_blank">nginx同一主机部署多个应用</a>
                                    <span class="text-muted">geeksun</span>
<a class="tag" taget="_blank" href="/search/nginx/1.htm">nginx</a>
                                    <div>近日有一需求,需要在一台主机上用nginx部署2个php应用,分别是wordpress和wiki,探索了半天,终于部署好了,下面把过程记录下来。 
1.   在nginx下创建vhosts目录,用以放置vhost文件。 
mkdir vhosts 
  
2.   修改nginx.conf的配置, 在http节点增加下面内容设置,用来包含vhosts里的配置文件 
#</div>
                                </li>
                                <li><a href="/article/3064.htm"
                                       title="ubuntu添加admin权限的用户账号" target="_blank">ubuntu添加admin权限的用户账号</a>
                                    <span class="text-muted">hongtoushizi</span>
<a class="tag" taget="_blank" href="/search/ubuntu/1.htm">ubuntu</a><a class="tag" taget="_blank" href="/search/useradd/1.htm">useradd</a>
                                    <div>ubuntu创建账号的方式通常用到两种:useradd 和adduser .   本人尝试了useradd方法,步骤如下:  
1:useradd 
   使用useradd时,如果后面不加任何参数的话,如:sudo useradd sysadm 创建出来的用户将是默认的三无用户:无home directory ,无密码,无系统shell。 
顾应该如下操作: 
  </div>
                                </li>
                                <li><a href="/article/3191.htm"
                                       title="第五章 常用Lua开发库2-JSON库、编码转换、字符串处理" target="_blank">第五章 常用Lua开发库2-JSON库、编码转换、字符串处理</a>
                                    <span class="text-muted">jinnianshilongnian</span>
<a class="tag" taget="_blank" href="/search/nginx/1.htm">nginx</a><a class="tag" taget="_blank" href="/search/lua/1.htm">lua</a>
                                    <div>   JSON库 
  
在进行数据传输时JSON格式目前应用广泛,因此从Lua对象与JSON字符串之间相互转换是一个非常常见的功能;目前Lua也有几个JSON库,本人用过cjson、dkjson。其中cjson的语法严格(比如unicode \u0020\u7eaf),要求符合规范否则会解析失败(如\u002),而dkjson相对宽松,当然也可以通过修改cjson的源码来完成</div>
                                </li>
                                <li><a href="/article/3318.htm"
                                       title="Spring定时器配置的两种实现方式OpenSymphony Quartz和java Timer详解" target="_blank">Spring定时器配置的两种实现方式OpenSymphony Quartz和java Timer详解</a>
                                    <span class="text-muted">yaerfeng1989</span>
<a class="tag" taget="_blank" href="/search/timer/1.htm">timer</a><a class="tag" taget="_blank" href="/search/quartz/1.htm">quartz</a><a class="tag" taget="_blank" href="/search/%E5%AE%9A%E6%97%B6%E5%99%A8/1.htm">定时器</a>
                                    <div>原创整理不易,转载请注明出处:Spring定时器配置的两种实现方式OpenSymphony Quartz和java Timer详解 
代码下载地址:http://www.zuidaima.com/share/1772648445103104.htm 
有两种流行Spring定时器配置:Java的Timer类和OpenSymphony的Quartz。 
1.Java Timer定时 
首先继承jav</div>
                                </li>
                                <li><a href="/article/3445.htm"
                                       title="Linux下df与du两个命令的差别?" target="_blank">Linux下df与du两个命令的差别?</a>
                                    <span class="text-muted">pda158</span>
<a class="tag" taget="_blank" href="/search/linux/1.htm">linux</a>
                                    <div> 一、df显示文件系统的使用情况,与du比較,就是更全盘化。     最经常使用的就是 df -T,显示文件系统的使用情况并显示文件系统的类型。     举比例如以下:     [root@localhost ~]# df -T     Filesystem                   Type &n</div>
                                </li>
                                <li><a href="/article/3572.htm"
                                       title="[转]SQLite的工具类 ---- 通过反射把Cursor封装到VO对象" target="_blank">[转]SQLite的工具类 ---- 通过反射把Cursor封装到VO对象</a>
                                    <span class="text-muted">ctfzh</span>
<a class="tag" taget="_blank" href="/search/VO/1.htm">VO</a><a class="tag" taget="_blank" href="/search/android/1.htm">android</a><a class="tag" taget="_blank" href="/search/sqlite/1.htm">sqlite</a><a class="tag" taget="_blank" href="/search/%E5%8F%8D%E5%B0%84/1.htm">反射</a><a class="tag" taget="_blank" href="/search/Cursor/1.htm">Cursor</a>
                                    <div>在写DAO层时,觉得从Cursor里一个一个的取出字段值再装到VO(值对象)里太麻烦了,就写了一个工具类,用到了反射,可以把查询记录的值装到对应的VO里,也可以生成该VO的List。 
  
使用时需要注意:   
考虑到Android的性能问题,VO没有使用Setter和Getter,而是直接用public的属性。  
表中的字段名需要和VO的属性名一样,要是不一样就得在查询的SQL中</div>
                                </li>
                                <li><a href="/article/3699.htm"
                                       title="该学习笔记用到的Employee表" target="_blank">该学习笔记用到的Employee表</a>
                                    <span class="text-muted">vipbooks</span>
<a class="tag" taget="_blank" href="/search/oracle/1.htm">oracle</a><a class="tag" taget="_blank" href="/search/sql/1.htm">sql</a><a class="tag" taget="_blank" href="/search/%E5%B7%A5%E4%BD%9C/1.htm">工作</a>
                                    <div>    这是我在学习Oracle是用到的Employee表,在该笔记中用到的就是这张表,大家可以用它来学习和练习。 
 
 

drop table Employee;
-- 员工信息表
create table Employee(
       -- 员工编号
       EmpNo number(3) primary key,
       -- 姓</div>
                                </li>
                </ul>
            </div>
        </div>
    </div>

<div>
    <div class="container">
        <div class="indexes">
            <strong>按字母分类:</strong>
            <a href="/tags/A/1.htm" target="_blank">A</a><a href="/tags/B/1.htm" target="_blank">B</a><a href="/tags/C/1.htm" target="_blank">C</a><a
                href="/tags/D/1.htm" target="_blank">D</a><a href="/tags/E/1.htm" target="_blank">E</a><a href="/tags/F/1.htm" target="_blank">F</a><a
                href="/tags/G/1.htm" target="_blank">G</a><a href="/tags/H/1.htm" target="_blank">H</a><a href="/tags/I/1.htm" target="_blank">I</a><a
                href="/tags/J/1.htm" target="_blank">J</a><a href="/tags/K/1.htm" target="_blank">K</a><a href="/tags/L/1.htm" target="_blank">L</a><a
                href="/tags/M/1.htm" target="_blank">M</a><a href="/tags/N/1.htm" target="_blank">N</a><a href="/tags/O/1.htm" target="_blank">O</a><a
                href="/tags/P/1.htm" target="_blank">P</a><a href="/tags/Q/1.htm" target="_blank">Q</a><a href="/tags/R/1.htm" target="_blank">R</a><a
                href="/tags/S/1.htm" target="_blank">S</a><a href="/tags/T/1.htm" target="_blank">T</a><a href="/tags/U/1.htm" target="_blank">U</a><a
                href="/tags/V/1.htm" target="_blank">V</a><a href="/tags/W/1.htm" target="_blank">W</a><a href="/tags/X/1.htm" target="_blank">X</a><a
                href="/tags/Y/1.htm" target="_blank">Y</a><a href="/tags/Z/1.htm" target="_blank">Z</a><a href="/tags/0/1.htm" target="_blank">其他</a>
        </div>
    </div>
</div>
<footer id="footer" class="mb30 mt30">
    <div class="container">
        <div class="footBglm">
            <a target="_blank" href="/">首页</a> -
            <a target="_blank" href="/custom/about.htm">关于我们</a> -
            <a target="_blank" href="/search/Java/1.htm">站内搜索</a> -
            <a target="_blank" href="/sitemap.txt">Sitemap</a> -
            <a target="_blank" href="/custom/delete.htm">侵权投诉</a>
        </div>
        <div class="copyright">版权所有 IT知识库 CopyRight © 2000-2050 E-COM-NET.COM , All Rights Reserved.
<!--            <a href="https://beian.miit.gov.cn/" rel="nofollow" target="_blank">京ICP备09083238号</a><br>-->
        </div>
    </div>
</footer>
<!-- 代码高亮 -->
<script type="text/javascript" src="/static/syntaxhighlighter/scripts/shCore.js"></script>
<script type="text/javascript" src="/static/syntaxhighlighter/scripts/shLegacy.js"></script>
<script type="text/javascript" src="/static/syntaxhighlighter/scripts/shAutoloader.js"></script>
<link type="text/css" rel="stylesheet" href="/static/syntaxhighlighter/styles/shCoreDefault.css"/>
<script type="text/javascript" src="/static/syntaxhighlighter/src/my_start_1.js"></script>





</body>

</html>