WebSocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
WebSocket的优势
很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。(即在做实时推送或者聊天等业务场景,通常使用WebSocket)
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
客户端的 JavaScript
var url="ip:port/xf/chatroom"; //远程Websocket服务端站点,改为你自己的接口
var websocket = null;
if ('WebSocket' in window) {
websocket = new WebSocket("ws://"+url);
} else {
alert("Not support WebScoket!")
}
websocket.onopen = onOpen;
websocket.onmessage = onMessage;
websocket.onerror = onError;
websocket.onclose = onClose;
///
/**
* Websocket前端业务逻辑
*/
///
/**客户端发起连接
* @param {Object} openEvent
*/
function onOpen(openEvent) {
console.log("正在连接...")
}
/**
* @param {Object} 收到服务端的消息
*/
function onMessage(event) {
if(typeof event.data =='string'){
var element = document.createElement("p");
element.innerHTML=event.data;
document.getElementById("plane").appendChild(element);
}else{
var reader = new FileReader();
reader.onload=function(eve){
if(eve.target.readyState==FileReader.DONE)
{
var img = document.createElement("img");
img.src=this.result;
document.getElementById("plane").appendChild(img);
}
};
reader.readAsDataURL(event.data);
}
}
/**
* 连接出错
*/
function onError() {
console.log("连接失败,请检查服务端是否正常启动");
}
/**
* 客户端关闭连接
* @param {Object} event
*/
function onClose(event) {
console.log(event.reason)
}
/**
* 客户端发送消息
*/
function doSend() {
if (websocket.readyState == 1) { //0-CONNECTING;1-OPEN;2-CLOSING;3-CLOSED
var msg = document.getElementById("message").value;
if(msg){
websocket.send(msg);
}
sendFile(msg);
document.getElementById("message").value="";
} else {
alert("connect fail!");
}
}
/**
* 发送消息
* @param {Object} isWithText
*/
function sendFile(isWithText){
var inputElement = document.getElementById("file");
var fileList = inputElement.files;
var file=fileList[0];
if(!file) return;
websocket.send(file.name+":fileStart");
var reader = new FileReader();
//以二进制形式读取文件
reader.readAsArrayBuffer(file);
//文件读取完毕后该函数响应
reader.onload = function loaded(evt) {
var blob = evt.target.result;
//发送二进制表示的文件
websocket.send(blob);
if(isWithText){
websocket.send(file.name+":fileFinishWithText");
}else{
websocket.send(file.name+":fileFinishSingle");
}
console.log("finnish");
}
inputElement.outerHTML=inputElement.outerHTML; //清空<input type="file">的值
}
/**
* 客户端断开连接
*/
function disconnect(){
if (websocket != null) {
websocket.close();
websocket = null;
}
}
服务端Java实现
服务端主要通过Springboot编写,所以首页引入websocket starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 拦截器配置,客户端与服务端做握手动作前后的相关工作
package cn.xfnihao.chat.interceptor;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import java.util.Map;
import java.util.Random;
/**
* @Author Fang chenjiang
* @Date 2020/11/20
*/
public class HandlershakeInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> attributes) throws Exception {
//attributes是session里面的所有属性的map表示
attributes.put("user", getRandomNickName());
return super.beforeHandshake(request, response, handler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
super.afterHandshake(request, response, wsHandler, ex);
}
//给每个进来的人(session)随机分配个昵称,这里没做控制,所以聊天室内的昵称可能发生重复
public String getRandomNickName(){
String[] nickNameArray={"Captain America","Deadpool","Hawkeye","Hulk","Iron Man","Spider Man","Thor","Wolverine","Black Panther","Colossus"};
Random random=new Random();
return nickNameArray[random.nextInt(10)];
}
}
- WebSocket核心配置
/**
* @Author Fang chenjiang
* @Date 2020/11/20
*/
@Configuration //配置类
@EnableWebSocket //声明支持websocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//addHandler注册和路由的功能,当客户端发起websocket连接,把/path交给对应的handler处理,而不实现具体的业务逻辑,可以理解为收集和任务分发中心。
//setAllowedOrigins(String[] domains),允许指定的域名或IP(含端口号)建立长连接,默认只有本地。如果不限时使用"*"号,如果指定了域名,则必须要以http或https开头。
//addInterceptors,顾名思义就是为handler添加拦截器,可以在调用handler前后加入自定义的逻辑代码。
//Websocket:ws://ip:port/path
registry.addHandler(ChatRoom(), "xf/chatroom").setAllowedOrigins("*").addInterceptors(handshakeInterceptor());
}
@Bean
public HandshakeInterceptor handshakeInterceptor(){
return new HandlershakeInterceptor();
}
@Bean
public XfChatRoom ChatRoom(){
return new XfChatRoom();
}
}
- 服务端聊天室核心代码
/**
* 聊天室核心业务
* @Author Fang chenjiang
* @Date 2020/11/20
*/
@Slf4j
public class XfChatRoom extends AbstractWebSocketHandler {
@Autowired
QiniuUtils qiniuUtils;
public final static List<WebSocketSession> sessionList =Collections.synchronizedList(new ArrayList<>());
SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
FileOutputStream output;
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
System.out.println("Connection established..."+webSocketSession.getId());
System.out.println(webSocketSession.getAttributes().get("user")+" Login");
webSocketSession.sendMessage(new TextMessage("I'm "+(webSocketSession.getAttributes().get("user"))));
sessionList.add(webSocketSession);
}
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus status) throws Exception {
System.out.println("Connection closed..."+webSocketSession.getRemoteAddress()+" "+status);
System.out.println(webSocketSession.getAttributes().get("user")+" Logout");
sessionList.remove(webSocketSession);
}
@Override
public void handleTextMessage(WebSocketSession websocketsession, TextMessage message)
{
String payload=message.getPayload();
String textString;
try {
if(payload.endsWith(":fileStart")){
output=new FileOutputStream(new File("F:\\images\\"+payload.split(":")[0]));
}else if(payload.endsWith(":fileFinishSingle")){
output.close();
String fileName=payload.split(":")[0];
for(WebSocketSession session:sessionList){
if(session.getId().equals(websocketsession.getId())){
textString=" I ("+format.format(new Date())+")<br>";
}else{
textString=websocketsession.getAttributes().get("user")+" ("+format.format(new Date())+")<br>";
}
TextMessage textMessage = new TextMessage(textString);
session.sendMessage(textMessage);
sendPicture(session,fileName);
}
}else if(payload.endsWith(":fileFinishWithText")){
output.close();
String fileName=payload.split(":")[0];
for(WebSocketSession session:sessionList){
sendPicture(session,fileName);
}
}else{
for(WebSocketSession session: sessionList){
if(session.getId().equals(websocketsession.getId())){
textString=" I ("+format.format(new Date())+")<br>"+payload;
}else{
textString=websocketsession.getAttributes().get("user")+" ("+format.format(new Date())+")<br>"+payload;
}
TextMessage textMessage = new TextMessage(textString);
session.sendMessage(textMessage);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void handleBinaryMessage(WebSocketSession session, BinaryMessage message)
{
ByteBuffer buffer= message.getPayload();
try {
output.write(buffer.array());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
if(webSocketSession.isOpen()){
webSocketSession.close();
}
System.out.println(throwable.toString());
System.out.println("WS connection error,close..."+webSocketSession.getRemoteAddress());
}
@Override
public boolean supportsPartialMessages() {
return true;
}
public void sendPicture(WebSocketSession session,String fileName){
FileInputStream input;
try {
File file=new File("F:\\images\\"+fileName);
input = new FileInputStream(file);
byte bytes[] = new byte[(int) file.length()];
input.read(bytes);
BinaryMessage byteMessage=new BinaryMessage(bytes);
session.sendMessage(byteMessage);
input.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
当前的整个demo支持文字和文件传输,不过文件传输仅在本地实现,通常在实际开发中,我们会借助第三方文件服务器(例如七牛,阿里云等)对文件进行存储管理。
总结
WebSocket实现聊天室功能大致框架就如上所说,整个客户端(HTML+JavaScript)和服务端(Java)交互过程并不难,主要关注其中核心部分即可,另外,注意体会WebSocket与传统轮训方式的区别,以及TCP在整个全双工通信的实现。