一、WebSocket概述
http://www.ruanyifeng.com/blog/2017/05/websocket.html
Workerman一款開源高性能異步PHP socket即時通訊框架https://workerman.net
HTTP是無連接的:有請求才會有響應,如果沒有請求,服務器想主動推送信息給瀏覽器是不可能的。

比如圖文直播、聊天室原理:長輪詢。
setInterval(function(){ $.get() },1000)
間隔一定的時間,主動向服務器發起請求,詢問是否有新消息。
WebSocket是一種網絡通信協議,是HTML5中的新協議。需要服務器和瀏覽器共同支持,實現全雙工通信。

服務器:PHP5.6、Java1.7、Nodejs 6以上。
瀏覽器:Android 6.0及以上版本。
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。
WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
在 WebSocket API 中,瀏覽器和服務器只需要做一個握手的動作,然后,瀏覽器和服務器之間就形成了一條快速通道。兩者之間就直接可以數據互相傳送。
二、Socket.io
socket.io是一個跨瀏覽器支持WebSocket的實時通訊的JS。Nodejs中實現socket非常好用的包。
API:
https://www.npmjs.com/package/socket.io
npm install --save socket.io
默認有一個自動路由的js文件http://127.0.0.1:3000/socket.io/socket.io.js

前端代碼(從官網抄的模板):
<!doctype html> <html> <head> <title>Socket.IO chat</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font: 13px Helvetica, Arial; } form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; } form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; } form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; } #messages { list-style-type: none; margin: 0; padding: 0; } #messages li { padding: 5px 10px; } #messages li:nth-child(odd) { background: #eee; } </style> </head> <body> <ul id="messages"></ul> <form action=""> <input id="m" autocomplete="off" /> <button>Send</button> </form> <script type="text/javascript" src="/socket.io/socket.io.js"></script> <script type="text/javascript"> var socket = io(); </script> </body> </html>
后端:
var express = require('express'); var app = express(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile(__dirname + '/index.html'); }); //監聽客戶端,有用戶連接的時候觸發(建立前后端連接) io.on('connection', function(socket){ console.log('有個用戶連接了'); }); http.listen(3000);
node app.js
現在兩個端已經實時通訊連接上了:


消息收發的響應:
前端emit發:
<script type="text/javascript">
var socket = io();
$("button").click(function(){
socket.emit("info", "你好");
return false;
});
</script>
服務端on收:
io.on('connection', function(socket){
console.log('有個用戶連接了');
socket.on("info", function(data){ console.log(data); });
});
接下來的事情:
實現聊天室功能:如果有某個客戶端用戶將消息發給了服務端,服務端要發給所有已經連接的客戶端,這里就涉及到廣播,廣播就是給所有已經連接服務端的socket對象進行集體的消息發送。
完整的聊天室前端:
<!doctype html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
...
</style>
</head>
<body>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" />
<button>Send</button>
</form>
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript">
var socket = io();
//點擊按鈕發出數據
$("button").click(function(){
socket.emit("info" , {
content : $("#m").val()
});
$("#m").val("");
return false; //阻止瀏覽器默認請求行為,禁止刷新頁面
});
//客戶端收到服務端的msg廣播消息的時候觸發的函數
socket.on("msg", function(data){
$("<li>" + data.content + "</li>").prependTo("ul");
});
</script>
</body>
</html>
后端:
io.on('connection', function(socket){
console.log('有個用戶連接了');
//服務端收到了info的消息
socket.on("info" , function(data){
console.log(data.content);
//立即廣播通知所有已連接的客戶端
io.emit('msg', data);
});
});
http.listen(3000);
三、微信小程序和WebSocket
小程序的上線版本,必須是https協議會wss協議(即websocket的安全版本)
如果結合微信小程序使用,Nodejs不能使用socket.io,因為socket.io在前端需要用script引入一個js文件,但是小程序不支持這樣引入。但是沒有關系,因為小程序自帶websocket的API。

前端開啟:
Page({ onLoad(){ wx.connectSocket({ url: 'ws://127.0.0.1:8080', }) } })
后端需要安裝一個依賴:
https://www.npmjs.com/package/ws
npm install --save ws
后端app.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws){
console.log("有人鏈接了");
});

