websocket练习

一.介绍

  • 服务器端和浏览器端建立通道,在http协议基础上实现的.
  • 之后的通讯都是通过这个通道进行的,无需在发起请求和响应(这一点与http协议是有区别的,http协议是请求响应模型,每一次与服务器的通讯都是一次和响应)
  • 使用最新的websocket JSR356开发规范, tomcat7是支持该规范的,在tomcat7中的http://localhost:8081/examples/websocket/ 有demo展示

在tomcat7的安装目录usr/local/tomcat/lib下会存在websocket的jar包

websocket-api.jar
tomcat7-websocket.jar

其中tomcat7-websocket.jar是对websocket-api.jar接口的一个实现
注意:tomcat版本必须是7以上

二.webSockAPI (ServerApplicationConfig)

  • 项目启动时会自动启动,类似与ContextListener.
    是webSocket的核心配置类。

三.ServerApplicationConfig 有两个方法

getEndPointConfigs 获取所有以接口方式配置的webSocket类。
getAnnotatedEndpointClasses 扫描src下所有类@ServerEndPoint注解的类。
提示: EndPoint 就指的是 一个webSocket的一个服务端程序

四.实现一个webSocket应用程序,我们要学会几个基本操作

  1. 开启连接
  2. 客户端给服务器端发送数据
  3. 服务器端接收数据
  4. 服务器端给客户端发送数据
  5. 客户端接收数据
  6. @ServerEndPoint("/hello")
    在webSocket的服务程序类上面加上 注解。
    表示的连接路径是: ws://localhost:8080/helloworld/hello
  7. 监听三类基本事件: onopen, onmessage,onclose
    onmessage 是发送数据时的响应事件。(响应客户端ws.send()方法发送的数据)
    onopen是打开连接时的响应事件。
    onclose是关闭连接时的响应事件。(客户端关闭连接时触发)

五.编写demo

两个webSocket服务:简单输出和聊天室
简单输出访问页面:http://localhost:8081/webSocket1/index.jsp
聊天室访问页面:http://localhost:8081/webSocket1/login.jsp
以下具体描述聊天室功能,简单输出功能请查看我的代码库

  • 项目加载三个包
    基础接口包:websocket-api.jar
    tomcat7实现基础包:tomcat7-websocket.jar
    谷歌公司的json和java对象转换包:gson-2.2.4.jar
  • 目录结构如下:

  • SocketConfig.java内容

该文件可以参考tomcat的
/usr/local/tomcat/webapps/examples/WEB-INF/classes/websocket/ExamplesConfig.java

package com.lxf.config;
/**
 * ServerApplicationConfig接口的实现类
 * 在web容器启动的加载并执行,
 */

import java.util.Set;

import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;
//ServerApplicationConfig接口的实现类会在web容器启动的时候自动执行
public class SocketConfig  implements ServerApplicationConfig{
    /**
     * @param scaned 代表是由web服务器启动的时候,扫码本项目中带有@ServerEndpoint注解的类,
     *  将这些类实例放到该接合中,并传入到本方法中(也称依赖注入,被动式编程)
     */
    
    public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scaned) {
        // TODO Auto-generated method stub
        System.out.println("websocket config---------------扫描到有websocket的endPoint服务个数:"+scaned.size());
        //在这里我们可以做一些过滤操作和一些自己的业务逻辑
        
        //最后给服务器返回,服务器端就会注册这些socket server,工前台访问
        return scaned;
    }

    
    public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> arg0) {
        // TODO Auto-generated method stub
        return null;
    }

}
  • Message.java内容

package com.lxf.dataVo;
/**
 * 用来接收用户页面发来的数据的结构
 */
import java.util.Date;
import java.util.List;

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();
        // TODO Auto-generated constructor stub
    }    
}
  • LoginServlet中doPost()内容
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        doGet(request, response);
        request.setCharacterEncoding("utf-8");
        //获取用户名
        String username= request.getParameter("username");
        //将用户名设置为session属性    
        request.getSession().setAttribute("username", username);
        //重定向的聊天jsp页面    
        response.sendRedirect("chat.jsp");
    }

以上完整代码请查看

  • 聊天室ChatSocket内容

