2021/09/27

WebRTC API

Web Real-Time Communication API 是提供網頁進行視訊或語音串流通訊的技術,可以直接使用瀏覽器進行資料交換。WebRTC 可提供瀏覽器在不需要安裝外掛程式或第三方軟體下,分享應用程式的資料和進行電話會議。

因為 WebRTC 還在發展當中,各 browser 有不同程度的 codec 支援以及 WebRTC 功能。故建議使用 adapter ,這是 Google 提供,減少跨瀏覽器差異的 library。

browser compatability matrix 是不同瀏覽器的相容表。

WebRTC 有多種功能,包含 audio, video conferencing,檔案交換,screen sharing,identity management,支援傳統交換機的 DTMF signals,兩端點之間的連線不需要特殊的 drivers/plugins,也可不需要中間 server。

兩個 peers 之間是以 RTCPeerConnection 產生連線,當連線建立後,就能夠使用 MediaStream 或 RTCDataChannel。

MediaStream 包含了數個 media information tracks,是由 MediaStreamTrack 介面表示的物件,可包含多種 media data: audio, video, text,大部分的 streams 包含至少一條 audio track,一條 video track,可用來傳送 live media,或將 media 存檔。

也可以用 RTCDataChannel 介面在兩端之間,傳遞任意的 binray data,這邊可用來傳送 channel-information, 交換 metadata,game status, file transfer。

WebRTC References

Connection setup and management

Interfaces

Interfaces Description
RTCPeerConnection local 與 remote peer 之間的 WebRTC connection
RTCDataChannel connection 的兩個 peers 之間的 bi-directional data channel
RTCDataChannelEvent 將 RTCDataChannel 接到 RTCPerrConnection 的 event
RTCSessionDescription session 的參數,裡面包含了 description type,代表它所描述的部分 offer/answer negotiation process 以及該 seesion的 SDP descriptor
RTCStatsReport connection 或是 tack on connection 的統計資訊。以 RTCPeerConnection.getStats() 取得。使用 WebRTC 的統計資料有另外的 WebRTC Statistics API
RTCIceCandidate 代表一個可用來建立 RTCPeerConnection 的 candidate ICE server
RTCIceTransport ICE transport 的資訊
RTCPeerConnectionIceEvent 在 RTCPeerConnection 中,有關 ICE candidates 的相關 events。event 是使用 type: icecandidate
RTCRtpSender 在 RTCPeerConnection 裡面的 MediaStreamTrack,用來encoding 傳輸資料
RTCRtpReceiver 在 RTCPeerConnection 裡面的 MediaStreamTrack,用來decoding 傳輸資料
RTCTrackEvent track event 介面。通常是 RTCRtpReceiver 接到 RTCPeerConnection 時發生,代表 RTCPeerConenction 產生了新的 MediaStreamTrack
RTCSctpTransport SCTP transport 的資訊,可在 RTCPeerConnection 的 data channel 收送資料時,透過 SCTP packets,存取Datagram Transport Layer Security(DTLS)

Dictionaries

Dictionaries Description
RTCConfiguration RTCPeerConnection 的 conifguration options
RTCIceServer 定義如何連接 ICE Sever,例如 Stun or Turn server
RTCRtpContributingSource 有關 contributing source (SCRC) 的資訊,包含該 source 被播放了一個最新的 packet 的 most recent time

ex:

var configuration = { iceServers: [{
                          urls: "stun:stun.services.mozilla.com",
                          username: "louis@mozilla.com",
                          credential: "webrtcdemo"
                      }, {
                          urls: ["stun:stun.example.com", "stun:stun-1.example.com"]
                      }]
};

var pc = new RTCPeerConnection(configuration);

Events

Events Description
bufferedamountlow data channel 目前 buffered 的資料量,也就是 bufferedAmount property 已經低於 bufferedAmountLowThreshold 這個 minimum buffered data size
close data channel 已結束 process,進入 closed state,data transport 已經完全 closed,在沒有完全 closed 的時候,會發送 closing event
closing RTCDataChannel 已轉換到 closing state,如果完全停止則是 close event
connectionstatechange 連線狀態(connectionState) 改變
datachannel 有新的 RTCDataChannel 可以使用,事件類別為 RTCDataChannelEvent
error data channel 發生錯誤 RTCErrorEvent
error RTCDtlsTransport 發生錯誤 RTCErrorEvent,可能是 dtls-failure 或是 fingerprint-failure
gatheringstatechange RTCIceTransport 的 gathering state 已改變
icecandidate RTCPeerConnectonIceEvent
當 local device 識別到一個新的 ICE candidate,並需要 local peer 去呼叫 setLocalDescription() 時,會產生此 event
icecandidateerror RTCPeerConnectionIceErrorEvent
在 gathering ICE candidates 發生錯誤時
iceconnectionstatechange 當 ICE gathering state (屬性 icegatheringstate) 改變時,會發送給 RTCPeerConnection
message data channel 收到一個訊息,event type 為 MessageEvent
negotiationneeded 通知 RTCPeerconnection 需要處理 session negotiation,可在 setLocalDescription() 後,呼叫 createOffer()
open RTCDataChannel 的 data transport 已經 opened 或 re-opened
selectedcandidatepairchane RTCIceTransport 目前選用的 ICE candidates 改變
track 當新的 track 在成功協調 media streaming 後,會產生 RTCTrackevent 發送給 RTCPeerConnection
signalingstatechange 當 signalingstate 改變時,送給 peer connection。這會發生在呼叫 setLocalDescription() 或 setRemoteDescription() 之後
statechange RTCDtlsTransport 的狀態改變
statechange RTCIceTransport 的狀態改變
statechange RTCSctpTransport 的狀態改變

Types

Types Description
RTCSctpTransport.state 代表 RTCSctpTrnasport instance 的 state
RTCSessionDescriptionCallback 當要求 create offers 或 answers 時,RTCSessionDescriptionCallback 會傳給 RTCPeerConnection

Identity and security

管理 identity and security,用在連線的認證

identity Description
RTCIdentityProvider user agent,可要求處理 identity assertion
RTCIdentityAssertion 代表 current connection 的 identity of the remote peer。如果目前沒有 peer,就會回傳 null,一但被設定,就不能被修改
RTCIdentityProviderRegistrar Registers and identity provider (idP)
RTCIdentityEvent 代表 identity provider (idP) 產生了 identity assertion。event type 為 identityresult
RTCIdentityErrorEvent 代表 identity provider (idP) 產生了 error。event type 為 idpassertionerror 或 idpvalidationerror
RTCertificate RTCPeerConnection 用來認證的 certificate

Telephony

PSTN 使用的 dialing tone

Interfaces

Interfaces Description
RTCDTMFSender 發送 DTMF signaling
RTCDTMFToneChangeEvent tonechange event 用來表示 DTMF 開始或結束

Events

Events Description
tonechange 當connection 開始播放 DTMF tone,或是 RTCDTMFSender 的 toneBuffer 已經送完時,會產生 event type: RTCDTMFToneChangeEvent

WebRTC Connectivity

Signaling

WebRTC 必須搭配 signaling service 才能使用。用來交換 SDP 資訊,也就是 offer/answer 資料

  • Peer A init connection 產生 Offer
  • 透過 signal channel 發送 Offer 給 Peer B
  • Peer B 接收 Offer,產生 Answer
  • 透過 signal channel 回送 Answer

Session Descriptions

configuration of an endpoint on a WebRTC connection 就稱為 session description

以 SDP 描述,裡面包含 media type, format, transfer protocol, endpoint's IP/Port, 其他資訊

media data exchange 是以 Interactive Connectivity Establishment (ICE) 處理,該協定可讓兩個在 NAT 下的 devices 使用 intermediary 交換 offers, answers

每個 peer 都留存兩個 description:localdescription 及 remote description

當建立 call 或是 某一端要修改 configuration 時,就會透過 offer/answer process 處理,以下是基本的 steps

  1. caller 透過 MediaDevices.getUserMedia 取得 local Media
  2. caller 產生 RTCPeerConnection,呼叫 RTCPeerConnection.addTrack()
  3. caller 呼叫 RTCPeerConnection.createOffer() 產生 offer
  4. caller 呼叫 RTCPeerConnection.setLocalDescription() 發送 offer 到 local description
  5. caller 詢問 STUN servers 用以產生 ice candidates
  6. caller 使用 signaling server 傳送 offer 給 receiver
  7. receiver 接收 offer,呼叫 RTCPeerConnection.setRemoteDescription() 作為 remote description
  8. receiver 設定 call
    • capture local media
    • 呼叫 RTCPeerConnection.addTrack()
  9. receiver 透過 RTCPeerConnection.createAnswer() 產生 answer
  10. receiver 呼叫 RTCPeerConnection.setLocalDescription(),傳入 answer,也就是將 answer 設定為 local description
  11. receiver 使用 signaling server 發送 answer 給 caller
  12. caller 收到 answer
  13. caller 呼叫 RTCPeerConnection.setRemoteDescription() 將 answer 設定為 remote description
  14. 然後就能開始互傳 media

Pending and Current Descriptions

在處理 localDescription 與 remoteDescription 時,可能會因為格式不相容而被 reject,因此需要一個機制能夠 propose a new format,直到另一端接受

current description:由 RTCPeerConnection.currentLocalDescription 及 RTCPeerConenction.currentRemoteDescription 回傳,這也是目前雙方同意使用的 description。

pending description:由 RTCPeerConnection.pendingLocalDescription 及 RTCPeerConnection.pendingRemoteDescription 回傳,代表 setLocalDesciption() 或 setRemoteDescription() 目前考慮中的 description

RTCPeerConnection.localDescription 與 RTCPeerConnection.remoteDescription 回傳的 description,如果在 pending description 狀態,就是回傳 pending description,否則就回傳 current description

呼叫 setLocalDescription() 或 setRemoteDescription() 時,description 會先設定為 pending description,然後 WebRTC layer 開始評估要不要接受,一但 proposed description 被接受,current description 就會改成 pending description 的值,pending description 被改為 null

pendingLocalDescription 包含 offer or answer,還有 local ICE candidates

pendingRemoteDescription 包含 remote ICE candidates,可呼叫 RTCPeerconnection.addIceCandidate() 提供資訊

ICE Candidates

除了交換 media information,還需要交換 network connection 的 informations,也就是 ICE candidate 以及 peer 能夠通訊的方法 (directly or through a TURN server)

通常每個 peer 會先 propose 最佳的 candidates,理想狀態要使用 UDP (速度快,且容易 recover 中斷),但 ICE 標準也允許使用 TCP (當無法使用 UDP 時),另外不是所有瀏覽器都支援 ICE over TCP

UDP candidate types

  • host:實體 IP,可直接連線

  • srflx:Server Reflexive and Relayed Candidates

    host 因為經過 NAT,透過 Stun/Turn server 產生的 candidate。connection 最初要求來自 STUN server 的 candidate,並將 request 傳給 remote peer

  • prflx: Peer Reflexive Candidates

    來自 symmetric NAT,通常是 trickle ICE (也就是 additional candidate 交換,發生在 primary signaling 之後,在 verification 結束以前) 的 additional candidate

    是直接發送給 peer host 產生的 candidate

  • relay

    類似 srflx,但是由 Turn Server 產生的中繼 candidate IP:Port

tcp 有三種:active, passive, so

  • active

    會打開 outbound connection,且不接受 incoming connection request。這是最常見的類型

  • passive

    只會接受 incoming connection

  • so

    會嘗試同時在兩個終端直接打開連線

Choosing a candidate pair

ICE layer 會選擇兩個 peers 中的某一個當作 controlling agent。ICE Agent 會決定 connection 最後使用的 candidate pair,另一個 peer 就稱為 controlled agent。可透過 RTCIceCandidate.transport.role 查詢

controlling agent 除了決定 candidate pair 以外,還會透過 STUN,signaling 選用的 controlled agent,更新 offer。controlled agent 只會等待通知使用哪一個 candidate pair

ICE session 可能會讓 controlling agent 選擇多個 candidate pair,每次分享資訊給 controlled agent 時,兩端會重新設定 connection 使用的 candidate pair

一但 ICE session 完成,configuration 就固定了,除非發生 ICE reset

在每個 candidate generation 結束時,會以 RTCIceCandidate 格式發送 end-of-candidates notification,也就是 candidate 屬性為空字串。這個 candidate 還是要呼叫 addIceCandidate() 加入 connection,以便通知 remote peer

如果在 negotiation 時,沒有其他的 candidates,就會發送 end-of-candidates notification,發送 candidate 屬性為 null 的 RTCIceCandidate。這個訊息不需要傳給 remote peer。這是 legacy notification of a state,可用 icegatheringstatechange event 偵測得知

ICE rollbacks

如果不像掛斷既有的 call,可作 renegotiation

ICE rollback 會 restore 上次連線中 signalingState 為 stable 的 SDP offer

發送 description 其 type 為 rollback,就可以 init rollback,該 description 的其他 properties 都會被忽略

當 peer 已經有先前產生的 offer 時,ICE agent 會自動產生 rollback。如果 local peer 的狀態為 have-local-offer,也就是 local pper 有發送過 offer

呼叫 setRemoteDescription() 可收到 received offer triggers rollback

完整的 exchange


Signaling and video calling

Signaling and video calling

discovery 與 negotiation process 用以產生 conenction,稱為 signaling

