使用JSR 356 进行java websocket编程

 java  使用JSR 356 进行java websocket编程已关闭评论
1月 032017
 

这是一篇翻译的文章, 版本虽不是最新,但有参考价值。

转自:http://www.oschina.net/translate/how-to-build-java-websocket-applications-using-the-jsr-356-api

大家都知道这样一个事实,那就是HTTP(Hypertext Transfer Protocol)是一个无状态的请求-响应式协议。HTTP协议的这种简单设计使它颇具扩展性却不够高效,并且不适合于频繁交互的实时网络应用。HTTP被设计用来进行文档共享而不是用来建立频繁交互的网络应用。HTTP天生就不太正规,对每一个http请求/响应,都要通过线路传输许多头信息。

在HTTP 1.1版本之前,每一个提交到服务器的请求都会创建一个新的链接。这种情况在HTTP 1.1中通过引入HTTP持久化连接得以改进。持久化连接允许web浏览器复用同样的连接来获取图片,脚本等等。

HTTP被设计成半双工的,这意味着同一时刻只允许向一个方向上传输数据。Walkie-talkie是一个半双工设施的例子,因为一个时刻只能有一个人说话。开发者们已经创造出了一些工作方法或者应对方法来克服HTTP的这个缺点。这些工作方法包括轮询,长效轮询和

什么是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,如下图所示。该信息来自于http://caniuse.com/#feat=websockets.

WebSocket browser support

WebSocket是如何工作的?

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

WebSockets带来了性能,简单化和更少带宽消耗

  1. WebSockets比其它工作方式比如轮询更有效也更高效。因为它需要更少的带宽并且降低了延时。
  2. WebSockets简化了实时应用的结构体系。
  3. WebSockets在点到点发送消息时不需要头信息。这显著的降低了带宽。

WebSocket使用案例

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

  • 聊天应用
  • 多人游戏
  • 股票交易和金融应用
  • 文档合作编辑
  • 社交应用

Java中使用WebSockets

在Java社区中下面的情形很普遍,不同的供应商和开发者编写类库来使用某项技术,一段时间之后当该技术成熟时它就会被标准化,来使开发者可以在不同实现之间互相操作,而不用冒供应商锁定的风险。当JSR 365启动时,WebSocket就已经有了超过20个不同的Java实现。它们中的大多数都有着不同的API。JSR 356是把Java的WebSocket API进行标准化的成果。开发者们可以撇开具体的实现,直接使用JSR 356 API来创建WebSocket应用。WebSocket API是完全由事件驱动的。

JSR 356 — WebSockets的Java API

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支持。

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

Tyrus — JSR 356 参考实现

TyrusJSR 356的参考实现。我们会在下一节中以独立模式用Tyrus开发一个简单应用。所有Tyrus组件都是用Java SE 7编译器进行构建的。这意味着,你也至少需要不低于Java SE 7的运行环境才能编译和运行该应用示例。它不能够在Apache Tomcat 7中运行,因为它依赖于servlet 3.1规范。

使用WebSockets开发一个单词游戏

现在我们准备创建一个非常简单的单词游戏。游戏者会得到一个字母排序错乱的单词,他或她需要把这个单词恢复原样。我们将为每一次游戏使用一个单独的连接。

本应用的源代码可以从github获取 https://github.com/shekhargulati/wordgame

步骤 1 : 创建一个模板Maven项目

开始时,我们使用Maven原型来创建一个模板Java项目。使用下面的命令来创建一个基于Maven的Java项目。

$ mvn archetype:generate -DgroupId=com.shekhar -DartifactId=wordgame -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

步骤 2 : 向pom.xml中添加需要的依赖

正如上节中提到的,你需要Java SE 7来构建使用Tyrus的应用。要在你的maven项目中使用Java 7,你需要在配置中添加maven编译器插件来使用Java 7,如下所示。

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <configuration>
                <compilerVersion>1.7</compilerVersion>
                <source>1.7</source>
                <target>1.7</target>
            </configuration>
        </plugin>
    </plugins>
</build>

下面,添加对JSR 356 API的依赖。javax.websocket-api的当前版本是 1.0。

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

下面我们将要添加与Tyrus JSR 356实现相关的依赖。tyrus-server包提供了JSR 356服务端WebSocket API实现,tyrus-client包提供了JSR356客户端WebSocket API实现。

<dependency>
    <groupId>org.glassfish.tyrus</groupId>
    <artifactId>tyrus-server</artifactId>
    <version>1.1</version>
</dependency>
<dependency>
    <groupId>org.glassfish.tyrus</groupId>
    <artifactId>tyrus-client</artifactId>
    <version>1.1</version>
</dependency>

最后,我们添加tyrus-container-grizzly依赖到我们的pom.xml中。这将提供一个独立的容器来部署WebSocket应用。
<dependency>
    <groupId>org.glassfish.tyrus</groupId>
    <artifactId>tyrus-container-grizzly</artifactId>
    <version>1.1</version>
</dependency>  

你可以在这里查看完整的pom.xml文件。

步骤 3 : 编写第一个JSR 356 WebSocket服务器终端

现在我们的项目已经设置完毕,我们将开始编写WebSocket服务器终端。你可以通过使用@ServerEndpoint注解来把任何Java POJO类声明为WebSocket服务器终端。开发者也可以指定用来部署终端的URI。URI要相对于WebSocket容器的根路径,必须以”/”开头。在如下所示的代码中,我们创建了一个非常简单的WordgameServerEndpoint。

package com.shekhar.wordgame.server;
  import java.io.IOException; import java.util.logging.Logger;
  import javax.websocket.CloseReason; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.CloseReason.CloseCodes; import javax.websocket.server.ServerEndpoint;
  @ServerEndpoint(value = "/game") public class WordgameServerEndpoint {
  private Logger logger = Logger.getLogger(this.getClass().getName());
  @OnOpen public void onOpen(Session session) {
        logger.info("Connected ... " + session.getId());
    }
  @OnMessage public String onMessage(String message, Session session) { switch (message) { case "quit": try {
                session.close(new CloseReason(CloseCodes.NORMAL_CLOSURE, "Game ended"));
            } catch (IOException e) { throw new RuntimeException(e);
            } break;
        } return message;
    }
  @OnClose public void onClose(Session session, CloseReason closeReason) {
        logger.info(String.format("Session %s closed because of %s", session.getId(), closeReason));
    }
}

@OnOpen注解用来标注一个方法,在WebSocket连接被打开时它会被调用。每一个连接都有一个和它关联的session。在上面的代码中,当onOpen()方法被调用时我们打印了一下session的id。对每一个WebSocket连接来说,被@OnOpen标注的方法只会被调用一次。

@OnMessage注解用来标注一个方法,每当收到一个消息时它都会被调用。所有业务代码都需要写入该方法内。上面的代码中,当从客户端收到”quit”消息时我们会关闭连接,其它情况下我们只是把消息原封不动的返回给客户端。所以,在收到“quit”消息以前,一个WebSocket连接将会一直打开。当收到退出消息时,我们在session对象上调用了关闭方法,告诉它session的原因。在示例代码中,我们说当游戏结束时这是一个正常的关闭。

@OnClose注解用来标注一个方法,当WebSocket连接关闭时它会被调用。

步骤 4 : 编写第一个JSR 356 WebSocket客户端终端

@ClientEndpoint注解用来标记一个POJO WebSocket客户端。类似于javax.websocket.server.ServerEndpoint,通过@ClientEndpoint标注的POJO能够使它的那些使用了网络套接字方法级别注解的方法,成为网络套接字生命周期方法。

package com.shekhar.wordgame.client;
  import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; import java.util.concurrent.CountDownLatch; import java.util.logging.Logger;
  import javax.websocket.ClientEndpoint; import javax.websocket.CloseReason; import javax.websocket.DeploymentException; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session;
  import org.glassfish.tyrus.client.ClientManager;
  @ClientEndpoint public class WordgameClientEndpoint {
  private Logger logger = Logger.getLogger(this.getClass().getName());
  @OnOpen public void onOpen(Session session) {
        logger.info("Connected ... " + session.getId()); try {
            session.getBasicRemote().sendText("start");
        } catch (IOException e) { throw new RuntimeException(e);
        }
    }
  @OnMessage public String onMessage(String message, Session session) {
        BufferedReader bufferRead = new BufferedReader(new InputStreamReader(System.in)); try {
            logger.info("Received ...." + message);
            String userInput = bufferRead.readLine(); return userInput;
        } catch (IOException e) { throw new RuntimeException(e);
        }
    }
  @OnClose public void onClose(Session session, CloseReason closeReason) {
        logger.info(String.format("Session %s close because of %s", session.getId(), closeReason));
    }
 
 
}

在上面的代码中,当WebSocket 连接被打开时,我们发送了一个“start”消息给服务器。每当从服务器收到一个消息时,被@OnMessage注解标注的onMessage方法就会被调用。它首先记录下消息让后等待用户的输入。用户的输入随后会被发送给服务器。最后,当WebSocket 连接关闭时,@OnClose标注的onClose()方法被被调用。正如你所看到的,客户单和服务器端的代码编程模式是相同的。这使得通过JSR 356 API来编写WebSocket应用的开发工作变得很容易。

********************************************************************************************************************************************************

最后附上oracle官网关于JSR 356 websocket api 如何整合进应用说明, http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html:

JSR 356, Java API for WebSocket

by Johan Vos

Learn how to integrate WebSockets into your applications.

Published April 2013

For many Web-based client-server applications, the old HTTP request-response model has its limitations. Information has to be transmitted from the server to the client in between requests, rather than upon request only.

A number of “hacks” have been used in the past to circumvent this problem, for example, long polling and Comet. However, the need for a standards-based, bidirectional and full-duplex channel between clients and a server has only increased.

In 2011, the IETF standardized the WebSocket protocol as RFC 6455. Since then, the majority of the Web browsers are implementing client APIs that support the WebSocket protocol. Also, a number of Java libraries have been developed that implement the WebSocket protocol.

The WebSocket protocol leverages the HTTP upgrade technology to upgrade an HTTP connection to a WebSocket. Once it is upgraded, the connection is capable of sending messages (data frames) in both directions, independent of each other (full duplex). No headers or cookies are required, which considerably lowers the required bandwidth. Typically, WebSockets are used to periodically send small messages (for example, a few bytes). Additional headers would often make the overhead larger than the payload.

JSR 356

JSR 356, Java API for WebSocket, specifies the API that Java developers can use when they want to integrate WebSockets into their applications—both on the server side as well as on the Java client side. Every implementation of the WebSocket protocol that claims to be compliant with JSR 356 must implement this API. As a consequence, developers can write their WebSocket-based applications independent of the underlying WebSocket implementation. This is a huge benefit, because it prevents a vendor-lock and allows for more choices and freedom of libraries and application servers.

JSR 356 is a part of the upcoming Java EE 7 standard; hence, all Java EE 7–compliant application servers will have an implementation of the WebSocket protocol that adheres to the JSR 356 standard. Once they are established, WebSocket client and server peers are symmetrical. The difference between a client API and a server API is, therefore, minimal. JSR 356 defines a Java client API as well, which is a subset of the full API required in Java EE 7.

A client-server application leveraging WebSockets typically contains a server component and one or more client components, as shown in Figure 1:

Figure 1

Figure 1

In this example, the server application is written in Java, and the WebSocket protocol details are handled by the JSR 356 implementation contained in the Java EE 7 container.

