ANDROID开机启动

当操作系统启动完毕之后会广播一个消息叫ACTION_BOOT_COMPLETED,应用程序可以通过实现BroadcastReceiver接口来监听这个消息,接到消息之后就可以启动程序,一般启动一个后台运行的Service,当然也可以直接启动某个activity.

我们来写个简单的service,以便在接收到系统启动完成的消息之后来启动这个后台执行的service

public class BootStartService extends IntentService {

    public UpdateService(){
        this("app start service");
    }
    public UpdateService(String name) {
        super(name);
    }
 
    @Override
    protected void onHandleIntent(Intent workIntent) {
        //启动
	String arg = workIntent.getStringExtra("arg");
        Log.d("BootStartService", "BootStartService start");
	//...做一些默默无闻的事情
    }
}

实现接口BroadcastReceiver来监听消息

public class BootStartReceiver extends BroadcastReceiver {
    static final String action_boot="android.intent.action.BOOT_COMPLETED";
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(action_boot)){
            Intent mServiceIntent = new Intent(context, BootStartService.class);
	    mServiceIntent.putExtra("arg", "app start args");
	    context.startService(mServiceIntent);
        }
    }
}

那么代码就已经完成了,记得在AndroidManifest.xml中加入下面的声明

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"></uses-permission>
<receiver android:name="cn.fullstacks.blogapp.BootStartReceiver">
	<intent-filter>
	<action android:name="android.intent.action.BOOT_COMPLETED" />
	<category android:name="android.intent.category.HOME" />
	</intent-filter>
</receiver>
<service android:name="cn.fullstacks.blogapp.BootStartService" android:exported="false"/>

重启之后就会看到你要的效果了

总结

开机启动一般是会启动一个后台的程序而不会启动个界面,这样就太霸道了,在系统管理或者一些安全软件里面会有控制开机启动的地方,那么那里面就会看到你的开机启动项了,如果被禁止掉了,你的这个启动消息就不会受到了,这也就是系统级别的消息过滤,如果说必须要开机启动而不想让其他软件阻止的话,那就是把程序写成安全软件了,这就是流氓的开端。

在一些即时通的软件里或者一些需要接受消息的软件,大部分都会做成开机启动,然后启动一个后台服务监听消息,当服务器推送消息过来之后在通知栏里面显示一条消息,这是最常用的场景了。

ANDROID WebSocket+STOMP 1.0版本

Android WebSocket并没有原生的支持,所以那些第三方库如雨后春笋的出现了,那些就不举例了,这里就把一些最简单的开源的东西整合到一起,并且加入了对STOMP的支持,这里命名为cn.fullstacks.websocket:

[wpdm_package id=’230′]

下面来看看怎么使用

WebSocket

List<BasicNameValuePair> extraHeaders = Arrays.asList(
    new BasicNameValuePair("Cookie", "session=abcd");
);

WebSocketClient client = new WebSocketClient(URI.create("wss://irccloud.com"), new WebSocketClient.Handler() {
    @Override
    public void onConnect() {
        Log.d(TAG, "Connected!");
    }

    @Override
    public void onMessage(String message) {
        Log.d(TAG, String.format("Got string message! %s", message));
    }

    @Override
    public void onMessage(byte[] data) {
        Log.d(TAG, String.format("Got binary message! %s", toHexString(data));
    }

    @Override
    public void onDisconnect(int code, String reason) {
        Log.d(TAG, String.format("Disconnected! Code: %d Reason: %s", code, reason));
    }

    @Override
    public void onError(Exception error) {
        Log.e(TAG, "Error!", error);
    }
}, extraHeaders);

client.connect();

连通之后就可以发送消息了

// Later… 
client.send("hello!");
client.send(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF });
client.disconnect();

STOMP

Map<String,String> headersSetup = new HashMap<String,String>();
headersSetup.put("Cookie","session=abc");
Stomp stomp = new Stomp("ws://localhost:8080/wsport", headersSetup, new ListenerWSNetwork() {
	@Override
	public void onState(int state) {
		Log.d("stomp onSate",String.format("state:%n",state));
	}
});
stomp.connect();
//订阅主题
stomp.subscribe(new Subscription("topic/greetings", new ListenerSubscription() {
	@Override
	public void onMessage(Map<String, String> headers, String body) {
		//接收到消息
	}
}));

连通好之后就可以发送消息了

stomp.send("/websocket/sendToTopic", headers,"hello stomp!");
stomp.disconnect();

Spring WebSocket实现消息订阅

WebSocket是底层协议,传输的都是没有没有定义格式的字符流或者字节流,那么在这个这个协议的基础之上的上层协议主要作用是定义传输的数据格式,方便业务需要,那么STOMP协议正是简单的格式定义,灵活的提供了在网页实现了消息的订阅,下面来看看Spring提供的消息模块实现消息订阅的一个简单例子。

