這又是非serious系列的簡(jiǎn)單嘗試0.0 最近大概只有買買買能讓我內(nèi)心得到平靜... 這篇的主題其實(shí)是長(zhǎng)連接哈~ 不喜歡的朋友可以跳過(guò)啦~
1. 搭建服務(wù)器
第一步就是你需要先安裝nodejs
稚补,然后安裝websocket:
npm install nodejs-websocket
然后你按照下面reference里面第一篇的方法就可以寫個(gè)server以及client html啦肋联,啟動(dòng)server就用node xxx.js
即可蚓挤。client就直接雙擊打開(kāi)html他自己就會(huì)連接你啟動(dòng)的server(localhost:3000)寇窑。
那么 websocket 是啥呢杨何?
Websocket是一個(gè)持久化的協(xié)議灌闺,相對(duì)于HTTP這種非持久的協(xié)議來(lái)說(shuō)霎俩。HTTP通常是有個(gè)request袭厂,然后立刻server就會(huì)給你response剑刑,但是server不能主動(dòng)發(fā)起給客戶端傳輸數(shù)據(jù)媳纬,必須要有request。即使是long poll 和 ajax輪詢 其實(shí)本質(zhì)還是需要client先發(fā)request施掏。
WebSocket 解決的第一個(gè)問(wèn)題是钮惠,通過(guò)第一個(gè) HTTP request 建立了 TCP 連接之后,之后的交換數(shù)據(jù)都不需要再發(fā) HTTP request了七芭,使得這個(gè)長(zhǎng)連接變成了一個(gè)真長(zhǎng)連接素挽。
在此基礎(chǔ)上 WebSocket 還是一個(gè)雙通道的連接,在同一個(gè) TCP 連接上既可以發(fā)也可以收信息狸驳。此外還有 multiplexing 功能毁菱,幾個(gè)不同的 URI 可以復(fù)用同一個(gè) WebSocket 連接米死。這些都是原來(lái)的 HTTP 不能做到的。
看起來(lái)是不是特別適合聊天場(chǎng)景贮庞!其實(shí)我本來(lái)想寫個(gè)直播的消息推送的峦筒,現(xiàn)在決定還是寫個(gè)聊天app吧~ (善變的女人...
2. 實(shí)現(xiàn)多播
首先先做一個(gè)簡(jiǎn)單地修改,讓多個(gè)客戶端(這里指網(wǎng)頁(yè)哈)可以連到server窗慎,然后一個(gè)客戶端的消息會(huì)廣播給所有客戶端物喷。
其實(shí)改動(dòng)很小哦,server端加一個(gè)廣播函數(shù)調(diào)用就可以啦:
function broadcast(server, msg) {
//server.connections是一個(gè)數(shù)組遮斥,包含所有連接進(jìn)來(lái)的客戶端
server.connections.forEach(function (conn) {
//connection.sendText方法可以發(fā)送指定的內(nèi)容到客戶端峦失,傳入一個(gè)字符串
//這里為遍歷每一個(gè)客戶端為其發(fā)送內(nèi)容
conn.sendText(msg);
})
}
var server = ws.createServer(function(conn){
console.log('New connection')
conn.on("text",function(str){
console.log("Received"+str)
// conn.sendText(str.toUpperCase()+"!!!") //大寫收到的數(shù)據(jù)
// conn.sendText(str) //收到直接發(fā)回去
broadcast(server,str);
})
……
}).listen(PORT)
客戶端其實(shí)不用改,跑起來(lái)就是這樣的:(它不讓我傳gif.. 傳了就鎖)
- server會(huì)有多個(gè)connection术吗,每個(gè)connect代表一個(gè)客戶端尉辑,如果你想實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)的消息傳輸,標(biāo)記住connection即可~
3. 標(biāo)識(shí)用戶1對(duì)1聊天
現(xiàn)在我們要加入login功能來(lái)實(shí)現(xiàn)一對(duì)一的聊天啦~
大概步驟是醬紫的:
- client加login功能较屿,并且在server端用login msg里面的name標(biāo)識(shí)connection
- client加send to who的功能隧魄,用戶填寫要發(fā)送消息給誰(shuí)
- server端根據(jù)用戶傳入消息的send to who標(biāo)識(shí),從自己connections里面找到conn隘蝎,然后send msg
server代碼大概是醬紫的:
var server = ws.createServer(function(conn){
console.log('New connection')
conn.on('text',function(message){
let info=JSON.parse(message);
if (info.type==='login') {
conn['user']=info.user;
console.log("login: "+info.user)
} else if (info.type==='message'){
server.connections.forEach(function (conn) {
console.log("has client: "+conn['user'])
if(conn['user']===info.to){
conn.send(info.message)
console.log("send: "+info.message)
}
})
console.log("msg to: "+info.to)
}
})
……
}).listen(PORT)
然后是client html的:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>WebSocket</title>
</head>
<body>
<h1>Echo Test</h1>
<input id="username" type="text"/>
<button id="loginBtn">Login</button>
<br/>
<input id="toUsername" type="text" placeholder="send to" />
<br/>
<input id="sendTxt" type="text"/>
<button id="sendBtn">發(fā)送</button>
<div id="recv"></div>
<script type="text/javascript">
// //官方示例的服務(wù)器
// var WebSocket = new WebSocket("ws://echo.websocket.org");
// wsServer搭建的服務(wù)器
var WebSocket = new WebSocket("ws://localhost:3000/");
WebSocket.onopen = function(){
console.log('websocket open');
document.getElementById("recv").innerHTML = "Connected";
}
WebSocket.onclose = function(){
console.log('websocket close');
}
WebSocket.onmessage = function(e){
console.log(e.data);
document.getElementById("recv").innerHTML = e.data;
}
document.getElementById("sendBtn").onclick = function(){
var txt = document.getElementById("sendTxt").value;
var toUser = document.getElementById("toUsername").value;
WebSocket.send(
JSON.stringify({
type:"message",
to:toUser, // 需要發(fā)送給誰(shuí)
message:txt,
})
);
}
document.getElementById("loginBtn").onclick = function(){
var username = document.getElementById("username").value;
WebSocket.send(
JSON.stringify({
type:"login",
user:username
})
);
}
</script>
</body>
</html>
然后是演示:(不讓我傳gif碎碎念again)
!
4. 心跳
websocket是前后端交互的長(zhǎng)連接购啄,前后端也都可能因?yàn)橐恍┣闆r導(dǎo)致連接失效并且相互之間沒(méi)有反饋提醒。因此為了保證連接的可持續(xù)性和穩(wěn)定性嘱么,websocket心跳重連就應(yīng)運(yùn)而生狮含。
在使用原生websocket的時(shí)候,如果設(shè)備網(wǎng)絡(luò)斷開(kāi)曼振,不會(huì)立刻觸發(fā)websocket的任何事件几迄,前端也就無(wú)法得知當(dāng)前連接是否已經(jīng)斷開(kāi)。這個(gè)時(shí)候如果調(diào)用websocket.send方法冰评,瀏覽器才會(huì)發(fā)現(xiàn)鏈接斷開(kāi)了乓旗,便會(huì)立刻或者一定短時(shí)間后(不同瀏覽器或者瀏覽器版本可能表現(xiàn)不同)觸發(fā)onclose函數(shù)。
后端websocket服務(wù)也可能出現(xiàn)異常集索,造成連接斷開(kāi)屿愚,這時(shí)前端也并沒(méi)有收到斷開(kāi)通知,因此需要前端定時(shí)發(fā)送心跳消息ping务荆,后端收到ping類型的消息妆距,立馬返回pong消息,告知前端連接正常函匕。如果一定時(shí)間沒(méi)收到pong消息娱据,就說(shuō)明連接不正常,前端便會(huì)執(zhí)行重連盅惜。
為了解決以上兩個(gè)問(wèn)題中剩,以前端作為主動(dòng)方忌穿,定時(shí)發(fā)送ping消息,用于檢測(cè)網(wǎng)絡(luò)和前后端連接問(wèn)題结啼。一旦發(fā)現(xiàn)異常掠剑,前端持續(xù)執(zhí)行重連邏輯,直到重連成功郊愧。
也就是說(shuō)前端需要做:
- 在
onclose
/onerror
的時(shí)候重連 - 設(shè)置定時(shí)器朴译,定時(shí)給server發(fā)消息
- 如果超時(shí)沒(méi)有拿到response,會(huì)自動(dòng)觸發(fā)
onclose
引發(fā)重連
服務(wù)端需要做:
- 收到client的心跳信號(hào)以后回復(fù)
5. 客戶端接入socket
首先你需要一個(gè) pod 庫(kù)SocketRocket
以及一個(gè)工具類
雖然你可能需要改一下上面那個(gè)工具類属铁,因?yàn)橛行┙涌诓挥昧嗣呤伲琤ut大多都是可以用的,現(xiàn)在你只要手機(jī)端搭建一個(gè)簡(jiǎn)單頁(yè)面就可以啦:
#import "SocketViewController.h"
#import "SocketRocketUtility.h"
#import <YYKit/NSDictionary+YYAdd.h>
@interface SocketViewController ()
@property (weak, nonatomic) IBOutlet UITextField *username;
@property (weak, nonatomic) IBOutlet UITextField *toName;
@property (weak, nonatomic) IBOutlet UITextField *msg;
@end
@implementation SocketViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
[[SocketRocketUtility instance] SRWebSocketOpenWithURLString:@"ws://192.168.1.107:3000/"];
}
- (IBAction)loginClicked:(id)sender {
NSString *username = self.username.text;
NSDictionary *dict = @{
@"type" : @"login",
@"user" : username,
};
NSString *jsonStr = [dict jsonStringEncoded];
[[SocketRocketUtility instance] sendString:jsonStr];
}
- (IBAction)sendClicked:(id)sender {
NSString *toName = self.toName.text;
NSString *msg = self.msg.text;
NSDictionary *dict = @{
@"type" : @"message",
@"to" : toName,
@"message" : msg
};
NSString *jsonStr = [dict jsonStringEncoded];
[[SocketRocketUtility instance] sendString:jsonStr];
}
@end
然后現(xiàn)在打開(kāi)server焦蘑,在網(wǎng)頁(yè)和手機(jī)端都打開(kāi)client盯拱,然后這兩個(gè)都可以互相通信了。
好啦這周末的work比較無(wú)聊啦例嘱,只是玩兒一下websocket~ 下周可能會(huì)搞iOS or 看看書吧~
Reference:
https://blog.csdn.net/qq_20367813/article/details/78020930
websocket是啥:參考知乎https://www.zhihu.com/question/20215561
推薦關(guān)于websocket的一篇:https://blog.csdn.net/asd051377305/article/details/108066378
http 長(zhǎng)短連接和websocket:http://caibaojian.com/http-connection-and-websocket.html
聊天參考:https://blog.csdn.net/qq_41097495/article/details/105835100
心跳:https://www.cnblogs.com/1wen/p/5808276.html
iOS接入socket:http://www.reibang.com/p/821b777555d3