WebSocket实现聊天室

需求

  • 实现用户登录功能
  • 展示用户好友列表功能
  • 实现用户历史消息展示
  • 实现单聊信息和群聊信息

效果展示

  • 用户登录
    在这里插入图片描述
  • 好友列表展示
    在这里插入图片描述
  • 历史消息展示
    在这里插入图片描述
  • 聊天

在这里插入图片描述

代码实现

说明:Springboot项目,页面是用 thymeleaf 整合的。

  • maven依赖
   <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
  • application.yml
spring:
  thymeleaf:
    cache: false
    suffix: .html
  • resource/templates目录下,创建页面
    1) login.html,点击登录调用了/user/login接口
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>

</head>
<body>

<div id="login" class="form-wrapper">
    <div class="header">
        登录
    </div>
</div>
<div>
    <span style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></span>
</div>
<form action="/user/login" method="post">

    <div >
        <div >
            <input th:type="text" th:name="username" placeholder="username">
        </div>
        <div >
            <input th:type="password" th:name="password" placeholder="password" >
        </div>
    </div>
    <div class="action" onclick="document.getElementById('lick1').click()">
        <div class="btn">
            确认
        </div>
    </div>
    <input th:type="submit" id="lick1">
</form>
</div>
</body>
</html>

2)chat.html,聊天主页面
[1]、退出登录按钮,调用了logout接口,把session中的token值清除了
[2]、好友列表,在跳转到chat页面的时候,调用了getUserList并且把用户列表数据注入到模型中,界面展示出来
[3]、连接websocket,调用了connectWebSocket()函数,调用了后端 websocket端点的onOpen方法
[4]、断开连接,调用了后台的onClose方法
[5]、发送消息,调用了后台的onMessage方法
[6]、查看历史消息,调用了后台的/history方法

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<html>
<head>
    <meta charset="UTF-8">
    <title>My WebSocket</title>
    <style>
        #message {
            margin-top: 40px;
            border: 1px solid gray;
            padding: 20px;
        }
    </style>
</head>
<body>

<div>
    用户token值:<span id="token" style="color: #ff0000" th:text="${session.token}"></span>        <div> <a href="/logout">退出登录</a></div>
</div>

<div>
    我的好友列表:<br/>
    <table border="1">
        <thead>

        </thead>
        <tbody>
        <tr th:each="user:${users}" style="color: blue">
           <a th:οnclick="aClick([[${user}]]);"  th:text="${user}" style="color: blue">  </a><br/>
        </tr>
        </tbody>
    </table>

</div>


昵称:<input type="text" id="nickname" th:value="${session.token}"/>
<button οnclick="conectWebSocket()">连接WebSocket</button>
<button οnclick="closeWebSocket()">断开连接</button>
<hr/>
<br/>
消息:<input id="text" type="text"/>
发送给谁: <input id="toUser" type="text">
<button οnclick="send()">发送消息</button>
<div id="message"></div>

历史消息:
<button οnclick="viewHistory()">查看历史消息</button>
<div id="history"></div>
</body>
<script type="text/javascript">
    var websocket = null;



    function conectWebSocket() {
        //判断当前浏览器是否支持WebSocket
        if ('WebSocket' in window) {

            let nickname = document.getElementById("nickname").value;
            if(nickname === ""){
                alert("请输入昵称");
                return;
            }

            websocket = new WebSocket("ws://localhost:8080/websocket/"+nickname);
        } else {
            alert('Not support websocket')
        }
        //连接发生错误的回调方法
        websocket.onerror = function () {
            setMessageInnerHTML("error");
        };
        //连接成功建立的回调方法
        websocket.onopen = function (event) {
            setMessageInnerHTML("Loc MSG: 成功建立连接");
        }
        //接收到消息的回调方法
        websocket.onmessage = function (event) {
            setMessageInnerHTML(event.data);
        }
        //连接关闭的回调方法
        websocket.onclose = function () {
            setMessageInnerHTML("Loc MSG:关闭连接");
        }
        //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
        window.onbeforeunload = function () {
            websocket.close();
        }
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //关闭连接
    function closeWebSocket() {
        websocket.close();
    }

    //发送消息
    function send() {
        var message = document.getElementById('text').value;
        var toUser = document.getElementById("toUser").value;

        var socketMsg = {msg:message,toUser:toUser};
        if(toUser == ''){
            //群聊
            socketMsg.type = 0;
        }else {
            //单聊
            socketMsg.type = 1;
        }

        websocket.send(JSON.stringify(socketMsg));
    }



    function aClick(e){
        console.log("aaa")
        console.log(e)
        document.getElementById("toUser").value = e;
    }


    function  viewHistory(){
        var toUser = document.getElementById("toUser").value;
        var httpRequest = new XMLHttpRequest(); //第一步:建立所需的对象
        httpRequest.open('GET', '/history?toUser='+toUser, true); //第二步:打开连接  将请求参数写在url中  ps:"./Ptest.php?name=test&nameone=testone"
        httpRequest.send(); //第三步:发送请求  将请求参数写在URL中
        httpRequest.onreadystatechange = function() {
            if (httpRequest.readyState == 4 && httpRequest.status == 200) {
                console.log(httpRequest.responseText);

                document.getElementById('history').innerHTML += httpRequest.responseText + '<br/>';
            }
        };
    }

</script>
</html>
  • 配置类,GetHttpConfiguration.java

作用:把Http请求中的参数,传递到WebSocket中

public class GetHttpConfiguration extends ServerEndpointConfig.Configurator {
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 通过getUserProperties()使得websocket连接类中可获取到配置类中得到的数据
        Map<String, Object> userProperties = sec.getUserProperties();
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        userProperties.put("token",httpSession.getAttribute("token").toString());
        super.modifyHandshake(sec, request, response);
    }
}

  • 拦截器,LoginInterceptor.java