创建项目

创建Spring mvc项目,参见之前的文章:spring-mvc简单配置

在pom.xml中加入websocket相关两个包的引用

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-websocket</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-messaging</artifactId>
			<version>${spring.version}</version>
		</dependency>

相应的在spring的配置文件中加上websocket的配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

	 <websocket:message-broker application-destination-prefix="/websocket">
        <websocket:stomp-endpoint path="/wsport" >
            <!-- <websocket:sockjs/> -->
        </websocket:stomp-endpoint>
        <websocket:simple-broker prefix="/topic,/queue" />
    </websocket:message-broker>

</beans>

websocket的配置简单说一下,application-destination-prefix=”/websocket” 这个是配置整个spring websocket服务端controller的访问跟路径即客户端发送消息的路径,这个path=”/wsport”就是服务端提供的订阅根路径了,最后prefix=”/topic,/queue” 是定义可以订阅的主题,订阅根路径加上订阅的主题就是一个完整的路径

这就是所有的配置了,下面看看后台的控制器写法,简单的例子

服务端

@Controller
public class ChatController {

private final SimpMessagingTemplate messagingTemplate;

@Autowired
public ChatController(SimpMessagingTemplate messagingTemplate) {
    this.messagingTemplate = messagingTemplate;
}


    /**
     * 发送消息到某个主题
     */
    @MessageMapping("/sendToTopic")
    public void sendToTopic(HashMap message){
        //发送
        this.messagingTemplate.convertAndSend("/topic/"+message.get("topic"),message.get("msg"));
    }

}

这个控制器的作用就是往某个主题发送消息,客户端传过来主题topic和要发送的消息msg,下面看看客户端的写法

这里客户端自然是使用sockjs , 可以从这里下载https://github.com/sockjs/sockjs-client/tree/master/dist

stomp可以从这里下载https://github.com/jmesnil/stomp-websocket/tree/master/lib , 这个脚本虽然作者不再维护了,但是一般应用也没啥问题。

把上面两个脚本引入到页面上之后,看看如何实现消息订阅和消息的发送

客户端

客户端和服务端握手连接

function connect() {
    stompClient =Stomp.client("ws://localhost:8080/fullstacks/wsport"); //注意这里/fullstacks是启动tomcat的项目名,换上自己的正确路径
    stompClient.connect({}, function(frame) {
        console.log('Connected: ' + frame);

//订阅
        stompClient.subscribe('/topic/greetings', function(greeting){
            console.log(greeting.body);
        });
    });
}

发送消息的方法

function sendToTopic() {
    stompClient.send("/websocket/sendToTopic", null,JSON.stringify({msg: "你好",topic:"greetings"})); //注意这里topic和订阅的地方一定要一致,要不然就收不到消息了啊
}

 

那么客户端就这么多代码,一个简单的订阅消息和发送消息的例子就完成,运行起来,然后发送一下试试看效果:

在调试窗口运行connect()

image

连接成功之后发送消息sendToTopic()

image

 

看看命令行打出来的日志,发送的内容和stomp协议对照一下,一步了然,我们发送的是json格式的数据,那么接收的消息也是一样的,下面接收消息有两行你好,不要紧张,上面是打印消息体的内容,下面那一行是接收到消息俺们自己打印出来的。

Overall

总的来讲,使用spring的东西就是配置配置,然后写一写很简单的代码就能实现很多内容,简单就是美,根据以上的思路完全可以实现属于自己的聊天或者在线会议之类,图片文件传送也是可以的,文件以base64编码传送过去,稍微封装一下就可以了。

当然,这个websocket服务是通用的,客户端是不局限于浏览器了,在移动设备上也是完全可以使用的,后续将会献上这项方面的一些实例。

STOMP协议规范

原文: STOMP Protocol Specification, Version 1.2

摘要

STOMP是一个简单的可互操作的协议, 被用于通过中间服务器在客户端之间进行异步消息传递。它定义了一种在客户端与服务端进行消息传递的文本格式.

STOMP已经被使用了很多年,并且支持很多消息brokers和客户端库。这个规范定义STOMP 1.2协议以及对1.1版本的更新。

发送反馈到stomp-spec@googlegroups.com.

概述

背景

由于需要用脚本语言如Ruby, Python, Perl去连接企业级的消息brokers, STOMP产生了.在这种情况下,STMOP实现了一些简单的操作,比如可靠地发送单一的消息,然后断开或者从目的地消费所有消息。

STOMP是除AMQP开放消息协议之外地另外一个选择, 实现了被用在JMS brokers中特定的有线协议,比如OpenWire. 它仅仅是实现通用消息操作中的一部分,并非想要覆盖全面的消息API.

