WebSocket(二) -- 使用原生webSocket实现一个简单的聊天

上文中,我们已经基本了解了webscoket的原理以及部分api的实现,接下来我们就使用websocket来实现一个简单的聊天室功能。

1. 需求分析

1.1 登陆聊天室:

在这里插入图片描述

1.2 登陆成功后与别人进行聊天

  1. 李四界面:
    在这里插入图片描述

  2. 张三界面:
    在这里插入图片描述

  3. 在李四页面的好友列表中,点击张三,与之聊天:
    在这里插入图片描述
    在这里插入图片描述

  4. 在张三好友列表页,点击李四,打开对话框,可以接收到李四的消息:
    在这里插入图片描述
    在这里插入图片描述

  5. 互相发送:
    在这里插入图片描述
    在这里插入图片描述

2. 实现流程:

(下图中蓝色的@onError应该为@onClose)
在这里插入图片描述

3. 消息格式:

在这里插入图片描述

4. 导入相关jar包

<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<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>

5. 代码实现:

5.1 POJO类:

5.1.1 浏览器发送给服务器的webSocket数据:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Message {
    private String toName;
    private String message;
    private String fromName;
}
5.1.2 服务端给客户端发送的websocket数据:
@Data
@NoArgsConstructor
@AllArgsConstructor
//用户间传送的消息
public class ResultMessage {
    private boolean isSystem;
    private String fromName;
    //private String toName;
    private Object message;
}
5.1.3 用于登陆返回给浏览器的数据
@Data
@AllArgsConstructor
@NoArgsConstructor
//登录时用到的信息
public class Result {
    private boolean flag;
    private String message;
}

5.2 消息工具类:

//封装发送的消息内容
public class MessageUtils {
    public static String getMessage(boolean isSystemMessage,String fromName,Object message){
        try{
            ResultMessage resultMessage = new ResultMessage();
            resultMessage.setSystem(isSystemMessage);
            resultMessage.setMessage(message);
            if(fromName != null){
                resultMessage.setFromName(fromName);
            }
//            if(toName !=null ){
//                resultMessage.setToName(toName);
//            }
            ObjectMapper mapper = new ObjectMapper();
            return mapper.writeValueAsString(resultMessage);
        }catch (JsonProcessingException e){
            e.printStackTrace();
        }
        return null;
    }
}

5.3 主要前端代码:

var toName;
var username;
//点击好友名称展示相关消息
function showChat(name){
    toName = name;
    //现在聊天框
    $("#content").html("");
    $("#content").css("visibility","visible");
    // 显示正在和谁聊天
    $("#Inchat").html("当前正与"+toName+"聊天");
    // 从sessionStorage中获取历史信息
    var chatData = sessionStorage.getItem(toName);
    if (chatData != null){
        // 将聊天记录渲染到聊天区
        $("#content").html(chatData);
    }
}
$(function () {
    $.ajax({
        url:"getUsername",
        success:function (res) {
            username = res;
        },
        async:false //同步请求,只有上面好了才会接着下面
    });
    //建立websocket连接
    //获取host解决后端获取httpsession的空指针异常
    var host = window.location.host;
    var ws = new WebSocket("ws://"+host+"/chat");
    ws.onopen = function (evt) {
        $("#username").html("<h3 style=\"text-align: center;\">用户:"+ username +"<span>-在线</span></h3>");
    }
    //接受消息
    ws.onmessage = function (evt) {
        //获取服务端推送的消息
        var dataStr = evt.data;
        //将dataStr转换为json对象
        var res = JSON.parse(dataStr);

        //判断是否是系统消息
        if(res.system){
            //系统消息
            //1.好友列表展示
            //2.系统广播的展示
            //此处声明的变量是调试时命名的,可以直接合并
            var names = res.message;
            var userlistStr = "";
            var broadcastListStr = "";
            var temp01 = "<a style=\"text-align: center; display: block;\" οnclick='showChat(\"";
            var temp03 = "\")'>";
            var temp04 = "</a></br>";
            var temp = "";
            for (var name of names){
                if (name != username){
                    temp = temp01 + name + temp03 + name + temp04;
                    userlistStr = userlistStr + temp;
                    broadcastListStr += "<p style='text-align: center'>"+ name +"上线了</p>";
                }
            }
            //渲染好友列表和系统广播
            $("#hyList").html(userlistStr);
            $("#xtList").html(broadcastListStr);

        } else {
            //不是系统消息
            // 将服务器推送的消息进行展示
            var str = "<span id='mes_left'>"+ res.message +"</span></br>";
            if (toName === res.fromName) {
                $("#content").append(str);
            }
            var chatData = sessionStorage.getItem(res.fromName);
            if (chatData != null){
                str = chatData + str;
            }
            //保存聊天消息
            sessionStorage.setItem(res.fromName,str);
        };
    }
    ws.onclose = function () {
        $("#username").html("<h3 style=\"text-align: center;\">用户:"+ username +"<span>-离线</span></h3>");
    }

    // 给发送按钮绑定单机事件
    //发送消息
    $("#submit").click(function () {
        //1.获取输入的内容
        var data = $("#input_text").val();
        //2.清空发送框
        $("#input_text").val("");
        var json = {"toName": toName ,"message": data};
        //将自己发送的数据也展示在聊天区
        var str = "<span id='mes_right'>"+ data +"</span></br>";
        $("#content").append(str);

        // 将聊天记录存放到sessionStorage中
        var chatData = sessionStorage.getItem(toName);
        if (chatData != null){
            str = chatData + str;
        }
        // 存放键和值,和谁的聊天,聊天的内容是什么
        sessionStorage.setItem(toName,str);
        //3.发送数据
        ws.send(JSON.stringify(json));
    })
})