A JavaFX client can rely on any JSR 356–compliant client implementation for handling the WebSocket-specific protocol issues. Other clients (for example, an iOS client and an HTML5 client) can use other (non-Java) implementations that are compliant with RFC 6455 in order to communicate with the server application.

Programming Model

The Expert Group that defined JSR 356 wanted to support patterns and techniques that are common to Java EE developers. As a consequence, JSR 356 leverages annotations and injection.

In general, two different programming models are supported:

  • Annotation-driven. Using annotated POJOs, developers can interact with the WebSocket lifecycle events.
  • Interface-driven. Developers can implement the Endpoint interface and the methods that interact with the lifecycle events.

Lifecycle Events

The typical lifecycle event of a WebSocket interaction goes as follows:

  • One peer (a client) initiates the connection by sending an HTTP handshake request.
  • The other peer (the server) replies with a handshake response.
  • The connection is established. From now on, the connection is completely symmetrical.
  • Both peers send and receive messages.
  • One of the peers closes the connection.

Most of the WebSocket lifecycle events can be mapped to Java methods, both in the annotation-driven and interface-driven approaches.

Annotation-Driven Approach

An endpoint that is accepting incoming WebSocket requests can be a POJO annotated with the @ServerEndpoint annotation. This annotation tells the container that the given class should be considered to be a WebSocket endpoint. The required value element specifies the path of the WebSocket endpoint.

Consider the following code snippet:

@ServerEndpoint("/hello") 
public class MyEndpoint { }

This code will publish an endpoint at the relative path hello. The path can include path parameters that are used in subsequent method calls; for example, /hello/{userid} is a valid path, where the value of {userid} can be obtained in lifecycle method calls using the @PathParam annotation.

In GlassFish, if your application is deployed with the contextroot mycontextroot in a Web container listening at port 8080 of localhost, the WebSocket will be accessible using ws://localhost:8080/mycontextroot/hello.

An endpoint that should initiate a WebSocket connection can be a POJO annotated with the @ClientEndpoint annotation. The main difference between @ClientEndpoint and a ServerEndpoint is that the ClientEndpoint does not accept a path value element, because it is not listening to incoming requests.

@ClientEndpoint 
public class MyClientEndpoint {}

Initiating a WebSocket connection in Java leveraging the annotation-driven POJO approach can be done as follows:

javax.websocket.WebSocketContainer container = 
javax.websocket.ContainerProvider.getWebSocketContainer();

container.conntectToServer(MyClientEndpoint.class, 
new URI("ws://localhost:8080/tictactoeserver/endpoint"));

Hereafter, classes annotated with @ServerEndpoint or @ClientEndpoint will be called annotated endpoints.

Once a WebSocket connection has been established, a Session is created and the method annotated with @OnOpen on the annotated endpoint will be called. This method can contain a number of parameters:

  • A javax.websocket.Session parameter, specifying the created Session
  • An EndpointConfig instance containing information about the endpoint configuration
  • Zero or more string parameters annotated with @PathParam, referring to path parameters on the endpoint path

The following method implementation will print the identifier of the session when a WebSocket is “opened”:

@OnOpen
public void myOnOpen (Session session) {
   System.out.println ("WebSocket opened: "+session.getId());
}

A Session instance is valid as long as the WebSocket is not closed. The Session class contains a number of interesting methods that allow developers to obtain more information about the connection. Also, the Session contains a hook to application-specific data, by means of the getUserProperties() method returning a Map<String, Object>. This allows developers to populate Session instances with session- and application-specific information that should be shared among method invocations.

When the WebSocket endpoint receives a message, the method annotated with @OnMessage will be called. A method annotated with @OnMessage can contain the following parameters:

  • The javax.websocket.Session parameter.
  • Zero or more string parameters annotated with @PathParam, referring to path parameters on the endpoint path.
  • The message itself. See below for an overview of possible message types.

When a text message has been sent by the other peer, the content of the message will be printed by the following code snippet:

@OnMessage
public void myOnMessage (String txt) {
   System.out.println ("WebSocket received message: "+txt);
} 

If the return type of the method annotated with @OnMessage is not void, the WebSocket implementation will send the return value to the other peer. The following code snippet returns the received text message in capitals back to the sender:

@OnMessage
public String myOnMessage (String txt) {
   return txt.toUpperCase();
} 

Another way of sending messages over a WebSocket connection is shown below:

RemoteEndpoint.Basic other = session.getBasicRemote();
other.sendText ("Hello, world");

In this approach, we start from the Session object, which can be obtained from the lifecycle callback methods (for example, the method annotated with @OnOpen). The getBasicRemote() method on the Session instance returns a representation of the other part of the WebSocket, the RemoteEndpoint. That RemoteEndpoint instance can be used for sending text or other types of messages, as described below.

When the WebSocket connection is closing, the method annotated with @OnClose is called. This method can take the following parameters:

  • The javax.websocket.Session parameter. Note that this parameter cannot be used once the WebSocket is really closed, which happens after the @OnClose annotated method returns.
  • A javax.websocket.CloseReason parameter describing the reason for closing the WebSocket, for example, normal closure, protocol error, overloaded service, and so on.
  • Zero or more string parameters annotated with @PathParam, referring to path parameters on the endpoint path.

The following code snippet will print the reason why a WebSocket is closing:

@OnClose
public void myOnClose (CloseReason reason) {
   System.out.prinlnt ("Closing a WebSocket due to "+reason.getReasonPhrase());
}

To be complete, there is one more lifecycle annotation: in case an error is received, the method annotated with @OnError will be called.

Interface-Driven Approach

The annotation-driven approach allows us to annotate a Java class and methods with lifecycle annotations. Using the interface-driven approach, a developer extends javax.websocket.Endpoint and overrides the onOpen, onClose, and onError methods:

public class myOwnEndpoint extends javax.websocket.Endpoint {
   public void onOpen(Session session, EndpointConfig config) {...}
   public void onClose(Session session, CloseReason closeReason) {...}
   public void onError (Session session, Throwable throwable) {...}
}

In order to intercept messages, a javax.websocket.MessageHandler needs to be registered in the onOpen implementation:

public void onOpen (Session session, EndpointConfig config) {
   session.addMessageHandler (new MessageHandler() {...});
}

MessageHandler is an interface with two subinterfaces: MessageHandler.Partial and MessageHandler.Whole. The MessageHandler.Partial interface should be used when the developer wants to be notified about partial deliveries of messages, and an implementation of MessageHandler.Whole should be used for notification about the arrival of a complete message.

The following code snippet listens to incoming text messages and sends the uppercase version of the text message back to the other peer:

public void onOpen (Session session, EndpointConfig config) {
   final RemoteEndpoint.Basic remote = session.getBasicRemote();
   session.addMessageHandler (new MessageHandler.Whole<String>() {
      public void onMessage(String text) {
                 try {
                     remote.sendString(text.toUpperCase());
                 } catch (IOException ioe) {
                     // handle send failure here
                 }
             }

   });
}

Message Types, Encoders, and Decoders

The Java API for WebSocket is very powerful, because it allows any Java object to be sent or received as a WebSocket message.

Basically, there are three different types of messages:

  • Text-based messages
  • Binary messages
  • Pong messages, which are about the WebSocket connection itself

When using the interface-driven model, each session can register at most one MessageHandler for each of these three different types of messages.

When using the annotation-driven model, for each different type of message, one @onMessage annotated method is allowed. The allowed parameters for specifying the message content in the annotated methods are dependent on the type of the message.

The Javadoc for the @OnMessage annotation clearly specifies the allowed message parameters based on the message type (the following is quoted from the Javadoc):

  • “if the method is handling text messages: 

    • String to receive the whole message
    • Java primitive or class equivalent to receive the whole message converted to that type
    • String and boolean pair to receive the message in parts
    • Reader to receive the whole message as a blocking stream
    • any object parameter for which the endpoint has a text decoder (Decoder.Text or Decoder.TextStream).
  • if the method is handling binary messages: 

  • if the method is handling pong messages: 

Any Java object can be encoded into a text-based or binary message using an encoder. This text-based or binary message is transmitted to the other peer, where it can be decoded into a Java object again—or it can be interpreted by another WebSocket library. Often, XML or JSON is used for the transmission of WebSocket messages, and the encoding/decoding then comes down to marshaling a Java object into XML or JSON and back.

An encoder is defined as an implementation of the javax.websocket.Encoder interface, and a decoder is an implementation of the javax.websocket.Decoder interface. Somehow, the endpoint instances need to know what the possible encoders and decoders are. Using the annotation-driven approach, a list of encoders and decoders is passed via the encoder and decoder elements in the @ClientEndpoint and @ServerEndpoint annotations.

The code in Listing 1 shows how to register a MessageEncoder class that defines the conversion of an instance of MyJavaObject to a text message. A MessageDecoder class is registered for the opposite conversion.

@ServerEndpoint(value="/endpoint", encoders = MessageEncoder.class, decoders= MessageDecoder.class)
public class MyEndpoint {
...
}

class MessageEncoder implements Encoder.Text<MyJavaObject> {
   @override
   public String encode(MyJavaObject obj) throws EncodingException {
      ...
   }
}

class MessageDecoder implements Decoder.Text<MyJavaObject> {
   @override 
   public MyJavaObject decode (String src) throws DecodeException {
      ...
   }

   @override 
   public boolean willDecode (String src) {
      // return true if we want to decode this String into a MyJavaObject instance
   }
}

Listing 1

The Encoder interface has a number of subinterfaces:

  • Encoder.Text for converting Java objects into text messages
  • Encoder.TextStream for adding Java objects to a character stream
  • Encoder.Binary for converting Java objects into binary messages
  • Encoder.BinaryStream for adding Java objects to a binary stream

Similarly, the Decoder interface has four subinterfaces:

  • Decoder.Text for converting a text message into a Java object
  • Decoder.TextStream for reading a Java object from a character stream
  • Decoder.Binary for converting a binary message into a Java object
  • Decoder.BinaryStream for reading a Java object from a binary stream

Conclusion

The Java API for WebSocket provides Java developers with a standard API to integrate with the IETF WebSocket standard. By doing so, Web clients or native clients leveraging any WebSocket implementation can easily communicate with a Java back end.

The Java API is highly configurable and flexible, and it allows Java developers to use their preferred patterns.

10月 212015
 

