0
点赞
收藏
分享

微信扫一扫

《Exploring Aligned Complementary Image Pair for Blind Motion Deblurring》

芷兮离离 2024-07-24 阅读 20

WebSocket介绍

什么是WebSocket

HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。在WebSocket中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,浏览器和服务器之间的数据交换变得更加简单。
在这里插入图片描述

WebSocket VS HTTP

它们的不同点:
1)HTTP的协议标识符是http,WebSocket的是ws;
2)HTTP请求只能由客户端发起,服务器无法主动向客户端推送消息,而WebSocket可以;
3)HTTP请求有同源限制,不同源之间通信需要跨域,而WebSocket没有同源限制。
它们的相同点:
1)都是应用层的通信协议;
2)默认端口一样,都是80或443;
3)都可以用于浏览器和服务器间的通信;
4)都基于TCP协议。

(都是基于TCP协议,http只能从客户端发送请求,websocket可以进行双向通信,任意一端(客户端,服务端)可以通过 建立的 连接将信息推到另一端)

WebSocket优点

  • 1)支持双向通信,实时性更强;
  • 2)更好的二进制支持;
  • 3)较少的控制开销:
    连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部;
  • 4)支持扩展:
    ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议(比如支持自定义压缩算法等)。

使用场景

业务场景场景概述
弹幕终端用户A在自己的手机端发送了一条弹幕信息,但是您也需要在客户A的手机端上将其他N个客户端发送的弹幕信息一并展示。需要通过WebSocket协议将其他客户端发送的弹幕信息从服务端全部推送至客户A的手机端,从而使客户A可以同时看到自己发送的弹幕和其他用户发送的弹幕。
在线教育老师进行一对多的在线授课,在客户端内编写的笔记、大纲等信息,需要实时推送至多个学生的客户端,需要通过WebSocket协议来完成。
股票等金融产品实时报价股票黄金等价格变化迅速,变化后,可以通过WebSocket协议将变化后的价格实时推送至世界各地的客户端,方便交易员迅速做出交易判断。
体育实况更新由于全世界体育爱好者数量众多,因此比赛实况成为其最为关心的热点。这类新闻中最好的体验就是利用WebSocket达到实时的更新。
视频会议和聊天尽管视频会议并不能代替和真人相见,但是应用场景众多。WebSocket可以帮助两端或多端接入会议的用户实时传递信息。
基于位置的应用越来越多的开发者借用移动设备的GPS功能来实现基于位置的网络应用。如果您一直记录终端用户的位置(例如:您的 App记录用户的运动轨迹),就可以收集到更加细致化的数据。

WebSocket快速开始

一个简单的WebSocket聊天Demo

模拟两个web客户端实现聊天功能

客户端代码

client.html:

<!DOCTYPE html>
<html>
<head lang="en">
	<meta charset="UTF-8">
	<title></title>
	<style>
		*{
			margin: 0;
			padding: 0;
		}
		.message{
			width: 60%;
			margin: 0 10px;
			display: inline-block;
			text-align: center;
			height: 40px;
			line-height: 40px;
			font-size: 20px;
			border-radius: 5px;
			border: 1px solid #B3D33F;
		}
		.form{
			width:100%;
			position: fixed;
			bottom: 300px;
			left: 0;
		}
		.connect{
			height: 40px;
			vertical-align: top;
			/* padding: 0; */
			width: 80px;
			font-size: 20px;
			border-radius: 5px;
			border: none;
			background: #B3D33F;
			color: #fff;
		}
	</style>
</head>
<body>
	<ul id="content"></ul>
	<form class="form">
		<input type="text" placeholder="请输入发送的消息" class="message" id="message"/>
		<input type="button" value="发送" id="send" class="connect"/>
		<input type="button" value="连接" id="connect" class="connect"/>
	</form>
	<script></script>
</body>
</html>

客户端js代码:

var oUl=document.getElementById('content');
	var oConnect=document.getElementById('connect');
	var oSend=document.getElementById('send');
	var oInput=document.getElementById('message');
	var ws=null;
	oConnect.onclick=function(){
	ws=new WebSocket('ws://localhost:5000');
	ws.onopen=function(){
		oUl.innerHTML+="<li>客户端已连接</li>";
	}
	ws.onmessage=function(evt){
		oUl.innerHTML+="<li>"+evt.data+"</li>";
	}
	ws.onclose=function(){
		oUl.innerHTML+="<li>客户端已断开连接</li>";
	};
	ws.onerror=function(evt){
		oUl.innerHTML+="<li>"+evt.data+"</li>";
	};
};
oSend.onclick=function(){
	if(ws){
		ws.send(oInput.value);
	}
}

这里使用的是w3c规范中关于HTML5 websocket API的原生API,这些api很简单,就是利用new WebSocket创建一个指定连接服务端地址的ws实例,然后为该实例注册onopen(连接服务端),onmessage(接受服务端数据),onclose(关闭连接)以及ws.send(建立连接后)发送请求。

WebSocket客户端的 API

WebSocket 构造函数

WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例:

var ws = new WebSocket('ws://localhost:8080');

执行上面语句之后,客户端就会与服务器进行连接。

webSocket.readyState

readyState属性返回实例对象的当前状态,共有四种:

switch (ws.readyState) {
	case WebSocket.CONNECTING:
		// do something
		break;
	case WebSocket.OPEN:
		// do something
		break;
	case WebSocket.CLOSING:
		// do something
		break;
	case WebSocket.CLOSED:
		// do something
		break;
	default:
		// this never happens
		break;
}

webSocket.onopen
实例对象的onopen属性,用于指定连接成功后的回调函数:

ws.onopen = function () {
	ws.send('Hello Server!');
}

如果要指定多个回调函数,可以使用addEventListener方法:

ws.addEventListener('open', function (event) {
	ws.send('Hello Server!');
});

webSocket.onclose
实例对象的onclose属性,用于指定连接关闭后的回调函数:

ws.onclose = function(event) {
	var code = event.code;
	var reason = event.reason;
	var wasClean = event.wasClean;
	// handle close event
};
ws.addEventListener("close", function(event) {
	var code = event.code;
	var reason = event.reason;
	var wasClean = event.wasClean;
	// handle close event
});

webSocket.onmessage
实例对象的onmessage属性,用于指定收到服务器数据后的回调函数:

ws.onmessage = function(event) {
	var data = event.data;
	// 处理数据
};

ws.addEventListener("message", function(event) {
	var data = event.data;
	// 处理数据
});

注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象):

ws.onmessage = function(event){
	if(typeof event.data === String){
		console.log("Received data string");
	}
	if(event.data instanceof ArrayBuffer){
		var buffer = event.data;
		console.log("Received arraybuffer");
	}
}

除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型:

// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
	console.log(e.data.size);
};

// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
	console.log(e.data.byteLength);
};

webSocket.send()
实例对象的send()方法用于向服务器发送数据。
发送文本的例子:

ws.send('your message');

发送 Blob 对象的例子:

var file = document
	.querySelector('input[type="file"]')
	.files[0];
ws.send(file);

发送 ArrayBuffer 对象的例子:

// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var ii = 0; ii < img.data.length; ii++) {
	binary[ii] = img.data[ii];
}
ws.send(binary.buffer);

webSocket.bufferedAmount
实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。

var data = new ArrayBuffer(10000000);
socket.send(data);

if (socket.bufferedAmount === 0) {
	// 发送完毕
} else {
	// 发送还没结束
}

webSocket.onerror
实例对象的onerror属性,用于指定报错时的回调函数:

socket.onerror = function(event) {
	// handle error event
};
socket.addEventListener("error", function(event) {
	// handle error event
});

服务端代码

服务端采用Node.js,这里需要基于一个nodejs-websocket的Node.js服务端的库,它是一个轻量级的Node.js websocket server端的实现,实际上也是使用Node.js提供的net模块写成的。

npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm i nodejs-websocket

server.js:

var app=require('http').createServer(handler);
var ws=require('nodejs-websocket');
var fs=require('fs');
app.listen(80);

function handler(req,res){
	fs.readFile(__dirname+'/client.html',function(err,data){
		if(err){
			res.writeHead(500);
			return res.end('error ');
		}
		res.writeHead(200);
		res.end(data);
	});
}

var server=ws.createServer(function(conn){
	console.log('new conneciton');
	conn.on("text",function(str){
		broadcast(server,str);
	});
	conn.on("close",function(code,reason){
		console.log('connection closed');
	})
}).listen(5000);

function broadcast(server, msg) {
	server.connections.forEach(function (conn) {
		conn.sendText(msg);
	})
}

首先利用http模块监听用户的http请求并显示client.html界面,然后创建一个websocket服务端等待用户连接,在接收到用户发送来的数据之后将它广播到所有连接到的客户端。

运行

node server.js

客户端发起连接

请求响应报文
在这里插入图片描述

结语
从上面的即时通讯聊天例子我们可以看到,要想做一个点对点的im应用,websocket采取的方式是让所有客户端连接服务端,服务器将不同客户端发送给自己的消息进行转发或者广播。
为了说明html5规范中的websocket在客户端采用了websocket原生的API,实际开发中,有比较著名的两个库socket.io和sockjs,它们都对原始的websocket API做了进一步封装,提供了更多功能,都分为客户端和服务端的实现,实际应用中,可以选择使用。

Socket.IO使用

Socket.IO是一个完全由JavaScript实现、基于Node.js、支持WebSocket的协议用于实时通信、跨平台的开源框架,它包括了客户端的JavaScript和服务器端的Node.js。

官方文档:https://socket.io/docs/v4/

环境准备
搭建Socket.IO环境需要先创建一个作为工作空间的目录,然后安装Node.js,并在工作空间下安装Socket.IO

cnpm install socket.io

服务端示例代码

var app = require('http').createServer(handler)
var io = require('socket.io')(app);
var fs = require('fs');
app.listen(80);
function handler (req, res) {
	fs.readFile(__dirname + '/index.html',
	function (err, data) {
		if (err) {
			res.writeHead(500);
			return res.end('Error loading index.html');
		}
		res.writeHead(200);
		res.end(data);
	});
}

io.on('connection', function (socket) {
	socket.emit('news', { hello: 'world' });
	socket.on('my other event', function (data) {
		console.log(data);
	});
// socket.io 使用 emit(eventname,data) 发送消息,使用on(eventname,callback)监听消息
	//监听客户端发送的 sendMsg 事件
	socket.on("sendMsg", function (data) {
		// data 为客户端发送的消息,可以是 字符串,json对象或buffer
		// 使用 emit 发送消息,broadcast 表示 除自己以外的所有已连接的socket客户端。
		socket.broadcast.emit("receiveMsg", data);
	})
});

客户端示例代码

<!DOCTYPE html>
<html lang="zh">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	<label>聊天内容:</label><br />
	<textarea id="content" style="height: 200px; width:300px;"></textarea>
	<br />
	<input id="sendMsg" type="text" />
	<button id="btn_send">发送</button>
<script src="/socket.io/socket.io.js"></script>

<script>
	var socket = io('http://localhost');
	socket.on('news', function (data) {
		console.log(data);
		socket.emit('my other event', { my: 'data' });
	});
	// 监听 receiveMsg 事件,用来接收其他客户端推送的消息

socket.on("receiveMsg", function (data) {
	content.value += data.client + ":" + data.msg + "\r\n";
});
var content = document.getElementById("content");
var sendMsg = document.getElementById("sendMsg");
var btn_send = document.getElementById("btn_send");
btn_send.addEventListener("click", function () {
	var data = {
		client: "客户端",
		msg: sendMsg.value
	};
	//给服务端发送 sendMsg事件名的消息
	socket.emit("sendMsg", data);
	content.value += data.client + ":" + data.msg + "\r\n";
	sendMsg.value = "";
});

</script>
</body>
</html>

springboot整合websocket

引入依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

WebSocket配置类

