构建java的webSocket应用



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

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

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

lwei
lwei
翻译于 2年前

1人顶

 翻译的不错哦!

使用轮询时,客户端采用同步调用来从服务器获取信息。如果服务器有新信息,它会在响应中返回数据。否则,就不会有信息返回给客户端,然后客户端会在一段时间之后再次创建一个新的连接。这是一种很低效却很简单的获得实时行为的方式。长效轮询是另一种工作方法,客户端会对服务器发起一个连接,服务器会保持该连接直到有可用数据或者超过了指定超期时间。长效轮询也被称为comet。由于同步的HTTP和这些异步应用之间的不匹配,这些方案易于复杂化,无标准化和低效化。

随着时间的推移,对于在客户端和服务器之间建立基于标准的、双向的、全双工通道的需求,已经增长了。在本文中,我们会看到WebSockets是如何帮助解决这些问题的,然后了解一下如何在Java中使用JSR 356 API来构建基于WebSocket的应用。

请注意本文不会讨论到OpenShift WebSocket支持。如果你想了解OpenShift WebSocket支持,请参考Marek Jelen这篇文章

lwei
lwei
翻译于 2年前

0人顶

 翻译的不错哦!

什么是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

lwei
lwei
翻译于 2年前

0人顶

 翻译的不错哦!

WebSocket是如何工作的?

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

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

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

WebSocket使用案例

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

  • 聊天应用
  • 多人游戏
  • 股票交易和金融应用
  • 文档合作编辑
  • 社交应用
lwei
lwei
翻译于 2年前

0人顶

 翻译的不错哦!

在Java中使用WebSockets

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

lwei
lwei
翻译于 2年前

0人顶

 翻译的不错哦!

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。后面我们创建一个应用的时候再来更详细的了解这些。

lwei
lwei
翻译于 2年前

0人顶

 翻译的不错哦!

Tyrus -- JSR 356 参考实现

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

使用WebSockets开发一个单词游戏

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

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

lwei
lwei
翻译于 2年前

0人顶

 翻译的不错哦!

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

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

?
1
$ 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,如下所示。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
< 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。

?
1
2
3
4
5
< 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实现。

?
1
2
3
4
5
6
7
8
9
10
< 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应用。

?
1
2
3
4
5
< dependency >
     < groupId >org.glassfish.tyrus</ groupId >
     < artifactId >tyrus-container-grizzly</ artifactId >
     < version >1.1</ version >
</ dependency >

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

lwei
lwei
翻译于 2年前

0人顶

 翻译的不错哦!

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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连接关闭时它会被调用。

lwei
lwei
翻译于 2年前

1人顶

 翻译的不错哦!

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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应用的开发工作变得很容易。

lwei
lwei
翻译于 2年前

0人顶

 翻译的不错哦!

其它翻译版本(1)
loading... 正在加载...

步骤 5: 创建并启动一个WebSocket服务器

我们需要一个服务器来部署我们的WebSocket @ServerEndpoint。该服务器是使用如下所示的tyrus服务器API来创建的。它会运行在8025端口。WordgameServerEndpoint可以通过ws://localhost:8025/websockets/game来访问。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.shekhar.wordgame.server;
  
import java.io.BufferedReader;
import java.io.InputStreamReader;
  
import org.glassfish.tyrus.server.Server;
  
public class WebSocketServer {
  
     public static void main(String[] args) {
         runServer();
     }
  
     public static void runServer() {
         Server server = new Server( "localhost" , 8025 , "/websockets" , WordgameServerEndpoint. class );
  
         try {
             server.start();
             BufferedReader reader = new BufferedReader( new InputStreamReader(System.in));
             System.out.print( "Please press a key to stop the server." );
             reader.readLine();
         } catch (Exception e) {
             throw new RuntimeException(e);
         } finally {
             server.stop();
         }
     }
}

如果你在使用Eclipse,你可以通过把它作为一个Java application(ALT+SHIFT+X,J)来运行,来启动该服务器。你会看到如下所示的日志。

?
1
2
3
4
5
6
7
8
9
10
11
Jul 26 , 2013 1 : 39 : 37 PM org.glassfish.tyrus.server.ServerContainerFactory create
INFO: Provider class loaded: org.glassfish.tyrus.container.grizzly.GrizzlyEngine
Jul 26 , 2013 1 : 39 : 38 PM org.glassfish.grizzly.http.server.NetworkListener start
INFO: Started listener bound to [ 0.0 . 0.0 : 8025 ]
Jul 26 , 2013 1 : 39 : 38 PM org.glassfish.grizzly.http.server.HttpServer start
INFO: [HttpServer] Started.
Jul 26 , 2013 1 : 39 : 38 PM org.glassfish.tyrus.server.Server start
INFO: WebSocket Registered apps: URLs all start with ws: //localhost:8025
Jul 26 , 2013 1 : 39 : 38 PM org.glassfish.tyrus.server.Server start
INFO: WebSocket server started.
Please press a key to stop the server.
lwei
lwei
翻译于 2年前

0人顶

 翻译的不错哦!

步骤 6 : 启动WebSocket客户端