该文件可以参考tomcat的
/usr/local/tomcat/webapps/examples/WEB-INF/classes/websocket/chat/ChatAnnotation.java

package com.lxf.socket;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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.google.gson.Gson;
import com.lxf.dataVo.Message;


/**
 * websocket 实现聊天室功能
 * @author lxf
 *
 */
("/chatSocket")
public class ChatSocket {
    //存储每个管道的ChatSocket对象的set集合
    private  static  Set<ChatSocket>  sockets=new HashSet<ChatSocket>();
    /**存储以<用户名,ChatSocket对象>的map,注意:这里必须使用static修饰,因为
        在每个客户端连接过来的时候tomcat都会创建一个ChatSocket实例,当我们使用广播给多个用户发消息
        或者在TestServlet中自己实例化ChatSocket的时候,给指定用户发消息,我们会根据sMap(key)里面的session发送
        如果不指定为static,sMap会为每个对象局部私有,作用域只在本对象中
        使用static修饰,也就意味着所有对象共享sMap
     */
    private  static  Map<String, ChatSocket>  sMap=new HashMap<String, ChatSocket>(); 
    //用来存储所有登录人的用户名
    private  static  List<String>   names=new ArrayList<String>();
    //websocket会话
    private  Session  session;
    //当前用户名
    public String username;
    //Gson用来处理json和java对象转换的谷歌开源类库
    public Gson  gson=new Gson();
    
    /**
     * 当客户端建立连接的时候触发
     * @param session
     */
    
    public  void open(Session  session){
            this.session=session;
            sockets.add(this);
            System.out.println("连接成功:sessionId="+session.getId());
            String  queryString = session.getQueryString();
            System.out.println(queryString);
            this.username = queryString.substring(queryString.indexOf("=")+1);
            names.add(this.username);
            sMap.put(username, this);
     
            Message   message=new Message();
            message.setAlert(this.username+"进入聊天室!!当前聊天室人数"+sockets.size());
            message.setNames(names);
            
            broadcast(sockets, gson.toJson(message) );    
    }
    public Map<String, ChatSocket> getSMap()
    {
        return this.sMap;
    }
    
    /**
     * 行客户端发送消息 ,当客户端触发ws.send()方法时触发
     * @param session
     * @param msg
     */
    
    public  void receive(Session  session,String msg ) throws IOException{
        //this.session.getBasicRemote().sendText(msg+"人数:"+sockets.size());
        
        Message  message=new Message();
        message.setSendMsg(msg);
        message.setFrom(this.username);
        message.setDate(new Date().toLocaleString());
        //指定一个用户发消息
        sendSingleMsg(this.username, message); 
        //给所有用户广播发消息
        //broadcast(sockets, gson.toJson(message));
    }
    
    /**
     * 客户端关闭浏览器时触发
     * @param session
     */
    
    public  void close(Session session){
        sockets.remove(this);
        names.remove(this.username);
        
        Message   message=new Message();
        message.setAlert(this.username+"退出聊天室!!");
        message.setNames(names);
        
        broadcast(sockets, gson.toJson(message));
    }
    
    /**
     * 给指定客户端发消息
     * @param ss
     * @param msg
     * @throws IOException 
     */
    public void sendSingleMsg(String uname, Message message) throws IOException
    {
        if(sMap.get(uname)!=null)
        {
            
            ChatSocket chatSocket = (ChatSocket)sMap.get(uname);
            System.out.println(chatSocket.session.getBasicRemote());
            //chatSocket.session.getBasicRemote().sendText("hello 111");
            chatSocket.session.getBasicRemote().sendText(gson.toJson(message));
        }else{
            System.out.println("获取对应用户信息丢失: " + username);
        }
        
    }
    
