65.9K
CodeProject 正在变化。 阅读更多。
Home

带Websocket的单房间聊天程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (13投票s)

2016年1月25日

CPOL

5分钟阅读

viewsIcon

67288

downloadIcon

3009

这是一个使用 Websocket 创建简单聊天程序的示例。

引言

这是一个使用 Websocket 创建简单聊天程序的示例。

背景

这是一个使用 Websocket 创建简单聊天程序的示例。为了充分关注 Websocket,该示例被保持得尽可能简单,只支持一个聊天室。由于这是一个简单的聊天室,用户无需密码即可登录房间。尽管后端是用 Java 在 Tomcat 服务器上创建的,但它应该对使用 .Net 或 Node.js 等其他平台的人们具有一定的参考价值。

附带的是一个 Maven Web 项目。我已使用 Java 1.8.0_65 和 Tomcat 7.0.54 对其进行了测试。我使用 Eclipse Java EE IDE for Web Developers Mars.1 Release (4.5.1) 作为我的开发 IDE。我还将其部署在 Amazon EC2 Ubuntu 实例上,并使用 Chrome、Firefox 和一些移动设备进行了测试。

服务器环境设置

在 Tomcat 7 及以上版本上使用 Websocket 不需要特殊的环境设置。至少在 Tomcat 7.0.54 上是这样的。在 "web.xml" 文件中也没有对 Websocket 的特殊配置。以下是附带 Maven 项目中的 "pom.xml" 文件。

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.song.example</groupId>
    <artifactId>single-room-chat</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    
    <properties>
        <tomcat.version>7.0.55</tomcat.version>
        <websocket.version>1.1</websocket.version>
        <jackson.version>2.6.4</jackson.version>
    </properties>
          
    <dependencies>
        <!-- Dependency needed by the Web-socket -->
        <!-- Tomcat has it, so no need to package into the war file -->
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>${websocket.version}</version>
            <scope>provided</scope>
        </dependency>
        
        <!-- Used to serialize the message from the browser -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        
        <!-- Sevlet jars for compilation, provided by Tomcat -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-servlet-api</artifactId>
            <version>${tomcat.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
      
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                <source>1.8</source>
                <target>1.8</target>
                </configuration>
            </plugin>
                      
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                <warSourceDirectory>WebContent</warSourceDirectory>
                <failOnMissingWebXml>true</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
</project>

 

  • "javax.websocket-api" 依赖项仅用于编译代码。由于 Tomcat 7 及以上版本默认支持 Websocket,因此我们无需将此依赖项打包到 war 文件中;
  • "jackson-databind" 依赖项用于序列化/反序列化来自 Web 浏览器的消息。如果您不需要序列化或想使用其他序列化方法,则无需将其添加为依赖项。

服务器与客户端之间的消息

对于简单的聊天室,消息也很简单。它只有一个消息类型和一个消息内容,这在 "ChatMessage.java" 文件中实现。

package com.song.chat.message;

public class ChatMessage {
    private MessageType messageType;
    private String message;

    public void setMessageType(MessageType v) { this.messageType = v; }
    public MessageType getMessageType() { return messageType; }
    public void setMessage(String v) { this.message = v; }
    public String getMessage() { return this.message; }
}

"MessageType" 在 "MessageType.java" 文件中被实现为一个 "enum"。

package com.song.chat.message;

public enum MessageType { LOGIN, MESSAGE }

此示例中只有两种消息类型。一种用于登录聊天室的请求,另一种用于发送消息以在房间中广播。

服务器端点

为了让浏览器通过 Websocket 与服务器通信,我们需要创建一个用 "@ServerEndpoint" 注释的类。

package com.song.web.socket;

import java.io.IOException;
import java.util.Map;
import java.util.logging.Logger;

import javax.websocket.CloseReason;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.song.chat.message.ChatMessage;
import com.song.chat.message.MessageType;
import com.song.chat.room.Room;

@ServerEndpoint(value = "/chat")
public class ChatEndpoint {
    private Logger log = Logger.getLogger(ChatEndpoint.class.getSimpleName());
    private Room room = Room.getRoom();

    @OnOpen
    public void open(final Session session, EndpointConfig config) {}

    @OnMessage
    public void onMessage(final Session session, final String messageJson) {
        ObjectMapper mapper = new ObjectMapper();
        ChatMessage chatMessage = null;
        try {
            chatMessage = mapper.readValue(messageJson, ChatMessage.class);
        } catch (IOException e) {
            String message = "Badly formatted message";
            try {
                session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, message));
            } catch (IOException ex) { log.severe(ex.getMessage()); }
        } ;

        Map<String, Object> properties = session.getUserProperties();
        if (chatMessage.getMessageType() == MessageType.LOGIN) {
            String name = chatMessage.getMessage();
            properties.put("name", name);
            room.join(session);
            room.sendMessage(name + " - Joined the chat room");
        }
        else {
            String name = (String)properties.get("name");
            room.sendMessage(name + " - " + chatMessage.getMessage());
        }
    }

    @OnClose
    public void onClose(Session session, CloseReason reason) {
        room.leave(session);
        room.sendMessage((String)session.getUserProperties().get("name") + " - Left the room");
    }

    @OnError
    public void onError(Session session, Throwable ex) { log.info("Error: " + ex.getMessage()); }
}
  • "@ServerEndpoint" 注释表示 "ChatEndpoint" 是一个 Websocket 端点。访问此端点的 URL 应为 "ws://server-address:port-number/single-room-chat/chat";
  • "@OnOpen" 注释表示当请求新的 Websocket 连接时,将调用 "onOpen" 方法;
  • "@OnMessage" 注释表示当新消息到达时,将调用 "onMessage" 方法。消息可以是登录请求,也可以是将消息广播到房间的消息;
  • "@OnClose" 注释表示当客户端关闭 Websocket 连接时,将调用 "onClose" 方法。即使连接关闭不是自愿的(可能是由于互联网连接丢失或错误情况),也会调用此方法;
  • "@OnError" 注释表示发生错误时,将调用 "onError" 方法

