1. 程式人生 > >如何用WebSocket實現一個簡單的聊天室以及單聊功能

如何用WebSocket實現一個簡單的聊天室以及單聊功能

百度百科中這樣定義WebSocket:WebSocket協議是基於TCP的一種新的網路協議。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊——允許伺服器主動傳送資訊給客戶端。簡單的說,WebSocket協議之前,雙工通訊是通過多個http連結來實現,這導致了效率低下,而WebSocket解決了這個問題。

1.1 思考: 傳統web的請求和響應模式中, 我們如何實現實時資訊傳輸, 如何實現伺服器反推資料?
    在瀏覽器中通過http僅能實現單向的通訊,comet可以一定程度上模擬雙向通訊,但效率較低,並需要伺服器有較好的支援; flash中的socket和xmlsocket可以實現真正的雙向通訊,通過 flex ajax bridge,可以在javascript中使用這兩項功能.
    可以預見,如果websocket一旦在瀏覽器中得到實現,將會替代上面兩項技術,得到廣泛的使用.面對這種狀況,HTML5定義了WebSocket協議,能更好的節省伺服器資源和頻寬並達到實時通訊。在JavaEE7中也實現了WebSocket協議。

1.2 WebSocket的目標:**打破傳統的web請求響應模型, 實現管道式的實時通訊。開啟一個瀏覽器和伺服器的通訊通道,持續連線! 伺服器給瀏覽器推送資料 非常方便!web的實時訊息通訊: 聊天,股票,遊戲,監控等等。**
1.3  軟體需求
        (1). 安裝jdk7 或更高版本
        (2). 下載tomcat7   兩者保持一致(32、64位)

2.1 實現一個webSocket應用程式,我們要學會幾個基本操作。
        (1).  開啟連線
        (2).  客戶端給伺服器端傳送資料
        (3
). 伺服器端接收資料 (4). 伺服器端給客戶端傳送資料 (5). 客戶端接收資料 (6). 監聽三類基本事件: onopen,onmessage,onclose 提示: onmessage 是傳送資料時的響應事件。onopen是開啟連線時的響應事件。onclose是關閉連線時的響應事件。

3.1 接下來正式實現聊天室功能,首先寫一個登入頁面

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding
="utf-8"%> <!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> <script type="text/javascript" src="jquery-1.4.4.min.js"></script> </head> <body> <form name="ff" action="LoginServlet" method="post" > 使用者名稱:<input name="username" /><br/> <input type="submit" value="登入"/> </form> </body> </html>

3.2 在web.xml配置LoginServlet

<servlet>
    <description></description>
    <display-name>LoginServlet</display-name>
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>com.bjsxt.servlet.LoginServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/LoginServlet</url-pattern>
  </servlet-mapping>

3.3 新增LoginServlet,用於處理登入請求以及跳轉到聊天室介面

package com.bjsxt.servlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginServlet extends HttpServlet {

    public void doGet(HttpServletRequest request,
              HttpServletResponse response)throws IOException,ServletException{

    }


    public void doPost(HttpServletRequest request,
              HttpServletResponse response)throws IOException,ServletException{

        String username = request.getParameter("username");
        System.out.println("doPost當前登入使用者為"+username);
        request.getSession().setAttribute("username",username);
        //這裡只是簡單地模擬登入,登陸之後直接跳轉到聊天頁面
        response.sendRedirect("chat.jsp");
    }
}

