一般客户端和服务端交互是由客户端发起一个请求,服务端回答响应。但有时候服务端需要主动的推送数据,比如视频、弹幕、新闻实时刷新等,这时候就用到了服务器推送技术。

1.Ajax短轮询

Ajax短轮询就是前端通过ajax不断向服务端发送请求,这种方式最简单但是性能最低,尤其在服务端未使用netty等高性能框架下。
客户端代码样例:
function showTime(){
…//发送请求

setInterval(showTime, 1000);

2.Ajax长轮询

长轮询是短轮询的变种,服务器使用我在之前的文章里面介绍的DeferredResult,服务器实现异步处理请求,增大服务器响应并发。有兴趣的可以看下之前的文章。

3.SSE

SSE(Server-Sent Events)全称是服务器发送事件。我们知道HTTP协议无法做到服务器主动推送信息,但是SSE协议中由服务器向客户端声明接下来要发送的是流信息,即数据会不断地发送过来。这时客户端不会关闭连接,会一直等着服务器发过来的新的数据流。
SSE基于HTTP协议,目前浏览器基本都支持(除了IE/Edge)。
SSE协议规范
服务器向浏览器发送的数据,必须是UTF-8编码并且具有如下的HTTP头信息:

  • Content-Type: text/event-stream
  • Cache-Control: no-cache
  • Connection: keep-alive

每次响应的信息由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式:
[field]: value\n。
field有四种类型的值:

  • data:数据内容
  • event:信息类型
  • id:数据标识符,浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。
  • retry:最大间隔时间

此外,有冒号开头的行表示注释。通常服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。
一个JSON数据的示例:

data: {\n
data: "context": "aaa",\n
data: }\n\n

SSE相对WebSocket优势:

  • SSE基于HTTP协议,现有的服务器软件都支持。WebSocket是一个独立协议。
  • SSE使用简单,WebSocket协议相对复杂。
  • SSE默认支持断线重连,WebSocket需要自己实现。
  • SSE支持自定义发送的消息类型。

缺点:
WebSocket更强大和灵活。因为它是全双工通道,可以双向通信;SSE是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。
前端核心代码:

if(!!window.EventSource){//判断浏览器支持度
        //拿到sse的对象
        var source = new EventSource('needPrice');
        //接收到服务器的消息
        source.onmessage=function (e) {
            var dataObj=e.data;
           ....//业务处理
        };

        source.onopen=function (e) {
        };

        source.onerror=function () {
        };

    }else{
        $("#hint").html("您的浏览器不支持SSE!");
    }

服务端核心代码:

public void push(HttpServletResponse response){
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("utf-8");
        Random r = new Random();
        int sendCount = 0;
        try {
            PrintWriter pw = response.getWriter();
            while(true){
                if(pw.checkError()){
                    return;
                }
                Thread.sleep(1000);
                //字符串拼接
                StringBuilder sb = new StringBuilder("");
                sb//.append("retry:2000\n")
                        .append("data:")
                        .append((r.nextInt(1000)+50)+",")
                        .append((r.nextInt(800)+100)+",")
                        .append((r.nextInt(2000)+150)+",")
                        .append((r.nextInt(1500)+100)+",")
                        .append("\n\n");

                pw.write(sb.toString());
                pw.flush();
                sendCount++;
                if(sendCount>=100){
                    return;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

4.WebSocket

WebSocket可以让客户端和服务器进行双向通信。WebSocket只需要建立一次连接,就可以一直保持连接状态。相比于轮询方式的不停建立连接显然效率要大大提高。使用WebSockets需要客户端和服务器都支持WebSockets协议,但不是所有浏览器都支持。

4.1 通信规范:

客户端的请求

  • Connection必须设置Upgrade,表示连接升级。
  • Upgrade字段必须设置Websocket,表示升级到Websocket协议。
  • Sec-WebSocket-Key是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。 Sec-WebSocket-Key加上一个特殊字符串计算SHA-1摘要,再进行BASE-64编码,将结果做为Sec-WebSocket-Accept 头的值返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
  • Sec-WebSocket-Version表示支持的Websocket版本。

服务器端
Upgrade:websocket
Connection:Upgrade
告诉客户端即将升级的是Websocket协议。Sec-WebSocket-Accept表示是经过服务器确认,并且加密过后的Sec-WebSocket-Key。
Sec-WebSocket-Protocol表示最终使用的协议。

4.2 代码示例:

(1)STOMP
WebSocket是个规范,在实际的实现中有HTML5规范中的WebSocket API、WebSocket的子协议STOMP。
STOMP(Simple Text Oriented Messaging Protocol):

  • 简单(流)文本定向消息协议
  • STOMP协议专为消息中间件设计,属于消息队列的一种协议, 和AMQP, JMS平级。STOMP协议很多MQ都已支持, 比如RabbitMq, ActiveMq。
  • 生产者(发送消息)、消息代理、消费者(订阅然后收到消息)
  • STOMP是基于帧的协议

客户端代码:

var stompClient = null;

$(function(){
    //连接SockJS的endpoint名称为"endpointMark"
    var socket = new SockJS('/endpointMark');
    stompClient = Stomp.over(socket);//使用STMOP子协议的WebSocket客户端
    stompClient.connect({},function(frame){//连接WebSocket服务端

        console.log('Connected:' + frame);
        //广播接收信息
        stompTopic();

    });
})

//关闭浏览器时关闭连接
window.onunload = function() {
   if(stompClient != null) {
        stompClient.disconnect();
    }
}

//一对多,发起订阅
function stompTopic(){
    //通过stompClient.subscribe订阅目标(destination)发送的消息(广播接收信息)
    stompClient.subscribe('/mass/getResponse',function(response){
        var message=JSON.parse(response.body);
		...//业务处理
    });
}

//群发消息
function sendMassMessage(){
    var postValue={};
    var chatValue=$("#sendChatValue");
    var userName=$("#selectName").val();
    postValue.name=userName;
    postValue.chatValue=chatValue.val();
    stompClient.send("/massRequest",{},JSON.stringify(postValue));
    chatValue.val("");
}

//单独发消息
function sendAloneMessage(){
    var postValue={};
    var chatValue=$("#sendChatValue2");
    var userName=$("#selectName").val();
    var sendToId=$("#selectName2").val();
    var response = $("#alone_div");
    postValue.name=userName;//发送者姓名
    postValue.chatValue=chatValue.val();//聊天内容
    postValue.userId=sendToId;//发送给谁

    stompClient.send("/aloneRequest",{},JSON.stringify(postValue));
    response.append("<div class='user-group'>" +
        "          <div class='user-msg'>" +
        "                <span class='user-reply'>"+chatValue.val()+"</span>" +
        "                <i class='triangle-user'></i>" +
        "          </div>" +userName+
        "     </div>");
    chatValue.val("");
}

//一对一,发起订阅
function stompQueue(){

    var userId=$("#selectName").val();
    alert("监听:"+userId)
    //通过stompClient.subscribe订阅目标(destination)发送的消息(队列接收信息)
    stompClient.subscribe('/queue/' + userId + '/alone',
        function(response){
        var message=JSON.parse(response.body);
       ...//业务处理
    });
}

前端页面核心代码:

<body>
<div>
    <div style="float:left;width:47%">
        <p>请选择你是谁:
        <select id="selectName" onchange="stompQueue();">
            <option value="1">请选择</option>
            <option value="Mark">Mark</option>
            <option value="James">James</option>
            <option value="Lison">Lison</option>
            <option value="Peter">Peter</option>
            <option value="King">King</option>
        </select>
        </p>
        <div class="chatWindow">
            <p style="color:darkgrey">群聊:</p>
            <section id="chatRecord1" class="chatRecord">
                <div id="mass_div" class="mobile-page">

                </div>
            </section>
            <section class="sendWindow">
                <textarea name="sendChatValue" id="sendChatValue" class="sendChatValue"></textarea>
                <input type="button" name="sendMessage" id="sendMassMessage" class="sendMessage" onclick="sendMassMessage()" value="发送">
            </section>
        </div>
    </div>


    <div style="float:right; width:47%">
        <p>请选择你要发给谁:
        <select id="selectName2">
            <option value="1">请选择</option>
            <option value="A">A</option>
            <option value="B">B</option>
        </select>
        </p>
        <div class="chatWindow">

            <p style="color:darkgrey">单聊:</p>
            <section id="chatRecord2" class="chatRecord">
                <div id="alone_div"  class="mobile-page">

                </div>
            </section>
            <section class="sendWindow">
                <textarea name="sendChatValue2" id="sendChatValue2" class="sendChatValue"></textarea>
                <input type="button" name="sendMessage" id="sendAloneMessage" class="sendMessage" onclick="sendAloneMessage()" value="发送">
            </section>
        </div>
    </div>
</div>

<!-- 独立JS -->
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
<script th:src="@{wechat_room.js}"></script>
</body>

服务端核心代码:

@Configuration
/*开启使用Stomp协议来传输基于消息broker的消息
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        /*注册STOMP协议的节点(endpoint),并映射指定的url,
        * 添加一个访问端点“/endpointMark”,客户端打开双通道时需要的url,
        * 允许所有的域名跨域访问,指定使用SockJS协议。*/
        registry.addEndpoint("/endpointMark")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        /*配置一个消息代理
        * mass 负责群聊
        * queue 单聊*/
        registry.enableSimpleBroker(
                "/mass","/queue");

        //一对一的用户,请求发到/queue
        registry.setUserDestinationPrefix("/queue");
    }

}
@Controller
public class StompController {

    @Autowired
    private SimpMessagingTemplate template;/*Spring实现的一个发送模板类*/

    /*消息群发,接受发送至自massRequest的请求*/
    @MessageMapping("/massRequest")
    @SendTo("/mass/getResponse")
    //SendTo 发送至 Broker 下的指定订阅路径mass ,
    // Broker再根据getResponse发送消息到订阅了/mass/getResponse的用户处
    public ChatRoomResponse mass(ChatRoomRequest chatRoomRequest){
        response.setName(chatRoomRequest.getName());
        response.setChatValue(chatRoomRequest.getChatValue());
        return response;
    }

    /*单独聊天,接受发送至自aloneRequest的请求*/
    @MessageMapping("/aloneRequest")
    //@SendToUser
    public ChatRoomResponse alone(ChatRoomRequest chatRoomRequest){
        ChatRoomResponse response=new ChatRoomResponse();
        response.setName(chatRoomRequest.getName());
        response.setChatValue(chatRoomRequest.getChatValue());
        //会发送到订阅了 /user/{用户的id}/alone 的用户处
        this.template.convertAndSendToUser(chatRoomRequest.getUserId()
                +"","/alone",response);
        return response;
    }
}

(2)websocket
前端核心代码:

var socket;
    if (typeof (WebSocket) == "undefined") {
        console.log("遗憾:您的浏览器不支持WebSocket");
    } else {
        console.log("恭喜:您的浏览器支持WebSocket");

        //实现化WebSocket对象
        //指定要连接的服务器地址与端口建立连接
        //ws对应http、wss对应https。
        socket = new WebSocket("ws://localhost:8080/ws/asset");
        //连接打开事件
        socket.onopen = function() {
            socket.send("消息内容");
        };
        //收到消息事件
        socket.onmessage = function(msg) {
         
        };
        //连接关闭事件
        socket.onclose = function() {
        };
        //发生了错误事件
        socket.onerror = function() {
        }

        //窗口关闭时,关闭连接
        window.unload=function() {
            socket.close();
        };
    }

后端核心代码:

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
@ServerEndpoint(value = "/ws/asset")
@Component
public class WebSocketServer {

    private static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
    private static final AtomicInteger OnlineCount = new AtomicInteger(0);
    // concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
    private static CopyOnWriteArraySet<Session> SessionSet
            = new CopyOnWriteArraySet<Session>();


    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        SessionSet.add(session);
        int cnt = OnlineCount.incrementAndGet(); // 在线数加1
        SendMessage(session, "连接成功");
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) {
        SessionSet.remove(session);
        int cnt = OnlineCount.decrementAndGet();
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message
     *            客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        SendMessage(session, "收到消息,消息内容:"+message);

    }

    /**
     * 出现错误
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    /**
     * 发送消息,实践表明,每次浏览器刷新,session会发生变化。
     * @param session
     * @param message
     */
    public static void SendMessage(Session session, String message) {
        try {
            session.getBasicRemote()
                    .sendText(String.format("%s (From Server,Session ID=%s)",
                            message,session.getId()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 群发消息
     * @param message
     * @throws IOException
     */
    public static void BroadCastInfo(String message) throws IOException {
        for (Session session : SessionSet) {
            if(session.isOpen()){
                SendMessage(session, message);
            }
        }
    }

    /**
     * 指定Session发送消息
     * @param sessionId
     * @param message
     * @throws IOException
     */
    public static void SendMessage(String sessionId,String message)
            throws IOException {
        Session session = null;
        for (Session s : SessionSet) {
            if(s.getId().equals(sessionId)){
                session = s;
                break;
            }
        }
        if(session!=null){
            SendMessage(session, message);
        }
        else{
        }
    }

}