β

Java WebSocket教程

鸟窝 5931 阅读

翻译自 masterthebossWebSockets tutorial on Wildfly 8,同时在翻译的过程中增加了很多的背景知识。

WebSocket 是web客户端和服务器之间新的通讯方式, 依然架构在HTTP协议之上。使用WebSocket连接, web应用程序可以执行实时的交互, 而不是以前的poll方式。

WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议,可以用来创建快速的更大规模的健壮的高性能实时的web应用程序。WebSocket通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。
在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

什么是WebSocket?

一个WebSocket是通过一个独立的TCP连接实现的、异步的、双向的、全双工的消息传递实现机制。WebSockets不是一个HTTP连接,却使用HTTP来引导一个WebSocket连接。一个全双工的系统允许同时进行双向的通讯。陆地线路电话是一个全双工设施的例子,因为它们允许两个通话者同时讲话并被对方听到。最初WebSocket被提议作为HTML5规范的一部分,HTML5承诺给现代的交互式的web应用带来开发上的便利和网络效率,但是随后WebSocket被移到一个仅用来存放WebSockets规范的独立的标准文档里。它包含两件事情 -- WebSocket协议规范,即2011年12月发布的RFC 6455,和WebSocket JavaScript API。

WebSocket协议利用HTTP 升级头信息来把一个HTTP连接升级为一个WebSocket连接。HTML5 WebSockets 解决了许多导致HTTP不适合于实时应用的问题,并且它通过避免复杂的工作方式使得应用结构很简单。

最新的浏览器都支持WebSockets,

WebSocket是如何工作的?

每一个WebSocket连接的生命都是从一个HTTP请求开始的。HTTP请求跟其他请求很类似,除了它拥有一个Upgrade头信息。Upgrade头信息表示一个客户端希望把连接升级为不同的协议。对WebSockets来说,它希望升级为WebSocket协议。当客户端和服务器通过底层连接第一次握手时,WebSocket连接通过把HTTP协议转换升级为WebSockets协议而得以建立。一旦WebSocket连接成功建立,消息就可以在客户端和服务器之间进行双向发送

一些可能的WebSockets使用案例有:

JSR 356,WebSocket的Java API,规定了开发者把WebSockets 整合进他们的应用时可以使用的Java API — 包括服务器端和Java客户端。JSR 356是Java EE 7标准中的一部分。这意味着所有Java EE 7兼容的应用服务器都将有一个遵守JSR 356标准的WebSocket协议的实现。开发者也可以在Java EE 7应用服务器之外使用JSR 356。目前Apache Tomcat 8提供了JSR 356 API的WebSocket支持。 Jboss Wildfly 8 (原JBoss Application Server)也支持JSR 356.

一个Java客户端可以使用兼容JSR 356的客户端实现,来连接到WebSocket服务器。对web客户端来说,开发者可以使用WebSocket JavaScript API来和WebSocket服务器进行通讯。WebSocket客户端和WebSocket服务器之间的区别,仅在于两者之间是通过什么方式连接起来的。一个WebSocket客户端是一个WebSocket终端,它初始化了一个到对方的连接。一个WebSocket服务器也是一个WebSocket终端,它被发布出去并且等待来自对方的连接。在客户端和服务器端都有回调监听方法 -- onOpen , onMessage , onError, onClose。

怎么创建你的第一个WebSocket应用呢?基本上我们还是会使用Javascript API编写WebSocket客户端, 在服务器端, 本文使用JSR 356规范定义的通用模式和技术处理WebSocket的通讯。

下面看一个简单的例子, 演示了如果使用JavaScript WebSocket客户端与运行在Wildfly 8服务器通信.

客户端代码

<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
    </head>
 
    <body>
        <meta charset="utf-8">
        <title>HelloWorld Web sockets</title>
        <script language="javascript" type="text/javascript">
            var wsUri = getRootUri() + "/websocket-hello/hello";
 
            function getRootUri() {
                return "ws://" + (document.location.hostname == "" ? "localhost" : document.location.hostname) + ":" +
                        (document.location.port == "" ? "8080" : document.location.port);
            }
 
            function init() {
                output = document.getElementById("output");
            }
 
            function send_message() {
 
                websocket = new WebSocket(wsUri);
                websocket.onopen = function(evt) {
                    onOpen(evt)
                };
                websocket.onmessage = function(evt) {
                    onMessage(evt)
                };
                websocket.onerror = function(evt) {
                    onError(evt)
                };
 
            }
 
            function onOpen(evt) {
                writeToScreen("Connected to Endpoint!");
                doSend(textID.value);
 
            }
 
            function onMessage(evt) {
                writeToScreen("Message Received: " + evt.data);
            }
 
            function onError(evt) {
                writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
            }
 
            function doSend(message) {
                writeToScreen("Message Sent: " + message);
                websocket.send(message);
            }
 
            function writeToScreen(message) {
                var pre = document.createElement("p");
                pre.style.wordWrap = "break-word";
                pre.innerHTML = message;
                  
                output.appendChild(pre);
            }
 
            window.addEventListener("load", init, false);
 
        </script>
 
        <h1 style="text-align: center;">Hello World WebSocket Client</h2>
 
        <br>
 
        <div style="text-align: center;">
            <form action="">
                <input onclick="send_message()" value="Send" type="button">
                <input id="textID" name="message" value="Hello WebSocket!" type="text"><br>
            </form>
        </div>
        <div id="output"></div>
