ZetCode

Jetty 中的 WebSocket

最后修改于 2024 年 1 月 27 日

WebSocket 是一种互联网协议,可在客户端和服务器之间提供双向通信。WebSocket 被设计用于 Web 浏览器和 Web 服务器,但也可以被任何客户端或服务器应用程序使用。

消息可以采用 UTF-8 文本或二进制格式进行传递。

WebSocketServlet

Jetty 的 WebSocketServlet 是一个将 Servlet 技术与 WebSocket API 连接起来的 Servlet。在 WebSocketServlet 的 configure 方法中,我们使用 WebSocketServletFactory 注册我们的 WebSocket。WebSocket 是处理传入的 WebSocket 升级请求的 Java 类。

在下面的示例中,一个 Servlet 处理来自 Web 浏览器客户端的 WebSocket 请求。

$ tree
.
├── build.xml
└── src
    ├── com
    │   └── zetcode
    │       ├── MyServlet.java
    │       └── MySocket.java
    └── web
        ├── index.html
        ├── index.js
        └── WEB-INF

5 directories, 5 files

这是项目目录的内容。

index.html
<!DOCTYPE html>
<html>
    <body>
        <script src="index.js"></script>
    </body>
</html>

index.html 文件包含一个 <script> 标签,该标签加载一个外部脚本。

index.js
var ws = new WebSocket("ws://:8080/ws/wsexample");

ws.onopen = function() {
    document.write("WebSocket opened <br>");
    ws.send("Hello Server");
};

ws.onmessage = function(evt) {
    document.write("Message: " + evt.data);
};

ws.onclose = function() {
    document.write("<br>WebSocket closed");
};

ws.onerror = function(err) {
    document.write("Error: " + err);
};

此 JavaScript 代码创建与 WebSocket 的连接。它为 WebSocket 事件定义了四个回调函数。

var ws = new WebSocket("ws://:8080/ws/wsexample");

创建了一个 WebSocket 对象。ws 是 WebSocket 连接的 URI 方案。构造函数接受一个标识 WebSocket 服务器和资源名称的 URI。

ws.onopen = function() {
    document.write("WebSocket opened <br>");
    ws.send("Hello Server");
};

当创建与 WebSocket 的连接时,会触发一个 Open 事件。send 方法将数据传输到 WebSocket。

ws.onmessage = function(evt) {
    document.write("Message: " + evt.data);
};

使用 document.write 方法将从 WebSocket 服务器接收到的消息写入 HTML 页面。

MyServlet.java
package com.zetcode;
 
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.annotation.WebServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
 
@WebServlet(name = "WebSocket Servlet", urlPatterns = { "/wsexample" })
public class MyServlet extends WebSocketServlet {

    @Override
    public void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().println("HTTP GET method not implemented.");
    }

    @Override
    public void configure(WebSocketServletFactory factory) {
        factory.getPolicy().setIdleTimeout(10000);
        factory.register(MySocket.class);
    }
}

MyServlet.java 类中,我们在 configure 方法中将一个 WebSocket 注册到 WebSocketServletFactory