抛砖引玉!!!,学习下相关知识。 (转自:https://imququ.com/post/html5-live-player-3.html)

连续写了两篇有关视频直播的文章之后,有同学问我为什么没有 WebRTC 相关内容。实际上一开始我就说过,我的需求是在移动 WEB 端上直播视频,而移动端浏览器现阶段对「WebRTC 的支持度」非常不乐观,所以我就直接无视它了。但我一时为了标题美观,活生生地把「移动 WEB 端」写成了「HTML5」,所以为了严谨我还是补上这一篇吧。

WebRTC(Web Real-Time Communication),中文一般翻译为「Web 实时通信」。它由一组标准、协议和 JavaScript API 组成,用于实现端到端的音视频及数据共享。与其他浏览器通信机制不同,WebRTC 通过 UDP 传输数据,而我们早已熟知的 XMLHttpRequest、WebSocket 都基于 TCP。

纵观整个浏览器市场,其实只有 Google 和 Mozilla 两家公司对 WebRTC 比较上心,Firefox 22 和 Chrome 23 就开始就支持了它;Microsoft 是搞了自己的一套标准,后续可能会跟 WebRTC 融合,但至少 IE 不会支持现阶段的 WebRTC 标准;Apple 也许是因为有 FaceTime 可以很好地实现 Apple 设备间的多媒体通讯,压根就没打算在 Safari 中增加对 WebRTC 的支持;至于 Opera,换内核后基本等同于 Chrome,这下更要被人无视了。

初识 WebRTC

WebRTC 涉及到很多复杂技术,不过好在浏览器已经把大多数复杂工作抽象成为下面三个 API:

  • MediaStream:获取音频和视频流;
  • RTCPeerConnection:音频和视频数据通信;
  • RTCDataChannel:任意应用数据通信;

MediaStream 对应的是 JS 里的 navigator.getUserMedia() 方法,它负责从底层平台获取音视频流。音视频流经过 WebRTC 音视频引擎的自动优化、编码和解码,就可以直接用或传输到各种目的地用。这里有个 Demo,就是用 getUserMedia 获取视频流,再把每一帧都转成 ASCII 字符播放。总之 MediaStream API 设计得很简单,使用起来也很方便。

RTCPeerConnection 用来建立和维护端到端连接,并提供高效的音视频流传输。整个 WebRTC 提供的 API 中,要数这个最复杂:

首先,要建立端到端连接,不可避免要解决 NAT 穿透问题,RTCPeerConnection 为此引入了 ICE(Interactive Connectivity Establishment)框架。ICE 致力于在端之间建立一条有效的通道,优先直连,其次用 STUN 协商,再不行只能用 TURN 转发。

STUN(Session Traversal Utilities for NAT)协议,解决了三个问题:1)获得外网 IP 和端口;2)在 NAT 中建立路由条目,绑定外网端口,使得到达外网 IP 和端口的入站分组能找到应用程序,不被丢弃;3)定义了一个简单的 keep-alive 机制,保证 NAT 路由条目不会因为超时而被删除。STUN 服务器必须架设在公网上,可以自己搭建,也可以使用第三方提供的公开服务,例如 Google 的「stun:stun.l.google.com:19302」。

TURN(Traversal Using Relays around NAT)协议,依赖外网中继设备在两端之间传递数据。简单说就是通过两端都可以访问的 TURN 服务转发消息,间接把两端连起来。TURN 还会尝试使用 TCP 建立,而不仅仅是 UDP,可靠性大大增强,带宽成本也随着大幅提升。根据 Google 的统计,UDP 服务中,有 8% 左右的情况下需要 TURN。

其次,要建立端到端的信道,还是需要借助服务端来交换和协商一些信息,这个过程被称之为 Signaling。WebRTC 并没有规则 Signaling 必须使用某种协议,而把选择权交给了应用程序。我们可以选用不同方式(XMLHttpRequest、WebSocket),采用已有的 SIP、Jingle、ISUP 等发信协议,来建立信道。

通常,在 WebRTC 应用中,建立信道这一步都是优先走 WebSocket,并支持降级为 HTTP。一来支持 WebRTC 的浏览器肯定都支持 WebSocket;二来 WebSocket 实时性更好一些。特别需要注意的是,WebSocket 只用来辅助建立端到端连接,一旦连接建立,信源在端到端之间的传输就完全不需要服务端了(当然 TURN 这种中继模式就另当别论)。

RTCDataChannel 用来支持端到端的任意应用数据交换。建立 RTCPeerConnection 连接之后,除了可以传输音视频流,还可以打开一个或多个信道用来传输任何文本或二进制内容,这就是 RTCDataChanel。DataChannel API 在使用上跟 WebSocket 非常类似,功能上都可以用来在端到端之间传输数据,但是本质上他们还是有区别的:

首先,WebRTC 端与端之间是对等的,DataChannel 可以由任何一方发起;这与 WebSocket 连接只能由客户端发起不同; 其次,WebSocket 的会话层协议 TLS 是可选的;而 WebRTC 的会话层协议 DTLS 是必须的,这表明通过 WebRTC 传输的数据一定会被加密; 再者,WebSocket 运行在 TCP 之上,每条消息天然有序并可靠;而 DataChannel 可以通过 SCTP 的交付属性选项来指定消息是有序还是乱序,是可靠还是部分可靠,部分可靠时还可以指定使用超时重传还是计数重传策略。

现阶段 DataChannel 运行在下列协议之上:

  • SCTP(Stream Control Transmission Protocol),流控制传输协议,提供了一些与 TCP 类似的特性;
  • DTLS(Datagram Transport Layer Security),传输内容加密,UDP 版的 TLS;
  • UDP(User Datagram Protocol),用户数据报协议,整个 WebRTC 的基础;

这里我并不打算完整地介绍如何从零开始使用 WebRTC,类似的文章网上大把。这里推荐几篇文章,后几篇中文的出自同一个作者,写得比较通俗易懂:

另外还推荐《Web 性能权威指南》这本书,它的第 3 章「UDP 的构成」和第 18 章「WebRTC」对 UDP 内网穿透和 WebRTC 有比较详细的介绍。

一对多直播

前面说过,WebRTC 是用来解决端到端的实时通信问题,也就是说它很适合用在网络电话这种需要双向视频通话的场景上。网上大部分 WebRTC 的 Demo 也都是在页面上放两个 Video,分别来播 localStream 和 RemoteStream。那么究竟 WebRTC 能否用来实现单向一对多直播呢?当然可以,而且貌似还很简单:

  • 首先必须有一个专门负责调用 getUserMedia 采集音视频的页面,我称之为信源服务;
  • 打开直播页面时,建立到信源服务的 PeerConnection,并通过 DataChannel 通知信源服务;
  • 信源服务收到通知后,通过对应 PeerConnection 的 addStream 方法提供直播流;
  • 直播页面监听 PeerConnection 的 onaddstream 事件,将获得的直播流用丢给 Video 播放;

为了方便,我使用了 PeerJS 这个开源项目来验证上面这个过程。PeerJS 对 WebRTC Api 进行了封装,使用更简单。它还提供了用来辅助建立连接的 Signaling 服务,在官网注册一个 Api Key 就能用。也可以通过 PeerJS Server 搭建自己的服务,只需要通过 npm install peer 装好 peer 后,再通过下面这行命令启动就可以了:

peerjs --port 9000 --key peerjs

启动好 Peer Server,在页面中引入 peer.js 就可以开始玩了。首先实现信源服务:

JS//由于其它端都要连它,指定一个固定的 ID var peer = new Peer('Server', {
    host: 'qgy18.imququ.com',
    port: 9003,
    path: '/',
    config: { 'iceServers': [
              { url: 'stun:stun.l.google.com:19302' }
        ]
    }
});

navigator.getUserMedia({ audio: false, video: true }, function(stream) { window.stream = stream;
}, function() { /*...*/ });

peer.on('connection', function(conn) {
    conn.on('data', function(clientId){ var call = peer.call(clientId, window.stream);

        call.on('close', function() { /*...*/ });
    });
});

然后就是直播服务:

JS//随机生成一个 ID var clientId = (+new Date).toString(36) + '_' + (Math.random().toString()).split('.')[1]; var peer = new Peer(clientId, {
    host: 'qgy18.imququ.com',
    port: 9003,
    path: '/',
    config: { 'iceServers': [
              { url: 'stun:stun.l.google.com:19302' }
        ]
    }
}); var conn = peer.connect('Server');

conn.on('open', function() {
    conn.send(clientId);
});

peer.on('call', function(call) {
    call.answer();
    call.on('stream', function(remoteStream) { var video = document.getElementById('video');
        video.src = window.URL.createObjectURL(remoteStream);
    });

    call.on('close', function() { /*...*/ });
});

直播页面通过指定 ID 的方式跟信源服务建立端到端连接,然后通过 DataChannel 告诉信源服务自己的 ID,信源服务收到消息后,主动把直播流发过来,直播页面应答后播放就可以了。整个过程原理就这么简单,这里有一个「完整的 Demo」。

看完上面的 Demo,你也许会想原来使用 WebRTC 直播这么简单,随便找台带摄像头的电脑,开个浏览器就能提供直播服务,那还搞 HLS、RTMP 什么的干嘛。

实际上,现实并没有那么美好,这个 Demo 也就玩玩儿还可以,真正使用起来问题还大着呢!

首先,虽然说在 WebRTC 直播方案中,服务端只扮演桥梁的工作,实际数据传输直接发生在端到端之间,但前面说过仍然会有 8% 的情况完全不能直连。要保证服务的高可用性,还是得考虑部署 TURN 这种复杂而昂贵的中转服务。

其次,Chrome 对每个 Tab 允许连接的终端数有限制,最多 256 个。实际上,在我最新的 Retina Macbook Pro 上,差不多有 10 个连接时,Chrome 就开始变得无比卡,风扇呼呼地转,内存被吃掉 6G,CPU 一直跑满,网络吞吐开始忙不过来,直播服务也开始变得极其不稳定。

所以实际使用方案中,一般还是需要 Media Server 的支持,把「端到多端」变成「端到 Media Server 到多端」的架构。Media Server 可以有更好的性能和带宽,可以自己实现 WebRTC 协议,也就有了支持更多用户的可能。

我找到一个名为 Janus 的 WebRTC Gateway,这个开源项目用 C 语言实现了对 WebRTC 的支持。Janus 自身实现得很简单,提供插件机制来支持不同的业务逻辑,配合官方自带插件就可以用来实现高效的 Media Server 服务。

Janus 官方提供的 Demo 在这里,我也尝试在我的 VPS 上部署了一套。Janus 有个 Streaming 插件,可以接受 GStreamer 推送的音视频流,然后通过 PeerConnection 推送给所有的用户。由于 GStreamer 可以直接读摄像头,也就不用再走 WebRTC 的 MediaStream 获取视频,这样架构就变成了传统的服务器到端了。整个过程比较复杂和曲折,这里不写了,有兴趣的同学可以单独找我讨论。

10月 212015
 