</body>
</html>

如你所见,要想使用WebSocket协议与服务器通信, 需要一个WebSocket对象。它会自动连接服务器.

websocket = new WebSocket(wsUri);

连接上会触发open事件:

websocket.onopen = function(evt) {
        onOpen(evt)
 };

一旦连接成功,则向服务器发送一个简单的hello消息。

websocket.send(message);

服务器端代码

有两种创建服务器端代码的方法:

建议开发时采用注解方式, 这样可以使用POJO就可以实现WebSocket Endpoint. 而且不限定处理事件的方法名。 代码也更简单。

本例就采用注解的方式, 接收WebSocket请求的类是一个POJO, 通过@ServerEndpoint标注. 这个注解告诉容器此类应该被当作一个WebSocket的Endpoint。value值就是WebSocket endpoint的path.

package com.sample.websocket;
 
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
 
  
@ServerEndpoint("/hello")
public class HelloWorldEndpoint {
 
     
    @OnMessage
    public String hello(String message) {
        System.out.println("Received : "+ message);
        return message;
    }
 
    @OnOpen
    public void myOnOpen(Session session) {
        System.out.println("WebSocket opened: " + session.getId());
    }
 
    @OnClose
    public void myOnClose(CloseReason reason) {
        System.out.println("Closing a WebSocket due to " + reason.getReasonPhrase());
    }
 
}

注意:这个例子还包括了其它两个回调函数: @OnOpen标注的方法在WebSocket连接开始时被调用, Web Session作为参数。 另外一个@OnClose标注的方法在连接关闭时被调用。

就是这么简单。但是为了编译这个例子你还需要Websockets API的实现,它在WildFly 8发布中(或者你用JSR 356的参考实现,或其它的容器提供的jar, 如tomcat):

modules\system\layers\base\javax\websocket\api\main\jboss-websocket-api_1.0_spec-1.0.0.Final.jar

对于Maven用户, 你需要增加undertow-websockets-jsr依赖:

<dependency>
	<groupId>org.jboss.spec.javax.websocket</groupId>
	<artifactId>jboss-websocket-api_1.0_spec</artifactId>
	<version>1.0.0.Final</version>
</dependency>

这个例子比较早,应该是2013年写的,jsr 256还未发布。 现在,你应该直接使用Java EE提供的API

<dependency>
	<groupId>javax.websocket</groupId>
	<artifactId>javax.websocket-api</artifactId>
	<version>1.1</version>
 </dependency>

编解码器

前面的例子中WebSocket通信的消息类型默认为String。接下来的例子演示如何使用Encoder和Decoder传输更复杂的数据。
Websocket使用Decoder将文本消息转换成Java对象,然后传给@OnMessage方法处理; 而当对象写入到session中时,Websocket将使用Encoder将Java对象转换成文本,再发送给客户端。
更常用的, 我们使用XML 或者 JSON 来传送数据,所以将会会将Java对象与XML/JSON数据相互转换.

下图描绘了客户端和服务器使用encoder/decoder标准通信过程。

声明Encoder/Decoder也是相当的简单: 你只需在@ServerEndpoint注解中增加encoder/decoder设置:

package com.sample.websocket;
 
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
 
@ServerEndpoint(value = "/hello",
        decoders = {
            MessageDecoder.class,},
        encoders = {
            MessageEncoder.class
        })
public class HelloWorldEndpoint {
 
    @OnMessage
    public Person hello(Person person, Session session) {
        if (person.getName().equals("john")) {
            person.setName("Mr. John");
        }
        try {
            session.getBasicRemote().sendObject(person);
            System.out.println("sent ");
        } catch (Exception ex) {
            Logger.getLogger(HelloWorldEndpoint.class.getName()).log(Level.SEVERE, null, ex);
        }
        return person;
 
    }
 
    @OnOpen
    public void myOnOpen(Session session) {
    }
 
}

正像你看到的, OnMessage方法使用Java Object person作为参数, 我们为名字增加个尊称再返回给客户端。通过session.getBasicRemote().sendObject(Object obj)返回数据.
容器负责使用你指定的Decoder将接收到的XML消息转为Java对象:

package com.sample.websocket;
 
import java.io.StringReader;
 
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;
import javax.xml.bind.*;
 
  
public class MessageDecoder implements Decoder.Text<Person> {
 
    @Override
    public Person decode(String s) {
        System.out.println("Incoming XML " + s);
        Person person = null;
        JAXBContext jaxbContext;
        try {
            jaxbContext = JAXBContext.newInstance(Person.class);
 
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
 
            StringReader reader = new StringReader(s);
            person = (Person) unmarshaller.unmarshal(reader);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return person;
    }
 
    @Override
    public boolean willDecode(String s) {
          
        return (s != null);
    }
 
    @Override
    public void init(EndpointConfig endpointConfig) {
        // do nothing.
    }
 
    @Override
    public void destroy() {
        // do nothing.
    }
}

这里我们使用JAXB做转换。我们只要实现一个泛型接口Decoder.Text 或者 Decoder.Binary, 根据你传输的数据是文本还是二进制选择一个.

所以数据由Decoder解码, 传给Endpoint (这里的 HelloWorldEndpoint), 在返回给client之前, 它还会被下面的Encoder转换成XML:

package com.sample.websocket;
 
import java.io.StringWriter;
  
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
  
public class MessageEncoder implements Encoder.Text<Person> {
 
    @Override
    public String encode(Person object) throws EncodeException {
 
        JAXBContext jaxbContext = null;
        StringWriter st = null;
        try {
            jaxbContext = JAXBContext.newInstance(Person.class);
 
            Marshaller marshaller = jaxbContext.createMarshaller();
            st = new StringWriter();
            marshaller.marshal(object, st);
            System.out.println("OutGoing XML " + st.toString());
 
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return st.toString();
    }
 
    @Override
    public void init(EndpointConfig endpointConfig) {
        // do nothing.
    }
 
    @Override
    public void destroy() {
        // do nothing.
    }
}

为了测试这个例子,将客户端的网页稍微修改一下以便能在textarea中粘帖XML:

<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
    </head>
 
    <body>
        <meta charset="utf-8">
        <title>HelloWorld Web sockets</title>
        <script language="javascript" type="text/javascript">
            var wsUri = getRootUri() + "/websocket-hello/hello";
 
            function getRootUri() {
                return "ws://" + (document.location.hostname == "" ? "localhost" : document.location.hostname) + ":" +
                        (document.location.port == "" ? "8080" : document.location.port);
            }
 
            function init() {
                output = document.getElementById("output");
            }
 
            function send_message() {
 
                websocket = new WebSocket(wsUri);
                websocket.onopen = function(evt) {
                    onOpen(evt)
                };
                websocket.onmessage = function(evt) {
                    onMessage(evt)
                };
                websocket.onerror = function(evt) {
                    onError(evt)
                };
 
            }
 
            function onOpen(evt) {
                writeToScreen("Connected to Endpoint!");
                doSend(textID.value);
 
            }
 
            function onMessage(evt) {
                writeToScreen("Message Received: " + evt.data);
            }
 
            function onError(evt) {
                writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
            }
 
            function doSend(message) {
                writeToScreen("Message Sent: " + message);
                websocket.send(message);
            }
 
            function writeToScreen(message) {
                alert(message);
                
            }
 
            window.addEventListener("load", init, false);
 
        </script>
 
        <h1 style="text-align: center;">Hello World WebSocket Client</h2>
 
        <br>
 
        <div style="text-align: center;">
            <form action="">
                <input onclick="send_message()" value="Send" type="button">
                <textarea id="textID" rows="4" cols="50" name="message" >
                </textarea>
            </form>
        </div>
        <div id="output"></div>
</body>
</html>

在文本框中输入下面的XML进行测试。

<person>
  <name>john</name>
  <surname>smith</surname>
</person>

翻译自 masterthebossWebSockets tutorial on Wildfly 8,同时在翻译的过程中增加了很多的背景知识。

WebSocket 是web客户端和服务器之间新的通讯方式, 依然架构在HTTP协议之上。使用WebSocket连接, web应用程序可以执行实时的交互, 而不是以前的poll方式。

WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议,可以用来创建快速的更大规模的健壮的高性能实时的web应用程序。WebSocket通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。
在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

作者:鸟窝
大道至简 衍化至繁
原文地址:Java WebSocket教程, 感谢原作者分享。

评论

  • http://www.blue-zero.com/WebSocket/ 在线WebSocket测试工具,完全兼容IE6及以上版本,超过128字节正常收发。

    回复   |   煌-_- 发表于 2016-03-16 15:34:52
  • 这个技术怎么实现一对多的聊天功能,能详细的讲一下吗 ?

    回复   |   丶Mr. 发表于 2016-04-15 12:21:53
  • 你好,请问一下为什么访问的时候,会报405错呢?

    回复   |   我的世界 发表于 2017-05-19 16:18:14

发表评论