STOMP目前已经是个成熟的协议,在wire-level方面, 它提供了一些简单的用例,但仍保持其核心设计原则:简单性和互操作性。 ### 协议概述

STOMP是基于frame的协议, 与HTTP的frame相似.一个frame包含一个command,一系列可选的headersbody.STOMP虽然是基于消息但同于也允许传递二进制消息。STMOP的默认消息格式是UTF-8,但是在消息体中同样支持其他格式编码。

STOMP服务器就好像是一系列的目的地, 消息会被发送到这里。STOMP协议把目的地当作不透明的字符串,其语法是服务端具体的实现。 此外STOMP没有定义目的地的交付语义是什么。 交付,或“消息交换”,语义的目的地可以从服务器到服务器,甚至从目的地到目的地。这使得服务器有可创造性的语义,去支持STOMP。

STOMP client的用户代理可以充当两个角色(可能同时): * 作为生产者,通过SENDframe发送消息到server * 作为消费者,发送SUBSCRIBEframe到目的地并且通过MESSAGEframe从server获取消息。

STOMP版本之间的变化

STOMP 1.2 大部分向后兼容1.1. 有两点不兼容的改变: * 用回车加换行符代替只用换行符结束frame * 简化了消息应答,用专用的header

除此之外,STOMP 1.2并没有增加新特性,而是阐述规格中的一些模糊概念,比如: * 重复的frame header条目 * content-lengthcontent-typeheaders的用法 * 必须支持servers STOMP frame * 连接延迟 * 作用域,订阅的唯一,事务的标示符 *RECEIPTframe的含义

设计哲学

简易性,互通性是STOMP主要设计哲学.
STOMP被设计成为轻量级的协议,它很容易用其他语言在client和server实现。这就意味着servers的架构没有太多的约束,以及没有太多的特性比如目的地命名空间,可靠的语法需要去实现。

在这份规格书里面,注意,我们没有明确定义的STOMP 1.2 servers特性。你应该查阅STMOMP servers 文档去获得这些特性的详细描述。

一致性

RFC 2119中详细地解释了MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT,RECOMMENDED, MAY, 和 OPTIONAL这些关键字
为了阻止来自服务端地攻击,保护内存溢出,消除平台限制,限制了不受约束的输入。

规格中一致性的级别适用于STOMP clients and STOMP servers.

STOMP Frames

STOMP是基于帧的协议,它假定底层为一个2-way的可靠流的网络协议(如TCP)。客户端和服务器通信使用STOMP帧流通讯。帧的结构看起来像:

COMMAND
header1:value1
header2:value2

Body^@

帧以command字符串开始,以EOL结束,其中包括可选回车符(13字节),紧接着是换行符(10字节)。command下面是0个或多个<key>:<value>格式的header条目, 每个条目由EOL结束。一个空白行(即额外EOL)表示header结束和body开始。body连接着NULL字节。本文档中的例子将使用^@,在ASCII中用control-@表示,代表NULL字节。NULL字节可以选择跟多个EOLs。欲了解更多关于STOMP帧的详细信息,请参阅Augmented BNF节本文件。

本文档中引用的所有command 和header 名字都是大小写敏感的.

编码方式

commands和headers 都是用UTF-8编码的.在用UTF-8编码的headers中除了CONNECTCONNECTED帧以外,任何的回车符,换行符,colon found(?)都将被转义.

转义的目的在于允许header中的键值包含那些把octets当作值的frame header.

为了向后兼容STOMP 1.0, CONNECTCONNECTED不会转义回车符,换行符,colon found(?)

C风格的字符串转义被用在UTF-8编码的headers中去转义回车符,换行符以及colon found.当解码headers时,必须使用下列转换: * \r (octet 92 and 114) translates to carriage return (octet 13) * \n (octet 92 and 110) translates to line feed (octet 10) * \c (octet 92 and 99) translates to : (octet 58) * \\ (octet 92 and 92) translates to \ (octet 92)

未定义转义序列如\t(octet 92 and 116)必须被视为一个致命的错误。相反,当编码帧头,必须使用逆转变.

The STOMP 1.0 specification included many example frames with padding in the headers and many servers and clients were implemented to trim or pad header values. This causes problems if applications want to send headers that SHOULD not get trimmed. In STOMP 1.2, clients and servers MUST never trim or pad headers with spaces.

Body

只有SEND, MESSAGE, 和ERROR帧有body。所有其他的帧不能有body。

标准header

大多数被用的header都有特殊的含义。

Header content-length

所有的帧可能都包括有content-length的header。它定义了消息体的大小。如果header包含了content-length, 包含空字节的消息体的最大字节数不能超过这个数. 帧仍然需要以空字节结束。