抛砖引玉!!!,学习下相关知识。 (转自:https://imququ.com/post/html5-live-player-2.html)

实际上,HLS 除了上回提到过的延迟很大这个缺点之外,在 iOS 的 Safari 浏览器中还只能全屏播放,也无法做到自动播放,这个是 iOS 系统对 Video 标签统一做的限制。有没有什么办法解决这些问题呢?

我们换个思路,既然原生 Video 有这样那样的问题,不如直接抛弃它。利用 Web Sockets 实现视频流的实时传输,使用纯 JS 进行视频解码,再用 Canvas 逐帧画出图像,这不就实现了直播么。当我有个这个想法之后,初步觉得可行,立马开始一番搜索,收获颇丰。

本文要用到的 Web Sockets 在移动端支持度如下表:

浏览器 支持新标准的版本
iOS Safari 6.1+
Android Browser(Webview) 4.4+
Chrome for Android 42+

(数据来源:caniuse

另外,转换视频格式、生成视频流还需要用到一个神器:FFmepg

Mac 下最简单的做法是通过 Homebrew 安装,直接 brew install ffmpeg 就可以了。Ubuntu 下可以先添加这个源:

sudo add-apt-repository ppa:mc3man/trusty-media

 sudo apt-get install ffmpeg,也能轻松搞定。

Decoder in JavaScript

纯 JS 实现的视频解码器我找到了两个可用的:Broadway  jsmpeg

Broadway 是一个 H.264 解码器,使用 Emscripten 工具从 Android 的 H.264 解码器转化而成,它还针对 WebGL 做了一些优化。

这个解码器支持 mp4 后缀的视频文件,有一些限制:不支持 weighted prediction for P-frames 和 CABAC entropy encoding。例如 iPhone 拍摄的视频它就不支持,可以用 FFmpeg 转一下:

ffmpeg -i in.mp4 -vcodec libx264 -pass 1 -coder 0 -bf 0 -flags -loop -wpredp 0 out.mp4

下面是 H.264 解码示例,视频来自于我的 iPhone 拍摄。用阅读器的同学请点到原文查看。

点击播放

这里还有一个长一点的 Demo,点击查看(加载完 6M 多的 mp4 文件才开始播放,请耐心等待,流量党慎入)。

jsmpeg 则是一个 MPEG1 解码器,它是由作者从头编写出来的,并不像 Broadway 那样是从其他语言翻译而成,所以代码可读性要好很多,代码也更轻量级。

jsmpeg 也对视频文件编码方式有一些要求:不支持 B-Frames,视频宽度必须是 2 的倍数。还是可以用 FFmpeg 来转换:

ffmpeg -i in.mp4 -f mpeg1video -vf "crop=iw-mod(iw,2):ih-mod(ih,2)" -b 0 out.mpg

下面是 MPEG1 解码示例,视频来自于网上。用阅读器的同学请点到原文查看。

点击播放

这里也有一个长一点的 Demo,点击查看(加载完 3M 多的 mpg 文件才开始播放。其实没什么好看的,内容跟上面一样,编码格式不同而已)。

Live Streaming

看到这里,大家肯定会说,这不是要一次性下完全部内容么,怎能称之为直播。是的,要实现直播,还要用 Web Sockets 实现一个实时传输流的服务。FFmpeg 支持很多直播流格式,但不支持 Web Sockets。解决方案是用 FFmpeg 开一个 HTTP 直播流,再开个 Node 服务转一下。

详细一点的过程是这样的,用 NodeJS 监听 FFmpeg 的 HTTP 直播地址,把收到的数据 通过 Web Sockets 广播给所有客户端。核心代码就是下面这几行:

JS//HTTP Server to accept incomming MPEG Stream var streamServer = require('http').createServer( function(request, response) {
    request.on('data', function(data){
        socketServer.broadcast(data, {binary:true});
    });
}).listen(STREAM_PORT);

这段代码来自于上面介绍的 jsmpeg 项目,完整代码在这里。启动这个服务试试(需先装好 ws 模块):

node ~/live/stream-server.js ququ 9091 9092 

三个参数分别是加密串、HTTP 端口、WS 端口。启动后,屏幕上会显示两个地址:

Listening for MPEG Stream on http://127.0.0.1:9091/<secret>/<width>/<height>
Awaiting WebSocket connections on ws://127.0.0.1:9092/

好了,现在就可以使用 FFmpeg 来推送 HTTP 视频流了:

ffmpeg -re -i fox.mpg -codec copy -f mpeg1video http://qgy18.imququ.com:9091/ququ/640/360 

-re 参数表示以视频实际播放速率解码,不加这个参数一般会导致直播速率变得飞快。ququ 是启动 Node 服务时指定的加密串,这样做个简单校验,避免 Node 转发不认识的流。最后的 640  360 是视频的宽高,可以根据实际情况指定。

最后,稍微改一下前面的 Demo,让 jsmpeg 从 WS 流中获取数据就可以实现直播了:

var canvas = document.getElementById('videoCanvas');
var client = new WebSocket('ws://qgy18.imququ.com:9092/');
var player = new jsmpeg(client, {canvas:canvas, autoplay: true});

完整示例地址

Capture Webcam

上面演示的是从文件中获取视频流进行直播,如果把数据源换成摄像头也很容易。FFmpeg 官方 wiki 上有在 Windows / MacOS / Linux 下读取摄像头的详细指南。

以 Mac 为例,它支持 AVFoundation 和 QTKit 两种不同的技术读取摄像头,在比较新的系统(10.7+)上,推荐使用 AVFoundation,QTKit 后续可能会被废弃。

我的 Mac 系统是最新的,直接使用 AVFoundation。首先查看可用摄像头列表:

SHELLffmpeg -f avfoundation -list_devices true -i "" [AVFoundation input device @ 0x7fdd53c228c0] AVFoundation video devices:
[AVFoundation input device @ 0x7fdd53c228c0] [0] FaceTime HD Camera
[AVFoundation input device @ 0x7fdd53c228c0] [1] Capture screen 0 [AVFoundation input device @ 0x7fdd53c228c0] AVFoundation audio devices:
[AVFoundation input device @ 0x7fdd53c228c0] [0] Built-in Microphone

可以看到,编号为 0 的 video 设备正是我想要的摄像头(编号为 1 的设备是电脑屏幕,直播屏幕也挺好玩),下面这行命令就可以捕捉它,并把视频流推到之前的 Node 服务上:

ffmpeg -f avfoundation -i "0" -f mpeg1video -b 500k -r 20 -vf scale=640:360 http://qgy18.imququ.com:9091/ququ/640/360

-b  -r 分别用来指定码率和帧率,可以根据实际情况调整。这样,我摄像头拍摄到的画面,就被 FFMpeg 捕捉下来实时推给远端 Node 服务,实时转化成 WS 数据流,广播给所有终端播放。虽然过程很曲折,但是基本上看不到延迟,体验还是很不错的。

最后,说几个问题:

  • 首先,最大的问题是:这种方案只实现了 Canvas 渲染画面部分,无法支持声音(没声音还好意思叫直播,摔!!!);
  • 其次,JS 解码能力还是稍微有点弱,最后的示例中我有意指定 scale 参数缩小了画面,但在 iPhone 6 Plus 非自带浏览器中还会卡(iOS 第三方 APP 赶紧放弃老系统,果断的把好内核用起来吧~~~);

1月 212015
 

虽然是英文的资料,但应该都能看懂, 介绍了使用注解和程序方式编写websock。 对于理解tomcat下websock sample会很有帮助

JSR 356 defines Java API for WebSocket 1.0. It defines a standard-based programming model for creating WebSocket client and server endpoint. Both kind of endpoints can be created programmatically or using annotations. This Tip OfThe Day (TOTD) provide short snippets of how to write a WebSocket client and server endpoint programmatically or using annotations.

The complete source code in this sample can be downloaded from here.

Lets start with annotation-based server endpoint.

@ServerEndpoint(“/websocket”)
public class MyEndpoint {
    
  @OnMessage
  public String echoText(String name) {
    return name;
  }
}

@ServerEndpoint marks the POJO as a WebSocket server endpoint. URI of the deployed endpoint is as valueattribute of the annotation.  echoText method is invoked whenever a message with text payload is received by this endpoint. Payload of the message is mapped to the parameter name. A synchronous response is returned to the client using the return value.

Programmatic server endpoint can be defined as:

public class MyEndpoint extends Endpoint {

  @Override
  public void onOpen(final Session session, EndpointConfig ec) {
    session.addMessageHandler(new MessageHandler.Whole<String>() {

      @Override
      public void onMessage(String text) {
        try {
          session.getBasicRemote().sendText(text);
        } catch (IOException ex) {
          Logger.getLogger(MyEndpoint.class.getName()).log(Level.SEVERE, null, ex);
        }
      }
  });
}

A programmatic server endpoint is defined by extending Endpoint abstract class. onOpen method is overridden to be notified of when a new conversation has started. Session captures the other end of the conversation.EndpointConfig identifies the configuration object used to configure this endpoint. Multiple MessageHandlers are registered to handle text, binary, and pong messages. The first parameter of onOpen captures the other end of the conversation. A synchronous response to the client is sent by calling getBasicRemote().sendText(…) method.

Programmatic server endpoint needs to be configured using ServerApplicationConfig.

public class MyApplicationConfig implements ServerApplicationConfig {

  @Override
  public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> set) {
    return new HashSet<ServerEndpointConfig>() {
      {
        add(ServerEndpointConfig.Builder
            .create(MyEndpoint.class, “/websocket”)
            .build());
      }
    };
  }

  @Override
  public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> set) {
    return Collections.emptySet();
  }
}

WebSocket runtime scans the WAR file with all implementations of ServerApplicationConfig and registers the endpoint returned from getEndpointConfigs and getAnnotatedEndpointClasses. The URI of the server endpoint is published using ServerEndpointConfig.Builder.

Now lets take a look at annotated client endpoint.

@ClientEndpoint
public class MyClient {
  @OnOpen
  public void onOpen(Session session) {
    try {
      String name = “Duke”;
      System.out.println(“Sending message to endpoint: ” + name);
      session.getBasicRemote().sendText(name);
    } catch (IOException ex) {
      Logger.getLogger(MyClient.class.getName()).log(Level.SEVERE, null, ex);
    }
  }
}

@ClientEndpoint marks the POJO as a WebSocket client endpoint. onOpen method is invoked whenever a new WebSocket connection is opened and is identified by @OnOpen annotation. Session captures the other end of the conversation. A synchronous message is sent to the server using session.getBasicRemote.sendText() method.

This client can connect to the endpoint as:

WebSocketContainer container = ContainerProvider.getWebSocketContainer();
String uri = “ws://localhost:8080” + request.getContextPath() + “/websocket”;
container.connectToServer(MyClient.class, URI.create(uri));

And finally programmatic client endpoint.

public class MyClient extends Endpoint {
  @Override
  public void onOpen(final Session session, EndpointConfig ec) {
    session.addMessageHandler(new MessageHandler.Whole<String>() {

      @Override
      public void onMessage(String text) {
        System.out.println(“Received response in client from endpoint: ” + text);
      }
  });
  try {
    String name = “Duke”;
      System.out.println(“Sending message from client -> endpoint: ” + name);
      session.getBasicRemote().sendText(name);
    } catch (IOException ex) {
      Logger.getLogger(MyClient.class.getName()).log(Level.SEVERE, null, ex);
    }
  }
}

The first parameter of onOpen captures the other end of the conversation.  EndpointConfig identifies the configuration object used to configure this endpoint. Multiple MessageHandlers are registered to handle text, binary, and pong messages. onMessage method is called whenever a message is received from the endpoint. A synchronous request to the server is sent by calling getBasicRemote().sendText(…) method.

This client can connect to the endpoint as:

WebSocketContainer container = ContainerProvider.getWebSocketContainer();
String uri = “ws://localhost:8080” + request.getContextPath() + “/websocket”;
container.connectToServer(MyClient.class, 
                null,
                URI.create(uri));

Now go download your GlassFish b82samples source code, and run them.

1月 062015
 
