HTML5操作麥克風獲取音頻數據(WAV)的一些基礎技能


基於HTML5的新特性,操作其實思路很簡單。

首先通過navigator獲取設備,然后通過設備監聽語音數據,進行原始數據采集。 相關的案例比較多,最典型的就是鏈接:https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API

 

第一部分: 代碼案例

 

下面,我這里是基於一個Github上的例子,做了些許調整,為了自己的項目做准備的。這里,重點不是說如何通過H5獲取Audio數據,重點是說這個過程中涉及的坑或者技術元素知識。直接上代碼!

1. HTML測試頁面

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta name="apple-mobile-web-capable" content="yes">
    <title>語音轉寫</title>    
    <link rel="stylesheet" type="text/css" href="css/style.css"/>
</head>
<body>
<div id="container">
    <div id="player">
        <h1>Voice Robot</h1>        
        <button id="btn-start-recording" onclick="startRecording();">錄音</button>
        <button id="btn-stop-recording" disabled onclick="stopRecording();">轉寫</button>
        <button id="btn-start-palying" disabled onclick="playRecording();">播放</button>                        
        <div id="inbo">
            <div id="change"></div>        
        </div>
        <input type="hidden" id="audiolength"> 
        <hr>
        <audio id="audioSave" controls autoplay></audio>        
        <textarea id="btn-text-content" class="text-content">你好啊</textarea>        
    </div>    
</div>
<script type="text/javascript" src="js/jquery-1.11.1.js"></script>
<script type="text/javascript" src="js/HZRecorder.js"></script>
<script src="js/main.js"></script>
</body>
</html>

頁面效果如下:

2. JS代碼(分為兩個部分,main.js,以及recorder.js)

2.1 main.js

//=======================================================================
//author: shihuc
//date: 2018-09-19
//動態獲取服務地址
//=======================================================================
var protocol = window.location.protocol;
var baseService = window.location.host;
var pathName = window.location.pathname;
var projectName = pathName.substring(0,pathName.substr(1).indexOf('/')+1);

var protocolStr = document.location.protocol;
var baseHttpProtocol = "http://";
if(protocolStr == "https:") {  
  baseHttpProtocol = "https://";
}
var svrUrl =  baseHttpProtocol + baseService + projectName + "/audio/trans";
//=========================================================================
  
var recorder = null;
var startButton = document.getElementById('btn-start-recording');
var stopButton = document.getElementById('btn-stop-recording');
var playButton = document.getElementById('btn-start-palying');

//var audio = document.querySelector('audio');
var audio = document.getElementById('audioSave');

function startRecording() {
    if(recorder != null) {
        recorder.close();
    }
    Recorder.get(function (rec) {
        recorder = rec;
        recorder.start();
    });
    stopButton.disabled = false;    
    playButton.disabled = false;
}

function stopRecording() {
    recorder.stop();    
    recorder.trans(svrUrl, function(res, errcode){
      if(errcode != 500){
        alert(res);
      }
    });
}

function playRecording() {
    recorder.play(audio);
}

 

2.2 reocrder.js