如帧体存在,SEND, MESSAGEERROR帧应该包含content-length.如果帧体包含空字节,那么这个帧必须包括content-length.

Header content-type

如果帧体存在,SEND, MESSAGEERROR帧应该包含content-type帮助接受者去理解帧体.如果设置了content-type, 它的值必须是描述帧体格式的MINE类型.否则,接收者应该认为帧体格式为二进制Blob.

text/开头的MINE类型的默认文本编码是UTF-8. 如果你正在用一个基于MINE类型的不同编码, 你应该添加;charset=<encoding>MINE类型。例如:如果你发送一个UTF-16编码的HTML body, 应该设置text/html;charset=utf-16. ;charset=<encoding>也能添加到任何非text/ MINE类型后去作为说明。UTF-8编码的XML是个很好的例子。它的编码被设置为application/xml;charset=utf-8.

所有STOMP客户端和服务端必须支持UTF-8编码和解码。因此,为了最大限度地使用在异构环境中的互操作性,建议基于文本的内容使用UTF-8编码.

Header receipt

任何除了CONNECT的客户端帧可以为receipt header指定任何值。这会让服务端应答带有RECEIPT的客户端帧的处理过程。

Repeated Header Entries

Since messaging systems can be organized in store and forward topologies, similar to SMTP, a message may traverse several messaging servers before reaching a consumer. A STOMP server MAY ‘update’ header values by either prepending headers to the message or modifying a header in-place in the message.

如果client或者server受到重复的header条目,只有第一个会被用作header条目的值。其他的值仅仅用来维持状态改变,或者被丢弃。

例如,如果client收到:

MESSAGE
foo:World
foo:Hello

^@

foo header的值为World.

大小限制

为了客户端滥用服务端的内存分配,服务端可以设置可分配的内存大小:

  • 单个帧允许帧头的个数
  • header中每一行的最大长度
  • 帧体的大小

如果超出了这些限制,server应该向client发送一个error frame,然后关闭连接.

连接延迟

STOMP servers必须支持client快速地连接server和断开连接。 这意味着server在连接重置前只允许被关闭的连接短时间地延迟.

结果就是,在socket重置前client可能不会收到server发来的最后一个frame(比如ERROR或者RECEIPTframe去应答DISCONNECTframe)

Connecting

STOMP client通过CONNECTframe与server建立流或者TCP连接.

CONNECT
accept-version:1.2
host:stomp.github.org

^@

如果server收到请求,将返回CONNECTEDframe:

CONNECTED
version:1.2

^@

server能拒绝所有的连接请求。server应该响应ERRORframe去说明为什么连接被拒绝然后关闭连接。

CONNECT or STOMP Frame

STOMP servers 处理STOMPframe必须和处理CONNECTframe一样。STOMP1.2 clients应该继续使用CONNECTcommand去向后兼容1.0.

使用STOMPframe的clients只能连接上STOMP1.2 servers(以及一些STOMP1.1 servers),但是好处在于协议探针能够从HTTP连接中区分开STOMP连接。

STOMP 1.2 clients 必须设置以下headers: * accept-version: clients支持的STOMP的版本号。详情见Protocol_Negotiation * host:client希望连接的虚拟主机名字,建议设置已经连接的socket为主机名,或者任何名字。如果headers没有匹配到任何可用的虚拟主机,支持虚拟主机的servers将选择默认的虚拟主机或者拒绝连接。

STOMP 1.2 clients可选择设置以下headers: * login: 用于在server验证的用户id *passcode: 用于在server验证的密码 * heart-beat: 心跳设置

CONNECTED Frame

STOMP 1.2 servers 必须设置以下headers:

  • version: 会话中STOMP版本。详情见Protocol_Negotiation

STOMP 1.2 servers可选择设置以下headers:

  • heart-beat: 心跳设置
  • session: 唯一的会话identifier
  • server: 描述STOMP server信息。它必须包含server-name,可以包含一些注释信息(用空格分开) server-name后面也可以带着可选的版本号.

      server = name ["/" version] *(comment)
    

    例如:

      server:Apache/1.3.9
    
Protocol Negotiation

STOMP1.1 以后的版本,CONNECTframe必须包括accept-versionheader.它的值为clients支持的STOMP版本号,多个版本号用,隔开。如果不存在accept-versionheader,那么表明clients只支持1.0.

在一次会话中将使用双方都支持的最高版本。

例如,如果client发送:

CONNECT
accept-version:1.0,1.1,2.0
host:stomp.github.org

^@

server将返回与客户端同时支持的最高版本。

CONNECTED
version:1.1

^@

如果client和server不支持共同的协议版本,server必须返回如下的ERRORframe,然后断开连接。

ERROR
version:1.2,2.1
content-type:text/plain

