天天看點

websocket實作聊天室應用,包括文字和圖檔上傳

談websocket之前,我們可以先複習一下web通信的一些概念。

推(PUSH)和拉(PULL):

推和拉是Web通信的兩種方式,從定義上來說它們最主要的差別在于方向性,推是伺服器主動向用戶端推送消息,拉則是用戶端主動發請求向伺服器擷取資料。

優缺點及适用場景:

1、拉模式是最常用、最成熟的一種方式,它依托了非常成熟的HTTP協定,有大量的工具類和庫可以使用,請求/響應模式幾乎每個程式員都知道,出現問題也容易找到相應的解決方案。

2、拉模式的限制特别少,任何一款浏覽器,隻要能稱為浏覽器的,不管多古老都支援拉,推的話像websocket是H5才支援,IE9都不支援,不僅是用戶端,服務端也是有限制的,要較新的伺服器才支援websocket,如Tomcat 7.0.5,WebLogic 12c ,IIS 7.0...以上的版本才“真正”支援websocket。

3、對于一般請求,拉模式占用的資源也少,比如請求個清單或者明細,伺服器處理完并傳回資料後,連接配接就斷開了,簡潔明了不占用連接配接資源。是以拉适合大多數一般的請求場景,但對于背景資料變化需要實時更新的,比如股票市場、網頁遊戲和聊天實等程式,拉就不适合了。

長輪詢和長連接配接

在websocket出現之前,要實作伺服器推,有兩種方式,分别是長輪詢和長連接配接。

長輪詢:

輪詢(Polling):用戶端(浏覽器)設一個定時器或死循環不停地向服務端發送查詢請求,輪詢的結果可能是服務端有更新就傳回新的資料,也可能時服務端根本就沒更新,得到一個空的資訊。但是不管結果如何,用戶端在下一個時間節點都會進行新的一輪請求。    

短輪詢: 服務端對每次請求立即傳回結果。

長輪詢: 長輪詢是對短輪詢的改善,服務端接收到請求後先檢視資料,如果有更新立即傳回,如果沒有則把請求挂起一段時間,在這段時間内有更新就傳回,如果一直沒更新直到逾時,伺服器會主動向用戶端發送本次輪詢無新資訊的正常響應,并斷開連接配接,這種響應也被稱之為“心跳”。再有就是如果網絡故障導緻輪詢的意外中斷,此時浏覽器将收到錯誤資訊。

由上可知,對于用戶端來說,不管是長輪詢還是短輪詢,用戶端的動作都是一樣的,就是死循環不停的去請求。即一種輪詢方式是否為長輪詢,是根據服務端的處理方式來決定的,與用戶端沒有關系。

長連接配接:

HTTP長連接配接:HTTP分為長連接配接和短連接配接,它們外在表現形式的差別就是請求/響應頭中是否有Connection:keep-alive。内在的差別就是TCP連接配接是否會被複用,即多個HTTP請求是否可以複用同一個TCP連接配接,可以即長連接配接,HTTP長連接配接可以節省了很多TCP連接配接建立和斷開的消耗。比如請求一個網頁,現在的網頁大多不僅僅隻有HTML,一般還包含了CSS、JS、圖檔等一系列資源,如果用短連接配接就需要建立幾個甚至幾十個TCP連接配接,長連接配接複用一個就好了,HTTP1.1以後預設都是長連接配接了。

實作伺服器推的長連接配接:基于iframe流的方式,其實作原理主要是在頁面隐藏一個iframe(display:none),前台設定iframe的src屬性為對一個長連接配接的請求,伺服器傳回對頁面函數的調用。使用iframe流方式的話浏覽器會一直轉圈(進度條一直沒完)。這種iframe流的長連接配接實作伺服器推的模式比較少見,與ajax長輪詢稍有差別。一般ajax輪詢使用XMLHttpRequest 對象發送請求,在 XMLHttpRequest 的 readystate 為 4(即資料傳輸結束)時使用回調函數對傳回的資料進行處理。而iframe服務端傳回的是JS腳本,用戶端得到該腳本後就自動執行。簡單的說,ajax傳回的是資料而後再使用回調函數處理該資料,iframe傳回的是JS腳本(腳本内可能包含資料),浏覽器自動執行。