现在服务器已经启动了并且WebSocket @ServerEndpoint也部署好了,我们准备把客户端作为一个Java应用来启动。我们会创建一个ClientManager实例,然后连接到服务器上的endpoint,如下所示。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@ClientEndpoint
public class WordgameClientEndpoint {
  
     private static CountDownLatch latch;
  
     private Logger logger = Logger.getLogger( this .getClass().getName());
  
     @OnOpen
     public void onOpen(Session session) {
         // same as above
     }
  
     @OnMessage
     public String onMessage(String message, Session session) {
         // same as above
     }
  
     @OnClose
     public void onClose(Session session, CloseReason closeReason) {
         logger.info(String.format( "Session %s close because of %s" , session.getId(), closeReason));
         latch.countDown();
     }
  
     public static void main(String[] args) {
         latch = new CountDownLatch( 1 );
  
         ClientManager client = ClientManager.createClient();
         try {
             client.connectToServer(WordgameClientEndpoint. class , new URI( "ws://localhost:8025/websockets/game" ));
             latch.await();
  
         } catch (DeploymentException | URISyntaxException | InterruptedException e) {
             throw new RuntimeException(e);
         }
     }
}

我们使用CountDownLatch来确保在代码执行之后,主线程将不再存在。当时间锁去减少onClose()方法里的计数器时,主线程会保持等待。然后程序结束。在main()方法里,我们创建了ClientManager实例,它被用来连接位于ws://localhost:8025/websockets/game的@ServerEndpoint。

把客户端作为一个Java应用来运行(ALT + SHIFT + X , J),你会看到下列日志信息。

?
1
2
3
4
Jul 26 , 2013 1 : 40 : 26 PM com.shekhar.wordgame.client.WordgameClientEndpoint onOpen
INFO: Connected ... 95f58833-c168-4a5f-a580-085810b4dc5a
Jul 26 , 2013 1 : 40 : 26 PM com.shekhar.wordgame.client.WordgameClientEndpoint onMessage
INFO: Received ....start

随意发送消息比如 "hello world",它会被重复并返回给你,如下所示。

?
1
2
3
4
INFO: Received ....start
hello world
Jul 26 , 2013 1 : 41 : 04 PM com.shekhar.wordgame.client.WordgameClientEndpoint onMessage
INFO: Received ....hello world

发送"quit"消息,WebSocket连接就会被关闭。

?
1
2
3
4
INFO: Received ....hello world
quit
Jul 26 , 2013 1 : 42 : 23 PM com.shekhar.wordgame.client.WordgameClientEndpoint onClose
INFO: Session 95f58833-c168-4a5f-a580-085810b4dc5a close because of CloseReason[ 1000 ,Game ended]
lwei
lwei
翻译于 2年前

0人顶

 翻译的不错哦!

步骤 7 : 添加游戏逻辑

现在我们准备添加游戏逻辑,发送一个排序错乱的单词给客户端,然后当从客户端接收到一个恢复后单词时,检查该单词是否正确。像下面这样改进WordgameServerEndpoint的代码。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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 unscrambledWord, Session session) {
         switch (unscrambledWord) {
         case "start" :
             logger.info( "Starting the game by sending first word" );
             String scrambledWord = WordRepository.getInstance().getRandomWord().getScrambledWord();
             session.getUserProperties().put( "scrambledWord" , scrambledWord);
             return scrambledWord;
         case "quit" :
             logger.info( "Quitting the game" );
             try {
                 session.close( new CloseReason(CloseCodes.NORMAL_CLOSURE, "Game finished" ));
             } catch (IOException e) {
                 throw new RuntimeException(e);
             }
         }
         String scrambledWord = (String) session.getUserProperties().get( "scrambledWord" );
         return checkLastWordAndSendANewWord(scrambledWord, unscrambledWord, session);
     }
  
     @OnClose
     public void onClose(Session session, CloseReason closeReason) {
         logger.info(String.format( "Session %s closed because of %s" , session.getId(), closeReason));
     }
  
     private String checkLastWordAndSendANewWord(String scrambledWord, String unscrambledWord, Session session) {
         WordRepository repository = WordRepository.getInstance();
         Word word = repository.getWord(scrambledWord);
  
         String nextScrambledWord = repository.getRandomWord().getScrambledWord();
  
         session.getUserProperties().put( "scrambledWord" , nextScrambledWord);
  
         String correctUnscrambledWord = word.getUnscrambbledWord();
  
         if (word == null || !correctUnscrambledWord.equals(unscrambledWord)) {
             return String.format( "You guessed it wrong. Correct answer %s. Try the next one .. %s" ,
                     correctUnscrambledWord, nextScrambledWord);
         }
         return String.format( "You guessed it right. Try the next word ...  %s" , nextScrambledWord);
     }
}

重启服务器和客户端,享受游戏吧。

结论

在本文中,我们看到了JSR 356 WebSocket API是如何帮助我们构建实时的全双工的Java应用程序的。JSR 356 WebSocket API非常简单,并且基于注解的开发模式使得构建WebSocket应用非常容易。在下篇博文中,我们会看一下Undertow,一个来自JBoss的Java编写的灵活的高性能web服务器。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值