WebRTC 需要兩個不同網路的端點,能夠互相找到對方,並協調 media format,需要透過第三方 server 處理 signaling

signaling server

兩個 devices 之間的 WebRTC 連線需要先透過 signaling server 協調兩端

WebRTC 不指定 transport mechanism,可使用 WebSocket 或是 XMLHttpRequest

server 不需要了解或是處理 signaling data content (SDP),只需要傳遞。ICE subsystem 要如何傳送 singaling data 給另一個 peer 是比較重要的問題。

準備給 signaling 使用的 chat server

chat server 透過 WebSocket 傳送 JSON string

可用類似方法,做 signaling 與 ICE negotiation

# 將訊息傳送給特定 target 的 function
function sendToOneUser(target, msgString) {
  var isUnique = true;
  var i;

  for (i=0; i < connectionArray.length; i++) {
    if (connectionArray[i].username === target) {
      connectionArray[i].send(msgString);
      break;
    }
  }
}

Designing the signaling protocol

需要一個 protocol 交換 message,以下利用 JSON object 作為範例

Exchange session descriptions

在開始 singaling process 時,user init call 並產生 offer,裡面包含 session description (SDP 格式),該資料要傳給 callee,callee 會以 answer 回應

設計用 video-offer video-answer 分別代表 offer, answer

JSON 欄位 說明
type message type, video-offer or video-answer
name sender's name
target callee's name
sdp SDP string

兩端可知道 codecs, codec parameters,但還不知道如何傳遞 media data。透過 ICE candidates 處理

Exchange ICE candidates

兩端需要交換 ICE candidates 協調真正的 connection,每個 ICE candidate 都會描述一個可使用的溝通管道。peer 會持續發送找到的 candidates,不管 streaming 是否已經開始了。

icecandidate event 會送給 RTCPeerConenction,並以 pc.setLocalDescription(offer) 完成 process

如果有更好的 candidate,stream 可修改 formats

欄位 說明
type new-ice-candidate
target callee's name
candidate SDP candidate string
不需要處理訊息內容,只需要傳給 remote peer

每個 ICE message 會提供 TCP/UDP, IP address, port, connection type (說明是 peer IP 或是 relay server),包含 NAT 資訊

note: 當收到 onicecandidate 時,要將 candidate 透過 signaling connection 傳給另一個 peer。收到 ICE candidate message 時,就呼叫 RTCPeerConnection.addIceCandidate()。建議不要嘗試修改 SDP

note: onicecandidate event 與 createAnswer() 都是 async call,注意 signaling 不要修改處理順序。setRemoteDescription() 後,才能呼叫 addIceCandidate()

Signaling transaction flow

  • Each user's client running within a web browser
  • Each user's web browser
  • The signaling server
  • The web server hosting the chat service

ICE candidate exchange process

在 local ICE layer 收到的 candidate,各自發送給另一端,當兩端同意使用某個 candidate,就會開始傳遞 media

當網路狀況有異動,peer 可能會建議切換到不同 media resolution 或 不同 codec,就會啟動新的 exchange of candidates

client application

兩個 video elements,一個 hangup 按鈕

<div class="flexChild" id="camera-container">
  <div class="camera-box">
    <video id="received_video" autoplay></video>
    <video id="local_video" autoplay muted></video>
    <button id="hangup-button" onclick="hangUpCall();" disabled>
      Hang Up
    </button>
  </div>
</div>
// 透過 WebSocket 發送訊息給 signaling server
function sendToServer(msg) {
  var msgJSON = JSON.stringify(msg);

  connection.send(msgJSON);
}
// users: 每個連線的 user 的 usernames
function handleUserlistMsg(msg) {
  var i;
  var listElem = document.querySelector(".userlistbox");

  while (listElem.firstChild) {
    listElem.removeChild(listElem.firstChild);
  }

  msg.users.forEach(function(username) {
    var item = document.createElement("li");
    item.appendChild(document.createTextNode(username));
    item.addEventListener("click", invite, false);

    listElem.appendChild(item);
  });
}
// 當 user click 某個想要呼叫的 username,就呼叫 inivte
var mediaConstraints = {
  audio: true, // We want an audio track
  video: true // ...and we want a video track
};

function invite(evt) {
  if (myPeerConnection) {
    alert("You can't start a call because you already have one open!");
  } else {
    var clickedUsername = evt.target.textContent;

    if (clickedUsername === myUsername) {
      alert("I'm afraid I can't let you talk to yourself. That would be weird.");
      return;
    }

    targetUsername = clickedUsername;
    createPeerConnection();

    navigator.mediaDevices.getUserMedia(mediaConstraints)
    .then(function(localStream) {
      document.getElementById("local_video").srcObject = localStream;
      localStream.getTracks().forEach(track => myPeerConnection.addTrack(track, localStream));
    })
    .catch(handleGetUserMediaError);
  }
}
// 如果 getUserMedia 發生 error 的錯誤處理
function handleGetUserMediaError(e) {
  switch(e.name) {
    case "NotFoundError":
      alert("Unable to open your call because no camera and/or microphone" +
            "were found.");
      break;
    case "SecurityError":
    case "PermissionDeniedError":
      // Do nothing; this is the same as the user canceling the call.
      break;
    default:
      alert("Error opening your camera and/or microphone: " + e.message);
      break;
  }

  closeVideoCall();
}
// 建立 PeerConnection
function createPeerConnection() {
  myPeerConnection = new RTCPeerConnection({
      iceServers: [     // Information about ICE servers - Use your own!
        {
          urls: "stun:stun.stunprotocol.org"
        }
      ]
  });

  myPeerConnection.onicecandidate = handleICECandidateEvent;
  myPeerConnection.ontrack = handleTrackEvent;
  myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
  myPeerConnection.onremovetrack = handleRemoveTrackEvent;
  myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
  myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent;
  myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
}
  • Starting negotiation
// 處理 negotiationneeded event
function handleNegotiationNeededEvent() {
  myPeerConnection.createOffer().then(function(offer) {
    return myPeerConnection.setLocalDescription(offer);
  })
  .then(function() {
    sendToServer({
      name: myUsername,
      target: targetUsername,
      type: "video-offer",
      sdp: myPeerConnection.localDescription
    });
  })
  .catch(reportError);
}
  • session negotiation
    • Handling the invitation
function handleVideoOfferMsg(msg) {
  var localStream = null;

  targetUsername = msg.name;
  createPeerConnection();

  var desc = new RTCSessionDescription(msg.sdp);

  myPeerConnection.setRemoteDescription(desc).then(function () {
    return navigator.mediaDevices.getUserMedia(mediaConstraints);
  })
  .then(function(stream) {
    localStream = stream;
    document.getElementById("local_video").srcObject = localStream;

    localStream.getTracks().forEach(track => myPeerConnection.addTrack(track, localStream));
  })
  .then(function() {
    return myPeerConnection.createAnswer();
  })
  .then(function(answer) {
    return myPeerConnection.setLocalDescription(answer);
  })
  .then(function() {
    var msg = {
      name: myUsername,
      target: targetUsername,
      type: "video-answer",
      sdp: myPeerConnection.localDescription
    };

    sendToServer(msg);
  })
  .catch(handleGetUserMediaError);
}
  • sending ICE candidates
function handleICECandidateEvent(event) {
  if (event.candidate) {
    sendToServer({
      type: "new-ice-candidate",
      target: targetUsername,
      candidate: event.candidate
    });
  }
}
  • receiving ICE candidates
function handleNewICECandidateMsg(msg) {
  var candidate = new RTCIceCandidate(msg.candidate);

  myPeerConnection.addIceCandidate(candidate)
    .catch(reportError);
}
  • receiving new streams
function handleTrackEvent(event) {
  document.getElementById("received_video").srcObject = event.streams[0];
  document.getElementById("hangup-button").disabled = false;
}
  • handling the removal of tracks
function handleRemoveTrackEvent(event) {
  var stream = document.getElementById("received_video").srcObject;
  var trackList = stream.getTracks();

  if (trackList.length == 0) {
    closeVideoCall();
  }
}
  • hangup
function hangUpCall() {
  closeVideoCall();
  sendToServer({
    name: myUsername,
    target: targetUsername,
    type: "hang-up"
  });
}
  • end call
function closeVideoCall() {
  var remoteVideo = document.getElementById("received_video");
  var localVideo = document.getElementById("local_video");

  if (myPeerConnection) {
    myPeerConnection.ontrack = null;
    myPeerConnection.onremovetrack = null;
    myPeerConnection.onremovestream = null;
    myPeerConnection.onicecandidate = null;
    myPeerConnection.oniceconnectionstatechange = null;
    myPeerConnection.onsignalingstatechange = null;
    myPeerConnection.onicegatheringstatechange = null;
    myPeerConnection.onnegotiationneeded = null;

    if (remoteVideo.srcObject) {
      remoteVideo.srcObject.getTracks().forEach(track => track.stop());
    }

    if (localVideo.srcObject) {
      localVideo.srcObject.getTracks().forEach(track => track.stop());
    }

    myPeerConnection.close();
    myPeerConnection = null;
  }

  remoteVideo.removeAttribute("src");
  remoteVideo.removeAttribute("srcObject");
  localVideo.removeAttribute("src");
  remoteVideo.removeAttribute("srcObject");

  document.getElementById("hangup-button").disabled = true;
  targetUsername = null;
}

ICE connection state

function handleICEConnectionStateChangeEvent(event) {
  switch(myPeerConnection.iceConnectionState) {
    case "closed":
    case "failed":
      closeVideoCall();
      break;
  }
}

ICE signaling state

function handleSignalingStateChangeEvent(event) {
  switch(myPeerConnection.signalingState) {
    case "closed":
      closeVideoCall();
      break;
  }
};

ICE gathering state

function handleICEGatheringStateChangeEvent(event) {
  // Our sample just logs information to console here,
  // but you can do whatever you need.
}

WebRTC Data Channel

Using WebRTC Data Cahnnel

建立 RTCPeerConnection 後,就能收送 media data,也可以使用 data channel,可安全地交換任何資料

因所有 WebRTC components 都要求加密,RTCDataChannel 會自動使用 Datagram Transport Layer Security (DTLS)

產生 data channel

RTCDataChannel 使用的 data transport 可用兩種方法產生

  • 以 WebRTC 產生 transport,並宣告為 remote peer (會收到 datachannel event),這種方法比較簡單且常用,但比較沒有彈性
  • 自己寫 code 協調 data transport,自己 signal 到另一端 peer,且要連接到 new channel

Automatic negotiation

通常可讓 peer connection 協調處理 RTCDataChannel connection

呼叫 createDataChannel() 且不設定 negotiated 屬性,或是設定為 false。就會讓 RTCPeerConnection 自動處理 RTCDataChannel

可透過傳給 RTCDataChannel 的 open event 判斷是否有成功連線

let dataChannel = pc.createDataChannel("MyApp Channel");

dataChannel.addEventListener("open", (event) => {
  beginTransmission(dataChannel);
});

Manual negotiation

先用 RTCPeerConnection.createDataChannel() 產生一個新的 RTCDataChannel,設定 negotiated 為 true

使用 webserver or others,要能 signal to the remote peer,並讓他產生自己的 RTCDataChannel,且 negotiated 為 true

let dataChannel = pc.createDataChannel("MyApp Channel", {
  negotiated: true
});

dataChannel.addEventListener("open", (event) => {
  beginTransmission(dataChannel);
});

requestRemoteChannel(dataChannel.id);

上面的 requestRemoteChannel 就是用來 trigger negotiation,產生 remote channel with the same ID as the local channel

Buffering

WebRTC data channel 支援 outbound data 的 buffering,會自動處理,且無法控制 buffer size。

使用 bufferedAmount, bufferedAmountLowThreshold, onbufferedamountlow, and bufferedamountlow

了解 message size limits

網路傳輸的封包有 size 限制

Firefox, Chrome 使用 usrsctp library 實作 SCTP,但兩個使用方法不同,firefox 會用多個 SCTP message 傳送 large message (deprecated),chrome 是用 series of messages

少於 16 kB 的訊息不需要考慮此問題

Concerns

目前不適合使用 RTCDataChannel 傳送超過 64kB 的 message,因為 SCTP 最初用在 signaling protocol,這邊假設 message 都很小,要支援超過網路 MTU 的 message 是後來增加的功能。這個技術需要訊息編上有順序的號碼,要依序傳送。

因為有越多越多 application 傳送大 message,造成重要的 signaling message 阻塞的問題。

目前 browser 利用 end-of-record (EOR) flag 支援 larger message,這表示 message 是 last one in a series。在支援 EOR 的狀況下,RTCDataChannel 可傳送大 message (可超過 256 kB,firefox 實作可傳送 1GB),在 256 kB 的狀況下,會發生 noticeable delays。

要解決此問題,新的 stream scheduler (SCTP ndata specification) 可 interleave 在不同 stream 傳送的 message,proposal 目前還在 IETF draft,如果實作,理論上可傳送任意大小的 message

firefox 在 bug 1381145 支援 ndata

chrome 在 bug 5696

Security

透過 WebRTC 傳送的資料都會被加密,RTCDataChannel 是用 Datagram Transport Layer Security (DTLS) 加密,based on TLS (https)

WebRTC 在兩個 agents 之間做 peer-to-peer conenction,資料不會傳給 web/application server