如:

iframe流實作方式:

服務端代碼:

[java]  view plain  copy

  1. while(true){  
  2.    while(!condition){<span style="font-family:Arial, Helvetica, sans-serif;">//模拟阻塞,等待條件成熟,推送資訊</span>  
  3. <span style="white-space:pre;"> </span>Thread.sleep(1000);<span style="font-family:Arial, Helvetica, sans-serif;">   </span>  
  4.    }  
  5.    num++;  
  6.    PrintWriter pw = resp.getWriter();    
  7.    pw.println("<script>parent.changeNumber("+num+")</script>");  
  8.    pw.flush();   
  9. }  

可以看到,服務端使用的是PrintWriter,等于說是将 [java]  view plain  copy

  1. <script>parent.changeNumber("+num+")</script>  

寫在浏覽器裡面

用戶端代碼:

[html]  view plain  copy

  1. <iframe id="polling" src="IframeCometServlet" style="display:none;"></iframe>   
  2. function changeNumber(num) {  //服務端傳回的字元串調用的方法  
  3.     document.getElementById("number").innerHTML = num;    
  4. }  

注意changeNumber(num)這個函數,與服務端傳回的一緻(服務端傳回的為什麼要帶一個parent.字首?因為是iframe接收啊,而changeNumber是定義在外面的)。

ajax長輪詢方式:

服務端代碼:

[java]  view plain  copy

  1. @RequestMapping(value = "/list")  
  2.    public String list(HttpServletRequest request) {  
  3.        while(condition){//等待條件成熟  
  4.           //do something  
  5.    break;  
  6. }  
  7. return "something";  
  8.    }  

用戶端代碼(該代碼要在循環裡面才能達到輪詢效果):

[html]  view plain  copy

  1. $.ajax({  
  2.         type: "POST",  
  3.         url: "/web/goods/list",    
  4.         data: JSON.stringify({data:'hello'}),  
  5.         contentType: "application/json; charset=utf-8",  
  6.         dataType: "json",  
  7.         success: function (result) {  
  8.           //do something  
  9.         }  
  10.   });  
websocket實作聊天室應用,包括文字和圖檔上傳
websocket實作聊天室應用,包括文字和圖檔上傳

通過長輪詢和長連接配接可以看到,它們都并不是真正的伺服器推,隻是變相的拉而已,這是對HTTP體系的妥協。

由此引出真正的伺服器推技術:WebSocket

WebSocket

WebSocket是什麼?

WebSocket和HTTP一樣,是基于TCP的應用層通信協定,通過維持一條持久的連接配接以實作浏覽器與伺服器全雙工通信,是随着H5一起出來的。

WebSocket和HTTP有什麼關系麼?

Websocket其實是一個新協定,除了最開始借用了HTTP來完成握手,後面跟HTTP基本沒有關系了。

websocket實作聊天室應用,包括文字和圖檔上傳

傳統的HTTP要不斷的建立、關閉連接配接(這裡的關閉并不是說關閉TCP連接配接,而是關閉HTTP連接配接),而且由于HTTP是無狀态性的,每次都要重新傳輸identity info(鑒别資訊),來告訴服務端你是誰,當然,一般隻是帶個JSESSIONID之類的,服務端通過這個就知道你的其他資訊了。WebSocket 是類似 Socket 的 TCP 長連接配接的通訊模式,一旦 WebSocket 連接配接建立後,後續資料都以幀序列的形式傳輸。在斷掉 WebSocket 連接配接前,都不需要用戶端和服務端重新發起連接配接請求,即隻需一次HTTP握手,整個通訊過程是建立在一次連接配接/狀态中,也就避免了HTTP的無狀态性。

WebSocket握手

WebSocket的握手過程就是建立WebSocket連接配接的過程,握手是借用了HTTP的。

總體步驟大緻如下:

Step 1: 建立TCP連接配接(這一步是一切的基礎,HTTP也一樣)。

