基于websocket实现的在线聊天室

SA21225241 2022-01-19 19:08:59

Websocket

1.Websocket介绍

Websocket是一种网络通信协议,是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。HTTP协议是一种无状态的、无连接的、单向的应用层协议,它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。这种通信模型的弊端在于HTTP协议无法实现服务器到客户端的消息主动推送,并且如果服务器有连续的状态变化,客户端获得消息是比较麻烦的,很浪费资源。我们介绍的主角是Websocket,其最大特点就是服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,这是HTTP协议所不具备的功能。这里我们通过一张图来说明HTTP与Websocket的区别:

img

通过上图我们可以发现,其原理和TCP基本相同,只需做一个握手动作,就可以形成一条快速通道。 客户端发起HTTP请求,附加头信息为:“Upgrade Websocket”,服务端解析报文,并返回握手信息进而建立链接。链接建立后即可进行数据传输,当传输结束后就可以选择断开链接。当然Websocket还有不少特点,比如数据格式比较轻量,性能开销小,通信高效,可以发送文本,也可以发送二进制数据,与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器等等。我们接下来介绍什么是Websocket协议

2.Websocket协议

Websocket协议有两部分:握手与数据传输

其中,握手部分是基于HTTP协议的,我们来看一个典型的Websocket握手报文:

GET /uin=xxxxxxxx&app=xxxxxxxxx&token=XXXXXXXXXXXX HTTP/1.1
Host: [server.example.cn:443](http://server.example.cn:443)
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36
Upgrade: websocket
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: user_id=XXXXX
Sec-WebSocket-Key: 1/2hTi/+eNURiekpNI4k5Q==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Protocol: binary, base64

上面字段中,Connection: Upgrade字段标识HTTP请求是一个协议升级请求。Upgrade: websocket表示协议升级为Websocket协议,Sec-WebSocket-Version: 13则表示客户端支持的Websocket版本。Sec-WebSocket-Key字段是客户端采用base64编码的24位随机字符序列,有点类似接头暗号,是服务器接受客户端新协议的证明。当握手成功时,通信就不采用HTTP协议了,而是采用Websocket独立的数据帧,大致如下图所示:

img

3.客户端的实现

实现Websocket的浏览器将通过Websocket对象公开所有必须的客户端功能,我们采用以下API创建该项目的Websocket对象:

 var ws = new WebSocket("ws://localhost/chat");

其中,Websocket对象的相关事件有:

img

Websocket对象的相关方法有:

img

4.服务端的实现

本项目是采用Java的,Java Websocket应用采用一系列的WebSocketEndpoint组成的。其中,Endpoint是一个Java对象,代表Websocket链接的一端,对于服务端,我们可以视为处理具体Websocket消息的接口,这一点就像servlet对于HTTP请求的作用。本项目中,我选择了注解式的方法来定义Endpoint(一个POJO),并添加@ServerEndpoint相关注解。服务端的Endpoint实例在握手时创建,并且在客户端与服务端连接的过程中一直有效(直到链接关闭)。在Endpoint接口中定义了与其生命周期相关的方法如下:

img

服务端通过@OnMessage的注释来指定接受客户端消息的方法,当然也可以利用给session添加MessageHandler的方法来处理接受客户端发来的消息。服务端发送消息给客户端则通过RomoteEndpoint完成,其实例由session维护。根据具体情况,可以使用session.getBasicRemote来获取同步消息发送实例,然后调用send()方法即可发送消息,当然对于推送异步消息发送实例,这里采用session.getAsynRemote来实现。

在线聊天室的实现

1.创建项目并添加Websocket依赖(项目使用springboot)

<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.webjars</groupId>
            <artifactId>webjars-locator-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>sockjs-client</artifactId>
            <version>1.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>stomp-websocket</artifactId>
            <version>2.3.3</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>3.3.7</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.1.1-1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <dependency>
            <groupId>com.google.collections</groupId>
            <artifactId>google-collections</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

2.前端页面

(1)登陆页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="js/jquery.min.js"></script>
<form id="loginForm">
    <label>账号 Username: </label><br>
    <input type="text" name="user"><br>
    <label>密码 Password:  </label><br>
    <input type="password" name="pwd"><br><br>
    <button type="button" id="btn">登录 Login</button>
</form>
<p id="err_msg"></p>
<script>
    $("#btn").click(function () {
        $.get("toLogin?",$("#loginForm").serialize(),function(res){
            if (res.flag){
                location.href = "main.html";//异步跳转到main.html的界面
            } else {
                $("#err_msg").html(res.message);
            }
        },"json");
    })
</script>
</body>
</html>

(2)主界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>
    #left{
        float: left;
        width: 30%;
        height: 500px;
        margin-left: 200px;
    }
    #right{
        margin-top: 20px;
        border: cornflowerblue 3px solid;
        float: right;
        width: 30%;
        height: 500px;
        margin-right: 300px;
    }
    #top{
        float: top;
        width: 30%;
        height: 250px;
    }
    #content{
        border: slateblue 3px solid;
        width: 100%;
        height: 500px;
    }
    #input{
        margin-top: 40px;
        width: 100%;
        height: 200px;
    }
    #input input{
        border: coral 2px solid;
        width: 100%;
        height: 100px;
        margin-bottom: 20px;
    }

    #input button{
        float: right;
    }
    #mes_left{
        float: left;

    }
    #mes_right{
        float: right;
        width: 50%;
        text-align: right;
    }
</style>
<script src="js/jquery.min.js"></script>
<h3 style="text-align: center" id="username"></h3>
<div>
    <div id="left">
        <h4 id="new"></h4>
        <div id="content">

        </div>
        <div id="input">
            <input type="text" id="input_text">

            <button id="submit">发送 Enter</button>
        </div>
    </div>
    <div id="right">
        <div id="top">
            <p>目前在线的好友</p>
            <div id="hylist">

            </div>
        </div>
        <div id="bottom">
            <p>系统广播</p>
            <div id="xtlist">
            </div>
        </div>
    </div>
</div>
<script>
    var username;
    $(function () {
        var toName;
        $.ajax({
            url:"getUsername",
            success:function (res) {
                username = res;
                $("#username").html("用户:"+ res +"<span>  在线</span>");
            },
            async:false //不能是异步请求,因为前后有依赖关系
        });
        //创建websocket对象
        var ws = new WebSocket("ws://localhost/chat");
        //给ws绑定上事件
        ws.onopen = function (ev) {
            //显示在线信息
            $("#username").html("用户:"+ username +"<span style='color: green' >  在线</span>");
        }
        //接收到服务端推送的消息后触发的实践
        ws.onmessage = function (ev) {
            var dataStr = ev.data;
            //将dataStr转化为json对象
            var res = JSON.parse(dataStr);
            //判断是否是系统消息
            if(res.system){
                //是系统消息
                //1.实现好友列表
                //2.实现系统广播
                var names = res.message;
                var userlistStr = "";
                var broadcastListStr = "";
                for (var name of names){
                    if (name != username){
                        userlistStr += "<a onclick='showChat(\""+name+"\")'>"+ name +"</a></br>";
                        broadcastListStr += "<p>"+ 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);
                //sessionStorage
                var chatdata = sessionStorage.getItem(res.fromName);
                if (chatdata != null){
                    str = chatdata + str;
                }
                sessionStorage.setItem(res.fromName,str);

            };
        },
        ws.onclose = function (ev) {
            //显示离线信息
            $("#username").html("用户:"+ username +"<span style='color: red' >  离线</span>");
        }

        showChat = function(name){
            // alert("dsaad");
            toName = name;
            //清空聊天区
            $("#content").html("");
            $("#new").html("正在和"+toName+"聊天");
            var chatdata = sessionStorage.getItem(toName);
            if (chatdata != null){
                $("#content").html(chatdata);
            }
        };
        //发送消息
        $("#submit").click(function () {
            //获取输入的内容
            var data = $("#input_text").val();
            //清除输入区的内容
            $("#input_text").val("");
            var json = {"toName": toName ,"message": data};
            //将数据展示在聊天区
            var str = "<span id='mes_right'>"+ data +"</span></br>";
            $("#content").append(str);

            var chatdata = sessionStorage.getItem(toName);
            if (chatdata != null){
                str = chatdata + str;
            }
            sessionStorage.setItem(toName,str);
            //发送数据
            ws.send(JSON.stringify(json));
        })
    })
</script>
</body>
</html>

3.POJO类

(1)Message类(浏览器发给服务端的websocket)

public class Message {
    private String toName;
    private String message;

    public String getToName() {
        return toName;
    }

    public void setToName(String toName) {
        this.toName = toName;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

(2)Resultmessage类(服务端给浏览器发送的websocket)

public class ResultMessage {
    private boolean isSystem;
    private String fromName;
    private Object message;//如果是系统消息就是数组

    public boolean isSystem() {
        return isSystem;
    }

    public void setSystem(boolean system) {
        isSystem = system;
    }

    public String getFromName() {
        return fromName;
    }

    public void setFromName(String fromName) {
        this.fromName = fromName;
    }

    public Object getMessage() {
        return message;
    }

    public void setMessage(Object message) {
        this.message = message;
    }
}

(3)Result类(用于登录响应给浏览器的类)

public class Result {
    private boolean flag;//登录成功还是失败
    private  String message;//登录结果提示
}

4.消息工具类

//用于封装消息的工具类
public class MessageUtils {
    public static String getMessage(boolean isSystemMessage,String fromName,Object message){
        try {
            ResultMessage result = new ResultMessage();
            result.setSystem(isSystemMessage);
            result.setMessage(message);
            if (fromName!=null){
                result.setFromName(fromName);
            }
            ObjectMapper mapper = new ObjectMapper();
            return mapper.writeValueAsString(result);
        }catch (JsonProcessingException e){
            e.printStackTrace();
        }
        return null;
    }
}

5.登录逻辑处理类

@RestController
public class LoginController {
    @RequestMapping("/toLogin")
    public Result tologin(@RequestParam("user") String user,@RequestParam("pwd") String pwd, HttpSession session){
        Result result = new Result();
        if (user.equals("SA21225241")&&pwd.equals("123456")){
            result.setFlag(true);
            session.setAttribute("user",user);
        }else if (user.equals("SA21225242")&&pwd.equals("123456")){
            result.setFlag(true);
            session.setAttribute("user",user);
        }else if (user.equals("Administer")&&pwd.equals("000")){
            result.setFlag(true);
            session.setAttribute("user",user);
        }
        else if (user.equals("SA21225243")&&pwd.equals("123456")){
            result.setFlag(true);
            session.setAttribute("user",user);
        }else {
            result.setFlag(false);
            result.setMessage("登录失败");
        }
        return result;
    }

    @RequestMapping("/getUsername")
    public String getUsername(HttpSession session){
        String username = (String) session.getAttribute("user");
        return username;
    }
}

6.获取HTTP的session类

public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        //把http的session对象存到配置对象中
        sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
    }
}

7.Websocket配置类

@Configuration
public class WebSocketConfig {
    @Bean
    //注入ServerEndpointExporter bean对象,自动注册使用注解@ServerEndpoint的bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

8.Endpoint类(用于实现聊天)

@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class)
//注意!虽然之前定义了配置类,但是和ChatEndpoint没关系,因此需要绑定关系,卡了好久
@Component
public class ChatEndpoint {

    //用来存储每一个用户客户端对象的ChatEndpoint对象
    private static Map<String,ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();
    //声明session对象,通过对象可以发送消息给指定的用户
    private Session session;
    //声明HttpSession对象,我们之前在HttpSession对象中存储了用户名
    private HttpSession httpSession;

    @OnOpen
    //链接建立时被调用
    public void onOpen(Session session, EndpointConfig config){
        this.session = session;//将局部session赋值给全局的session
        HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        this.httpSession = httpSession;//获取对象
        String username = (String)httpSession.getAttribute("user");
        onlineUsers.put(username,this);//将当前对象存储到容器中
        //将当前在线用户的用户名推送给所有的客户端
        //1 获取消息
        String message = MessageUtils.getMessage(true, null, getNames());
        //2 调用方法进行系统消息的推送
        broadcastAllUsers(message);
    }

    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();
        }
    }

    //返回在线用户名
    private Set<String> getNames()
    {
        return onlineUsers.keySet();
    }


    @OnMessage
    public void onMessage(String message,Session session){
        //将message数据转换成message对象
        try {
            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");
            String resultMessage = MessageUtils.getMessage(false, username, data);
            //发送数据
            onlineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
    //关闭
    @OnClose
    //链接关闭时被调用
    public void onClose(Session session) {
        String username = (String) httpSession.getAttribute("user");
        //从容器中删除指定的用户
        onlineUsers.remove(username);
        MessageUtils.getMessage(true,null,getNames());
    }}

9.ChatroomApp与相关属性

@SpringBootApplication
public class ChatroomApp {

    public static void main(String[] args) 
    {
        SpringApplication.run(ChatroomApp.class, args);
    }

}
server.port=80
spring.thymeleaf.cache=false

项目展示与总结

(1)登陆界面

img

(2)聊天界面

img

总结

由于不太会弄前端的东西,写出来的效果感觉有亿点点难看,不过基本功能是实现了的(虽然折腾了不少时间..)。通过本项目,我较为系统的了解了利用Websocket来编写程序的方法与技巧,在学习过程中认识到了自己的能力还是得多锻炼。

作者 NP241

...全文
362 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