Supported protocol versions are 1.2 2.1^@
心跳

心跳被用于去测试底层TCP连接的可用性,确保远端服务处于活动状态。

要使用心跳,每个部分必须声明它能干什么以及想要其他部分干什么. 通过在CONNECTCONNECTEDframe中增加heart-beatheader, 让心跳在会话开始被定义好。heart-beatheader必须包含两个用逗号隔开的正整数。

第一个数字代表发送方能做什么: * 0表示它不能发送心跳 * 否则它是能保证两次心跳的最小毫秒数

第二个数字代表发送方能获得什么: * 0表示它不想接收心跳 * 否则它表示两次心跳期望的毫秒数

heart-beatheader是OPTIONAL的。没有的话会被当作heart-beat:0,0header 处理,意思就是说它不会发送心跳并且不想接收心跳。

heart-beatheader提供了足够的信息去了解每个部分心跳是否可用,发送到哪里,频率的大小.

原始frame像这个样子:

CONNECT
heart-beat:<cx>,<cy>
   
CONNECTED:
heart-beat:<sx>,<sy>

对于client发送server的心跳: * 如果<cx>为0(client不能发送心跳)或者<sy>为0(server不想接收心跳),将不起任何作用。 * 否则心跳频率为MAX(<cx>,<sy>)毫秒数.

相反,<sx><cy>同样是这样的.

关于心跳本身,通过网络连接收到的任何数据表明远端服务是可用的。在给定的指向,如果心跳的频率被期望是<n>毫秒:

  • 发送者必须每<n>毫秒发送新数据。
  • 如果发送者没有真实的STOMP frame,必须发送一个end-of-line (EOL)
  • 如果接受者在规定的时间内没有收到新数据,表明连接已经断开
  • 由于时间误差,接收者应该容错和考虑定义错误的界限

Client Frames

client可以发送下列列表以外的frame,但是STOMP1.2 server会响应ERRORframe,然后关闭连接。

  • SEND
  • SUBSCRIBE
  • UNSUBSCRIBE
  • BEGIN
  • COMMIT
  • ABORT
  • ACK
  • NACK
  • DISCONNECT
SEND

SENDframe发送消息到目的地,它必须包含表示目的地地址的destinationheader.SENDframe body是被发送的消息。例如:

SEND
destination:/queue/a
content-type:text/plain

hello queue a
^@

这个消息被发送到/queue/a.注意STOMP把目的地看作为一个不透明的字符串,没有目的地假设的交互语义.你应该查阅STOMP server文档,搞清楚如何构造目的地名字。

可靠的消息语义是server指定的,依赖备用的目的地的值和其他消息headers,比如事务headers,或者其他server指定的消息headers。

SEND可以添加transactionheader来支持事务处理.

如果body存在,那么SENDframe应该包含一个content-lengthcontent-typeheader

一个应用可以给SENDframe增加任意多个用户定义的headers。 通常用于用户定义的头,让消费者能够根据应用程序定义的报头使用选择订阅帧过滤消息。 被定义的用户必须通过MESSAGEframe传送。

如果server不能无故成功处理SENDframe,那么server必须向client发送ERRORframe然后关闭连接。

SUBSCRIBE

SUBSCRIBEframe用于注册给定的目的地.和SENDframe一样,SUBSCRIBEframe需要包含destinationheader表明client想要订阅目的地。 被订阅的目的地收到的任何消息将通过MESSAGEframe发送给client。 ackheader控制着确认模式。

例子:

SUBSCRIBE
id:0
destination:/queue/foo
ack:client

^@

如果server不能成功创建此次订阅,那么server将返回ERRORframe然后关闭连接。

STOMP服务器可能支持额外的服务器特定的头文件,来自定义有关订阅传递语义.

SUBSCRIBE id Header

一个单连接可以对应多个开放的servers订阅,所以必须包含idheader去唯一标示这个订阅.这个idframe可以把此次订阅与接下来的MESSAGEframe和UNSUBSCRIBEframe联系起来。

在相同的连接中,不同的订阅必须拥有不同订阅id。

SUBSCRIBE ack Header

ackheader可用的值有auto, client,client-individual, 默认为auto.

ackauto时,client收到server发来的消息后不需要回复ACKframe.server假定消息发出去后client就已经收到。这种确认方式可以减少消息传输的次数.

ackclient时, client必须发送ACkframe给servers, 让它处理消息.如果在client发送ACKframe之前连接断开了,那么server将假设消息没有被处理,可能会再次发送消息给另外的客户端。client发送的ACKframe被当作时积累的确认。这就意味这种确认方式会去操作ACKframe指定的消息和订阅的所有消息

由于client不能处理某些消息,所以client应该发送NACKframe去告诉server它不能消费这些消息。

