WebSocket

Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。

简单的举个例子吧,用目前应用比较广泛的PHP生命周期来解释。

HTTP的生命周期通过 Request 来界定,也就是一个 Request 一个 Response,那么在 HTTP1.0 中,这次HTTP请求就结束了。

在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起

首先Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

告诉服务器我发送的是WebSocket协议

然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

需要理解一点,在使用WebSocket协议前,需要先使用HTTP协议用于构建最初的握手。这依赖于一个机制——建立HTTP,请求协议升级(或叫协议转换)。当服务器同意后,它会响应HTTP状态码101,表示同意切换协议。假设通过TCP套接字成功握手,HTTP协议升级请求通过,那么客户端和服务器端都可以彼此互发消息。

其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

广播式

服务端有消息时,会将消息发送给所有连接了当前endpoint的浏览器

@Configuration
@EnableWebSocketMessageBroker 
// 开启使用STOMP协议来传输基于(MessageBroker)的消息  controller可以使用@MessageMapping(类似于RequestMapping)
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{

    @Override    //注册STOMP协议的 节点映射制定的url,使用SockJS协议
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint").withSockJS();
    }
    
    @Override//配置消息代理
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
    }


}

控制器

@Controller
public class WSController {

    @MessageMapping("/welcome")
    @SendTo("/topic/getResponse") // 当服务段有消息时会对订阅了@sendTo的浏览器发送消息
    public Response say(Message msg) throws InterruptedException {
        Thread.sleep(3000);
        return new Response("Welcome" + msg.getName()+ "!");
    }
}


@SendTo 等同于

@Autowired
private SimpMessagingTemplate messagingTemplate;

messagingTemplate.convertAndSend("",对象)

 

客户端

<!DOCTYPE html>
<html xmlns:th="http//www.thymeleaf.org">
<head>
<meta charset="UTF-8"></meta>
<title>Insertsssssssssssssss title here</title>
</head>
<body onload="disconnect()">
<noscript><h2 style="color:#ff0000">貌似不支持websocket</h2></noscript>
<div>
    <div>
        <button id="connect" onclick="connect()">连接</button>
        <button id="disconnect" disabled="disabled" onclick="disconnect()">断开连接</button>
    </div>
    <div id="conversationDiv">
        <label>输入你的名s字ss</label>
        <input type="text" id="name"/>
        <button id="sendName" onclick="sendName()">发送</button>
        <p id="response"></p>
    </div>
</div>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript">
    var stompClient = null;
    
    function setConnected(connected) {
        $("#connect").disabled = connected;
        $("#disconnect").disabled = connected;
        $("#conversationDiv")[0].style.visibility = connected?'visible':'hidden';
        $('#response').html();
    }
    
    function connect() {
        var socket = new SockJS('/endpoint');  //连接SockJS的endpoint
        stompClient = Stomp.over(socket);  //使用STOMP子协议的WebSocket客户
        stompClient.connect({}, function(frame) {  //连接WebSocket服务端
            setConnected(true);
            console.log('Connected:'+ frame);
            stompClient.subscribe('/topic/getResponse', function(response) {
                showResponse(JSON.parse(response.body).responseMessage);
            });
        });
    }
    
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log("Disconnected");
    }
    
    function sendName() {
        var name = $('#name').val();
        stompClient.send("/welcome",{},JSON.stringify({'name':name}));
    }
    
    function showResponse(message) {
        var response = $("#response");
        response.html(message);
    }
</script>
</body>
</html>

预期的效果是:当一个浏览器发送一个消息到服务端时,其他注册的浏览器也能接受到从服务端发送来的这个消息

但是广播不能解决由谁发送由谁接受的问题,下面就来解决这个问题