在 Tomcat 环境中,"ChatEndpoint" 类不是单例。它具有会话范围,为来自客户端的每个 Websocket 连接会话实例化。一旦建立连接,会话实例将用于所有消息,直到连接会话关闭为止。"Room.java" 实现聊天室,负责将消息广播到所有活动的 Websocket 连接。

package com.song.chat.room;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.websocket.Session;

public class Room {
    private static Room instance = null;
    private List<Session> sessions = new ArrayList<Session>();

    public synchronized void join(Session session) { sessions.add(session); }
    public synchronized void leave(Session session) { sessions.remove(session); }
    public synchronized void sendMessage(String message) {
        for (Session session: sessions) {
            if (session.isOpen()) {
                try { session.getBasicRemote().sendText(message); }
                catch (IOException e) { e.printStackTrace(); }
            }
        }
    }

    public synchronized static Room getRoom() {
        if (instance == null) { instance = new Room(); }
        return instance;
    }
}
  • 由于该应用程序是单个聊天室,因此 "Room" 类是单例;
  • 由于房间里会有很多用户,所有方法都是同步的,以避免冲突和竞态条件。

客户端

客户端 HTML 布局在 "index.jsp" 文件中实现。

<body>
<div id="container">
    <div id="loginPanel">
        <div id="infoLabel">Type a name to join the room</div>
        <div style="padding: 10px;">
            <input id="txtLogin" type="text" class="loginInput"
                onkeyup="proxy.login_keyup(event)" />
            <button type="button" class="loginInput" onclick="proxy.login()">Login</button>
        </div>
    </div>
    <div id="msgPanel" style="display: none">
        <div id="msgContainer" style="overflow: auto;"></div>
        <div id="msgController">
            <textarea id="txtMsg" 
                title="Enter to send message"
                onkeyup="proxy.sendMessage_keyup(event)"
                style="height: 20px; width: 100%"></textarea>
            <button style="height: 30px; width: 100px" type="button"
                onclick="proxy.logout()">Logout</button>
        </div>
    </div>
</div>
</body>

用于创建 Websocket 并与服务器通信的 JavaScript 在 "chatroom.js" 文件中实现。

