在客戶機和服務(wù)器之間建立單一的雙向連接,這就意味著客戶只需要發(fā)送一個請求到服務(wù)端,那么服務(wù)端則會進(jìn)行處理,處理好后則將其返回給客戶端,客戶端則可以在等待這個時間繼續(xù)去做其他工作,整個過程是異步的。在本系列教程中,將指導(dǎo)用戶如何在JAVA EE 7的容器GlassFish 4中,使用JAVA EE 7中的全新的解析Json API(JSR-353),以及綜合運用jQuery和Bootstrap。本文要求讀者有一定的HTML 5 Websocket的基礎(chǔ)原理知識。
效果圖
我們先來看下在完成這個教程后的效果圖,如下所示:
準(zhǔn)備工作
我們使用的是JDK 7 和MAVN 3進(jìn)行庫的構(gòu)建工作,首先看pom.xml中關(guān)于Jave EE 7的部分:
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
|
<properties> <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir> <project.build.sourceEncoding>UTF- 8 </project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version> 7.0 </version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version> 3.1 </version> <configuration> <source> 1.7 </source> <target> 1.7 </target> <compilerArguments> <endorseddirs>${endorsed.dir}</endorseddirs> </compilerArguments> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version> 2.3 </version> <configuration> <failOnMissingWebXml> false </failOnMissingWebXml> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version> 2.6 </version> [..] </plugin> </plugins> </build> |
同時,為了能使用GlassFish 4,需要增加如下的插件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
plugin> < groupId >org.glassfish.embedded</ groupId > < artifactId >maven-embedded-glassfish-plugin</ artifactId > < version >4.0</ version > < configuration > < goalPrefix >embedded-glassfish</ goalPrefix > < app >${basedir}/target/${project.artifactId}-${project.version}.war</ app > < autoDelete >true</ autoDelete > < port >8080</ port > < name >${project.artifactId}</ name > < contextRoot >hascode</ contextRoot > </ configuration > < executions > < execution > < goals > < goal >deploy</ goal > </ goals > </ execution > </ executions > </ plugin > |
設(shè)置Websocket的Endpoint
我們先來看服務(wù)端Websocket的代碼如下,然后再做進(jìn)一步解析:
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
|
package com.hascode.tutorial; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import javax.websocket.EncodeException; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; @ServerEndpoint (value = "/chat/{room}" , encoders = ChatMessageEncoder. class , decoders = ChatMessageDecoder. class ) public class ChatEndpoint { private final Logger log = Logger.getLogger(getClass().getName()); @OnOpen public void open( final Session session, @PathParam ( "room" ) final String room) { log.info( "session openend and bound to room: " + room); session.getUserProperties().put( "room" , room); } @OnMessage public void onMessage( final Session session, final ChatMessage chatMessage) { String room = (String) session.getUserProperties().get( "room" ); try { for (Session s : session.getOpenSessions()) { if (s.isOpen() && room.equals(s.getUserProperties().get( "room" ))) { s.getBasicRemote().sendObject(chatMessage); } } } catch (IOException | EncodeException e) { log.log(Level.WARNING, "onMessage failed" , e); } } } |
下面分析下上面的代碼:
使用@ ServerEndpoint定義一個新的endpoint,其中的值指定了URL并且可以使用PathParams參數(shù),就象在JAX-RS中的用法一樣。
所以值“/chat/{room}”允許用戶通過如下形式的URL去連接某個聊天室:ws://0.0.0.0:8080/hascode/chat/java
在大括號中的值(即room),可以通過使用javax.websocket.server.PathParam,在endpoint的生命周期回調(diào)方法中以參數(shù)的方式注入。
此外,我們要使用一個編碼和解碼的類,因為我們使用的是一個DTO形式的類,用于在服務(wù)端和客戶端傳送數(shù)據(jù)。
當(dāng)用戶第一次連接到服務(wù)端,輸入要進(jìn)入聊天室的房號,則這個房號以參數(shù)的方式注入提交,并且使用session.getUserProperties將值保存在用戶的屬性map中。
當(dāng)一個聊天參與者通過tcp連接發(fā)送信息到服務(wù)端,則循環(huán)遍歷所有已打開的session,每個session被綁定到指定的聊天室中,并且接收編碼和解碼的信息。
如果我們想發(fā)送簡單的文本信息或和二進(jìn)制格式的信息,則可以使用session.getBasicRemote().sendBinary() 或session.getBasicRemote().sendText()
接下來我們看下用于代表信息傳遞實體(DTO:Data Transfer Object)的代碼,如下:
1
2
3
4
5
6
7
8
9
10
11
|
package com.hascode.tutorial; import java.util.Date; public class ChatMessage { private String message; private String sender; private Date received; // 其他getter,setter方法 } |
聊天消息的轉(zhuǎn)換
在這個應(yīng)用中,將編寫一個編碼和解碼類,用于在聊天信息和JSON格式間進(jìn)行轉(zhuǎn)換。
先來看下解碼類的實現(xiàn),這將會把傳遞到服務(wù)端的聊天信息轉(zhuǎn)換為ChatMessage實體類。在這里,使用的是Java API for JSON Processing(JSR353)規(guī)范去將JSON格式的信息轉(zhuǎn)換為實體類,代碼如下,其中重寫的willDecode方法,這里默認(rèn)返回為true。
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
|
package com.hascode.tutorial; import java.io.StringReader; import java.util.Date; import javax.json.Json; import javax.json.JsonObject; import javax.websocket.DecodeException; import javax.websocket.Decoder; import javax.websocket.EndpointConfig; public class ChatMessageDecoder implements Decoder.Text<ChatMessage> { @Override public void init( final EndpointConfig config) { } @Override public void destroy() { } @Override public ChatMessage decode( final String textMessage) throws DecodeException { ChatMessage chatMessage = new ChatMessage(); JsonObject obj = Json.createReader( new StringReader(textMessage)) .readObject(); chatMessage.setMessage(obj.getString( "message" )); chatMessage.setSender(obj.getString( "sender" )); chatMessage.setReceived( new Date()); return chatMessage; } @Override public boolean willDecode( final String s) { return true ; } } |
同樣再看下編碼類的代碼,這個類相反,是將ChatMessage類轉(zhuǎn)換為Json格式,代碼如下:
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
|
package com.hascode.tutorial; import javax.json.Json; import javax.websocket.EncodeException; import javax.websocket.Encoder; import javax.websocket.EndpointConfig; public class ChatMessageEncoder implements Encoder.Text<ChatMessage> { @Override public void init( final EndpointConfig config) { } @Override public void destroy() { } @Override public String encode( final ChatMessage chatMessage) throws EncodeException { return Json.createObjectBuilder() .add( "message" , chatMessage.getMessage()) .add( "sender" , chatMessage.getSender()) .add( "received" , chatMessage.getReceived().toString()).build() .toString(); } } |
這里可以看到JSR-353的強大威力,只需要調(diào)用Json.createObjectBuilder就可以輕易把一個DTO對象轉(zhuǎn)化為JSON了。
通過Bootstrap、Javacsript搭建簡易客戶端
最后,我們綜合運用著名的Bootstrap、jQuery框架和Javascript設(shè)計一個簡易的客戶端。我們在src/main/weapp目錄下新建立index.html文件,代碼如下:
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
<!DOCTYPE html> < html lang = "en" > < head > [..] < script > var wsocket; var serviceLocation = "ws://0.0.0.0:8080/hascode/chat/"; var $nickName; var $message; var $chatWindow; var room = ''; function onMessageReceived(evt) { //var msg = eval('(' + evt.data + ')'); var msg = JSON.parse(evt.data); // native API var $messageLine = $('< tr >< td class = "received" >' + msg.received + '</ td >< td class = "user label label-info" >' + msg.sender + '</ td >< td class = "message badge" >' + msg.message + '</ td ></ tr >'); $chatWindow.append($messageLine); } function sendMessage() { var msg = '{"message":"' + $message.val() + '", "sender":"' + $nickName.val() + '", "received":""}'; wsocket.send(msg); $message.val('').focus(); } function connectToChatserver() { room = $('#chatroom option:selected').val(); wsocket = new WebSocket(serviceLocation + room); wsocket.onmessage = onMessageReceived; } function leaveRoom() { wsocket.close(); $chatWindow.empty(); $('.chat-wrapper').hide(); $('.chat-signin').show(); $nickName.focus(); } $(document).ready(function() { $nickName = $('#nickname'); $message = $('#message'); $chatWindow = $('#response'); $('.chat-wrapper').hide(); $nickName.focus(); $('#enterRoom').click(function(evt) { evt.preventDefault(); connectToChatserver(); $('.chat-wrapper h2').text('Chat # '+$nickName.val() + "@" + room); $('.chat-signin').hide(); $('.chat-wrapper').show(); $message.focus(); }); $('#do-chat').submit(function(evt) { evt.preventDefault(); sendMessage() }); $('#leave-room').click(function(){ leaveRoom(); }); }); </ script > </ head > < body > < div class = "container chat-signin" > < form class = "form-signin" > < h2 class = "form-signin-heading" >Chat sign in</ h2 > < label for = "nickname" >Nickname</ label > < input type = "text" class = "input-block-level" placeholder = "Nickname" id = "nickname" > < div class = "btn-group" > < label for = "chatroom" >Chatroom</ label > < select size = "1" id = "chatroom" > < option >arduino</ option > < option >java</ option > < option >groovy</ option > < option >scala</ option > </ select > </ div > < button class = "btn btn-large btn-primary" type = "submit" id = "enterRoom" >Sign in</ button > </ form > </ div > <!-- /container --> < div class = "container chat-wrapper" > < form id = "do-chat" > < h2 class = "alert alert-success" ></ h2 > < table id = "response" class = "table table-bordered" ></ table > < fieldset > < legend >Enter your message..</ legend > < div class = "controls" > < input type = "text" class = "input-block-level" placeholder = "Your message..." id = "message" style = "height:60px" /> < input type = "submit" class = "btn btn-large btn-block btn-primary" value = "Send message" /> < button class = "btn btn-large btn-block" type = "button" id = "leave-room" >Leave room</ button > </ div > </ fieldset > </ form > </ div > </ body > </ html > |
在上面的代碼中,要注意如下幾點:
在Javascript端要調(diào)用websocket的話,要用如下的方式發(fā)起連接即可:ws://IP:PORT/CONTEXT_PATH/ENDPOINT_URL e.g ws://0.0.0.0:8080/hascode/chat/java
創(chuàng)建一個Websocket連接的方法很簡單,使用的是var wsocket = new WebSocket(‘ws://0.0.0.0:8080/hascode/chat/java');
要獲得來自服務(wù)端返回的信息,只需要在回調(diào)函數(shù)wsocket.onmessage中設(shè)置對應(yīng)的獲取返回信息的方法即可。
發(fā)送一個Websocket消息到服務(wù)端,使用的方法是wsocket.send(),其中可以發(fā)送的消息可以文本或者二進(jìn)制數(shù)據(jù)。
關(guān)閉連接使用的是wsocket.close()。
最后,我們通過mvn package embedded-glassfish:run進(jìn)行代碼的部署,然后就可以看到本文開始部分截圖的效果。
以上就是用JavaEE7、Websockets和GlassFish4實現(xiàn)的聊天室,希望對大家的學(xué)習(xí)有所幫助。