Step 2: 浏覽器借助一個WebSocket 用戶端對象,連接配接類似 ws://yourdomain:port/path或wss://yourdomain:port/path的URL,WebSocket 用戶端對象會自動解析并識别為要建立WebSocket 連接配接,進而發送HTTP Get 請求,并為請求自動添加一些供WebSocket使用的HTTP Headers字段。注意,連接配接頭變成了ws,而不是一般的http,這是協定的意思,ws代表websocket。

Step 3: Server收到HTTP請求後,會把Step 1的TCP連接配接的應用層協定從HTTP轉變為WebSocket。至此,HTTP部分就退出舞台了,WebSocket開始接管一切。

WebSocket握手封包(HTTP)分析:

請求

[html]  view plain  copy

  1. GET /chat HTTP/1.1  
  2. Host: 127.0.0.1:8080  
  3. Upgrade: websocket  
  4. Connection: Upgrade  
  5. Sec-WebSocket-Key: JkTukkPc4Rha+nIkbYhEkQ==  
  6. Sec-WebSocket-Version: 13  
  7. Origin: http://localhost:8080  

響應

[html]  view plain  copy

  1. HTTP/1.1 101 Switching Protocols  
  2. Upgrade: websocket  
  3. Connection: Upgrade  
  4. Sec-WebSocket-Accept: RD69G0BS8RPH/GbY6rBsZI75pjk=  
  5. Server:Apache-Coyote/1.1  
  6. Date:Sat, 17 Jun 2017 15:35:41 GMT  

以上封包和一般的HTTP有很多不同的地方:

1、請求和響應都有Upgrade: websocket和Connection: Upgrade,這是協定轉換的意思,也就是請求時告訴伺服器,本次連接配接需要的是websocket通信協定,而不是一般的HTTP,響應也帶有該響應頭表示轉換成功。

一般響應成功的響應頭是 HTTP/1.1 200 OK、HTTP/1.1 404 NOT FOUND等,此處是HTTP/1.1 101 Switching Protocols,同樣101也是HTTP的一種響應狀态,表示協定轉換成功。

2、Sec-WebSocket-Key: JkTukkPc4Rha+nIkbYhEkQ==和Sec-WebSocket-Accept: RD69G0BS8RPH/GbY6rBsZI75pjk=

這是websocket握手的檢驗,也即測試伺服器是否提供websocket服務,如果提供,Sec-WebSocket-Key和Sec-WebSocket-Accept必然是有對應關系的,具體的關系如下:

Sec-WebSocket-Key拼接上258EAFA5-E914-47DA-95CA-C5AB0DC85B11(這串magic string是寫在協定裡面的,所有的websocket底層實作都要拼接該串)

得到對拼接結果

JkTukkPc4Rha+nIkbYhEkQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

使用SHA-1(160數位)進行哈希操作,對哈希後的結果base64 進行編碼,就可以得到結果

RD69G0BS8RPH/GbY6rBsZI75pjk=

以上操作不僅僅是服務端要做,用戶端也同樣做了該操作,然後将自己計算得到的結果和伺服器傳來的Sec-WebSocket-Accept作比較,如果相等則沒問題,握手成功,如果不一緻,則握手失敗。

3、Origin用于防止未授權的跨域腳本攻擊,伺服器可以從Origin決定是否接受該WebSocket連接配接,所有來源于浏覽器的請求都會帶上該請求頭。如,已授權站點www.mydomain.com,并且産生cookie,使用者沒關閉授權站點的時候點選另外的www.hack.com下面的連結,則該連接配接可以帶着授權的cookie去做很多事。如果有了Origin,并且在服務端setAllowedOrigins("www.mydomain.com")則可以避免。

4、Sec-WebSocket-Version: 13 ,表明了websocket的版本,以前各廠商混戰的時候什麼樣的版本都有,還好現在已經統一了,用13版即可。

代碼以及部署

我的程式部署于tomcat7.0.5,基于Spring 4.3.7,需要使用支援websocket的IE10、Chrome和Firefox等較進階别版本的浏覽器打開。

程式主要分為6個部分:2個配置xml、3個java檔案、1個jsp。

前台主要用H5提供的websocket對象,它存在于頂層對象window下,凡是支援H5的浏覽器都實作了該對象。