    /**
     * 给所有登录用户广播
     * @param ss
     * @param msg
     */
    public void broadcast(Set<ChatSocket>  ss ,String msg ){
        
        for (Iterator iterator = ss.iterator(); iterator.hasNext();) {
            ChatSocket chatSocket = (ChatSocket) iterator.next();
            try {
                System.out.println(chatSocket.session.getBasicRemote());
                chatSocket.session.getBasicRemote().sendText(msg);
                
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 用户登录login.jsp内容
<%@ 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"  />
    </form>

</body>
</html>
  • 聊天室chat.jsp内容

该文件可以参考tomcat的
/usr/local/tomcat/webapps/examples/websocket/chat.xhtml

<%@ 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 url="ws://localhost:8081/webSocket1/chatSocket?username=${sessionScope.username}";
    
    window.onload=connect;
    function connect(){
         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);
            
            if(result.alert!=undefined){
                $("#content").append(result.alert+"<br/>");
            }
            
            if(result.names!=undefined){
                $("#userList").html("");
                $(result.names).each(function(){
                    $("#userList").append(this+"<br/>");
                });
            }
            
            if(result.from!=undefined){
                $("#content").append(result.from+" "+result.date+
                        " 说:<br/>"+result.sendMsg+"<br/>");
            }           
         };
    }
    
    function  send(){
        var value= $("#msg").val();
        ws.send(value);
    }
</script>
</head>
<body>

    <h3>欢迎 ${sessionScope.username } 使用本系统!!</h3>

    <div  id="content"  style="
        border: 1px solid black; width: 400px; height: 300px;
        float: left;
    "  ></div>
    <div  id="userList"  style="
        border: 1px solid black; width: 100px; height: 300px;
        float:left;
    "  ></div>

    <div  style="clear: both;" >
        <input id="msg"  /><button  onclick="send();"  >send</button>
    </div>
</body>
</html>
  • 以上完成后,启动tomcat,启动的过程中会在控制台中看到:
websocket config---------------扫描到有websocket的endPoint服务个数:2

以上是SocketConfig.java的getAnnotatedEndpointClasses方法执行的,因为我的项目中有简单输出和聊天室两个webSocket服务,所以websocket的endPoint服务个数:2

  • 访问用户登录页面
http://localhost:8081/webSocket1/login.jsp
  • 登录后跳转到
http://localhost:8081/webSocket1/chat.jsp

进行聊天

  • 此时可以多开几个浏览器测试

六.总结

  • 配置好服务端后每个客户端浏览器都可以 Chat.socket = new WebSocket(host);这种方式与服务端生成管道,进行通讯;
  • tomcat服务器启动的时候会扫码所有带注解@ServerEndpoint("/chatSocket")的类,并将其发在ServerApplicationConfig的实现类中的scaned的set入参中
    @Override
    public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scaned) {
        // TODO Auto-generated method stub
        System.out.println("websocket config---------------扫描到有websocket的endPoint服务个数:"+scaned.size());
        //在这里我们可以做一些过滤操作和一些自己的业务逻辑
        
        //最后给服务器返回,服务器端就会注册这些socket server,工前台访问
        return scaned;
    }
  • 以上这个类负责过滤一些必要的信息,然后必须返回 return scaned,tomcat启动返回的scaned集合中的webSocket服务(也就相当于在web容器中有对应的webSocket实例),供客户端连接;
  • 注意:webSocket是多个实例的,每个通道一个实例;
  • 所以我们如果想在servlet中或者是java类中自己指定用户发消息,必须将每个通道实例放在webSocket类中有static修饰的Map中,集合key=用户标识,value=该通道的实例,如下:
private  static  Map<String, ChatSocket>  sMap=new HashMap<String, ChatSocket>(); 
  • 如果想在servlet中自行给指定用户发送消息可以进行如下操作:(注意:指定的用户必须提前通过客户端浏览器发起连接)TestServlet.java
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        //response.getWriter().append("Served at: ").append(request.getContextPath());
        ChatSocket chat = new ChatSocket();
        Map<String, ChatSocket> sMap = chat.getSMap();
        Message  message=new Message();
        message.setSendMsg("我是自己的servlet给liangxifeng发送的信息,别人是收不到!");
        message.setFrom("servlet");
        message.setDate(new Date().toLocaleString());
        sMap.get("liangxifeng").sendSingleMsg("liangxifeng", message);
        response.getWriter().append("Served at: ").append(request.getContextPath());
    }

以上完整代码请查看

  • 访问:http://localhost:8081/webSocket1/TestServlet,
    就可以单独给用户liangxifeng发送消息了,而不经过客户端浏览器.