按照OSI网络分层模型,IP是网络层协议,TCP是传输层协议,而HTTP是应用层的协议。在这三者之间,SPDY和WebSocket都是与HTTP相关的协议,而TCP是HTTP底层的协议。
一、HTTP的不足
HTTP协议经过多年的使用,发现了一些不足,主要是性能方面的,包括:
HTTP的连接问题,HTTP客户端和服务器之间的交互是采用请求/应答模式,在客户端请求时,会建立一个HTTP连接,然后发送请求消息,服务端给出应答消息,然后连接就关闭了。(后来的HTTP1.1支持持久连接)
因为TCP连接的建立过程是有开销的,如果使用了SSL/TLS开销就更大。
在浏览器里,一个网页包含许多资源,包括HTML,CSS,JavaScript,图片等等,这样在加载一个网页时要同时打开连接到同一服务器的多个连接。
HTTP消息头问题,现在的客户端会发送大量的HTTP消息头,由于一个网页可能需要50-100个请求,就会有相当大的消息头的数据量。
HTTP通信方式问题,HTTP的请求/应答方式的会话都是客户端发起的,缺乏服务器通知客户端的机制,在需要通知的场景,如聊天室,游戏,客户端应用需要不断地轮询服务器。
而SPDY和WebSocket是从不同的角度来解决这些不足中的一部分。除了这两个技术,还有其他技术也在针对这些不足提出改进。
二、SPDY
SPDY的主要目的是减少50%以上的页面加载时间,但是呢不增加部署的复杂性,不影响客户端和服务端的Web应用,只需要浏览器和Web服务器支持SPDY。主要有以下几点:
多路复用,一个TCP连接上同时跑多个HTTP请求。请求可设定优先级。
去除不需要的HTTP头,压缩HTTP头,以减少需要的网络带宽。
使用了SSL作为传输协议提供数据安全。
对传输的数据使用gzip进行压缩
提供服务方发起通信,并向客户端推送数据的机制。
实质上,SPDY就是想不影响HTTP语义的情况下,替换HTTP底层传输的协议来加快页面加载时间。
SPDY的解决办法就是设计了一个会话层协议--帧协议,解决多路复用,优先级等问题,然后在其上实现了HTTP的语义。
三、WebSocket
WebSocket则提供使用一个TCP连接进行双向通讯的机制,包括网络协议和API,以取代网页和服务器采用HTTP轮询进行双向通讯的机制。
本质上来说,WebSocket是不限于HTTP协议的,但是由于现存大量的HTTP基础设施,代理,过滤,身份认证等等,WebSocket借用HTTP和HTTPS的端口。
由于使用HTTP的端口,因此TCP连接建立后的握手消息是基于HTTP的,由服务器判断这是一个HTTP协议,还是WebSocket协议。 WebSocket连接除了建立和关闭时的握手,数据传输和HTTP没丁点关系了。
WebSocket也有自己一套帧协议。
四、SPDY和WebSocket的关系
SPDY和WebSocket的关系比较复杂。
补充关系,二者侧重点不同。SPDY更侧重于给Web页面的加载提速,而WebSocket更强调为Web应用提供一种双向的通讯机制以及API。
竞争关系,二者解决的问题有交集,比如在服务器推送上SPDY和WebSocket都提供了方案。
承载关系,试想,如果SPDY的标准化早于WebSocket,WebSocket完全可以侧重于API,利用SPDY的帧机制和多路复用机制实现该API。 Google提出草案,说WebSocket可以跑在SPDY之上。WebSocket的连接建立在SPDY的流之上,将WebSocket的帧映射到SPDY的帧上。

融合关系,如微软在HTTP Speed+Mobility中所做的。

转自:http://zhidao.baidu.com/link?url=zo9w-nkuUWu1v9Mt1xNTOcpMNxWIv8i2bwcZmfmBW5hpmEFfnekn3JCkbpZww8J2_701CaEkpwDzbp4F-2qbmm6fFIQG6kG3G9PY3nzE99G

tomcat使用 websocket时maven配置

 MAVEN, tomcat  tomcat使用 websocket时maven配置已关闭评论
12月 312013
 

最近项目需使用到 tomcat websocket,发现配置时总报错,通过引用外部的lib却报错“The hierarchy of the type ‘Class name’ is inconsistent”。 后来查网上资料有解决,记下来供参考:

<dependency>
   <groupId>org.apache.tomcat</groupId>
   <artifactId>tomcat-catalina</artifactId>
   <version>7.0.39</version>
   <scope>provided</scope>
</dependency>
 
  <dependency>
   <groupId>org.apache.tomcat</groupId>
   <artifactId>tomcat-coyote</artifactId>
   <version>7.0.39</version>
   <scope>provided</scope>
</dependency>

8月 162013
 

转自:http://express.ruanko.com/ruanko-express_56/tech-overnight2.html

本文演示如何使用Eclipse和Tomcat开发一个简单的WebSocket聊天室程序。

一、开发环境:

  1. JDK1.6
  2. Eclipse Java EE IDE for Web Developers,本文使用的是:Indigo Release。
  3. Tomcat 7.0.27开始支持WebSocket服务,本文使用的是:Tomcat 7.0.34。

二、配置Eclipse中的Tomcat7服务器运行环境

如果在此之前已经进行过服务器的配置,则可以跳过此步骤。

1.在菜单中选择:Window→Preferences→Server→Runtime Environment。

2.点击Add新增Tomcat7运行环境

3.选择Tomcat7的安装路径

4.新建一个Tomcat7服务器

在菜单中选择New→Other→Server→Server,在新建服务器窗口中填写服务器名称,并指定服务器运行环境。

三、创建Web项目

1.在菜单中选择File→New→Project在新建项目向导中选择Dynamic Web Project。

2.输入项目名称,例如:webchat,在Target runtime中选择之前所配置的Tomcat7运行环境。该步骤之后的源码路径设置和web模块设置都可以使用默认设置,直接点击“Finish”即可。

四、编写项目代码

项目创建成功后,首先生成servlet类,具体操作方法如下:

1.在项目上点击右键,在快捷菜单中选择New→Servlet。

2.填写servlet的所在的包和类名,需要注意的是,在Supperclass中需要选择org.apache.catalina.websocket.WebSocketServlet作为servlet的父类。

3.点击“Next”之后,Servlet创建向导会提示该Servlet中需要包含哪些方法,这里可以去掉默认方法的复选框,在WebSocketServlet中不需要实现doGet和doPost方法。

WebSocketServlet代码清单:


package websocket.chat;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;

import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.apache.catalina.websocket.WsOutbound;

@WebServlet("/ChatWebSocketServlet")
public class ChatWebSocketServlet extends WebSocketServlet {
    private static final long serialVersionUID = 1L;
    private static final String GUEST_PREFIX = "Guest";
    //用于自动生成连接ID
    private final AtomicInteger connectionIds = new AtomicInteger(0);
    //用于保存所有连接Inbound的集合
    private final Set<ChatMessageInbound> connections =
            new CopyOnWriteArraySet<ChatMessageInbound>();
    //生成新的连接Inbound,WebSocketServlet必须实现该方法
    @Override
    protected StreamInbound createWebSocketInbound(String subProtocol,
            HttpServletRequest request) {
        return new ChatMessageInbound(connectionIds.incrementAndGet());
    }
    //聊天消息处理类
    private final class ChatMessageInbound extends MessageInbound {
        private final String nickname;
        private ChatMessageInbound(int id) {
            this.nickname = GUEST_PREFIX + id;
        }
        //创建新的连接时触发
        @Override
        protected void onOpen(WsOutbound outbound) {
            connections.add(this);
            String message = String.format("* %s %s",
                    nickname, "has joined.");
            broadcast(message);
        }
        //关闭连接时触发
        @Override
        protected void onClose(int status) {
            connections.remove(this);
            String message = String.format("* %s %s",
                    nickname, "has disconnected.");
            broadcast(message);
        }
        //发送二进制消息时触发
        @Override
        protected void onBinaryMessage(ByteBuffer message) throws IOException {
            throw new UnsupportedOperationException(
                    "Binary message not supported.");
        }
        //发送文本消息时触发
        @Override
        protected void onTextMessage(CharBuffer message) throws IOException {
            // Never trust the client
            String filteredMessage = String.format("%s: %s",
                    nickname, message.toString());
            broadcast(filteredMessage);
        }
        //该方法向所有活动连接发送文本信息
        private void broadcast(String message) {
            for (ChatMessageInbound connection : connections) {
                try {
                    CharBuffer buffer = CharBuffer.wrap(message);
                    connection.getWsOutbound().writeTextMessage(buffer);
                } catch (IOException ignore) {
                }
            }
        }
    }
}

在WebContent中新建一个html页面,如:chat.html,页面代码清单:


<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
    <style type="text/css">
        input#chat {
            width: 410px
        }
        #console-container {
            width: 400px;
        }
        #console {
            border: 1px solid #CCCCCC;
            border-right-color: #999999;
            border-bottom-color: #999999;
            height: 170px;
            overflow-y: scroll;
            padding: 5px;
            width: 100%;
        }
        #console p {
            padding: 0;
            margin: 0;
        }
    </style>
    <script type="text/javascript">
        var Chat = {};
        Chat.socket = null;
        Chat.connect = (function(host) {
            if ('WebSocket' in window) {
                Chat.socket = new WebSocket(host);
            } else if ('MozWebSocket' in window) {
                Chat.socket = new MozWebSocket(host);
            } else {
                Console.log('Error: WebSocket is not supported by this browser.');
                return;
            }
			//建立连接触发事件
            Chat.socket.onopen = function () {
                Console.log('Info: WebSocket connection opened.');
                document.getElementById('chat').onkeydown = function(event) {
                    if (event.keyCode == 13) {
                        Chat.sendMessage();
                    }
                };
            };
			//关闭连接触发事件
            Chat.socket.onclose = function () {
                document.getElementById('chat').onkeydown = null;
                Console.log('Info: WebSocket closed.');
            };
			//接收消息触发事件
            Chat.socket.onmessage = function (message) {
                Console.log(message.data);
            };
        });
		//初始化聊天对象方法,注意URL中的项目名称和Servlet名称
        Chat.initialize = function() {
            if (window.location.protocol == 'http:') {
                Chat.connect('ws://' + window.location.host + '/webchat/ChatWebSocketServlet');
            } else {
                Chat.connect('wss://' + window.location.host + '/webchat/ChatWebSocketServlet');
            }
        };
		//发送聊天信息方法
        Chat.sendMessage = (function() {
            var message = document.getElementById('chat').value;
            if (message != '') {
                Chat.socket.send(message);
                document.getElementById('chat').value = '';
            }
        });
        var Console = {};
		//显示消息记录
        Console.log = (function(message) {
            var console = document.getElementById('console');
            var p = document.createElement('p');
            p.style.wordWrap = 'break-word';
            p.innerHTML = message;
            console.appendChild(p);
            while (console.childNodes.length > 25) {
                console.removeChild(console.firstChild);
            }
            console.scrollTop = console.scrollHeight;
        });
        Chat.initialize();
    </script>
</head>
<body>
<div>
    <p>
        <input type="text" placeholder="type and press enter to chat" id="chat">
    </p>
    <div id="console-container">
        <div id="console"></div>
    </div>
</div>
</body>
</html>

代码完成后进行编译并部署到Tomcat7服务器:

启动服务后,打开支持HTML5的浏览器,如Chrome,Firefox(版本6以上)等,输入项目的访问地址:http://localhost:8080/webchat/chat.html。

连接成功时,能够看到页面输出的消息:Info: WebSocket connection opened.以及服务器发送的欢迎词:* Guest1 has joined.接下来可以在其他窗口中打开相同的链接,在其中一个窗口中的输入框内输入消息,按回车键后,其他所有窗口中都会看到从这个窗口发送的消息。一个简单的Web聊天室功能就完成了。

MyEclipse下开发WebSocket应用的注意事项:

由于MyEclipse下的默认部署方式与Eclipse有所区别,并且较低版本的MyEclipse不支持Tomcat7环境下的开发,因此,需要手动将Tomcat7的服务器运行环境添加到项目的构建路径中。

  1. 在项目上点击右键,选择:Build Path→Configure Build Path;
  2. 点击Add Library→Server Runtime;
  3. 选择之前配置好的Tomcat7运行时环境。

