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

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

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

 

第一部分: 代碼案例css

 

下面,我這裏是基於一個Github上的例子,作了些許調整,爲了本身的項目作準備的。這裏,重點不是說如何經過H5獲取Audio數據,重點是說這個過程當中涉及的坑或者技術元素知識。直接上代碼!html

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>

頁面效果以下:java

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

2.1 main.jsweb

//=======================================================================
//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.jsajax

(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 CSSspring

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 內部服務器錯誤
相關文章
相關標籤/搜索