背景主要用Spring提供的websocket配置類WebSocketConfigurer、攔截器HttpSessionHandshakeInterceptor和處理器AbstractWebSocketHandler來實作。

如下:

web.xml

[html]  view plain  copy

  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>WebsocketTest</display-name>  
  4.     <servlet>  
  5.         <servlet-name>springmvc</servlet-name>  
  6.         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
  7.         <init-param>  
  8.         <param-name>contextConfigLocation</param-name>  
  9.         <!-- <param-value>classpath:spring-mvc.xml</param-value> --><!-- 類路徑 -->  
  10.         <param-value>/WEB-INF/spring-mvc.xml</param-value> <!-- 相對路徑 -->  
  11.         </init-param>  
  12.     </servlet>  
  13.     <servlet-mapping>  
  14.         <servlet-name>springmvc</servlet-name>  
  15.         <url-pattern>*.sc</url-pattern>  
  16.     </servlet-mapping>  
  17.   <welcome-file-list>  
  18.     <welcome-file>index.html</welcome-file>  
  19.     <welcome-file>index.htm</welcome-file>  
  20.     <welcome-file>index.jsp</welcome-file>  
  21.     <welcome-file>default.html</welcome-file>  
  22.     <welcome-file>default.htm</welcome-file>  
  23.     <welcome-file>default.jsp</welcome-file>  
  24.   </welcome-file-list>  
  25. </web-app>  

spring-mvc.xml

[html]  view plain  copy

  1. <?xml version="1.0" encoding="UTF-8"?>    
  2. <beans xmlns="http://www.springframework.org/schema/beans"    
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"    
  4.     xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p"    
  5.     xmlns:context="http://www.springframework.org/schema/context"    
  6.     xsi:schemaLocation="    
  7.             http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd    
  8.             http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd    
  9.             http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd    
  10.             http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">    
  11.    <context:component-scan base-package="com.websocket"></context:component-scan>    
  12. </beans>    

WebSocketConfig.java

[java]  view plain  copy

  1. package com.websocket;  
  2. import org.springframework.context.annotation.Bean;  
  3. import org.springframework.context.annotation.Configuration;  
  4. import org.springframework.web.socket.config.annotation.EnableWebSocket;  
  5. import org.springframework.web.socket.config.annotation.WebSocketConfigurer;  
  6. import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;  
  7. @Configuration  //配置類    
  8. @EnableWebSocket  //聲明支援websocket    
  9. public class WebSocketConfig implements WebSocketConfigurer{    
  10.     @Override    
  11.     public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {    
  12.     <span style="white-space:pre;">   </span>String[] allowsOrigins={"http://localhost:8080"};  
  13.     <span style="white-space:pre;">   </span>//addHandler注冊和路由的功能,當用戶端發起websocket連接配接,把/path交給對應的handler處理,而不實作具體的業務邏輯,可以了解為收集和任務分發中心。  
  14.     <span style="white-space:pre;">   </span>//setAllowedOrigins(String[] domains),允許指定的域名或IP(含端口号)建立長連接配接,預設隻有本地。如果不限時使用"*"号,如果指定了域名,則必須要以http或https開頭。  
  15.     <span style="white-space:pre;">   </span>//addInterceptors,顧名思義就是為handler添加攔截器,可以在調用handler前後加入自定義的邏輯代碼。  
  16.         registry.addHandler(ChatRoom(), "/chat.sc").setAllowedOrigins("*").addInterceptors(handshakeInterceptor());   
  17.         //允許用戶端使用SockJS    
  18.         //SockJS 是一個浏覽器上運作的 JavaScript 庫,如果浏覽器不支援 WebSocket,該庫可以模拟對 WebSocket 的支援。  
  19.         registry.addHandler(ChatRoom(), "/sockjs/chat.sc").addInterceptors(handshakeInterceptor()).withSockJS();    
  20.     }    
  21.     @Bean    
  22.     public HandshakeInterceptor handshakeInterceptor(){    
  23.         return new HandshakeInterceptor();    
  24.     }    
  25.     @Bean    
  26.     public ChatRoom ChatRoom(){    
  27.         return new ChatRoom();    
  28.     }  
  29. }    