5.3 登陆及拦截器实现:

5.3.1 登陆接口:
@RestController
//模拟登录操作
@Slf4j
public class CertificationController {
    @RequestMapping("/toLogin")
    public Result toLogin(String user, String pwd, HttpSession httpSession){
        Result result = new Result();
        httpSession.setMaxInactiveInterval(30*60);
        log.info(user+"登录验证中..");
        if(httpSession.getAttribute("user") != null){
            result.setFlag(false);
            result.setMessage("不支持一个浏览器登录多个用户!");
            return result;
        }
        if ("张三".equals(user)&&"123".equals(pwd)){
            result.setFlag(true);
            log.info(user+"登录验证成功");
            httpSession.setAttribute("user",user);
        }else if ("李四".equals(user)&&"123".equals(pwd)){
            result.setFlag(true);
            log.info(user+"登录验证成功");
            httpSession.setAttribute("user",user);
        }else if ("田七".equals(user)&&"123".equals(pwd)){
            result.setFlag(true);
            log.info(user+"登录验证成功");
            httpSession.setAttribute("user",user);
        }
        else if ("王五".equals(user)&&"123".equals(pwd)){
            result.setFlag(true);
            log.info(user+"登录验证成功");
            httpSession.setAttribute("user",user);
        }else {
            result.setFlag(false);
            log.info(user+"验证失败");
            result.setMessage("登录失败");
        }
        return result;
    }
    @RequestMapping("/getUsername")
    public String getUsername(HttpSession httpSession){
        String username = (String) httpSession.getAttribute("user");
        return username;
    }
}
5.3.2 拦截器:
//配置拦截路径
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {

    @Autowired
    private UserInterceptor userInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userInterceptor)
                .addPathPatterns("/main");
    }
}

@Component
@Slf4j
public class UserInterceptor implements HandlerInterceptor {
    //没用数据库,暂且用集合来存储已登录等用户,进行拦截。
    public static Map<String, String> onLineUsers = new ConcurrentHashMap<>();;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        HttpSession httpSession = request.getSession();
        String username = (String) httpSession.getAttribute("user");
        log.info("进入拦截器"+"==="+"进入拦截器的用户是:"+username);
        if(username != null && !onLineUsers.containsKey(username)){
            onLineUsers.put(username,username);
            log.info("已进入拦截器判断");
            log.info("已存储的用户01"+onLineUsers);
            return true;
        }else {
            log.info("已存储的用户02" + onLineUsers);
            log.info("未进入判断,进行重定向");
            httpSession.removeAttribute("user");
            response.sendRedirect("/loginerror");
            return false;
        }
    }
}