SCTP NAT

解决iptables nat sctp协议无效的问题

A----->B-----C

IP如下:

A:1.1.1.1

B:1.1.1.2; 2.2.2.1

C:2.2.2.2

需求為,A 需要使用sctp連通C

在B機器上添加iptables規則為:

iptables -t nat -I PREROUTING -d 1.1.1.2 -p sctp --dport 11111 -j DNAT --to-destination  2.2.2.2:11111

就是把A發出的目的地址:Port 由1.1.1.2:11111轉變為2.2.2.2:11111

同時在C的接口上使用tcpdump抓包,發現並沒有接收到sctp message,為檢驗網絡是否正常(包括路由等配置),僅將上述規則中的sctp改為tcp進行tcp的連通測試

iptables -t nat -I PREROUTING -d 1.1.1.1 -p tcp --dport 11111 -j DNAT --to-destination  2.2.2.2:11111

發現C機器上可以抓到tcp報文,說明網絡沒有問題,iptables的規則也沒有問題。

因為tcpdump抓包解包並不需要系統支持特定的協議,懷疑可能是iptables規則因為某種原因沒有生效,借助google發現瞭解決辦法:iptables-nat-not-work-for-sctp

載入nfconntrackproto_sctp即可,該模塊用來對sctp進行連接跟蹤

modprobe nf_conntrack_proto_sctp

連接跟蹤模塊可以參見:nf_conntrack連接跟蹤模塊

Why is SCTP not much used/known

  • NAT: Doesn't cross NAT very well/at all (less than 1% internet home & enterprise routers do NAT on SCTP).

sctp over udp encapsulation does not work under NAT network


ORTC

Object Real-Time Communication 是新的規格,提供開發 WebRTC application 高階 API,ORTC 不使用 SDP,也不支援 Offer/Answer state machine。ORTC 使用 "sender", "receiver", "transport" 物件及設定參數,用以描述該物件的功能。"Tracks" 是由 sender 編碼,透過 transport 傳送,在 receiver 解碼。而 "data channels"是直接透過 transport 傳送。

ORTC 最初由 MS Edge 實作支援,但目前 ORTC 很多的物件定義,已經被吸收進入 WebRTC 1.0 核心標準中。

WebRTC, ORTC difference

ref: ORTC draft spec

ref: WebRTC的现状和未来(上)

References

WebRTC API

samples

WebRTC wiki

30-26之 WebRTC 的 P2P 即時通信與小範例

2021/09/13

ICE: Interactive Connectivity Establishment

ICE 是以 P2P 連線為前提的一種協調方法,主要是 VOIP、P2P視訊在使用。因為現在的網路終端,通常都在 Firewall 裡面,透過 NAT 上網,導致 P2P 連線的困難度大增,因此 ICE 透過 Stun 的技術,判讀 NAT 類型,當遇到特殊的 NAT 類型,無法處理 P2P 時,就搭配 Turn Server 為兩個終端進行資料中繼傳輸。

ICE

ref: Introduction to WebRTC protocols

主要用在 UDP 多媒體 session,為了解決 NAT 的問題,ICE 可嘗試找到多個 candidates,取得最佳的連線方法。

ICE 透過 stun server 查詢 NAT 類型,以及取得自己對外的 IP:Port。

如果無法直接 p2p 連線,就會改用 Turn Server 中繼,互相傳遞資料。

SDP

P2P通訊標準協議ICE

RFC4566 SDP 是用來協調 session 傳輸所要交換的資訊,內容可能會有這些

會話描述:
     v=  (protocol version)
     o=  (originator and session identifier)
     s=  (session name)
     i=* (session information)
     u=* (URI of description)
     e=* (email address)
     p=* (phone number)
     c=* (connection information -- not required if included in
          all media)
     b=* (zero or more bandwidth information lines)
     One or more time descriptions ("t=" and "r=" lines; see below)
     z=* (time zone adjustments)
     k=* (encryption key)
     a=* (zero or more session attribute lines)
     Zero or more media descriptions

時間資訊描述:
     t=  (time the session is active)
     r=* (zero or more repeat times)

多媒體資訊描述(如果有的話):
     m=  (media name and transport address)
     i=* (media title)
     c=* (connection information -- optional if included at
          session level)
     b=* (zero or more bandwidth information lines)
     k=* (encryption key)
     a=* (zero or more media attribute lines)

ex:

      v=0
      o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5
      s=SDP Seminar
      i=A Seminar on the session description protocol
      u=http://www.example.com/seminars/sdp.pdf
      e=j.doe@example.com (Jane Doe)
      c=IN IP4 224.2.17.12/127
      t=2873397496 2873404696
      a=recvonly
      m=audio 49170 RTP/AVP 0
      m=video 51372 RTP/AVP 99
      a=rtpmap:99 h263-1998/90000

NAT

依據 NAT的限制方式,可分為四種:

  • Full Cone NAT(one-to-one)

    • 只是單純的做位址轉換,並未對進出的封包設限
  • Address-Restricted Cone NAT

    • 從內部送出之封包的目的地 IP 位址會被記住。只有這些曾經收過這些封包的位址可以送封包進入內部 IP。由其他位址送進來的封包,都會被拒絕。
  • Port-Restricted cone NAT

    • 封包進出比 Restricted Cone 增加了一個限制, 從內部送出之封包的目的地的IP 位址及 Port Number 會被記住。 由外部送進來的封包,除了由那些接收過內部所送出的封包的IP 位址及 Port Number 所送來的封包之外,都會被拒絕。
  • Symmetric NAT

    • 前三種NAT在做位址轉換時,無論封包是送往哪裡, NAT內部同一內部位址都對應到同一個外部位址:Port,但 Symmetric NAT 內則每一內部位址對於不同的目的地,會對應到不同的外部 IP address:Port。
    • Symmetric NAT只允許先由私有網域內的機器,發送封包到網際網路中的接收端可以回傳封包

判斷方法

NAT Test 提供一種方式,可以判斷是否為 Symmetric NAT,因為 Understanding Different NAT Types and Hole-Punching 的說明,只要有一端為 Symmetric NAT,就無法直接 p2p 連線。

判斷的方法是利用 RTCPeerConnection 對 google stun server 建立連線,由 onicecandidate 判讀, port 跟 rport 是否一樣。

// JScript File

function parseCandidate(line) {
    console.log("parseCandidate="+line);
    // candidate:842163049 1 udp 1677729535 220.132.127.162 42280 typ srflx raddr 0.0.0.0 rport 0 generation 0 ufrag 1cTw network-cost 999

    var parts;
    // Parse both variants.
    // 兩種:a=candidate:  or  candidate:
    if (line.indexOf('a=candidate:') === 0) {
        parts = line.substring(12).split(' ');
    } else {
        parts = line.substring(10).split(' ');
    }

    var candidate = {
        foundation: parts[0],
        component: parts[1],
        protocol: parts[2].toLowerCase(),
        priority: parseInt(parts[3], 10),
        ip: parts[4],
        port: parseInt(parts[5], 10),
        // skip parts[6] == 'typ'
        type: parts[7]
    };

    for (var i = 8; i < parts.length; i += 2) {
        switch (parts[i]) {
            case 'raddr':
                candidate.relatedAddress = parts[i + 1];
                break;
            case 'rport':
                candidate.relatedPort = parseInt(parts[i + 1], 10);
                break;
            case 'tcptype':
                candidate.tcpType = parts[i + 1];
                break;
            default: // Unknown extensions are silently ignored.
                break;
        }
    }

    console.log("candidate=",candidate);
    // {
    //     "foundation": "842163049",
    //     "component": "1",
    //     "protocol": "udp",
    //     "priority": 1677729535,
    //     "ip": "220.132.127.162",
    //     "port": 42280,
    //     "type": "srflx",
    //     "relatedAddress": "0.0.0.0",
    //     "relatedPort": 0
    // }
    return candidate;
};

function testNat() {
    //reset lblResult
    document.getElementById("MainContent_lblResult").style.color = "#696969";
    document.getElementById("MainContent_lblResult").innerHTML = "Test started " + new Date().toLocaleString() + "<br>";

    var candidates = {};
    var pc = new RTCPeerConnection({
        iceServers: [
            { urls: 'stun:stun1.l.google.com:19302' },
            { urls: 'stun:stun2.l.google.com:19302' }
        ]
    });
    document.getElementById("MainContent_lblResult").innerHTML += "Create data channel to Google STUN servers<br>";
    pc.createDataChannel("foo");
    document.getElementById("MainContent_lblResult").innerHTML += "Testing...Please Wait...<br>";
    pc.onicecandidate = function (e) {
        console.log(e);

        if (e.candidate && e.candidate.candidate.indexOf('srflx') !== -1) {
            console.log("e.candidate.candidate="+e.candidate.candidate);

            var cand = parseCandidate(e.candidate.candidate);

            // console.log("candidates[cand.relatedPort]=",candidates[cand.relatedPort]);
            if (!candidates[cand.relatedPort]) candidates[cand.relatedPort] = [];
            // console.log("candidates[cand.relatedPort]=",candidates[cand.relatedPort]);
            candidates[cand.relatedPort].push(cand.port);
            // console.log("candidates[cand.relatedPort]=",candidates[cand.relatedPort]);

        } else if (!e.candidate) {
            // All ICE candidates have been sent
            console.log("candidates=",candidates);
            // {
            //     "0": [
            //         42280
            //     ]
            // }
            if (Object.keys(candidates).length === 1) {
                var ports = candidates[Object.keys(candidates)[0]];
                console.log("ports", ports);

                //window.alert(ports.length === 1 ? 'normal nat' : 'symmetric nat');
                if (ports.length === 1) {
                    document.getElementById("MainContent_lblResult2").innerHTML = "Completed: Normal NAT";
                } else {
                    document.getElementById("MainContent_lblResult2").innerHTML = "Completed: Symmetric NAT";
                }
                document.getElementById("MainContent_lblResult2").style.color = "green";
            }
        }
    };
    pc.createOffer()
        .then(offer => pc.setLocalDescription(offer))

};

另一個判斷的 TypeScript: isNormalNat.ts

ICE协议下NAT穿越的实现(STUN&TURN) 這邊有說明,stun server 是如何判斷 NAT 類型的

candidate type

ref: RFC 5245

ref: WebRTC connectivity

udp 有四種:host, srflx, prflx, relay

  • host:實體 IP,可直接連線

  • srflx:Server Reflexive and Relayed Candidates

    host 因為經過 NAT,透過 Stun/Turn server 產生的 candidate

  • prflx: Peer Reflexive Candidates

    其中一端來自 symmetric NAT

  • relay

    由 Turn Server 產生的中繼 IP:Port

tcp 有三種:active, passive, so

  • active

    會打開 outbound connection,且不接受 incoming connection request。這是最常見的類型

  • passive

    只會接受 incoming connection

  • so

    會嘗試同時在兩個終端直接打開連線

References

互動式連接建立

P2P通訊標準協議ICE

ICE (Interactive Connectivity Establishment)

你需要知道关于ICE的三件事

30-29之 WebRTC 的 P2P 打洞術 ( ICE )

[知識篇]WebRTC - ICE(STUN/TURN)

Peer-to-Peer Communication Across Network Address Translators

about 82% of the NATs tested support hole punching for UDP, and about 64% support hole punching for TCP streams

Trickle ICE

RFC 5389 - STUN 協定介紹

[筆記] WebRTC 網路影音-通訊協定篇(protocol of media, video and audio)

NAT 及防火牆之來源

Understanding Different NAT Types and Hole-Punching

30-28之 WebRTC 連線前傳 - 為什麼 P2P 連線很麻煩 ? ( NAT )

NAT -- 基本概念

WebRTC ICE 狀態與提名處理

測試 NAT 類型

SIP穿越NAT的rport機制

WebRTC ICE candidate里面的raddr和rport表示什么?

Network Configuration for VoIP Providers

ICE介绍 (RFC 5245)

rfc 5245 笔记

C ICE implementation library

libice

libnice

WebRtc音视频实时通信--libnice库介绍

How to play libnice-ly with your NAT

libnice for iOS and Android.

libjuice

2021/09/06

GraphQL in java

GraphQL 是由 Facebook 在 2012 提出,2015 開源,2018 移交給 GraphQL 基金會。他跟 SQL 不同,並不是隸屬於某一種資料庫的查詢語言,GraphQL 也不是圖形資料庫,他單純是一種簡化 web API 的開發的方法。一般來說在網頁開發時,如果需要使用後端資料庫的資料,就會撰寫 rest 或 web service 的介面,根據資料對應產生許多 rest API,當頁面內的資料來源越多,rest API 就越複雜。GraphQL 透過資料定義,能夠用統一的 rest 介面,提供查詢多樣化資料的功能,也能避免大量冗餘資料傳給網頁。

GraphQL 除了查詢功能,支援了讀取、寫入、資料變更訂閱的功能。

以下根據 Getting started with GraphQL Java and Spring Boot 初步了解 GraphQL

專案目標

根據這個 GraphQL schema 定義

type Query {
  bookById(id: ID): Book 
}

type Book {
  id: ID
  name: String
  pageCount: Int
  author: Author
}

type Author {
  id: ID
  firstName: String
  lastName: String
}

製作一個提供這個 schema 的 GraphQL Server,可透過網頁 API 用以下語法查詢