作用:登录拦截器,必须要在session中有token的值,否则跳转去登录。

public class LoginInterceptor implements HandlerInterceptor {
    static final Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        HttpSession session = request.getSession();
        Object token = session.getAttribute("token");
        if (Objects.isNull(token)) {
            response.sendRedirect("/login");
            return false;
        }
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {
        logger.info("postHandle...");
    }
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        logger.info("afterCompletion...");
    }
}
  • 配置类,WebConfig.java

作用:配置拦截器,配置不登录的路径

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()) //可以把配置类加入bean 然后autowier得到 或者@Bean 返回值得到 JavaConfig 三种方法都可以
                .addPathPatterns("/**") //拦截的路径 **代表所有
                .excludePathPatterns("/login","/user/login"); //不拦截的路径
    }
}

  • 配置类,WebSocketConfig.java

作用:开启websocket支持

@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 扫描@ServerEndpoint,将@ServerEndpoint修饰的类注册为websocket
     * 如果使用外置tomcat,则不需要此配置
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
  • 模型类,History.java

作用:封装聊天消息历史类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class History {
   private String from;
   private String to;
   private String time;
   private String content;
}

  • 模型类,User.java

作用:封装用户登录

@Data
public class User {
    private String username;
    private String password;
}
  • 模型类,SocketMsg.java

作用:封装消息发送类

public class SocketMsg {
    private int type;   //聊天类型0:群聊,1:单聊.
    private String fromUser;//发送者.
    private String toUser;//接受者.
    private String msg;//消息
    public int getType() {
        return type;
    }
    public void setType(int type) {
        this.type = type;
    }
    public String getFromUser() {
        return fromUser;
    }
    public void setFromUser(String fromUser) {
        this.fromUser = fromUser;
    }
    public String getToUser() {
        return toUser;
    }
    public void setToUser(String toUser) {
        this.toUser = toUser;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}
  • html路由类,SsoController.java

作用:配置login登录页面视图,配置chat页面视图,配置logout页面视图,配置登录逻辑

@Controller
@CrossOrigin
public class SsoController {
    @Autowired
    private UserController userController;
    @RequestMapping(value = "login", method = {RequestMethod.POST, RequestMethod.GET})
    public String login() {
        return "login";
    }

    @RequestMapping(value = "chat", method = {RequestMethod.POST, RequestMethod.GET})
    public String chat(HttpSession session,Model model) {
        List<String> users = userController.getUserList(session);
        model.addAttribute("a","123");
        model.addAttribute("users",users);
        return "chat";
    }


    @RequestMapping("logout")
    public String logOut(HttpSession session){
        session.removeAttribute("token");
        return "login";
    }

    @PostMapping("/user/login")
    public String login(HttpServletRequest request, HttpSession session, Map<String, Object> map, Model model) {
        String name = request.getParameter("username");
        String password = request.getParameter("password");
        if (UserController.userMap.containsKey(name)) {
            if (UserController.userMap.get(name).equals(password)) {
                session.setAttribute("token", name);
                return "redirect:/chat";
            }
        }

        model.addAttribute("msg", "请输入正确的账号和密码");
        return "login";
    }
}
  • 我的好友类,UserController.java

作用:模拟数据库用户,模拟用户的好友数据

@CrossOrigin
@RestController
public class UserController {
    //所有的用户
    public static Map<String, String> userMap = new HashMap<>();
    //每个用户对应的好友
    public static Map<String, List<String>> friendsMap = new HashMap<>();