5.4 WebSocketConfig配置类:

作用:使使用了@ServerEndpoint注解的bean注册到spring中

@Configuration
//websocket要做的配置类
public class WebSocketConfig {
    // 注入ServerEndpointExporter bean对象,可以自动注册使用了@ServerEndpoint注解的bean
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

5.5 从request中获取httpsession对象

//@Configuration
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
    //此方法用来获取httpssion对象
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        if(httpSession != null) {
            sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
        }
    }
}

5.6 聊天室方法:

@Slf4j
// 声明资源路径chat, 让之前编写的插入HttpSession到config的配置文件生效
@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfigurator.class)
// 交给spring处理,但是还没完,spring并不会创建这个类的对象,需要加上spring对endpoint支持,
// 我们可以添加一个configuration的配置类,注入一个ServerEndpointExporter的bean对象,可以自动注册使用了@ServerEndpoint注解的bean
@Component
public class ChatEndPoint {
    //用线程安全的map来保存每个对象对应的EndPoint对象
    private static Map<String, ChatEndPoint> onLineUsers = new ConcurrentHashMap<>();
    //声明一个session对象,通过该对象可以发送消息给指定用户,不能设置为静态,每个ChatEndPoint有一个session才能区分.(websocket的session)
    private Session session;
    //保存当前登录浏览器的用户
    private HttpSession httpSession;

    //建立连接时发送系统广播
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        // 将局部的session对象赋值给成员session
        // 因为websocket涉及到多例,也就是多个线程调用此方法,所以使用this关键字来确保是同一个客户端在操作
        this.session = session;

        HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        this.httpSession = httpSession;
        String username = (String) httpSession.getAttribute("user");
        log.info("上线用户名称:" + username);

        // 将当前成功登陆的用户存储到容器中
        onLineUsers.put(username, this);
        // 将当前在线用户的用户名推送给所有的客户端
        // 1. 获取消息
        String message = MessageUtils.getMessage(true, null, getNames());
        // 2. 调用方法进行消息的推送
        broadcastAllUsers(message);
    }

    //获取当前登录的用户
    private Set<String> getNames() {
        return onLineUsers.keySet();
    }

    //发送系统消息
    private void broadcastAllUsers(String message) {
        try {
            // 需要将消息推送给所有在线用户
            Set<String> names = onLineUsers.keySet();
            for (String name : names) {
                ChatEndPoint chatEndPoint = onLineUsers.get(name);
                chatEndPoint.session.getBasicRemote().sendText(message);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //用户之间的信息发送
    @OnMessage
    // 接收到客户端发送的数据时调用
    public void onMessage(String message, Session session) {
        try {
            // 将string类型的message转换为message对象
            ObjectMapper mapper = new ObjectMapper();
            Message mess = mapper.readValue(message, Message.class);
            // 找到服务器接收到的消息的客户端接收方(也就是找出消息是发送给谁的,消息接收方)
            String toName = mess.getToName();
            // 获取消息数据
            String data = mess.getMessage();
            // 获取当前当前登陆用户(消息发送方)
            String username = (String) httpSession.getAttribute("user");
            log.info(username + "向" + toName + "发送的消息:" + data);
            // 格式化需要发送的消息和接收人
            String resultMessage = MessageUtils.getMessage(false, username, data);
            if (StringUtils.hasLength(toName)) {
                // 发送数据
                onLineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //用户断开连接的断后操作
    @OnClose
    public void onClose(Session session) {
        String username = (String) httpSession.getAttribute("user");
        log.info("离线用户:" + username);
        if (username != null) {
            onLineUsers.remove(username);
            UserInterceptor.onLineUsers.remove(username);
        }
        httpSession.removeAttribute("user");
        String message = MessageUtils.getMessage(true, null, getNames());
        broadcastAllUsers(message);
    }
}

至此,使用原生websocket实现一个聊天窗完成,接下来,将使用websocket+stomp实现群聊的功能

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值