(function (window) {  
    //兼容  
    window.URL = window.URL || window.webkitURL;  
    //請求麥克風
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;  
  
    var Recorder = function (stream, config) {  
        //創建一個音頻環境對象  
        audioContext = window.AudioContext || window.webkitAudioContext;  
        var context = new audioContext();  
        
        config = config || {};  
        config.channelCount = 1;
        config.numberOfInputChannels = config.channelCount;
        config.numberOfOutputChannels = config.channelCount;
        config.sampleBits = config.sampleBits || 16;      //采樣數位 8, 16  
        //config.sampleRate = config.sampleRate || (context.sampleRate / 6);   //采樣率(1/6 44100)
        config.sampleRate = config.sampleRate || 8000;   //采樣率16K
        //創建緩存,用來緩存聲音  
        config.bufferSize = 4096;
        
        //將聲音輸入這個對像  
        var audioInput = context.createMediaStreamSource(stream);  
          
        //設置音量節點  
        var volume = context.createGain();
        audioInput.connect(volume);  
  
        // 創建聲音的緩存節點,createScriptProcessor方法的  
        // 第二個和第三個參數指的是輸入和輸出都是聲道數。
        var recorder = context.createScriptProcessor(config.bufferSize, config.channelCount, config.channelCount); 
         
        //用來儲存讀出的麥克風數據,和壓縮這些數據,將這些數據轉換為WAV文件的格式
        var audioData = {  
            size: 0          //錄音文件長度  
            , buffer: []     //錄音緩存  
            , inputSampleRate: context.sampleRate    //輸入采樣率  
            , inputSampleBits: 16                    //輸入采樣數位 8, 16  
            , outputSampleRate: config.sampleRate    //輸出采樣率  
            , oututSampleBits: config.sampleBits     //輸出采樣數位 8, 16  
            , input: function (data) {  
                this.buffer.push(new Float32Array(data));  //Float32Array
                this.size += data.length;  
            }  
            , getRawData: function () { //合並壓縮  
                //合並  
                var data = new Float32Array(this.size);  
                var offset = 0;  
                for (var i = 0; i < this.buffer.length; i++) {
                    data.set(this.buffer[i], offset);  
                    offset += this.buffer[i].length;  
                }  
                //壓縮
                var getRawDataion = parseInt(this.inputSampleRate / this.outputSampleRate);  
                var length = data.length / getRawDataion;  
                var result = new Float32Array(length);  
                var index = 0, j = 0;  
                while (index < length) {  
                    result[index] = data[j];  
                    j += getRawDataion;  
                    index++;  
                }  
                return result;
            }             
            ,getFullWavData: function(){
              var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);  
              var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);  
              var bytes = this.getRawData();  
              var dataLength = bytes.length * (sampleBits / 8);  
              var buffer = new ArrayBuffer(44 + dataLength);  
              var data = new DataView(buffer);  
              var offset = 0;  
              var writeString = function (str) {  
                for (var i = 0; i < str.length; i++) {  
                    data.setUint8(offset + i, str.charCodeAt(i));  
                }  
              };  
              // 資源交換文件標識符   
              writeString('RIFF'); offset += 4;  
              // 下個地址開始到文件尾總字節數,即文件大小-8   
              data.setUint32(offset, 36 + dataLength, true); offset += 4;  
              // WAV文件標志  
              writeString('WAVE'); offset += 4;  
              // 波形格式標志   
              writeString('fmt '); offset += 4;  
              // 過濾字節,一般為 0x10 = 16   
              data.setUint32(offset, 16, true); offset += 4;  
              // 格式類別 (PCM形式采樣數據)   
              data.setUint16(offset, 1, true); offset += 2;  
              // 通道數   
              data.setUint16(offset, config.channelCount, true); offset += 2;  
              // 采樣率,每秒樣本數,表示每個通道的播放速度   
              data.setUint32(offset, sampleRate, true); offset += 4;  
              // 波形數據傳輸率 (每秒平均字節數) 單聲道×每秒數據位數×每樣本數據位/8   
              data.setUint32(offset, config.channelCount * sampleRate * (sampleBits / 8), true); offset += 4;  
              // 快數據調整數 采樣一次占用字節數 單聲道×每樣本的數據位數/8   
              data.setUint16(offset, config.channelCount * (sampleBits / 8), true); offset += 2;  
              // 每樣本數據位數   
              data.setUint16(offset, sampleBits, true); offset += 2;  
              // 數據標識符   
              writeString('data'); offset += 4;  
              // 采樣數據總數,即數據總大小-44   
              data.setUint32(offset, dataLength, true); offset += 4; 
              // 寫入采樣數據   
              data = this.reshapeWavData(sampleBits, offset, bytes, data);
// var wavd = new Int8Array(data.buffer.byteLength); // var pos = 0; // for (var i = 0; i < data.buffer.byteLength; i++, pos++) { // wavd[i] = data.getInt8(pos); // } 
// return wavd;

return new Blob([data], { type: 'audio/wav' }); } ,closeContext:function(){ context.close(); //關閉AudioContext否則錄音多次會報錯。 } ,getPureWavData: function(offset) { var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits) var bytes = this.getRawData(); var dataLength = bytes.length * (sampleBits / 8); var buffer = new ArrayBuffer(dataLength); var data = new DataView(buffer); data = this.reshapeWavData(sampleBits, offset, bytes, data); // var wavd = new Int8Array(data.buffer.byteLength); // var pos = 0; // for (var i = 0; i < data.buffer.byteLength; i++, pos++) { // wavd[i] = data.getInt8(pos); // }
// return wavd;
                  return new Blob([data], { type: 'audio/wav' });

} ,reshapeWavData: function(sampleBits, offset, iBytes, oData) { if (sampleBits === 8) { for (var i = 0; i < iBytes.length; i++, offset++) { var s = Math.max(-1, Math.min(1, iBytes[i])); var val = s < 0 ? s * 0x8000 : s * 0x7FFF; val = parseInt(255 / (65535 / (val + 32768))); oData.setInt8(offset, val, true); } } else { for (var i = 0; i < iBytes.length; i++, offset += 2) { var s = Math.max(-1, Math.min(1, iBytes[i])); oData.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); } } return oData; } }; //開始錄音 this.start = function () { audioInput.connect(recorder); recorder.connect(context.destination); }; //停止 this.stop = function () { recorder.disconnect(); }; //獲取音頻文件 this.getBlob = function () { this.stop(); return audioData.getFullWavData(); }; //回放 this.play = function (audio) { audio.src = window.URL.createObjectURL(this.getBlob()); audio.onended = function() { $('#play').text("Play"); }; }; //停止播放 this.stopPlay=function(audio){ audio.pause(); } this.close=function(){ audioData.closeContext(); } //上傳 this.upload = function (url, pdata, callback) { var fd = new FormData(); fd.append('file', this.getBlob()); var xhr = new XMLHttpRequest(); for (var e in pdata) fd.append(e, pdata[e]); if (callback) { xhr.upload.addEventListener('progress', function (e) { callback('uploading', e); }, false); xhr.addEventListener('load', function (e) { callback('ok', e); }, false); xhr.addEventListener('error', function (e) { callback('error', e); }, false); xhr.addEventListener('abort', function (e) { callback('cancel', e); }, false); } xhr.open('POST', url); xhr.send(fd); }; this.trans = function (url, callback) { var fd = new FormData(); var buffer = audioData.getPureWavData(0); fd.set('wavData', buffer); fd.set('wavSize', buffer.size); console.log("wavSize: " + buffer.size); document.getElementById('btn-text-content').value = "當前錄音長度為:" + buffer.size; var xhr = new XMLHttpRequest(); xhr.open('POST', url, false); //async=false,采用同步方式處理 xhr.onreadystatechange = function(){ if (xhr.readyState == 4) { //響應數據接收完畢 callback(xhr.responseText, xhr.status); } } xhr.send(fd); }; var $bo=$("#inbo"); var $change=$("#change"); var width=$bo.width(); //音頻采集 recorder.onaudioprocess = function (e) { audioData.input(e.inputBuffer.getChannelData(0)); //獲取輸入和輸出的數據緩沖區 var input = e.inputBuffer.getChannelData(0); //繪制條形波動圖 for(i=0;i<width;i++){ var changeWidth=width/2*input[input.length*i/width|0]; $change.width(changeWidth); } var timeHidden=document.getElementById('audiolength'); timeHidden.Value=e.playbackTime; console.log(timeHidden.Value); if(timeHidden.Value>=60){ recorder.disconnect(); setTimeout(saveAudio(),500); } }; }; //拋出異常 Recorder.throwError = function (message) { throw new function () { this.toString = function () { return message; };}; }; //是否支持錄音 Recorder.canRecording = (navigator.getUserMedia != null); //獲取錄音機 Recorder.get = function (callback, config) { if (callback) { if (navigator.getUserMedia) { navigator.getUserMedia( { audio: true } //只啟用音頻 A , function (stream) { //stream這個參數是麥克風的輸入流,將這個流傳遞給Recorder var rec = new Recorder(stream, config); callback(rec); } , function (error) { switch (error.code || error.name) { case 'PERMISSION_DENIED': case 'PermissionDeniedError': Recorder.throwError('用戶拒絕提供信息。'); break; case 'NOT_SUPPORTED_ERROR': case 'NotSupportedError': Recorder.throwError('瀏覽器不支持硬件設備。'); break; case 'MANDATORY_UNSATISFIED_ERROR': case 'MandatoryUnsatisfiedError': Recorder.throwError('無法發現指定的硬件設備。'); break; default: Recorder.throwError('無法打開麥克風。異常信息:' + (error.code || error.name)); break; } }); } else { Recorder.throwErr('當前瀏覽器不支持錄音功能。'); return; } } }; window.Recorder = Recorder; })(window);