点对点式

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/","/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login")
            .defaultSuccessUrl("/chat")
            .permitAll()
            .and()
            .logout()
            .permitAll();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("aaa").password("123").roles("USER")
            .and()
            .withUser("bbb").password("123").roles("USER");
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/static/**");
    }
}
Security配置
同上 在WebSocketConfig 注册endpoint  和增加消息代理
@Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    @MessageMapping("/chat")
    public void handleChat(Principal principal, Message msg) {
        if (principal.getName().equals("aaa")) {

            messagingTemplate.convertAndSendToUser("bbb",
                    "/queue/notifications", principal.getName() + "-send:"
                            + msg.getName());
        } else {
            messagingTemplate.convertAndSendToUser("aaa",
                    "/queue/notifications", principal.getName() + "-send:"
                            + msg.getName());
        }
    }
<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8" />
<head>
    <title>Home</title>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<p>
    聊天室
</p>

<form id="wiselyForm">
    <textarea rows="4" cols="60" name="text"></textarea>
    <input type="submit"/>
</form>

<script th:inline="javascript">
    $('#wiselyForm').submit(function(e){
        e.preventDefault();
        var text = $('#wiselyForm').find('textarea[name="text"]').val();
        sendSpittle(text);
    });
    //链接endpoint名称为 "/endpointChat" 的endpoint。
    var sock = new SockJS("/endpointChat");
    var stomp = Stomp.over(sock);
    stomp.connect('guest', 'guest', function(frame) {

        /**
 订阅了/user/queue/notifications 发送的消息,这里雨在控制器的 convertAndSendToUser 定义的地址保持一致,

         *  这里多用了一个/user,并且这个user 是必须的,使用user 才会发送消息到指定的用户。

         *  */
        stomp.subscribe("/user/queue/notifications", handleNotification);
    });

    function handleNotification(message) {
        $('#output').append("<b>Received: " + message.body + "</b><br/>")
    }

    function sendSpittle(text) {
        stomp.send("/chat", {}, JSON.stringify({ 'name': text }));//3
    }
    $('#stop').click(function() {sock.close()});
</script>

<div id="output"></div>
</body>
</html>

 对于群聊, 可以加上监听器,监听STOMP注册的用户

 *STOMP 监听类   用于session的注册
 */
public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent>{

    @Autowired
    SocketSessionRegistry webAgentSessionRegistry;
    
    @Override
    public void onApplicationEvent(SessionConnectEvent event) {
        StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
        
        String agentId = sha.getNativeHeader("login").get(0);
        String sessionId = sha.getSessionId();
        webAgentSessionRegistry.registerSessionId(agentId, sessionId);
    }

}
遍历发送
@GetMapping(value = "/msg/sendcommuser") public @ResponseBody OutMessage SendToCommUserMessage(HttpServletRequest request){ List<String> keys=webAgentSessionRegistry.getAllSessionIds().entrySet() .stream().map(Map.Entry::getKey) .collect(Collectors.toList()); Date date=new Date(); keys.forEach(x->{ String sessionId=webAgentSessionRegistry.getSessionIds(x).stream().findFirst().get().toString(); template.convertAndSendToUser(sessionId,"/topic/greetings",new OutMessage("commmsg:allsend, " + "send comm" +date.getTime()+ "!"),createHeaders(sessionId)); }); return new OutMessage("sendcommuser, " + new Date() + "!"); }

 

 

STOMP协议分析

STOMP协议与HTTP协议很相似,它基于TCP协议,使用了以下命令:

CONNECT
SEND
SUBSCRIBE
UNSUBSCRIBE
BEGIN
COMMIT
ABORT
ACK
NACK
DISCONNECT

STOMP的客户端和服务器之间的通信是通过“帧”(Frame)实现的,每个帧由多“行”(Line)组成。
第一行包含了命令,然后紧跟键值对形式的Header内容。
第二行必须是空行。
第三行开始就是Body内容,末尾都以空字符结尾。
STOMP的客户端和服务器之间的通信是通过MESSAGE帧、RECEIPT帧或ERROR帧实现的,它们的格式相似。

应用场景

在Web应用中,客户端和服务器端需要以较高频率和较低延迟来交换事件时,适合用WebSocket。因此WebSocket适合财经、游戏、协作等应用场景。

而只有在低延迟和高频消息通信的场景下,选用WebSocket协议才是非常适合的。即使是这样的应用场景,仍然存在是选择WebSocket通信呢?又或者是选择REST HTTP通信呢?
答案是会根据应用程序的需求而定。但是,也可能同时使用这两种技术,把需要频繁交换的数据放到WebSocket中实现,而把REST API作为过程性的业务的实现技术。另外,当REST API的调用中需要把某个信息广播给多个客户端是,也可以通过WebSocket连接来实现。

 

 

对于一些其他的访问方式可以看下

长轮询适合浏览器的Chat聊天、股票行情显示、股票状态更新、体育直播的结果显示等。当然,不是所有的例子都是对延迟很敏感的,但它们的需求都比较相似。
在标准的HTTP请求响应语义中,浏览器发起请求,服务器发送一个响应,这意味着在浏览器发起新请求前,服务器不能发送新信息给客户端浏览器。有几种解决方法,包括:传统的轮询、长轮询、HTTP流、WebSocket协议等。

1、传统的轮询