<!--index.wxml--> <view class="container"> <view class="t">{{a}}</view> <button bindtap="sendmsg">按我</button> </view>
//index.js Page({ data: { a: 0 }, onLoad(){ //前端發起websocket連接 wx.connectSocket({ // 可以在WiFi環境下的IP地址測試 // url: 'ws://192.168.0.150:8080', url: 'ws://127.0.0.1:8080' }) //監聽WebSocket接受到服務器的廣播消息通知事件 wx.onSocketMessage((res)=>{ console.log(res.data) this.setData({ a:res.data }) }) }, //點擊按鈕發送消息給服務端 send(){ wx.sendSocketMessage({ data: "你好!", }) } })
后端app.js
Nodejs的ws這個庫沒有廣播功能,必須讓開發者將socket對象存為數組,要廣播的時候,遍歷數組中每個項,依次給他們發送信息即可。
const WebSocket = require('ws');
//創建連接和監聽端口
const wss = new WebSocket.Server({port:8080});
var ws_arr = []; //存儲所有已經連接的人的ws對象
var a = 0;
//響應客戶端的連接
wss.on('connection', function(ws){
console.log("有人連接了");
ws_arr.push(ws); //將當前進來的人存儲到數組
//監聽客戶端發送的消息
ws.on("message", function(message){
console.log("服務端收到了消息:" + message)
a++;
//遍歷所有人,廣播通知所有客戶端,把消息傳送給他們
ws_arr.forEach(item=>{
item.send(a);
})
})
})
四、搖一搖大PK
微信沒有提供搖一搖API,必須使用加速計,自己寫代碼感應x、y、z的變化。
加速計的坐標軸如圖,是個三維的坐標。我們需要通過x、y、z三個軸的方向的加速度計算出搖動手機時,手機搖動方向的加速度。