当ack模式是client-individual,确认工作就像客户端确认模式(除了由客户端发送的ACKNACK帧)不会被累计。这意味着,后续ACK, NACK消息帧,也不能影响前面的消息的确认。

UNSUBSCRIBE

UNSUBSCRIBEframe被用于去移除已经存在订阅。一旦订阅被删除后,STOMP连接将不再会收到来自订阅发出的消息。

一个单连接可以对应多个开放的server订阅,所以必须包含idheader去唯一标示被删除的订阅.这个header中的id必须匹配已存在订阅.

例如:

UNSUBSCRIBE
id:0

^@
ACK

ACKclientclient-individual去确认订阅消息的消费.只有通过ACK确认过后,订阅的消息才算是被消费.

ACKframe必须包含一个idheader去匹配将要被确认的ackheader中的id.可以选择地指定transactionheader表明消息确认应该是命名事务地一部分。

ACK
id:12345
transaction:tx1

^@
NACK

NACKACK相反地作用。它地作用是告诉server client不想消费这个消息。server然后发送这个消息给另外的client,丢弃它或者把它放在无效的消息队列中。这种准确的行为是server特定的。

NACK有相同的ACKheaders: id(必选)和transaction(可选)。

NACK适用于单个消息(订阅的ack模式为client-individual), 或者那些还没有被ACK'edNACK'ed的消息(订阅模式ack为client).

BEGIN

BEGIN用于事务的开始。事务被用于发送和确认消息,被发送和被确认的消息在事务过程中会被自动处理。

BEGIN
transaction:tx1

^@

transactionheader是必填的,并且事务id将被用于在SEND, COMMIT, ABORT, ACK, and NACK frames去绑定命名的事务.在相同的连接中,不同事务必须用不同的id

如果client发送DISCONNECTframe或者TCP连接失败,任何已开始但没有提交的事务默认都会被中断.

COMMIT

COMMIT用于在过程中提交事务.

COMMIT
transaction:tx1

^@

transactionheader是必填的并且必须指定将要提交的事务的id.

ABORT

ABORT用于在过程中回滚事务.

ABORT
transaction:tx1

^@

transactionheader是必填的并且必须指定将要提交的事务的id.

DISCONNECT

client能在任何时候断开server的连接,但是不能保证已经发送的frame已经到达了server。为了让这一切显得不那么暴力,client确保所有已经发送的frames已经被server收到,client应该做以下3点:

  1. 发送带有receiptheader的DISCONNECTframe

     DISCONNECT
     receipt:77
     ^@
    
  2. 等待带有RECEIPTframe的响应

     RECEIPT
     receipt-id:77
     ^@
    
  3. 关闭socket

注意,如果server过早地关闭socket,client将不会收到期望地RECEIPTframe.见Connection_Lingering

client发送DISCONNECTframe后不必要在发送任何frame.

Server Frames

server偶尔也会发送frame给客户端(除了连接最初的CONNECTEDframe).

这些frames为: * MESSAGE * RECEIPT * ERROR

MESSAGE

MESSAGEframe用于将订阅的消息发送给client.

MESSAGEframe必须包含destinationheader表明信息要到达的目的地。如果消息已经用STOMP发送,那么destinationheader应该和SENDframe中的一样。

MESSAGEframe必须包含带有唯一标识的message-idheader和带有将接收消息的订阅的idsubscriptionheader.

如果从订阅收到的消息需要明确作出确认(client或者client-individual模式),那么MESSAGEframe必须包含带有任何值的ackheader.这个header将把消息和后来的ACK,NACKframe关联起来。

下面这个frame body包含了消息的内容:

MESSAGE
subscription:0
message-id:007
destination:/queue/a
content-type:text/plain

hello queue a^@

如果frame body包含内容的话,MESSAGEframe应该包含content-lengthheader和content-typeheader.

除了那些server指定的headers, 消息被发送到目的地时,MESSAGEframe同样应该包括所有用户定义的headers.查阅有关文档,找出那些server指定添加到messages的headers.

RECEIPT

server成功处理请求带有receipt的client frame后, 将发送RECEIPTframe到client.RECEIPTframe必须包含receipt-id header,它的值为client frame中receiptheader的值。

RECEIPT
receipt-id:message-12345

^@

RECEIPTframe是作为server处理的client frame后的应答. 既然STOMP是基于流的,那么receipt也是对server已经收到所有的frames的累积确认。但是,以前的frames可能并没有被完全处理。如果clients断开连接,以前接收到的frames应该继续被server处理。

ERROR

如果出错的话,server将发送ERRORframe.这种情况下,server还应该断开连接。查看下一章connection lingering ERRORframe应该包含带有简单错误信息的messageheader,或者Body包含详细的描述信息,也可能没有。

