基于webrtc物联网硬件控制研究与设计的项目的项目复原——3
前言
上一个帖子说到了将环境配置到了Ubuntu环境。接下来由于自己的物理机并没有摄像头,所以没办法在Ubuntu里模拟摄像头,只能将对应的源代码注释一下,先将ssh渠道跑起来。接下来就记录一下这次的过程,并夹杂着对这个项目的一些解析。
ICE协议的作用和两种模式
我认为ICE协议及其所运行的服务器最终作用就是让通讯双方达成点对点的连接,这件事在没有NAT的网络环境下很容易达成,但是在有NAT存在的复杂网络环境下就需要一套协议来完成。ICE协议是多种协议的集成,其目的是针对不同的网络环境选择不同的子协议来达成通讯的最优解,模式有STUN和TURN这两种模式,其中STUN模式通讯中心只作为正式通讯前双方交换自己信息的中继节点。而TURN模式则是在STUN模式无法解决问题时才使用,其原理是作为双方交流的中继节点,转发数据流,在这种模式下作为中转的服务器将消耗较大资源。这两种模式的示意图如下所示:
- STUN
- TURN
ICE服务器出现问题
根据程序流程正常建立连接之后,会如下报错:
同时设备端会提示如下错误信息:
根据错误提示开启FireFox浏览器的WebRTC的日记可以发现所有ICE中转服务器均挂了
这里我认为原因有两点:
- 修改了RTC.go里面源代码造成的错误;
- 该项目没有实现TURN类型的服务器,只实现了STUN类型的服务器
这两个原因我倾向于第二个原因,因为在论文中完全没有第二种通讯类型的影子,另外WebRTC可以只传输数据而不是必须和媒体流连接。综上所述,我认为可能是Vmware的网络拓扑无法支持STUN协议。接下来就是问题的解决。
问题解决
由于该问题是网络拓扑引起的,这里记录一下出现问题的网络拓扑图:
接下来将网络环境改成桥接模式:
点击虚拟网络编辑器,将VmNet0和自己的上网网卡连接
上网网卡在windows下可在命令行环境下输入ipconfig /all 来查看。
修改完成的网络架构图如下所示:
这个WebRtc就修复好了:
关于服务器web端的页面的一些说明
可以看到,在服务器启动之后输出了以下界面
前两个看名字应该是用于服务器资源获取的,由于没用过这个框架,具体是啥我也不知道。剩下的作用如下:
- /ping
该页面的作用应该是判断服务器是否正常的,服务器返回回来的是一个JSON数据。
- /index
即首页 - /doc
即说明文档 - /answer
该页面不是用于http请求的,是服务器和设备端建立websocket的url连接,通过该连接设备通知服务器设备上线,接受密码等等事宜。在设备端的可以看到相关代码:
u := url.URL{Scheme: "ws", Host: Conf.ServerAddr, Path: "/answer"}
fmt.Println("connecting to ", u.String())
//创建一个新的WebSocket连接
ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
fmt.Println(err)
return
}
- /offer
该页面和answer一样,区别在于这个设备是面向用户的。客户端的相关连接代码如下:
function initWebsocket(){
wsClient = new WebSocket("ws://"+document.location.host+"/offer");
wsClient.onopen = function() {
console.log("connected to server");
initWebRTC();
}
wsClient.onclose = function(e) {
btnClose();
console.log("connection closed (" + e.code + ")");
}
wsClient.onmessage = function(e) {
console.log("message received: " + e.data);
var obj = JSON.parse(e.data);
if(obj.type == "error"){
log(obj.msg);
stopSession();
}else if(obj.type == "answer"){
var remoteSessionDescription = obj.data;
if (remoteSessionDescription === '') {
alert('Session Description must not be empty');
}
try {
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(remoteSessionDescription))));
btnOpen();
} catch (e) {
alert(e);
}
}else if(obj.type == "password"){
$('#login_modal').modal('show');
$('#device_id_show').val(obj.device_id);
$('#password_show').val("");
}
}
}
- devices
该设备维护了一个在线设备列表,所使用的数据类型为JSON
同时,在首页的刷新按钮和在线设备列表的数据来源也是这个地方:
刷新按钮的触发函数如下:
function getDevices(){
$("#devices_list").empty();
$("#dropdown_menu_link").text("请选择你要连接的设备");
$("#dropdown_menu_link").attr("value","");
var el = document.getElementById('remote-video');
el.srcObject = null;
$.ajax({
type:"GET",
url:"/devices",
dataType:"json",
success:function(data){
if(data.data == null){
$("#devices_list").prepend("<a class=\"dropdown-item disabled\" οnclick=\"dropdownShow($(this).text())\">当前没有设备在线</a>");
}else{
$.each(data.data, function (index, value) {
DevicesList[value.device_id] = value.using;
name = value.device_id +(value.using?"<font color=\"red\">[使用中]</font>":"<font color=\"green\">[可用]</font>");
$("#devices_list").prepend("<a class=\"dropdown-item\" οnclick=\"dropdownShow($(this))\" value=\""+value.device_id+"\">"+name+"</a>");
console.log(name);
});
}
},
error:function(jqXHR){
console.log("Error: "+jqXHR.status);
}
});
}
rtc.go修改后源码
package client
//noinspection GoUnresolvedReference
import (
"fmt"
"github.com/gorilla/websocket"
"github.com/pion/webrtc/v2"
"strconv"
"strings"
"webrtc-remote-control-device/lib/signal"
)
func startRTC(ws *websocket.Conn, offer string, stopRTC chan string) {
// Create a new RTCPeerConnection 创建RTC对等连接
peerConnection, err := webrtc.NewPeerConnection(Conf.RTCConfig)
if err != nil {
fmt.Println(err)
return
}
// Set the handler for ICE connection state
// This will notify you when the peer has connected/disconnected
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
})
// Create a audio track
/*audioTrack, err := peerConnection.NewTrack(webrtc.DefaultPayloadTypeOpus, rand.Uint32(), "audio", "pion1")
if err != nil {
fmt.Println(err)
return
}
if _, err = peerConnection.AddTrack(audioTrack); err != nil {
fmt.Println(err)
return
}*/
// Create a video track
/*videoTrack, err := peerConnection.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "video", "pion2")
if err != nil {
fmt.Println(err)
}
if _, err = peerConnection.AddTrack(videoTrack); err != nil {
fmt.Println(err)
return
}*/
peerConnection.OnDataChannel(func(dc *webrtc.DataChannel) {
if dc.Label() == "SSH" {
sshDataChannelHandler(dc)
}
if dc.Label() == "Control" {
controlDataChannelHandler(dc)
}
})
// Set the remote SessionDescription
offer_ := webrtc.SessionDescription{}
signal.Decode(offer, &offer_)
err = peerConnection.SetRemoteDescription(offer_)
if err != nil {
fmt.Println(err)
return
}
// Create an answer
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
fmt.Println(err)
return
}
// Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer)
if err != nil {
fmt.Println(err)
}
// Output the answer in base64 so we can paste it in browser
req := &Session{}
req.Type = "answer"
req.DeviceId = Conf.DeviceId
req.Data = signal.Encode(answer)
if err = ws.WriteJSON(req); err != nil {
fmt.Println(err)
return
}
// Start pushing buffers on these tracks
/*audioPipeline := gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{audioTrack}, Conf.AudioSrc)
videoPipeline := gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{videoTrack}, Conf.VideoSrc)
audioPipeline.Start()
videoPipeline.Start()*/
<-stopRTC
close(stopRTC)
/*audioPipeline.Stop()
videoPipeline.Stop()*/
peerConnection.Close()
return
}
func controlDataChannelHandler(dc *webrtc.DataChannel) {
dc.OnOpen(func() {
err := dc.SendText("please input command")
if err != nil {
fmt.Println("write data error:", err)
dc.Close()
}
})
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
result := controlHandler(msg.Data)
dc.SendText(result)
})
dc.OnClose(func() {
fmt.Printf("Close Control socket")
})
}
func sshDataChannelHandler(dc *webrtc.DataChannel) {
dc.OnOpen(func() {
for {
var user string
var password string
rtcin := make(chan string)
step := make(chan string)
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
user = string(msg.Data)
//fmt.Println(user)
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
password = string(msg.Data)
//fmt.Println(password)
step <- ""
})
})
<-step
sshSession, err := initSSH(user, password, dc, rtcin)
if err != nil {
dc.SendText(err.Error())
continue
}
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
msg_ := string(msg.Data)
if len(msg_) >= 10 {
ss := strings.Fields(msg_)
if ss[0] == "resize" {
cols, _ := strconv.Atoi(ss[1])
rows, _ := strconv.Atoi(ss[2])
sshSession.WindowChange(cols, rows)
//fmt.Println(msg_)
return
}
}
rtcin <- msg_
})
break
}
})
dc.OnClose(func() {
fmt.Printf("Close SSH socket")
})
}