    static {
        userMap.put("zhangsan", "123456");
        userMap.put("lisi", "123456");
        userMap.put("wangwu", "123456");
        userMap.put("zhaoliu", "123456");
        userMap.put("yangsilu", "123456");
        userMap.put("ranqilin", "123456");
        userMap.put("xuqiaodi", "123456");
        userMap.put("luowengang", "123456");

        friendsMap.put("zhangsan", List.of("lisi", "wangwu", "luowengang", "zhaoliu"));
        friendsMap.put("lisi", List.of("zhangsan", "wangwu", "ranqilin"));
        friendsMap.put("wangwu", List.of("zhangsan", "lisi", "xuqiaodi", "yangsilu"));

    }

    //获取我的好友
    @GetMapping("user/list")
    public List<String> getUserList(HttpSession session) {
        Object token = session.getAttribute("token");
        List<String> users = friendsMap.get(token.toString());
        return users;
    }
}
  • 聊天历史控制器,HistoryController.java

作用:1)获取用户聊天历史数据
2)提供了checkSocket接口,用于判断当前用户是否已经连接了webSocket。需要在前端连接websocket的时候做一个限制,目前没有实现。

@CrossOrigin
@RestController
public class HistoryController {

    // 判断当前用户是否连接了webSocket
    @GetMapping("/checkSocket")
    public boolean checkSocket(HttpSession session) {
        Object token = session.getAttribute("token");
        String fromUser = token.toString();
        return MyWebSocket.sessionIdNameMap.values().contains(fromUser);
    }

    @GetMapping("/history")
    public List<History> getHistory(@RequestParam("toUser")String toUser, HttpSession session){
        Object token = session.getAttribute("token");
        String fromUser = token.toString();

        List<String> datas = FileUtil.readFileLine();
        List<History> historyList = new ArrayList<>();
        for (String data : datas) {
            historyList.add(JSON.parseObject(data,History.class));
        }
        //排序
        List<History> collect = historyList.stream().filter(item -> {
            String from = item.getFrom();
            String to = item.getTo();
            boolean flag = checkIn(from, to, fromUser, toUser);
            return flag;
        }).sorted((x1,x2)-> x2.getTime().compareTo(x1.getTime())).
                collect(Collectors.toList());

        return collect;
    }

    private boolean checkIn(String from, String to, String fromUser, String toUser) {
        if(from.equals(fromUser) && to.equals(toUser)){
            return true;
        }
        if(from.equals(toUser) && to.equals(fromUser)){
            return true;
        }
        return false;
    }
}
  • websocket核心类,MyWebSocket.java

作用:1)onOpen方法,某个客户端的会话session存储到map中。每个客户端的socket对象存储在webSocketSet中。
2)onClose方法,连接关闭时候,把webSocket对象移除。
3)onError方法,如果连接发送异常会调用
4)OnMessage方法,客户端发送消息的方法。根据当前session的id,获取到发起方的Session对象,根据传递参数的用户名,获取到接收方的Session对象。判断接收方对象是否为空,不为空就可以发送消息。

@ServerEndpoint(value = "/websocket/{nickname}", configurator = GetHttpConfiguration.class)
@Component
public class MyWebSocket {

    //用来记录sessionId和该session进行绑定
    private static Map<String, Session> map = new ConcurrentHashMap<>();
    // sessionId和username的映射
    private static Map<String, String> sessionIdNameMap = new ConcurrentHashMap<>();