3.4 新增聊天介面

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<!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>
<script type="text/javascript" src="jquery-1.4.4.min.js"></script>
<script type="text/javascript">
    var ws;
    var userName = ${sessionScope.username};
    //通過URL請求服務端(chat為專案名稱)
    var url = "ws://localhost:8080/chat/chatSocket?username=${sessionScope.username}";

    //進入聊天頁面就是一個通訊管道
    window.onload = function() {
        if ('WebSocket' in window) {
            ws = new WebSocket(url);
        } else if ('MozWebSocket' in window) {
            ws = new MozWebSocket(url);
        } else {
            alert('WebSocket is not supported by this browser.');
            return;
        }

        //監聽伺服器傳送過來的所有資訊
        ws.onmessage = function(event) {
            eval("var result=" + event.data);

            //如果後臺發過來的alert不為空就顯示出來
            if (result.alert != undefined) {
                $("#content").append(result.alert + "<br/>");
            }

            //如果使用者列表不為空就顯示
            if (result.names != undefined) {
                //重新整理使用者列表之前清空一下列表,免得會重複,因為後臺只是單純的新增
                $("#userList").html("");
                $(result.names).each(
                        function() {
                            $("#userList").append(
                                    "<input  type=checkbox  value='"+this+"'/>"
                                            + this + "<br/>");
                        });
            }

            //將使用者名稱字和當前時間以及傳送的資訊顯示在頁面上
            if (result.from != undefined) {
                $("#content").append(
                        result.from + " " + result.date + " 說:<br/>"
                                + result.sendMsg + "<br/>");
            }

        };
    };

    //將訊息傳送給後臺伺服器
    function send() {
        //拿到需要單聊的使用者名稱
        //alert("當前登入使用者為"+userName);
        var ss = $("#userList :checked");
        //alert("群聊還是私聊"+ss.size());
        var to = $('#userList :checked').val();
        if (to == userName) {
            alert("你不能給自己傳送訊息啊");
            return;
        }
        //根據勾選的人數確定是群聊還是單聊
        var value = $("#msg").val();
        //alert("訊息內容為"+value);
        var object = null;
        if (ss.size() == 0) {
            object = {
                msg : value,
                type : 1, //1 廣播 2單聊    
            };
        } else {
            object = {
                to : to,
                msg : value,
                type : 2, //1 廣播 2單聊    
            };
        }
        //將object轉成json字串傳送給服務端
        var json = JSON.stringify(object);
        //alert("str="+json);
        ws.send(json);
        //訊息傳送後將訊息欄清空
        $("#msg").val("");
    }
</script>
</head>
<body>

    <h3>歡迎 ${sessionScope.username }使用本聊天系統!!</h3>

    <div id="content"
        style="border: 1px solid black; width: 400px; height: 300px; float: left; color: #7f3f00;"></div>
    <div id="userList"
        style="border: 1px solid black; width: 120px; height: 300px; float: left; color: #00ff00;"></div>

    <div style="clear: both;" style="color:#00ff00">
        <input id="msg" />
        <button onclick="send();">傳送訊息</button>
    </div>
</body>
</html>

3.5 新增啟動類

package com.bjsxt.init;

import java.util.Set;

import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;
/**
 * 專案啟動時會自動啟動,類似與ContextListener.
 * 是webSocket的核心配置類。
 * @author xiongzichao
 *
 */
public class ServerConfig implements ServerApplicationConfig {

    //掃描src下所有類@ServerEndPoint註解的類。
    @Override
    public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scan) {
        System.out.println("掃描到"+scan.size()+"個服務端程式");
        return scan;
    }

    //獲取所有以介面方式配置的webSocket類。
    @Override
    public Set<ServerEndpointConfig> getEndpointConfigs(
            Set<Class<? extends Endpoint>> point) {
        System.out.println("實現EndPoint介面的類數量:"+point.size());
        return null;
    }

}

3.6 新增客戶端傳送給服務端訊息實體

package com.bjsxt.vo;
/**
 * 客戶端傳送給服務端訊息實體
 * @author xiongzichao
 *
 */
public class ContentVo {

    private String to;
    private String msg;
    private Integer type;
    public String getTo() {
        return to;
    }
    public void setTo(String to) {
        this.to = to;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public Integer getType() {
        return type;
    }
    public void setType(Integer type) {
        this.type = type;
    }

}

3.7 新增服務端傳送給客戶端訊息實體

package com.bjsxt.vo;

import java.util.Date;
import java.util.List;

/**
 * 服務端傳送給客戶端訊息實體
 * @author xiongzichao
 *
 */
public class Message {

    private  String  alert;   //

    private  List<String>  names;

    private  String  sendMsg;

    private String  from;

    private String  date;


    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public String getSendMsg() {
        return sendMsg;
    }

    public void setSendMsg(String sendMsg) {
        this.sendMsg = sendMsg;
    }

    public String getFrom() {
        return from;
    }

    public void setFrom(String from) {
        this.from = from;
    }

    public String getAlert() {
        return alert;
    }

    public void setAlert(String alert) {
        this.alert = alert;
    }

    public List<String> getNames() {
        return names;
    }

    public void setNames(List<String> names) {
        this.names = names;
    }