2.3 CSS

body {
    margin: 0;
    background: #f0f0f0;
    font-family:  'Roboto', Helvetica, Arial, sans-serif;
}

#container {
    margin-top: 30px;
}

h1 {
    margin: 0;
}

button {
    padding: 10px;
    background: #eee;
    border: none;
    border-radius: 3px;
    color: #ffffff;
    font-family: inherit;
    font-size: 16px;
    outline: none !important;
    cursor: pointer;
}

button[disabled] {
    background: #aaa !important;
    cursor: default;
}

#btn-start-recording {
    background: #5db85c;
}

#btn-stop-recording {
    background: #d95450;
}

#btn-start-palying {
    background: #d95450;
}

#btn-start-saving {
    background: #d95450;
}

#player {
    max-width: 600px;
    margin: 0 auto;
    padding: 20px 20px;
    border: 1px solid #ddd;
    background: #ffffff;
}

.text-content {    
    margin: 20px auto;
    resize:none;
    background: #dbdbdb;
    width: 100%;
    font-size: 14px;
    padding:5px 5px;
    border-radius: 5px;
    min-height: 100px;
    box-sizing: border-box;
}

audio {
    width: 100%;
}

#inbo{
    width: 100%;
    height: 20px;
    border: 1px solid #ccc;
    margin-top: 20px;
}
#change{
    height: 20px;
    width: 0;
    background-color: #009933;
}
View Code

 