    //用来存放每个客户端对应的MyWebSocket对象。
    private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    private String nickname;

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("nickname") String nickname, EndpointConfig config) {
        this.session = session;
        this.nickname = nickname;
        //在建立连接的时候,就保存频道号(这里使用的是session.getId()作为频道号)和session之间的对应关系
        map.put(session.getId(), session);
        Object token = config.getUserProperties().get("token");
        sessionIdNameMap.put(session.getId(), token.toString());

        webSocketSet.add(this);     //加入set中
        System.out.println("有新连接加入!当前在线人数为" + webSocketSet.size());
        this.session.getAsyncRemote().sendText("恭喜" + nickname + "成功连接上WebSocket-->频道号是:" + nickname + "当前在线人数为:" + webSocketSet.size());
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        String id = session.getId();
        webSocketSet.remove(this);  //从set中删除
        sessionIdNameMap.remove(id);
        map.remove(id);
        System.out.println("有一连接关闭!当前在线人数为" + webSocketSet.size());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session, @PathParam("nickname") String nickname) {
        System.out.println("来自客户端的消息-->" + nickname + ":" + message);
        //群发消息
        // broadcast(nickname + ":" +message);

        //从客户端穿过来是json数据,转成SocketMsg对象,根据type判断是单聊还是群聊
        ObjectMapper objectMapper = new ObjectMapper();
        SocketMsg socketMsg;
        try {
            socketMsg = objectMapper.readValue(message, SocketMsg.class);
            if (socketMsg.getType() == 1) {
                //单聊,需要找到发送者和接受者
                socketMsg.setFromUser(session.getId()); //发送者
                Session fromSession = map.get(socketMsg.getFromUser());
                Session toSession = map.get(getSessionId(sessionIdNameMap, socketMsg.getToUser()));

                //发送给接受者
                if (toSession != null) {
                    String from = nickname + ":" + socketMsg.getMsg();
                    String to = nickname + ":" + socketMsg.getMsg();
                    fromSession.getAsyncRemote().sendText(from);
                    toSession.getAsyncRemote().sendText(to);
                    //保存消息
                    String time = getTime();
                    History history = History.builder()
                            .from(sessionIdNameMap.get(session.getId()))
                            .to(socketMsg.getToUser())
                            .time(time)
                            .content(nickname + ":" + socketMsg.getMsg())
                            .build();
                    FileUtil.toFile(JSON.toJSONString(history));
                } else {
                    fromSession.getAsyncRemote().sendText("系统消息:对方不在线或者您输入的频道号不对");
                }

            } else {
                //群发消息
                broadcast(nickname + ": " + socketMsg.getMsg());
                // 保存消息
            }

        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }


    }

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

    /**
     * 群发自定义消息
     */
    public void broadcast(String message) {
        for (MyWebSocket item : webSocketSet) {
            //同步异步说明参考:http://blog.csdn.net/who_is_xiaoming/article/details/53287691
            //this.session.getBasicRemote().sendText(message);
            item.session.getAsyncRemote().sendText(message);//异步发送消息.
        }
    }

    public String getTime(){
        SimpleDateFormat format = new SimpleDateFormat("yyyy-dd-mm HH:mm:ss");
        return format.format(new Date());
    }

    public String getSessionId(Map<String, String> map, String name) {
        if (StringUtils.isBlank(name)) return null;
        for (Map.Entry<String, String> entry : map.entrySet()) {
            if (entry.getValue().equals(name)) {
                return entry.getKey();
            }
        }
        return null;
    }
}
  • 历史消息存储和读取工具类,FileUtil.java

作用:把历史消息存储到文件中。

public class FileUtil {

    private static final String file = "D:\\websocket-sse\\wechat\\history.txt";

    public static void main(String[] args) {
        readFileLine();
    }

    public static void toFile(String content) {
        BufferedWriter out = null;
        try {
            out = new BufferedWriter(new OutputStreamWriter(
                    new FileOutputStream(file, true)));
            out.write(content + "\n");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();

            }

        }

    }


    public static List<String> readFileLine() {
        List<String> list = new ArrayList<>();
        try (FileInputStream in = new FileInputStream(file)) {
            Scanner sc = new Scanner(in, "UTF-8");
            while (sc.hasNext()) {
                String content = sc.nextLine();
                if (StringUtils.isNotBlank(content)) {
                    list.add(content);
                }
            }

        } catch (IOException e) {
        }
        return list;
    }

}

测试

  • 启动项目
  • 浏览器访问 localhost:8080/login,进入登录页面 ,分别用两个不同的浏览器,登录zhangsan/123456和lisi/123456
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 分别都点击 连接WebSocket
    在这里插入图片描述
  • 在lisi的登录界面,点击好友列表中的zhangsan,代表和zhangsan聊天,自动在发送给谁中展示了zhangsan;在zhangsan的登录界面中,点击好友列表中的lisi,代表和lisi聊天,自动在发送给谁中展示了lisi。

在这里插入图片描述

  • 查看历史记录
    在这里插入图片描述
  • 发送消息,在消息文本框中输入要发送的消息,然后点击发送消息
    在这里插入图片描述
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值