首先,创建一个聊天室的数据库:
create database if not exists `chatroom_websocket` default character set `utf8`;
use `chatroom_websocket`;
create table if not exists `user`
(id int primary key auto_increment comment '用户id',
username varchar(20) unique not null comment '用户名',
password varchar(100) not null comment 'md5加密后的密码'
);
注意修改上一篇准备工作中写的配置文件中的数据库名:
业务开发分为3层:
- dao层:java操作数据库,把信息持久化到数据库中
- service层:中间的业务层,具体处理用户业务
- controller:调用service,获取数据,返回给客户端/从客户端获得数据,调用业务进行处理
先实现用户模块的注册与登录:
无论是用户模块,还是之后拓展的文件模块,操作数据库都是四步骤,不同的地方是执行的sql语句不同,所以其他三个步骤可以封装,以供继承。因此,在dao包中创建BaseDao类:
package com.bit.chatroom.dao;
//封装基础操作:数据源,获取连接,关闭资源
//所有业务都需要继承这个dao层
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.bit.chatroom.utils.CommUtil;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import java.util.Stack;
public class Basedao {
private static DataSource dataSource;
static
{Properties properties=CommUtil.loadProperties("datasource.properties");
try {
dataSource=DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
System.err.println("数据源加载失败");
}
}
//获取数据库连接,只让子类使用
protected Connection getConnection()
{
try {
return dataSource.getConnection();
} catch (SQLException e) {
System.err.println("获取连接失败");
}
return null;
}
//关闭资源 Connection,Statement,(ResultSet)
protected void closeResources(Connection connection, Statement statement)
{if(connection!=null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(statement!=null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
protected void closeResources(Connection connection, Statement statement, ResultSet resultSet) throws SQLException
{closeResources(connection,statement);
if (resultSet!=null)
resultSet.close();}}
接下来在dao包下新建AccountDao类:
package com.bit.chatroom.dao;
//用户模块的dao,要实现用户的注册和登录
import com.bit.chatroom.entity.user;
import org.apache.commons.codec.digest.DigestUtils;
import java.sql.*;
public class AccountDao extends Basedao {
//用户登录--查询query(select)
public user userLogin(String username,String passsword) throws SQLException
{Connection connection=null;
PreparedStatement statement=null;
ResultSet resultSet=null;
user user2=null;
try{
connection=getConnection();
String sql="select * from user where username=? and password=?";
statement=connection.prepareStatement(sql);
statement.setString(1,username);
statement.setString(2,DigestUtils.md5Hex(passsword));
resultSet=statement.executeQuery();
if(resultSet.next())
{user2=getUserInfo(resultSet);}
} catch (SQLException e) {
System.err.println("查询用户信息出错");
}
finally {
closeResources(connection,statement,resultSet);
}
return user2;
}
//需要把resultSet数据表信息转换成entity实体中的User类。
public user getUserInfo(ResultSet resultSet) throws SQLException
{user user1=new user();
user1.setId(resultSet.getInt("id"));
user1.setUsername(resultSet.getString("username"));
user1.setPassword(resultSet.getString("password"));
return user1;}
//用户注册--插入insert
public boolean userRegister(user user3)
{boolean issuccess=false;//不报错的话再改为true
String userName=user3.getUsername();
String password=user3.getPassword();
Connection connection=null;
PreparedStatement statement=null;
try {
connection = getConnection();
String sql = "insert into user(username,password) values (?,?)";
statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);//主键受影响行数
statement.setString(1, userName);
statement.setString(2, DigestUtils.md5Hex(password));
issuccess = (statement.executeUpdate() == 1);
}
catch (Exception ex)
{System.err.println("用户注册失败");ex.printStackTrace();}
finally
{closeResources(connection,statement);}
return issuccess;
}
}
每写完一层都需要进行单元测试,先对用户注册进行测试:
@Test
public void userRegister() {
user user4=new user();
user4.setUsername("test1");
user4.setPassword("123");
boolean isSuccess=accountDao.userRegister(user4);
Assert.assertEquals(true,isSuccess);
}
}
测试通过。
user表变成了:
接下来测试用户登录
@Test
public void userLogin() throws SQLException {
user user5=accountDao.userLogin("test1","123");
System.out.println(user5);
Assert.assertNotNull(user5);
}
测试结果为:
把前端页面放在webapp包下:
部署Tomcat。
当Application context是“/”时, 是放在根目录root下,方便访问。
运行Tomcat,来到前端页面:
实现注册功能,在service包下 新建AccountService类:
package com.bit.chatroom.service;
import com.bit.chatroom.dao.AccountDao;
import com.bit.chatroom.entity.user;
import java.sql.SQLException;
public class AccountService {
private AccountDao accountDao = new AccountDao();
//用户注册
public boolean userLogin(String username, String password) {
user user6 = null;
try {
user6 = accountDao.userLogin(username, password);
} catch (SQLException e) {
e.printStackTrace();
}
if (user6 == null) {
return false;
}
return true;
}
//用户登录
public boolean userRegister(String username,String password)
{user user7=new user();
user7.setUsername(username);
user7.setPassword(password);
return accountDao.userRegister(user7);
}
}
在controller包中新建AccountController类:
package com.bit.chatroom.controller;
import com.bit.chatroom.dao.AccountDao;
import com.bit.chatroom.service.AccountService;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(urlPatterns = "/doRegister")
public class AccountController extends HttpServlet {
private AccountService accountService=new AccountService();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
String username=request.getParameter("username");
String password=request.getParameter("password");
response.setContentType("text/html;charset=utf8");
PrintWriter writer=response.getWriter();
if(accountService.userRegister(username,password))
//用户注册成功,弹框提示,返回登录界面
{
writer.println("<script>alert(\"注册成功!\");\n" +
"window.location.href=\"/index.html\";\n" +
"\n" +
"</script>");
//参照popup.html
}
//用户登录失败,可能是用户名已存在,或是数据库存在问题,返回原页面
else{
writer.println("<script>alert(\"注册失败!\");\n" +
"window.location.href=\"/registration.html\";\n" +
"\n" +
"</script>");
}
}
@Override
protected void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException
{doGet(request,response);}
}
在webapp包中新建popup.html写弹框:
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>alert("注册成功!");
window.location.href="/index.html";
</script>
</body>
</html>
(上述可通过jsp实现,不易出错。)
可实现注册:
引入WebSocket的知识:
WebSocket
Http协议是如何工作的:
浏览器端和服务器端,一般都是浏览器发送请求——单边通信。
服务端是被动通信。
之前的多线程聊天室有服务端和客户端,服务端可以给客户端发送信息——全双工信息。
而WebSocket是全双工通信,服务端可以主动向浏览器发送信息。仍属于TCP-IP协议 是应用层协议。
接下来实现登录:
先在 webapp包中新建websocket.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<body>
请输入要发送的信息:
<input type="text" id="text">
<button onclick="">发送信息 </button>
<hr>
收到服务端信息为
<div id="read_from_server"></div>
<hr>
<button onclick="">关闭WebSocket</button>
<script>
var websocket=null;
if('WebSocket' in window){console.log("浏览器支持WebSocket!")
//传入后端地址
{websocket=new WebSocket("ws://localhost:8080/websocket");}
else {console.log("浏览器不支持WebSocket");}}
//浏览器与服务端建立连接后回调方法,打开一个连接时......
websocket.onopen=function () {
console.log("webSocket连接成功");
}
//浏览器收到服务器信息,收到一个服务器信息时......
websocket.onmessage=function (event) {
var msg=event.data;
flushDiv(msg);
}
//建立websocket失败
websocket.onerror=function () {
console.log("webSocket连接失败");
}
//客户端socket关闭
websocket.onclose=function () { websocket.close();}
//将浏览器信息发送到服务端
function senMsg2Servei() {
var msg=document.getElementById("text").value;
websocket.send(msg);
}
//刷新当前的div
function flushDiv() {
document.getElementById("read_from_server").innerText=msg;
}
function cloWebSocket() {
websocket.close();
}
//主动将当前窗口的webSocket关闭
window.onbeforeunload=function () { cloWebSocket() }
</script>
</body>
</head>
</html>
接下来,在service包中新建WebSocket类。
该类的注解需要与websocket.html中的一致:
package com.bit.chatroom.service;
import javax.servlet.annotation.WebServlet;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
//该注解可把当前类标记为WebSocket类
@ServerEndpoint("/websocket")
public class WebSocket {
//需要存储所有连接到后端的websocket
//静态:共享,不必每次打开便签就在Tomcat中新建一个实例。不必每个窗口都有ArraySet
private static CopyOnWriteArraySet<WebSocket> clients=new CopyOnWriteArraySet<>();
//绑定当前websocket会话
private Session session;
//建立连接时调用
@OnOpen
public void onOpen() {
this.session = session;
clients.add(this);
System.out.println("有新的连接,当前的Session ID 为:" + session.getId() + ",当前聊天室共有" + clients.size() + "人");
}}
运行Tomcat,浏览器显示上图效果。
在刷新一下界面,控制台输出:
继续写websocket.html,加上@OnError、@OnMessage:
package com.bit.chatroom.service;
import javax.servlet.annotation.WebServlet;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
//该注解可把当前类标记为WebSocket类
@ServerEndpoint("/websocket")
public class WebSocket {
//需要存储所有连接到后端的websocket
//静态:共享,不必每次打开便签就在Tomcat中新建一个实例。不必每个窗口都有ArraySet
private static CopyOnWriteArraySet<WebSocket> clients=new CopyOnWriteArraySet<>();
//绑定当前websocket会话
private Session session;
//建立连接时调用
@OnOpen
public void onOpen(Session session) {
this.session = session;
clients.add(this);
System.out.println("有新的连接,当前的Session ID 为:" + session.getId() + ",当前聊天室共有" + clients.size() + "人");
}
@OnError
public void onError(Throwable e)
{System.err.println("websocket连接失败!");
e.printStackTrace();}
@OnMessage
public void onMessage(String msg) throws IOException {//默认是群聊
System.out.println("浏览器发来的信息是:"+msg);
for(WebSocket webSocket:clients)
webSocket.sendMsg(msg);}
public void sendMsg(String msg) throws IOException
{this.session.getBasicRemote().sendText(msg);}
@OnClose
public void onClose()
{System.out.println("有用户退出聊天室");
clients.remove(this);
System.out.println("当前聊天室还剩下"+clients.size()+"人");}
}
在controller包中新建LoginController类:
package com.bit.chatroom.controller;
import com.bit.chatroom.service.AccountService;
import com.bit.chatroom.utils.CommUtil;
import com.sun.deploy.net.HttpResponse;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(urlPatterns = "/login")
public class LoginController extends HttpServlet {
AccountService accountService=new AccountService();
@Override
protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException
{String userName=request.getParameter("username");
String password=request.getParameter("password");
response.setContentType("text/html;charset=utf8");
PrintWriter out=response.getWriter();
if(CommUtil.strIsnull(userName)||CommUtil.strIsnull(password))
{//登录失败,返回登录页面
out.println("<script>\n" +
" alert(\"用户名或密码为空\")\n" +
" window.location.href=\"/index.html\";\n" +
"</script>\n" +
"\n");
}
//用户名和密码不为空
if(accountService.userLogin(userName,password))
{//登录成功,跳转到聊天页面
}
else {
//用户名或密码不对,登录失败,返回登录页面,再次登录
out.println("<script>\n" +
" alert(\"用户名或密码不正确\")\n" +
" window.location.href=\"/index.html\";\n" +
"</script>\n" +
"\n");
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{doGet(request,response);}
}
注意该类与Index.xml的一致。在index.xml中,登录表单提交的action是"/login",类的注释@WebServlet(urlPatterns)应为"/login"。
类与index.xml中input的属性名要对应:
String userName=request.getParameter("username");
在CommUtils中写方法判断输入的用户名和密码字符串是否为空:
public static boolean strIsnull(String str)
{return str==null||str.equals("");
//顺序不可颠倒,否则str变成空指针了。
}
在webapp中写入模板引擎FreeMarker(鼻祖是JSP):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<title>时话时说</title>
<link rel="stylesheet" href="assets/css/bootstrap.css"/>
<link rel="stylesheet" href="assets/css/font-awesome.min.css"/>
<link rel="stylesheet" href="assets/css/build.css"/>
<link rel="stylesheet" type="text/css" href="assets/css/qq.css"/>
</head>
<body>
<div class="qqBox">
<div class="context">
<div class="conLeft">
<ul id="userList">
</ul>
</div>
<div class="conRight">
<div class="Righthead">
<div class="headName">${username}</div>
</div>
<div class="RightCont">
<ul class="newsList" id="message">
</ul>
</div>
<div class="RightMiddle">
<div class="file">
<form id="form_photo" method="post" enctype="multipart/form-data"
style="width:auto;">
<input type="file" name="filename" id="filename" onchange="fileSelected();"
style="display:none;">
</form>
</div>
</div>
<div class="RightFoot">
<textarea id="dope"
style="width: 100%;height: 100%; border: none;outline: none;padding:20px 0 0 3%;" name=""
rows="" cols=""></textarea>
<button id="fasong" class="sendBtn" onclick="send()" style="border-radius: 5px">发送</button>
</div>
</div>
</div>
</div>
<script src="assets/js/jquery_min.js"></script>
<script src="https://cdn.bootcss.com/jquery.form/4.2.2/jquery.form.min.js"></script>
<script type="text/javascript">
var webSocket = null;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
webSocket = new WebSocket('ws://127.0.0.1:8080/websocket?username=' + '${username}');
} else {
alert("当前浏览器不支持WebSocket");
}
//连接发生错误的回调方法
webSocket.onerror = function () {
setMessageInnerHTML("WebSocket连接发生错误!");
};
webSocket.onopen = function () {
setMessageInnerHTML("WebSocket连接成功!")
};
webSocket.onmessage = function (event) {
$("#userList").html("");
eval("var msg=" + event.data + ";");
if (undefined != msg.content)
setMessageInnerHTML(msg.content);
if (undefined != msg.names) {
$.each(msg.names, function (key, value) {
var htmlstr = '<li>'
+ '<div class="checkbox checkbox-success checkbox-inline">'
+ '<input type="checkbox" class="styled" id="' + key + '" value="' + key + '" checked>'
+ '<label for="' + key + '"></label>'
+ '</div>'
+ '<div class="liLeft"><img src="assets/img/robot2.jpg"/></div>'
+ '<div class="liRight">'
+ '<span class="intername">' + value + '</span>'
+ '</div>'
+ '</li>'
$("#userList").append(htmlstr);
})
}
};
webSocket.onclose = function () {
setMessageInnerHTML("WebSocket连接关闭");
};
window.onbeforeunload = function () {
closeWebSocket();
};
function closeWebSocket() {
webSocket.close();
}
function send() {
var time = new Date().toLocaleString();
var message = $("#dope").val();
$("#dope").val("");
//发送消息
var htmlstr = '<li><div class="answerHead"><img src="assets/img/2.png"></div><div class="answers">'
+ '[本人]' + ' ' + time + '<br/>' + message + '</div></li>';
webSocketSend(htmlstr,message,"");
};
function webSocketSend(htmlstr,message,re){
$("#message").append(htmlstr);
var ss = $("#userList :checked");
var to = "";
$.each(ss, function (key, value) {
to += value.getAttribute("value") + "-";
});
console.info(to);
if (ss.size() == 0) {
var obj = {
msg: message,
type: 1
}
} else {
var obj = {
to: to,
msg: message,
type: 2
}
}
var msg = JSON.stringify(obj);
webSocket.send(msg);
if(re){
$("#jsonImg").attr("src", string.data);
// loadDiv(re);
}
}
//回车键发送消息
$(document).keypress(function (e) {
if ((e.keyCode || e.which) == 13) {
$("#fasong").click();
}
});
//局部刷新div
function loadDiv(sJ) {
$("#delayImgPer").html('<img src="'+sJ+'" class="delayImg" >');
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
var msg = '<li>'
+ '<div class="nesHead">'
+ '<img src="assets/img/robot.jpg">'
+ ' </div>'
+ '<div class="news">'
+ innerHTML
+ '</div>'
+ '</li>';
$("#message").append(msg);
}
</script>
</body>
</html>
后端必须加载模板引擎,需要在Tomcat中配置加载ftl页面的路径——用监听器WebListerner,类似于类中的静态代码块,项目启动时监听器也执行,可以在监听器中放全局配置,各个Servlet都可使用。例如:
在chatroom包下新建一个包config,其下新建一个类FreeMarker:
获取文件/文件夹绝对路径的方式:
在LoginController中写当用户名和密码正确时,登录操作:
//用户名和密码不为空
if(accountService.userLogin(userName,password))
{//登录成功,跳转到聊天页面
//需要把用户名传到前端
//加载chat.ftl,在本类中写方法getTemplate
Template template=getTemplate(request,"/chat,ftl");
Map<String,String> map=new HashMap<>();
map.put("username",userName);
try {
template.process(map,out);
} catch (TemplateException e) {
e.printStackTrace();
}
}
getTemplate方法:
private Template getTemplate(HttpServletRequest request,String fileName)
{Configuration cfg=(Configuration) request.getServletContext().getAttribute(FreeMarkerListener.TEMPLATE_KEY);
try {
return cfg.getTemplate(fileName);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
进行测试,用户名和密码正确后,在浏览器中显示:
开发一般是前后端分类;
[前端发给后端的API]
JSON字符串中有参数:
群聊{“msg”:“777”,“type”:1}
私聊{“to”:“0-”,“msg”:“3333”,“type”:2}
"msg"是消息内容,"type"是群聊/私聊类型,私聊中的"to"是发送对象,"0-"是SessionId。如果是"0-1-2-"是发给0、1、2对象。
[后端发给前端的API]
public class Message2Client
{private String content;
//用户列表,所有用户在服务端保存
private Map<String,String> names;
//<SessionID,用户名>
}
public class MessageFromClient
{private String msg;
private String type;
private String to;
}
接下来实现群聊功能,在WebSocket中写:
在entity包中新建类MessageFromClient和类Message2Client:
package com.bit.chatroom.entity;
//前端发来的
// 群聊{"msg":"777","type":1}
//私聊{"to":"0-","msg":"3333","type":2}
//需要把字符串还原成对象,再通过get、set方法操作
import lombok.Data;
@Data
public class MessageFromClient {
//聊天信息
private String msg;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
//聊天类别,1表示群聊,2表示私聊
private String type;
//私聊的对象SessionID
private String to;
}
package com.bit.chatroom.entity;
import java.util.Map;
public class Message2Client {
//聊天内容和用户列表
private String content;
//服务端登录的所有列表
private Map<String,String> names;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Map<String, String> getNames() {
return names;
}
public void setNames(Map<String, String> names) {
this.names = names;
}
}
再写用户退出的显示:
@OnClose
public void onClose() throws IOException {//将客户端移除
clients.remove(this);
names.remove(session.getId());
System.out.println("有用户下线了,用户名为" + userName);
//这时需要给所有在线用户一个下线通知
Message2Client message2Client = new Message2Client();
message2Client.setContent(userName + "下线啦");
message2Client.setName(names);
//发送信息
String jsonStr = CommUtil.objectToJson(message2Client);
for (WebSocket webSocket : clients) {
webSocket.sendMsg(jsonStr);
}
}
最后实现消息的发送:
在Message2Client类中写setContent的方法重载:
public void setContent(String userName,String msg)
{this.content=userName+"说:"+msg;}