小結: 僅僅就這個案例來看,需要注意幾點

A. 這個例子將采集的數據WAV格式,傳遞到服務端(http),瀏覽器需求是要用HTTPS的協議

B. 傳遞數據,若直接用上面JS文件中紅色部分代碼進行傳遞,而不是用基於Blob的數據進行傳,會出現數據轉碼錯誤,這個錯誤是邏輯錯誤,不會遇到exception。 所謂邏輯錯誤,是原始的數據,被轉換成了ASCII碼了,即被當字符串信息了。參照下面的這個截圖:

49其實是ASCII的1,50其實是ASCII的2,依次類推,之所以發現這個問題,是因為研究數據長度,即JS前端顯示的A長度,但是服務端顯示的缺是比A長很多的值,但是baos.toString顯示的內容和JS前端顯示的數字內容一樣。。。仔細一研究,發現上述總結的問題。后面還會介紹,XMLHttpRequest傳遞數據給后台時,Blob相關的數據其妙效果!!!

 

下面進入第二部分:知識點的總結

1. FormData

FormData對象用以將數據編譯成鍵值對,以便用XMLHttpRequest來發送數據。其主要用於發送表單數據,但亦可用於發送帶鍵數據(keyed data),而獨立於表單使用。如果表單enctype屬性設為multipart/form-data ,則會使用表單的submit()方法來發送數據,從而,發送數據具有同樣形式。

語法:
var formData = new FormData(form)
參數form是Optional

An HTML <form> element — when specified, the FormData object will be populated with the form's current keys/values using the name property of each element for the keys and their submitted value for the values. It will also encode file input content.

創建一個新的表單對象,其中form來源於一個html的form標簽,這個form參數,可以非必填。
關於FormData類型的方法,可以參照下面的連接https://developer.mozilla.org/zh-CN/docs/Web/API/FormData自行閱讀,非常清楚。

 

通常情況下,FormData的創建,有兩種形式:
1》從零開始創建FormData對象
你可以自己創建一個FormData對象,然后調用它的append()方法來添加字段,像這樣:

var formData = new FormData();
formData.append("username", "Groucho");
formData.append("accountnum", 123456); //數字123456會被立即轉換成字符串 "123456"
// HTML 文件類型input,由用戶選擇
formData.append("userfile", fileInputElement.files[0]);
// JavaScript file-like 對象
var content = '<a id="a"><b id="b">hey!</b></a>'; // 新文件的正文...
var blob = new Blob([content], { type: "text/xml"});
formData.append("webmasterfile", blob);
var request = new XMLHttpRequest();
request.open("POST", "http://foo.com/submitform.php");
request.send(formData);

注意:

A> 字段 "userfile" 和 "webmasterfile" 都包含一個文件. 字段 "accountnum" 是數字類型,它將被FormData.append()方法轉換成字符串類型(FormData 對象的字段類型可以是 Blob, File, 或者 string: 如果它的字段類型不是Blob也不是File,則會被轉換成字符串類)
B> 一個 Blob對象表示一個不可變的, 原始數據的類似文件對象。Blob表示的數據不一定是一個JavaScript原生格式。 File 接口基於Blob,繼承 blob功能並將其擴展為支持用戶系統上的文件。你可以通過 Blob() 構造函數創建一個Blob對象

 

2》通過HTML表單創建FormData對象
想要構造一個包含Form表單數據的FormData對象,需要在創建FormData對象時指定表單的元素。

<form id="myForm" action="" method="post" enctype="multipart/form-data">
<input type="text" name="param1">參數1
<input type="text" name="param2">參數2 
<input type="file" name="param3">參數3 
</form>

然后看如何操作表單form元素構建FormData:

var formElement = document.getElementById("myForm");;
var request = new XMLHttpRequest();
request.open("POST", svrUrl);
var formData = new FormData(formElement);
request.send(formData);

注意:

A> 這里基於表單元素form進行構建FormData對象,然后提交了帶有文件類型的數據到后台,這里enctype,必須是multipart/form-data,表單必須指定。enctype屬性規定在將表單數據發送到服務器之前如何對其進行編碼
B>form標簽中,只有method="post"時才使用enctype屬性。enctype常見的類型值

application/x-www-form-urlencoded          默認。在發送前對所有字符進行編碼(將空格轉換為 "+" 符號,特殊字符轉換為 ASCII HEX 值)。


multipart/form-data              不對字符編碼。當使用有文件上傳控件的表單時,該值是必需的。


text/plain                  將空格轉換為 "+" 符號,但不編碼特殊字符。

C>如果FormData對象是通過表單創建的,則表單中指定的請求方式會被應用到方法open()中
D>你還可以直接向FormData對象附加File或Blob類型的文件,如下所示:
formData.append("myfile", myBlob, "filename.txt");
使用append()方法時,可以通過第三個可選參數設置發送請求的頭 Content-Disposition 指定文件名。如果不指定文件名(或者不支持該參數時),將使用名字“blob”。
如果你設置正確的配置項,你也可以通過jQuery來使用FormData對象:

var fd = new FormData(document.querySelector("form"));
fd.append("CustomField", "This is some extra data");
$.ajax({
   url: "stash.php",
   type: "POST",
   data: fd,
   processData: false, // 不處理數據
   contentType: false // 不設置內容類型
});

E>通過AJAX提交表單和上傳文件可以不使用FormData對象

 

 

 

2. XMLHttpRequest請求

下面看看官方文檔的描述:

Use XMLHttpRequest (XHR) objects to interact with servers. You can retrieve data from a URL without having to do a full page refresh. This enables a Web page to update just part of a page without disrupting what the user is doing. XMLHttpRequest is used heavily in Ajax programming.
Despite its name, XMLHttpRequest can be used to retrieve any type of data, not just XML, and it supports protocols other than HTTP (including file and ftp).
If your communication needs involve receiving event or message data from the server, consider using server-sent events through the EventSource interface. For full-duplex communication, WebSockets may be a better choice.

相應的詳細描述,請參考鏈接https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

1》下面說說常用的幾個函數:
a. onreadystatechange
XMLHttpRequest.onreadystatechange = callback;
下面看看例子:

var xhr = new XMLHttpRequest(), method = "GET", url = "https://developer.mozilla.org/";
xhr.open(method, url, true);
xhr.onreadystatechange = function () {
   if(xhr.readyState === 4 && xhr.status === 200) {
       console.log(xhr.responseText);
   }
};
xhr.send();

注意:服務端怎么能給出responseText呢?或者其他響應參數。其實,還是蠻簡單的,只要搞清楚http的工作流程,不要受到springMVC或者Jersey等MVC框架迷惑,其實這些高端框架,也是對http的數據流進行了封裝,因為HTTP流程,數據都是有一個請求HttpServletRequest和一個HttpServletResponse對應的,一個對應請求一個對應響應。響應就是服務端給到客戶端的應答,所以,我們給輸出的時候,一定要注意,直接操作HttpServletResponse時,是進行數據流操作。類似下面的一段例子:

PrintWriter out = response.getWriter();
out.print(text);
out.flush();//一定要有這個操作,否則數據不會發出去,停留在buffer中。

 

b. open

The XMLHttpRequest method open() initializes a newly-created request, or re-initializes an existing one.

注意:Calling this method for an already active request (one for which open() has already been called) is the equivalent of calling abort(). 意思是說,對一個已經開啟的request,在沒有結束時,再次調用open,等效於調用abort進行中斷了

語法:

XMLHttpRequest.open(method, url)
XMLHttpRequest.open(method, url, async)
XMLHttpRequest.open(method, url, async, user)
XMLHttpRequest.open(method, url, async, user, password)

說明: 

I) The HTTP request method to use, such as "GET", "POST", "PUT", "DELETE", etc. Ignored for non-HTTP(S) URLs. 注意,只支持HTTP系列請求,其他將被忽視掉
II) method和url是必填項,async是可選的,默認是true,表示open啟動的請求默認是異步的

 

c. send

The XMLHttpRequest method send() sends the request to the server. If the request is asynchronous (which is the default), this method returns as soon as the request is sent and the result is delivered using events. If the request is synchronous, this method doesn't return until the response has arrived.
send() accepts an optional parameter which lets you specify the request's body; this is primarily used for requests such as PUT. If the request method is GET or HEAD, the body parameter is ignored and the request body is set to null.
If no Accept header has been set using the setRequestHeader(), an Accept header with the type "*/*" (any type) is sent.

語法:

XMLHttpRequest.send(body)

注意:The best way to send binary content (e.g. in file uploads) is by using an ArrayBufferView or Blob in conjunction with the send() method.

下面看看ArrayBufferView對應的內容:

I) ArrayBufferView is a helper type representing any of the following JavaScript TypedArray types:

Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array or
DataView.

我的項目經驗告知我,用ArrayBufferView傳遞數據的話,基於FormData傳遞,會存在將原始數據轉成字符串的效果,這個也是符合FormData技術介紹的,如前面的注意事項內容。 所以,為了方便,強烈建議數據(二進制)文件的傳遞,用Blob類型,保持原始數據格式,不會轉碼

II)看看Blob的內容

A Blob object represents a file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system.
To construct a Blob from other non-blob objects and data, use the Blob() constructor. To create a blob that contains a subset of another blob's data, use the slice() method. To obtain a Blob object for a file on the user's file system, see the File documentation.

 

2》獲取響應

responseText: 獲得字符串形式的響應數據 
responseXML: 獲得XML形式的響應數據(用的較少,大多數情況用JSON) 
status, statusText: 以數字和文本形式返回HTTP狀態碼 
getAllResponseHeader(): 獲取所有的響應報頭 
getResponseHeader(參數): 查詢響應中某個字段的值

 

3》屬性

readyState: 響應是否成功 
0:請求為初始化,open還沒有調用 
1:服務器連接已建立,open已經調用了 
2:請求已接收,接收到頭信息了 
3:請求處理中,接收到響應主題了 
4:請求已完成,且響應已就緒,也就是響應完成了 

 

4》另附http請求相應代碼

200 請求成功
202 請求被接受但處理未完成
204 接收到空的響應
400 錯誤請求
404 請求資源未找到
500 內部服務器錯誤

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM