初识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));
            }
        }
    }
}

这样就简单实现了一个聊天室。

效果图如下:

websocket聊天室

这么写可以发送html标签来达到发送图片的目的🤣

标签: Web

添加新评论