{
  bookById(id: "book-1"){
    id
    name
    pageCount
    author {
      firstName
      lastName
    }
  }
}

得到的查詢結果為

{ 
  "bookById":
  {
    "id":"book-1",
    "name":"Harry Potter and the Philosopher's Stone",
    "pageCount":223,
    "author": {
      "firstName":"Joanne",
      "lastName":"Rowling"
    }
  }
}

Project

首先產生一個 Java Maven Project,修改 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.graphqljava.tutorial</groupId>
    <artifactId>bookdetails</artifactId>
    <version>1.0.0</version>

    <dependencies>
        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java</artifactId>
            <version>11.0</version>
        </dependency>

        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java-spring-boot-starter-webmvc</artifactId>
            <version>1.0</version>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>26.0-jre</version>
        </dependency>

        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java-spring-boot-starter-webmvc</artifactId>
            <version>1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>

        <!-- testing facilities -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.1.2.RELEASE</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

schema.graphqls

將 GraphQL schema 放在 src/main/resources/schema.graphqls

type Query {
    bookById(id: ID): Book
}

type Book {
    id: ID
    name: String
    pageCount: Int
    author: Author
}

type Author {
    id: ID
    firstName: String
    lastName: String
}

GraphQLProvider.java

利用 schema.graphqls,製作 com.graphqljava.tutorial.bookdetails.GraphQLProvider.java

package com.graphqljava.tutorial.bookdetails;

import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.net.URL;

import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;

@Component
public class GraphQLProvider {


    @Autowired
    GraphQLDataFetchers graphQLDataFetchers;

    private GraphQL graphQL;

    @PostConstruct
    public void init() throws IOException {
        // 以 Resources 讀取 schema.graphqls 產生 GraphQLSchema, GraphQL
        URL url = Resources.getResource("schema.graphqls");
        String sdl = Resources.toString(url, Charsets.UTF_8);
        GraphQLSchema graphQLSchema = buildSchema(sdl);
        this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
    }

    // 實作 buildSchema 產生 GraphQLSchema
    private GraphQLSchema buildSchema(String sdl) {
        TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
        RuntimeWiring runtimeWiring = buildWiring();
        SchemaGenerator schemaGenerator = new SchemaGenerator();
        return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
    }

