1: webSocket
一般情况Web 应用的交互过程通常是客户端通过浏览器发出一个请求,服务器端接收请求后进行处理并返回结果给客户端,客户端浏览器将信息呈现到浏览器中
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage(mes?d?) 事件来接收服务器返回的数据
Websocket 使用 ws 或 wss 的统一资源标志符,类似于 HTTPS,其中 wss 表示在 TLS 之上的 Websocket
Websocket 使用和 HTTP 相同的 TCP 端口,可以绕过大多数防火墙的限制。默认情况下,Websocket 协议使用 80 端口;运行在 TLS 之上时,默认使用 443 端口。
相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输
客户端(发请求,建立链接):啦啦啦,有没有新信息(Request)
服务端:没有。。(Response)
客户端(发请求,建立链接):啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)
ajax轮询 需要服务器有很快的处理速度和资源。(速度)
long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)
2:TCP 协议
面向连接、传输可靠(保证数据正确性)、有序(保证数据顺序)、传输大量数据(流模式)、速度慢、对系统资源的要求多,程序结构较复杂,
第一次握手
客户端向服务器发出连接请求报文,这时报文首部中的同部位SYN=1,同时随机生成初始序列号 seq=x,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状
态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。这个三次握手中的开始。表示客户端想要和服务端建立连接。
第二次握手
TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己随机初始化一个序列号 seq=y,此
时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。这个报文带有SYN(建立连接)和ACK(确认)标志,询问客户端
是否准备好。
第三次握手
TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。
TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。这里客户端表示我已经准备好。
思考:为什么要三次握手呢,有人说两次握手就好了
举例:已失效的连接请求报文段。
client发送了第一个连接的请求报文,但是由于网络不好,这个请求没有立即到达服务端,而是在某个网络节点中滞留了,直到某个时间才到达server,本来这已经是一个失效
的报文,但是server端接收到这个请求报文后,还是会想client发出确认的报文,表示同意连接。假如不采用三次握手,那么只要server发出确认,新的建立就连接了,但其实这个
请求是失效的请求,client是不会理睬server的确认信息,也不会向服务端发送确认的请求,但是server认为新的连接已经建立起来了,并一直等待client发来数据,这样,server的
很多资源就没白白浪费掉了,采用三次握手就是为了防止这种情况的发生,server会因为收不到确认的报文,就知道client并没有建立连接。这就是三次握手的作用。
第一次握手
TCP发送一个FIN(结束),用来关闭客户到服务端的连接。
客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),
此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
第二次握手
服务端收到这个FIN,他发回一个ACK(确认),确认收到序号为收到序号+1,和SYN一样,一个FIN将占用一个序号。
服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器
通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个
状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
第三次握手
服务端发送一个FIN(结束)到客户端,服务端关闭客户端的连接。
服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,
此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
第四次握手
客户端发送ACK(确认)报文确认,并将确认的序号+1,这样关闭完成。
客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时
TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
3:UDP 协议
(用户数据报协议,User Data Protocol):(类似发短信)
面向非连接 、传输不可靠(可能丢包)、无序、传输少量数据(数据报模式)、速度快,对系统资源的要求少,程序结构较简单 ,
UDP支持一对一,一对多,多对一和多对多的交互通信,
UDP的首部开销小,只有8个字节。
Socket其实并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。
4:WebSocket与HTTP的关系
1. 都是一样基于TCP的,都是可靠性传输协议。
2. 都是应用层协议
3. WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息。HTTP是单向的。
4. WebSocket是需要浏览器和服务器握手进行建立连接的。而http是浏览器发起向服务器的连接,服务器预先并不知道这个
//创建连接
websocket = new WebSocket(webSocketMessageUrl);
//发送数据
建立连接后触发Socket.onopen 可以使用sent使用连接发送数据
//接收数据
并通过 onmessage 事件来接收服务器返回的数据
解决HTTP缺陷
非持久性 同步有延 消耗资源 无状态协议。 被动性
5: WebSocket 配置群聊的基本流程
第一步:导入websocket的依赖
第二步:websocket的配置
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
第三步:建立ServerEndpoint的java类,能够接受客户端发送过来的信息和发送给客户端信息
#webSocket群聊配置
webSocketMessage:
path: wss://guandao.gsfccs.com/webSocketMessage
@ServerEndpoint("/webSocketMessage")
@Component
public class WebSocketMessage {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketMessage> webSocketSet = new CopyOnWriteArraySet<WebSocketMessage>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private static final List<Map<String, Object>> users;
static {
users = new ArrayList<>();
}
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
Map<String, Object> maps = new HashMap<>();
maps.put("session", session);
maps.put("userId","");
maps.put("username","");
users.add(maps);//加入set中
addOnlineCount();//在线数加1
System.out.println("有新连接加入!当前在线人数为:" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
for (Map<String, Object> map : users){
Session session = (Session) map.get("session");
if(session == this.session){
ScoketMessage sm = new ScoketMessage();
sm.setType("close");
sm.setFromUser(map.get("username").toString());
sm.setMessage("用户"+map.get("username").toString()+"离开会议!");
sendMessageAll(JsonUtils.objectToJson(sm));
users.remove(map);
}
}
subOnlineCount();//在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
ScoketMessage sm = JSON.parseObject(message,ScoketMessage.class);
if(sm.getType().equals("ping")){
System.out.println("WebSocketMessage连接正常");
} else if(sm.getType().equals("join")){
for (Map<String, Object> map : users){
if(map.get("session") == session){
map.put("userId", sm.getUserId());
map.put("username", sm.getFromUser());
sendMessageAll(JsonUtils.objectToJson(sm));
System.out.println("用户:【" + sm.getFromUser() + "】连接成功");
}
}
} else {
sendMessageAll(JsonUtils.objectToJson(sm));
System.out.println("来自WebSocket的消息:" + JsonUtils.objectToJson(sm));
}
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
/**
* 群发自定义消息
*/
public void sendMessageAll(String message){
for (Map<String, Object> map : users){
Session session = (Session) map.get("session");
if(session.isOpen()){
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketMessage.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketMessage.onlineCount--;
}
}
第四部 js 获取WebSocket 请求的代码
var websocket = null;
var lockReconnect = false; //避免ws重复连接
createWebSocket();
function createWebSocket(){
try{
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
websocket = new WebSocket(webSocketMessageUrl);
} else if('MozWebSocket' in window){
websocket = new MozWebSocket(webSocketMessageUrl);
} else {
alert('您的浏览器不支持WebSocket');
}
initEventHandle();
}catch(e){
reconnect();
console.log("创建WebSocket异常:"+e);
}
}
function initEventHandle(){
//连接成功建立的回调方法
websocket.onopen = function(event){
var sendTime = getCurrentDate(4);
var msg = {
type: "join",
userId: userId,
fromUser: account,
sendTime: sendTime,
message:"欢迎"+account+"加入会议!"
};
websocket.send(JSON.stringify(msg));
console.log("【WebSocket消息】:"+JSON.stringify(msg));
};
//连接发生错误的回调方法
websocket.onerror = function(){
console.log("【WebSocket消息】:连接错误");
reconnect();
};
//接收到消息的回调方法
websocket.onmessage = function(event){
var data = JSON.parse(event.data);
if(data.type=='join'){
var html = "<div class='history-toolbar'>"+
"<button class='u-btn u-btn-none f-w-140'>"+data.message+"</button>"+
"</div>";
$("#webSocketMsg").append(html);
} else if(data.type=='message'){
if(data.fromUser == account){
var html = "<div class='msg msg-right'>"+
"<div class='avatar'></div>"+
"<div class='msgcontent'>"+
"<div class='nick'>"+data.sendTime+" "+data.fromUser+"</div>"+
"<div class='value'>"+data.message+"</div>"+
"</div>"+
"</div>";
$("#webSocketMsg").append(html);
} else {
var html = "<div class='msg msg-left'>"+
"<div class='avatar'></div>"+
"<div class='msgcontent'>"+
"<div class='nick'>"+data.fromUser+" "+data.sendTime+"</div>"+
"<div class='value'>"+data.message+"</div>"+
"</div>"+
"</div>";
$("#webSocketMsg").append(html);
}
} else if(data.type=='close'){
var html = "<div class='history-toolbar'>"+
"<button class='u-btn u-btn-none f-w-140'>"+data.message+"</button>"+
"</div>";
$("#webSocketMsg").append(html);
}
//收到消息后自动滚动到消息列表底部
var ele = document.getElementById('chat-msg-list');
ele.scrollTop = ele.scrollHeight;
};
//连接关闭的回调方法
websocket.onclose = function(){
console.log("【WebSocket消息】:连接断开");
reconnect();
};
//每隔30秒发送心跳包
setInterval(heartCheck,30000);
}
function reconnect() {
if(lockReconnect) return;
lockReconnect = true;
setTimeout(function () { //没连接上会一直重连,设置延迟避免请求过多
createWebSocket();
lockReconnect = false;
}, 30000);
}
//心跳
function heartCheck(){
var sendTime = getCurrentDate(2);
var msg = {
type: "ping",
sendTime: sendTime,
message:"ping"
};
websocket.send(JSON.stringify(msg));
}
//发送消息
function sendMsg(){
var message = document.getElementById('msgValue').value;
if(message == ''){
layer.msg("请输入需要发送的内容");
return false;
}
var sendTime = getCurrentDate(4);
var value = replace_em(message);
var msg = {
type: "message",
userId: userId,
fromUser:account,
sendTime: sendTime,
message:value
};
websocket.send(JSON.stringify(msg));
document.getElementById('msgValue').value='';
}
function replace_em(str){
str = str.replace(/\</g,'<');
str = str.replace(/\>/g,'>');
str = str.replace(/\n/g,'<br/>');
str = str.replace(/\[em_([0-9]*)\]/g,'<img src="/plugin/qqFace/arclist/$1.gif" border="0" />');
return str;
}
//连接关闭的回调方法
websocket.onclose = function(){
};
//获取时间
function getCurrentDate(format) {
var now = new Date();
var year = now.getFullYear(); //得到年份
var month = now.getMonth();//得到月份
var date = now.getDate();//得到日期
var day = now.getDay();//得到周几
var hour = now.getHours();//得到小时
var minu = now.getMinutes();//得到分钟
var sec = now.getSeconds();//得到秒
month = month + 1;
if (month < 10) month = "0" + month;
if (date < 10) date = "0" + date;
if (hour < 10) hour = "0" + hour;
if (minu < 10) minu = "0" + minu;
if (sec < 10) sec = "0" + sec;
var time = "";
//精确到天
if(format==1){
time = year + "-" + month + "-" + date;
}
//精确到分
else if(format==2){
time = year + "-" + month + "-" + date+ " " + hour + ":" + minu + ":" + sec;
}
//去掉年
else if(format==3){
time = month + "-" + date+ " " + hour + ":" + minu + ":" + sec;
}
//去掉年月日
else if(format==4){
time = hour + ":" + minu + ":" + sec;
}
return time;
}
webSocket连接过程
1: js中创建webSocket 对象获取我们配置文件中配置的wss 域名路径进行连接
websocket = new WebSocket(webSocketMessageUrl);
2:连接成功后通过 onopen()来检测加入房间的用户
3:通过socket.send方法来发送我们的数据信息
websocket.send(JSON.stringify(msg));
后台代码主要是配置 websocket
1: 在配置文件中配置webSocket wss访问路径
2:配置ServerEndpoint java类用来接收数据