ERROR
receipt-id:message-12345
content-type:text/plain
content-length:171
message: malformed frame received

The message:
-----
MESSAGE
destined:/queue/a
receipt:message-12345

Hello queue a!
-----
Did not contain a destination header, which is REQUIRED
for message propagation.
^@

如果错误关联到了具体的某个client frame,那么server应该增加额外的headers去识别引起错误的frame。例如,如果frame包含receiptheader,ERRORframe应该设置receipt-idheader的值为引起错误的frame的receiptheader的值。

如果frame body包含内容的话,ERRORframe应该包含content-lengthheader和content-typeheader

Frames and Headers

除了上述标准headers之外(content-length, content-type, receipt),下面列出了所有规范中定义的headers:

  • CONNECT or STOMP
    • REQUIRED: accept-version, host
    • OPTIONAL: login, passcode, heart-beat
  • CONNECTED
    • REQUIRED: version
    • OPTIONAL: session, server, heart-beat
  • SEND
    • REQUIRED: destination
    • OPTIONAL: transaction
  • SUBSCRIBE
    • REQUIRED: destination, id
    • OPTIONAL: ack
  • UNSUBSCRIBE
    • REQUIRED: id
    • OPTIONAL: none
  • ACK or NACK
    • REQUIRED: id
    • OPTIONAL: transaction
  • BEGIN or COMMIT or ABORT
    • REQUIRED: transaction
    • OPTIONAL: none
  • DISCONNECT
    • REQUIRED: none
    • OPTIONAL: receipt
  • MESSAGE
    • REQUIRED: destination, message-id, subscription
    • OPTIONAL: ack
  • RECEIPT
    • REQUIRED: receipt-id
    • OPTIONAL: none
  • ERROR
    • REQUIRED: none
    • OPTIONAL: message

除此之外,SENDMESSAGEframes可能包含任意的用户定义的headers ,它们会成为carried message的一部分。同样,ERRORframe应该包含额外的headers来识别引起错误的frame。

最终,STOMP servers可以用额外的headers去访问持久化或者有效期特性.查阅server文档获得更多信息。

Augmented BNF

A STOMP session can be more formally described using the Backus-Naur Form (BNF) grammar used in HTTP/1.1 RFC 2616.

NULL                = <US-ASCII null (octet 0)>
LF                  = <US-ASCII line feed (aka newline) (octet 10)>
CR                  = <US-ASCII carriage return (octet 13)>
EOL                 = [CR] LF 
OCTET               = <any 8-bit sequence of data>

frame-stream        = 1*frame

frame               = command EOL
                      *( header EOL )
                      EOL
                      *OCTET
                      NULL
                      *( EOL )

command             = client-command | server-command

client-command      = "SEND"
                      | "SUBSCRIBE"
                      | "UNSUBSCRIBE"
                      | "BEGIN"
                      | "COMMIT"
                      | "ABORT"
                      | "ACK"
                      | "NACK"
                      | "DISCONNECT"
                      | "CONNECT"
                      | "STOMP"

server-command      = "CONNECTED"
                      | "MESSAGE"
                      | "RECEIPT"
                      | "ERROR"

header              = header-name ":" header-value
header-name         = 1*<any OCTET except CR or LF or ":">
header-value        = *<any OCTET except CR or LF or ":">
License

This specification is licensed under the Creative Commons Attribution v3.0 license.

WebSocket简介

网络上搜集的一个对Websocket的非官方理解,通俗易懂,借用一下

一、WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)
首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充可以通过这样一张图理解

有交集,但是并不是全部。
另外Html5是指的一系列新的API,或者说新规范,新技术。Http协议本身只有1.0和1.1,而且跟Html本身没有直接关系。。
通俗来说,你可以用HTTP协议传输非Html数据,就是这样=。=
再简单来说,层级不一样
二、Websocket是什么样的协议,具体有什么优点
首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
简单的举个例子吧,用目前应用比较广泛的PHP生命周期来解释。
1) HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么HTTP1.0,这次HTTP请求就结束了。
在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。
教练,你BB了这么多,跟Websocket有什么关系呢?
_(:з」∠)_好吧,我正准备说Websocket呢。。
首先Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。
在握手阶段是一样的
——-以下涉及专业技术内容,不想看的可以跳过lol:,或者只看加黑内容——–
首先我们来看个典型的Websocket握手(借用Wikipedia的。。)

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

熟悉HTTP的童鞋可能发现了,这段类似HTTP协议的握手请求中,多了几个东西。
我会顺便讲解下作用。

Upgrade: websocket
Connection: Upgrade

这个就是Websocket的核心了,告诉Apache、Nginx等服务器:注意啦,我发起的是Websocket协议,快点帮我找到对应的助理处理~不是那个老土的HTTP。

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