    public Message() {
        super();
    }
}

3.8 新增服務端程式

package com.bjsxt.server;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import com.bjsxt.vo.ContentVo;
import com.bjsxt.vo.Message;
import com.google.gson.Gson;

/**
 * 總通訊管道
 * @author xiongzichao
 *
 */
@ServerEndpoint("/chatSocket")
public class ChatSocket {

    //定義一個全域性變數集合sockets,使用者存放每個登入使用者的通訊管道
    private  static  Set<ChatSocket>  sockets=new HashSet<ChatSocket>();
    //定義一個全域性變數Session,用於存放登入使用者的使用者名稱
    private  Session  session;
    //定義一個全域性變數map,key為使用者名稱,該使用者對應的session為value
    private  static  Map<String, Session>  map=new HashMap<String, Session>();
    //定義一個數組,用於存放所有的登入使用者,顯示在聊天頁面的使用者列表欄中
    private  static  List<String>names=new ArrayList<String>();
    private String username;
    private Gson  gson=new Gson();

    /*
     * 監聽使用者登入
     */
    @OnOpen
    public void open(Session session){
        this.session = session;
        //將當前連線上的使用者session資訊全部存到scokets中
        sockets.add(this);
        //拿到URL路徑後面所有的引數資訊
        String queryString = session.getQueryString();
        System.out.println();
        //擷取=後面的引數資訊(使用者名稱),將引數資訊賦值給全域性的使用者名稱
        this.username = queryString.substring(queryString.indexOf("=")+1);
        //每登入一個使用者,就將該使用者名稱存入到names陣列中,用於重新整理好友列表
        names.add(this.username);
        //將當前登入使用者以及對應的session存入到map中
        this.map.put(this.username, this.session);
        System.out.println("使用者"+this.username+"進入聊天室");
        Message message = new Message();
        message.setAlert("使用者"+this.username+"進入聊天室");
        //將當前所有登入使用者存入到message中,用於廣播發送到聊天頁面
        message.setNames(names);
        //將聊天資訊廣播給所有通訊管道(sockets)
        broadcast(sockets, gson.toJson(message) );
    }

    /*
     * 退出登入
     */
    @OnClose
    public void close(Session session){
        //移除退出登入使用者的通訊管道
        sockets.remove(this);
        //將使用者名稱從names中剔除,用於重新整理好友列表
        names.remove(this.username);
        Message message = new Message();
        System.out.println("使用者"+this.username+"退出聊天室");
        message.setAlert(this.username+"退出當前聊天室!!!");
        //重新整理好友列表
        message.setNames(names);
        broadcast(sockets, gson.toJson(message));
    }

    /*
     * 接收客戶端傳送過來的訊息,然後判斷是廣播還是單聊
     */
    @OnMessage
    public void receive(Session  session,String msg) throws IOException{
        //將客戶端訊息轉成json物件
        ContentVo vo = gson.fromJson(msg, ContentVo.class);
        //如果是群聊,就像訊息廣播給所有人
        if(vo.getType()==1){
            Message message = new Message();
            message.setDate(new Date().toLocaleString());
            message.setFrom(this.username);
            message.setSendMsg(vo.getMsg());
            broadcast(sockets, gson.toJson(message));
        }else{
            Message message = new Message();
            message.setDate(new Date().toLocaleString());
            message.setFrom(this.username);
            message.setAlert(vo.getMsg());
            message.setSendMsg("<font color=red>正在私聊你:</font>"+vo.getMsg());
            String to  = vo.getTo();
            //根據單聊物件的名稱拿到要單聊物件的Session
            Session to_session = this.map.get(to);
            //如果是單聊,就將訊息傳送給對方
            to_session.getBasicRemote().sendText(gson.toJson(message));
        }
    }

    /*
     * 廣播訊息
     */
    public void broadcast(Set<ChatSocket>sockets ,String msg){
        //遍歷當前所有的連線管道,將通知資訊傳送給每一個管道
        for(ChatSocket socket : sockets){
            try {
                //通過session傳送資訊
                socket.session.getBasicRemote().sendText(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4.0 實現介面 
具體實現介面如圖

 

注意

    這裡一共需要三個jar包,分別是WebSocket-api.jar這個定義webSocket應用程式開發介面!tomcat7-webSocket.jar tomcat伺服器對於webSocket介面的實現!!還有一個gson.jar用於序列化實體類資訊。前面兩個jar包在tomcat7的lib目錄裡面可以找到。