index.js
page({ data:{ x:0, y:0, z:0 }, onLoad(){ var lastX = 0; var lastY = 0; wx.onAccelerometerChange((res)=>{ //如果當前的x或y減去上一次x或y的差 大於0.5,就設定為搖一搖成功 if(Math.abs(res.x - lastX) > 0.5 || Math.abs(res.y - lastY) > 0.5){ wx.showToast({ title: "成功" }); lastX = res.x; lastY = res.y; this.setData({ x : res.x, y : res.y, z : res.z }) } }) } })
后端app.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
//存儲所有人的ws對象
var ws_arr = [];
//存儲所有人的分數
// var score_arr = ["nickName":"測試賬戶","avatarUrl":"x.jpg", "n":0];
var score_arr = [];
var a = 0;
wss.on('connection', function(ws){
console.log("有人鏈接了");
ws_arr.push(ws); //將每個進來的用戶存儲到數組
ws.on('message', function(message){
console.log("服務器收到了推送:" + message);
//變為JSON對象
var messageObj = JSON.parse(message);
//當搖一搖時,判斷數組中有沒有這個人,有就讓這個人的n++
var isHave = false;
score_arr.forEach(item=>{
if(item.nickName == messageObj.nickName){
item.n ++;
isHave = true;
}
});
//如果沒有就添加到數組中
if(!isHave){
score_arr.push({
nickName : messageObj.nickName,
avatarUrl: messageObj.avatarUrl,
n : 0
})
}
console.log({"score_arr" : score_arr})
//廣播發送給客戶端(前端)
ws_arr.forEach(item=>{
item.send(JSON.stringify({
"score_arr" : score_arr })); }); });
});
用戶搖一搖案例:
<!--index.wxml--> <view class="container"> <view class="userinfo"> <button open-type="getUserInfo" bindgetuserinfo="getUserInfo">獲取頭像昵稱</button> </view> <view wx:for="{{arr}}"> {{item.nickName}} <image style="width:90px;height:90px;" src="{{item.avatarUrl}}"></image> {{item.n}} </view> </view>
index.js
const app = getApp() Page({ data: { userInfo: {}, hasUserInfo: false, canIUse: wx.canIUse('button.open-type.getUserInfo'), arr : [] }, onLoad: function () { if (app.globalData.userInfo) { this.setData({ userInfo: app.globalData.userInfo, hasUserInfo: true }) } else if (this.data.canIUse) { // 由於 getUserInfo 是網絡請求,可能會在 Page.onLoad 之后才返回 // 所以此處加入 callback 以防止這種情況 app.userInfoReadyCallback = res => { this.setData({ userInfo: res.userInfo, hasUserInfo: true }) } } else { // 在沒有 open-type=getUserInfo 版本的兼容處理 wx.getUserInfo({ success: res => { app.globalData.userInfo = res.userInfo this.setData({ userInfo: res.userInfo, hasUserInfo: true }) } }) } //鏈接socket服務器(可以填WiFi的IP地址測試) wx.connectSocket({ url: 'ws://127.0.0.1:8080' }); //當socket連接打開后,監聽搖一搖: var self = this; var lastX = 0; wx.onSocketOpen(function(res){ wx.onAccelerometerChange(function(res){ //如果當前的x 減去上一次x的差 大於0.6,就設定為搖一搖成功 if(Math.abs(res.x - lastX) > 0.6){ wx.showToast({ title: "搖一搖成功" }); //告訴服務器我是誰 wx.sendSocketMessage({ data: JSON.stringify({ "nickName": self.data.userInfo.nickName, "avatarUrl": self.data.userInfo.avatarUrl }) }) } lastX = res.x; }); }); //接收到服務器廣播信息的時候做的事情 wx.onSocketMessage(function(res){ var obj = JSON.parse(res.data); //轉對象 var arr = obj.score_arr; //按照n值大小排序 arr.sort((a,b)=>{ return b.n - a.n }) self.setData({arr}); //存儲到本地data中的arr數組 }); }, getUserInfo: function (e) { console.log(e) app.globalData.userInfo = e.detail.userInfo this.setData({ userInfo: e.detail.userInfo, hasUserInfo: true }) } });
五、地圖和地理位置
騰訊地理位置服務https://lbs.qq.com/
地圖自己是不能定位的,需要獲取地理位置定位,而且地圖API和地理位置API是分開。
<!--index.wxml--> <view class="container"> <view class="userinfo"> <button open-type="getUserInfo" bindgetuserinfo="getUserInfo">獲取頭像昵稱</button> </view> <map markers="{{markers}}" id="map" longitude="{{longitude}}" latitude="{{latitude}}"
scale="14" style="width:100%;height:300px;"></map> </view>
Page({ data: { markers : [] }, onLoad(){ //頁面加載進來要先定位 var self = this; wx.getLocation({ type: 'gcj02', success: function(res){ self.setData({ latitude: res.latitude, //緯度 longitude: res.longitude //經度 }); } }); //連接socket服務器 wx.connectSocket({ url: 'ws://192.168.1.175:8080' }); //接收到服務器廣播信息的時候做的事情 wx.onSocketMessage(function (res) { var obj = JSON.parse(res.data); console.log(obj) } //微信沒有提供當某人地理位置改變時候的on事件,所以setInterval()。 //每3秒更新一次定位,然后發送給服務端,服務端再通知客戶端 clearInterval(timer); var timer = setInterval(function(){ wx.getLocation({ type: 'gcj02', success: function(res){ wx.sendSocketMessage({ data: JSON.stringify({ "nickName": self.data.userInfo.nickName, "avatarUrl": self.data.userInfo.avatarUrl, "latitude": res.latitude, "longitude": res.longitude }) }) } }); },3000); } });
后端app.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
var ws_arr = []; //存儲所有人的ws對象
var location_arr = []; //存儲所有人的地理位置
wss.on('connection', function (ws) {
console.log("有人鏈接了");
//放入數組
ws_arr.push(ws);
ws.on('message', function (message) {
console.log("服務器收到了推送" + message);
//變為JSON對象
var messageObj = JSON.parse(message);
//判斷數組中有沒有我
var isHave = false;
location_arr.forEach(item=>{
if(item.nickName == messageObj.nickName){
item.latitude = messageObj.latitude;
item.longitude = messageObj.longitude;
isHave = true;
}
});
//如果沒有
if(!isHave){
location_arr.push({
nickName : messageObj.nickName,
avatarUrl: messageObj.avatarUrl,
latitude : messageObj.latitude, longitude: messageObj.longitude
})
}
console.log({"location_arr" : location_arr})
//廣播通知客戶端
ws_arr.forEach(item=>{
item.send(JSON.stringify({
"location_arr" : location_arr
}));
});
});
});
臨時設置大頭針markers

var tempPathObj = {}; //存儲這個人的昵稱,根據昵稱獲取頭像,如這個對象沒有這個人就要下載頭像 //接收到服務器廣播信息的時候做的事情 wx.onSocketMessage(function(res){ var obj = JSON.parse(res.data); self.setData({ markers : [] //清空 }); //iconPath不支持網絡地址,要通過wx.download()接口下載得到臨時地址 obj.location_arr.forEach(item=>{ //根據昵稱,判斷這個對象中有沒有這個人,如果有直接用 if(tempPathObj.hasOwnProperty(self.data.userInfo.nickName)){ self.setData({ markers: [ ...self.data.markers, { //如果對象中有這個人,就直接用這個人的頭像 iconPath: tempPathObj[self.data.userInfo.nickName], id: 0, latitude: item.latitude, longitude: item.longitude, width: 50, height: 50 } ] }); } else { //如果沒有就下載頭像,並且將這個人存起來,以后可以直接用 wx.downloadFile({ url: item.avatarUrl, success(data){ console.log(data.tempFilePath); self.setData({ markers : [ ...self.data.markers, { iconPath: data.tempFilePath, id: 0, latitude: item.latitude, longitude: item.longitude, width: 50, height: 50 } ] }); // console.log(self.data.markers); //將頭像的臨時地址存儲給這個人 tempPathObj[self.data.userInfo.nickName] = data.tempFilePath; } }) } }); });