首先,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠窝,我要验证尼是不是真的是Websocket助理。
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。。不过现在还好,已经定下来啦~大家都使用的一个东西~ 脱水:服务员,我要的是13岁的噢→_→
然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!

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

这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~

Upgrade: websocket
Connection: Upgrade

依然是固定的,告诉客户端即将升级的是Websocket协议,而不是mozillasocket,lurnarsocket或者shitsocket。
然后,Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。服务器:好啦好啦,知道啦,给你看我的ID CARD来证明行了吧。。
后面的,Sec-WebSocket-Protocol 则是表示最终使用的协议。
至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行了。
具体的协议就不在这阐述了。
——————技术解析部分完毕——————

你TMD又BBB了这么久,那到底Websocket有什么鬼用,http long poll,或者ajax轮询不都可以实现实时信息传递么。

好好好,年轻人,那我们来讲一讲Websocket有什么用。
来给你吃点胡(苏)萝(丹)卜(红)

三、Websocket的作用
在讲Websocket之前,我就顺带着讲下 long poll 和 ajax轮询 的原理。
首先是 ajax轮询 ,ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。
场景再现:
客户端:啦啦啦,有没有新信息(Request)
服务端:没有(Response)
客户端:啦啦啦,有没有新信息(Request)
服务端:没有。。(Response)
客户端:啦啦啦,有没有新信息(Request)
服务端:你好烦啊,没有啊。。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:好啦好啦,有啦给你。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:。。。。。没。。。。没。。。没有(Response) —- loop
long poll
long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。
场景再现
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)
服务端:额。。 等待到有消息的时候。。来 给你(Response)
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop
从上面可以看出其实这两种方式,都是在不断地建立HTTP连接,然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性
何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。
简单地说就是,服务器是一个很懒的冰箱(这是个梗)(不会、不能主动发起连接),但是上司有命令,如果有客户来,不管多么累都要好好接待。
说完这个,我们再来说一说上面的缺陷(原谅我废话这么多吧OAQ)
从上面很容易看出来,不管怎么样,上面这两种都是非常消耗资源的。
ajax轮询 需要服务器有很快的处理速度和资源。(速度)
long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)
所以ajax轮询 和long poll 都有可能发生这种情况。
客户端:啦啦啦啦,有新信息么?
服务端:月线正忙,请稍后再试(503 Server Unavailable)
客户端:。。。。好吧,啦啦啦,有新信息么?
服务端:月线正忙,请稍后再试(503 Server Unavailable)

客户端:
然后服务端在一旁忙的要死:冰箱,我要更多的冰箱!更多。。更多。。(我错了。。这又是梗。。)
————————–
言归正传,我们来说Websocket吧
通过上面这个例子,我们可以看出,这两种方式都不是最好的方式,需要很多资源。
一种需要更快的速度,一种需要更多的’电话’。这两种都会导致’电话’的需求越来越高。
哦对了,忘记说了HTTP还是一个状态协议。
通俗的说就是,服务器因为每天要接待太多客户了,是个健忘鬼,你一挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第二次还得再告诉服务器一遍。
所以在这种情况下出现了,Websocket出现了。
他解决了HTTP的这几个难题。
首先,被动性,当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。
所以上面的情景可以做如下修改。
客户端:啦啦啦,我要建立Websocket协议,需要的服务:chat,Websocket协议版本:17(HTTP Request)
服务端:ok,确认,已升级为Websocket协议(HTTP Protocols Switched)
客户端:麻烦你有信息的时候推送给我噢。。
服务端:ok,有的时候会告诉你的。
服务端:balabalabalabala
服务端:balabalabalabala
服务端:哈哈哈哈哈啊哈哈哈哈
服务端:笑死我了哈哈哈哈哈哈哈
就变成了这样,只需要经过一次HTTP请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你)
这样的协议解决了上面同步有延迟,而且还非常消耗资源的这种情况。
那么为什么他会解决服务器上消耗资源的问题呢?
其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(PHP等)来处理。
简单地说,我们有一个非常快速的接线员(Nginx),他负责把问题转交给相应的客服(Handler)
本身接线员基本上速度是足够的,但是每次都卡在客服(Handler)了,老有客服处理速度太慢。,导致客服不够。
Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。
这样就可以解决客服处理速度过慢的问题了。
同时,在传统的方式上,要不断的建立,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁。
虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。
但是Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。
同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的。。),没有信息的时候就交给接线员(Nginx),不需要占用本身速度就慢的客服(Handler)
——————–
至于怎么在不支持Websocket的客户端上使用Websocket。。答案是:不能
但是可以通过上面说的 long poll 和 ajax 轮询来 模拟出类似的效果