但是在MyEclipse的默认部署方式下,部署时会将Tomcat7中的jar包全部部署到WEB-INF/lib文件夹下,这样的话会与Tomcat7/lib中的包发生冲突,因此,还需要配置服务器的部署策略:

  1. 在项目上点击右键,选择Properties;
  2. 弹出窗口中选择MyEclipse→Web;
  3. 在Deployment选项卡中,去掉JARs from User Libraries前面的复选框。

这样就不会将服务器运行时环境下的jar包发布到服务器中,避免与服务器下的lib发生冲突,使程序能够正常运行。

6月 132013
 

转自:http://redstarofsleep.iteye.com/blog/1488639

注意:tomcat 7.29后一些版本支持websocket的接口并没有发布在对外的发布包里,因此如果需要测试,应该下载源码然后自己build(build的方法在下载的源码包里有详细的说明),切记!!!

 

之前大概的看过WebSocket,当时Tomcat还不支持WebSocket,所以当时写了一篇基于Jetty的WebSocket实现,地址如下:

http://redstarofsleep.iteye.com/blog/1307608

 

现在Tomcat7.0.27发布了,从这个版本开始Tomcat就支持WebSocket了。

Tomcat的WebSocket和Jetty的大致上差不多,大同小异,这里就简单的贴两个类吧(此例子未考虑多线程的情况):

 

第一个类,这个和Jetty一样,需要一个Servlet来处理WebSocket请求:

Java代码  收藏代码

  1. package lhc.websocket;  
  2.   
  3. import org.apache.catalina.websocket.StreamInbound;  
  4. import org.apache.catalina.websocket.WebSocketServlet;  
  5.   
  6. public class MyWebSocketServlet extends WebSocketServlet {  
  7.   
  8.     private static final long serialVersionUID = -7178893327801338294L;  
  9.   
  10.     @Override  
  11.     protected StreamInbound createWebSocketInbound(String arg0) {  
  12. System.out.println(“##########”);  
  13.         return new MyMessageInbound();  
  14.     }  
  15.   
  16. }  

这个Servlet继承自WebSocketServlet,实现createWebSocketInbound方法。该方法返回第二个类的实例。

 

第二个类,处理每一次具体的WebSocket任务:

Java代码  收藏代码

  1. package lhc.websocket;  
  2.   
  3. import java.io.IOException;  
  4. import java.nio.ByteBuffer;  
  5. import java.nio.CharBuffer;  
  6.   
  7. import lhc.init.InitServlet;  
  8.   
  9. import org.apache.catalina.websocket.MessageInbound;  
  10. import org.apache.catalina.websocket.WsOutbound;  
  11.   
  12. public class MyMessageInbound extends MessageInbound {  
  13.   
  14.     @Override  
  15.     protected void onBinaryMessage(ByteBuffer arg0) throws IOException {  
  16.         // TODO Auto-generated method stub  
  17.           
  18.     }  
  19.   
  20.     @Override  
  21.     protected void onTextMessage(CharBuffer msg) throws IOException {  
  22.         for (MessageInbound messageInbound : InitServlet.getSocketList()) {  
  23.             CharBuffer buffer = CharBuffer.wrap(msg);  
  24.             WsOutbound outbound = messageInbound.getWsOutbound();  
  25.             outbound.writeTextMessage(buffer);  
  26.             outbound.flush();  
  27.         }  
  28.           
  29.     }  
  30.   
  31.     @Override  
  32.     protected void onClose(int status) {  
  33.         InitServlet.getSocketList().remove(this);  
  34.         super.onClose(status);  
  35.     }  
  36.   
  37.     @Override  
  38.     protected void onOpen(WsOutbound outbound) {  
  39.         super.onOpen(outbound);  
  40.         InitServlet.getSocketList().add(this);  
  41.     }  
  42.       
  43.       
  44.   
  45. }  

 

 这个类继承自MessageInbound类,必须实现onBinaryMessage和onTextMessage方法。Jetty中只有一个onMessage方法,而Tomcat细化成了2个方法。

 

还要一个初始化的Servlet

Java代码  收藏代码

  1. package lhc.init;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. import javax.servlet.ServletConfig;  
  7. import javax.servlet.ServletException;  
  8. import javax.servlet.http.HttpServlet;  
  9.   
  10. import org.apache.catalina.websocket.MessageInbound;  
  11.   
  12. public class InitServlet extends HttpServlet {  
  13.   
  14.     private static final long serialVersionUID = -3163557381361759907L;  
  15.       
  16.     private static List<MessageInbound> socketList;    
  17.       
  18.     public void init(ServletConfig config) throws ServletException {    
  19.         InitServlet.socketList = new ArrayList<MessageInbound>();    
  20.         super.init(config);    
  21.         System.out.println(“Server start============”);    
  22.     }    
  23.         
  24.     public static List<MessageInbound> getSocketList() {    
  25.         return InitServlet.socketList;    
  26.     }    
  27. }  

 

 

最后,web.xml中进行一下Servlet的配置。

Xml代码  收藏代码

  1. <?xml version=“1.0” encoding=“UTF-8”?>  
  2. <web-app xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance” xmlns=“http://java.sun.com/xml/ns/javaee” xmlns:web=“http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd” xsi:schemaLocation=“http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd” id=“WebApp_ID” version=“3.0”>  
  3.   <display-name>wsoc</display-name>  
  4.   <welcome-file-list>  
  5.     <welcome-file>index.jsp</welcome-file>  
  6.   </welcome-file-list>  
  7.     
  8.   <servlet>  
  9.     <servlet-name>mywebsocket</servlet-name>  
  10.     <servlet-class>lhc.websocket.MyWebSocketServlet</servlet-class>  
  11.   </servlet>  
  12.   <servlet-mapping>  
  13.     <servlet-name>mywebsocket</servlet-name>  
  14.     <url-pattern>*.do</url-pattern>  
  15.   </servlet-mapping>  
  16.     
  17.   <servlet>  
  18.     <servlet-name>initServlet</servlet-name>  
  19.     <servlet-class>lhc.init.InitServlet</servlet-class>  
  20.     <load-on-startup>1</load-on-startup>  
  21.   </servlet>  
  22. </web-app>  

 

 

页面的话,就是标准的HTML5的websocket,这个和服务器是否是Tomcat或者Jetty是无关的:

Html代码  收藏代码

  1. <%@ page language=“java” contentType=“text/html; charset=UTF-8”  
  2.     pageEncoding=“UTF-8”%>  
  3. <!DOCTYPE html>  
  4. <html>  
  5. <head>  
  6. <meta http-equiv=“Content-Type” content=“text/html; charset=UTF-8”>  
  7. <title>Index</title>  
  8. <script type=“text/javascript”>  
  9. var ws = null;  
  10. function startWebSocket() {  
  11.     if (‘WebSocket’ in window)  
  12.         ws = new WebSocket(“ws://localhost:8080/wsoc/mywebsocket.do”);  
  13.     else if (‘MozWebSocket’ in window)  
  14.         ws = new MozWebSocket(“ws://localhost:8080/wsoc/mywebsocket.do”);  
  15.     else  
  16.         alert(“not support”);  
  17.       
  18.       
  19.     ws.onmessage = function(evt) {  
  20.         alert(evt.data);  
  21.     };  
  22.       
  23.     ws.onclose = function(evt) {  
  24.         alert(“close”);  
  25.     };  
  26.       
  27.     ws.onopen = function(evt) {  
  28.         alert(“open”);  
  29.     };  
  30. }  
  31.   
  32. function sendMsg() {  
  33.     ws.send(document.getElementById(‘writeMsg’).value);  
  34. }  
  35. </script>  
  36. </head>  
  37. <body onload=“startWebSocket();”>  
  38. <input type=“text” id=“writeMsg”></input>  
  39. <input type=“button” value=“send” onclick=“sendMsg()”></input>  
  40. </body>  
  41. </html>  

 

websocket与node.js的完美结合

 Nodejs  websocket与node.js的完美结合已关闭评论
5月 162013
 

转自:http://cnodejs.org/topic/4f16442ccae1f4aa27001139

 
之所以写下此文,是我觉得越是简单的技术往往能发挥越重要的作用,随着各种新的技术的诞生,实时web技术已经走进我们。websocket和node.js使开发实时应用非常简单,同时性能也非常高。 

关于websocket 

websocket是html5的重要feature,它直接在浏览器上对与socket的支持,这给了web开发无限的想象,虽然以前也有flash socket+js的实现,不过毕竟不稳定,而且兼容性有很多问题,当然websocket的普及也依赖于支持html5标准的浏览器的更新,目前只有chrome、safari、firefox 4.0等少数浏览器可以支持,不过大势所驱,加上智能移动设备的普及,websocket可以有更大的作为。 

他解决了web实时化的问题,相比传统http有如下好处: 

  • 一个WEB客户端只建立一个TCP连接
  • Websocket服务端可以推送(push)数据到web客户端.
  • 有更加轻量级的头,减少数据传送量
  •  

  •  

本文来重点来分析下。 

websocket的原理和应用

在继续本文之前,让我们了解下websocket的原理: 

websocket通信协议实现的是基于浏览器的原生socket,这样原先只有在c/s模式下的大量开发模式都可以搬到web上来了,基本就是通过浏览器的支持在web上实现了与服务器端的socket通信。 

WebSocket没有试图在HTTP之上模拟server推送,而是直接在TCP之上定义了帧协议,因此WebSocket能够支持双向的通信。 

首先来介绍下websocket客户端与服务端建立连接的过程: 

先用js创建一个WebSocket实例,使用ws协议建立服务器连接,ws://www.cnodejs.org:8088 

ws开头是普通的websocket连接,wss是安全的websocket连接,类似于https。 

客户端与服务端建立握手,发送如下信息: 

GET /echo HTTP/1.1

Upgrade: WebSocket

Connection: Upgrade

Host: www.cnodejs.org:8088

Origin: http://www.cnodejs.com

服务端会发回如下: 

HTTP/1.1 101 Web Socket Protocol Handshake

Upgrade: WebSocket

Connection: Upgrade

WebSocket-Origin: http://www.cnodejs.org

WebSocket-Location: ws://www.cnodejs.org:8088/echo

具体的ws协议,可以参考: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 

我们在开发过程中不需要考虑协议的细节,因为websocket API已经帮我们封装好了。 

需要注意的是所有的通信数据都是以”x00″开头以”xFF”结尾的,并且都是UTF-8编码的。 

这个过程类似于http的建立连接过程,不同的是,建立连接之后,接下来客户端和服务端的任何交互都需要再有这个动作。客户端通过websocket API提供的如下4个事件进行编程: 

  • onopen 建立连接后触发
  • onmessage 收到消息后触发
  • onerror 发生错误时触发
  • onclose 关闭连接时触发
  •  

  •  

让我们全面了解一下websocket API,他其实非常简单,下面是所有的API: 

[Constructor(in DOMString url, in optional DOMString protocols)]

[Constructor(in DOMString url, in optional DOMString[] protocols)]

interface WebSocket {

readonly attribute DOMString url;



// ready state

const unsigned short CONNECTING = 0;

const unsigned short OPEN = 1;

const unsigned short CLOSING = 2;

const unsigned short CLOSED = 3;

readonly attribute unsigned short readyState;

readonly attribute unsigned long bufferedAmount;



// networking

attribute Function onopen;

attribute Function onmessage;

attribute Function onerror;

attribute Function onclose;

readonly attribute DOMString protocol;

void send(in DOMString data);

void close();

};

WebSocket implements EventTarget;

详细的websocket API,可以参考此文: http://dev.w3.org/html5/websockets/ 

node.js与websocket的结合

终于讲到了正题了,node.js如何与websocket结合,websocket API是基于事件的,他是对于客户端而言,而对于服务端来说,如何来处理呢?其实可以简单的理解为实现websocket协议的socket server开发。 

node.js天生就是一个高效的服务端语言,可以直接使用javascript直接来处理来自客户端的请求,这样如果服务端这边需要大量的业务逻辑开发,则可以直接使用node开发。通过node和websocket的结合可以开发出很多实时性要求很高的web应用,如游戏、直播、股票、监控、IM等等。 

而node.js如何实现websocket的支持,已经有一个比较成熟的开源系统node-websocket-server: https://github.com/miksago/node-websocket-server,让我们来探究一二: 

其实原理也是很简单就是用node实现了websocket draft-76的协议,同时他对外提供了api,可以方便其他应用程序简化编程。 

它继承了node的http.Server的事件和方法,这样它简化了服务端的编程,同时可以处理http的请求。 

为了实现连接之间的通信和消息的广播,它实现了一个manager类,给每一个连接创建一个id,然后在内存中维护一个连接链表,并提供了上线和下线的自动管理。 

它还提供对以下几个事件的接口: 

  • listening 当服务器准备好接受客户端请求时
  • request 当一个http 请求发生时触发
  • stream
  • close
  • clientError
  • error
  •  

  •  

让我们看看一个node-websocket-server提供的一个server的例子: 

var sys = require("sys")

  , ws = require('../lib/ws/server');



var server = ws.createServer({debug: true});



// Handle WebSocket Requests

server.addListener("connection", function(conn){

  conn.send("Connection: "+conn.id);



  conn.addListener("message", function(message){

    conn.broadcast("<"+conn.id+"> "+message);



    if(message == "error"){

      conn.emit("error", "test");

    }

  });

});



server.addListener("error", function(){

  console.log(Array.prototype.join.call(arguments, ", "));

});



server.addListener("disconnected", function(conn){

  server.broadcast("<"+conn.id+"> disconnected");

});



server.listen(8000);

 

这个例子非常的简单,可以看到对于websocket的server端开发,我们已经不需要考虑websocket协议的实现,他几乎有着和客户端浏览器上websocket API一样的事件,只有对连接、断开连接、消息、错误等事件进行处理,这样应用的开发就非常的灵活了。

 

实例:用websocket和node.js搭建实时监控系统

通过websocket打通了浏览器和服务端之后,我们就可以尝试搭建一个实际的应用,这里以实时监控系统为例。 

 

直接与linux自身监控工具的结合,将监控结果通过websocket直接更到网页上,由于建立了socket长连接,绑定iostat的标准输出的事件,做到了真正的实时。同时可以支持对监控结果的讨论,增加了一个简单的chat,基于事件的通讯中,chat和监控同时发送完全不受影响,所以还可以把更多的事件加入进来。 

让我们来看看这个过程: 

首先是用node.js捕获iostat的输出: 

var sys = require("sys")

  , ws = require('../lib/ws/server');



var sys = require('sys');

var spawn = require('child_process').spawn;

var mon = spawn("iostat",["-I","5"]);

spawn可以根据参数启动一个进程,同时可以对stdout, stderr, exit code进行捕获,当这些事件触发时,可以绑定我们的函数,同时捕获其输出。 
这里是iostat的标准输出: 



disk0 cpu load average

KB/t tps MB/s us sy id 1m 5m 15m

14.64 4 0.06 7 5 88 0.76 0.95 0.90

我们捕获他的输出,将其发送到客户端去: 

  mon.stdout.on('data',function(data) {

    data = format_string(data);

    sys.puts(data);

    conn.send("#mon:"+data+"");

  });

客户端也就是浏览器,在收到消息后,对其进行简单的字符串处理,然后就可以直接在网页中输出了。 

w.onmessage = function(e) {

    var msg = e.data;

    if(msg.match(/#mon:/)) {

        var monarr = msg.split(":")[1].split(" ");

        var body = "";

        for(var item in monarr) {

            body += ""+monarr[item]+""

        }

        $("#iobody").html(body);

        //log(monarr[0]);



    }

    else

        log(e.data);

}

这里自定义了一个#mon的简单协议,这样可以对更多类型的输出分开处理。 

服务端和客户端总共100多行的代码,就已经实现了一个实时服务器性能监控系统。 
全部代码下载地址: http://cnodejs.googlecode.com/svn/trunk/monsocket/examples/ 
(注:本程序仅在mac osx下测试通过) 

如果加上RGraph(基于html5),则可以打造更加精美的实时展现:  http://www.rgraph.net/docs/dynamic.html 

总结

这篇文章适合node.js的初学者或者对于websocket不够了解的人,总结起来,就是以下几个点: 

  • 使用websocket API可以开发实时web应用
  • websocket api和 node.js可以很完美的配合
  • node-websocket-server 封装了websocket协议,使服务端进行websocket的开发,非常的简单
  • node的易用性,使其在服务端略加编程,即可以打造一个完美的后台服务
  • node的事件驱动的机制保证了系统的高效,可以使用EventEmitter定义自己的事件触发
  • 对于命令行输出可以使用spawn来捕获,通过在web应用中充分利用linux的各种系统工具
  •  

WebSocket实战

 Nodejs  WebSocket实战已关闭评论
5月 162013
 

转自:http://ued.sina.com.cn/?p=900

前言

互联网发展到现在,早已超越了原始的初衷,人类从来没有像现在这样依赖过他;也正是这种依赖,促进了互联网技术的飞速发展。而终端设备的创新与发展,更加速了互联网的进化;

 

HTTP/1.1规范发布于1999年,同年12月24日,HTML4.01规范发布;尽管已到2012年,但HTML4.01仍是主流;虽然HTML5的草案已出现了好几个年头,但转正日期,遥遥无期,少则三五年,多则数十年;而HTML5的客户代理(对于一般用户而言,就是浏览器),则已百家争鸣,星星向荣;再加上移动终端的飞速发展,在大多数情况下,我们都可以保证拥有一个HTML5的运行环境,所以,我们来分享一下HTML5中的WebSocket协议;

本文包含以下六个方面:
1. WebSocket的前世今生
2. WebSocket是什么
3. 为什么使用WebSocket
4. 搭建WebSocket服务器
5. WebSocket API
6. 实例解析

以上六点分为两大块,前3点侧重理论,主要让大家明白WebSocket是什么,而后3点则结合代码实战,加深对WebSocket的认知。

一、WebSocket的前世今生

Web 应用的信息交互过程通常是客户端通过浏览器发出一个请求,服务器端接收和审核完请求后进行处理并返回结果给客户端,然后客户端浏览器将信息呈现出来,这种机制对于信息变化不是特别频繁的应用尚能相安无事,但是对于那些实时要求比较高的应用来说就显得捉襟见肘了。我们需要一种高效节能的双向通信机制来保证数据的实时传输。有web TCP之称的WebSocket应运而生,给开发人员提供了一把强有力的武器来解决疑难杂症。
(PS:其实,在早期的HTML5规范中,并没有包含WebSocket的定义,一些早期的HTML5书籍中,完全没有WebSocket的介绍。直到后来,才加入到当前的草案中。)

二、WebSocket是什么?

其实,从背景介绍中,我们大致的可以猜出,WebSocket是干什么用的。前面我们提到,WebSocket有web TCP之称,既然是TCP,肯定是用来做通信的,但是它又有不同的地方,WebSocket作为HTML5中新增的一种通信协议,由通信协议和编程API组成,它能够在浏览器和服务器之间建立双向连接,以基于事件的方式,赋予浏览器原生的实时通信能力,来扩展我们的web应用,增加用户体验,提升应用的性能。何谓双向?服务器端和客户端可以同时发送并响应请求,而不再像HTTP的请求和响应。

三、为什么使用WebSocket

在WebSocket出现之前,我们有一些其它的实时通讯方案,比较常用的有轮询,长轮询,流,还有基于Flash的交换数据的方式,接下来,我们一一分析一下,各种通信方式的特点。

① 轮询
这是最早的一种实现实时web应用的方案;原理比较简单易懂,就是客户端以一定的时间间隔向服务器发送请求,以频繁请求的方式来保持客户端和服务器端的数据同步。但是问题也很明显:当客户端以固定频率向服务器端发送请求时,服务器端的数据可能并没有更新,这样会带来很多无谓的请求,浪费带宽,效率低下。

② 长轮询
长轮询是对定时轮询的改进和提高,目地是为了降低无效的网络传输。当服务器端没有数据更新的时候,连接会保持一段时间周期直到数据或状态改变或者时间过期,通过这种机制来减少无效的客户端和服务器间的交互。当然,如果服务端的数据变更非常频繁的话,这种机制和定时轮询比较起来没有本质上的性能的提高。

③ 流
长轮询是对定时轮询的改进和提高,目地是为了降低无效的网络传输。当服务器端没有数据更新的时候,连接会保持一段时间周期直到数据或状态改变或者时间过期,通过这种机制来减少无效的客户端和服务器间的交互。当然,如果服务端的数据变更非常频繁的话,这种机制和定时轮询比较起来没有本质上的性能的提高。

④ 基于Flash的实时通讯方式
Flash有自己的socket实现,这为实时通信提供了可能。我们可以利用Flash完成数据交换,再利用Flash暴露出相应的接口,方便JavaScript调用,来达到实时传输数据的目的。这种方式比前面三种方式都要高效,而且应用场景比较广泛;因为flash本身的安装率很高;但是在当前的互联网环境下,移动终端对flash的支持并不好,以IOS为主的系统中根本没有flash的存在,而在android阵营中,虽然有flash的支持,但实际的使用效果差强人意,即使是配置较高的移动设备,也很难让人满意。就在前几天(2012年6月底),Adobe官方宣布,不在支持android4.1以后的系统,这基本上宣告了flash在移动终端上的死亡。

下面是轮询和长轮询的信息流转图:

对比完四种不同的实时通信方式,不难发现,除了基于flash的方案外,其它三种方式都是用AJAX方式来模拟实时的效果,每次客户端和服务器端交互时,都是一次完整的HTTP请求和应答的过程,而每一次的HTTP请求和应答都带有完整的HTTP头信息,这就增加每次的数据传输量,而且这些方案中客户端和服务端的编程实现比较复杂。

接下来,我们再来看一下WebSocket,为什么要使用它呢?高效节能,简单易用。
下图是来自websocket.org的测试结果:

在流量和负载增大的情况下,WebSocket 方案相比传统的 Ajax 轮询方案有极大的性能优势;而在开发方面,也十分简单,我们只需要实例化WebSocket,创建连接,查看是否连接成功,然后就可以发送和相应消息了。我们会在后面的实例中去详细的说明API。

四、搭建WebSocket服务器

其实,在服务器的选择上很广,基本上,主流语言都有WebSocket的服务器端实现,而我们作为前端开发工程师,当然要选择现在比较火热的NodeJS作为我们的服务器端环境了。

NodeJS本身并没有原生的WebSocket支持,但是有第三方的实现(大家要是有兴趣的话,完全可以参考WebSocket协议来做自己的实现),我们选择了“ws”作为我们的服务器端实现。

由于本文的重点是讲解WebSocket,所以,对于NodeJS不做过多的介绍,不太熟悉的朋友可以去参考NodeJS入门指南(http://www.nodebeginner.org/index-zh-cn.html)。

安装好NodeJS之后,我们需要安装“ws”,也就是我们的WebSocket实现,安装方法很简单,在终端或者命令行中输入:

1 npm install ws

,等待安装完成就可以了。

接下来,我们需要启动我们的WebSocket服务。首先,我们需要构建自己的HTTP服务器,在NodeJS中构建一个简单的HTTP服务器很简单,so easy。代码如下:

1 var app = http.createServer( onRequest ).listen( 8888 );

 

onRequest()作为回调函数,它的作用是处理请求,然后做出响应,实际上就是根据接收的URL,在服务器上查找相应的资源,最终返回给浏览器。
在构建了HTTP服务器后,我们需要启动WebSocket服务,代码如下:

1 var WebSocketServer = require('ws').Server;
2 var wss = new WebSocketServer( { server : app } );

 

从代码中可以看出,在初始化WebSocket服务时,把我们刚才构建好的HTTP实例传递进去就好。到这里,我们的服务端代码差不多也就编写完成了。怎么样?很简单吧。

五、WebSocket API

上面我们介绍了WebSocket服务端的知识,接下来,我们需要编写客户端代码了。在前面我们说过,客户端的API也是一如既往的简单:

见上图:ready state中定义的是socket的状态,分为connection、open、closing和closed四种状态,从字面上就可以区分出它们所代表的状态。


上图描述的是WebSocket的事件,分为onopen、onerror和onclose;


上图为消息的定义,主要是接收和发送消息。注意:可以发送二进制的数据。

以上个图的具体的含义就不再一一赘述,详细描述请参考:
http://www.w3.org/TR/2012/WD-websockets-20120524/
PS:由于WebSocket API(截止到2012年7月)还是草案,API文档和上文所描述的会有所不同,请以官方文档为主,这也是我为什么不详细描述API中各个属性的原因。

另外一点需要提醒大家的是:在前端开发中,浏览器兼容是必不可少的,而WebSocket在主浏览器中的兼容还是不错的,火狐和Chrome不用说,最新版的支持非常不错,而且支持二进制数据的发送和接收。但是IE9并不支持,对于国内的大多数应用场景,WebSocket无法大规模使用。

截图来自(http://tongji.baidu.com/data/browser),之所以选择百度的统计数据,是因为更加符合国内的实际情况。图中所展示的是2012年4月1日到2012年6月30日之间的统计数据,从图中不难看出IE6.0、奇虎360、IE7.0和IE8.0加起来一共占据了77%的市场,FireFox属于其他,chrome只有5.72%的份额,再一次告诉我们,我们的主战场依然是IE系。

既然是IE系,那么对于WebSocket在实际app中的应用就基本不可能了。但我们完全可以在chrome、FireFox、以及移动版的IOS浏览器中使用它。

六、实例解析

搭建好了服务端,熟悉了API,接下来,我们要开始构建我们的应用了。鉴于WebSocket自身的特点,我们的第一个demo选择了比较常见的聊天程序,我们暂且取名为chat。

说到聊天,大家最先想到的肯定是QQ,没错,我们所实现的应用和QQ类似,而且还是基于web的。因为是demo,我们的功能比较简陋,仅实现了最简单的会话功能。就是启动WebSocket服务器后,客户端发起连接,连接成功后,任意客户端发送消息,都会被服务器广播给所有已连接的客户端,包括自己。

既然需要客户端,我们需要构建一个简单的html页面,页面中样式和元素,大家可以自由发挥,只要能够输入消息,有发送按钮,最后有一个展示消息的区域即可。具体的样子大家可以看附件中的demo。

写玩HTML页面之后,我们需要添加客户端脚本,也就是和WebSocket相关的代码;前面我们说过,WebSocket的API本身很简单,所以,我们的客户端代码也很直接,如下:

1 var wsServer = 'ws://localhost:8888/';
2 var websocket = new WebSocket(wsServer);
3 websocket.binaryType = "arraybuffer";
4 websocket.onopen = onOpen;
5 websocket.onclose = onClose;
6 websocket.onmessage = onMessage;
7 websocket.onerror = onError;

 

首先,我们需要指定WebSocket的服务地址,也就是var wsServer = ‘ws://localhost:8888/’;

然后,我们实例化WebSocket,new WebSocket(wsServer),
剩下的就是指定相应的回调函数了,分别是onOpen,onClose,onMessage和onError,对于咱们的实验应用来说,onopen、onclose、onerror甚至可以不管,咱们重点关注一下onmessage。

onmessage()这个回调函数会在客户端收到消息时触发,也就是说,只要服务器端发送了消息,我们就可以通过onmessage拿到发送的数据,既然拿到了数据,接下去该怎么玩,就随便我们了。请看下面的伪代码:

1 function onMessage(evt) {
2     var json = JSON.parse(evt.data);
3     commands[json.event](json.data);
4 }

 

因为onmessage只接收字符串和二进制类型的数据,如果需要发送json格式的数据,就需要我们转换一下格式,把字符串转换成JSON格式。只要是支持WebSocket,肯定原生支持window.JSON,所以,我们可以直接使用JSON.parse()和JSON.stringify()来进行转换。
转换完成后,我们就得到了我们想要的数据了,接下来所做的工作就是将消息显示出来。实际上就是

1 Elements.innerHTML += data + '</br>';

 

上面展现了客户端的代码,服务器端的代码相对要简单一些,因为我们的服务器端使用的是第三方实现,我们只需要做一些初始化工作,然后在接收到消息时,将消息广播出去即可,下面是具体的代码:

01 var app = http.createServer( onRequest ).listen( 8888 );
02 var WebSocketServer = require('ws').Server,
03     wss = new WebSocketServer( { server : app } );
04 wss.on('connection', function( ws ) {
05     console.log('connection successful!');
06     ws.on('message', function( data, flags ) {
07         console.log(data);
08         //do something here
09     });
10     ws.on('close', function() {
11         console.log('stopping client');
12     });
13 });

 

我们可以通过wss.clients获得当前已连接的所有客户端,然后遍历,得到实例,调用send()方法发送数据;

1 var clients = wss.clients, len = clients.length, i = 0;
2         for( ; i < len; i = i + 1 ){
3             clients[i].send( msg );
4         }

 

说到这里,一个双向通信的实例基本完成,当然,上面都是伪代码,完整的demo请查看附件。

除了常见的聊天程序以外,大家完全可以发挥创意,构建一些“好玩”的应用;
接下来,分享另外一个应用,“你画我猜”这个应用,很多人都接触过,大致上是:某个人在屏幕上画一些图形,这些图片会实时展示在其它人的屏幕上,然后来猜画的是什么。

利用WebSocket和canvas,我们可以很轻松的构建类似的应用。当然,我们这里只是demo,并没有达到产品级的高度,这里只是为大家提供思路;
首先,我们再次明确一下,WebSocket赋予了我们在浏览器端和服务器进行双向通信的能力,这样,我们可以实时的将数据发送给服务器,然后再广播给所有的客户端。这和聊天程序的思路是一致的。

接下来,服务器端的代码不用做任何修改,在html页面中准备一个canvas,作为我们的画布。如何在canvas上用鼠标画图形呢?我们需要监听mousedown、mousemove和mouseup三个鼠标事件。说到这里,大家应该知道怎么做了。没错,就是在按下鼠标的时候,记录当前的坐标,移动鼠标的时候,把坐标发送给服务器,再由服务器把坐标数据广播给所有的客户端,这样就可以在所有的客户端上同步绘画了;最后,mouseup的时候,做一些清理工作就ok了。下面是一些伪代码:

01 var WhiteBoard = function( socket, canvasId ){
02                 var lastPoint = null,
03                     mouseDown = false,
04                     canvas = getById(canvasId),
05                     ctx = canvas.getContext('2d');
06  
07                 var handleMouseDown = function(event) {
08                     mouseDown = true;
09                     lastPoint = resolveMousePosition.bind( canvas, event )();
10                 };
11  
12                 var handleMouseUp = function(event) {
13                     mouseDown = false;
14                     lastPoint = null;
15                 };
16  
17                 var handleMouseMove = function(event) {
18                     if (!mouseDown) { return; }
19                     var currentPoint = resolveMousePosition.bind( canvas, event )();
20                     socket.send(JSON.stringify({
21                         event: 'draw',
22                         data: {
23                             points: [
24                                 lastPoint.x,
25                                 lastPoint.y,
26                                 currentPoint.x,
27                                 currentPoint.y
28                             ]
29                         }
30                     }));
31  
32                     lastPoint = currentPoint;
33                 };         
34  
35                 var init = function(){
36                     addEvent( canvas, 'mousedown', handleMouseDown );
37                     addEvent( canvas, 'mouseup', handleMouseUp );
38                     addEvent( canvas, 'mousemove', handleMouseMove );
39  
40                     var img = new Image();
41                     addEvent( img, 'load', function(e){
42                         canvas.width = img.width;
43                         canvas.height = img.height;
44                         ctx.drawImage( img, 0, 0 );
45                     } );
46                     img.src = '/img/diablo3.png';
47                 };
48  
49                 var drawLine = function(data) {
50                     var points = data.points;
51                     ctx.strokeStyle = 'rgb(255, 15, 255)';
52                     ctx.beginPath();
53                     ctx.moveTo( points[0] + 0.5, points[1] + 0.5 );
54                     ctx.lineTo( points[2] + 0.5, points[3] + 0.5 );
55                     ctx.stroke();
56                 };
57  
58                 function resolveMousePosition(event) {
59                     var x, y;
60                     if (event.offsetX) {
61                         x = event.offsetX;
62                         y = event.offsetY;
63                     } else {  //(注意)实际开发中,这样获取鼠标相对canvas的坐标是不对的
64                         x = event.layerX - this.offsetLeft;
65                         y = event.layerY - this.offsetTop;
66                     }
67                     return { x: x, y: y };
68                 };
69  
70                 init();
71  
72                 return {
73                     draw : drawLine
74                     //ctx : ctx,
75                     //canvas : canvas
76                 }
77             }( websocket, 'drawsomething' );

对于canvas不熟悉的同学,请自己去搜索一下,有许多不错的教程。其它方面,和聊天应用的思路基本一样。

最后,我们需要明确一点,WebSocket本身的优点很明显,但是作为一个正在演变中的web规范,我们必须清楚的认识到WebSocket在构建应用时的一些风险;虽然本身有很多局限性,但是这项技术本身肯定是大势所趋,WebSocket在移动终端,在chrome web store都有用武之地,我们可以进行大胆的尝试,让我们在技术的革新中不被淘汰。

Resources:
http://www.w3.org/TR/websockets/
W3 API的官方文档,有详细的接口设计文档和实现步骤
http://tools.ietf.org/html/rfc6455
WebSocket协议
http://tools.ietf.org/html/rfc6202
Known Issues and Best Practices for the Use of Long Polling and Streaming in Bidirectional HTTP
http://msdn.microsoft.com/en-us/library/ie/hh673567(v=vs.85).aspx
msdn中关于WebSocket的介绍
https://developer.mozilla.org/en/WebSockets
http://caniuse.com/#feat=websockets
Compatibility tables for support of HTML5, CSS3, SVG and more in desktop and mobile browsers.