浏览器保持发送请求,检查服务器是否有新信息返回,服务器对于每次请求均应立即响应。这适合的场景下,轮询可以设定为合理的时间间隔。例如,邮件客户端可以每隔10分钟检查服务器是否有新邮件。传统的轮询的优点是简单且工作可靠。然而,其缺点是效率不高。如果需要尽快获得新信息,那么轮询频率就必须非常高。

2、长轮询

浏览器不断发送请求,但是服务器不予以响应,一直到服务器有了新信息才响应客户端。从客户端的角度看它和传统的轮询相同。但从服务器端的角度来看它与传统的轮询相比,减少了服务器端的开销。
那么响应应该保持Open多久呢?浏览器通常对此时间的设置是5分钟,而网络中介(比如代理)对此时间设置的更短。因此,即使服务器端没有新消息,客户端也应该定期发起一个新长轮询请求。IEFT文件建议这个时间间隔在30秒~120秒之间,而实际使用取决于你的网络情况。
IEFT文件: http://tools.ietf.org/html/rfc6202

长轮询可以极大地减少需要低延迟的接收信息更新请求的数量,特别是新信息在无规律的时间间隔变得可用时。但是,如果信息更新的越频繁,那么整个方案就越像传统的轮询。

3、HTTP流

浏览器向服务器发出请求,服务器要发送信息时就会响应。但是它与长轮询不同,服务器需保持响应是Open的,有更新时就会响应客户端。该方法去除了轮询的需要,而且偏离了典型的HTTP请求/响应的语义。例如,客户端和服务器需要协商如何解释响应流,这样客户端会知道哪一个更新信息结束了,哪一个更新信息开始了。但是,网络中介可以缓存响应流,阻挠此方法的意图。这就是为什么长轮询更为常用。

4、WebSocket协议

浏览器发送一个HTTP请求到服务器,请求切换到WebSocket协议,服务器响应,确认升级协议到WebSocket。此后,浏览器和服务器可以在TCP套接字上双向发送数据帧。

WebSocket协议被设计用于取代需要轮询,特别是适用于需要在服务器和浏览器之间频繁交换数据的场景。在HTTP协议上完成初始握手,以确保WebSocket请求可以穿透防火墙。

WebSockets双向交换的数据有两种类型,文本信息或二进制信息。这使得它与RESTful HTTP方法有显著不同。事实上,还有一些其它协议,比如XMPP,AMQP,STOMP等,目前仍在广泛使用。

WebSocket协议已经被IETF组织进行了标准化,而WebSocket API规范也由W3C标准完成了制订。在Java领域也制订了JSR-356规范以支持WebSocket协议。像Jetty、Tomcat这样的Servlet容器也实现了对WebSocket协议的支持。

5、长连接(Persistent Connection)

HTTP Persistent Connection,即HTTP长连接,也叫HTTP Keep-alive或HTTP Connection Reuse。其思想是使用单个的TCP连接来发送和接收多个HTTP请求/响应,而不是为每个请求/响应都建立一个新连接。新发布的HTTP /2协议就使用了这种思想,并进一步允许在单个连接上多路复用多个并发的请求/响应。
而早期的长连接技术只是要求在客户端与服务器之间创建和保持稳定可靠的连接。早期由于浏览器技术发展较缓慢,没有为这种机制的实现提供很好的支持。早期通常的做法是在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据。

6、Pushlet

在这种技术中,服务器端利用了HTTP长连接的优点,使得响应总是Open的,即服务器不会终止响应,有效地让浏览器可以在初始页面加载后继续加载其它内容。随后服务器端可以周期性的发送JavaScript代码片段来更新页面的内容,从而达到推动能力。通过使用这种技术,客户端不需要Java Applet或其它插件才能保持与服务器的连接Open;客户端会对服务器推送的新事件自动通知。其缺点是服务器端缺少对浏览器端的超时控制,如果浏览器发生超时,必须使用页面刷新。
Pushlets的官方站点: http://www.pushlets.com/ 
Pushlet从2000年发展到2010年,逐渐淡出市场。

7、Comet

Comet是一个Web应用模型,它使用一个HTTP长连接,允许服务器推送数据到浏览器,无需浏览器显式的发起请求。Comet技术是这种技术方式的统称,实际上有多种具体的实现技术,下面以具体的时间轴介绍Comet技术有哪些。
1)早期的Java Applet
2)2000年兴起的Pushlets框架
3)Hidden iframe
4)XMLHttpRequest
5)XMLHttpRequest的长轮询
6)脚本标签长轮询

posted @ 2018-03-20 16:53  jojoworld  阅读(709)  评论(0编辑  收藏  举报