@Override
public void doGet(HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {
    response.getWriter().println("HTTP GET method not implemented.");
}

WebSocket 请求不同于 HTTP GET 请求。我们的 Servlet 返回一条消息,说明 GET 方法未实现,用于尝试通过 GET 方法连接到此 Servlet。如果我们没有实现此方法,我们将收到 Jetty 返回的 HTTP 错误 405 — HTTP 方法 GET 不被此 URL 支持 — 错误消息。

factory.getPolicy().setIdleTimeout(10000);

连接将在十秒后超时。届时将触发 Close 事件。

MySocket.java
package com.zetcode;

import java.io.IOException;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;

@WebSocket
public class MySocket {

    @OnWebSocketClose
    public void onClose(int statusCode, String reason) {
        System.out.println("Close: " + reason);
    }

    @OnWebSocketError
    public void onError(Throwable t) {
        System.out.println("Error: " + t.getMessage());
    }

    @OnWebSocketConnect
    public void onConnect(Session session) {
        System.out.println("Connect: " + session.getRemoteAddress().getAddress());
        
        try {
            session.getRemote().sendString("Hello Webbrowser");
        } catch (IOException e) {
            System.out.println("IO Exception");
        }
    }

    @OnWebSocketMessage
    public void onMessage(String message) {
        System.out.println("Message: " + message);
    }
}

这是一个处理 WebSocket 请求的 Java 类。

@WebSocket
public class MySocket {
...
}

@WebSocket 是一个标识 WebSocket 类的注解。

@OnWebSocketConnect
public void onConnect(Session session) {
    System.out.println("Connect: " + session.getRemoteAddress().getAddress());
    
    try {
        session.getRemote().sendString("Hello Webbrowser");
    } catch (IOException e) {
        System.out.println("IO Exception");
    }
}

@OnWebSocketConnect 注解标记了一个接收连接 Open 事件的方法。sendString 方法将消息发送回客户端。

@OnWebSocketMessage
public void onMessage(String message) {
    System.out.println("Message: " + message);
}

@OnWebSocketMessage 注解标记了一个接收来自客户端的消息事件的方法。消息将被写入我们启动 Jetty 的控制台。

build.xml
<?xml version="1.0" encoding="UTF-8"?>

<project name="WebSocket" default="compile">
    
    <property name="name" value="ws"/>
    <property environment="env"/>
    <property name="src.dir" value="src"/>
    <property name="web.dir" value="${src.dir}/web" />
    <property name="build.dir" location="${web.dir}/WEB-INF/classes"/>
    <property name="jetty.lib.dir" location="${env.JETTY_HOME}/lib"/>
    <property name="dist.dir" location="dist"/>
    <property name="deploy.path" location="${env.JETTY_BASE}/webapps"/>
  
    <path id="compile.classpath">
        <fileset dir="${jetty.lib.dir}"/>
    </path>
  
    <target name="init">
        <mkdir dir="${build.dir}"/>
        <mkdir dir="${dist.dir}"/>
    </target>     
  
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${build.dir}" 
               includeantruntime="false">
            <classpath refid="compile.classpath"/>
        </javac>
        <echo>Compilation completed</echo>
    </target>
  
    <target name="archive" depends="compile">
        <war destfile="${dist.dir}/${name}.war" needxmlfile="false">
            <fileset dir="${web.dir}"/>
        </war>
        <echo>Archive created</echo>
    </target> 
  
    <target name="clean" depends="init">
        <delete dir="${build.dir}"/>
        <delete dir="${dist.dir}"/>
        <echo>Cleaning completed</echo>
    </target>  
    
    <target name="deploy" depends="archive">
        <copy file="${dist.dir}/${name}.war" overwrite="true" 
              todir="${deploy.path}"/>
        <echo>Archive deployed</echo>
    </target>    
    
</project>

这是项目的 Ant build.xml 文件。

$ java -jar $JETTY_HOME/start.jar --add-to-start=websocket

我们需要将 WebSocket 模块添加到 Jetty 的基础目录(base directory)中,如果它尚未启用的话。

$ curl localhost:8080/ws/wsexample
HTTP GET method not implemented.

发送 HTTP GET 请求会产生此输出。

WebSocket client
图:WebSocket 客户端

浏览器是一个连接到 WebSocket 服务器并发送消息的客户端。它还从服务器接收“Hello Webbrowser”消息。

...
2014-09-06 17:08:42.193:INFO:oejs.Server:main: Started @4094ms
Connect: /127.0.0.1
Message: Hello Server
Error: Timeout on Read
Close: Idle Timeout

这些是出现在 Jetty 控制台上的消息。

WebSocket 客户端

在本节中,我们将展示如何从两个不同的客户端创建 WebSocket 请求。第一个客户端是一个使用 Python websocket 库的 Python 脚本。

$ sudo apt-get install python-websocket

我们需要安装 python-websocket 库。

#!/usr/bin/python

import websocket

websocket.enableTrace(True)
ws = websocket.create_connection("ws://:8080/ws/wsexample")
print "Sending message"
ws.send("Hello server")
result = ws.recv()
print "Received '%s'" % result 

该脚本将消息发送到我们的 WebSocket 服务器并接收响应。

$ ./pyclient.py 
Sending message
Received 'Hello client'

运行程序,我们得到此输出。

在第二个客户端中,我们使用 Jetty 的 WebSocketClientClientUpgradeRequest 类来建立 WebSocket 连接并发送和接收消息。

package com.zetcode;

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class JettyWebSocketClient {

    public static void main(String[] args) throws Exception {
        
        JettyWebSocketClient app = new JettyWebSocketClient();
        app.start();
    }
    
    public void start() throws Exception {

        WebSocketClient client = new WebSocketClient();
        MyWebSocket socket = new MyWebSocket();
        
        client.start();
        
        URI destUri = new URI("ws://:8080/ws/wsexample");
        
        ClientUpgradeRequest request = new ClientUpgradeRequest();
        System.out.println("Connecting to: " + destUri);
        client.connect(socket, destUri, request);
        socket.awaitClose(5, TimeUnit.SECONDS);

        client.stop();
    }

    @WebSocket
    public class MyWebSocket {
    
        private final CountDownLatch closeLatch = new CountDownLatch(1);

        @OnWebSocketConnect
        public void onConnect(Session session) throws IOException {
        
            System.out.println("Sending message: Hello server");
            session.getRemote().sendString("Hello server");
        }

        @OnWebSocketMessage
        public void onMessage(String message) {
            System.out.println("Message from Server: " + message);
        }

        @OnWebSocketClose
        public void onClose(int statusCode, String reason) {
            System.out.println("WebSocket Closed. Code:" + statusCode);
        }

        public boolean awaitClose(int duration, TimeUnit unit) 
                throws InterruptedException {
            return this.closeLatch.await(duration, unit);
        }
    }
}

这个例子是一个 Java WebSocket 客户端。

URI destUri = new URI("ws://:8080/ws/wsexample");

创建了一个指向我们 websocket 端点的 URI。

ClientUpgradeRequest request = new ClientUpgradeRequest();

打开 WebSocket 连接的初始对话是通过 HTTP 协议完成的。客户端向服务器发送一个Upgrade请求。如果服务器支持 WebSocket 协议,它会同意协议切换。Jetty 使用 ClientUpgradeRequest 类来创建 Upgrade 请求。

client.connect(socket, destUri, request);

我们通过已建立的 WebSocket 连接到指定的 URI 并发送 Upgrade 请求。

session.getRemote().sendString("Hello server");

消息使用 sendString 方法发送。

<?xml version="1.0" encoding="UTF-8"?>

<project name="JettyWebSocketClient" default="compile">
    
    <property name="name" value="wsclient"/>
    <property environment="env"/>
    <property name="src.dir" value="src"/>
    <property name="build.dir" location="build"/>
    <property name="jetty.lib.dir" location="${env.JETTY_HOME}/lib"/>
  
    <path id="compile.classpath">
        <fileset dir="${jetty.lib.dir}"/>
    </path>
    
 <path id="run.classpath">
        <pathelement path="${build.dir}"/>
        <fileset dir="${jetty.lib.dir}"> 
            <include name="**/*.jar"/> 
        </fileset>
    </path>      
  
    <target name="init">
        <mkdir dir="${build.dir}"/>
    </target>     
  
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${build.dir}" 
                includeantruntime="false">
            <classpath refid="compile.classpath"/>
        </javac>
        <echo>Compilation completed</echo>
    </target>
   
    <target name="clean" depends="init">
        <delete dir="${build.dir}"/>
        <echo>Cleaning completed</echo>
    </target>  
    
    <target name="run" depends="compile">
        <echo>Running the program</echo>
        <java classname="com.zetcode.JettyWebSocketClient" 
                classpathref="run.classpath"/>
    </target> 
    
</project>

这是编译和运行 WebSocket 客户端的 Ant 构建文件。

$ ant run
Buildfile: /home/janbodnar/prog/jetty/websocket3/build.xml
init:
compile:
     [echo] Compilation completed
run:
     [echo] Running the program
     [java] 2014-09-07 23:13:39.796:INFO::main: Logging initialized @1230ms
     [java] Connecting to: ws://:8080/ws/wsexample
     [java] Sending message: Hello server
     [java] Message from server: Hello client
     [java] WebSocket closed. Code:1001
BUILD SUCCESSFUL
Total time: 6 seconds

运行代码时,我们得到此输出。

在本章的 Jetty 教程中,我们已经在 Jetty 中建立了 WebSocket 连接。

作者

我叫 Jan Bodnar,我是一名充满激情的程序员,拥有丰富的编程经验。我自 2007 年以来一直在撰写编程文章。到目前为止,我已撰写了 1,400 多篇文章和 8 本电子书。我在教授编程方面拥有超过十年的经验。

列出所有Java教程