    // 透過 graphQLDataFetchers,註冊兩個 dataFetcher
    private RuntimeWiring buildWiring() {
        return RuntimeWiring.newRuntimeWiring()
                .type(newTypeWiring("Query")
                        .dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
                .type(newTypeWiring("Book")
                        .dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
                .build();
    }

    @Bean
    public GraphQL graphQL() {
        return graphQL;
    }

}

以下是 GraphQL 與 GraphQLSchema 的關係圖

GraphQLDataFetchers.java

com.graphqljava.tutorial.bookdetails.GraphQLDataFetchers.java

package com.graphqljava.tutorial.bookdetails;

import com.google.common.collect.ImmutableMap;
import graphql.schema.DataFetcher;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

@Component
public class GraphQLDataFetchers {

    private static List<Map<String, String>> books = Arrays.asList(
            ImmutableMap.of("id", "book-1",
                    "name", "Harry Potter and the Philosopher's Stone",
                    "pageCount", "223",
                    "authorId", "author-1"),
            ImmutableMap.of("id", "book-2",
                    "name", "Moby Dick",
                    "pageCount", "635",
                    "authorId", "author-2"),
            ImmutableMap.of("id", "book-3",
                    "name", "Interview with the vampire",
                    "pageCount", "371",
                    "authorId", "author-3")
    );

    private static List<Map<String, String>> authors = Arrays.asList(
            ImmutableMap.of("id", "author-1",
                    "firstName", "Joanne",
                    "lastName", "Rowling"),
            ImmutableMap.of("id", "author-2",
                    "firstName", "Herman",
                    "lastName", "Melville"),
            ImmutableMap.of("id", "author-3",
                    "firstName", "Anne",
                    "lastName", "Rice")
    );

    public DataFetcher getBookByIdDataFetcher() {
        return dataFetchingEnvironment -> {
            String bookId = dataFetchingEnvironment.getArgument("id");
            return books
                    .stream()
                    .filter(book -> book.get("id").equals(bookId))
                    .findFirst()
                    .orElse(null);
        };
    }

    public DataFetcher getAuthorDataFetcher() {
        return dataFetchingEnvironment -> {
            Map<String, String> book = dataFetchingEnvironment.getSource();
            String authorId = book.get("authorId");
            return authors
                    .stream()
                    .filter(author -> author.get("id").equals(authorId))
                    .findFirst()
                    .orElse(null);
        };
    }
}

BookDetailsApplication.java

製作主程式 com.graphqljava.tutorial.bookdetails.BookDetailsApplication.java

package com.graphqljava.tutorial.bookdetails;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BookDetailsApplication {

    public static void main(String[] args) {
        SpringApplication.run(BookDetailsApplication.class, args);
    }

}

Postman

在 Postman 的 APIs 可填寫 graphql schema 定義,在進行 API 查詢時,Postman 就能根據 schema 提供語法 hint

在 Request 中,將 Data Type 改為 GraphQL,就能進行 GraphQL 查詢並得到結果

{
  bookById(id: "book-1"){
    id
    name
    pageCount
    author {
      firstName
      lastName
    }
  }
}

References

GraphQL

GraphQL or gRPC in Java

Build and Test a CRUD API using GraphQL, Spring Boot and MongoDB

GraphQL, MongoDB and Java: An introduction

Spring Boot + GraphQL + MongoDB example with Spring Data & graphql-java

GraphQL Java 从Schema文件到GraphQL实例源码解析

GraphQL Java从入门到实践

Netflix DGS framework

An Advanced GraphQL with Spring Boot and Netflix DGS

2021/08/23

ASMR

ASMR(Autonomous Sensory Meridian Response)稱為「自發性感官經絡反應」「自發性知覺高潮反應」「顱內高潮」,主要是透過視覺、聽覺、嗅覺等知覺,刺激人類顱內、頭皮、後背以及四肢等部分,而產生愉悅的感知現象。

例如,有人聽到他人在吃炸雞的咀嚼聲、竊竊私語、切菜聲、衣物磨擦聲、燒木柴的劈啪聲等,身體就會出現一種酥麻感,進而感到治癒、放鬆。

2010年,珍妮佛·艾倫(Jennifer Allen)提出將該現象命名為「自主性感官經絡反應」

  • (A):自主性 – 自發的、自治的、有或沒有控制
  • (S):感官 – 跟感官或感覺有關
  • (M):經絡 – 表示高峰、高潮或最高點發展
  • (R):反應 – 意指由外部或內部事物觸發的體驗

ASMR 用於感官行銷,透過五感(視覺、聽覺、嗅覺、味覺、觸覺),連結消費者的感受,建立品牌在感官上的形象。

  • IKEA

IKEA在2017年時就推出一則廣告影片,影片中的解說員輕聲細語介紹床具,且透過整理抱枕、磨蹭衣物、輕拍檯燈等,發出舒服的聲響,讓觀眾可以從另一層面,來感受家具的舒適性。

IKEA便是透過拍攝ASMR影片,把聽覺、觸覺的效果展現出來,很多人看了之後,就會覺得他們家的家具質感很好,藉此產生了好印象。這部影片至今已累積3百多萬次觀看。

“Oddly IKEA”: IKEA ASMR

  • Netflix

Netflix上架的節目《家有壁爐》突然爆紅,一個小時的影片只拍攝著火爐的火光,和柴火發出的劈啪聲,豪無劇情、無對白。觀眾卻因為火舌的舞動、木柴發出的劈啪聲,感到無比的治癒感,而埋單。

《家有壁爐》爆紅後,甚至推出「4K樺木版」,也奪得知名電影電視評論網站《IMDb》8分的高分。

ref: 《家有壁爐》劈啪聲燒百萬竟獲IMDb高分 網友讚:超療癒

  • Apple

美國科技巨頭蘋果也搭上ASMR熱潮。蘋果為了推銷 Airpods、iPhone XS,2019年推出了四支ASMR 影片,包含木匠打磨、耳邊細語、雨滴打在不同物體上、行走叢林腳下摩擦的沙沙聲。利用拍攝環境白噪音,讓消費者身心放鬆,也藉此強調iPhone XS與iPhone XS Max收音、錄影畫質表現。

ref: Apple 最新的廣告系列是讓人舒爽的 ASMR 影片

  • 可口可樂

可口可樂在2019年時,推出了四支可口可樂ASMR影片,分別搭配烤肉、披薩、漢堡、義大利麵四種主食,他們將可樂那種氣泡聲,或是冰塊碰撞、倒飲料、瓶蓋的聲音強化後,讓消費者產生生理反應,想當場就打開一瓶可樂喝。

ref: ASMR Coca-Cola шашлыкhttps://youtu.be/FgXQPWGwcL0)

ASMR Coca-Cola пицца

  • 森永製菓

2013年,森有上市一款口味、成分並不出奇的巧克力冰棒,然而,該產品在2017年時,銷量卻突然翻3倍,就靠一支「咬冰棒聲音」的影片。

2016年,森永找感官訊息工程專家研究商品,結果發現,該款冰棒7mm厚巧克力外層的「質地」和「聲音」令人喜愛。

調查結果出來後,森永不做任何廣告,只透過網路影片傳達吃冰棒時候所產生的「Pakipaki」聲,2017年的銷量便是2013年的3倍。

ref: 鈴木奈々、ASMRやってみた60分版

  • 全家

FamilyMart 便利商店在 2019 年的時候,推出了相關影片。影片當中有熟悉的開門聲、結帳聲、咖啡機…等等不同種類的聲音,參雜其中,主要都是店內常常聽見的聲音。

全家療癒音|ASMR|讀書 專注 冥想 工作用BGM|白噪音|Relaxing FamilyMart Convenient Store Background Sounds

  • KFC

KFChill - Finger Lickin’ Good Vibes

  • Disney+

Zenimation

  • 噴火龍

【公式】ASMR・焚き火音 - ヒトカゲといっしょ Charmander's Fireside Slumber

  • 雕刻

ref: 【雕刻 ASMR】欣賞可以有很多種方式,而有人更享受過程

  • 寫字

ref: 如果你喜歡【寫字聲 ASMR】的話,那八成你是用功的孩子!

  • slime

ref: 史萊姆/鬼口水【簡單製作教學】,做出 100 萬觀看的 ASMR 影片吧!

References

ASMR wiki

森永靠「咬冰棒聲」銷量翻3倍!ASMR是什麼?連Netflix、蘋果都在用

【ASMR 行銷】連知名品牌也愛用!讓銷售量翻 2~3 倍的秘密!

聲音新浪潮 品牌必學ASMR淘金術

全球首間ASMR博物館來了!網路上爆紅、一聽就療癒

2021/08/09

MongoDB Cluster

設定三個節點的 MongoDB Cluster

在三台機器設定測試 MongoDB Cluster

環境設定

一個由三個 replica set 組成的 shard server,三個 config server,三個 route server。

shard1: 3 replica sets
192.168.1.11:27019
192.168.1.12:27019
192.168.1.13:27019

3 config server
192.168.1.11:27018
192.168.1.12:27018
192.168.1.13:27018

3 route server
192.168.1.11:27017
192.168.1.12:27017
192.168.1.13:27017

安裝

vi /etc/yum.repos.d/mongodb-org-4.2.repo

[mongodb-org-4.2]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/4.2/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-4.2.asc
yum install -y mongodb-org

# 安裝後,會修改的檔案
# mongodb system service
/usr/lib/systemd/system/mongod.service
# mongodb config file
/etc/mongod.conf

/usr/lib/systemd/system/mongod.service

[Unit]
Description=High-performance, schema-free document-oriented database
After=network.target
Documentation=https://docs.mongodb.org/manual

[Service]
User=mongod
Group=mongod
Environment="OPTIONS=-f /etc/mongod.conf"
ExecStart=/usr/bin/mongod $OPTIONS
ExecStartPre=/usr/bin/mkdir -p /var/run/mongodb
ExecStartPre=/usr/bin/chown mongod:mongod /var/run/mongodb
ExecStartPre=/usr/bin/chmod 0755 /var/run/mongodb
PermissionsStartOnly=true
PIDFile=/var/run/mongodb/mongod.pid
Type=forking
# file size
LimitFSIZE=infinity
# cpu time
LimitCPU=infinity
# virtual memory size
LimitAS=infinity
# open files
LimitNOFILE=64000
# processes/threads
LimitNPROC=64000
# locked memory
LimitMEMLOCK=infinity
# total threads (user+kernel)
TasksMax=infinity
TasksAccounting=false
# Recommended limits for for mongod as specified in
# http://docs.mongodb.org/manual/reference/ulimit/#recommended-settings

[Install]
WantedBy=multi-user.target

/etc/mongod.conf

# more /etc/mongod.conf
# mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log

# Where and how to store data.
storage:
  dbPath: /var/lib/mongo
  journal:
    enabled: true
#  engine:
#  mmapv1:
#  wiredTiger:

# how the process runs
processManagement:
  fork: true  # fork and run in background
  pidFilePath: /var/run/mongodb/mongod.pid  # location of pidfile
  timeZoneInfo: /usr/share/zoneinfo

# network interfaces
net:
  port: 27017
  bindIp: 127.0.0.1  # Listen to local interface only, comment to listen on all interfaces.


#security:

#operationProfiling:

#replication:

#sharding:

## Enterprise-Only Options

#auditLog:

#snmp:

先將 server 停掉

sudo systemctl stop mongod

備份舊資料

mv /var/lib/mongo-cfgsvr /var/lib/mongo-cfgsvr.bak
mv /var/lib/mongo /var/lib/mongo.bak

mkdir -p /var/lib/mongo-cfgsvr
chown -R mongod:mongod /var/lib/mongo-cfgsvr

mkdir -p /var/lib/mongo
chown -R mongod:mongod /var/lib/mongo

修改 os 設定

echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

ssh 免密碼登入

/etc/hosts

192.168.1.11   larzio1
192.168.1.12   larzio2
192.168.1.13   larzio3
# ssh 免密碼

yum -y install openssh-clients

larzio1:
mkdir -p /root/.ssh
cd /root/.ssh
ssh-keygen -t dsa
按enter直到完成

(id_larzio1.pub為自己辨識用的名稱)
scp id_dsa.pub 192.168.1.12:/root/.ssh/id_larzio1.pub
scp id_dsa.pub 192.168.1.13:/root/.ssh/id_larzio1.pub

ssh 192.168.1.12(13)
cd /root/.ssh/
cat id_larzio1.pub >> authorized_keys
完成ssh免密碼

larzio2:
cd /root/.ssh
ssh-keygen -t dsa
按enter直到完成

(id_larzio2.pub為自己辨識用的名稱)
scp id_dsa.pub 192.168.1.11:/root/.ssh/id_larzio2.pub
scp id_dsa.pub 192.168.1.13:/root/.ssh/id_larzio2.pub


ssh 192.168.1.11(13)
cd /root/.ssh/
cat id_larzio2.pub >> authorized_keys
完成ssh免密碼


larzio3:
cd /root/.ssh
ssh-keygen -t dsa
按enter直到完成

(id_larzio3.pub為自己辨識用的名稱)
scp id_dsa.pub 192.168.1.11:/root/.ssh/id_larzio3.pub
scp id_dsa.pub 192.168.1.12:/root/.ssh/id_larzio3.pub


ssh 192.168.1.11(12)
cd /root/.ssh/
cat id_larzio3.pub >> authorized_keys
完成ssh免密碼

Note 備份, 還原 資料庫

mongodump -u root -p passwd --authenticationDatabase admin -d larzio -o /root/download/backup/

mongorestore -u root -p passwd --authenticationDatabase admin -d larzio --drop /root/download/backup/larzio

Config Server

步驟

  1. security key file
  2. config server 設定檔
  3. 產生 config server db path
  4. 啟動 config node service
  5. 部署到其他兩台機器
  6. 建立 replica set

security keyfile

openssl rand -base64 756 > /root/mongodb-keyfile

mkdir -p /var/lib/mongo
chown mongod.mongod /var/lib/mongo

cp -p /root/mongodb-keyfile /var/lib/mongo/
chmod 400 /var/lib/mongo/mongodb-keyfile
chown mongod.mongod /var/lib/mongo/mongodb-keyfile
# 複製到其他兩台機器
scp /var/lib/mongo/mongodb-keyfile  root@192.168.1.12:/var/lib/mongo/mongodb-keyfile
scp /var/lib/mongo/mongodb-keyfile  root@192.168.1.13:/var/lib/mongo/mongodb-keyfile

# 在 12, 13
sudo chmod 400 /var/lib/mongo/mongodb-keyfile
sudo chown mongod.mongod /var/lib/mongo/mongodb-keyfile

config file

cp -p /etc/mongod.conf /etc/mongod-cfgsvr.conf

vi /etc/mongod-cfgsvr.conf
# 修改以下設定
#  systemLog.path
#  storage.dbPath
#  net.port
#  net.bindIp
#  security.keyFile
#  sharding.clusterRole

# mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  logRotate: reopen
  path: /var/log/mongodb/mongod-cfgsvr.log

# Where and how to store data.
storage:
  dbPath: /var/lib/mongo-cfgsvr
  journal:
    enabled: true
#  engine:
#  mmapv1:
#  wiredTiger:

# how the process runs
processManagement:
  fork: true  # fork and run in background
  pidFilePath: /var/run/mongodb/mongod-cfgsvr.pid  # location of pidfile
  timeZoneInfo: /usr/share/zoneinfo

# network interfaces
net:
  port: 27018
  bindIp: 0.0.0.0  # Listen to local interface only, comment to listen on all interfaces.

security:
  keyFile: /var/lib/mongo/mongodb-keyfile
#  authorization: enabled

#operationProfiling:

replication:
  replSetName: rs-config

sharding:
  clusterRole: configsvr

## Enterprise-Only Options

#auditLog:

#snmp:

config server dbpath

mkdir -p /var/lib/mongo-cfgsvr
chown -R mongod:mongod /var/lib/mongo-cfgsvr

config server service

# 不要用這個方式直接啟動, 所有產生的檔案owner 都會是 root:root
#mongod -f /etc/mongod-cfgsvr.conf

建立 mongod-cfgsvr service file

vi /usr/lib/systemd/system/mongod-cfgsvr.service

[Unit]
Description=High-performance, schema-free document-oriented database
After=network.target
Documentation=https://docs.mongodb.org/manual

[Service]
User=mongod
Group=mongod
Environment="OPTIONS=-f /etc/mongod-cfgsvr.conf"
ExecStart=/usr/bin/mongod $OPTIONS
ExecStartPre=/usr/bin/mkdir -p /var/run/mongodb
ExecStartPre=/usr/bin/chown mongod:mongod /var/run/mongodb
ExecStartPre=/usr/bin/chmod 0755 /var/run/mongodb
PermissionsStartOnly=true
PIDFile=/var/run/mongodb/mongod-cfgsvr.pid
Type=forking
# file size
LimitFSIZE=infinity
# cpu time
LimitCPU=infinity
# virtual memory size
LimitAS=infinity
# open files
LimitNOFILE=64000
# processes/threads
LimitNPROC=64000
# locked memory
LimitMEMLOCK=infinity
# total threads (user+kernel)
TasksMax=infinity
TasksAccounting=false
# Recommended limits for for mongod as specified in
# http://docs.mongodb.org/manual/reference/ulimit/#recommended-settings

[Install]
WantedBy=multi-user.target

啟動

systemctl daemon-reload
systemctl enable mongod-cfgsvr
systemctl start mongod-cfgsvr

deploy

複製到其他兩台機器

scp /etc/mongod-cfgsvr.conf root@192.168.1.12:/etc/mongod-cfgsvr.conf
scp /etc/mongod-cfgsvr.conf root@192.168.1.13:/etc/mongod-cfgsvr.conf

scp /usr/lib/systemd/system/mongod-cfgsvr.service root@192.168.1.13:/usr/lib/systemd/system/mongod-cfgsvr.service

scp /usr/lib/systemd/system/mongod-cfgsvr.service root@192.168.1.12:/usr/lib/systemd/system/mongod-cfgsvr.service

在 12, 13

mkdir -p /var/lib/mongo-cfgsvr
chown -R mongod:mongod /var/lib/mongo-cfgsvr

# 啟動 config-svr
systemctl daemon-reload
systemctl enable mongod-cfgsvr
systemctl start mongod-cfgsvr

replica set

先回到 192.168.1.11 關掉 config server,註解 replica, shard 的設定部分,以免 create user 發生 "no master" 的問題

# sudo mongod -f /etc/mongod-cfgsvr.conf -shutdown
systemctl stop mongod-cfgsvr

vi /etc/mongod-cfgsvr.conf
#註解掉 replica 與 shard

重新啟動 config server

#sudo mongod -f /etc/mongod-cfgsvr.conf
systemctl start mongod-cfgsvr
mongo -port 27018

use admin

db.createUser( {
    user: "root",
    pwd: "passwd",
    roles: [ { role: "root", db: "admin" } ]
  });

db.auth('root', 'passwd');

db.createUser( {
    user: "admin",
    pwd: "passwd",
    roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
  });
vi /etc/mongod-cfgsvr.conf
# 開啟 replica 與 shard

重新啟動服務

#sudo mongod -f /etc/mongod-cfgsvr.conf -shutdown
#sudo mongod -f /etc/mongod-cfgsvr.conf
systemctl restart mongod-cfgsvr

登入 mongo

mongo -port 27018 -u 'root' -p 'passwd' -authenticationDatabase 'admin'

rs.initiate(
  {
    _id: "rs-config",
    configsvr: true,
    members: [
      { _id : 0, host : "192.168.1.11:27018" },
      { _id : 1, host : "192.168.1.12:27018" },
      { _id : 2, host : "192.168.1.13:27018" }
    ]
  }
)

rs.status()

Router Server

192.168.1.11, 192.168.1.12

config file

vi /etc/mongod-router.conf

# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  logRotate: reopen
  path: /var/log/mongodb/mongod-router.log

processManagement:
  fork: true  # fork and run in background
  pidFilePath: /var/run/mongodb/mongod-router.pid  # location of pidfile
  timeZoneInfo: /usr/share/zoneinfo

# network interfaces
net:
  port: 27017
  bindIp: 0.0.0.0  # Listen to local interface only, comment to listen on all interfaces.


security:
  keyFile: /var/lib/mongo/mongodb-keyfile

sharding:
  configDB: rs-config/192.168.1.11:27018,192.168.1.12:27018,192.168.1.13:27018

router service

建立 mongod-router service file

vi /usr/lib/systemd/system/mongod-router.service

[Unit]
Description=High-performance, schema-free document-oriented database
After=network.target
Documentation=https://docs.mongodb.org/manual

[Service]
User=mongod
Group=mongod
Environment="OPTIONS=-f /etc/mongod-router.conf"
ExecStart=/usr/bin/mongos $OPTIONS
ExecStartPre=/usr/bin/mkdir -p /var/run/mongodb
ExecStartPre=/usr/bin/chown mongod:mongod /var/run/mongodb
ExecStartPre=/usr/bin/chmod 0755 /var/run/mongodb
PermissionsStartOnly=true
PIDFile=/var/run/mongodb/mongod-router.pid
Type=forking
# file size
LimitFSIZE=infinity
# cpu time
LimitCPU=infinity
# virtual memory size
LimitAS=infinity
# open files
LimitNOFILE=64000
# processes/threads
LimitNPROC=64000
# locked memory
LimitMEMLOCK=infinity
# total threads (user+kernel)
TasksMax=infinity
TasksAccounting=false
# Recommended limits for for mongod as specified in
# http://docs.mongodb.org/manual/reference/ulimit/#recommended-settings

[Install]
WantedBy=multi-user.target

啟動 router

# sudo mongos -f /etc/mongod-router.conf
systemctl daemon-reload
systemctl enable mongod-router
systemctl start mongod-router

deploy

scp /etc/mongod-router.conf root@192.168.1.12:/etc/mongod-router.conf

scp /usr/lib/systemd/system/mongod-router.service root@192.168.1.12:/usr/lib/systemd/system/mongod-router.service

在 192.168.1.12

systemctl daemon-reload
systemctl enable mongod-router
systemctl start mongod-router

Shard Server

config file

vi /etc/mongod.conf

# mongod.conf
# 修改
# - net.port
# - net.bindIp
# - security.keyFile

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  logRotate: reopen
  path: /var/log/mongodb/mongod.log

# Where and how to store data.
storage:
  dbPath: /var/lib/mongo
  journal:
    enabled: true
#  engine:
#  mmapv1:
#  wiredTiger:

# how the process runs
processManagement:
  fork: true  # fork and run in background
  pidFilePath: /var/run/mongodb/mongod.pid  # location of pidfile
  timeZoneInfo: /usr/share/zoneinfo

# network interfaces
net:
  port: 27019
  bindIp: 0.0.0.0

security:
    keyFile: /var/lib/mongo/mongodb-keyfile

#operationProfiling:

#replication:

#sharding:

## Enterprise-Only Options

#auditLog:

在另外兩台 server 做一樣的設定 接下來,先啟動第一台 shard server,建立 replica set 之前必須要有 root user,否則會失敗。

scp /etc/mongod.conf root@192.168.1.12:/etc/mongod.conf
scp /etc/mongod.conf root@192.168.1.13:/etc/mongod.conf

啟動第一台 shard server

建立使用者

systemctl start mongod
mongo -port 27019

use admin

db.createUser( {
    user: "root",
    pwd: "passwd",
    roles: [ { role: "root", db: "admin" } ]
  });

db.auth('root', 'passwd');

db.createUser( {
    user: "admin",
    pwd: "passwd",
    roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
  });

關閉 mongod 服務

systemctl stop mongod

修改

vi /etc/mongod.conf

replication:
  replSetName: rs-data
    
sharding:
  clusterRole: shardsvr

啟動 mongod

systemctl start mongod

在 192.168.1.12, 192.168.1.13 複製設定, 啟動 mogod

scp /etc/mongod.conf root@192.168.1.12:/etc/mongod.conf
scp /etc/mongod.conf root@192.168.1.13:/etc/mongod.conf
systemctl start mongod

登入 mongo 並設定 replica

mongo -port 27019 -u 'root' -p 'passwd' -authenticationDatabase 'admin'

rs.initiate(
  {
    _id: "rs-data",
    configsvr: false,
    members: [
      { _id : 0, host : "192.168.1.11:27019" },
      { _id : 1, host : "192.168.1.12:27019" },
      { _id : 2, host : "192.168.1.13:27019" }
    ]
  }
)

登入 router (mongos) 設定 shard

mongo -port 27017 -u 'root' -p 'passwd' -authenticationDatabase 'admin'

sh.addShard('rs-data/192.168.1.11:27019,192.168.1.12:27019,192.168.1.13:27019')

把認證模式給開啟,要加入認證模式的有 config 與 shard 開啟很簡單只要將 security.authorization 設定為 enabled

vi /etc/mongod-cfgsvr.conf

security:
    keyFile: /var/lib/mongo/mongodb-keyfile
    authorization: enabled
vi /etc/mongod.conf

security:
    keyFile: /var/lib/mongo/mongodb-keyfile
    authorization: enabled
scp /etc/mongod-cfgsvr.conf root@192.168.1.12:/etc/mongod-cfgsvr.conf
scp /etc/mongod-cfgsvr.conf root@192.168.1.13:/etc/mongod-cfgsvr.conf

scp /etc/mongod.conf root@192.168.1.12:/etc/mongod.conf
scp /etc/mongod.conf root@192.168.1.13:/etc/mongod.conf

重新啟動服務

systemctl restart mongod
systemctl restart mongod-cfgsvr

logrotate

vim /etc/logrotate.d/mongod

/var/log/mongodb/mongod.log  {
    daily
    missingok
    rotate 30
    copytruncate
    dateext
    compress
    notifempty
    create 644 mongod mongod
    sharedscripts
    postrotate
        /bin/kill -SIGUSR1 'cat /var/run/mongodb/mongod.pid 2> /dev/null' 2> /dev/null || true
    endscript
}

vim /etc/logrotate.d/mongod-cfgsvr


/var/log/mongodb/mongod-cfgsvr.log {
    daily
    missingok
    rotate 30
    copytruncate
    dateext
    compress
    notifempty
    create 644 mongod mongod
    sharedscripts
    postrotate
        /bin/kill -SIGUSR1 'cat /var/run/mongodb/mongod-cfgsvr.pid 2> /dev/null' 2> /dev/null || true
    endscript
}

vim /etc/logrotate.d/mongod-router


/var/log/mongodb/mongod-router.log {
    daily
    missingok
    rotate 30
    copytruncate
    dateext
    compress
    notifempty
    create 644 mongod mongod
    sharedscripts
    postrotate
        /bin/kill -SIGUSR1 'cat /var/run/mongodb/mongod-router.pid 2> /dev/null' 2> /dev/null || true
    endscript
}

測試

logrotate -f -v /etc/logrotate.d/mongod

database user

在目標資料庫建立使用者

mongo -port 27017 -u 'root' -p 'passwd' -authenticationDatabase 'admin'

use larzio

db.createUser({
    user: "larzio",
    pwd: "passwd",
    roles: [{ role: "readWrite", db: "larzio" }, { role: "dbAdmin", db: "larzio" }]
})

References

在 cent os 7上安裝 mongodb with Sharded Cluster (1) config server

在 cent os 7上安裝 mongodb with Sharded Cluster (2) router server 與 shard server

MongoDB Sharding 分散式儲存架構建置 (實作篇)

2021/08/02

Chatbot

系統分類

  • 問答 QA 系統

    一問一答,一般沒有對話管理的功能。偏重問句分析,取得問句的主題、問題詞、中心動詞。問句分析目前主要採用 template 比對和語意分析兩種方法

  • 任務導向型對話系統

    目的是解決使用者的明確需求。透過對話管理追蹤目前狀態,確定目的與需求。重點在對話管理,將自然語言映射為使用者的意圖和對應的槽位。

  • 閒聊系統

    使用者無特定目的,沒有具體的需求的多輪人機對話。

  • 主動推薦系統

    人機自然互動

不同類型的系統都包含這三個模組

  • 自然語言理解 NLU
  • 自然語音生成 NLG
  • 對話管理

自然語言理解 NLU

可從語音、音韻、詞態、文法、語意、語用六個維度理解自然語言

  • 語音

    跟發音有關,例如中文拼音

  • 音韻

    由語音組合起來的讀音,例如中文拼音 + 四種聲調

  • 詞態

    詞態封裝可用 NLU,資訊量大小取決於具體的語言種類。拉丁語系有很詞態變化,中文沒有太多的詞態變化,只有偏旁的差異,例如:他 她

  • 文法

    主要研究詞語如何組成合乎語法的句子,文法提供單字組成句子的約束條件,為語意的合成提供框架

  • 語意、語用

    自然語言包含和表達的意思

自然語言的難度

  • 沒有固定的格式,相同的意思有多種句式表達,改了一個字、調整語調、語序,都可能改變語意
  • 不斷有新的詞彙出現
  • 不同的場景(上下文),同樣的句子有不同的意思

問句 + 上下文 ------> 自然語言理解 NLU -------> 語意

英文單字以空格分隔,但中文詞語沒有自然分隔符號,故要先進行分詞處理

NLU 需要提供的模組功能

  • 實體識別 Named Entity Recognition

    識別具有特定意義的實體,例如:人名、時間、地名及專有名詞

  • 使用者意圖識別

    顯式及隱式意圖

    ex: "好熱啊" -> 可能是想知道現在的氣溫,或是控制空調

  • 情感識別

    顯式及隱式情感

    ex: "今天心情很好" -> 正面 -> 顯式、容易判斷

    ex: "今天跟客戶談判出了問題" -> 負面情感 -> 程式很難判斷

  • 指代消解、省略恢復

    在聊天主題背景一致的情況下,對話過程通常會習慣使用代詞,取代出現過的實體。或為了方便表述,省略部分句子。

  • 回覆確認

    如果發生判斷模糊的狀況,chatbot 要主動詢問意圖,也就是回覆確認

  • 拒識判斷

    chatbot 要能主動拒絕識別,及回覆超過自身理解/回覆範圍,或涉及敏感話題

NLU 的方法分為基於規則和統計兩種

  • 基於規則

    人工定義很多語法規則,利用規則定義如何從文字中提取語意。NLU 根據規則解析輸入該模組的文字。

    優點:靈活,可定義各式各樣的規則

    缺點:需要大量不同場景的規則,隨著規則數量增加,維護規則的難度也增加

    適合簡單的場景,可快速時做一個簡單可用的 NLU

  • 基於統計

    資料量大,就要用統計方法訓練模型。

    優點:資料驅動

    缺點:訓練資料難以取得,模型難以解釋和偵錯

    適合處理分類和序列標註的問題。可將意圖識別定義為分類問題:輸入句子的文字特徵,輸出該特徵所屬的意圖分類。 SVM、AdaBoost 演算法

    實體識別就是序列標註問題:輸入句子的文字特徵,輸出特徵中,每個字詞屬於某個實體的機率。HMM、CRF(Conditional Radom Field) 演算法

採用 deep learning 方法時,需要大量的資料,因為長尾資料普遍存在,基於統計的方法,受訓練資料品質影響很大

實作通常結合使用兩種方法

  1. 沒有資料及資料較少,先採用基於規則的方法,累積資料後,再採用基於統計的方法
  2. 基於統計的方法可涵蓋大部分的場景,在涵蓋不到的場景,改用基於規則的方法

NLU 產品,強調通用性,很難客製

  • Facebook 的 Wit.ai

  • Google 的 api.ai

  • MS 的 LUIS.AI

NLU 基本技術

詞法分析 (分詞 + 詞性標註) ----> 句法分析 ---> 語意分析

  • 詞法分析 lexical analysis

    常用漢字有六、七千字,遠多於 26 個英文字母,中文詞之間沒有明確的分隔標記,多音現象嚴重,缺少詞態變化(單複數、時態、陰陽性),這些特性帶來了中文分詞方法、重疊詞區分(黑 / 黑黑的)、歧義欄位切分、專有名詞識別等等問題

    詞法分析包含 分詞 + 詞性標註 兩個部分

    • 分詞 word segmentation 有基於詞表和基於統計兩種方法

    基於詞表:逐字掃瞄字串,當子字串跟詞表的詞相同就算吻合。再細分為 最大比對法、逆向最大比對法、雙向掃描法、逐詞巡訪法等等。ex: IKAnalyzer

    基於統計:根據人工標註的詞性和統計特徵,對中文進行建模,透過模型計算各分詞出現的機率,以機率最大的分詞作為結果,常用 HMM、CRF、LSTM+CRF 演算法。ex: ICTCLAS、Standford Word Segmenter * 詞性是詞語最基本的文法屬性之一,詞性標註 Part-Of-Speech Tagging (POS Tagging) 是詞法分析的一部分

    將句子的每一個詞賦予特定類別,ex: 動詞、名詞、介系詞,句子中最能代表資訊的是名詞、動詞、形容詞、副詞,這四種是 Open Class,詞量會隨著時間增加,Closed Class 包含冠詞、介系詞、連接詞,數量固定

    主要採用 HMM,陸續採用判別式的最大熵模型、支援向量機模型等。有基於規則、基於統計兩種方法

    基於規則:依照詞性關係與上下文情境建造詞類消岐規則

    基於統計:將機率最大的詞性作為結果

    工具:Stanford Log-linear Part-Of-Speech Tagger、LTP

  • 句法分析 Syntactic Parsing

    分析輸入的文句,得到句法結構。從字串得到句法結構的過程。ex: 句法驅動和統計的機器翻譯

    不同的句法形式,對應到不同的句法分析演算法,片語結構及依存結構,是最常用的兩類文法體系。以片語結構樹為目標的句法分析器應用範圍最廣。

    分析結果以樹狀結構的形式呈現,稱為句法分析樹。

    根據句法結構不同的表示形式,可將句法分析分為以下三種

    • 依存句法分析 Dependency Syntactic Parsing:識別詞彙之間的相互依存關係

    基本假設:一個句子存在主體(被修飾詞)和修飾詞,句子中,詞的修飾關係具有方向性,通常是一個詞支配另一個詞,這種支配關係就是依存文法。

    詞和詞之間的依存(修飾)關係,本質上包含在句法結構中。

    一個依存關係連接的兩個詞,分別是 head 核心詞與 dependent 依存詞

    依存關係的五條公理:

    1. 一個句子只有一個成分是獨立的
    2. 其他成分直接依存於某一個成分
    3. 任一個成分都不能依存於兩個或兩個以上的成分
    4. 如果 A 直接依存於 B,C在句子中位於 A, B 之間,則 C 直接依存於 B,或依存於 A, B之間某一個成分
    5. 中心成分左右兩邊的其他成分,相互之間不發生關係

    常見依存關係

    關係類型 標籤 描述 例子
    主謂關係 SBV subject-verb 他邀請我跳舞 (他 <- 邀請)
    動賓關係 VOB 直接賓語 verb-object 媽媽給我一個吻 (給 -> 吻)
    間賓關係 IOB 間接賓語 indirect-object 媽媽給我一個吻 (給 -> 我)
    前置賓語 FOB 前置賓語 fronting-object 莫我肯顧 (我 <- 顧)
    兼語 DBL double 他邀請我跳舞 (邀請 -> 我)
    定中關係 ATT attribute 紅寶石 (紅 <- 寶石)
    狀中結構 ADV adverbial 特別嚴厲(特別 <- 嚴厲)
    動補結構 CMP complement 打掃完衛生(打掃 -> 完)
    同位語 APS appositive 我本人非常高興(我 <- 本人)
    並列關係 COO coordinate 天空和海洋 (和 -> 海洋)
    介賓關係 POB preposition-object 在陽光下 (在 -> 下)
    左附加關係 LAD left adjunct 天空和海洋 (和 <- 海洋)
    右附加關係 RAD right adjunct 朋友們 (朋友 -> 們)
    獨立結構 IS independent structure 我五歲,他四歲 (各自獨立)
    核心關係 HED head 美麗的花朵爭相開放 (花朵 是整個句子的核心)

    目前採用資料驅動的依存句法分析,將資料分為訓練集和測試集。基於圖 graph-based 和 基於轉移 transition-based 兩種分析方法。

    基於圖 graph-based:

    ​ 將句子(字串)和對應的依存樹組成的資料做為訓練資料,訓練目的是,學習一個可以預測一句依存樹未知的最佳依存樹。建模過程需要增加依存樹限制條件,例如圖的邊是有向邊,在有向的路徑上,一個詞只能被存取一次,每個詞只能有一個支配節點。

    基於轉移 transition-based:

    ​ 將圖預測轉變為序列標註問題,主要的轉移系統有 arg-eager, arc-standard, easy-first 等等,基於轉移的系統有三個操作

    1. 將單字從 buffer 移入 stack,或將單字從 stack 移回
    2. 從 stack 將單字 pop
    3. 建立帶有 label 的有向邊(左向邊或右向邊)

    基於圖在短句的表現較好,但長句容易受到早期錯誤的影響。基於轉移在長句有較好的表現,但是缺乏豐富的結構化特徵。

    • 片語結構句法分析 Phrase-structure Syntactic Parsing,也稱為成分句法分析 Constituent Syntatic Parsing:識別句子中的片語結構和片語之間的層級句法關係

    基於 Context Free Grammar (CFG),其規則分為人工編寫規則和資料驅動的自動學習規則兩類。

    人工編寫的缺點:規則之間的衝突會隨規則數量的增多而加劇,不易增加新規則。資料驅動的自動學習規則,開發週期短,且規則運作良好,目前為主流方法。

    為了在句法分析導入統計資訊,需要將 CFG 擴充為 PCFG: Probabilistic Context Free Grammar,最後利用 Maxmimum Likelihood Estimation (MLE) 計算每一條規則的機率。

    因為 CFG 的獨立性假設過於嚴格(一條文法規則的確定,僅與該規則左側句子的非終結符號有關,與上下文資訊無關),導致文法中缺乏其他資訊用於規則消岐,因此分析器的效能較低。這個問題有兩種弱化 CFG 假設的方法:一種是使用詞彙化 Lexicalization 方法,一種是使用符號重標記 Symbol Refinement 的方法,透過改寫非終結符號的方式,將上下文資訊導入句法分析器。

    • 深層文法句法分析,利用深層文法,對句子進行深層句法及語意分析,包含詞彙化樹鄰接文法 Lexicalized Tree Adjoining Grammar (LTAG)、詞彙功能文法 Lexical Functional Grammar (LFG)、組合範疇文法 Combinatory Categorial Grammar (CCG)

    依存句法分析是淺層句法分析,適合用在多語言環境,深層文法採用相對複雜的文法,句法和語意資訊較為豐富,分析器複雜。

    先將句子的基礎特徵(詞、詞性、類別標籤)向量化,再利用 MLP 進行特徵提取。

    深層學習的優點:

    1. 只需要句子的基礎特徵,利用向量乘法組合向量化特徵,理論上可達到任意元的特徵組合
    2. 能使用更多基礎特徵
  • 語意分析 semantic analysis

    語意是資料對應現實世界中,事物所代表的涵義。語意分析涉及語言學、計算語言學、人工智慧、機器學習、認知語言等多個學科,最終目的是理解句子表達的真實涵義。

    1. 語意分析在機器翻譯的應用,統計機器翻譯提升了翻譯的效能
    2. 語意搜尋:搜尋不在拘泥於根據輸入關鍵字字面的意思,而是要了解背後真正的意圖
    3. 實現大數據的理解與價值發現的有效手段

    Chatbot 利用語意分析可得知使用者的意圖、情感,藉由上下文情境的語意建模,保持 chatbot 的個性一致

自然語言表示

三種常用的文字特徵表示模型,將自然語言表示為電腦可以理解的形式

  • 詞袋模型 Bag Of Words, BOW

    最初用在資訊檢索 Information Retrieval (IR)。每個詞的出現都不依賴於其他詞是否出現的假設。在表示文件時,可忽略文字的語序、文法、句法,將其視為片語的組合

    ex: 以下兩份文件

    (1) 台北今天下雨,台中也下。

    (2) 台北和台中今天都下雨。

    建構辭典(前面是索引):

    Dictionary = {1: "台北", 2: "今天", 3: "下", 4: "雨", 5: "台中", 6: "也", 7: "和", 8: "都"}

    根據索引,可用向量表示該單詞在文件中出現的次數

    下:在第一句話出現 2 次,第二句話中出現 1 次

    (1) [1, 1, 2, 1, 1, 1, 0, 0]
    (2) [1, 1, 1, 1, 1, 0, 1, 1]

    詞袋模型可將文件轉換為次數的向量,但沒有表示單詞在原句中出現的位置,這是明顯的缺點。

  • 詞頻-逆向文件頻率 TF-IDF: Term Frequency-Inverse Document Frequency

    基於統計的加權方法,常用於 IR,用具體詞彙在文件中出現的次數,和其在語料庫出現的次數,評估該詞彙對相關文件的重要程度。TF-IDF 常被搜尋引擎用來評估文件與查詢之間的相關程度。

    TF (Term Frequency 詞頻) 就是詞語在文件出現的次數

    IDF (Inverse Document Frequency 逆向文件頻率) 詞語普遍重要性的度量。

    詞彙在指定文件內的高 TF,高 IDF,將使該詞彙在文件內享有較高權重的 TF-IDF

    TF-IDF 傾向於過濾常見詞彙,保留重要詞彙的做法。核心概念是:在一篇文件中出現頻率高,但在其他文件很少出現的詞彙,有較好的類別區分效果。

    但實際上,同一類文件頻繁出現的詞彙,往往代表該類文件的特徵,這類詞彙有較高的權重,應該視為該類文件的特徵詞,這是 IDF 的不足處。

  • 詞嵌入 Word Embedding

    將深度學習導入 NLP 的核心技術之一。

    要在 NLP 使用機器學習,必須找到一種適合將自然語言數學化的方法。最初用 one hot representation 方法,利用詞表大小維度的向量描述單詞,每個向量中多數元素為 0,只有該詞彙在詞表對應位置的維度為 1。

    ex: 詞表 H,包含 N 個詞彙,「雨傘」是 H 的第 2 個詞彙,「傘」是 H 的第 4 個詞彙

    「雨傘」的 one hot representation 為 [0, 1, 0 , 0, 0, 0 .....]

    「傘」的 one hot representation 為 [0, 0, 0 , 1, 0, 0 .....]

    同時分配 ID,「雨傘」的 ID 為 2,「傘」的 ID 為 4

    One-Hot Encoding 將所有詞彙單獨考慮,以向量表示,難於發現同義及反義的詞彙關係。另外因向量過於稀疏,在機器學習容易造成維度災難。

    Word Embedding 在基於 One-Hot Encoding 時,增加了單詞間的語意聯繫,並降低詞向量維度。

    一個包含 t 個詞彙 \[ w_1, w_2, ..., w_t \] 的句子,自然語言的機率 (也稱為語言模型)為 \[ p(w_1, w_2, ..., w_t) \\ = p(w_1)*p(w_2|w_1)*...*p(w_t|w_1,w_2,...,w_{t-1}) \\ = p(w_t|w_1,w_2,...,w_{t-1}) \]

    對於 N-gram 模型來說

    \[ p(w_1, w_2, ..., w_t) ≌ p(w_t|w_{t-n+1},w_{t-n+2},...,w_{t-1}) \]

    Yoshua Bengio 發表三層神經網路建構語言模型的方法

    第一層:輸入層,輸入句子內已知前 n-1 個詞彙的詞向量,將前 n-1 個詞向量拼接成一個向量

    第二層:隱藏層

    第三層:輸出層,第 i 個節點的值,等於下一個詞為 \( w_i \) 的機率的對數

    在最佳化模型的過程中,同時對單詞的詞向量進行優化。最佳化後可得到語言模型及詞向量。

    Encoder-Decoder 是文字處理的框架,可用於 chatbot、機器翻譯、文字摘要、句法分析。也就是由句子 (篇章)X,產生句子(篇章) Y 的通用模型。如果 X, Y 是相異語言,就是自動翻譯器。

    Encoder 負責將 X 變換為中間語意 C,Decoder 將 C 和歷史存在的 Y,產生 i 時刻的 \(Y_i\)

    Google 於 2008 年發表 BERT

    對於基於生成的 chatbot,除了 Encoder-Decoder 解決核心問題外,還需要注意多輪對話、安全回答、個性一致的問題

    • 多輪對話問題

      要將前文聊天資料,導入Encoder-Decoder,可產生更好的回應

      簡單拼接 context 及本次輸入句子的方法,因為 RNN 輸入模型的長度增長而減低成效 -> RNN 對於過長輸入資料敏感的問題

      以多層前饋神經網路替代 RNN:多層前饋神經網路的輸出,代表上下文聊天資訊,和目前輸入內容的中間語意表示,Decoder 根據中間語意產生回覆。

      另一種方法:階層式神經網路 Hierarchical Neural Network (HNN),本質類似 Encode-Decoder框架。Encoder 採用二級結構,以第一級句子 RNN(Sentence RNN) 編碼句子的每一個單詞,形成中間語意,第二級句子 RNN 根據上下文句子出現的先後順序序列,對第一級中間語意進行編碼。這個 RNN 稱為 Context RNN

      RNN 對出現在多輪對話的語言片段進行編碼,Context RNN 對時間進行編碼,解碼 RNN 負責對下一個對話回覆進行預測

    • 避免安全回答

      生成式 chatbot 的問題是「安全回答」,例如不管使用者輸入什麼,都回答 I Don't Know、Sure、呵呵、是嗎

      會發生這個現象的原因是,訓練的資料確實包含很多無意義的回答。問題在於訓練資料的詞語在句子不同位置的機率分佈,呈現出明顯的長尾狀況。也就是神經網路回覆陷入局部最佳解,可藉由給模型增加一些干擾,使其跳出局部最佳解。因此就把 Generative Adversarial Network (GAN) 導入聊天回覆生成系統,以解決安全回答的問題

      將系統分為生成器 Generator、判別器 Discriminator 兩個子系統,生成器使用 seq2seq 模型,以前文為輸入,再輸出對話語句,判別器用來判斷前文產生的回答是否接近人類行為。生成器不斷改良答案欺騙判別器,判別器不斷以生成器的回答作為負例。直到兩者收斂。

    • 個性一致問題

      chatbot 會被當作有個性的虛擬人物,該人物必須要有一致的年齡、喜好、習慣、語言風格。

      seq2seq 訓練都是單句資訊對單據回覆的映射關係,沒有統一的個性資訊。利用 seq2seq 很難保持個性資訊一致

      方法:將預定義的個性化資訊,透過 word embedding 方法呈現,仍然採用 seq2seq,也就是把個性資訊匯入 Decoder,

  • 基於知識圖譜的 NLU

    知識圖譜是結構化的語意知識庫,以符號形式描述真實世界存在的各種實體、概念、及其相互關係,其基本單位是「實體-關係-實體」形式的三元組,以及實體及其相關屬性的「屬性-值」對(Attribute-Value Pair、AVP)

    知識圖譜用 global 唯一的識別字來標籤知識圖譜的每個實體或概念。每個「屬性-值」都是對實體內在具體特型的刻畫,利用關係連接兩個實體,描述實體間的關係,構成網路知識結構。知識圖譜可視為一張巨大、包含節點與邊的圖,其中節點表示真實世界的實體或概念,網路的邊代表實體間的各種語意關係,這個圖模型可用 W3C 提出的 Resource Description Framework (RDF) 或屬性圖表示

    知識圖譜是知識表示與推理、資料庫、資訊檢索、自然語言處理等多種技術融合的產物

    • 知識表示

      知識在電腦內儲存和處理格式,一般以三元組表示一筆知識,頭尾實體是圖譜的節點,關係是圖譜的邊。

      知識表示使用的資料結構,最常見的是 graph 和 tree。每個邊和節點都有中繼資料。現有圖形式資料庫的缺點:在知識表示存在侷限,導致專案成本高,無法混合表示結構與非結構化資料。

      知識酷的資料由結構與非結構化資料混合而成,專案上,廣為接受是使用 Tree,其中 JSON 滿足了結構與非結構化混合的需求,是目前最常用的知識表示方式。缺點是無法結合 machine learning

      為解決以上問題,提出基於幾何空間的知識表示方法,每個實體是空間中的一點,關係是平移向量,每個元組都以平移原則作為基本幾何表示形式,頭實體可按照關係向量移動到尾實體。根據這種方法,可設計出基於統計的AI演算法。

    • 知識建構

      結構化資料可用簡單的映射,對應到知識圖譜

      半結構化資料 ex: html,可用 wrapper,提取資訊存放到特定格式的知識圖譜

      非結構化資料,以 text mining 發現文字隱含的模式

    • 知識融合

      由各來源提取知識後,要融合為一個知識庫,融合過程。

      本體 ontology,提供統一的術語字典,構成術語間的關係,根據具體業務建立或修改資料模型的功能

      本體比對演算法:模式比對 schema matching和實例比對 instance matching 兩種

      模式比對 schema matching:尋找本體中屬性和概念之間的對應關係,大規模本體比對一般使用 anchor 技術,將來自兩個本體的相似概念作為起點,根據這兩個相似概念的父概念、子概念,逐漸建構小型的相似片段,進而找出相符的概念。同時利用反覆運算,將新的相符概念作為新的 anchor,再根據 anchor 相關本體的鄰居資訊,建置新的片段。不斷反覆此過程,直到找不到新的相符概念為止。分而治之的方法

      實例比對可評估來自不同異質資料實例對的相似度,評估的結果用來判斷這些實力是否指向特定領域的相同實體。利用 Locality-Sensitive Hashing 提高實例比對的可擴充性方法,與使用向量空間模型表示實例,基於規則採用 inverted index,取得最初相符候選的方法


      chatbot 特殊需求

    • 需要個性化的知識圖譜

    • 需要動態知識圖譜

      要有生活規律,描述生活軌跡

    • 需要刻畫主觀情感的知識圖譜

      回覆時,除客觀事實外,要增加個性化主觀認知的情感元素

    • 要提供 API

    • 要有多媒體知識圖譜

      結合圖片、語音、文字

自然語言生成

分為 pipeline, integrated 兩種

pipeline 個模組間互相獨立,只有輸入、輸出界面。

integrated 系統模組之間緊密結合

integrated 符合人腦設計,但實作困難,現時常用的是 pipeline,有文字規劃(說什麼)、句子規劃(怎麼說)、句法實現(讓句子連貫)三個模組。

chatbot 對話生成技術

  • 檢索式

    由對話資料庫找出最佳回覆,只能用固定語言回覆

  • 生成式

    由 chatbot 創造句子

    • 需要關聯文法結構和應用特有的語意表徵
    • context sensitive,語言要整合時間、地點、位置等資訊
    • 基於 machine learning 產生的回覆很難解釋,難以被理解

基於範本的 NLG

範本由 sentence和 word 組成,是含有變數的 sentence,word 是範本中變數對應的可能值

適合任務驅動的對話系統

  1. 對話管理模組會根據目前的對話狀態,使用者輸入的資料,生成下一步動作的相關資訊,也就是選擇句子範本,及可選的詞彙範本
  2. NLU 需要利用詞彙範本、句子範本、有限狀態機,進行 slot filling 的相關工作

基於深度學習的 NLG

GAN 在電腦視覺,尤其是圖形生成方面有顯著成果。

對話管理

維護更新對話狀態與動作選擇,對話狀態是一種能夠處理聊天資料的表徵。包含所有可能會影響機器下一步決策的資訊。

動作選擇是基於目前狀態,選擇下一步合適的動作,例如向 user 詢問需要補充的資訊,執行要求的動作等等。

ex: user 輸入「幫我給媽媽預定一束花」,接下來可能是

  1. 詢問可接受的價位「請問預期價位如何?」
  2. 確認可接受的價位「像上次買兩百元的花可以嗎?」
  3. 直接預訂「好的,已預訂價值兩百元的紅玫瑰」

模組

  • 對話行為識別

    預先定義或動態產生,使用者對話意圖的抽象表示形式。分為封閉式、開放式兩種。

    封閉式:將對話意圖映射到預先定義好的對話行為類別體系,通常用在特定領域/任務,ex: 設定鬧鐘、票務預訂、酒店預訂

    開放式:對話行為沒有預先定義好的對話行為類別體系,ex: 閒聊系統

  • 對話狀態識別

    狀態跟 context 及對話行為相關,狀態轉移由前一時刻的對話狀態,與目前使用者輸入的對話行為決定

  • 對話策略學習

    讓機器從「人-人」的真實對話資料學習對話的行為與狀態

  • 對話獎勵

    通常將槽位填充效率、回覆流行度等參數納入考量

    基於強化學習的長期獎勵機制


常見的對話管理方法

  1. Finite State Machine FSM

    要人工定義對話系統可能出現的所有狀態,簡單易用,但需要人工設計,無法用於複雜場景

  2. 基於統計

    將對話過程表示為部分可見的馬可夫決策過程,只需要定義決策過程的狀態和動作,機器可透過學習得到不同狀態間的轉移關係

  3. 基於神經網路

    用神經網路學習動作選擇的策略,將自然語言理解的輸出,及其他特徵,都作為神經網路的輸入,把選擇的動作作為神經網路的輸出。需要大量訓練資料,為獲得大規模應用驗證

  4. 基於框架

    slot-value pair

    用於特定領域的對話系統


對話管理的挑戰

  1. 手工編寫的對話策略,難以涵蓋所有對話場景
  2. 基於統計與神經網路的方法,需要大量對話資料
  3. 要求大量的領域知識、對話知識,以產生有意義的回覆

One-shot Learning 和 Zero-shot Learning 可用少量(無)樣本進行訓練,以解決對話系統「冷開機」問題,透過 reward function 學習,不斷增加對話模型

seqGAN 採用對抗網路實作離散序列的生成模型,解決 GAN 難以應用於自然語言處理領域的問題,並可用來選擇最佳的獎勵函數與參數

2021/07/26

如何查看遠端機器的 mnesia

要先知道 remote erlang node 的 cookie 值

在 local 機器先啟動一個 erlang node, setcookie 的部分要設定跟 remote erlang node 一樣

erl -setcookie cookievalue -sname obs1
% 啟動 observer
(obs1@cmbp)1> observer:start().

在上面選單的 Nodes -> Connect to Node,輸入遠端 nodename@hostname

larzio1@larzio

就可以查看遠端的 mnesia 資料了

Note: 只要點 Application 頁籤就會 crash,不知道原因

2021/07/19

Distributed OTP Applications

OTP application 可以轉換為 distributed application,用途是在多個 erlang cluster nodes 之間,distributed application 能夠在這些節點中間,只運作一個 application。

OTP distributed application 能夠設定為一個運作的主節點,其他節點則是在該主節點失效時,能夠接手選擇產生另一個 application 繼續運作,這是 failover。當主節點恢復時,這個 application 會重新在主節點啟動,原本接手的節點會停止該 application,這是 takeover。運作的細節可參考 Distributed OTP Applications

以該文章的 8ball 實例,測試三個節點運作 OTP distributed application 的狀況。

產生 app

# 透過 rebar 產生 app
rebar create-app appid=m8ball

修改 src 裡面的檔案

m8ball.app.src

{application, m8ball,
 [{vsn, "1.0.0"},
  {description, "Answer vital questions"},
%%  {modules, [m8ball, m8ball_sup, m8ball_server]},
  {applications, [stdlib, kernel, crypto]},
%%  {registered, [m8ball, m8ball_sup, m8ball_server]},
  {mod, {m8ball, []}},
  {env, [
    {answers, {<<"Yes">>, <<"No">>, <<"Doubtful">>,
               <<"I don't like your tone">>, <<"Of course">>,
               <<"Of course not">>, <<"*backs away slowly and runs away*">>}}
  ]}
 ]}.

m8ball.erl

-module(m8ball).
-behaviour(application).
-export([start/2, stop/1]).
-export([ask/1]).

%%%%%%%%%%%%%%%%%
%%% CALLBACKS %%%
%%%%%%%%%%%%%%%%%

%% start({failover, Node}, Args) is only called
%% when a start_phase key is defined.
%% application:which_applications().
start(normal, []) ->
    io:format("application normal start m8ball~n"),
    m8ball_sup:start_link();
start({takeover, OtherNode}, []) ->
    io:format("application takeover m8ball from ~p~n", [OtherNode]),
    m8ball_sup:start_link().

stop(_State) ->
    io:format("application stop m8ball~n"),
    ok.

%%%%%%%%%%%%%%%%%
%%% INTERFACE %%%
%%%%%%%%%%%%%%%%%
ask(Question) ->
    m8ball_server:ask(Question).

m8ball_server.erl

-module(m8ball_server).
-behaviour(gen_server).
-export([start_link/0, stop/0, ask/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         code_change/3, terminate/2]).

%%%%%%%%%%%%%%%%%
%%% INTERFACE %%%
%%%%%%%%%%%%%%%%%
start_link() ->
    gen_server:start_link({global, ?MODULE}, ?MODULE, [], []).

stop() ->
    gen_server:call({global, ?MODULE}, stop).

ask(_Question) -> % the question doesn't matter!
    gen_server:call({global, ?MODULE}, question).

%%%%%%%%%%%%%%%%%
%%% CALLBACKS %%%
%%%%%%%%%%%%%%%%%
init([]) ->
    % <<A:32, B:32, C:32>> = crypto:strong_rand_bytes(12),
    % rand:seed(A,B,C),
    <<I1:32/unsigned-integer, I2:32/unsigned-integer, I3:32/unsigned-integer>> = crypto:strong_rand_bytes(12),
    rand:seed(exsplus, {I1, I2, I3}),
    {ok, []}.

handle_call(question, _From, State) ->
    {ok, Answers} = application:get_env(m8ball, answers),
    Answer = element(rand:uniform(tuple_size(Answers)), Answers),
    {reply, Answer, State};
handle_call(stop, _From, State) ->
    {stop, normal, ok, State};
handle_call(_Call, _From, State) ->
    {noreply, State}.

handle_cast(_Cast, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

terminate(_Reason, _State) ->
    ok.

m8ball_sup.erl

-module(m8ball_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).

start_link() ->
    supervisor:start_link({global,?MODULE}, ?MODULE, []).

init([]) ->
    {ok, {{one_for_one, 1, 10},
          [{m8ball,
            {m8ball_server, start_link, []},
            permanent,
            5000,
            worker,
            [m8ball_server]
          }]}}.

config files

a.config

[{kernel,
  [{distributed, [{m8ball,
                   3000,
                  [a@cmbp, {b@cmbp, c@cmbp}]}]},
   {sync_nodes_mandatory, []},
   {sync_nodes_optional, [b@cmbp, c@cmbp]},
   {sync_nodes_timeout, 5000}
  ]
 }
].

b.config

[{kernel,
  [{distributed, [{m8ball,
                   3000,
                  [a@cmbp, {b@cmbp, c@cmbp}]}]},
   {sync_nodes_mandatory, []},
   {sync_nodes_optional, [a@cmbp, c@cmbp]},
   {sync_nodes_timeout, 5000}
  ]
 }
].

c.config

[{kernel,
  [{distributed, [{m8ball,
                   3000,
                  [a@cmbp, {b@cmbp, c@cmbp}]}]},
   {sync_nodes_mandatory, []},
   {sync_nodes_optional, [a@cmbp, b@cmbp]},
   {sync_nodes_timeout, 5000}
  ]
 }
].

startup script

runa.sh

# erl -sname a -config a.config -pa ebin -eval 'application:start(crypto), application:start(m8ball)'

erl -sname a -config a.config -pa ebin -eval 'application:ensure_all_started(m8ball)'

runb.sh

# erl -sname b -config b.config -pa ebin -eval 'application:start(crypto), application:start(m8ball)'

erl -sname b -config b.config -pa ebin -eval 'application:ensure_all_started(m8ball)'

runc.sh

# erl -sname c -config c.config -pa ebin -eval 'application:start(crypto), application:start(m8ball)'

erl -sname c -config c.config -pa ebin -eval 'application:ensure_all_started(m8ball)'

compile

rebar compile

測試

先啟動 node a,因為 b, c 還沒有啟動,這時候, a 在啟動時,會等 5s,看看 b, c 是不是有啟動。 5s 後,才會進入 console。

application:which_applications(). 可以查詢目前運作的 applications

./runa.sh

Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [kernel-poll:false]

Eshell V9.3  (abort with ^G)
(a@cmbp)1> application:which_applications().
[{m8ball,"Answer vital questions","1.0.0"},
 {crypto,"CRYPTO","4.2.1"},
 {stdlib,"ERTS  CXC 138 10","3.4.4"},
 {kernel,"ERTS  CXC 138 10","5.4.3"}]

如果啟動 b,一樣會等 5s。但如果同時再啟動 c,就會直接進入 console。

./runb.sh
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [kernel-poll:false]

Eshell V9.3  (abort with ^G)
(b@cmbp)1> application:which_applications().
[{crypto,"CRYPTO","4.2.1"},
 {stdlib,"ERTS  CXC 138 10","3.4.4"},
 {kernel,"ERTS  CXC 138 10","5.4.3"}]
./runc.sh
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [kernel-poll:false]

Eshell V9.3  (abort with ^G)
(c@cmbp)1> application:which_applications().
[{crypto,"CRYPTO","4.2.1"},
 {stdlib,"ERTS  CXC 138 10","3.4.4"},
 {kernel,"ERTS  CXC 138 10","5.4.3"}]

透過 application 查詢,可得知 m8ball 運作在 node a

如果把 node a 關掉,等待 5s 後,進行 application 查詢,可發現 m8ball 運作在 node b

(b@cmbp)2> application:which_applications().
[{m8ball,"Answer vital questions","1.0.0"},
 {crypto,"CRYPTO","4.2.1"},
 {stdlib,"ERTS  CXC 138 10","3.4.4"},
 {kernel,"ERTS  CXC 138 10","5.4.3"}]

再把 node b 關掉,等待 5s 後,進行 application 查詢,可發現 m8ball 運作在 node c。

重新啟動 node a,這時候 node c 的 application 會被停止

(c@cmbp)6>
=INFO REPORT==== 26-Mar-2021::15:45:28 ===
    application: m8ball
    exited: stopped
    type: temporary

但是並沒有回到 node a 運作。這邊認為有可能是 erlang 的問題。

(a@cmbp)1>
=INFO REPORT==== 26-Mar-2021::15:45:28 ===
    application: m8ball
    exited: {{already_started,<5269.88.0>},
             {m8ball,start,[{takeover,c@cmbp},[]]}}
    type: temporary

=INFO REPORT==== 26-Mar-2021::15:45:28 ===
    application: crypto
    exited: stopped
    type: temporary

在 takeover 時, b,c 兩個 node 如果少了一個,就會發生問題。

再重新執行一次 a

$ ./runa.sh
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [kernel-poll:false]

Eshell V9.3  (abort with ^G)
(a@cmbp)1> application:which_applications().
[{m8ball,"Answer vital questions","1.0.0"},
 {crypto,"CRYPTO","4.2.1"},
 {stdlib,"ERTS  CXC 138 10","3.4.4"},
 {kernel,"ERTS  CXC 138 10","5.4.3"}]

Note:如果只做兩個 Node,這時候就沒有發生上面 takeover 的問題。


在任意一個節點,只要 m8ball 有在某一個節點運作,就可以使用 m8ball

(b@cmbp)3> m8ball:ask("Questions?").
<<"*backs away slowly and runs away*">>
(b@cmbp)4> m8ball:ask("Questions?").
<<"No">>
(b@cmbp)5> m8ball:ask("Questions?").
<<"Of course">>

References

Distributed OTP Applications