初识WebSocket
初识WebSocket
用Java和JavaScript基于WebSocket完成聊天室Demo
什么是WebSocket,WebSocket是一种基于TCP的网络协议,就像HTTP一样,它与HTTP最大的不同就是它是全双工的,也就是服务器可以主动发送数据给浏览器(是不是像Java中的Socket)。在HTTP中,浏览器发起请求之后服务器才能响应,给浏览器发送数据,服务器不能主动给浏览器发送数据。
但是在很多时候,最简单的就比如聊天室,在Http中只能采用轮训的方式,也就是浏览器不停地访问服务器查询有没有消息,这样做效率很低,而且非常浪费流量,WebSocket就是解决“服务器无法主动推送数据”这一难点而发明的。
目前浏览器基本都支持WebSocket,这种协议有着如下特点:
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
websocket以ws
开头,一个标准的ws网址像这样:
ws://ip:port/path
其中IP可以被域名代替。下面我会给出一个websocket下的聊天室Demo
基于WebSocket的聊天室Demo
环境:JDK 1.8.0_211
开发工具:IDEA
项目管理工具:maven
前端页面:bootstrap
代码有详细的注释,应该比较好懂的。
前端页面:chat.html
<!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">
<title>基于WebSocket的在线聊天室</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
<style>
.chatFont {
/* 消息字体大小 */
font-size: 16px;
}
.input {
/* 输入框绝对定位 */
position: fixed;
top: 75%;
right: 3%;
width: 70%;
font-size: 16px;
}
</style>
</head>
<body>
<!--侧边栏-->
<div class="column col-xs-3" id="sidebar">
<h3 class="text-center">在线人员</h3>
<ul class="nav" id="onlineUser">
<!-- 在线用户显示在这里 -->
</ul>
</div>
<div class="col-xs-9">
<div class="panel panel-info">
<div class="panel-heading"><h3>聊天室</h3></div>
<div class="panel-body" id="show" style="height: 380px;">
<!-- 消息显示在这里 -->
<div class="chatFont">聊天记录<br></div>
</div>
</div>
<div class="input">
<label for="msg">请输入</label>
<textarea class="form-control" rows="5" id="msg"></textarea>
</div>
</div>
</body>
<script src="js/jquery-3.4.1.js"></script>
<script src="js/bootstrap.min.js"></script>
<script>
//这个num是用来限制消息条数,不然消息会撑破面板(前端技术太渣只能用这种笨办法)
var num = 1;
// 创建WebSocket对象
var socket = new WebSocket("ws://localhost:8080/chat");
//发送消息
var sendMsg = function () {
var input = $('#msg');
if (input.val()) {
socket.send(input.val());
} else {
alert('消息不能为空')
}
//清空输入框
input.val("");
};
//输入框键盘事件检测
var keyDown = function (e) {
if (!e.ctrlKey && e.keyCode == 13) { // enter 键
sendMsg();
} else if (e.keyCode == 13 && e.ctrlKey) {
//实现换行,这个没写
alert('ctrl enter');
}
};
//绑定输入框键盘事件
$('#msg').keydown(keyDown);
//websocket监听事件,收到消息时触发
socket.onmessage = function (ev) {
//console.log(ev);
showMsg(ev.data);
};
//显示消息
var showMsg = function (data) {
//后端使用Json传输
data = JSON.parse(data);
//如果有消息则显示在消息框
if (data.msg) {
var text = '<div class="chatFont">' + '[' + data.userName + ']:' + data.msg + '<br></div>';
$('#show').append(text);
}
//如果是新用户则添加在线成员,针对新用户
if (data.onlineUser) {
console.log(data.onlineUser);
var online = data.onlineUser;
$.each(online, function (index, element) {
$('#onlineUser').append('<li class="active" id="' + element + '"><a href="#">' + element + '</a></li>');
})
}
//如果有新用户进来则添加在线成员,针对已在线成员
if (data.addUser) {
$('#onlineUser').append('<li class="active" id="' + data.userName + '"><a href="#">' + data.userName + '</a></li>');
}
//如果有用户离开则移除在线成员
if (data.removeUser) {
$('#' + data.userName + '').remove();
}
//消息大于15条时删除最上边的,防止撑破消息栏
if ($('#show').children('div').length>15){
$('#show').children('div:first').remove();
}
};
//连接断开时触发,清空绑定
socket.onclose = function (ev) {
$('#msg').unbind();
$('#show').text('连接已断开');
};
//连接时触发,创建用户名
socket.onopen = function (ev) {
var name = prompt('请输入昵称:');
if (name) {
socket.send(name);
} else {
socket.send('游客:' + Math.random() * 100000000000000000);
}
}
</script>
</html>
前端技术有点水,只能高出这么简陋的页面了。
即使这样也要搞个漂亮的首页😆: index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>首页</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="jumbotron">
<div class="container text-center" >
<h2 class="text-info" style="font-family:宋体;font-weight:bold;font-size:49px">基于WebSocket的在线聊天室</h2>
<br>
<div class="text-muted">与世界分享你的逼格</div>
<br>
<br>
</div>
<div class="container text-center">
<button class="btn btn-primary" id="enter">进入</button>
</div>
</div>
</body>
<script src="js/jquery-3.4.1.js"></script>
<script src="js/bootstrap.min.js"></script>
<script>
$('#enter').click(function () {
location.href = 'chat.html';
});
</script>
</html>
接下来是后端,首先添加依赖: pom.xml
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>
然后是服务端主体: 后端实现
面向对象,对消息进行封装:
public class Message {
private String userName;//用户
private String msg;//消息主体
private boolean addUser;//用户加入
private boolean removeUser;//用户离开
//清空状态
public void clearMsg(){
msg = null;
addUser = false;
removeUser = false;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public boolean isAddUser() {
return addUser;
}
public void setAddUser(boolean addUser) {
this.addUser = addUser;
}
public boolean isRemoveUser() {
return removeUser;
}
public void setRemoveUser(boolean removeUser) {
this.removeUser = removeUser;
}
@Override
public String toString() {
return "Message{" +
"userName='" + userName + '\'' +
", msg='" + msg + '\'' +
", addUser=" + addUser +
", removeUser=" + removeUser +
'}';
}
}
服务主体:
import com.alibaba.fastjson.JSON;
import com.bilibili.pojo.Message;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
//访问路径,类似http中的@WebServlet()注解
@ServerEndpoint("/chat")
public class Chat {
//每一个连接都会创建一个Chat对象,所以创建一个静态Map来保存已连接用户
private static final Map<String, Chat> clientMap = new HashMap<>();
private boolean firstFlag = true;//是否第一次访问
private String name;
private Session session;//这里的Session和servlet中的Session不是同一个种
private Message message = new Message();
/**
* 客户端连接时执行的方法
* @param session 客户端session
* @throws IOException
*/
@OnOpen
public void start(Session session) throws IOException {
this.session = session;
System.out.println("连接");
}
/**
* 客户端断开
*/
@OnClose
public void end() {
//从连接对象中移除
clientMap.remove(name, this);
//向所有人发送一个有人离开的消息
message.clearMsg();
message.setUserName(name);
message.setMsg("离开了聊天室!");
message.setRemoveUser(true);
// 发送消息
sendMsg(JSON.toJSONString(message));
System.out.println("断开");
}
/**
* 服务端收到消息
* @param msg
*/
@OnMessage
public void receive(String msg) {
if (firstFlag) {
//把第一次的消息作为用户名
name = msg;
//构造发送给所有人的消息
message.setMsg("加入了聊天室!");
message.setUserName(name);
message.setAddUser(true);
//获取当前在线用户
List<String> onlineUser = new ArrayList<>(clientMap.keySet());
clientMap.put(name, this);
try {
//直接构造Json,给新连接的用户发送刷新在线用户的消息
session.getBasicRemote().sendText("{\"onlineUser\":"+JSON.toJSONString(onlineUser)+"}");
} catch (IOException e) {
e.printStackTrace();
}
// 给所有用户发送有人进入的消息
sendMsg(JSON.toJSONString(message));
firstFlag = false;
} else {
//不是第一次则直接发送消息
message.clearMsg();
message.setMsg(msg);
sendMsg(JSON.toJSONString(message));
}
}
// 当客户端通信出现错误时,激发该方法
@OnError
public void onError(Throwable t) throws Throwable {
System.out.println("WebSocket服务端错误 " + t);
}
//发送消息的方法
public void sendMsg(String msg) {
// 遍历服务器关联的所有客户端
Chat client = null;
for (String nickname : clientMap.keySet()) {
try {
client = clientMap.get(nickname);
synchronized (client) {
// 发送消息
client.session.getBasicRemote().sendText(msg);
}
} catch (IOException e) {
System.out.println("聊天错误,向客户端 " + client + " 发送消息出现错误。");
clientMap.remove(name, client);
try {
client.session.close();
} catch (IOException e1) {
}
Message newMessage = new Message();
newMessage.setMsg("["+client.name+"]已经被断开了连接。");
sendMsg(JSON.toJSONString(newMessage));
}
}
}
}
这样就简单实现了一个聊天室。
效果图如下:
这么写可以发送html标签来达到发送图片的目的🤣