@Configuration
public class WebSocketConfig {
   /**
	* 注入ServerEndpointExporter,
	* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
	*/
	@Bean
	public ServerEndpointExporter serverEndpointExporter() {
		return new ServerEndpointExporter();
	}
}

WebSocket操作类
@ ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端

WebSocketServer

@Component
@ServerEndpoint(value = "/websocket/{userId}")
public class WebSocket {
	private final static Logger logger = LogManager.getLogger(WebSocket.class);
	/**
	 * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的
     */
     private static AtomicInteger onlineCount = new AtomicInteger();
     /**
	  * concurrent包的线程安全Map,用来存放每个客户端对应的MyWebSocket对象
	  */
	  private static ConcurrentHashMap<String, WebSocket> webSocketMap = new ConcurrentHashMap<>();
	  /**
	   * 与某个客户端的连接会话,需要通过它来给客户端发送数据
	   */
	   private Session session;
	   private String userId;

	  /**
	   * 连接建立成功调用的方法
	   */
	   @OnOpen
	   public void onOpen(Session session, @PathParam("userId") String userId) {
			this.session = session;
			this.userId = userId;
			//加入map
			webSocketMap.put(userId, this);
			addOnlineCount(); //在线数加1
			logger.info("用户{}连接成功,当前在线人数为{}", userId, getOnlineCount());
			try {
				sendMessage(String.valueOf(this.session.getQueryString()));
			} catch (IOException e) {
				logger.error("IO异常");
			}
		}
	/**
	 * 连接关闭调用的方法
	 */
	@OnClose
	public void onClose() {
		//从map中删除
		webSocketMap.remove(userId);
		subOnlineCount(); //在线数减1
		logger.info("用户{}关闭连接!当前在线人数为{}", userId, getOnlineCount());
	}
	/**
	 * 收到客户端消息后调用的方法
	 *
	 * @param message 客户端发送过来的消息
	 */
	@OnMessage
	public void onMessage(String message, Session session) {
		logger.info("来自客户端用户:{} 消息:{}",userId, message);
		//群发消息
		for (String item : webSocketMap.keySet()) {
			try {
				webSocketMap.get(item).sendMessage(message);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	/**
	 * 发生错误时调用
	 *
	 * @OnError
	 */
	@OnError
	public void onError(Session session, Throwable error) {
		logger.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
		error.printStackTrace();
	}
	/**
	 * 向客户端发送消息
	 */
	public void sendMessage(String message) throws IOException {
		this.session.getBasicRemote().sendText(message);
		//this.session.getAsyncRemote().sendText(message);
	}

	/**
	 * 通过userId向客户端发送消息
	 */
	public void sendMessageByUserId(String userId, String message) throws IOException {
		logger.info("服务端发送消息到{},消息:{}",userId,message);
		if(StringUtils.hasLength(userId) && webSocketMap.containsKey(userId)){
			webSocketMap.get(userId).sendMessage(message);
		}else{
			logger.error("用户{}不在线",userId);
		}
	}
	/**
	 * 群发自定义消息
	 */
	public static void sendInfo(String message) throws IOException {
		for (String item : webSocketMap.keySet()) {
			try {
				webSocketMap.get(item).sendMessage(message);
			} catch (IOException e) {
				continue;
			}
		}
	}
	public static int getOnlineCount() {
		return onlineCount.get();
	}
	public static void addOnlineCount() {
		onlineCount.incrementAndGet();
	}
	public static void subOnlineCount() {
		onlineCount.decrementAndGet();
	}
}
	@RestController
	@RequestMapping("/webSocket")
	public class WebSocketController {
		@Autowired
		private WebSocket webSocket;
		@RequestMapping("/sentMessage")
		public void sentMessage(String userId,String message){
			try {
				webSocket.sendMessageByUserId(userId,message);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
@ServerEndpoint("/webSocket/{username}")
@Component
public class WebSocketServer {
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static AtomicInteger onlineNum = new AtomicInteger();

    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
    private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();

    //发送消息
    public void sendMessage(Session session, String message) throws IOException {
        if(session != null){
            synchronized (session) {
                System.out.println("发送数据:" + message);
                session.getBasicRemote().sendText(message);
            }
        }
    }
    //给指定用户发送信息
    public void sendInfo(String userName, String message){
        Session session = sessionPools.get(userName);
        try {
            sendMessage(session, message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    // 群发消息
    public void broadcast(String message){
    	for (Session session: sessionPools.values()) {
            try {
                sendMessage(session, message);
            } catch(Exception e){
                e.printStackTrace();
                continue;
            }
        }
    }

    //建立连接成功调用
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "username") String userName){
        sessionPools.put(userName, session);
        addOnlineCount();
        System.out.println(userName + "加入webSocket!当前人数为" + onlineNum);
        // 广播上线消息
        Message msg = new Message();
        msg.setDate(new Date());
        msg.setTo("0");
        msg.setText(userName);
        broadcast(JSON.toJSONString(msg,true));
    }

    //关闭连接时调用
    @OnClose
    public void onClose(@PathParam(value = "username") String userName){
        sessionPools.remove(userName);
        subOnlineCount();
        System.out.println(userName + "断开webSocket连接!当前人数为" + onlineNum);
        // 广播下线消息
        Message msg = new Message();
        msg.setDate(new Date());
        msg.setTo("-2");
        msg.setText(userName);
        broadcast(JSON.toJSONString(msg,true));
    }

    //收到客户端信息后,根据接收人的username把消息推下去或者群发
    // to=-1群发消息
    @OnMessage
    public void onMessage(String message) throws IOException{
        System.out.println("server get" + message);
        Message msg=JSON.parseObject(message, Message.class);
		msg.setDate(new Date());
		if (msg.getTo().equals("-1")) {
			broadcast(JSON.toJSONString(msg,true));
		} else {
			sendInfo(msg.getTo(), JSON.toJSONString(msg,true));
		}
    }

    //错误时调用
    @OnError
    public void onError(Session session, Throwable throwable){
        System.out.println("发生错误");
        throwable.printStackTrace();
    }

    public static void addOnlineCount(){
        onlineNum.incrementAndGet();
    }

    public static void subOnlineCount() {
        onlineNum.decrementAndGet();
    }

    public static AtomicInteger getOnlineNumber() {
        return onlineNum;
    }

    public static ConcurrentHashMap<String, Session> getSessionPools() {
        return sessionPools;
    }
}


LoginController

@Controller
public class LoginController {
	@Autowired
	LoginService loginservice;

	@RequestMapping("/loginvalidate")
	public String loginvalidate(@RequestParam("username") String username,@RequestParam("password") String pwd,HttpSession httpSession){
		if(username==null) {
			return "login";
		}
		String realpwd=loginservice.getpwdbyname(username);
		if(realpwd != null&& pwd.equals(realpwd)) {
			long uid=loginservice.getUidbyname(username);
			httpSession.setAttribute("uid", uid);
			return "chatroom";
		}
		return "fail";
	}

	@RequestMapping("/login")
	public String login(){
		return "login";
	}

	@RequestMapping("/logout")
	public String logout(HttpSession httpSession){
		return "login";
	}

	@RequestMapping(value="/currentuser",method = RequestMethod.GET)
	@ResponseBody
	public User currentuser(HttpSession httpSession){
		Long uid = (Long)httpSession.getAttribute("uid");
		String name = loginservice.getnamebyid(uid);
		return new User(uid, name);
	}
}

ChatController

@Controller
public class ChatController {

	@Autowired
	LoginService loginservice;
	

	@RequestMapping("/onlineusers")
	@ResponseBody
	public Set<String> onlineusers(@RequestParam("currentuser") String currentuser) {
		ConcurrentHashMap<String, Session> map = WebSocketServer.getSessionPools();
		Set<String> set = map.keySet();
		Iterator<String> it = set.iterator();
		Set<String> nameset = new HashSet<String>();
		while (it.hasNext()) {
			String entry = it.next();
			if (!entry.equals(currentuser))
				nameset.add(entry);
		}
		return nameset;
	}


	@RequestMapping("getuid")
	@ResponseBody
	public User getuid(@RequestParam("username") String username) {
		Long a = loginservice.getUidbyname(username);
		User u = new User();
		u.setUid(a);
		return u;
	}
}

LoginServiceImpl

@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,timeout=5)
@Service("loginservice")
public class LoginServiceImpl implements LoginService{
	@Autowired
	private LoginMapper loginmapper;
	
	@Override
	public String getpwdbyname(String name) {
		Staff s=loginmapper.getpwdbyname(name);
		if(s!=null) {
			return s.getPassword();
		}
		return null;
	}

	@Override
	public Long getUidbyname(String name) {
		Staff s = loginmapper.getpwdbyname(name);
		if (s != null){
			return (long) s.getStaff_id();
		}
		return null;
	}

	@Override
	public String getnamebyid(long id) {
		Staff s=loginmapper.getnamebyid(id);
		if(s!=null) {
			return s.getUsername();
		}
		return null;
	}
}

实体类
Message

public class Message {
	//发送者name
	public String from;
	//接收者name
	public String to;
	//发送的文本
	public String text;
	//发送时间
	@JSONField(format="yyyy-MM-dd HH:mm:ss")
	public Date date;
}

MSG类

public class MSG {
	String msg;
	
	public MSG(String msg) {
		super();
		this.msg = msg;
	}

	public String getMsg() {
		return msg;
	}

	public void setMsg(String msg) {
		this.msg = msg;
	}
}

Staff类

public class Staff {
	private byte staff_id;
	private String first_name;
	private String last_name;
	private short address_id;
	private String email;
	private String username;
	private String password;
	private String last_update;
}

User类

public class User {
	Long uid;
	String name;
}

WebSocket在线测试工具
https://websocket.jsonin.com/

测试

WebSocket原理

如何建立连接

客户端:申请协议升级

首先,客户端发起协议升级请求。可以看到,采用的是标准的HTTP报文格式,且只支持GET方法:

GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000/url
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==

服务端:响应协议升级

服务端返回内容如下,状态代码101表示协议切换:

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=

到此完成协议升级,后续的数据交互都按照新的协议来。

Sec-WebSocket-Key/Accept的作用

前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基础的防护,减少恶意
连接、意外连接。

  • @
  • 强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端/服务端是否合法的 ws客户端、ws服务端,其实并没有实际性的保证

数据帧格式

客户端、服务端数据的交换,离不开数据帧格式的定义。WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。

  • 发送端:将消息切割成多个帧,并发送给服务端;
  • 接收端:接收消息帧,并将关联的帧重新组装成完整的消息。

具体的帧格式如下所示:
在这里插入图片描述

数据传递

一旦WebSocket客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。WebSocket根据opcode来区分操作的类型。比如0x8表示断开连接,0x0-0x2表示数据交互

数据分片

WebSocket的每条消息可能被切分成多个数据帧。当WebSocket的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧。
FIN=1表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。FIN=0,则接收方还需要继续监听接收其余的数据帧。
此外,opcode在数据交换的场景下,表示的是数据的类型。0x01表示文本,0x02表示二进制。而0x00比较特殊,表示延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。

数据分片例子

下面例子可以很好地演示数据的分片。客户端向服务端两次发送消息,服务端收到消息后回应客户端,这里主要看客户端往服务端发送的消息。

Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!

第一条消息:
FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。
第二条消息:

  • 1)FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧;
  • 2)FIN=0,opcode=0x0,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后;
  • 3)FIN=1,opcode=0x0,表示消息已经发送完成,没有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。服务端可以将关联的数据帧组装成完整的消息。

连接保持+心跳

WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。

但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。

这个时候,可以采用心跳来实现:

  • 发送方->接收方:ping
  • 接收方->发送方:pong

ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。

举例:WebSocket服务端向客户端发送ping,只需要如下代码(采用ws模块)

ws.ping('', false, true);
举报

相关推荐

0 条评论