HandshakeInterceptor.java

[java]  view plain  copy

  1. package com.websocket;  
  2. import java.util.Map;  
  3. import java.util.Random;  
  4. import org.springframework.http.server.ServerHttpRequest;  
  5. import org.springframework.http.server.ServerHttpResponse;  
  6. import org.springframework.web.socket.WebSocketHandler;  
  7. import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;  
  8. public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor{    
  9.     @Override    
  10.     public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> attributes) throws Exception {    
  11.         //attributes是session裡面的所有屬性的map表示  
  12.         attributes.put("user", getRandomNickName());  
  13.         return super.beforeHandshake(request, response, handler, attributes);    
  14.     }   
  15.     @Override    
  16.     public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {    
  17.         super.afterHandshake(request, response, wsHandler, ex);    
  18.     }          
  19.     //給每個進來的人(session)随機配置設定個昵稱,這裡沒做控制,是以聊天室内的昵稱可能發生重複  
  20.     public String getRandomNickName(){  
  21.         String[] nickNameArray={"Captain America","Deadpool","Hawkeye","Hulk","Iron Man","Spider Man","Thor","Wolverine","Black Panther","Colossus"};  
  22.         Random random=new Random();  
  23.         return nickNameArray[random.nextInt(10)];  
  24.     }  
  25. }    

ChatRoom.java

[html]  view plain  copy

  1. package com.websocket;  
  2. import java.io.File;  
  3. import java.io.FileInputStream;  
  4. import java.io.FileOutputStream;  
  5. import java.io.IOException;  
  6. import java.nio.ByteBuffer;  
  7. import java.text.SimpleDateFormat;  
  8. import java.util.ArrayList;  
  9. import java.util.Collections;  
  10. import java.util.Date;  
  11. import java.util.List;  
  12. import org.apache.log4j.Logger;  
  13. import org.springframework.web.socket.BinaryMessage;  
  14. import org.springframework.web.socket.CloseStatus;  
  15. import org.springframework.web.socket.TextMessage;  
  16. import org.springframework.web.socket.WebSocketSession;  
  17. import org.springframework.web.socket.handler.AbstractWebSocketHandler;  