var CreateProxy = function(wsUri) {
    var websocket = null;
    var audio = null;
    var elements = null;
    
    var playSound = function() {
        if (audio == null) {
            audio = new Audio('content/sounds/beep.wav');
        }
        
        audio.play();
    };
    
    var showMsgPanel = function() {
            elements.loginPanel.style.display = "none";
            elements.msgPanel.style.display = "block";
            elements.txtMsg.focus();
    };
            
    var hideMsgPanel = function() {
            elements.loginPanel.style.display = "block";
            elements.msgPanel.style.display = "none";
            elements.txtLogin.focus();
    };
    
    var displayMessage = function(msg) {
        if (elements.msgContainer.childNodes.length == 100) {
            elements.msgContainer.removeChild(elements.msgContainer.childNodes[0]);
        }
        
        var div = document.createElement('div');
        div.className = 'msgrow';
        var textnode = document.createTextNode(msg);
        div.appendChild(textnode); 
        elements.msgContainer.appendChild(div);
        
        elements.msgContainer.scrollTop = elements.msgContainer.scrollHeight;
    };
    
    var clearMessage = function() {
        elements.msgContainer.innerHTML = '';
    };
    
    return {
        login: function() {
            elements.txtLogin.focus();
            
            var name = elements.txtLogin.value.trim();
            if (name == '') { return; }
            
            elements.txtLogin.value = '';
            
            // Initiate the socket and set up the events
            if (websocket == null) {
                websocket = new WebSocket(wsUri);
                
                websocket.onopen = function() {
                    var message = { messageType: 'LOGIN', message: name };
                    websocket.send(JSON.stringify(message));
                };
                websocket.onmessage = function(e) {
                    displayMessage(e.data);
                    showMsgPanel();
                    playSound();
                };
                websocket.onerror = function(e) {};
                websocket.onclose = function(e) {
                    websocket = null;
                    clearMessage();
                    hideMsgPanel();
                };
            }
        },
        sendMessage: function() {
            elements.txtMsg.focus();
            
            if (websocket != null && websocket.readyState == 1) {
                var input = elements.txtMsg.value.trim();
                if (input == '') { return; }
                
                elements.txtMsg.value = '';
                
                var message = { messageType: 'MESSAGE', message: input };
                
                // Send a message through the web-socket
                websocket.send(JSON.stringify(message));
            }
        },
        login_keyup: function(e) { if (e.keyCode == 13) { this.login(); } },
        sendMessage_keyup: function(e) { if (e.keyCode == 13) { this.sendMessage(); } },
        logout: function() {
            if (websocket != null && websocket.readyState == 1) { websocket.close();}
        },
        initiate: function(e) {
            elements = e;
            elements.txtLogin.focus();
        }
    }
};

"CreateProxy" 函数在 "index.jsp" 文件中的 "DOMContentLoaded" 事件中使用。

var proxy = CreateProxy(wsUri);

document.addEventListener("DOMContentLoaded", function(event) {
    console.log(document.getElementById('loginPanel'));
    proxy.initiate({
        loginPanel: document.getElementById('loginPanel'),
        msgPanel: document.getElementById('msgPanel'),
        txtMsg: document.getElementById('txtMsg'),
        txtLogin: document.getElementById('txtLogin'),
        msgContainer: document.getElementById('msgContainer')
    });
});

您可能想稍微注意一下 "chatroom.js" 文件中的 "login" 函数。这是在客户端创建 Websocket 的地方。您还应该注意到它还设置了与服务器端点类似的 "onopen"、"onmessage"、"onclose" 和 "onerror" 事件。根据我的经验,"onerror" 方法可能并不总是可靠触发,但 "onclose" 事件总是会触发,即使关闭是由于互联网连接丢失或 Web 服务器关闭。

运行应用程序

附带的是一个 Maven 项目。您可以通过 "mvn clean install" 来构建它,并将 war 文件部署到 Tomcat 服务器以运行它。您也可以将其导入 Eclipse 中运行。如果您不确定如何将 Maven 项目导入 Eclipse,可以参考此链接。应用程序启动后,您将看到登录页面,要求您输入任何用户名即可登录聊天室。

登录房间后,您可以发送和接收消息。您可以尝试在多个浏览器窗口中打开网页,看看消息是否可靠地在房间中广播。

关注点

  • 这是一个使用 Websocket 创建简单聊天程序的示例;
  • 它有点粗糙,但足够简单,因此重点在于 Websoket;
  • 每当我们谈论套接字时,我们都需要仔细考虑其可靠性,因为互联网本身并不可靠。我已经将此示例部署在 Amazon EC2 Ubuntu 实例上,并在使用移动互联网连接购物时用一些移动设备进行了测试;
  • 虽然这个例子是在 Tomcat 上运行的,但如果您的平台是 .Net 或 Node.js,它应该具有一定的参考价值;
  • 我希望您喜欢我的帖子,也希望这个例子能在某个方面帮助到您。

历史

第一次修订 - 2016/1/25

© . All rights reserved.