[html]  view plain  copy

  1. public class ChatRoom extends AbstractWebSocketHandler{  
  2.     public final static List<WebSocketSession> sessionList = Collections.synchronizedList(new ArrayList<WebSocketSession>());  
  3.     SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");  
  4.     Logger logger = Logger.getLogger(this.getClass());  
  5.     FileOutputStream output;  
  6.     @Override    
  7.     public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {    
  8.         System.out.println("Connection established..."+webSocketSession.getRemoteAddress());    
  9.         System.out.println(webSocketSession.getAttributes().get("user")+" Login");  
  10.         webSocketSession.sendMessage(new TextMessage("I'm "+(webSocketSession.getAttributes().get("user"))));  
  11.         sessionList.add(webSocketSession);  
  12.     }    
  13.     @Override    
  14.     public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus status) throws Exception {    
  15.         System.out.println("Connection closed..."+webSocketSession.getRemoteAddress()+" "+status);   
  16.         System.out.println(webSocketSession.getAttributes().get("user")+" Logout");  
  17.         sessionList.remove(webSocketSession);  
  18.     }  
  19.     @Override  
  20.     public void handleTextMessage(WebSocketSession websocketsession, TextMessage message)  
  21.     {  
  22.         String payload=message.getPayload();  
  23.         String textString;  
  24.         try {  
  25.             if(payload.endsWith(":fileStart")){  
  26.                 output=new FileOutputStream(new File("D:\\images\\"+payload.split(":")[0]));  
  27.             }else if(payload.endsWith(":fileFinishSingle")){  
  28.                 output.close();  
  29.                 String fileName=payload.split(":")[0];  
  30.                 for(WebSocketSession session:sessionList){  
  31.                     if(session.getId().equals(websocketsession.getId())){  
  32.                         textString=" I ("+format.format(new Date())+")<br>";  
  33.                     }else{  
  34.                         textString=websocketsession.getAttributes().get("user")+" ("+format.format(new Date())+")<br>";  
  35.                     }  
  36.                     TextMessage textMessage = new TextMessage(textString);   
  37.                     session.sendMessage(textMessage);   
  38.                     sendPicture(session,fileName);  
  39.                 }  
  40.             }else if(payload.endsWith(":fileFinishWithText")){  
  41.                 output.close();  
  42.                 String fileName=payload.split(":")[0];  
  43.                 for(WebSocketSession session:sessionList){  
  44.                     sendPicture(session,fileName);  
  45.                 }  
  46.             }else{  
  47.                 for(WebSocketSession session: sessionList){  
  48.                     if(session.getId().equals(websocketsession.getId())){  
  49.                         textString=" I ("+format.format(new Date())+")<br>"+payload;  
  50.                     }else{  
  51.                         textString=websocketsession.getAttributes().get("user")+" ("+format.format(new Date())+")<br>"+payload;  
  52.                     }  
  53.                     TextMessage textMessage = new TextMessage(textString);   
  54.                     session.sendMessage(textMessage);   
  55.                 }  
  56.             }  
  57.         } catch (IOException e) {  
  58.             e.printStackTrace();  
  59.         }  
  60.     }  
  61.     @Override  
  62.     public void handleBinaryMessage(WebSocketSession session, BinaryMessage message)  
  63.     {  
  64.         ByteBuffer buffer= message.getPayload();  
  65.         try {  
  66.             output.write(buffer.array());  
  67.         } catch (IOException e) {  
  68.             e.printStackTrace();  
  69.         }  
  70.     }  
  71.     @Override    
  72.     public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {    
  73.         if(webSocketSession.isOpen()){    
  74.             webSocketSession.close();    
  75.         }    
  76.         System.out.println(throwable.toString());    
  77.         System.out.println("WS connection error,close..."+webSocketSession.getRemoteAddress());    
  78.     }    
  79.     @Override    
  80.     public boolean supportsPartialMessages() {    
  81.         return true;    
  82.     }    
  83.     public void sendPicture(WebSocketSession session,String fileName){  
  84.         FileInputStream input;  
  85.         try {  
  86.             File file=new File("D:\\images\\"+fileName);  
  87.             input = new FileInputStream(file);  
  88.             byte bytes[] = new byte[(int) file.length()];   
  89.             input.read(bytes);  
  90.             BinaryMessage byteMessage=new BinaryMessage(bytes);  
  91.             session.sendMessage(byteMessage);  
  92.             input.close();  
  93.         } catch (Exception e) {  
  94.             e.printStackTrace();  
  95.         }  
  96.     }  
  97. }  

chatRoom.jsp

[html]  view plain  copy

  1. <!DOCTYPE html>    
  2. <html>    
  3. <head>    
  4. <meta charset="UTF-8">    
  5. <title>Insert title here</title>    
  6. <script type="text/javascript" src="js/sockjs.min.js"></script>    
  7. <script type="text/javascript" src="js/jquery.js"></script>    
  8. <script type="text/javascript">    
  9.     var url = "127.0.0.1:8080/websocket";    
  10.     var websocket = null;    
  11.     if ('WebSocket' in window) {    
  12.         websocket = new WebSocket("ws://" + url + "/chat.sc");    
  13.     } else {    
  14.         websocket = new SockJS("http://" + url + "/sockjs/chat.sc");    
  15.     }    
  16.     websocket.onopen = onOpen;    
  17.     websocket.onmessage = onMessage;    
  18.     websocket.onerror = onError;    
  19.     websocket.onclose = onClose;    
  20.     function onOpen(openEvent) {    
  21.         document.getElementById("plane").innerHTML = document.getElementById("plane").innerHTML+ "OPEN<br/>";   
  22.     }    
  23.     function onMessage(event) {    
  24.         if(typeof event.data =='string'){  
  25.             var element = document.createElement("p");  
  26.             element.innerHTML=event.data;  
  27.             document.getElementById("plane").appendChild(element);  
  28.         }else{  
  29.             var reader = new FileReader();  
  30.             reader.onload=function(eve){  
  31.                  if(eve.target.readyState==FileReader.DONE)  
  32.                  {  
  33.                     var img = document.createElement("img");  
  34.                     img.src=this.result;  
  35.                     document.getElementById("plane").appendChild(img);  
  36.                  }  
  37.              };  
  38.              reader.readAsDataURL(event.data);  
  39.         }  
  40.     }    
  41.     function onError() {    
  42.     }    
  43.     function onClose(event) {    
  44.         console.log(event.reason)  
  45.         document.getElementById("plane").innerHTML = document.getElementById("plane").innerHTML+ "CLOSE<br/>";    
  46.     }    
  47.     function doSend() {    
  48.         if (websocket.readyState == 1) {  //0-CONNECTING;1-OPEN;2-CLOSING;3-CLOSED  
  49.             var msg = document.getElementById("message").value;  
  50.             if(msg){  
  51.                 websocket.send(msg);   
  52.             }  
  53.             sendFile(msg);  
  54.             document.getElementById("message").value="";  
  55.         } else {    
  56.             alert("connect fail!");    
  57.         }    
  58.     }    
  59.     function sendFile(isWithText){  
  60.         var inputElement = document.getElementById("file");  
  61.         var fileList = inputElement.files;  
  62.         var file=fileList[0];  
  63.         if(!file) return;  
  64.         websocket.send(file.name+":fileStart");  
  65.         var reader = new FileReader();  
  66.         //以二進制形式讀取檔案  
  67.         reader.readAsArrayBuffer(file);  
  68.         //檔案讀取完畢後該函數響應  
  69.         reader.onload = function loaded(evt) {  
  70.             var blob = evt.target.result;  
  71.             //發送二進制表示的檔案  
  72.             websocket.send(blob);  
  73.             if(isWithText){  
  74.                 websocket.send(file.name+":fileFinishWithText");  
  75.             }else{  
  76.                 websocket.send(file.name+":fileFinishSingle");  
  77.             }  
  78.             console.log("finnish");  
  79.         }  
  80.         inputElement.outerHTML=inputElement.outerHTML; //清空<input type="file">的值  
  81.     }  
  82.     function disconnect(){    
  83.         if (websocket != null) {    
  84.             websocket.close();    
  85.             websocket = null;    
  86.         }    
  87.     }    
  88.     function reconnect(){    
  89.         if (websocket != null) {    
  90.             websocket.close();    
  91.             websocket = null;    
  92.         }    
  93.         if ('WebSocket' in window) {    
  94.             websocket = new WebSocket("ws://" + url + "/chat.sc");    
  95.         } else {    
  96.             websocket = new SockJS("http://" + url + "/sockjs/chat.sc");    
  97.         }    
  98.         websocket.onopen = onOpen;    
  99.         websocket.onmessage = onMessage;    
  100.         websocket.onerror = onError;    
  101.         websocket.onclose = onClose;    
  102.     }    
  103.     function clean(){  
  104.         document.getElementById("plane").innerHTML = "";    
  105.     }  
  106. </script>    
  107. </head>    
  108. <body>    
  109.     <div>    
  110.         <button id="disconnect" onclick="disconnect()">disconnect</button>    
  111.         <button id="send" onclick="doSend()">send</button>    
  112.         <button id="reconnect" onclick="reconnect()">reconnect</button>    
  113.         <button id="clean" onclick="clean()">clean</button>   
  114.         <!-- <br>  
  115.         <input type="button" value="sendFile" onclick="sendFile()"/> -->  
  116.         <input type="file" id="file" />  
  117.     </div>    
  118.     <div>    
  119.        <input id="message"  type="text" style="width: 350px"></input>    
  120.     </div>    
  121.     <div id="plane"></div>    
  122. </body>    
  123. </html>    

部署成功後,通路http://localhost:8080/websocket/view/chatRoom.jsp 可以得到效果如下圖所示,分别是聊天室内3個人的聊天頁面:

websocket實作聊天室應用,包括文字和圖檔上傳
websocket實作聊天室應用,包括文字和圖檔上傳
websocket實作聊天室應用,包括文字和圖檔上傳