feat(mtp): stomp server with custom usp spec

This commit is contained in:
leandrofars 2023-10-28 16:00:27 -03:00
parent 64f0d5a088
commit 6ccd0aa0e8
85 changed files with 7655 additions and 10 deletions

View File

@ -0,0 +1,20 @@
## Authors
* John Jeffery
* Hiram Jerónimo Pérez
* Alessandro Siragusa
* DaytonG
* Erik Benoist
* Evan Borgstrom
* Fernando Ribeiro
* Fredrik Rubensson
* Laurent Luce
* Oliver, Jonathan
* Paul P. Komkoff
* Raphael Tiersch
* Tom Lee
* Tony Le
* Voronkov Artem
* Whit Marbut
* hanjm
* yang.zhang4

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2012 The go-stomp authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,44 @@
This STOMP implementation was forked from <a href="https://github.com/go-stomp/stomp">https://github.com/go-stomp/stomp</a> and we customized it to accomplish with TR-369 protocol
# stomp
Go language implementation of a STOMP client library.
![Build Status](https://github.com/go-stomp/stomp/actions/workflows/test.yml/badge.svg?branch=master)
[![Go Reference](https://pkg.go.dev/badge/github.com/go-stomp/stomp/v3.svg)](https://pkg.go.dev/github.com/go-stomp/stomp/v3)
Features:
* Supports STOMP Specifications Versions 1.0, 1.1, 1.2 (https://stomp.github.io/)
* Protocol negotiation to select the latest mutually supported protocol
* Heart beating for testing the underlying network connection
* Tested against RabbitMQ v3.0.1
## Usage Instructions
```
go get github.com/go-stomp/stomp/v3
```
For API documentation, see https://pkg.go.dev/github.com/go-stomp/stomp/v3
Breaking changes between this previous version and the current version are
documented in [breaking_changes.md](breaking_changes.md).
## License
Copyright 2012 - Present The go-stomp authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,48 @@
package stomp
import (
"github.com/go-stomp/stomp/v3/frame"
)
// The AckMode type is an enumeration of the acknowledgement modes for a
// STOMP subscription.
type AckMode int
// String returns the string representation of the AckMode value.
func (a AckMode) String() string {
switch a {
case AckAuto:
return frame.AckAuto
case AckClient:
return frame.AckClient
case AckClientIndividual:
return frame.AckClientIndividual
}
panic("invalid AckMode value")
}
// ShouldAck returns true if this AckMode is an acknowledgement
// mode which requires acknowledgement. Returns true for all values
// except AckAuto, which returns false.
func (a AckMode) ShouldAck() bool {
switch a {
case AckAuto:
return false
case AckClient, AckClientIndividual:
return true
}
panic("invalid AckMode value")
}
const (
// No acknowledgement is required, the server assumes that the client
// received the message.
AckAuto AckMode = iota
// Client acknowledges messages. When a client acknowledges a message,
// any previously received messages are also acknowledged.
AckClient
// Client acknowledges message. Each message is acknowledged individually.
AckClientIndividual
)

View File

@ -0,0 +1,80 @@
# Breaking Changes
This document provides a list of breaking changes since the V1 release
of the stomp client library.
## v2 and v3
### Module support
Version 2 was released before module support was present in golang, and changes were tagged wit that version.
Therefore we had to update again the import path.
The API it's stable the only breaking change is the import path.
Version 3:
```go
import (
"github.com/go-stomp/stomp/v3"
)
```
## v1 and v2
### 1. No longer using gopkg.in
Version 1 of the library used Gustavo Niemeyer's `gopkg.in` facility for versioning Go libraries.
For a number of reasons, the `stomp` library no longer uses this facility. For this reason the
import path has changed.
Version 1:
```go
import (
"gopkg.in/stomp.v1"
)
```
Version 2:
```go
import (
"github.com/go-stomp/stomp"
)
```
### 2. Frame types moved to frame package
Version 1 of the library included a number of types to do with STOMP frames in the `stomp`
package, and the `frame` package consisted of just a few constant definitions.
It was decided to move the following types out of the `stomp` package and into the `frame` package:
* `stomp.Frame` -> `frame.Frame`
* `stomp.Header` -> `frame.Header`
* `stomp.Reader` -> `frame.Reader`
* `stomp.Writer` -> `frame.Writer`
This change was considered worthwhile for the following reasons:
* This change reduces the surface area of the `stomp` package and makes it easier to learn.
* Ideally, users of the `stomp` package do not need to directly reference the items in the `frame`
package, and the types moved are not needed in normal usage of the `stomp` package.
### 3. Use of functional options
Version 2 of the stomp library makes use of functional options to provide a clean, flexible way
of specifying options in the following API calls:
* [Dial()](http://godoc.org/github.com/go-stomp/stomp#Dial)
* [Connect()](http://godoc.org/github.com/go-stomp/stomp#Connect)
* [Conn.Send()](http://godoc.org/github.com/go-stomp/stomp#Conn.Send)
* [Transaction.Send()](http://godoc.org/github.com/go-stomp/stomp#Transaction.Send)
* [Conn.Subscribe()](http://godoc.org/github.com/go-stomp/stomp#Conn.Subscribe)
The idea for this comes from Dave Cheney's very excellent blog post,
[Functional Options for Friendly APIs](http://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis).
While these new APIs are a definite improvement, they do introduce breaking changes with Version 1.

View File

@ -5,7 +5,7 @@ import (
"net"
"os"
stomp_server "github.com/go-stomp/stomp/server"
"github.com/go-stomp/stomp/v3/server"
"github.com/joho/godotenv"
)
@ -47,14 +47,14 @@ func main() {
Passwd: os.Getenv("STOMP_PASSWORD"),
}
l, err := net.Listen("tcp", stomp_server.DefaultAddr)
l, err := net.Listen("tcp", server.DefaultAddr)
if err != nil {
log.Println("Error to open tcp port: ", err)
}
s := stomp_server.Server{
Addr: stomp_server.DefaultAddr,
HeartBeat: stomp_server.DefaultHeartBeat,
s := server.Server{
Addr: server.DefaultAddr,
HeartBeat: server.DefaultHeartBeat,
Authenticator: &creds,
}

View File

@ -0,0 +1,774 @@
package stomp
import (
"errors"
"io"
"net"
"strconv"
"sync"
"time"
"github.com/go-stomp/stomp/v3/frame"
)
// Default time span to add to read/write heart-beat timeouts
// to avoid premature disconnections due to network latency.
const DefaultHeartBeatError = 5 * time.Second
// Default send timeout in Conn.Send function
const DefaultMsgSendTimeout = 10 * time.Second
// Default receipt timeout in Conn.Send function
const DefaultRcvReceiptTimeout = 30 * time.Second
// Default receipt timeout in Conn.Disconnect function
const DefaultDisconnectReceiptTimeout = 30 * time.Second
// Reply-To header used for temporary queues/RPC with rabbit.
const ReplyToHeader = "reply-to"
// A Conn is a connection to a STOMP server. Create a Conn using either
// the Dial or Connect function.
type Conn struct {
conn io.ReadWriteCloser
readCh chan *frame.Frame
writeCh chan writeRequest
version Version
session string
server string
readTimeout time.Duration
writeTimeout time.Duration
msgSendTimeout time.Duration
rcvReceiptTimeout time.Duration
disconnectReceiptTimeout time.Duration
hbGracePeriodMultiplier float64
closed bool
closeMutex *sync.Mutex
options *connOptions
log Logger
}
type writeRequest struct {
Frame *frame.Frame // frame to send
C chan *frame.Frame // response channel
}
// Dial creates a network connection to a STOMP server and performs
// the STOMP connect protocol sequence. The network endpoint of the
// STOMP server is specified by network and addr. STOMP protocol
// options can be specified in opts.
func Dial(network, addr string, opts ...func(*Conn) error) (*Conn, error) {
c, err := net.Dial(network, addr)
if err != nil {
return nil, err
}
host, _, err := net.SplitHostPort(c.RemoteAddr().String())
if err != nil {
c.Close()
return nil, err
}
// Add option to set host and make it the first option in list,
// so that if host has been explicitly specified it will override.
opts = append([]func(*Conn) error{ConnOpt.Host(host)}, opts...)
return Connect(c, opts...)
}
// Connect creates a STOMP connection and performs the STOMP connect
// protocol sequence. The connection to the STOMP server has already
// been created by the program. The opts parameter provides the
// opportunity to specify STOMP protocol options.
func Connect(conn io.ReadWriteCloser, opts ...func(*Conn) error) (*Conn, error) {
reader := frame.NewReader(conn)
writer := frame.NewWriter(conn)
c := &Conn{
conn: conn,
closeMutex: &sync.Mutex{},
}
options, err := newConnOptions(c, opts)
if err != nil {
return nil, err
}
c.log = options.Logger
if options.ReadBufferSize > 0 {
reader = frame.NewReaderSize(conn, options.ReadBufferSize)
}
if options.WriteBufferSize > 0 {
writer = frame.NewWriterSize(conn, options.ReadBufferSize)
}
readChannelCapacity := 20
writeChannelCapacity := 20
if options.ReadChannelCapacity > 0 {
readChannelCapacity = options.ReadChannelCapacity
}
if options.WriteChannelCapacity > 0 {
writeChannelCapacity = options.WriteChannelCapacity
}
c.hbGracePeriodMultiplier = options.HeartBeatGracePeriodMultiplier
c.readCh = make(chan *frame.Frame, readChannelCapacity)
c.writeCh = make(chan writeRequest, writeChannelCapacity)
if options.Host == "" {
// host not specified yet, attempt to get from net.Conn if possible
if connection, ok := conn.(net.Conn); ok {
host, _, err := net.SplitHostPort(connection.RemoteAddr().String())
if err == nil {
options.Host = host
}
}
// if host is still blank, use default
if options.Host == "" {
options.Host = "default"
}
}
connectFrame, err := options.NewFrame()
if err != nil {
return nil, err
}
err = writer.Write(connectFrame)
if err != nil {
return nil, err
}
response, err := reader.Read()
if err != nil {
return nil, err
}
if response == nil {
return nil, errors.New("unexpected empty frame")
}
if response.Command != frame.CONNECTED {
return nil, newError(response)
}
c.server = response.Header.Get(frame.Server)
c.session = response.Header.Get(frame.Session)
if versionString := response.Header.Get(frame.Version); versionString != "" {
version := Version(versionString)
if err = version.CheckSupported(); err != nil {
return nil, Error{
Message: err.Error(),
Frame: response,
}
}
c.version = version
} else {
// no version in the response, so assume version 1.0
c.version = V10
}
if heartBeat, ok := response.Header.Contains(frame.HeartBeat); ok {
readTimeout, writeTimeout, err := frame.ParseHeartBeat(heartBeat)
if err != nil {
return nil, Error{
Message: err.Error(),
Frame: response,
}
}
c.readTimeout = readTimeout
c.writeTimeout = writeTimeout
if c.readTimeout > 0 {
// Add time to the read timeout to account for time
// delay in other station transmitting timeout
c.readTimeout += options.HeartBeatError
}
if c.writeTimeout > options.HeartBeatError {
// Reduce time from the write timeout to account
// for time delay in transmitting to the other station
c.writeTimeout -= options.HeartBeatError
}
}
c.msgSendTimeout = options.MsgSendTimeout
c.rcvReceiptTimeout = options.RcvReceiptTimeout
c.disconnectReceiptTimeout = options.DisconnectReceiptTimeout
if options.ResponseHeadersCallback != nil {
options.ResponseHeadersCallback(response.Header)
}
go readLoop(c, reader)
go processLoop(c, writer)
return c, nil
}
// Version returns the version of the STOMP protocol that
// is being used to communicate with the STOMP server. This
// version is negotiated with the server during the connect sequence.
func (c *Conn) Version() Version {
return c.version
}
// Session returns the session identifier, which can be
// returned by the STOMP server during the connect sequence.
// If the STOMP server does not return a session header entry,
// this value will be a blank string.
func (c *Conn) Session() string {
return c.session
}
// Server returns the STOMP server identification, which can
// be returned by the STOMP server during the connect sequence.
// If the STOMP server does not return a server header entry,
// this value will be a blank string.
func (c *Conn) Server() string {
return c.server
}
// readLoop is a goroutine that reads frames from the
// reader and places them onto a channel for processing
// by the processLoop goroutine
func readLoop(c *Conn, reader *frame.Reader) {
for {
f, err := reader.Read()
if err != nil {
close(c.readCh)
return
}
c.readCh <- f
}
}
// processLoop is a goroutine that handles io with
// the server.
func processLoop(c *Conn, writer *frame.Writer) {
channels := make(map[string]chan *frame.Frame)
var readTimeoutChannel <-chan time.Time
var readTimer *time.Timer
var writeTimeoutChannel <-chan time.Time
var writeTimer *time.Timer
defer c.MustDisconnect()
for {
if c.readTimeout > 0 && readTimer == nil {
readTimer = time.NewTimer(time.Duration(float64(c.readTimeout) * c.hbGracePeriodMultiplier))
readTimeoutChannel = readTimer.C
}
if c.writeTimeout > 0 && writeTimer == nil {
writeTimer = time.NewTimer(c.writeTimeout)
writeTimeoutChannel = writeTimer.C
}
select {
case <-readTimeoutChannel:
// read timeout, close the connection
err := newErrorMessage("read timeout")
sendError(channels, err)
return
case <-writeTimeoutChannel:
// write timeout, send a heart-beat frame
err := writer.Write(nil)
if err != nil {
sendError(channels, err)
return
}
writeTimer = nil
writeTimeoutChannel = nil
case f, ok := <-c.readCh:
// stop the read timer
if readTimer != nil {
readTimer.Stop()
readTimer = nil
readTimeoutChannel = nil
}
if !ok {
err := newErrorMessage("connection closed")
sendError(channels, err)
return
}
if f == nil {
// heart-beat received
continue
}
switch f.Command {
case frame.RECEIPT:
if id, ok := f.Header.Contains(frame.ReceiptId); ok {
if ch, ok := channels[id]; ok {
ch <- f
delete(channels, id)
close(ch)
}
} else {
err := &Error{Message: "missing receipt-id", Frame: f}
sendError(channels, err)
return
}
case frame.ERROR:
c.log.Error("received ERROR; Closing underlying connection")
for _, ch := range channels {
ch <- f
close(ch)
}
c.closeMutex.Lock()
defer c.closeMutex.Unlock()
c.closed = true
c.conn.Close()
return
case frame.MESSAGE:
if id, ok := f.Header.Contains(frame.Subscription); ok {
if ch, ok := channels[id]; ok {
ch <- f
} else {
c.log.Infof("ignored MESSAGE for subscription: %s", id)
}
}
}
case req, ok := <-c.writeCh:
// stop the write timeout
if writeTimer != nil {
writeTimer.Stop()
writeTimer = nil
writeTimeoutChannel = nil
}
if !ok {
sendError(channels, errors.New("write channel closed"))
return
}
if req.C != nil {
if receipt, ok := req.Frame.Header.Contains(frame.Receipt); ok {
// remember the channel for this receipt
channels[receipt] = req.C
}
}
// default is to always send a frame.
var sendFrame = true
switch req.Frame.Command {
case frame.SUBSCRIBE:
id, _ := req.Frame.Header.Contains(frame.Id)
channels[id] = req.C
// if using a temp queue, map that destination as a known channel
// however, don't send the frame, it's most likely an invalid destination
// on the broker.
if replyTo, ok := req.Frame.Header.Contains(ReplyToHeader); ok {
channels[replyTo] = req.C
sendFrame = false
}
case frame.UNSUBSCRIBE:
id, _ := req.Frame.Header.Contains(frame.Id)
// is this trying to be too clever -- add a receipt
// header so that when the server responds with a
// RECEIPT frame, the corresponding channel will be closed
req.Frame.Header.Set(frame.Receipt, id)
}
// frame to send, if enabled
if sendFrame {
err := writer.Write(req.Frame)
if err != nil {
sendError(channels, err)
return
}
}
}
}
}
// Send an error to all receipt channels.
func sendError(m map[string]chan *frame.Frame, err error) {
frame := frame.New(frame.ERROR, frame.Message, err.Error())
for _, ch := range m {
ch <- frame
}
}
// Disconnect will disconnect from the STOMP server. This function
// follows the STOMP standard's recommended protocol for graceful
// disconnection: it sends a DISCONNECT frame with a receipt header
// element. Once the RECEIPT frame has been received, the connection
// with the STOMP server is closed and any further attempt to write
// to the server will fail.
func (c *Conn) Disconnect() error {
c.closeMutex.Lock()
defer c.closeMutex.Unlock()
if c.closed {
return nil
}
ch := make(chan *frame.Frame)
c.writeCh <- writeRequest{
Frame: frame.New(frame.DISCONNECT, frame.Receipt, allocateId()),
C: ch,
}
err := readReceiptWithTimeout(ch, c.disconnectReceiptTimeout, ErrDisconnectReceiptTimeout)
if err == nil {
c.closed = true
return c.conn.Close()
}
if err == ErrDisconnectReceiptTimeout {
c.closed = true
_ = c.conn.Close()
}
return err
}
// MustDisconnect will disconnect 'ungracefully' from the STOMP server.
// This method should be used only as last resort when there are fatal
// network errors that prevent to do a proper disconnect from the server.
func (c *Conn) MustDisconnect() error {
c.closeMutex.Lock()
defer c.closeMutex.Unlock()
if c.closed {
return nil
}
// just close writeCh
close(c.writeCh)
c.closed = true
return c.conn.Close()
}
// Send sends a message to the STOMP server, which in turn sends the message to the specified destination.
// If the STOMP server fails to receive the message for any reason, the connection will close.
//
// The content type should be specified, according to the STOMP specification, but if contentType is an empty
// string, the message will be delivered without a content-type header entry. The body array contains the
// message body, and its content should be consistent with the specified content type.
//
// Any number of options can be specified in opts. See the examples for usage. Options include whether
// to receive a RECEIPT, should the content-length be suppressed, and sending custom header entries.
func (c *Conn) Send(destination, contentType string, body []byte, opts ...func(*frame.Frame) error) error {
c.closeMutex.Lock()
defer c.closeMutex.Unlock()
if c.closed {
return ErrAlreadyClosed
}
f, err := createSendFrame(destination, contentType, body, opts)
if err != nil {
return err
}
if _, ok := f.Header.Contains(frame.Receipt); ok {
// receipt required
request := writeRequest{
Frame: f,
C: make(chan *frame.Frame),
}
err := sendDataToWriteChWithTimeout(c.writeCh, request, c.msgSendTimeout)
if err != nil {
return err
}
err = readReceiptWithTimeout(request.C, c.rcvReceiptTimeout, ErrMsgReceiptTimeout)
if err != nil {
return err
}
} else {
// no receipt required
request := writeRequest{Frame: f}
err := sendDataToWriteChWithTimeout(c.writeCh, request, c.msgSendTimeout)
if err != nil {
return err
}
}
return nil
}
func readReceiptWithTimeout(responseChan chan *frame.Frame, timeout time.Duration, timeoutErr error) error {
var timeoutChan <-chan time.Time
if timeout > 0 {
timeoutChan = time.After(timeout)
}
select {
case <-timeoutChan:
return timeoutErr
case response := <-responseChan:
if response.Command != frame.RECEIPT {
return newError(response)
}
return nil
}
}
func sendDataToWriteChWithTimeout(ch chan writeRequest, request writeRequest, timeout time.Duration) error {
if timeout <= 0 {
ch <- request
return nil
}
timer := time.NewTimer(timeout)
select {
case <-timer.C:
return ErrMsgSendTimeout
case ch <- request:
timer.Stop()
return nil
}
}
func createSendFrame(destination, contentType string, body []byte, opts []func(*frame.Frame) error) (*frame.Frame, error) {
// Set the content-length before the options, because this provides
// an opportunity to remove content-length.
f := frame.New(frame.SEND, frame.ContentLength, strconv.Itoa(len(body)))
f.Body = body
f.Header.Set(frame.Destination, destination)
if contentType != "" {
f.Header.Set(frame.ContentType, contentType)
}
for _, opt := range opts {
if opt == nil {
continue
}
if err := opt(f); err != nil {
return nil, err
}
}
return f, nil
}
func (c *Conn) sendFrame(f *frame.Frame) error {
// Lock our mutex, but don't close it via defer
// If the frame requests a receipt then we want to release the lock before
// we block on the response, otherwise we can end up deadlocking
c.closeMutex.Lock()
if c.closed {
c.closeMutex.Unlock()
c.conn.Close()
return ErrClosedUnexpectedly
}
if _, ok := f.Header.Contains(frame.Receipt); ok {
// receipt required
request := writeRequest{
Frame: f,
C: make(chan *frame.Frame),
}
c.writeCh <- request
// Now that we've written to the writeCh channel we can release the
// close mutex while we wait for our response
c.closeMutex.Unlock()
var response *frame.Frame
if c.writeTimeout > 0 {
select {
case response, ok = <-request.C:
case <-time.After(c.writeTimeout):
ok = false
}
} else {
response, ok = <-request.C
}
if ok {
if response.Command != frame.RECEIPT {
return newError(response)
}
} else {
return ErrClosedUnexpectedly
}
} else {
// no receipt required
request := writeRequest{Frame: f}
c.writeCh <- request
// Unlock the mutex now that we're written to the write channel
c.closeMutex.Unlock()
}
return nil
}
// Subscribe creates a subscription on the STOMP server.
// The subscription has a destination, and messages sent to that destination
// will be received by this subscription. A subscription has a channel
// on which the calling program can receive messages.
func (c *Conn) Subscribe(destination string, ack AckMode, opts ...func(*frame.Frame) error) (*Subscription, error) {
c.closeMutex.Lock()
defer c.closeMutex.Unlock()
if c.closed {
c.conn.Close()
return nil, ErrClosedUnexpectedly
}
ch := make(chan *frame.Frame)
subscribeFrame := frame.New(frame.SUBSCRIBE,
frame.Destination, destination,
frame.Ack, ack.String())
for _, opt := range opts {
if opt == nil {
continue
}
err := opt(subscribeFrame)
if err != nil {
return nil, err
}
}
// If the option functions have not specified the "id" header entry,
// create one.
id, ok := subscribeFrame.Header.Contains(frame.Id)
if !ok {
id = allocateId()
subscribeFrame.Header.Add(frame.Id, id)
}
request := writeRequest{
Frame: subscribeFrame,
C: ch,
}
closeMutex := &sync.Mutex{}
sub := &Subscription{
id: id,
destination: destination,
conn: c,
ackMode: ack,
C: make(chan *Message, 16),
closeMutex: closeMutex,
closeCond: sync.NewCond(closeMutex),
}
go sub.readLoop(ch)
// TODO is this safe? There is no check if writeCh is actually open.
c.writeCh <- request
return sub, nil
}
// TODO check further for race conditions
// Ack acknowledges a message received from the STOMP server.
// If the message was received on a subscription with AckMode == AckAuto,
// then no operation is performed.
func (c *Conn) Ack(m *Message) error {
f, err := c.createAckNackFrame(m, true)
if err != nil {
return err
}
if f != nil {
return c.sendFrame(f)
}
return nil
}
// Nack indicates to the server that a message was not received
// by the client. Returns an error if the STOMP version does not
// support the NACK message.
func (c *Conn) Nack(m *Message) error {
f, err := c.createAckNackFrame(m, false)
if err != nil {
return err
}
if f != nil {
return c.sendFrame(f)
}
return nil
}
// Begin is used to start a transaction. Transactions apply to sending
// and acknowledging. Any messages sent or acknowledged during a transaction
// will be processed atomically by the STOMP server based on the transaction.
func (c *Conn) Begin() *Transaction {
t, _ := c.BeginWithError()
return t
}
// BeginWithError is used to start a transaction, but also returns the error
// (if any) from sending the frame to start the transaction.
func (c *Conn) BeginWithError() (*Transaction, error) {
id := allocateId()
f := frame.New(frame.BEGIN, frame.Transaction, id)
err := c.sendFrame(f)
return &Transaction{id: id, conn: c}, err
}
// Create an ACK or NACK frame. Complicated by version incompatibilities.
func (c *Conn) createAckNackFrame(msg *Message, ack bool) (*frame.Frame, error) {
if !ack && !c.version.SupportsNack() {
return nil, ErrNackNotSupported
}
if msg.Header == nil || msg.Subscription == nil || msg.Conn == nil {
return nil, ErrNotReceivedMessage
}
if msg.Subscription.AckMode() == AckAuto {
if ack {
// not much point sending an ACK to an auto subscription
return nil, nil
} else {
// sending a NACK for an ack:auto subscription makes no
// sense
return nil, ErrCannotNackAutoSub
}
}
var f *frame.Frame
if ack {
f = frame.New(frame.ACK)
} else {
f = frame.New(frame.NACK)
}
switch c.version {
case V10, V11:
f.Header.Add(frame.Subscription, msg.Subscription.Id())
if messageId, ok := msg.Header.Contains(frame.MessageId); ok {
f.Header.Add(frame.MessageId, messageId)
} else {
return nil, missingHeader(frame.MessageId)
}
case V12:
// message frame contains ack header
if ack, ok := msg.Header.Contains(frame.Ack); ok {
// ack frame should reference it as id
f.Header.Add(frame.Id, ack)
} else {
return nil, missingHeader(frame.Ack)
}
}
return f, nil
}

View File

@ -0,0 +1,327 @@
package stomp
import (
"fmt"
"strings"
"time"
"github.com/go-stomp/stomp/v3/frame"
"github.com/go-stomp/stomp/v3/internal/log"
)
// ConnOptions is an opaque structure used to collection options
// for connecting to the other server.
type connOptions struct {
FrameCommand string
Host string
ReadTimeout time.Duration
WriteTimeout time.Duration
HeartBeatError time.Duration
MsgSendTimeout time.Duration
RcvReceiptTimeout time.Duration
DisconnectReceiptTimeout time.Duration
HeartBeatGracePeriodMultiplier float64
Login, Passcode string
AcceptVersions []string
Header *frame.Header
ReadChannelCapacity, WriteChannelCapacity int
ReadBufferSize, WriteBufferSize int
ResponseHeadersCallback func(*frame.Header)
Logger Logger
}
func newConnOptions(conn *Conn, opts []func(*Conn) error) (*connOptions, error) {
co := &connOptions{
FrameCommand: frame.CONNECT,
ReadTimeout: time.Minute,
WriteTimeout: time.Minute,
HeartBeatGracePeriodMultiplier: 1.0,
HeartBeatError: DefaultHeartBeatError,
MsgSendTimeout: DefaultMsgSendTimeout,
RcvReceiptTimeout: DefaultRcvReceiptTimeout,
DisconnectReceiptTimeout: DefaultDisconnectReceiptTimeout,
Logger: log.StdLogger{},
}
// This is a slight of hand, attach the options to the Conn long
// enough to run the options functions and then detach again.
// The reason we do this is to allow for future options to be able
// to modify the Conn object itself, in case that becomes desirable.
conn.options = co
defer func() { conn.options = nil }()
// compatibility with previous version: ignore nil options
for _, opt := range opts {
if opt != nil {
err := opt(conn)
if err != nil {
return nil, err
}
}
}
if len(co.AcceptVersions) == 0 {
co.AcceptVersions = append(co.AcceptVersions, string(V10), string(V11), string(V12))
}
return co, nil
}
func (co *connOptions) NewFrame() (*frame.Frame, error) {
f := frame.New(co.FrameCommand)
if co.Host != "" {
f.Header.Set(frame.Host, co.Host)
}
// heart-beat
{
send := co.WriteTimeout / time.Millisecond
recv := co.ReadTimeout / time.Millisecond
f.Header.Set(frame.HeartBeat, fmt.Sprintf("%d,%d", send, recv))
}
// login, passcode
if co.Login != "" || co.Passcode != "" {
f.Header.Set(frame.Login, co.Login)
f.Header.Set(frame.Passcode, co.Passcode)
}
// accept-version
f.Header.Set(frame.AcceptVersion, strings.Join(co.AcceptVersions, ","))
// custom header entries -- note that these do not override
// header values already set as they are added to the end of
// the header array
f.Header.AddHeader(co.Header)
return f, nil
}
// Options for connecting to the STOMP server. Used with the
// stomp.Dial and stomp.Connect functions, both of which have examples.
var ConnOpt struct {
// Login is a connect option that allows the calling program to
// specify the "login" and "passcode" values to send to the STOMP
// server.
Login func(login, passcode string) func(*Conn) error
// Host is a connect option that allows the calling program to
// specify the value of the "host" header.
Host func(host string) func(*Conn) error
// UseStomp is a connect option that specifies that the client
// should use the "STOMP" command instead of the "CONNECT" command.
// Note that using "STOMP" is only valid for STOMP version 1.1 and later.
UseStomp func(*Conn) error
// AcceptVersoin is a connect option that allows the client to
// specify one or more versions of the STOMP protocol that the
// client program is prepared to accept. If this option is not
// specified, the client program will accept any of STOMP versions
// 1.0, 1.1 or 1.2.
AcceptVersion func(versions ...Version) func(*Conn) error
// HeartBeat is a connect option that allows the client to specify
// the send and receive timeouts for the STOMP heartbeat negotiation mechanism.
// The sendTimeout parameter specifies the maximum amount of time
// between the client sending heartbeat notifications from the server.
// The recvTimeout paramter specifies the minimum amount of time between
// the client expecting to receive heartbeat notifications from the server.
// If not specified, this option defaults to one minute for both send and receive
// timeouts.
HeartBeat func(sendTimeout, recvTimeout time.Duration) func(*Conn) error
// HeartBeatError is a connect option that will normally only be specified during
// testing. It specifies a short time duration that is larger than the amount of time
// that will take for a STOMP frame to be transmitted from one station to the other.
// When not specified, this value defaults to 5 seconds. This value is set to a much
// shorter time duration during unit testing.
HeartBeatError func(errorTimeout time.Duration) func(*Conn) error
// MsgSendTimeout is a connect option that allows the client to specify
// the timeout for the Conn.Send function.
// The msgSendTimeout parameter specifies maximum blocking time for calling
// the Conn.Send function.
// If not specified, this option defaults to 10 seconds.
// Less than or equal to zero means infinite
MsgSendTimeout func(msgSendTimeout time.Duration) func(*Conn) error
// RcvReceiptTimeout is a connect option that allows the client to specify
// how long to wait for a receipt in the Conn.Send function. This helps
// avoid deadlocks. If this is not specified, the default is 30 seconds.
RcvReceiptTimeout func(rcvReceiptTimeout time.Duration) func(*Conn) error
// DisconnectReceiptTimeout is a connect option that allows the client to specify
// how long to wait for a receipt in the Conn.Disconnect function. This helps
// avoid deadlocks. If this is not specified, the default is 30 seconds.
DisconnectReceiptTimeout func(disconnectReceiptTimeout time.Duration) func(*Conn) error
// HeartBeatGracePeriodMultiplier is used to calculate the effective read heart-beat timeout
// the broker will enforce for each clients connection. The multiplier is applied to
// the read-timeout interval the client specifies in its CONNECT frame
HeartBeatGracePeriodMultiplier func(multiplier float64) func(*Conn) error
// Header is a connect option that allows the client to specify a custom
// header entry in the STOMP frame. This connect option can be specified
// multiple times for multiple custom headers.
Header func(key, value string) func(*Conn) error
// ReadChannelCapacity is the number of messages that can be on the read channel at the
// same time. A high number may affect memory usage while a too low number may lock the
// system up. Default is set to 20.
ReadChannelCapacity func(capacity int) func(*Conn) error
// WriteChannelCapacity is the number of messages that can be on the write channel at the
// same time. A high number may affect memory usage while a too low number may lock the
// system up. Default is set to 20.
WriteChannelCapacity func(capacity int) func(*Conn) error
// ReadBufferSize specifies number of bytes that can be used to read the message
// A high number may affect memory usage while a too low number may lock the
// system up. Default is set to 4096.
ReadBufferSize func(size int) func(*Conn) error
// WriteBufferSize specifies number of bytes that can be used to write the message
// A high number may affect memory usage while a too low number may lock the
// system up. Default is set to 4096.
WriteBufferSize func(size int) func(*Conn) error
// ResponseHeaders lets you provide a callback function to get the headers from the CONNECT response
ResponseHeaders func(func(*frame.Header)) func(*Conn) error
// Logger lets you provide a callback function that sets the logger used by a connection
Logger func(logger Logger) func(*Conn) error
}
func init() {
ConnOpt.Login = func(login, passcode string) func(*Conn) error {
return func(c *Conn) error {
c.options.Login = login
c.options.Passcode = passcode
return nil
}
}
ConnOpt.Host = func(host string) func(*Conn) error {
return func(c *Conn) error {
c.options.Host = host
return nil
}
}
ConnOpt.UseStomp = func(c *Conn) error {
c.options.FrameCommand = frame.STOMP
return nil
}
ConnOpt.AcceptVersion = func(versions ...Version) func(*Conn) error {
return func(c *Conn) error {
for _, version := range versions {
if err := version.CheckSupported(); err != nil {
return err
}
c.options.AcceptVersions = append(c.options.AcceptVersions, string(version))
}
return nil
}
}
ConnOpt.HeartBeat = func(sendTimeout, recvTimeout time.Duration) func(*Conn) error {
return func(c *Conn) error {
c.options.WriteTimeout = sendTimeout
c.options.ReadTimeout = recvTimeout
return nil
}
}
ConnOpt.HeartBeatError = func(errorTimeout time.Duration) func(*Conn) error {
return func(c *Conn) error {
c.options.HeartBeatError = errorTimeout
return nil
}
}
ConnOpt.MsgSendTimeout = func(msgSendTimeout time.Duration) func(*Conn) error {
return func(c *Conn) error {
c.options.MsgSendTimeout = msgSendTimeout
return nil
}
}
ConnOpt.RcvReceiptTimeout = func(rcvReceiptTimeout time.Duration) func(*Conn) error {
return func(c *Conn) error {
c.options.RcvReceiptTimeout = rcvReceiptTimeout
return nil
}
}
ConnOpt.DisconnectReceiptTimeout = func(disconnectReceiptTimeout time.Duration) func(*Conn) error {
return func(c *Conn) error {
c.options.DisconnectReceiptTimeout = disconnectReceiptTimeout
return nil
}
}
ConnOpt.HeartBeatGracePeriodMultiplier = func(multiplier float64) func(*Conn) error {
return func(c *Conn) error {
c.options.HeartBeatGracePeriodMultiplier = multiplier
return nil
}
}
ConnOpt.Header = func(key, value string) func(*Conn) error {
return func(c *Conn) error {
if c.options.Header == nil {
c.options.Header = frame.NewHeader(key, value)
} else {
c.options.Header.Add(key, value)
}
return nil
}
}
ConnOpt.ReadChannelCapacity = func(capacity int) func(*Conn) error {
return func(c *Conn) error {
c.options.ReadChannelCapacity = capacity
return nil
}
}
ConnOpt.WriteChannelCapacity = func(capacity int) func(*Conn) error {
return func(c *Conn) error {
c.options.WriteChannelCapacity = capacity
return nil
}
}
ConnOpt.ReadBufferSize = func(size int) func(*Conn) error {
return func(c *Conn) error {
c.options.ReadBufferSize = size
return nil
}
}
ConnOpt.WriteBufferSize = func(size int) func(*Conn) error {
return func(c *Conn) error {
c.options.WriteBufferSize = size
return nil
}
}
ConnOpt.ResponseHeaders = func(callback func(*frame.Header)) func(*Conn) error {
return func(c *Conn) error {
c.options.ResponseHeadersCallback = callback
return nil
}
}
ConnOpt.Logger = func(log Logger) func(*Conn) error {
return func(c *Conn) error {
if log != nil {
c.options.Logger = log
}
return nil
}
}
}

View File

@ -0,0 +1,786 @@
package stomp
import (
"fmt"
"io"
"time"
"github.com/go-stomp/stomp/v3/frame"
"github.com/go-stomp/stomp/v3/testutil"
"github.com/golang/mock/gomock"
. "gopkg.in/check.v1"
)
type fakeReaderWriter struct {
reader *frame.Reader
writer *frame.Writer
conn io.ReadWriteCloser
}
func (rw *fakeReaderWriter) Read() (*frame.Frame, error) {
return rw.reader.Read()
}
func (rw *fakeReaderWriter) Write(f *frame.Frame) error {
return rw.writer.Write(f)
}
func (rw *fakeReaderWriter) Close() error {
return rw.conn.Close()
}
func (s *StompSuite) Test_conn_option_set_logger(c *C) {
fc1, fc2 := testutil.NewFakeConn(c)
go func() {
defer func() {
fc2.Close()
fc1.Close()
}()
reader := frame.NewReader(fc2)
writer := frame.NewWriter(fc2)
f1, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f1.Command, Equals, "CONNECT")
f2 := frame.New("CONNECTED")
err = writer.Write(f2)
c.Assert(err, IsNil)
}()
ctrl := gomock.NewController(s.t)
mockLogger := testutil.NewMockLogger(ctrl)
conn, err := Connect(fc1, ConnOpt.Logger(mockLogger))
c.Assert(err, IsNil)
c.Check(conn, NotNil)
c.Assert(conn.log, Equals, mockLogger)
}
func (s *StompSuite) Test_unsuccessful_connect(c *C) {
fc1, fc2 := testutil.NewFakeConn(c)
stop := make(chan struct{})
go func() {
defer func() {
fc2.Close()
close(stop)
}()
reader := frame.NewReader(fc2)
writer := frame.NewWriter(fc2)
f1, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f1.Command, Equals, "CONNECT")
f2 := frame.New("ERROR", "message", "auth-failed")
err = writer.Write(f2)
c.Assert(err, IsNil)
}()
conn, err := Connect(fc1)
c.Assert(conn, IsNil)
c.Assert(err, ErrorMatches, "auth-failed")
}
func (s *StompSuite) Test_successful_connect_and_disconnect(c *C) {
testcases := []struct {
Options []func(*Conn) error
NegotiatedVersion string
ExpectedVersion Version
ExpectedSession string
ExpectedHost string
ExpectedServer string
}{
{
Options: []func(*Conn) error{ConnOpt.Host("the-server")},
ExpectedVersion: "1.0",
ExpectedSession: "",
ExpectedHost: "the-server",
ExpectedServer: "some-server/1.1",
},
{
Options: []func(*Conn) error{},
NegotiatedVersion: "1.1",
ExpectedVersion: "1.1",
ExpectedSession: "the-session",
ExpectedHost: "the-server",
},
{
Options: []func(*Conn) error{ConnOpt.Host("xxx")},
NegotiatedVersion: "1.2",
ExpectedVersion: "1.2",
ExpectedSession: "the-session",
ExpectedHost: "xxx",
},
}
for _, tc := range testcases {
resetId()
fc1, fc2 := testutil.NewFakeConn(c)
stop := make(chan struct{})
go func() {
defer func() {
fc2.Close()
close(stop)
}()
reader := frame.NewReader(fc2)
writer := frame.NewWriter(fc2)
f1, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f1.Command, Equals, "CONNECT")
host, _ := f1.Header.Contains("host")
c.Check(host, Equals, tc.ExpectedHost)
connectedFrame := frame.New("CONNECTED")
if tc.NegotiatedVersion != "" {
connectedFrame.Header.Add("version", tc.NegotiatedVersion)
}
if tc.ExpectedSession != "" {
connectedFrame.Header.Add("session", tc.ExpectedSession)
}
if tc.ExpectedServer != "" {
connectedFrame.Header.Add("server", tc.ExpectedServer)
}
err = writer.Write(connectedFrame)
c.Assert(err, IsNil)
f2, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f2.Command, Equals, "DISCONNECT")
receipt, _ := f2.Header.Contains("receipt")
c.Check(receipt, Equals, "1")
err = writer.Write(frame.New("RECEIPT", frame.ReceiptId, "1"))
c.Assert(err, IsNil)
}()
client, err := Connect(fc1, tc.Options...)
c.Assert(err, IsNil)
c.Assert(client, NotNil)
c.Assert(client.Version(), Equals, tc.ExpectedVersion)
c.Assert(client.Session(), Equals, tc.ExpectedSession)
c.Assert(client.Server(), Equals, tc.ExpectedServer)
err = client.Disconnect()
c.Assert(err, IsNil)
<-stop
}
}
func (s *StompSuite) Test_successful_connect_get_headers(c *C) {
var respHeaders *frame.Header
testcases := []struct {
Options []func(*Conn) error
Headers map[string]string
}{
{
Options: []func(*Conn) error{ConnOpt.ResponseHeaders(func(f *frame.Header) { respHeaders = f })},
Headers: map[string]string{"custom-header": "test", "foo": "bar"},
},
}
for _, tc := range testcases {
resetId()
fc1, fc2 := testutil.NewFakeConn(c)
stop := make(chan struct{})
go func() {
defer func() {
fc2.Close()
close(stop)
}()
reader := frame.NewReader(fc2)
writer := frame.NewWriter(fc2)
f1, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f1.Command, Equals, "CONNECT")
connectedFrame := frame.New("CONNECTED")
for key, value := range tc.Headers {
connectedFrame.Header.Add(key, value)
}
err = writer.Write(connectedFrame)
c.Assert(err, IsNil)
f2, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f2.Command, Equals, "DISCONNECT")
receipt, _ := f2.Header.Contains("receipt")
c.Check(receipt, Equals, "1")
err = writer.Write(frame.New("RECEIPT", frame.ReceiptId, "1"))
c.Assert(err, IsNil)
}()
client, err := Connect(fc1, tc.Options...)
c.Assert(err, IsNil)
c.Assert(client, NotNil)
c.Assert(respHeaders, NotNil)
for key, value := range tc.Headers {
c.Assert(respHeaders.Get(key), Equals, value)
}
err = client.Disconnect()
c.Assert(err, IsNil)
<-stop
}
}
func (s *StompSuite) Test_successful_connect_with_nonstandard_header(c *C) {
resetId()
fc1, fc2 := testutil.NewFakeConn(c)
stop := make(chan struct{})
go func() {
defer func() {
fc2.Close()
close(stop)
}()
reader := frame.NewReader(fc2)
writer := frame.NewWriter(fc2)
f1, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f1.Command, Equals, "CONNECT")
c.Assert(f1.Header.Get("login"), Equals, "guest")
c.Assert(f1.Header.Get("passcode"), Equals, "guest")
c.Assert(f1.Header.Get("host"), Equals, "/")
c.Assert(f1.Header.Get("x-max-length"), Equals, "50")
connectedFrame := frame.New("CONNECTED")
connectedFrame.Header.Add("session", "session-0voRHrG-VbBedx1Gwwb62Q")
connectedFrame.Header.Add("heart-beat", "0,0")
connectedFrame.Header.Add("server", "RabbitMQ/3.2.1")
connectedFrame.Header.Add("version", "1.0")
err = writer.Write(connectedFrame)
c.Assert(err, IsNil)
f2, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f2.Command, Equals, "DISCONNECT")
receipt, _ := f2.Header.Contains("receipt")
c.Check(receipt, Equals, "1")
err = writer.Write(frame.New("RECEIPT", frame.ReceiptId, "1"))
c.Assert(err, IsNil)
}()
client, err := Connect(fc1,
ConnOpt.Login("guest", "guest"),
ConnOpt.Host("/"),
ConnOpt.Header("x-max-length", "50"))
c.Assert(err, IsNil)
c.Assert(client, NotNil)
c.Assert(client.Version(), Equals, V10)
c.Assert(client.Session(), Equals, "session-0voRHrG-VbBedx1Gwwb62Q")
c.Assert(client.Server(), Equals, "RabbitMQ/3.2.1")
err = client.Disconnect()
c.Assert(err, IsNil)
<-stop
}
func (s *StompSuite) Test_connect_not_panic_on_empty_response(c *C) {
resetId()
fc1, fc2 := testutil.NewFakeConn(c)
stop := make(chan struct{})
go func() {
defer func() {
fc2.Close()
close(stop)
}()
reader := frame.NewReader(fc2)
_, err := reader.Read()
c.Assert(err, IsNil)
_, err = fc2.Write([]byte("\n"))
c.Assert(err, IsNil)
}()
client, err := Connect(fc1, ConnOpt.Host("the_server"))
c.Assert(err, NotNil)
c.Assert(client, IsNil)
fc1.Close()
<-stop
}
func (s *StompSuite) Test_successful_disconnect_with_receipt_timeout(c *C) {
resetId()
fc1, fc2 := testutil.NewFakeConn(c)
defer func() {
fc2.Close()
}()
go func() {
reader := frame.NewReader(fc2)
writer := frame.NewWriter(fc2)
f1, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f1.Command, Equals, "CONNECT")
connectedFrame := frame.New("CONNECTED")
err = writer.Write(connectedFrame)
c.Assert(err, IsNil)
}()
client, err := Connect(fc1, ConnOpt.DisconnectReceiptTimeout(1 * time.Nanosecond))
c.Assert(err, IsNil)
c.Assert(client, NotNil)
err = client.Disconnect()
c.Assert(err, Equals, ErrDisconnectReceiptTimeout)
c.Assert(client.closed, Equals, true)
}
// Sets up a connection for testing
func connectHelper(c *C, version Version) (*Conn, *fakeReaderWriter) {
fc1, fc2 := testutil.NewFakeConn(c)
stop := make(chan struct{})
reader := frame.NewReader(fc2)
writer := frame.NewWriter(fc2)
go func() {
f1, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f1.Command, Equals, "CONNECT")
f2 := frame.New("CONNECTED", "version", version.String())
err = writer.Write(f2)
c.Assert(err, IsNil)
close(stop)
}()
conn, err := Connect(fc1)
c.Assert(err, IsNil)
c.Assert(conn, NotNil)
<-stop
return conn, &fakeReaderWriter{
reader: reader,
writer: writer,
conn: fc2,
}
}
func (s *StompSuite) Test_subscribe(c *C) {
ackModes := []AckMode{AckAuto, AckClient, AckClientIndividual}
versions := []Version{V10, V11, V12}
for _, ackMode := range ackModes {
for _, version := range versions {
subscribeHelper(c, ackMode, version)
subscribeHelper(c, ackMode, version,
SubscribeOpt.Header("id", "client-1"),
SubscribeOpt.Header("custom", "true"))
}
}
}
func subscribeHelper(c *C, ackMode AckMode, version Version, opts ...func(*frame.Frame) error) {
conn, rw := connectHelper(c, version)
stop := make(chan struct{})
go func() {
defer func() {
rw.Close()
close(stop)
}()
f3, err := rw.Read()
c.Assert(err, IsNil)
c.Assert(f3.Command, Equals, "SUBSCRIBE")
id, ok := f3.Header.Contains("id")
c.Assert(ok, Equals, true)
destination := f3.Header.Get("destination")
c.Assert(destination, Equals, "/queue/test-1")
ack := f3.Header.Get("ack")
c.Assert(ack, Equals, ackMode.String())
for i := 1; i <= 5; i++ {
messageId := fmt.Sprintf("message-%d", i)
bodyText := fmt.Sprintf("Message body %d", i)
f4 := frame.New("MESSAGE",
frame.Subscription, id,
frame.MessageId, messageId,
frame.Destination, destination)
if version == V12 && ackMode.ShouldAck() {
f4.Header.Add(frame.Ack, messageId)
}
f4.Body = []byte(bodyText)
err = rw.Write(f4)
c.Assert(err, IsNil)
if ackMode.ShouldAck() {
f5, _ := rw.Read()
c.Assert(f5.Command, Equals, "ACK")
if version == V12 {
c.Assert(f5.Header.Get(frame.Id), Equals, messageId)
} else {
c.Assert(f5.Header.Get("subscription"), Equals, id)
c.Assert(f5.Header.Get("message-id"), Equals, messageId)
}
}
}
f6, _ := rw.Read()
c.Assert(f6.Command, Equals, "UNSUBSCRIBE")
c.Assert(f6.Header.Get(frame.Receipt), Not(Equals), "")
c.Assert(f6.Header.Get(frame.Id), Equals, id)
err = rw.Write(frame.New(frame.RECEIPT, frame.ReceiptId, f6.Header.Get(frame.Receipt)))
c.Assert(err, IsNil)
f7, _ := rw.Read()
c.Assert(f7.Command, Equals, "DISCONNECT")
err = rw.Write(frame.New(frame.RECEIPT, frame.ReceiptId, f7.Header.Get(frame.Receipt)))
c.Assert(err, IsNil)
}()
var sub *Subscription
var err error
sub, err = conn.Subscribe("/queue/test-1", ackMode, opts...)
c.Assert(sub, NotNil)
c.Assert(err, IsNil)
for i := 1; i <= 5; i++ {
msg := <-sub.C
messageId := fmt.Sprintf("message-%d", i)
bodyText := fmt.Sprintf("Message body %d", i)
c.Assert(msg.Subscription, Equals, sub)
c.Assert(msg.Body, DeepEquals, []byte(bodyText))
c.Assert(msg.Destination, Equals, "/queue/test-1")
c.Assert(msg.Header.Get(frame.MessageId), Equals, messageId)
if version == V12 && ackMode.ShouldAck() {
c.Assert(msg.Header.Get(frame.Ack), Equals, messageId)
}
c.Assert(msg.ShouldAck(), Equals, ackMode.ShouldAck())
if msg.ShouldAck() {
err = msg.Conn.Ack(msg)
c.Assert(err, IsNil)
}
}
err = sub.Unsubscribe(SubscribeOpt.Header("custom", "true"))
c.Assert(err, IsNil)
err = conn.Disconnect()
c.Assert(err, IsNil)
}
func (s *StompSuite) TestTransaction(c *C) {
ackModes := []AckMode{AckAuto, AckClient, AckClientIndividual}
versions := []Version{V10, V11, V12}
aborts := []bool{false, true}
nacks := []bool{false, true}
for _, ackMode := range ackModes {
for _, version := range versions {
for _, abort := range aborts {
for _, nack := range nacks {
subscribeTransactionHelper(c, ackMode, version, abort, nack)
}
}
}
}
}
func subscribeTransactionHelper(c *C, ackMode AckMode, version Version, abort bool, nack bool) {
conn, rw := connectHelper(c, version)
stop := make(chan struct{})
go func() {
defer func() {
rw.Close()
close(stop)
}()
f3, err := rw.Read()
c.Assert(err, IsNil)
c.Assert(f3.Command, Equals, "SUBSCRIBE")
id, ok := f3.Header.Contains("id")
c.Assert(ok, Equals, true)
destination := f3.Header.Get("destination")
c.Assert(destination, Equals, "/queue/test-1")
ack := f3.Header.Get("ack")
c.Assert(ack, Equals, ackMode.String())
for i := 1; i <= 5; i++ {
messageId := fmt.Sprintf("message-%d", i)
bodyText := fmt.Sprintf("Message body %d", i)
f4 := frame.New("MESSAGE",
frame.Subscription, id,
frame.MessageId, messageId,
frame.Destination, destination)
if version == V12 && ackMode.ShouldAck() {
f4.Header.Add(frame.Ack, messageId)
}
f4.Body = []byte(bodyText)
err = rw.Write(f4)
c.Assert(err, IsNil)
beginFrame, err := rw.Read()
c.Assert(err, IsNil)
c.Assert(beginFrame, NotNil)
c.Check(beginFrame.Command, Equals, "BEGIN")
tx, ok := beginFrame.Header.Contains(frame.Transaction)
c.Assert(ok, Equals, true)
if ackMode.ShouldAck() {
f5, _ := rw.Read()
if nack && version.SupportsNack() {
c.Assert(f5.Command, Equals, "NACK")
} else {
c.Assert(f5.Command, Equals, "ACK")
}
if version == V12 {
c.Assert(f5.Header.Get(frame.Id), Equals, messageId)
} else {
c.Assert(f5.Header.Get("subscription"), Equals, id)
c.Assert(f5.Header.Get("message-id"), Equals, messageId)
}
c.Assert(f5.Header.Get("transaction"), Equals, tx)
}
sendFrame, _ := rw.Read()
c.Assert(sendFrame, NotNil)
c.Assert(sendFrame.Command, Equals, "SEND")
c.Assert(sendFrame.Header.Get("transaction"), Equals, tx)
commitFrame, _ := rw.Read()
c.Assert(commitFrame, NotNil)
if abort {
c.Assert(commitFrame.Command, Equals, "ABORT")
} else {
c.Assert(commitFrame.Command, Equals, "COMMIT")
}
c.Assert(commitFrame.Header.Get("transaction"), Equals, tx)
}
f6, _ := rw.Read()
c.Assert(f6.Command, Equals, "UNSUBSCRIBE")
c.Assert(f6.Header.Get(frame.Receipt), Not(Equals), "")
c.Assert(f6.Header.Get(frame.Id), Equals, id)
err = rw.Write(frame.New(frame.RECEIPT, frame.ReceiptId, f6.Header.Get(frame.Receipt)))
c.Assert(err, IsNil)
f7, _ := rw.Read()
c.Assert(f7.Command, Equals, "DISCONNECT")
err = rw.Write(frame.New(frame.RECEIPT, frame.ReceiptId, f7.Header.Get(frame.Receipt)))
c.Assert(err, IsNil)
}()
sub, err := conn.Subscribe("/queue/test-1", ackMode)
c.Assert(sub, NotNil)
c.Assert(err, IsNil)
for i := 1; i <= 5; i++ {
msg := <-sub.C
messageId := fmt.Sprintf("message-%d", i)
bodyText := fmt.Sprintf("Message body %d", i)
c.Assert(msg.Subscription, Equals, sub)
c.Assert(msg.Body, DeepEquals, []byte(bodyText))
c.Assert(msg.Destination, Equals, "/queue/test-1")
c.Assert(msg.Header.Get(frame.MessageId), Equals, messageId)
c.Assert(msg.ShouldAck(), Equals, ackMode.ShouldAck())
tx := msg.Conn.Begin()
c.Assert(tx.Id(), Not(Equals), "")
if msg.ShouldAck() {
if nack && version.SupportsNack() {
err = tx.Nack(msg)
c.Assert(err, IsNil)
} else {
err = tx.Ack(msg)
c.Assert(err, IsNil)
}
}
err = tx.Send("/queue/another-queue", "text/plain", []byte(bodyText))
c.Assert(err, IsNil)
if abort {
err = tx.Abort()
c.Assert(err, IsNil)
} else {
err = tx.Commit()
c.Assert(err, IsNil)
}
}
err = sub.Unsubscribe()
c.Assert(err, IsNil)
err = conn.Disconnect()
c.Assert(err, IsNil)
}
func (s *StompSuite) TestHeartBeatReadTimeout(c *C) {
conn, rw := createHeartBeatConnection(c, 100, 10000, time.Millisecond)
go func() {
f1, err := rw.Read()
c.Assert(err, IsNil)
c.Assert(f1.Command, Equals, "SUBSCRIBE")
messageFrame := frame.New("MESSAGE",
"destination", f1.Header.Get("destination"),
"message-id", "1",
"subscription", f1.Header.Get("id"))
messageFrame.Body = []byte("Message body")
err = rw.Write(messageFrame)
c.Assert(err, IsNil)
}()
sub, err := conn.Subscribe("/queue/test1", AckAuto)
c.Assert(err, IsNil)
c.Check(conn.readTimeout, Equals, 101*time.Millisecond)
//println("read timeout", conn.readTimeout.String())
msg, ok := <-sub.C
c.Assert(msg, NotNil)
c.Assert(ok, Equals, true)
msg, ok = <-sub.C
c.Assert(msg, NotNil)
c.Assert(ok, Equals, true)
c.Assert(msg.Err, NotNil)
c.Assert(msg.Err.Error(), Equals, "read timeout")
msg, ok = <-sub.C
c.Assert(msg, IsNil)
c.Assert(ok, Equals, false)
}
func (s *StompSuite) TestHeartBeatWriteTimeout(c *C) {
c.Skip("not finished yet")
conn, rw := createHeartBeatConnection(c, 10000, 100, time.Millisecond*1)
go func() {
f1, err := rw.Read()
c.Assert(err, IsNil)
c.Assert(f1, IsNil)
}()
time.Sleep(250)
err := conn.Disconnect()
c.Assert(err, IsNil)
}
func createHeartBeatConnection(
c *C,
readTimeout, writeTimeout int,
readTimeoutError time.Duration) (*Conn, *fakeReaderWriter) {
fc1, fc2 := testutil.NewFakeConn(c)
stop := make(chan struct{})
reader := frame.NewReader(fc2)
writer := frame.NewWriter(fc2)
go func() {
f1, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(f1.Command, Equals, "CONNECT")
c.Assert(f1.Header.Get("heart-beat"), Equals, "1,1")
f2 := frame.New("CONNECTED", "version", "1.2")
f2.Header.Add("heart-beat", fmt.Sprintf("%d,%d", readTimeout, writeTimeout))
err = writer.Write(f2)
c.Assert(err, IsNil)
close(stop)
}()
conn, err := Connect(fc1,
ConnOpt.HeartBeat(time.Millisecond, time.Millisecond),
ConnOpt.HeartBeatError(readTimeoutError))
c.Assert(conn, NotNil)
c.Assert(err, IsNil)
<-stop
return conn, &fakeReaderWriter{
reader: reader,
writer: writer,
conn: fc2,
}
}
// Testing Timeouts when receiving receipts
func sendFrameHelper(f *frame.Frame, c chan *frame.Frame) {
c <- f
}
//// GIVEN_TheTimeoutIsExceededBeforeTheReceiptIsReceived_WHEN_CallingReadReceiptWithTimeout_THEN_ReturnAnError
func (s *StompSuite) Test_TimeoutTriggers(c *C) {
const timeout = 1 * time.Millisecond
f := frame.Frame{}
request := writeRequest{
Frame: &f,
C: make(chan *frame.Frame),
}
err := readReceiptWithTimeout(request.C, timeout, ErrMsgReceiptTimeout)
c.Assert(err, NotNil)
}
//// GIVEN_TheChannelReceivesTheReceiptBeforeTheTimeoutExpires_WHEN_CallingReadReceiptWithTimeout_THEN_DoNotReturnAnError
func (s *StompSuite) Test_ChannelReceviesReceipt(c *C) {
const timeout = 1 * time.Second
f := frame.Frame{}
request := writeRequest{
Frame: &f,
C: make(chan *frame.Frame),
}
receipt := frame.Frame{
Command: frame.RECEIPT,
}
go sendFrameHelper(&receipt, request.C)
err := readReceiptWithTimeout(request.C, timeout, ErrMsgReceiptTimeout)
c.Assert(err, IsNil)
}
//// GIVEN_TheChannelReceivesMessage_AND_TheMessageIsNotAReceipt_WHEN_CallingReadReceiptWithTimeout_THEN_ReturnAnError
func (s *StompSuite) Test_ChannelReceviesNonReceipt(c *C) {
const timeout = 1 * time.Second
f := frame.Frame{}
request := writeRequest{
Frame: &f,
C: make(chan *frame.Frame),
}
receipt := frame.Frame{
Command: "NOT A RECEIPT",
}
go sendFrameHelper(&receipt, request.C)
err := readReceiptWithTimeout(request.C, timeout, ErrMsgReceiptTimeout)
c.Assert(err, NotNil)
}
//// GIVEN_TheTimeoutIsSetToZero_AND_TheMessageIsReceived_WHEN_CallingReadReceiptWithTimeout_THEN_DoNotReturnAnError
func (s *StompSuite) Test_ZeroTimeout(c *C) {
const timeout = 0 * time.Second
f := frame.Frame{}
request := writeRequest{
Frame: &f,
C: make(chan *frame.Frame),
}
receipt := frame.Frame{
Command: frame.RECEIPT,
}
go sendFrameHelper(&receipt, request.C)
err := readReceiptWithTimeout(request.C, timeout, ErrMsgReceiptTimeout)
c.Assert(err, IsNil)
}

View File

@ -0,0 +1,57 @@
package stomp
import (
"github.com/go-stomp/stomp/v3/frame"
)
// Error values
var (
ErrInvalidCommand = newErrorMessage("invalid command")
ErrInvalidFrameFormat = newErrorMessage("invalid frame format")
ErrUnsupportedVersion = newErrorMessage("unsupported version")
ErrCompletedTransaction = newErrorMessage("transaction is completed")
ErrNackNotSupported = newErrorMessage("NACK not supported in STOMP 1.0")
ErrNotReceivedMessage = newErrorMessage("cannot ack/nack a message, not from server")
ErrCannotNackAutoSub = newErrorMessage("cannot send NACK for a subscription with ack:auto")
ErrCompletedSubscription = newErrorMessage("subscription is unsubscribed")
ErrClosedUnexpectedly = newErrorMessage("connection closed unexpectedly")
ErrAlreadyClosed = newErrorMessage("connection already closed")
ErrMsgSendTimeout = newErrorMessage("msg send timeout")
ErrMsgReceiptTimeout = newErrorMessage("msg receipt timeout")
ErrDisconnectReceiptTimeout = newErrorMessage("disconnect receipt timeout")
ErrNilOption = newErrorMessage("nil option")
)
// StompError implements the Error interface, and provides
// additional information about a STOMP error.
type Error struct {
Message string
Frame *frame.Frame
}
func (e Error) Error() string {
return e.Message
}
func missingHeader(name string) Error {
return newErrorMessage("missing header: " + name)
}
func newErrorMessage(msg string) Error {
return Error{Message: msg}
}
func newError(f *frame.Frame) Error {
e := Error{Frame: f}
if f.Command == frame.ERROR {
if message := f.Header.Get(frame.Message); message != "" {
e.Message = message
} else {
e.Message = "ERROR frame, missing message header"
}
} else {
e.Message = "Unexpected frame: " + f.Command
}
return e
}

View File

@ -0,0 +1,242 @@
package stomp_test
import (
"fmt"
"net"
"time"
"github.com/go-stomp/stomp/v3"
"github.com/go-stomp/stomp/v3/frame"
)
func ExampleConn_Send(c *stomp.Conn) error {
// send with receipt and an optional header
err := c.Send(
"/queue/test-1", // destination
"text/plain", // content-type
[]byte("Message number 1"), // body
stomp.SendOpt.Receipt,
stomp.SendOpt.Header("expires", "2049-12-31 23:59:59"))
if err != nil {
return err
}
// send with no receipt and no optional headers
err = c.Send("/queue/test-2", "application/xml",
[]byte("<message>hello</message>"))
if err != nil {
return err
}
return nil
}
// Creates a new Header.
func ExampleNewHeader() {
/*
Creates a header that looks like the following:
login:scott
passcode:tiger
host:stompserver
accept-version:1.1,1.2
*/
h := frame.NewHeader(
"login", "scott",
"passcode", "tiger",
"host", "stompserver",
"accept-version", "1.1,1.2")
doSomethingWith(h)
}
// Creates a STOMP frame.
func ExampleNewFrame() {
/*
Creates a STOMP frame that looks like the following:
CONNECT
login:scott
passcode:tiger
host:stompserver
accept-version:1.1,1.2
^@
*/
f := frame.New("CONNECT",
"login", "scott",
"passcode", "tiger",
"host", "stompserver",
"accept-version", "1.1,1.2")
doSomethingWith(f)
}
func doSomethingWith(f ...interface{}) {
}
func doAnotherThingWith(f interface{}, g interface{}) {
}
func ExampleConn_Subscribe_1() error {
conn, err := stomp.Dial("tcp", "localhost:61613")
if err != nil {
return err
}
sub, err := conn.Subscribe("/queue/test-2", stomp.AckClient)
if err != nil {
return err
}
// receive 5 messages and then quit
for i := 0; i < 5; i++ {
msg := <-sub.C
if msg.Err != nil {
return msg.Err
}
doSomethingWith(msg)
// acknowledge the message
err = conn.Ack(msg)
if err != nil {
return err
}
}
err = sub.Unsubscribe()
if err != nil {
return err
}
return conn.Disconnect()
}
// Example of creating subscriptions with various options.
func ExampleConn_Subscribe_2(c *stomp.Conn) error {
// Subscribe to queue with automatic acknowledgement
sub1, err := c.Subscribe("/queue/test-1", stomp.AckAuto)
if err != nil {
return err
}
// Subscribe to queue with client acknowledgement and a custom header value
sub2, err := c.Subscribe("/queue/test-2", stomp.AckClient,
stomp.SubscribeOpt.Header("x-custom-header", "some-value"))
if err != nil {
return err
}
doSomethingWith(sub1, sub2)
return nil
}
func ExampleTransaction() error {
conn, err := stomp.Dial("tcp", "localhost:61613")
if err != nil {
return err
}
defer conn.Disconnect()
sub, err := conn.Subscribe("/queue/test-2", stomp.AckClient)
if err != nil {
return err
}
// receive 5 messages and then quit
for i := 0; i < 5; i++ {
msg := <-sub.C
if msg.Err != nil {
return msg.Err
}
tx := conn.Begin()
doAnotherThingWith(msg, tx)
tx.Send("/queue/another-one", "text/plain",
[]byte(fmt.Sprintf("Message #%d", i)), nil)
// acknowledge the message
err = tx.Ack(msg)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
}
err = sub.Unsubscribe()
if err != nil {
return err
}
return nil
}
// Example of connecting to a STOMP server using an existing network connection.
func ExampleConnect() error {
netConn, err := net.DialTimeout("tcp", "stomp.server.com:61613", 10*time.Second)
if err != nil {
return err
}
stompConn, err := stomp.Connect(netConn)
if err != nil {
return err
}
defer stompConn.Disconnect()
doSomethingWith(stompConn)
return nil
}
// Connect to a STOMP server using default options.
func ExampleDial_1() error {
conn, err := stomp.Dial("tcp", "192.168.1.1:61613")
if err != nil {
return err
}
err = conn.Send(
"/queue/test-1", // destination
"text/plain", // content-type
[]byte("Test message #1")) // body
if err != nil {
return err
}
return conn.Disconnect()
}
// Connect to a STOMP server that requires authentication. In addition,
// we are only prepared to use STOMP protocol version 1.1 or 1.2, and
// the virtual host is named "dragon". In this example the STOMP
// server also accepts a non-standard header called 'nonce'.
func ExampleDial_2() error {
conn, err := stomp.Dial("tcp", "192.168.1.1:61613",
stomp.ConnOpt.Login("scott", "leopard"),
stomp.ConnOpt.AcceptVersion(stomp.V11),
stomp.ConnOpt.AcceptVersion(stomp.V12),
stomp.ConnOpt.Host("dragon"),
stomp.ConnOpt.Header("nonce", "B256B26D320A"))
if err != nil {
return err
}
err = conn.Send(
"/queue/test-1", // destination
"text/plain", // content-type
[]byte("Test message #1")) // body
if err != nil {
return err
}
return conn.Disconnect()
}

View File

@ -0,0 +1,98 @@
package main
import (
"flag"
"fmt"
"os"
"github.com/go-stomp/stomp/v3"
)
const defaultPort = ":61613"
var serverAddr = flag.String("server", "localhost:61613", "STOMP server endpoint")
var messageCount = flag.Int("count", 10, "Number of messages to send/receive")
var queueName = flag.String("queue", "/queue/client_test", "Destination queue")
var helpFlag = flag.Bool("help", false, "Print help text")
var stop = make(chan bool)
// these are the default options that work with RabbitMQ
var options []func(*stomp.Conn) error = []func(*stomp.Conn) error{
stomp.ConnOpt.Login("guest", "guest"),
stomp.ConnOpt.Host("/"),
}
func main() {
flag.Parse()
if *helpFlag {
fmt.Fprintf(os.Stderr, "Usage of %s\n", os.Args[0])
flag.PrintDefaults()
os.Exit(1)
}
subscribed := make(chan bool)
go recvMessages(subscribed)
// wait until we know the receiver has subscribed
<-subscribed
go sendMessages()
<-stop
<-stop
}
func sendMessages() {
defer func() {
stop <- true
}()
conn, err := stomp.Dial("tcp", *serverAddr, options...)
if err != nil {
println("cannot connect to server", err.Error())
return
}
for i := 1; i <= *messageCount; i++ {
text := fmt.Sprintf("Message #%d", i)
err = conn.Send(*queueName, "text/plain",
[]byte(text), nil)
if err != nil {
println("failed to send to server", err)
return
}
}
println("sender finished")
}
func recvMessages(subscribed chan bool) {
defer func() {
stop <- true
}()
conn, err := stomp.Dial("tcp", *serverAddr, options...)
if err != nil {
println("cannot connect to server", err.Error())
return
}
sub, err := conn.Subscribe(*queueName, stomp.AckAuto)
if err != nil {
println("cannot subscribe to", *queueName, err.Error())
return
}
close(subscribed)
for i := 1; i <= *messageCount; i++ {
msg := <-sub.C
expectedText := fmt.Sprintf("Message #%d", i)
actualText := string(msg.Body)
if expectedText != actualText {
println("Expected:", expectedText)
println("Actual:", actualText)
}
}
println("receiver finished")
}

View File

@ -0,0 +1,8 @@
package frame
// Valid values for the "ack" header entry.
const (
AckAuto = "auto" // Client does not send ACK
AckClient = "client" // Client sends ACK/NACK
AckClientIndividual = "client-individual" // Client sends ACK/NACK for individual messages
)

View File

@ -0,0 +1,26 @@
package frame
// STOMP frame commands. Used upper case naming
// convention to avoid clashing with STOMP header names.
const (
// Connect commands.
CONNECT = "CONNECT"
STOMP = "STOMP"
CONNECTED = "CONNECTED"
// Client commands.
SEND = "SEND"
SUBSCRIBE = "SUBSCRIBE"
UNSUBSCRIBE = "UNSUBSCRIBE"
ACK = "ACK"
NACK = "NACK"
BEGIN = "BEGIN"
COMMIT = "COMMIT"
ABORT = "ABORT"
DISCONNECT = "DISCONNECT"
// Server commands.
MESSAGE = "MESSAGE"
RECEIPT = "RECEIPT"
ERROR = "ERROR"
)

View File

@ -0,0 +1,34 @@
package frame
import (
"strings"
"unsafe"
)
var (
replacerForEncodeValue = strings.NewReplacer(
"\\", "\\\\",
"\r", "\\r",
"\n", "\\n",
":", "\\c",
)
replacerForUnencodeValue = strings.NewReplacer(
"\\r", "\r",
"\\n", "\n",
"\\c", ":",
"\\\\", "\\",
)
)
// Reduce one allocation on copying bytes to string
func bytesToString(b []byte) string {
/* #nosec G103 */
return *(*string)(unsafe.Pointer(&b))
}
// Unencodes a header value using STOMP value encoding
// TODO: return error if invalid sequences found (eg "\t")
func unencodeValue(b []byte) (string, error) {
s := replacerForUnencodeValue.Replace(bytesToString(b))
return s, nil
}

View File

@ -0,0 +1,15 @@
package frame
import (
. "gopkg.in/check.v1"
)
type EncodeSuite struct{}
var _ = Suite(&EncodeSuite{})
func (s *EncodeSuite) TestUnencodeValue(c *C) {
val, err := unencodeValue([]byte(`Contains\r\nNewLine and \c colon and \\ backslash`))
c.Check(err, IsNil)
c.Check(val, Equals, "Contains\r\nNewLine and : colon and \\ backslash")
}

View File

@ -0,0 +1,9 @@
package frame
import (
"errors"
)
var (
ErrInvalidHeartBeat = errors.New("invalid heart-beat")
)

View File

@ -0,0 +1,38 @@
/*
Package frame provides functionality for manipulating STOMP frames.
*/
package frame
// A Frame represents a STOMP frame. A frame consists of a command
// followed by a collection of header entries, and then an optional
// body.
type Frame struct {
Command string
Header *Header
Body []byte
}
// New creates a new STOMP frame with the specified command and headers.
// The headers should contain an even number of entries. Each even index is
// the header name, and the odd indexes are the assocated header values.
func New(command string, headers ...string) *Frame {
f := &Frame{Command: command, Header: &Header{}}
for index := 0; index < len(headers); index += 2 {
f.Header.Add(headers[index], headers[index+1])
}
return f
}
// Clone creates a deep copy of the frame and its header. The cloned
// frame shares the body with the original frame.
func (f *Frame) Clone() *Frame {
fc := &Frame{Command: f.Command}
if f.Header != nil {
fc.Header = f.Header.Clone()
}
if f.Body != nil {
fc.Body = make([]byte, len(f.Body))
copy(fc.Body, f.Body)
}
return fc
}

View File

@ -0,0 +1,67 @@
package frame
import (
"testing"
. "gopkg.in/check.v1"
)
func TestFrame(t *testing.T) {
TestingT(t)
}
type FrameSuite struct{}
var _ = Suite(&FrameSuite{})
func (s *FrameSuite) TestNew(c *C) {
f := New("CCC")
c.Check(f.Header.Len(), Equals, 0)
c.Check(f.Command, Equals, "CCC")
f = New("DDDD", "abc", "def")
c.Check(f.Header.Len(), Equals, 1)
k, v := f.Header.GetAt(0)
c.Check(k, Equals, "abc")
c.Check(v, Equals, "def")
c.Check(f.Command, Equals, "DDDD")
f = New("EEEEEEE", "abc", "def", "hij", "klm")
c.Check(f.Command, Equals, "EEEEEEE")
c.Check(f.Header.Len(), Equals, 2)
k, v = f.Header.GetAt(0)
c.Check(k, Equals, "abc")
c.Check(v, Equals, "def")
k, v = f.Header.GetAt(1)
c.Check(k, Equals, "hij")
c.Check(v, Equals, "klm")
}
func (s *FrameSuite) TestClone(c *C) {
f1 := &Frame{
Command: "AAAA",
}
f2 := f1.Clone()
c.Check(f2.Command, Equals, f1.Command)
c.Check(f2.Header, IsNil)
c.Check(f2.Body, IsNil)
f1.Header = NewHeader("aaa", "1", "bbb", "2", "ccc", "3")
f2 = f1.Clone()
c.Check(f2.Header.Len(), Equals, f1.Header.Len())
for i := 0; i < f1.Header.Len(); i++ {
k1, v1 := f1.Header.GetAt(i)
k2, v2 := f2.Header.GetAt(i)
c.Check(k1, Equals, k2)
c.Check(v1, Equals, v2)
}
f1.Body = []byte{1, 2, 3, 4, 5, 6, 5, 4, 77, 88, 99, 0xaa, 0xbb, 0xcc, 0xff}
f2 = f1.Clone()
c.Check(len(f2.Body), Equals, len(f1.Body))
for i := 0; i < len(f1.Body); i++ {
c.Check(f1.Body[i], Equals, f2.Body[i])
}
}

View File

@ -0,0 +1,192 @@
package frame
import (
"strconv"
)
// STOMP header names. Some of the header
// names have commands with the same name
// (eg Ack, Message, Receipt). Commands use
// an upper-case naming convention, header
// names use pascal-case naming convention.
const (
ContentLength = "content-length"
ContentType = "content-type"
Receipt = "receipt"
AcceptVersion = "accept-version"
Host = "host"
Version = "version"
Login = "login"
Passcode = "passcode"
HeartBeat = "heart-beat"
Session = "session"
Server = "server"
Destination = "destination"
Id = "id"
Ack = "ack"
Transaction = "transaction"
ReceiptId = "receipt-id"
Subscription = "subscription"
MessageId = "message-id"
Message = "message"
/* TR-369 section 4.4.2.1 [Subscribing a USP Endpoint to a STOMP Destination] */
/*
R-STOMP.14: USP Agents that receive a subscribe-dest STOMP Header in the CONNECTED
frame MUST use that STOMP destination in the destination STOMP header when sending a
SUBSCRIBE frame.
*/
SubscribeDest = "subscribe-dest"
)
// A Header represents the header part of a STOMP frame.
// The header in a STOMP frame consists of a list of header entries.
// Each header entry is a key/value pair of strings.
//
// Normally a STOMP header only has one header entry for a given key, but
// the STOMP standard does allow for multiple header entries with the same
// key. In this case, the first header entry contains the value, and any
// subsequent header entries with the same key are ignored.
//
// Example header containing 6 header entries. Note that the second
// header entry with the key "comment" would be ignored.
//
// login:scott
// passcode:tiger
// host:stompserver
// accept-version:1.0,1.1,1.2
// comment:some comment
// comment:another comment
type Header struct {
slice []string
}
// NewHeader creates a new Header and populates it with header entries.
// This function expects an even number of strings as parameters. The
// even numbered indices are keys and the odd indices are values. See
// the example for more information.
func NewHeader(headerEntries ...string) *Header {
h := &Header{}
h.slice = append(h.slice, headerEntries...)
if len(h.slice)%2 != 0 {
h.slice = append(h.slice, "")
}
return h
}
// Add adds the key, value pair to the header.
func (h *Header) Add(key, value string) {
h.slice = append(h.slice, key, value)
}
// AddHeader adds all of the key value pairs in header to h.
func (h *Header) AddHeader(header *Header) {
if header != nil {
for i := 0; i < header.Len(); i++ {
key, value := header.GetAt(i)
h.Add(key, value)
}
}
}
// Set replaces the value of any existing header entry with the specified key.
// If there is no existing header entry with the specified key, a new
// header entry is added.
func (h *Header) Set(key, value string) {
if i, ok := h.index(key); ok {
h.slice[i+1] = value
} else {
h.slice = append(h.slice, key, value)
}
}
// Get gets the first value associated with the given key.
// If there are no values associated with the key, Get returns "".
func (h *Header) Get(key string) string {
value, _ := h.Contains(key)
return value
}
// GetAll returns all of the values associated with a given key.
// Normally there is only one header entry per key, but it is permitted
// to have multiple entries according to the STOMP standard.
func (h *Header) GetAll(key string) []string {
var values []string
for i := 0; i < len(h.slice); i += 2 {
if h.slice[i] == key {
values = append(values, h.slice[i+1])
}
}
return values
}
// Returns the header name and value at the specified index in
// the collection. The index should be in the range 0 <= index < Len(),
// a panic will occur if it is outside this range.
func (h *Header) GetAt(index int) (key, value string) {
index *= 2
return h.slice[index], h.slice[index+1]
}
// Contains gets the first value associated with the given key,
// and also returns a bool indicating whether the header entry
// exists.
//
// If there are no values associated with the key, Get returns ""
// for the value, and ok is false.
func (h *Header) Contains(key string) (value string, ok bool) {
var i int
if i, ok = h.index(key); ok {
value = h.slice[i+1]
}
return
}
// Del deletes all header entries with the specified key.
func (h *Header) Del(key string) {
for i, ok := h.index(key); ok; i, ok = h.index(key) {
h.slice = append(h.slice[:i], h.slice[i+2:]...)
}
}
// Len returns the number of header entries in the header.
func (h *Header) Len() int {
return len(h.slice) / 2
}
// Clone returns a deep copy of a Header.
func (h *Header) Clone() *Header {
hc := &Header{slice: make([]string, len(h.slice))}
copy(hc.slice, h.slice)
return hc
}
// ContentLength returns the value of the "content-length" header entry.
// If the "content-length" header is missing, then ok is false. If the
// "content-length" entry is present but is not a valid non-negative integer
// then err is non-nil.
func (h *Header) ContentLength() (value int, ok bool, err error) {
text, ok := h.Contains(ContentLength)
if !ok {
return 0, false, nil
}
n, err := strconv.ParseUint(text, 10, 32)
if err != nil {
return 0, true, err
}
value = int(n)
ok = true
return value, ok, nil
}
// Returns the index of a header key in Headers, and a bool to indicate
// whether it was found or not.
func (h *Header) index(key string) (int, bool) {
for i := 0; i < len(h.slice); i += 2 {
if h.slice[i] == key {
return i, true
}
}
return -1, false
}

View File

@ -0,0 +1,69 @@
package frame
import (
. "gopkg.in/check.v1"
)
func (s *FrameSuite) TestHeaderGetSetAddDel(c *C) {
h := &Header{}
c.Assert(h.Get("xxx"), Equals, "")
h.Add("xxx", "yyy")
c.Assert(h.Get("xxx"), Equals, "yyy")
h.Add("xxx", "zzz")
c.Assert(h.GetAll("xxx"), DeepEquals, []string{"yyy", "zzz"})
h.Set("xxx", "111")
c.Assert(h.Get("xxx"), Equals, "111")
h.Del("xxx")
c.Assert(h.Get("xxx"), Equals, "")
}
func (s *FrameSuite) TestHeaderClone(c *C) {
h := Header{}
h.Set("xxx", "yyy")
h.Set("yyy", "zzz")
hc := h.Clone()
h.Del("xxx")
h.Del("yyy")
c.Assert(hc.Get("xxx"), Equals, "yyy")
c.Assert(hc.Get("yyy"), Equals, "zzz")
}
func (s *FrameSuite) TestHeaderContains(c *C) {
h := NewHeader("xxx", "yyy", "zzz", "aaa", "xxx", "ccc")
v, ok := h.Contains("xxx")
c.Assert(v, Equals, "yyy")
c.Assert(ok, Equals, true)
v, ok = h.Contains("123")
c.Assert(v, Equals, "")
c.Assert(ok, Equals, false)
}
func (s *FrameSuite) TestContentLength(c *C) {
h := NewHeader("xxx", "yy", "content-length", "202", "zz", "123")
cl, ok, err := h.ContentLength()
c.Assert(cl, Equals, 202)
c.Assert(ok, Equals, true)
c.Assert(err, Equals, nil)
h.Set("content-length", "twenty")
cl, ok, err = h.ContentLength()
c.Assert(cl, Equals, 0)
c.Assert(ok, Equals, true)
c.Assert(err, NotNil)
h.Del("content-length")
cl, ok, err = h.ContentLength()
c.Assert(cl, Equals, 0)
c.Assert(ok, Equals, false)
c.Assert(err, IsNil)
}
func (s *FrameSuite) TestLit(c *C) {
_ = Frame{
Command: "CONNECT",
Header: NewHeader("login", "xxx", "passcode", "yyy"),
Body: []byte{1, 2, 3, 4},
}
}

View File

@ -0,0 +1,44 @@
package frame
import (
"math"
"regexp"
"strconv"
"strings"
"time"
)
var (
// Regexp for heart-beat header value
heartBeatRegexp = regexp.MustCompile("^[0-9]+,[0-9]+$")
)
const (
// Maximum number of milliseconds that can be represented
// in a time.Duration.
maxMilliseconds = math.MaxInt64 / int64(time.Millisecond)
)
// ParseHeartBeat parses the value of a STOMP heart-beat entry and
// returns two time durations. Returns an error if the heart-beat
// value is not in the correct format, or if the time durations are
// too big to be represented by the time.Duration type.
func ParseHeartBeat(heartBeat string) (time.Duration, time.Duration, error) {
if !heartBeatRegexp.MatchString(heartBeat) {
return 0, 0, ErrInvalidHeartBeat
}
slice := strings.Split(heartBeat, ",")
value1, err := strconv.ParseInt(slice[0], 10, 64)
if err != nil {
return 0, 0, ErrInvalidHeartBeat
}
value2, err := strconv.ParseInt(slice[1], 10, 64)
if err != nil {
return 0, 0, ErrInvalidHeartBeat
}
if value1 > maxMilliseconds || value2 > maxMilliseconds {
return 0, 0, ErrInvalidHeartBeat
}
return time.Duration(value1) * time.Millisecond,
time.Duration(value2) * time.Millisecond, nil
}

View File

@ -0,0 +1,77 @@
package frame
import (
"time"
. "gopkg.in/check.v1"
)
func (s *FrameSuite) TestParseHeartBeat(c *C) {
testCases := []struct {
Input string
ExpectedDuration1 time.Duration
ExpectedDuration2 time.Duration
ExpectError bool
ExpectedError error
}{
{
Input: "0,0",
ExpectedDuration1: 0,
ExpectedDuration2: 0,
},
{
Input: "20000,60000",
ExpectedDuration1: 20 * time.Second,
ExpectedDuration2: time.Minute,
},
{
Input: "86400000,31536000000",
ExpectedDuration1: 24 * time.Hour,
ExpectedDuration2: 365 * 24 * time.Hour,
},
{
Input: "20r000,60000",
ExpectedDuration1: 0,
ExpectedDuration2: 0,
ExpectedError: ErrInvalidHeartBeat,
},
{
Input: "99999999999999999999,60000",
ExpectedDuration1: 0,
ExpectedDuration2: 0,
ExpectedError: ErrInvalidHeartBeat,
},
{
Input: "60000,99999999999999999999",
ExpectedDuration1: 0,
ExpectedDuration2: 0,
ExpectedError: ErrInvalidHeartBeat,
},
{
Input: "-60000,60000",
ExpectedDuration1: 0,
ExpectedDuration2: 0,
ExpectedError: ErrInvalidHeartBeat,
},
{
Input: "60000,-60000",
ExpectedDuration1: 0,
ExpectedDuration2: 0,
ExpectedError: ErrInvalidHeartBeat,
},
}
for _, tc := range testCases {
d1, d2, err := ParseHeartBeat(tc.Input)
c.Check(d1, Equals, tc.ExpectedDuration1)
c.Check(d2, Equals, tc.ExpectedDuration2)
if tc.ExpectError || tc.ExpectedError != nil {
c.Check(err, NotNil)
if tc.ExpectedError != nil {
c.Check(err, Equals, tc.ExpectedError)
}
} else {
c.Check(err, IsNil)
}
}
}

View File

@ -0,0 +1,157 @@
package frame
import (
"bufio"
"bytes"
"errors"
"io"
)
const (
bufferSize = 4096
newline = byte(10)
cr = byte(13)
colon = byte(58)
nullByte = byte(0)
)
var (
ErrInvalidCommand = errors.New("invalid command")
ErrInvalidFrameFormat = errors.New("invalid frame format")
)
// The Reader type reads STOMP frames from an underlying io.Reader.
// The reader is buffered, and the size of the buffer is the maximum
// size permitted for the STOMP frame command and header section.
// A STOMP frame is rejected if its command and header section exceed
// the buffer size.
type Reader struct {
reader *bufio.Reader
}
// NewReader creates a Reader with the default underlying buffer size.
func NewReader(reader io.Reader) *Reader {
return NewReaderSize(reader, bufferSize)
}
// NewReaderSize creates a Reader with an underlying bufferSize
// of the specified size.
func NewReaderSize(reader io.Reader, bufferSize int) *Reader {
return &Reader{reader: bufio.NewReaderSize(reader, bufferSize)}
}
// Read a STOMP frame from the input. If the input contains one
// or more heart-beat characters and no frame, then nil will
// be returned for the frame. Calling programs should always check
// for a nil frame.
func (r *Reader) Read() (*Frame, error) {
commandSlice, err := r.readLine()
if err != nil {
return nil, err
}
if len(commandSlice) == 0 {
// received a heart-beat newline char (or cr-lf)
return nil, nil
}
f := New(string(commandSlice))
//println("RX:", f.Command)
switch f.Command {
// TODO(jpj): Is it appropriate to perform validation on the
// command at this point. Probably better to validate higher up,
// this way this type can be useful for any other non-STOMP protocols
// which happen to use the same frame format.
case CONNECT, STOMP, SEND, SUBSCRIBE,
UNSUBSCRIBE, ACK, NACK, BEGIN,
COMMIT, ABORT, DISCONNECT, CONNECTED,
MESSAGE, RECEIPT, ERROR:
// valid command
default:
return nil, ErrInvalidCommand
}
// read headers
for {
headerSlice, err := r.readLine()
if err != nil {
return nil, err
}
if len(headerSlice) == 0 {
// empty line means end of headers
break
}
index := bytes.IndexByte(headerSlice, colon)
if index <= 0 {
// colon is missing or header name is zero length
return nil, ErrInvalidFrameFormat
}
name, err := unencodeValue(headerSlice[0:index])
if err != nil {
return nil, err
}
value, err := unencodeValue(headerSlice[index+1:])
if err != nil {
return nil, err
}
//println(" ", name, ":", value)
f.Header.Add(name, value)
}
// get content length from the headers
if contentLength, ok, err := f.Header.ContentLength(); err != nil {
// happens if the content is malformed
return nil, err
} else if ok {
// content length specified in the header, so use that
f.Body = make([]byte, contentLength)
for bytesRead := 0; bytesRead < contentLength; {
n, err := r.reader.Read(f.Body[bytesRead:contentLength])
if err != nil {
return nil, err
}
bytesRead += n
}
// read the next byte and verify that it is a null byte
terminator, err := r.reader.ReadByte()
if err != nil {
return nil, err
}
if terminator != 0 {
return nil, ErrInvalidFrameFormat
}
} else {
f.Body, err = r.reader.ReadBytes(nullByte)
if err != nil {
return nil, err
}
// remove trailing null
f.Body = f.Body[0 : len(f.Body)-1]
}
// pass back frame
return f, nil
}
// read one line from input and strip off terminating LF or terminating CR-LF
func (r *Reader) readLine() (line []byte, err error) {
line, err = r.reader.ReadBytes(newline)
if err != nil {
return
}
switch {
case bytes.HasSuffix(line, crlfSlice):
line = line[0 : len(line)-len(crlfSlice)]
case bytes.HasSuffix(line, newlineSlice):
line = line[0 : len(line)-len(newlineSlice)]
}
return
}

View File

@ -0,0 +1,140 @@
package frame
import (
"io"
"strings"
"testing/iotest"
. "gopkg.in/check.v1"
)
type ReaderSuite struct{}
var _ = Suite(&ReaderSuite{})
func (s *ReaderSuite) TestConnect(c *C) {
reader := NewReader(strings.NewReader("CONNECT\nlogin:xxx\npasscode:yyy\n\n\x00"))
frame, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(frame, NotNil)
c.Assert(len(frame.Body), Equals, 0)
// ensure we are at the end of input
frame, err = reader.Read()
c.Assert(frame, IsNil)
c.Assert(err, Equals, io.EOF)
}
func (s *ReaderSuite) TestMultipleReads(c *C) {
text := "SEND\ndestination:xxx\n\nPayload\x00\n" +
"SEND\ndestination:yyy\ncontent-length:12\n" +
"dodgy\\c\\n\\cheader:dodgy\\c\\n\\r\\nvalue\\ \\\n\n" +
"123456789AB\x00\x00"
ioreaders := []io.Reader{
strings.NewReader(text),
iotest.DataErrReader(strings.NewReader(text)),
iotest.HalfReader(strings.NewReader(text)),
iotest.OneByteReader(strings.NewReader(text)),
}
for _, ioreader := range ioreaders {
// uncomment the following line to view the bytes being read
//ioreader = iotest.NewReadLogger("RX", ioreader)
reader := NewReader(ioreader)
frame, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(frame, NotNil)
c.Assert(frame.Command, Equals, "SEND")
c.Assert(frame.Header.Len(), Equals, 1)
v := frame.Header.Get("destination")
c.Assert(v, Equals, "xxx")
c.Assert(string(frame.Body), Equals, "Payload")
// now read a heart-beat from the input
frame, err = reader.Read()
c.Assert(err, IsNil)
c.Assert(frame, IsNil)
// this frame has content-length
frame, err = reader.Read()
c.Assert(err, IsNil)
c.Assert(frame, NotNil)
c.Assert(frame.Command, Equals, "SEND")
c.Assert(frame.Header.Len(), Equals, 3)
v = frame.Header.Get("destination")
c.Assert(v, Equals, "yyy")
n, ok, err := frame.Header.ContentLength()
c.Assert(n, Equals, 12)
c.Assert(ok, Equals, true)
c.Assert(err, IsNil)
k, v := frame.Header.GetAt(2)
c.Assert(k, Equals, "dodgy:\n:header")
c.Assert(v, Equals, "dodgy:\n\r\nvalue\\ \\")
c.Assert(string(frame.Body), Equals, "123456789AB\x00")
// ensure we are at the end of input
frame, err = reader.Read()
c.Assert(frame, IsNil)
c.Assert(err, Equals, io.EOF)
}
}
func (s *ReaderSuite) TestSendWithContentLength(c *C) {
reader := NewReader(strings.NewReader("SEND\ndestination:xxx\ncontent-length:5\n\n\x00\x01\x02\x03\x04\x00"))
frame, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(frame, NotNil)
c.Assert(frame.Command, Equals, "SEND")
c.Assert(frame.Header.Len(), Equals, 2)
v := frame.Header.Get("destination")
c.Assert(v, Equals, "xxx")
c.Assert(frame.Body, DeepEquals, []byte{0x00, 0x01, 0x02, 0x03, 0x04})
// ensure we are at the end of input
frame, err = reader.Read()
c.Assert(frame, IsNil)
c.Assert(err, Equals, io.EOF)
}
func (s *ReaderSuite) TestInvalidCommand(c *C) {
reader := NewReader(strings.NewReader("sEND\ndestination:xxx\ncontent-length:5\n\n\x00\x01\x02\x03\x04\x00"))
frame, err := reader.Read()
c.Check(frame, IsNil)
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "invalid command")
}
func (s *ReaderSuite) TestMissingNull(c *C) {
reader := NewReader(strings.NewReader("SEND\ndeestination:xxx\ncontent-length:5\n\n\x00\x01\x02\x03\x04\n"))
f, err := reader.Read()
c.Check(f, IsNil)
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "invalid frame format")
}
func (s *ReaderSuite) TestSubscribeWithoutId(c *C) {
c.Skip("TODO: implement validate")
reader := NewReader(strings.NewReader("SUBSCRIBE\ndestination:xxx\nIId:7\n\n\x00"))
frame, err := reader.Read()
c.Check(frame, IsNil)
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "missing header: id")
}
func (s *ReaderSuite) TestUnsubscribeWithoutId(c *C) {
c.Skip("TODO: implement validate")
reader := NewReader(strings.NewReader("UNSUBSCRIBE\nIId:7\n\n\x00"))
frame, err := reader.Read()
c.Check(frame, IsNil)
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "missing header: id")
}

View File

@ -0,0 +1,100 @@
package frame
import (
"bufio"
"io"
)
// slices used to write frames
var (
colonSlice = []byte{58} // colon ':'
crlfSlice = []byte{13, 10} // CR-LF
newlineSlice = []byte{10} // newline (LF)
nullSlice = []byte{0} // null character
)
// Writes STOMP frames to an underlying io.Writer.
type Writer struct {
writer *bufio.Writer
}
// Creates a new Writer object, which writes to an underlying io.Writer.
func NewWriter(writer io.Writer) *Writer {
return NewWriterSize(writer, 4096)
}
func NewWriterSize(writer io.Writer, bufferSize int) *Writer {
return &Writer{writer: bufio.NewWriterSize(writer, bufferSize)}
}
// Write the contents of a frame to the underlying io.Writer.
func (w *Writer) Write(f *Frame) error {
var err error
if f == nil {
// nil frame means send a heart-beat LF
_, err = w.writer.Write(newlineSlice)
if err != nil {
return err
}
} else {
_, err = w.writer.Write([]byte(f.Command))
if err != nil {
return err
}
_, err = w.writer.Write(newlineSlice)
if err != nil {
return err
}
//println("TX:", f.Command)
if f.Header != nil {
for i := 0; i < f.Header.Len(); i++ {
key, value := f.Header.GetAt(i)
//println(" ", key, ":", value)
_, err = replacerForEncodeValue.WriteString(w.writer, key)
if err != nil {
return err
}
_, err = w.writer.Write(colonSlice)
if err != nil {
return err
}
_, err = replacerForEncodeValue.WriteString(w.writer, value)
if err != nil {
return err
}
_, err = w.writer.Write(newlineSlice)
if err != nil {
return err
}
}
}
_, err = w.writer.Write(newlineSlice)
if err != nil {
return err
}
if len(f.Body) > 0 {
_, err = w.writer.Write(f.Body)
if err != nil {
return err
}
}
// write the final null (0) byte
_, err = w.writer.Write(nullSlice)
if err != nil {
return err
}
}
err = w.writer.Flush()
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,48 @@
package frame
import (
"bytes"
"strings"
. "gopkg.in/check.v1"
)
type WriterSuite struct{}
var _ = Suite(&WriterSuite{})
func (s *WriterSuite) TestWrites(c *C) {
var frameTexts = []string{
"CONNECT\nlogin:xxx\npasscode:yyy\n\n\x00",
"SEND\n" +
"destination:/queue/request\n" +
"tx:1\n" +
"content-length:5\n" +
"\n\x00\x01\x02\x03\x04\x00",
"SEND\ndestination:x\n\nABCD\x00",
"SEND\ndestination:x\ndodgy\\nheader\\c:abc\\n\\c\n\n123456\x00",
}
for _, frameText := range frameTexts {
writeToBufferAndCheck(c, frameText)
}
}
func writeToBufferAndCheck(c *C, frameText string) {
reader := NewReader(strings.NewReader(frameText))
frame, err := reader.Read()
c.Assert(err, IsNil)
c.Assert(frame, NotNil)
var b bytes.Buffer
var writer = NewWriter(&b)
err = writer.Write(frame)
c.Assert(err, IsNil)
newFrameText := b.String()
c.Check(newFrameText, Equals, frameText)
c.Check(b.String(), Equals, frameText)
}

View File

@ -1,8 +1,9 @@
module github.com/leandrofars/stomp
module github.com/go-stomp/stomp/v3
go 1.21.3
go 1.15
require (
github.com/go-stomp/stomp v2.1.4+incompatible // indirect
github.com/golang/mock v1.6.0
github.com/joho/godotenv v1.5.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
)

View File

@ -1,4 +1,34 @@
github.com/go-stomp/stomp v2.1.4+incompatible h1:D3SheUVDOz9RsjVWkoh/1iCOwD0qWjyeTZMUZ0EXg2Y=
github.com/go-stomp/stomp v2.1.4+incompatible/go.mod h1:VqCtqNZv1226A1/79yh+rMiFUcfY3R109np+7ke4n0c=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -0,0 +1,17 @@
package stomp
import (
"strconv"
"sync/atomic"
)
var _lastId uint64
// allocateId returns a unique number for the current
// process. Starts at one and increases. Used for
// allocating subscription ids, receipt ids,
// transaction ids, etc.
func allocateId() string {
id := atomic.AddUint64(&_lastId, 1)
return strconv.FormatUint(id, 10)
}

View File

@ -0,0 +1,43 @@
package stomp
import (
. "gopkg.in/check.v1"
"runtime"
)
// only used during testing, does not need to be thread-safe
func resetId() {
_lastId = 0
}
func (s *StompSuite) SetUpSuite(c *C) {
resetId()
runtime.GOMAXPROCS(runtime.NumCPU())
}
func (s *StompSuite) TearDownSuite(c *C) {
runtime.GOMAXPROCS(1)
}
func (s *StompSuite) TestAllocateId(c *C) {
c.Assert(allocateId(), Equals, "1")
c.Assert(allocateId(), Equals, "2")
ch := make(chan bool, 50)
for i := 0; i < 50; i++ {
go doAllocate(100, ch)
}
for i := 0; i < 50; i++ {
<-ch
}
c.Assert(allocateId(), Equals, "5003")
}
func doAllocate(count int, ch chan bool) {
for i := 0; i < count; i++ {
_ = allocateId()
}
ch <- true
}

View File

@ -0,0 +1,51 @@
package log
import (
"fmt"
stdlog "log"
)
var (
debugPrefix = "DEBUG: "
infoPrefix = "INFO: "
warnPrefix = "WARN: "
errorPrefix = "ERROR: "
)
func logf(prefix string, format string, value ...interface{}) {
_ = stdlog.Output(3, fmt.Sprintf(prefix+format+"\n", value...))
}
type StdLogger struct{}
func (s StdLogger) Debugf(format string, value ...interface{}) {
logf(debugPrefix, format, value...)
}
func (s StdLogger) Debug(message string) {
logf(debugPrefix, "%s", message)
}
func (s StdLogger) Infof(format string, value ...interface{}) {
logf(infoPrefix, format, value...)
}
func (s StdLogger) Info(message string) {
logf(infoPrefix, "%s", message)
}
func (s StdLogger) Warningf(format string, value ...interface{}) {
logf(warnPrefix, format, value...)
}
func (s StdLogger) Warning(message string) {
logf(warnPrefix, "%s", message)
}
func (s StdLogger) Errorf(format string, value ...interface{}) {
logf(errorPrefix, format, value...)
}
func (s StdLogger) Error(message string) {
logf(errorPrefix, "%s", message)
}

View File

@ -0,0 +1,13 @@
package stomp
type Logger interface {
Debugf(format string, value ...interface{})
Infof(format string, value ...interface{})
Warningf(format string, value ...interface{})
Errorf(format string, value ...interface{})
Debug(message string)
Info(message string)
Warning(message string)
Error(message string)
}

View File

@ -0,0 +1,68 @@
package stomp
import (
"io"
"github.com/go-stomp/stomp/v3/frame"
)
// A Message represents a message received from the STOMP server.
// In most cases a message corresponds to a single STOMP MESSAGE frame
// received from the STOMP server. If, however, the Err field is non-nil,
// then the message corresponds to a STOMP ERROR frame, or a connection
// error between the client and the server.
type Message struct {
// Indicates whether an error was received on the subscription.
// The error will contain details of the error. If the server
// sent an ERROR frame, then the Body, ContentType and Header fields
// will be populated according to the contents of the ERROR frame.
Err error
// Destination the message has been sent to.
Destination string
// MIME content type.
ContentType string // MIME content
// Connection that the message was received on.
Conn *Conn
// Subscription associated with the message.
Subscription *Subscription
// Optional header entries. When received from the server,
// these are the header entries received with the message.
Header *frame.Header
// The message body, which is an arbitrary sequence of bytes.
// The ContentType indicates the format of this body.
Body []byte // Content of message
}
// ShouldAck returns true if this message should be acknowledged to
// the STOMP server that sent it.
func (msg *Message) ShouldAck() bool {
if msg.Subscription == nil {
// not received from the server, so no acknowledgement required
return false
}
return msg.Subscription.AckMode() != AckAuto
}
func (msg *Message) Read(p []byte) (int, error) {
if len(msg.Body) == 0 {
return 0, io.EOF
}
n := copy(p, msg.Body)
msg.Body = msg.Body[n:]
return n, nil
}
func (msg *Message) ReadByte() (byte, error) {
if len(msg.Body) == 0 {
return 0, io.EOF
}
n := msg.Body[0]
msg.Body = msg.Body[1:]
return n, nil
}

View File

@ -0,0 +1,55 @@
package stomp
import (
"github.com/go-stomp/stomp/v3/frame"
)
// SendOpt contains options for for the Conn.Send and Transaction.Send functions.
var SendOpt struct {
// Receipt specifies that the client should request acknowledgement
// from the server before the send operation successfully completes.
Receipt func(*frame.Frame) error
// NoContentLength specifies that the SEND frame should not include
// a content-length header entry. By default the content-length header
// entry is always included, but some message brokers assign special
// meaning to STOMP frames that do not contain a content-length
// header entry. (In particular ActiveMQ interprets STOMP frames
// with no content-length as being a text message)
NoContentLength func(*frame.Frame) error
// Header provides the opportunity to include custom header entries
// in the SEND frame that the client sends to the server. This option
// can be specified multiple times if multiple custom header entries
// are required.
Header func(key, value string) func(*frame.Frame) error
}
func init() {
SendOpt.Receipt = func(f *frame.Frame) error {
if f.Command != frame.SEND {
return ErrInvalidCommand
}
id := allocateId()
f.Header.Set(frame.Receipt, id)
return nil
}
SendOpt.NoContentLength = func(f *frame.Frame) error {
if f.Command != frame.SEND {
return ErrInvalidCommand
}
f.Header.Del(frame.ContentLength)
return nil
}
SendOpt.Header = func(key, value string) func(*frame.Frame) error {
return func(f *frame.Frame) error {
if f.Command != frame.SEND {
return ErrInvalidCommand
}
f.Header.Add(key, value)
return nil
}
}
}

View File

@ -0,0 +1,88 @@
package client
import (
. "gopkg.in/check.v1"
)
// Test suite for testing that channels work the way I expect.
type ChannelSuite struct{}
var _ = Suite(&ChannelSuite{})
func (s *ChannelSuite) TestChannelWhenClosed(c *C) {
ch := make(chan int, 10)
ch <- 1
ch <- 2
select {
case i, ok := <-ch:
c.Assert(i, Equals, 1)
c.Assert(ok, Equals, true)
default:
c.Error("expected value on channel")
}
select {
case i := <-ch:
c.Assert(i, Equals, 2)
default:
c.Error("expected value on channel")
}
select {
case _ = <-ch:
c.Error("not expecting anything on the channel")
default:
}
ch <- 3
close(ch)
select {
case i := <-ch:
c.Assert(i, Equals, 3)
default:
c.Error("expected value on channel")
}
select {
case _, ok := <-ch:
c.Assert(ok, Equals, false)
default:
c.Error("expected value on channel")
}
select {
case _, ok := <-ch:
c.Assert(ok, Equals, false)
default:
c.Error("expected value on channel")
}
}
func (s *ChannelSuite) TestMultipleChannels(c *C) {
ch1 := make(chan int, 10)
ch2 := make(chan string, 10)
ch1 <- 1
select {
case i, ok := <-ch1:
c.Assert(i, Equals, 1)
c.Assert(ok, Equals, true)
case _ = <-ch2:
default:
c.Error("expected value on channel")
}
select {
case _ = <-ch1:
c.Error("not expected")
case _ = <-ch2:
c.Error("not expected")
default:
}
}

View File

@ -0,0 +1,7 @@
/*
Package client implements client connectivity in the STOMP server.
The key abstractions include a connection, a subscription and
a client request.
*/
package client

View File

@ -0,0 +1,12 @@
package client
import (
"gopkg.in/check.v1"
"testing"
)
// Runs all gocheck tests in this package.
// See other *_test.go files for gocheck tests.
func TestClient(t *testing.T) {
check.TestingT(t)
}

View File

@ -0,0 +1,25 @@
package client
import (
"time"
"github.com/go-stomp/stomp/v3"
)
// Contains information the client package needs from the
// rest of the STOMP server code.
type Config interface {
// Method to authenticate a login and associated passcode.
// Returns true if login/passcode is valid, false otherwise.
Authenticate(login, passcode string) bool
// Default duration for read/write heart-beat values. If this
// returns zero, no heart-beat will take place. If this value is
// larger than the maximu permitted value (which is more than
// 11 days, but less than 12 days), then it is truncated to the
// maximum permitted values.
HeartBeat() time.Duration
// Logger provides the logger for a client
Logger() stomp.Logger
}

View File

@ -0,0 +1,781 @@
package client
import (
"fmt"
"io"
"net"
"strconv"
"time"
"github.com/go-stomp/stomp/v3"
"github.com/go-stomp/stomp/v3/frame"
)
// Maximum number of pending frames allowed to a client.
// before a disconnect occurs. If the client cannot keep
// up with the server, we do not want the server to backlog
// pending frames indefinitely.
const maxPendingWrites = 16
// Maximum number of pending frames allowed before the read
// go routine starts blocking.
const maxPendingReads = 16
// Represents a connection with the STOMP client.
type Conn struct {
config Config
rw net.Conn // Network connection to client
writer *frame.Writer // Writes STOMP frames directly to the network connection
requestChannel chan Request // For sending requests to upper layer
subChannel chan *Subscription // Receives subscription messages for client
writeChannel chan *frame.Frame // Receives unacknowledged (topic) messages for client
readChannel chan *frame.Frame // Receives frames from the client
stateFunc func(c *Conn, f *frame.Frame) error // State processing function
writeTimeout time.Duration // Heart beat write timeout
version stomp.Version // Negotiated STOMP protocol version
closed bool // Is the connection closed
txStore *txStore // Stores transactions in progress
lastMsgId uint64 // last message-id value
subList *SubscriptionList // List of subscriptions requiring acknowledgement
subs map[string]*Subscription // All subscriptions, keyed by id
validator stomp.Validator // For validating STOMP frames
log stomp.Logger
}
// Creates a new client connection. The config parameter contains
// process-wide configuration parameters relevant to a client connection.
// The rw parameter is a network connection object for communicating with
// the client. All client requests are sent via the ch channel to the
// upper layer.
func NewConn(config Config, rw net.Conn, ch chan Request) *Conn {
c := &Conn{
config: config,
rw: rw,
requestChannel: ch,
subChannel: make(chan *Subscription, maxPendingWrites),
writeChannel: make(chan *frame.Frame, maxPendingWrites),
readChannel: make(chan *frame.Frame, maxPendingReads),
txStore: &txStore{},
subList: NewSubscriptionList(),
subs: make(map[string]*Subscription),
log: config.Logger(),
}
go c.readLoop()
go c.processLoop()
return c
}
// Write a frame to the connection without requiring
// any acknowledgement.
func (c *Conn) Send(f *frame.Frame) {
// Place the frame on the write channel. If the
// write channel is full, the caller will block.
c.writeChannel <- f
}
// Send and ERROR message to the client. The client
// connection will disconnect as soon as the ERROR
// message has been transmitted. The message header
// will be based on the contents of the err parameter.
func (c *Conn) SendError(err error) {
f := frame.New(frame.ERROR, frame.Message, err.Error())
c.Send(f) // will close after successful send
}
// Send an ERROR frame to the client and immediately. The error
// message is derived from err. If f is non-nil, it is the frame
// whose contents have caused the error. Include the receipt-id
// header if the frame contains a receipt header.
func (c *Conn) sendErrorImmediately(err error, f *frame.Frame) {
errorFrame := frame.New(frame.ERROR,
frame.Message, err.Error())
// Include a receipt-id header if the frame that prompted the error had
// a receipt header (as suggested by the STOMP protocol spec).
if f != nil {
if receipt, ok := f.Header.Contains(frame.Receipt); ok {
errorFrame.Header.Add(frame.ReceiptId, receipt)
}
}
// send the frame to the client, ignore any error condition
// because we are about to close the connection anyway
_ = c.sendImmediately(errorFrame)
}
// Sends a STOMP frame to the client immediately, does not push onto the
// write channel to be processed in turn.
func (c *Conn) sendImmediately(f *frame.Frame) error {
return c.writer.Write(f)
}
// Go routine for reading bytes from a client and assembling into
// STOMP frames. Also handles heart-beat read timeout. All read
// frames are pushed onto the read channel to be processed by the
// processLoop go-routine. This keeps all processing of frames for
// this connection on the one go-routine and avoids race conditions.
func (c *Conn) readLoop() {
reader := frame.NewReader(c.rw)
expectingConnect := true
readTimeout := time.Duration(0)
for {
if readTimeout == time.Duration(0) {
// infinite timeout
c.rw.SetReadDeadline(time.Time{})
} else {
c.rw.SetReadDeadline(time.Now().Add(readTimeout * 2))
}
f, err := reader.Read()
if err != nil {
if err == io.EOF {
c.log.Errorf("connection closed: %s", c.rw.RemoteAddr())
} else {
c.log.Errorf("read failed: %v : %s", err, c.rw.RemoteAddr())
}
// Close the read channel so that the processing loop will
// know to terminate, if it has not already done so. This is
// the only channel that we close, because it is the only one
// we know who is writing to.
close(c.readChannel)
return
}
if f == nil {
// if the frame is nil, then it is a heartbeat
continue
}
// If we are expecting a CONNECT or STOMP command, extract
// the heart-beat header and work out the read timeout.
// Note that the processing loop will duplicate this to
// some extent, but letting this go-routine work out its own
// read timeout means no synchronization is necessary.
if expectingConnect {
// Expecting a CONNECT or STOMP command, get the heart-beat
cx, _, err := getHeartBeat(f)
// Ignore the error condition and treat as no read timeout.
// The processing loop will handle the error again and
// process correctly.
if err == nil {
// Minimum value as per server config. If the client
// has requested shorter periods than this value, the
// server will insist on the longer time period.
min := asMilliseconds(c.config.HeartBeat(), maxHeartBeat)
// apply a minimum heartbeat
if cx > 0 && cx < min {
cx = min
}
readTimeout = time.Duration(cx) * time.Millisecond
expectingConnect = false
}
}
// Add the frame to the read channel. Note that this will block
// if we are reading from the client quicker than the server
// can process frames.
c.readChannel <- f
}
}
// Go routine that processes all read frames and all write frames.
// Having all processing in one go routine helps eliminate any race conditions.
func (c *Conn) processLoop() {
defer c.cleanupConn()
c.writer = frame.NewWriter(c.rw)
c.stateFunc = connecting
var timerChannel <-chan time.Time
var timer *time.Timer
for {
if c.writeTimeout > 0 && timer == nil {
timer = time.NewTimer(c.writeTimeout)
timerChannel = timer.C
}
select {
case f, ok := <-c.writeChannel:
if !ok {
// write channel has been closed, so
// exit go-routine (after cleaning up)
return
}
// have a frame to the client with
// no acknowledgement required (topic)
// stop the heart-beat timer
if timer != nil {
timer.Stop()
timer = nil
}
c.allocateMessageId(f, nil)
// write the frame to the client
err := c.writer.Write(f)
if err != nil {
// if there is an error writing to
// the client, there is not much
// point trying to send an ERROR frame,
// so just exit go-routine (after cleaning up)
return
}
// if the frame just sent to the client is an error
// frame, we disconnect
if f.Command == frame.ERROR {
// sent an ERROR frame, so disconnect
return
}
case f, ok := <-c.readChannel:
if !ok {
// read channel has been closed, so
// exit go-routine (after cleaning up)
return
}
// Just received a frame from the client.
// Validate the frame, checking for mandatory
// headers and prohibited headers.
if c.validator != nil {
err := c.validator.Validate(f)
if err != nil {
c.log.Warningf("validation failed for %s frame: %v", f.Command, err)
c.sendErrorImmediately(err, f)
return
}
}
// Pass to the appropriate function for handling
// according to the current state of the connection.
err := c.stateFunc(c, f)
if err != nil {
c.sendErrorImmediately(err, f)
return
}
case sub, ok := <-c.subChannel:
if !ok {
// subscription channel has been closed,
// so exit go-routine (after cleaning up)
return
}
// have a frame to the client which requires
// acknowledgement to the upper layer
// stop the heart-beat timer
if timer != nil {
timer.Stop()
timer = nil
}
// there is the possibility that the subscription
// has been unsubscribed just prior to receiving
// this, so we check
if _, ok = c.subs[sub.id]; ok {
// allocate a message-id, note that the
// subscription id has already been set
c.allocateMessageId(sub.frame, sub)
// write the frame to the client
err := c.writer.Write(sub.frame)
if err != nil {
// if there is an error writing to
// the client, there is not much
// point trying to send an ERROR frame,
// so just exit go-routine (after cleaning up)
return
}
if sub.ack == frame.AckAuto {
// subscription does not require acknowledgement,
// so send the subscription back the upper layer
// straight away
sub.frame = nil
c.requestChannel <- Request{Op: SubscribeOp, Sub: sub}
} else {
// subscription requires acknowledgement
c.subList.Add(sub)
}
} else {
// Subscription no longer exists, requeue
c.requestChannel <- Request{Op: RequeueOp, Frame: sub.frame}
}
case _ = <-timerChannel:
// stop the heart-beat timer
if timer != nil {
timer.Stop()
timer = nil
}
// write a heart-beat
err := c.writer.Write(nil)
if err != nil {
return
}
}
}
}
// Called when the connection is closing, and takes care of
// unsubscribing all subscriptions with the upper layer, and
// re-queueing all unacknowledged messages to the upper layer.
func (c *Conn) cleanupConn() {
// clean up any pending transactions
c.txStore.Init()
c.discardWriteChannelFrames()
// Unsubscribe every subscription known to the upper layer.
// This should be done before cleaning up the subscription
// channel. If we requeued messages before doing this,
// we might end up getting them back again.
for _, sub := range c.subs {
// Note that we only really need to send a request if the
// subscription does not have a frame, but for simplicity
// all subscriptions are unsubscribed from the upper layer.
c.requestChannel <- Request{Op: UnsubscribeOp, Sub: sub}
}
// Clear out the map of subscriptions
c.subs = nil
// Every subscription requiring acknowledgement has a frame
// that needs to be requeued in the upper layer
for sub := c.subList.Get(); sub != nil; sub = c.subList.Get() {
c.requestChannel <- Request{Op: RequeueOp, Frame: sub.frame}
}
// empty the subscription and write queue
c.discardWriteChannelFrames()
c.cleanupSubChannel()
// Tell the upper layer we are now disconnected
c.requestChannel <- Request{Op: DisconnectedOp, Conn: c}
// empty the subscription and write queue one more time
c.discardWriteChannelFrames()
c.cleanupSubChannel()
// Should not hurt to call this if it is already closed?
c.rw.Close()
}
// Discard anything on the write channel. These frames
// do not get acknowledged, and are either topic MESSAGE
// frames or ERROR frames.
func (c *Conn) discardWriteChannelFrames() {
for finished := false; !finished; {
select {
case _, ok := <-c.writeChannel:
if !ok {
finished = true
}
default:
finished = true
}
}
}
func (c *Conn) cleanupSubChannel() {
// Read the subscription channel until it is empty.
// Each frame should be requeued to the upper layer.
for finished := false; !finished; {
select {
case sub, ok := <-c.subChannel:
if !ok {
finished = true
} else {
c.requestChannel <- Request{Op: RequeueOp, Frame: sub.frame}
}
default:
finished = true
}
}
}
// Send a frame to the client, allocating necessary headers prior.
func (c *Conn) allocateMessageId(f *frame.Frame, sub *Subscription) {
if f.Command == frame.MESSAGE || f.Command == frame.ACK {
// allocate the value of message-id for this frame
c.lastMsgId++
messageId := strconv.FormatUint(c.lastMsgId, 10)
f.Header.Set(frame.MessageId, messageId)
f.Header.Set(frame.Id, messageId)
// if there is any requirement by the client to acknowledge, set
// the ack header as per STOMP 1.2
if sub == nil || sub.ack == frame.AckAuto {
f.Header.Del(frame.Ack)
} else {
f.Header.Set(frame.Ack, messageId)
}
}
}
// State function for expecting connect frame.
func connecting(c *Conn, f *frame.Frame) error {
switch f.Command {
case frame.CONNECT, frame.STOMP:
return c.handleConnect(f)
}
return notConnected
}
// State function for after connect frame received.
func connected(c *Conn, f *frame.Frame) error {
switch f.Command {
case frame.CONNECT, frame.STOMP:
return unexpectedCommand
case frame.DISCONNECT:
return c.handleDisconnect(f)
case frame.BEGIN:
return c.handleBegin(f)
case frame.ABORT:
return c.handleAbort(f)
case frame.COMMIT:
return c.handleCommit(f)
case frame.SEND:
return c.handleSend(f)
case frame.SUBSCRIBE:
return c.handleSubscribe(f)
case frame.UNSUBSCRIBE:
return c.handleUnsubscribe(f)
case frame.ACK:
return c.handleAck(f)
case frame.NACK:
return c.handleNack(f)
case frame.MESSAGE, frame.RECEIPT, frame.ERROR:
// should only be sent by the server, should not come from the client
return unexpectedCommand
}
return unknownCommand
}
func (c *Conn) handleConnect(f *frame.Frame) error {
var err error
if _, ok := f.Header.Contains(frame.Receipt); ok {
// CONNNECT and STOMP frames are not allowed to have
// a receipt header.
return receiptInConnect
}
// if either of these fields are absent, pass nil to the
// authenticator function.
login, _ := f.Header.Contains(frame.Login)
passcode, _ := f.Header.Contains(frame.Passcode)
if !c.config.Authenticate(login, passcode) {
// sleep to slow down a rogue client a little bit
c.log.Error("authentication failed")
time.Sleep(time.Second)
return authenticationFailed
}
c.version, err = determineVersion(f)
if err != nil {
c.log.Error("protocol version negotiation failed")
return err
}
c.validator = stomp.NewValidator(c.version)
if c.version == stomp.V10 {
// don't want to handle V1.0 at the moment
// TODO: get working for V1.0
c.log.Errorf("unsupported version %s", c.version)
return unsupportedVersion
}
cx, cy, err := getHeartBeat(f)
if err != nil {
c.log.Error("invalid heart-beat")
return err
}
// Minimum value as per server config. If the client
// has requested shorter periods than this value, the
// server will insist on the longer time period.
min := asMilliseconds(c.config.HeartBeat(), maxHeartBeat)
// apply a minimum heartbeat
if cx > 0 && cx < min {
cx = min
}
if cy > 0 && cy < min {
cy = min
}
// the read timeout has already been processed in the readLoop
// go-routine
c.writeTimeout = time.Duration(cy) * time.Millisecond
/* TR-369 section 4.4.1.1 [Connecting a USP Endpoint to the STOMP Server] */
/*
R-STOMP.4: USP Endpoints sending a STOMP frame MUST include (in addition to other
mandatory STOMP headers) an endpoint-id STOMP header containing the
Endpoint ID of the USP Endpoint sending the frame.
*/
endpointId := f.Header.Get("endpoint-id")
response := frame.New(frame.CONNECTED,
frame.Version, string(c.version),
frame.Server, "stompd/x.y.z", // TODO: get version
frame.HeartBeat, fmt.Sprintf("%d,%d", cy, cx),
frame.SubscribeDest, "oktopus/v1/agent/"+endpointId,
)
c.sendImmediately(response)
c.stateFunc = connected
// tell the upper layer we are connected
c.requestChannel <- Request{Op: ConnectedOp, Conn: c}
return nil
}
// Sends a RECEIPT frame to the client if the frame f contains
// a receipt header. If the frame does contain a receipt header,
// it will be removed from the frame.
func (c *Conn) sendReceiptImmediately(f *frame.Frame) error {
if receipt, ok := f.Header.Contains(frame.Receipt); ok {
// Remove the receipt header from the frame. This is handy
// for transactions, because the frame has its receipt
// header removed prior to entering the transaction store.
// When the frame is processed upon transaction commit, it
// will not have a receipt header anymore.
f.Header.Del(frame.Receipt)
return c.sendImmediately(frame.New(frame.RECEIPT,
frame.ReceiptId, receipt))
}
return nil
}
func (c *Conn) handleDisconnect(f *frame.Frame) error {
// As soon as we receive a DISCONNECT frame from a client, we do
// not want to send any more frames to that client, with the exception
// of a RECEIPT frame if the client has requested one.
// Ignore the error condition if we cannot send a RECEIPT frame,
// as the connection is about to close anyway.
_ = c.sendReceiptImmediately(f)
return nil
}
func (c *Conn) handleBegin(f *frame.Frame) error {
// the frame should already have been validated for the
// transaction header, but we check again here.
if transaction, ok := f.Header.Contains(frame.Transaction); ok {
// Send a receipt and remove the header
err := c.sendReceiptImmediately(f)
if err != nil {
return err
}
return c.txStore.Begin(transaction)
}
return missingHeader(frame.Transaction)
}
func (c *Conn) handleCommit(f *frame.Frame) error {
// the frame should already have been validated for the
// transaction header, but we check again here.
if transaction, ok := f.Header.Contains(frame.Transaction); ok {
// Send a receipt and remove the header
err := c.sendReceiptImmediately(f)
if err != nil {
return err
}
return c.txStore.Commit(transaction, func(f *frame.Frame) error {
// Call the state function (again) for each frame in the
// transaction. This time each frame is stripped of its transaction
// header (and its receipt header as well, if it had one).
return c.stateFunc(c, f)
})
}
return missingHeader(frame.Transaction)
}
func (c *Conn) handleAbort(f *frame.Frame) error {
// the frame should already have been validated for the
// transaction header, but we check again here.
if transaction, ok := f.Header.Contains(frame.Transaction); ok {
// Send a receipt and remove the header
err := c.sendReceiptImmediately(f)
if err != nil {
return err
}
return c.txStore.Abort(transaction)
}
return missingHeader(frame.Transaction)
}
func (c *Conn) handleSubscribe(f *frame.Frame) error {
id, ok := f.Header.Contains(frame.Id)
if !ok {
return missingHeader(frame.Id)
}
dest, ok := f.Header.Contains(frame.Destination)
if !ok {
return missingHeader(frame.Destination)
}
ack, ok := f.Header.Contains(frame.Ack)
if !ok {
ack = frame.AckAuto
}
sub, ok := c.subs[id]
if ok {
return subscriptionExists
}
sub = newSubscription(c, dest, id, ack)
c.subs[id] = sub
// send information about new subscription to upper layer
c.requestChannel <- Request{Op: SubscribeOp, Sub: sub}
return nil
}
func (c *Conn) handleUnsubscribe(f *frame.Frame) error {
id, ok := f.Header.Contains(frame.Id)
if !ok {
return missingHeader(frame.Id)
}
sub, ok := c.subs[id]
if !ok {
return subscriptionNotFound
}
// remove the subscription
delete(c.subs, id)
// tell the upper layer of the unsubscribe
c.requestChannel <- Request{Op: UnsubscribeOp, Sub: sub}
return nil
}
func (c *Conn) handleAck(f *frame.Frame) error {
var err error
var msgId string
if ack, ok := f.Header.Contains(frame.Ack); ok {
msgId = ack
} else if msgId, ok = f.Header.Contains(frame.MessageId); !ok {
return missingHeader(frame.MessageId)
}
// expecting message id to be a uint64
msgId64, err := strconv.ParseUint(msgId, 10, 64)
if err != nil {
return err
}
// Send a receipt and remove the header
err = c.sendReceiptImmediately(f)
if err != nil {
return err
}
if tx, ok := f.Header.Contains(frame.Transaction); ok {
// the transaction header is removed from the frame
err = c.txStore.Add(tx, f)
if err != nil {
return err
}
} else {
// handle any subscriptions that are acknowledged by this msg
c.subList.Ack(msgId64, func(s *Subscription) {
// remove frame from the subscription, it has been delivered
s.frame = nil
// let the upper layer know that this subscription
// is ready for another frame
c.requestChannel <- Request{Op: SubscribeOp, Sub: s}
})
}
return nil
}
func (c *Conn) handleNack(f *frame.Frame) error {
var err error
var msgId string
if ack, ok := f.Header.Contains(frame.Ack); ok {
msgId = ack
} else if msgId, ok = f.Header.Contains(frame.MessageId); !ok {
return missingHeader(frame.MessageId)
}
// expecting message id to be a uint64
msgId64, err := strconv.ParseUint(msgId, 10, 64)
if err != nil {
return err
}
// Send a receipt and remove the header
err = c.sendReceiptImmediately(f)
if err != nil {
return err
}
if tx, ok := f.Header.Contains(frame.Transaction); ok {
// the transaction header is removed from the frame
err = c.txStore.Add(tx, f)
if err != nil {
return err
}
} else {
// handle any subscriptions that are acknowledged by this msg
c.subList.Nack(msgId64, func(s *Subscription) {
// send frame back to upper layer for requeue
c.requestChannel <- Request{Op: RequeueOp, Frame: s.frame}
// remove frame from the subscription, it has been requeued
s.frame = nil
// let the upper layer know that this subscription
// is ready for another frame
c.requestChannel <- Request{Op: SubscribeOp, Sub: s}
})
}
return nil
}
// Handle a SEND frame received from the client. Note that
// this method is called after a SEND message is received,
// but also after a transaction commit.
func (c *Conn) handleSend(f *frame.Frame) error {
// Send a receipt and remove the header
err := c.sendReceiptImmediately(f)
if err != nil {
return err
}
if tx, ok := f.Header.Contains(frame.Transaction); ok {
// the transaction header is removed from the frame
err = c.txStore.Add(tx, f)
if err != nil {
return err
}
} else {
// not in a transaction
// change from SEND to MESSAGE
f.Command = frame.MESSAGE
c.requestChannel <- Request{Op: EnqueueOp, Frame: f}
}
return nil
}

View File

@ -0,0 +1,36 @@
package client
const (
notConnected = errorMessage("expected CONNECT or STOMP frame")
unexpectedCommand = errorMessage("unexpected frame command")
unknownCommand = errorMessage("unknown command")
receiptInConnect = errorMessage("receipt header prohibited in CONNECT or STOMP frame")
authenticationFailed = errorMessage("authentication failed")
txAlreadyInProgress = errorMessage("transaction already in progress")
txUnknown = errorMessage("unknown transaction")
unsupportedVersion = errorMessage("unsupported version")
subscriptionExists = errorMessage("subscription already exists")
subscriptionNotFound = errorMessage("subscription not found")
invalidFrameFormat = errorMessage("invalid frame format")
invalidCommand = errorMessage("invalid command")
unknownVersion = errorMessage("incompatible version")
notConnectFrame = errorMessage("operation valid for STOMP and CONNECT frames only")
invalidHeartBeat = errorMessage("invalid format for heart-beat")
invalidOperationForFrame = errorMessage("invalid operation for frame")
exceededMaxFrameSize = errorMessage("exceeded max frame size")
invalidHeaderValue = errorMessage("invalid header value")
)
type errorMessage string
func (e errorMessage) Error() string {
return string(e)
}
func missingHeader(name string) errorMessage {
return errorMessage("missing header: " + name)
}
func prohibitedHeader(name string) errorMessage {
return errorMessage("prohibited header: " + name)
}

View File

@ -0,0 +1,119 @@
package client
import (
"regexp"
"sort"
"strconv"
"strings"
"github.com/go-stomp/stomp/v3"
"github.com/go-stomp/stomp/v3/frame"
)
const (
// Maximum permitted heart-beat timeout, about 11.5 days.
// Any client CONNECT frame with a larger value than this
// will be rejected.
maxHeartBeat = 999999999
)
var (
// Regexp for heart-beat header value
heartBeatRegexp = regexp.MustCompile("^[0-9]{1,9},[0-9]{1,9}$")
)
// Determine the most acceptable version based on the accept-version
// header of a CONNECT or STOMP frame.
//
// Returns stomp.V10 if a CONNECT frame and the accept-version header is missing.
//
// Returns an error if the frame is not a CONNECT or STOMP frame, or
// if the accept-header is malformed or does not contain any compatible
// version numbers. Also returns an error if the accept-header is missing
// for a STOMP frame.
//
// Otherwise, returns the highest compatible version specified in the
// accept-version header. Compatible versions are V1_0, V1_1 and V1_2.
func determineVersion(f *frame.Frame) (version stomp.Version, err error) {
// frame can be CONNECT or STOMP with slightly different
// handling of accept-verion for each
isConnect := f.Command == frame.CONNECT
if !isConnect && f.Command != frame.STOMP {
err = notConnectFrame
return
}
// start with an error, and remove if successful
err = unknownVersion
if acceptVersion, ok := f.Header.Contains(frame.AcceptVersion); ok {
// sort the versions so that the latest version comes last
versions := strings.Split(acceptVersion, ",")
sort.Strings(versions)
for _, v := range versions {
switch stomp.Version(v) {
case stomp.V10:
version = stomp.V10
err = nil
case stomp.V11:
version = stomp.V11
err = nil
case stomp.V12:
version = stomp.V12
err = nil
}
}
} else {
// CONNECT frames can be missing the accept-version header,
// we assume V1.0 in this case. STOMP frames were introduced
// in V1.1, so they must have an accept-version header.
if isConnect {
// no "accept-version" header, so we assume 1.0
version = stomp.V10
err = nil
} else {
err = missingHeader(frame.AcceptVersion)
}
}
return
}
// Determine the heart-beat values in a CONNECT or STOMP frame.
//
// Returns 0,0 if the heart-beat header is missing. Otherwise
// returns the cx and cy values in the frame.
//
// Returns an error if the heart-beat header is malformed, or if
// the frame is not a CONNECT or STOMP frame. In this implementation,
// a heart-beat header is considered malformed if either cx or cy
// is greater than MaxHeartBeat.
func getHeartBeat(f *frame.Frame) (cx, cy int, err error) {
if f.Command != frame.CONNECT &&
f.Command != frame.STOMP &&
f.Command != frame.CONNECTED {
err = invalidOperationForFrame
return
}
if heartBeat, ok := f.Header.Contains(frame.HeartBeat); ok {
if !heartBeatRegexp.MatchString(heartBeat) {
err = invalidHeartBeat
return
}
// no error checking here because we are confident
// that everything will work because the regexp matches.
slice := strings.Split(heartBeat, ",")
value1, _ := strconv.ParseUint(slice[0], 10, 32)
value2, _ := strconv.ParseUint(slice[1], 10, 32)
cx = int(value1)
cy = int(value2)
} else {
// heart-beat header not present
// this else clause is not necessary, but
// included for clarity.
cx = 0
cy = 0
}
return
}

View File

@ -0,0 +1,82 @@
package client
import (
"github.com/go-stomp/stomp/v3"
"github.com/go-stomp/stomp/v3/frame"
. "gopkg.in/check.v1"
)
type FrameSuite struct{}
var _ = Suite(&FrameSuite{})
func (s *FrameSuite) TestDetermineVersion_V10_Connect(c *C) {
f := frame.New(frame.CONNECT)
version, err := determineVersion(f)
c.Check(err, IsNil)
c.Check(version, Equals, stomp.V10)
}
func (s *FrameSuite) TestDetermineVersion_V10_Stomp(c *C) {
// the "STOMP" command was introduced in V1.1, so it must
// have an accept-version header
f := frame.New(frame.STOMP)
_, err := determineVersion(f)
c.Check(err, Equals, missingHeader(frame.AcceptVersion))
}
func (s *FrameSuite) TestDetermineVersion_V11_Connect(c *C) {
f := frame.New(frame.CONNECT)
f.Header.Add(frame.AcceptVersion, "1.1")
version, err := determineVersion(f)
c.Check(version, Equals, stomp.V11)
c.Check(err, IsNil)
}
func (s *FrameSuite) TestDetermineVersion_MultipleVersions(c *C) {
f := frame.New(frame.CONNECT)
f.Header.Add(frame.AcceptVersion, "1.2,1.1,1.0,2.0")
version, err := determineVersion(f)
c.Check(version, Equals, stomp.V12)
c.Check(err, IsNil)
}
func (s *FrameSuite) TestDetermineVersion_IncompatibleVersions(c *C) {
f := frame.New(frame.CONNECT)
f.Header.Add(frame.AcceptVersion, "0.2,0.1,1.3,2.0")
version, err := determineVersion(f)
c.Check(version, Equals, stomp.Version(""))
c.Check(err, Equals, unknownVersion)
}
func (s *FrameSuite) TestHeartBeat(c *C) {
f := frame.New(frame.CONNECT,
frame.AcceptVersion, "1.2",
frame.Host, "XX")
// no heart-beat header means zero values
x, y, err := getHeartBeat(f)
c.Check(x, Equals, 0)
c.Check(y, Equals, 0)
c.Check(err, IsNil)
f.Header.Add("heart-beat", "123,456")
x, y, err = getHeartBeat(f)
c.Check(x, Equals, 123)
c.Check(y, Equals, 456)
c.Check(err, IsNil)
f.Header.Set(frame.HeartBeat, "invalid")
x, y, err = getHeartBeat(f)
c.Check(x, Equals, 0)
c.Check(y, Equals, 0)
c.Check(err, Equals, invalidHeartBeat)
f.Header.Del(frame.HeartBeat)
_, _, err = getHeartBeat(f)
c.Check(err, IsNil)
f.Command = frame.SEND
_, _, err = getHeartBeat(f)
c.Check(err, Equals, invalidOperationForFrame)
}

View File

@ -0,0 +1,32 @@
package client
import (
"strconv"
"github.com/go-stomp/stomp/v3/frame"
)
// Opcode used in client requests.
type RequestOp int
func (r RequestOp) String() string {
return strconv.Itoa(int(r))
}
// Valid value for client request opcodes.
const (
SubscribeOp RequestOp = iota // subscription ready
UnsubscribeOp // subscription not ready
EnqueueOp // send a message to a queue
RequeueOp // re-queue a message, not successfully sent
ConnectedOp // connection established
DisconnectedOp // connection disconnected
)
// Client requests received to be processed by main processing loop
type Request struct {
Op RequestOp // opcode for request
Sub *Subscription // SubscribeOp, UnsubscribeOp
Frame *frame.Frame // EnqueueOp, RequeueOp
Conn *Conn // ConnectedOp, DisconnectedOp
}

View File

@ -0,0 +1,84 @@
package client
import (
"github.com/go-stomp/stomp/v3/frame"
)
type Subscription struct {
conn *Conn
dest string
id string // client's subscription id
ack string // auto, client, client-individual
msgId uint64 // message-id (or ack) for acknowledgement
subList *SubscriptionList // am I in a list
frame *frame.Frame // message allocated to subscription
}
func newSubscription(c *Conn, dest string, id string, ack string) *Subscription {
return &Subscription{
conn: c,
dest: dest,
id: id,
ack: ack,
}
}
func (s *Subscription) Destination() string {
return s.dest
}
func (s *Subscription) Ack() string {
return s.ack
}
func (s *Subscription) Id() string {
return s.id
}
func (s *Subscription) IsAckedBy(msgId uint64) bool {
switch s.ack {
case frame.AckAuto:
return true
case frame.AckClient:
// any later message acknowledges an earlier message
return msgId >= s.msgId
case frame.AckClientIndividual:
return msgId == s.msgId
}
// should not get here
panic("invalid value for subscript.ack")
}
func (s *Subscription) IsNackedBy(msgId uint64) bool {
// TODO: not sure about this, interpreting NACK
// to apply to an individual message
return msgId == s.msgId
}
func (s *Subscription) SendQueueFrame(f *frame.Frame) {
s.setSubscriptionHeader(f)
s.frame = f
// let the connection deal with the subscription
// acknowledgement
s.conn.subChannel <- s
}
// Send a message frame to the client, as part of this
// subscription. Called within the queue when a message
// frame is available.
func (s *Subscription) SendTopicFrame(f *frame.Frame) {
s.setSubscriptionHeader(f)
// topics are handled differently, they just go
// straight to the client without acknowledgement
s.conn.writeChannel <- f
}
func (s *Subscription) setSubscriptionHeader(f *frame.Frame) {
if s.frame != nil {
panic("subscription already has a frame pending")
}
f.Header.Set(frame.Subscription, s.id)
}

View File

@ -0,0 +1,107 @@
package client
import (
"container/list"
)
// Maintains a list of subscriptions. Not thread-safe.
type SubscriptionList struct {
// TODO: implement linked list locally, adding next and prev
// pointers to the Subscription struct itself.
subs *list.List
}
func NewSubscriptionList() *SubscriptionList {
return &SubscriptionList{list.New()}
}
// Add a subscription to the back of the list. Will panic if
// the subscription destination does not match the subscription
// list destination. Will also panic if the subscription has already
// been added to a subscription list.
func (sl *SubscriptionList) Add(sub *Subscription) {
if sub.subList != nil {
panic("subscription is already in a subscription list")
}
sl.subs.PushBack(sub)
sub.subList = sl
}
// Gets the first subscription in the list, or nil if there
// are no subscriptions available. The subscription is removed
// from the list.
func (sl *SubscriptionList) Get() *Subscription {
if sl.subs.Len() == 0 {
return nil
}
front := sl.subs.Front()
sub := front.Value.(*Subscription)
sl.subs.Remove(front)
sub.subList = nil
return sub
}
// Removes the subscription from the list.
func (sl *SubscriptionList) Remove(s *Subscription) {
for e := sl.subs.Front(); e != nil; e = e.Next() {
if e.Value.(*Subscription) == s {
sl.subs.Remove(e)
s.subList = nil
return
}
}
}
// Search for a subscription with the specified id and remove it.
// Returns a pointer to the subscription if found, nil otherwise.
func (sl *SubscriptionList) FindByIdAndRemove(id string) *Subscription {
for e := sl.subs.Front(); e != nil; e = e.Next() {
sub := e.Value.(*Subscription)
if sub.id == id {
sl.subs.Remove(e)
return sub
}
}
return nil
}
// Finds all subscriptions in the subscription list that are acked by the
// specified message-id (or ack) header. The subscription is removed from
// the list and the callback function called for that subscription.
func (sl *SubscriptionList) Ack(msgId uint64, callback func(s *Subscription)) {
for e := sl.subs.Front(); e != nil; {
next := e.Next()
sub := e.Value.(*Subscription)
if sub.IsAckedBy(msgId) {
sl.subs.Remove(e)
callback(sub)
}
e = next
}
}
// Finds all subscriptions in the subscription list that are *nacked* by the
// specified message-id (or ack) header. The subscription is removed from
// the list and the callback function called for that subscription. Current
// understanding that all NACKs are individual, but not sure
func (sl *SubscriptionList) Nack(msgId uint64, callback func(s *Subscription)) {
for e := sl.subs.Front(); e != nil; {
next := e.Next()
sub := e.Value.(*Subscription)
if sub.IsNackedBy(msgId) {
sl.subs.Remove(e)
callback(sub)
}
e = next
}
}
// Invoke a callback function for every subscription in the list.
func (sl *SubscriptionList) ForEach(callback func(s *Subscription, isLast bool)) {
for e := sl.subs.Front(); e != nil; {
next := e.Next()
sub := e.Value.(*Subscription)
callback(sub, next == nil)
e = next
}
}

View File

@ -0,0 +1,113 @@
package client
import (
. "gopkg.in/check.v1"
)
type SubscriptionListSuite struct{}
var _ = Suite(&SubscriptionListSuite{})
func (s *SubscriptionListSuite) TestAddAndGet(c *C) {
sub1 := newSubscription(nil, "/dest", "1", "client")
sub2 := newSubscription(nil, "/dest", "2", "client")
sub3 := newSubscription(nil, "/dest", "3", "client")
sl := NewSubscriptionList()
sl.Add(sub1)
sl.Add(sub2)
sl.Add(sub3)
c.Check(sl.Get(), Equals, sub1)
// add the subscription again, should go to the back
sl.Add(sub1)
c.Check(sl.Get(), Equals, sub2)
c.Check(sl.Get(), Equals, sub3)
c.Check(sl.Get(), Equals, sub1)
c.Check(sl.Get(), IsNil)
}
func (s *SubscriptionListSuite) TestAddAndRemove(c *C) {
sub1 := newSubscription(nil, "/dest", "1", "client")
sub2 := newSubscription(nil, "/dest", "2", "client")
sub3 := newSubscription(nil, "/dest", "3", "client")
sl := NewSubscriptionList()
sl.Add(sub1)
sl.Add(sub2)
sl.Add(sub3)
c.Check(sl.subs.Len(), Equals, 3)
// now remove the second subscription
sl.Remove(sub2)
c.Check(sl.Get(), Equals, sub1)
c.Check(sl.Get(), Equals, sub3)
c.Check(sl.Get(), IsNil)
}
func (s *SubscriptionListSuite) TestAck(c *C) {
sub1 := &Subscription{dest: "/dest1", id: "1", ack: "client", msgId: 101}
sub2 := &Subscription{dest: "/dest3", id: "2", ack: "client-individual", msgId: 102}
sub3 := &Subscription{dest: "/dest4", id: "3", ack: "client", msgId: 103}
sub4 := &Subscription{dest: "/dest4", id: "4", ack: "client", msgId: 104}
sl := NewSubscriptionList()
sl.Add(sub1)
sl.Add(sub2)
sl.Add(sub3)
sl.Add(sub4)
c.Check(sl.subs.Len(), Equals, 4)
var subs []*Subscription
callback := func(s *Subscription) {
subs = append(subs, s)
}
// now remove the second subscription
sl.Ack(103, callback)
c.Assert(len(subs), Equals, 2)
c.Assert(subs[0], Equals, sub1)
c.Assert(subs[1], Equals, sub3)
c.Assert(sl.Get(), Equals, sub2)
c.Assert(sl.Get(), Equals, sub4)
c.Assert(sl.Get(), IsNil)
}
func (s *SubscriptionListSuite) TestNack(c *C) {
sub1 := &Subscription{dest: "/dest1", id: "1", ack: "client", msgId: 101}
sub2 := &Subscription{dest: "/dest3", id: "2", ack: "client-individual", msgId: 102}
sub3 := &Subscription{dest: "/dest4", id: "3", ack: "client", msgId: 103}
sub4 := &Subscription{dest: "/dest4", id: "4", ack: "client", msgId: 104}
sl := NewSubscriptionList()
sl.Add(sub1)
sl.Add(sub2)
sl.Add(sub3)
sl.Add(sub4)
c.Check(sl.subs.Len(), Equals, 4)
var subs []*Subscription
callback := func(s *Subscription) {
subs = append(subs, s)
}
// now remove the second subscription
sl.Nack(103, callback)
c.Assert(len(subs), Equals, 1)
c.Assert(subs[0], Equals, sub3)
c.Assert(sl.Get(), Equals, sub1)
c.Assert(sl.Get(), Equals, sub2)
c.Assert(sl.Get(), Equals, sub4)
c.Assert(sl.Get(), IsNil)
}

View File

@ -0,0 +1,65 @@
package client
import (
"container/list"
"github.com/go-stomp/stomp/v3/frame"
)
type txStore struct {
transactions map[string]*list.List
}
// Initializes a new store or clears out an existing store
func (txs *txStore) Init() {
txs.transactions = nil
}
func (txs *txStore) Begin(tx string) error {
if txs.transactions == nil {
txs.transactions = make(map[string]*list.List)
}
if _, ok := txs.transactions[tx]; ok {
return txAlreadyInProgress
}
txs.transactions[tx] = list.New()
return nil
}
func (txs *txStore) Abort(tx string) error {
if list, ok := txs.transactions[tx]; ok {
list.Init()
delete(txs.transactions, tx)
return nil
}
return txUnknown
}
// Commit causes all requests that have been queued for the transaction
// to be sent to the request channel for processing. Calls the commit
// function (commitFunc) in order for each request that is part of the
// transaction.
func (txs *txStore) Commit(tx string, commitFunc func(f *frame.Frame) error) error {
if list, ok := txs.transactions[tx]; ok {
for element := list.Front(); element != nil; element = list.Front() {
err := commitFunc(list.Remove(element).(*frame.Frame))
if err != nil {
return err
}
}
delete(txs.transactions, tx)
return nil
}
return txUnknown
}
func (txs *txStore) Add(tx string, f *frame.Frame) error {
if list, ok := txs.transactions[tx]; ok {
f.Header.Del(frame.Transaction)
list.PushBack(f)
return nil
}
return txUnknown
}

View File

@ -0,0 +1,81 @@
package client
import (
"github.com/go-stomp/stomp/v3/frame"
. "gopkg.in/check.v1"
)
type TxStoreSuite struct{}
var _ = Suite(&TxStoreSuite{})
func (s *TxStoreSuite) TestDoubleBegin(c *C) {
txs := txStore{}
err := txs.Begin("tx1")
c.Assert(err, IsNil)
err = txs.Begin("tx1")
c.Assert(err, Equals, txAlreadyInProgress)
}
func (s *TxStoreSuite) TestSuccessfulTx(c *C) {
txs := txStore{}
err := txs.Begin("tx1")
c.Check(err, IsNil)
err = txs.Begin("tx2")
c.Assert(err, IsNil)
f1 := frame.New(frame.MESSAGE,
frame.Destination, "/queue/1")
f2 := frame.New(frame.MESSAGE,
frame.Destination, "/queue/2")
f3 := frame.New(frame.MESSAGE,
frame.Destination, "/queue/3")
f4 := frame.New(frame.MESSAGE,
frame.Destination, "/queue/4")
err = txs.Add("tx1", f1)
c.Assert(err, IsNil)
err = txs.Add("tx1", f2)
c.Assert(err, IsNil)
err = txs.Add("tx1", f3)
c.Assert(err, IsNil)
err = txs.Add("tx2", f4)
var tx1 []*frame.Frame
txs.Commit("tx1", func(f *frame.Frame) error {
tx1 = append(tx1, f)
return nil
})
c.Check(err, IsNil)
var tx2 []*frame.Frame
err = txs.Commit("tx2", func(f *frame.Frame) error {
tx2 = append(tx2, f)
return nil
})
c.Check(err, IsNil)
c.Check(len(tx1), Equals, 3)
c.Check(tx1[0], Equals, f1)
c.Check(tx1[1], Equals, f2)
c.Check(tx1[2], Equals, f3)
c.Check(len(tx2), Equals, 1)
c.Check(tx2[0], Equals, f4)
// already committed, so should cause an error
err = txs.Commit("tx1", func(f *frame.Frame) error {
c.Fatal("should not be called")
return nil
})
c.Check(err, Equals, txUnknown)
}

View File

@ -0,0 +1,21 @@
package client
import (
"time"
)
// Convert a time.Duration to milliseconds in an integer.
// Returns the duration in milliseconds, or max if the
// duration is greater than max milliseconds.
func asMilliseconds(d time.Duration, max int) int {
if max < 0 {
max = 0
}
max64 := int64(max)
msec64 := int64(d / time.Millisecond)
if msec64 > max64 {
msec64 = max64
}
msec := int(msec64)
return msec
}

View File

@ -0,0 +1,23 @@
package client
import (
. "gopkg.in/check.v1"
"math"
"time"
)
type UtilSuite struct{}
var _ = Suite(&UtilSuite{})
func (s *UtilSuite) TestAsMilliseconds(c *C) {
d := time.Duration(30) * time.Millisecond
c.Check(asMilliseconds(d, math.MaxInt32), Equals, 30)
// approximately one year
d = time.Duration(365) * time.Duration(24) * time.Hour
c.Check(asMilliseconds(d, math.MaxInt32), Equals, math.MaxInt32)
d = time.Duration(365) * time.Duration(24) * time.Hour
c.Check(asMilliseconds(d, maxHeartBeat), Equals, maxHeartBeat)
}

View File

@ -0,0 +1,158 @@
package server
import (
"net"
"strings"
"time"
"github.com/go-stomp/stomp/v3"
"github.com/go-stomp/stomp/v3/frame"
"github.com/go-stomp/stomp/v3/server/client"
"github.com/go-stomp/stomp/v3/server/queue"
"github.com/go-stomp/stomp/v3/server/topic"
)
type requestProcessor struct {
server *Server
ch chan client.Request
tm *topic.Manager
qm *queue.Manager
stop bool // has stop been requested
}
func newRequestProcessor(server *Server) *requestProcessor {
proc := &requestProcessor{
server: server,
ch: make(chan client.Request, 128),
tm: topic.NewManager(),
}
if server.QueueStorage == nil {
proc.qm = queue.NewManager(queue.NewMemoryQueueStorage())
} else {
proc.qm = queue.NewManager(server.QueueStorage)
}
return proc
}
func (proc *requestProcessor) Serve(l net.Listener) error {
go proc.Listen(l)
for {
r := <-proc.ch
switch r.Op {
case client.SubscribeOp:
if isQueueDestination(r.Sub.Destination()) {
queue := proc.qm.Find(r.Sub.Destination())
// todo error handling
queue.Subscribe(r.Sub)
} else {
topic := proc.tm.Find(r.Sub.Destination())
topic.Subscribe(r.Sub)
}
case client.UnsubscribeOp:
if isQueueDestination(r.Sub.Destination()) {
queue := proc.qm.Find(r.Sub.Destination())
// todo error handling
queue.Unsubscribe(r.Sub)
} else {
topic := proc.tm.Find(r.Sub.Destination())
topic.Unsubscribe(r.Sub)
}
case client.EnqueueOp:
destination, ok := r.Frame.Header.Contains(frame.Destination)
if !ok {
// should not happen, already checked in lower layer
panic("missing destination")
}
if isQueueDestination(destination) {
queue := proc.qm.Find(destination)
queue.Enqueue(r.Frame)
} else {
topic := proc.tm.Find(destination)
topic.Enqueue(r.Frame)
}
case client.RequeueOp:
destination, ok := r.Frame.Header.Contains(frame.Destination)
if !ok {
// should not happen, already checked in lower layer
panic("missing destination")
}
// only requeue to queues, should never happen for topics
if isQueueDestination(destination) {
queue := proc.qm.Find(destination)
queue.Requeue(r.Frame)
}
}
}
// this is no longer required for go 1.1
panic("not reached")
}
func isQueueDestination(dest string) bool {
return strings.HasPrefix(dest, QueuePrefix)
}
func (proc *requestProcessor) Listen(l net.Listener) {
config := newConfig(proc.server)
timeout := time.Duration(0) // how long to sleep on accept failure
for {
rw, err := l.Accept()
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
if timeout == 0 {
timeout = 5 * time.Millisecond
} else {
timeout *= 2
}
if max := 5 * time.Second; timeout > max {
timeout = max
}
proc.server.Log.Infof("stomp: Accept error: %v; retrying in %v", err, timeout)
time.Sleep(timeout)
continue
}
return
}
timeout = 0
// TODO: need to pass Server to connection so it has access to
// configuration parameters.
_ = client.NewConn(config, rw, proc.ch)
}
// This is no longer required for go 1.1
panic("not reached")
}
type config struct {
server *Server
}
func newConfig(s *Server) *config {
return &config{server: s}
}
func (c *config) HeartBeat() time.Duration {
if c.server.HeartBeat == time.Duration(0) {
return DefaultHeartBeat
}
return c.server.HeartBeat
}
func (c *config) Authenticate(login, passcode string) bool {
if c.server.Authenticator != nil {
return c.server.Authenticator.Authenticate(login, passcode)
}
// no authentication defined
return true
}
func (c *config) Logger() stomp.Logger {
return c.server.Log
}

View File

@ -0,0 +1,23 @@
package queue
// Queue manager.
type Manager struct {
qstore Storage // handles queue storage
queues map[string]*Queue
}
// Create a queue manager with the specified queue storage mechanism
func NewManager(qstore Storage) *Manager {
qm := &Manager{qstore: qstore, queues: make(map[string]*Queue)}
return qm
}
// Finds the queue for the given destination, and creates it if necessary.
func (qm *Manager) Find(destination string) *Queue {
q, ok := qm.queues[destination]
if !ok {
q = newQueue(destination, qm.qstore)
qm.queues[destination] = q
}
return q
}

View File

@ -0,0 +1,21 @@
package queue
import (
. "gopkg.in/check.v1"
)
type ManagerSuite struct{}
var _ = Suite(&ManagerSuite{})
func (s *ManagerSuite) TestManager(c *C) {
mgr := NewManager(NewMemoryQueueStorage())
q1 := mgr.Find("/queue/1")
c.Assert(q1, NotNil)
q2 := mgr.Find("/queue/2")
c.Assert(q2, NotNil)
c.Assert(mgr.Find("/queue/1"), Equals, q1)
}

View File

@ -0,0 +1,70 @@
package queue
import (
"container/list"
"github.com/go-stomp/stomp/v3/frame"
)
// In-memory implementation of the QueueStorage interface.
type MemoryQueueStorage struct {
lists map[string]*list.List
}
func NewMemoryQueueStorage() Storage {
m := &MemoryQueueStorage{lists: make(map[string]*list.List)}
return m
}
func (m *MemoryQueueStorage) Enqueue(queue string, frame *frame.Frame) error {
l, ok := m.lists[queue]
if !ok {
l = list.New()
m.lists[queue] = l
}
l.PushBack(frame)
return nil
}
// Pushes a frame to the head of the queue. Sets
// the "message-id" header of the frame if it is not
// already set.
func (m *MemoryQueueStorage) Requeue(queue string, frame *frame.Frame) error {
l, ok := m.lists[queue]
if !ok {
l = list.New()
m.lists[queue] = l
}
l.PushFront(frame)
return nil
}
// Removes a frame from the head of the queue.
// Returns nil if no frame is available.
func (m *MemoryQueueStorage) Dequeue(queue string) (*frame.Frame, error) {
l, ok := m.lists[queue]
if !ok {
return nil, nil
}
element := l.Front()
if element == nil {
return nil, nil
}
return l.Remove(element).(*frame.Frame), nil
}
// Called at server startup. Allows the queue storage
// to perform any initialization.
func (m *MemoryQueueStorage) Start() {
m.lists = make(map[string]*list.List)
}
// Called prior to server shutdown. Allows the queue storage
// to perform any cleanup.
func (m *MemoryQueueStorage) Stop() {
m.lists = nil
}

View File

@ -0,0 +1,64 @@
package queue
import (
"github.com/go-stomp/stomp/v3/frame"
. "gopkg.in/check.v1"
)
type MemoryQueueSuite struct{}
var _ = Suite(&MemoryQueueSuite{})
func (s *MemoryQueueSuite) Test1(c *C) {
mq := NewMemoryQueueStorage()
mq.Start()
f1 := frame.New(frame.MESSAGE,
frame.Destination, "/queue/test",
frame.MessageId, "msg-001",
frame.Subscription, "1")
err := mq.Enqueue("/queue/test", f1)
c.Assert(err, IsNil)
f2 := frame.New(frame.MESSAGE,
frame.Destination, "/queue/test",
frame.MessageId, "msg-002",
frame.Subscription, "1")
err = mq.Enqueue("/queue/test", f2)
c.Assert(err, IsNil)
f3 := frame.New(frame.MESSAGE,
frame.Destination, "/queue/test2",
frame.MessageId, "msg-003",
frame.Subscription, "2")
err = mq.Enqueue("/queue/test2", f3)
c.Assert(err, IsNil)
// attempt to dequeue from a different queue
f, err := mq.Dequeue("/queue/other-queue")
c.Check(err, IsNil)
c.Assert(f, IsNil)
f, err = mq.Dequeue("/queue/test2")
c.Check(err, IsNil)
c.Assert(f, Equals, f3)
f, err = mq.Dequeue("/queue/test")
c.Check(err, IsNil)
c.Assert(f, Equals, f1)
f, err = mq.Dequeue("/queue/test")
c.Check(err, IsNil)
c.Assert(f, Equals, f2)
f, err = mq.Dequeue("/queue/test")
c.Check(err, IsNil)
c.Assert(f, IsNil)
f, err = mq.Dequeue("/queue/test2")
c.Check(err, IsNil)
c.Assert(f, IsNil)
}

View File

@ -0,0 +1,86 @@
/*
Package queue provides implementations of server-side queues.
*/
package queue
import (
"github.com/go-stomp/stomp/v3/frame"
"github.com/go-stomp/stomp/v3/server/client"
)
// Queue for storing message frames.
type Queue struct {
destination string
qstore Storage
subs *client.SubscriptionList
}
// Create a new queue -- called from the queue manager only.
func newQueue(destination string, qstore Storage) *Queue {
return &Queue{
destination: destination,
qstore: qstore,
subs: client.NewSubscriptionList(),
}
}
// Add a subscription to a queue. The subscription is removed
// whenever a frame is sent to the subscription and needs to
// be re-added when the subscription decides that the message
// has been received by the client.
func (q *Queue) Subscribe(sub *client.Subscription) error {
// see if there is a frame available for this subscription
f, err := q.qstore.Dequeue(sub.Destination())
if err != nil {
return err
}
if f == nil {
// no frame available, so add to the subscription list
q.subs.Add(sub)
} else {
// a frame is available, so send straight away without
// adding the subscription to the list
sub.SendQueueFrame(f)
}
return nil
}
// Unsubscribe a subscription.
func (q *Queue) Unsubscribe(sub *client.Subscription) {
q.subs.Remove(sub)
}
// Send a message to the queue. If a subscription is available
// to receive the message, it is sent to the subscription without
// making it to the queue. Otherwise, the message is queued until
// a message is available.
func (q *Queue) Enqueue(f *frame.Frame) error {
// find a subscription ready to receive the frame
sub := q.subs.Get()
if sub == nil {
// no subscription available, add to the queue
return q.qstore.Enqueue(q.destination, f)
} else {
// subscription is available, send it now without adding to queue
sub.SendQueueFrame(f)
}
return nil
}
// Send a message to the front of the queue, probably because it
// failed to be sent to a client. If a subscription is available
// to receive the message, it is sent to the subscription without
// making it to the queue. Otherwise, the message is queued until
// a message is available.
func (q *Queue) Requeue(f *frame.Frame) error {
// find a subscription ready to receive the frame
sub := q.subs.Get()
if sub == nil {
// no subscription available, add to the queue
return q.qstore.Requeue(q.destination, f)
} else {
// subscription is available, send it now without adding to queue
sub.SendQueueFrame(f)
}
return nil
}

View File

@ -0,0 +1,12 @@
package queue
import (
"gopkg.in/check.v1"
"testing"
)
// Runs all gocheck tests in this package.
// See other *_test.go files for gocheck tests.
func TestQueue(t *testing.T) {
check.TestingT(t)
}

View File

@ -0,0 +1,34 @@
package queue
import (
"github.com/go-stomp/stomp/v3/frame"
)
// Interface for queue storage. The intent is that
// different queue storage implementations can be
// used, depending on preference. Queue storage
// mechanisms could include in-memory, and various
// persistent storage mechanisms (eg file system, DB, etc)
type Storage interface {
// Pushes a MESSAGE frame to the end of the queue. Sets
// the "message-id" header of the frame before adding to
// the queue.
Enqueue(queue string, frame *frame.Frame) error
// Pushes a MESSAGE frame to the head of the queue. Sets
// the "message-id" header of the frame if it is not
// already set.
Requeue(queue string, frame *frame.Frame) error
// Removes a frame from the head of the queue.
// Returns nil if no frame is available.
Dequeue(queue string) (*frame.Frame, error)
// Called at server startup. Allows the queue storage
// to perform any initialization.
Start()
// Called prior to server shutdown. Allows the queue storage
// to perform any cleanup.
Stop()
}

View File

@ -0,0 +1,30 @@
package server
import (
"github.com/go-stomp/stomp/v3/frame"
)
// QueueStorage is an interface that abstracts the queue storage mechanism.
// The intent is that different queue storage implementations can be
// used, depending on preference. Queue storage mechanisms could include
// in-memory, and various persistent storage mechanisms (eg file system, DB, etc).
type QueueStorage interface {
// Enqueue adds a MESSAGE frame to the end of the queue.
Enqueue(queue string, frame *frame.Frame) error
// Requeue adds a MESSAGE frame to the head of the queue.
// This will happen if a client fails to acknowledge receipt.
Requeue(queue string, frame *frame.Frame) error
// Dequeue removes a frame from the head of the queue.
// Returns nil if no frame is available.
Dequeue(queue string) (*frame.Frame, error)
// Start is called at server startup. Allows the queue storage
// to perform any initialization.
Start()
// Stop is called prior to server shutdown. Allows the queue storage
// to perform any cleanup, such as flushing to disk.
Stop()
}

View File

@ -0,0 +1,89 @@
/*
Package server contains a simple STOMP server implementation.
*/
package server
import (
"net"
"time"
"github.com/go-stomp/stomp/v3"
"github.com/go-stomp/stomp/v3/internal/log"
)
// The STOMP server has the concept of queues and topics. A message
// sent to a queue destination will be transmitted to the next available
// client that has subscribed. A message sent to a topic will be
// transmitted to all subscribers that are currently subscribed to the
// topic.
//
// Destinations that start with this prefix are considered to be queues.
// Destinations that do not start with this prefix are considered to be topics.
const QueuePrefix = "/queue"
// Default server parameters.
const (
// Default address for listening for connections.
DefaultAddr = ":61613"
// Default read timeout for heart-beat.
// Override by setting Server.HeartBeat.
DefaultHeartBeat = time.Minute
)
// Interface for authenticating STOMP clients.
type Authenticator interface {
// Authenticate based on the given login and passcode, either of which might be nil.
// Returns true if authentication is successful, false otherwise.
Authenticate(login, passcode string) bool
}
// A Server defines parameters for running a STOMP server.
type Server struct {
Addr string // TCP address to listen on, DefaultAddr if empty
Authenticator Authenticator // Authenticates login/passcodes. If nil no authentication is performed
QueueStorage QueueStorage // Implementation of queue storage. If nil, in-memory queues are used.
HeartBeat time.Duration // Preferred value for heart-beat read/write timeout, if zero, then DefaultHeartBeat.
Log stomp.Logger
}
// ListenAndServe listens on the TCP network address addr and then calls Serve.
func ListenAndServe(addr string) error {
s := &Server{Addr: addr}
return s.ListenAndServe()
}
// Serve accepts incoming TCP connections on the listener l, creating a new
// STOMP service thread for each connection.
func Serve(l net.Listener) error {
s := &Server{}
return s.Serve(l)
}
// ListenAndServe listens on the TCP network address s.Addr and
// then calls Serve to handle requests on the incoming connections.
// If s.Addr is blank, then DefaultAddr is used.
func (s *Server) ListenAndServe() error {
addr := s.Addr
if addr == "" {
addr = DefaultAddr
}
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(l)
}
// Serve accepts incoming connections on the Listener l, creating a new
// service thread for each connection. The service threads read
// requests and then process each request.
func (s *Server) Serve(l net.Listener) error {
if s.Log == nil {
s.Log = log.StdLogger{}
}
proc := newRequestProcessor(s)
return proc.Serve(l)
}

View File

@ -0,0 +1,170 @@
package server
import (
"fmt"
"net"
"runtime"
"testing"
"time"
"github.com/go-stomp/stomp/v3"
. "gopkg.in/check.v1"
)
func TestServer(t *testing.T) {
TestingT(t)
}
type ServerSuite struct{}
var _ = Suite(&ServerSuite{})
func (s *ServerSuite) SetUpSuite(c *C) {
runtime.GOMAXPROCS(runtime.NumCPU())
}
func (s *ServerSuite) TearDownSuite(c *C) {
runtime.GOMAXPROCS(1)
}
func (s *ServerSuite) TestConnectAndDisconnect(c *C) {
addr := ":59091"
l, err := net.Listen("tcp", addr)
c.Assert(err, IsNil)
defer func() { l.Close() }()
go Serve(l)
conn, err := net.Dial("tcp", "127.0.0.1"+addr)
c.Assert(err, IsNil)
client, err := stomp.Connect(conn)
c.Assert(err, IsNil)
err = client.Disconnect()
c.Assert(err, IsNil)
conn.Close()
}
func (s *ServerSuite) TestHeartBeatingTolerance(c *C) {
// Heart beat should not close connection exactly after not receiving message after cx
// it should add a pretty decent amount of time to counter network delay of other timing issues
l, err := net.Listen("tcp", `127.0.0.1:0`)
c.Assert(err, IsNil)
defer func() { l.Close() }()
serv := Server{
Addr: l.Addr().String(),
Authenticator: nil,
QueueStorage: nil,
HeartBeat: 5 * time.Millisecond,
}
go serv.Serve(l)
conn, err := net.Dial("tcp", l.Addr().String())
c.Assert(err, IsNil)
defer conn.Close()
client, err := stomp.Connect(conn,
stomp.ConnOpt.HeartBeat(5 * time.Millisecond, 5 * time.Millisecond),
)
c.Assert(err, IsNil)
defer client.Disconnect()
time.Sleep(serv.HeartBeat * 20) // let it go for some time to allow client and server to exchange some heart beat
// Ensure the server has not closed his readChannel
err = client.Send("/topic/whatever", "text/plain", []byte("hello"))
c.Assert(err, IsNil)
}
func (s *ServerSuite) TestSendToQueuesAndTopics(c *C) {
ch := make(chan bool, 2)
println("number cpus:", runtime.NumCPU())
addr := ":59092"
l, err := net.Listen("tcp", addr)
c.Assert(err, IsNil)
defer func() { l.Close() }()
go Serve(l)
// channel to communicate that the go routine has started
started := make(chan bool)
count := 100
go runReceiver(c, ch, count, "/topic/test-1", addr, started)
<-started
go runReceiver(c, ch, count, "/topic/test-1", addr, started)
<-started
go runReceiver(c, ch, count, "/topic/test-2", addr, started)
<-started
go runReceiver(c, ch, count, "/topic/test-2", addr, started)
<-started
go runReceiver(c, ch, count, "/topic/test-1", addr, started)
<-started
go runReceiver(c, ch, count, "/queue/test-1", addr, started)
<-started
go runSender(c, ch, count, "/queue/test-1", addr, started)
<-started
go runSender(c, ch, count, "/queue/test-2", addr, started)
<-started
go runReceiver(c, ch, count, "/queue/test-2", addr, started)
<-started
go runSender(c, ch, count, "/topic/test-1", addr, started)
<-started
go runReceiver(c, ch, count, "/queue/test-3", addr, started)
<-started
go runSender(c, ch, count, "/queue/test-3", addr, started)
<-started
go runSender(c, ch, count, "/queue/test-4", addr, started)
<-started
go runSender(c, ch, count, "/topic/test-2", addr, started)
<-started
go runReceiver(c, ch, count, "/queue/test-4", addr, started)
<-started
for i := 0; i < 15; i++ {
<-ch
}
}
func runSender(c *C, ch chan bool, count int, destination, addr string, started chan bool) {
conn, err := net.Dial("tcp", "127.0.0.1"+addr)
c.Assert(err, IsNil)
client, err := stomp.Connect(conn)
c.Assert(err, IsNil)
started <- true
for i := 0; i < count; i++ {
client.Send(destination, "text/plain",
[]byte(fmt.Sprintf("%s test message %d", destination, i)))
//println("sent", i)
}
ch <- true
}
func runReceiver(c *C, ch chan bool, count int, destination, addr string, started chan bool) {
conn, err := net.Dial("tcp", "127.0.0.1"+addr)
c.Assert(err, IsNil)
client, err := stomp.Connect(conn)
c.Assert(err, IsNil)
sub, err := client.Subscribe(destination, stomp.AckAuto)
c.Assert(err, IsNil)
c.Assert(sub, NotNil)
started <- true
for i := 0; i < count; i++ {
msg := <-sub.C
expectedText := fmt.Sprintf("%s test message %d", destination, i)
c.Assert(msg.Body, DeepEquals, []byte(expectedText))
//println("received", i)
}
ch <- true
}

View File

@ -0,0 +1,24 @@
package topic
// Manager is a struct responsible for finding topics. Topics are
// not created by the package user, rather they are created on demand
// by the topic manager.
type Manager struct {
topics map[string]*Topic
}
// NewManager creates a new topic manager.
func NewManager() *Manager {
tm := &Manager{topics: make(map[string]*Topic)}
return tm
}
// Finds the topic for the given destination, and creates it if necessary.
func (tm *Manager) Find(destination string) *Topic {
t, ok := tm.topics[destination]
if !ok {
t = newTopic(destination)
tm.topics[destination] = t
}
return t
}

View File

@ -0,0 +1,21 @@
package topic
import (
. "gopkg.in/check.v1"
)
type ManagerSuite struct{}
var _ = Suite(&ManagerSuite{})
func (s *ManagerSuite) TestManager(c *C) {
mgr := NewManager()
t1 := mgr.Find("topic1")
c.Assert(t1, NotNil)
t2 := mgr.Find("topic2")
c.Assert(t2, NotNil)
c.Assert(mgr.Find("topic1"), Equals, t1)
}

View File

@ -0,0 +1,11 @@
package topic
import (
"github.com/go-stomp/stomp/v3/frame"
)
// Subscription is the interface that wraps a subscriber to a topic.
type Subscription interface {
// Send a message frame to the topic subscriber.
SendTopicFrame(f *frame.Frame)
}

View File

@ -0,0 +1,12 @@
package topic
import (
"gopkg.in/check.v1"
"testing"
)
// Runs all gocheck tests in this package.
// See other *_test.go files for gocheck tests.
func Test(t *testing.T) {
check.TestingT(t)
}

View File

@ -0,0 +1,73 @@
/*
Package topic provides implementations of server-side topics.
*/
package topic
import (
"container/list"
"github.com/go-stomp/stomp/v3/frame"
)
// A Topic is used for broadcasting to subscribed clients.
// In contrast to a queue, when a message is sent to a topic,
// that message is transmitted to all subscribed clients.
type Topic struct {
destination string
subs *list.List
}
// Create a new topic -- called from the topic manager only.
func newTopic(destination string) *Topic {
return &Topic{
destination: destination,
subs: list.New(),
}
}
// Subscribe adds a subscription to a topic. Any message sent to the
// topic will be transmitted to the subscription's client until
// unsubscription occurs.
func (t *Topic) Subscribe(sub Subscription) {
t.subs.PushBack(sub)
}
// Unsubscribe causes a subscription to be removed from the topic.
func (t *Topic) Unsubscribe(sub Subscription) {
for e := t.subs.Front(); e != nil; e = e.Next() {
if sub == e.Value.(Subscription) {
t.subs.Remove(e)
return
}
}
}
// Enqueue send a message to the topic. All subscriptions receive a copy
// of the message.
func (t *Topic) Enqueue(f *frame.Frame) {
switch t.subs.Len() {
case 0:
// no subscription, so do nothing
case 1:
// only one subscription, so can send the frame
// without copying
sub := t.subs.Front().Value.(Subscription)
sub.SendTopicFrame(f)
default:
// more than one subscription, send clone for
// all subscriptions except the last, which can
// have the frame without copying
for e := t.subs.Front(); e != nil; e = e.Next() {
sub := e.Value.(Subscription)
if e.Next() == nil {
// the last in the list, send the frame
// without copying
sub.SendTopicFrame(f)
} else {
sub.SendTopicFrame(f.Clone())
}
}
}
}

View File

@ -0,0 +1,63 @@
package topic
import (
"github.com/go-stomp/stomp/v3/frame"
. "gopkg.in/check.v1"
)
type TopicSuite struct{}
var _ = Suite(&TopicSuite{})
func (s *TopicSuite) TestTopicWithoutSubscription(c *C) {
topic := newTopic("destination")
f := frame.New(frame.MESSAGE,
frame.Destination, "destination")
topic.Enqueue(f)
}
func (s *TopicSuite) TestTopicWithOneSubscription(c *C) {
sub := &fakeSubscription{}
topic := newTopic("destination")
topic.Subscribe(sub)
f := frame.New(frame.MESSAGE,
frame.Destination, "destination")
topic.Enqueue(f)
c.Assert(len(sub.Frames), Equals, 1)
c.Assert(sub.Frames[0], Equals, f)
}
func (s *TopicSuite) TestTopicWithTwoSubscriptions(c *C) {
sub1 := &fakeSubscription{}
sub2 := &fakeSubscription{}
topic := newTopic("destination")
topic.Subscribe(sub1)
topic.Subscribe(sub2)
f := frame.New(frame.MESSAGE,
frame.Destination, "destination",
"xxx", "yyy")
topic.Enqueue(f)
c.Assert(len(sub1.Frames), Equals, 1)
c.Assert(len(sub2.Frames), Equals, 1)
c.Assert(sub1.Frames[0], Not(Equals), f)
c.Assert(sub2.Frames[0], Equals, f)
}
type fakeSubscription struct {
// frames received by the subscription
Frames []*frame.Frame
}
func (s *fakeSubscription) SendTopicFrame(f *frame.Frame) {
s.Frames = append(s.Frames, f)
}

View File

@ -0,0 +1,26 @@
/*
Package stomp provides operations that allow communication with a message broker that supports the STOMP protocol.
STOMP is the Streaming Text-Oriented Messaging Protocol. See http://stomp.github.com/ for more details.
This package provides support for all STOMP protocol features in the STOMP protocol specifications,
versions 1.0, 1.1 and 1.2. These features including protocol negotiation, heart-beating, value encoding,
and graceful shutdown.
Connecting to a STOMP server is achieved using the stomp.Dial function, or the stomp.Connect function. See
the examples section for a summary of how to use these functions. Both functions return a stomp.Conn object
for subsequent interaction with the STOMP server.
Once a connection (stomp.Conn) is created, it can be used to send messages to the STOMP server, or create
subscriptions for receiving messages from the STOMP server. Transactions can be created to send multiple
messages and/ or acknowledge multiple received messages from the server in one, atomic transaction. The
examples section has examples of using subscriptions and transactions.
The client program can instruct the stomp.Conn to gracefully disconnect from the STOMP server using the
Disconnect method. This will perform a graceful shutdown sequence as specified in the STOMP specification.
Source code and other details for the project are available at GitHub:
https://github.com/go-stomp/stomp
*/
package stomp

View File

@ -0,0 +1,18 @@
package stomp
import (
"testing"
"gopkg.in/check.v1"
)
// Runs all gocheck tests in this package.
// See other *_test.go files for gocheck tests.
func TestStomp(t *testing.T) {
check.Suite(&StompSuite{t})
check.TestingT(t)
}
type StompSuite struct {
t *testing.T
}

View File

@ -0,0 +1,63 @@
/*
A simple, stand-alone STOMP server.
TODO: graceful shutdown
TODO: UNIX daemon functionality
TODO: Windows service functionality (if possible?)
TODO: Logging options (syslog, windows event log)
*/
package main
import (
"flag"
"fmt"
"log"
"net"
"os"
"github.com/go-stomp/stomp/v3/server"
)
// TODO: experimenting with ways to gracefully shutdown the server,
// at the moment it just dies ungracefully on SIGINT.
/*
func main() {
// create a channel for listening for termination signals
stopChannel := newStopChannel()
for {
select {
case sig := <-stopChannel:
log.Println("received signal:", sig)
break
}
}
}
*/
var listenAddr = flag.String("addr", ":61613", "Listen address")
var helpFlag = flag.Bool("help", false, "Show this help text")
func main() {
flag.Parse()
if *helpFlag {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
os.Exit(1)
}
l, err := net.Listen("tcp", *listenAddr)
if err != nil {
log.Fatalf("failed to listen: %s", err.Error())
}
defer func() { l.Close() }()
log.Println("listening on", l.Addr().Network(), l.Addr().String())
server.Serve(l)
}

View File

@ -0,0 +1,19 @@
package main
import (
"os"
"os/signal"
)
// newStopChannel creates a channel for receiving signals
// for stopping the program. Calls an os-dependent setupStopSignals
// function.
func newStopChannel() chan os.Signal {
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt)
// os dependent between windows and unix
setupStopSignals(c)
return c
}

View File

@ -0,0 +1,17 @@
package main
import (
"os"
"os/signal"
"syscall"
)
// setupStopSignals sets up UNIX-specific signals for terminating
// the program
func setupStopSignals(signalChannel chan os.Signal) {
// TODO: not sure whether SIGHUP should be used here, only if not in
// daemon mode
signal.Notify(signalChannel, syscall.SIGHUP)
signal.Notify(signalChannel, syscall.SIGTERM)
}

View File

@ -0,0 +1,13 @@
package main
import (
"os"
)
func signals(signalChannel chan os.Signal) {
// Windows has no other signals other than os.Interrupt
// TODO: What might be good here is to simulate a signal
// if running as a Windows service and the stop request is
// received. Not sure how to do this though.
}

Binary file not shown.

View File

@ -0,0 +1,42 @@
package stomp
import (
"github.com/go-stomp/stomp/v3/frame"
)
// SubscribeOpt contains options for for the Conn.Subscribe function.
var SubscribeOpt struct {
// Id provides the opportunity to specify the value of the "id" header
// entry in the STOMP SUBSCRIBE frame.
//
// If the client program does specify the value for "id",
// it is responsible for choosing a unique value.
Id func(id string) func(*frame.Frame) error
// Header provides the opportunity to include custom header entries
// in the SUBSCRIBE frame that the client sends to the server.
Header func(key, value string) func(*frame.Frame) error
}
func init() {
SubscribeOpt.Id = func(id string) func(*frame.Frame) error {
return func(f *frame.Frame) error {
if f.Command != frame.SUBSCRIBE {
return ErrInvalidCommand
}
f.Header.Set(frame.Id, id)
return nil
}
}
SubscribeOpt.Header = func(key, value string) func(*frame.Frame) error {
return func(f *frame.Frame) error {
if f.Command != frame.SUBSCRIBE &&
f.Command != frame.UNSUBSCRIBE {
return ErrInvalidCommand
}
f.Header.Add(key, value)
return nil
}
}
}

View File

@ -0,0 +1,183 @@
package stomp
import (
"fmt"
"sync"
"sync/atomic"
"github.com/go-stomp/stomp/v3/frame"
)
const (
subStateActive = 0
subStateClosing = 1
subStateClosed = 2
)
// The Subscription type represents a client subscription to
// a destination. The subscription is created by calling Conn.Subscribe.
//
// Once a client has subscribed, it can receive messages from the C channel.
type Subscription struct {
C chan *Message
id string
destination string
conn *Conn
ackMode AckMode
state int32
closeMutex *sync.Mutex
closeCond *sync.Cond
}
// BUG(jpj): If the client does not read messages from the Subscription.C
// channel quickly enough, the client will stop reading messages from the
// server.
// Identification for this subscription. Unique among
// all subscriptions for the same Client.
func (s *Subscription) Id() string {
return s.id
}
// Destination for which the subscription applies.
func (s *Subscription) Destination() string {
return s.destination
}
// AckMode returns the Acknowledgement mode specified when the
// subscription was created.
func (s *Subscription) AckMode() AckMode {
return s.ackMode
}
// Active returns whether the subscription is still active.
// Returns false if the subscription has been unsubscribed.
func (s *Subscription) Active() bool {
return atomic.LoadInt32(&s.state) == subStateActive
}
// Unsubscribes and closes the channel C.
func (s *Subscription) Unsubscribe(opts ...func(*frame.Frame) error) error {
// transition to the "closing" state
if !atomic.CompareAndSwapInt32(&s.state, subStateActive, subStateClosing) {
return ErrCompletedSubscription
}
f := frame.New(frame.UNSUBSCRIBE, frame.Id, s.id)
for _, opt := range opts {
if opt == nil {
return ErrNilOption
}
err := opt(f)
if err != nil {
return err
}
}
s.conn.sendFrame(f)
// UNSUBSCRIBE is a bit weird in that it is tagged with a "receipt" header
// on the I/O goroutine, so the above call to sendFrame() will not wait
// for the resulting RECEIPT.
//
// We don't want to interfere with `s.C` since we might be "stealing"
// MESSAGEs or ERRORs from another goroutine, so use a sync.Cond to
// wait for the terminal state transition instead.
s.closeMutex.Lock()
for atomic.LoadInt32(&s.state) != subStateClosed {
s.closeCond.Wait()
}
s.closeMutex.Unlock()
return nil
}
// Read a message from the subscription. This is a convenience
// method: many callers will prefer to read from the channel C
// directly.
func (s *Subscription) Read() (*Message, error) {
if !s.Active() {
return nil, ErrCompletedSubscription
}
msg, ok := <-s.C
if !ok {
return nil, ErrCompletedSubscription
}
if msg.Err != nil {
return nil, msg.Err
}
return msg, nil
}
func (s *Subscription) closeChannel(msg *Message) {
if msg != nil {
s.C <- msg
}
atomic.StoreInt32(&s.state, subStateClosed)
close(s.C)
s.closeCond.Broadcast()
}
func (s *Subscription) readLoop(ch chan *frame.Frame) {
for {
f, ok := <-ch
if !ok {
state := atomic.LoadInt32(&s.state)
if state == subStateActive || state == subStateClosing {
msg := &Message{
Err: &Error{
Message: fmt.Sprintf("Subscription %s: %s: channel read failed", s.id, s.destination),
},
}
s.closeChannel(msg)
}
return
}
if f.Command == frame.MESSAGE {
destination := f.Header.Get(frame.Destination)
contentType := f.Header.Get(frame.ContentType)
msg := &Message{
Destination: destination,
ContentType: contentType,
Conn: s.conn,
Subscription: s,
Header: f.Header,
Body: f.Body,
}
s.C <- msg
} else if f.Command == frame.ERROR {
state := atomic.LoadInt32(&s.state)
if state == subStateActive || state == subStateClosing {
message, _ := f.Header.Contains(frame.Message)
text := fmt.Sprintf("Subscription %s: %s: ERROR message:%s",
s.id,
s.destination,
message)
s.conn.log.Info(text)
contentType := f.Header.Get(frame.ContentType)
msg := &Message{
Err: &Error{
Message: f.Header.Get(frame.Message),
Frame: f,
},
ContentType: contentType,
Conn: s.conn,
Subscription: s,
Header: f.Header,
Body: f.Body,
}
s.closeChannel(msg)
}
return
} else if f.Command == frame.RECEIPT {
state := atomic.LoadInt32(&s.state)
if state == subStateActive || state == subStateClosing {
s.closeChannel(nil)
}
return
} else {
s.conn.log.Infof("Subscription %s: %s: unsupported frame type: %+v", s.id, s.destination, f)
}
}
}

View File

@ -0,0 +1,113 @@
package testutil
import (
"errors"
. "gopkg.in/check.v1"
"io"
"net"
"time"
)
type FakeAddr struct {
Value string
}
func (addr *FakeAddr) Network() string {
return "fake"
}
func (addr *FakeAddr) String() string {
return addr.Value
}
// FakeConn is a fake connection used for testing. It implements
// the net.Conn interface and is useful for simulating I/O between
// STOMP clients and a STOMP server.
type FakeConn struct {
C *C
writer io.WriteCloser
reader io.ReadCloser
localAddr net.Addr
remoteAddr net.Addr
}
var (
ErrClosing = errors.New("use of closed network connection")
)
// NewFakeConn returns a pair of fake connections suitable for
// testing.
func NewFakeConn(c *C) (client *FakeConn, server *FakeConn) {
clientReader, serverWriter := io.Pipe()
serverReader, clientWriter := io.Pipe()
clientAddr := &FakeAddr{Value: "the-client:123"}
serverAddr := &FakeAddr{Value: "the-server:456"}
clientConn := &FakeConn{
C: c,
reader: clientReader,
writer: clientWriter,
localAddr: clientAddr,
remoteAddr: serverAddr,
}
serverConn := &FakeConn{
C: c,
reader: serverReader,
writer: serverWriter,
localAddr: serverAddr,
remoteAddr: clientAddr,
}
return clientConn, serverConn
}
func (fc *FakeConn) Read(p []byte) (n int, err error) {
n, err = fc.reader.Read(p)
return
}
func (fc *FakeConn) Write(p []byte) (n int, err error) {
return fc.writer.Write(p)
}
func (fc *FakeConn) Close() error {
err1 := fc.reader.Close()
err2 := fc.writer.Close()
if err1 != nil {
return err1
}
if err2 != nil {
return err2
}
return nil
}
func (fc *FakeConn) LocalAddr() net.Addr {
return fc.localAddr
}
func (fc *FakeConn) RemoteAddr() net.Addr {
return fc.remoteAddr
}
func (fc *FakeConn) SetLocalAddr(addr net.Addr) {
fc.localAddr = addr
}
func (fc *FakeConn) SetRemoteAddr(addr net.Addr) {
fc.remoteAddr = addr
}
func (fc *FakeConn) SetDeadline(t time.Time) error {
panic("not implemented")
}
func (fc *FakeConn) SetReadDeadline(t time.Time) error {
panic("not implemented")
}
func (fc *FakeConn) SetWriteDeadline(t time.Time) error {
panic("not implemented")
}

View File

@ -0,0 +1,61 @@
package testutil
import (
. "gopkg.in/check.v1"
"testing"
)
func TestTestUtil(t *testing.T) {
TestingT(t)
}
type FakeConnSuite struct{}
var _ = Suite(&FakeConnSuite{})
func (s *FakeConnSuite) TestFakeConn(c *C) {
//c.Skip("temporary")
fc1, fc2 := NewFakeConn(c)
one := []byte{1, 2, 3, 4}
two := []byte{5, 6, 7, 8, 9, 10, 11, 12, 13}
stop := make(chan struct{})
go func() {
defer func() {
fc2.Close()
close(stop)
}()
rx1 := make([]byte, 6)
n, err := fc2.Read(rx1)
c.Assert(n, Equals, 4)
c.Assert(err, IsNil)
c.Assert(rx1[0:n], DeepEquals, one)
rx2 := make([]byte, 5)
n, err = fc2.Read(rx2)
c.Assert(n, Equals, 5)
c.Assert(err, IsNil)
c.Assert(rx2, DeepEquals, []byte{5, 6, 7, 8, 9})
rx3 := make([]byte, 10)
n, err = fc2.Read(rx3)
c.Assert(n, Equals, 4)
c.Assert(err, IsNil)
c.Assert(rx3[0:n], DeepEquals, []byte{10, 11, 12, 13})
}()
c.Assert(fc1.C, Equals, c)
c.Assert(fc2.C, Equals, c)
n, err := fc1.Write(one)
c.Assert(n, Equals, 4)
c.Assert(err, IsNil)
n, err = fc1.Write(two)
c.Assert(n, Equals, len(two))
c.Assert(err, IsNil)
<-stop
}

View File

@ -0,0 +1,150 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./logger.go
// Package testutil is a generated GoMock package.
package testutil
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(message string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Debug", message)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(message interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), message)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(format string, value ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{format}
for _, a := range value {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(format interface{}, value ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{format}, value...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(message string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Error", message)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(message interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), message)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(format string, value ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{format}
for _, a := range value {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(format interface{}, value ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{format}, value...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Info mocks base method.
func (m *MockLogger) Info(message string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Info", message)
}
// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(message interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), message)
}
// Infof mocks base method.
func (m *MockLogger) Infof(format string, value ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{format}
for _, a := range value {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
// Infof indicates an expected call of Infof.
func (mr *MockLoggerMockRecorder) Infof(format interface{}, value ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{format}, value...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
}
// Warning mocks base method.
func (m *MockLogger) Warning(message string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Warning", message)
}
// Warning indicates an expected call of Warning.
func (mr *MockLoggerMockRecorder) Warning(message interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warning", reflect.TypeOf((*MockLogger)(nil).Warning), message)
}
// Warningf mocks base method.
func (m *MockLogger) Warningf(format string, value ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{format}
for _, a := range value {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Warningf", varargs...)
}
// Warningf indicates an expected call of Warningf.
func (mr *MockLoggerMockRecorder) Warningf(format interface{}, value ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{format}, value...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warningf", reflect.TypeOf((*MockLogger)(nil).Warningf), varargs...)
}

View File

@ -0,0 +1,5 @@
/*
Package testutil contains operations useful for testing. In particular,
it provides fake connections useful for testing client/server interactions.
*/
package testutil

View File

@ -0,0 +1,178 @@
package stomp
import (
"github.com/go-stomp/stomp/v3/frame"
)
// A Transaction applies to the sending of messages to the STOMP server,
// and the acknowledgement of messages received from the STOMP server.
// All messages sent and and acknowledged in the context of a transaction
// are processed atomically by the STOMP server.
//
// Transactions are committed with the Commit method. When a transaction is
// committed, all sent messages, acknowledgements and negative acknowledgements,
// are processed by the STOMP server. Alternatively transactions can be aborted,
// in which case all sent messages, acknowledgements and negative
// acknowledgements are discarded by the STOMP server.
type Transaction struct {
id string
conn *Conn
completed bool
}
// Id returns the unique identifier for the transaction.
func (tx *Transaction) Id() string {
return tx.id
}
// Conn returns the connection associated with this transaction.
func (tx *Transaction) Conn() *Conn {
return tx.conn
}
// Abort will abort the transaction. Any calls to Send, SendWithReceipt,
// Ack and Nack on this transaction will be discarded.
// This function does not wait for the server to process the ABORT frame.
// See AbortWithReceipt if you want to ensure the ABORT is processed.
func (tx *Transaction) Abort() error {
return tx.abort(false)
}
// Abort will abort the transaction. Any calls to Send, SendWithReceipt,
// Ack and Nack on this transaction will be discarded.
func (tx *Transaction) AbortWithReceipt() error {
return tx.abort(true)
}
func (tx *Transaction) abort(receipt bool) error {
if tx.completed {
return ErrCompletedTransaction
}
f := frame.New(frame.ABORT, frame.Transaction, tx.id)
if receipt {
id := allocateId()
f.Header.Set(frame.Receipt, id)
}
err := tx.conn.sendFrame(f)
if err != nil {
return err
}
tx.completed = true
return nil
}
// Commit will commit the transaction. All messages and acknowledgements
// sent to the STOMP server on this transaction will be processed atomically.
// This function does not wait for the server to process the COMMIT frame.
// See CommitWithReceipt if you want to ensure the COMMIT is processed.
func (tx *Transaction) Commit() error {
return tx.commit(false)
}
// Commit will commit the transaction. All messages and acknowledgements
// sent to the STOMP server on this transaction will be processed atomically.
func (tx *Transaction) CommitWithReceipt() error {
return tx.commit(true)
}
func (tx *Transaction) commit(receipt bool) error {
if tx.completed {
return ErrCompletedTransaction
}
f := frame.New(frame.COMMIT, frame.Transaction, tx.id)
if receipt {
id := allocateId()
f.Header.Set(frame.Receipt, id)
}
err := tx.conn.sendFrame(f)
if err != nil {
return err
}
tx.completed = true
return nil
}
// Send sends a message to the STOMP server as part of a transaction. The server will not process the
// message until the transaction is committed.
// This method returns without confirming that the STOMP server has received the message. If the STOMP server
// does fail to receive the message for any reason, the connection will close.
//
// The content type should be specified, according to the STOMP specification, but if contentType is an empty
// string, the message will be delivered without a content type header entry. The body array contains the
// message body, and its content should be consistent with the specified content type.
//
// TODO: document opts
func (tx *Transaction) Send(destination, contentType string, body []byte, opts ...func(*frame.Frame) error) error {
if tx.completed {
return ErrCompletedTransaction
}
f, err := createSendFrame(destination, contentType, body, opts)
if err != nil {
return err
}
f.Header.Set(frame.Transaction, tx.id)
return tx.conn.sendFrame(f)
}
// Ack sends an acknowledgement for the message to the server. The STOMP
// server will not process the acknowledgement until the transaction
// has been committed. If the subscription has an AckMode of AckAuto, calling
// this function has no effect.
func (tx *Transaction) Ack(msg *Message) error {
if tx.completed {
return ErrCompletedTransaction
}
f, err := tx.conn.createAckNackFrame(msg, true)
if err != nil {
return err
}
if f != nil {
f.Header.Set(frame.Transaction, tx.id)
err := tx.conn.sendFrame(f)
if err != nil {
return err
}
}
return nil
}
// Nack sends a negative acknowledgement for the message to the server,
// indicating that this client cannot or will not process the message and
// that it should be processed elsewhere. The STOMP server will not process
// the negative acknowledgement until the transaction has been committed.
// It is an error to call this method if the subscription has an AckMode
// of AckAuto, because the STOMP server will not be expecting any kind
// of acknowledgement (positive or negative) for this message.
func (tx *Transaction) Nack(msg *Message) error {
if tx.completed {
return ErrCompletedTransaction
}
f, err := tx.conn.createAckNackFrame(msg, false)
if err != nil {
return err
}
if f != nil {
f.Header.Set(frame.Transaction, tx.id)
err := tx.conn.sendFrame(f)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,21 @@
package stomp
import (
"github.com/go-stomp/stomp/v3/frame"
)
// Validator is an interface for validating STOMP frames.
type Validator interface {
// Validate returns nil if the frame is valid, or an error if not valid.
Validate(f *frame.Frame) error
}
func NewValidator(version Version) Validator {
return validatorNull{}
}
type validatorNull struct{}
func (v validatorNull) Validate(f *frame.Frame) error {
return nil
}

View File

@ -0,0 +1,40 @@
package stomp
// Version is the STOMP protocol version.
type Version string
const (
V10 Version = "1.0"
V11 Version = "1.1"
V12 Version = "1.2"
)
// String returns a string representation of the STOMP version.
func (v Version) String() string {
return string(v)
}
// CheckSupported is used to determine whether a particular STOMP
// version is supported by this library. Returns nil if the version is
// supported, or ErrUnsupportedVersion if not supported.
func (v Version) CheckSupported() error {
switch v {
case V10, V11, V12:
return nil
}
return ErrUnsupportedVersion
}
// SupportsNack indicates whether this version of the STOMP protocol
// supports use of the NACK command.
func (v Version) SupportsNack() bool {
switch v {
case V10:
return false
case V11, V12:
return true
}
// unknown version
return false
}

View File

@ -0,0 +1,79 @@
package stomp_test
import (
"testing"
"github.com/go-stomp/stomp/v3"
)
func TestSupportsNack(t *testing.T) {
testCases := []struct {
Version stomp.Version
SupportsNack bool
}{
{
Version: stomp.Version("1.0"),
SupportsNack: false,
},
{
Version: stomp.Version("1.1"),
SupportsNack: true,
},
{
Version: stomp.Version("1.2"),
SupportsNack: true,
},
{
Version: stomp.Version("xxx"),
SupportsNack: false,
},
}
for _, testCase := range testCases {
version := testCase.Version
expected := testCase.SupportsNack
actual := version.SupportsNack()
if expected != actual {
t.Errorf("Version %v: SupportsNack: expected %v, actual %v",
version, expected, actual)
}
}
}
func TestCheckSupported(t *testing.T) {
testCases := []struct {
Version stomp.Version
Err error
}{
{
Version: stomp.Version("1.0"),
Err: nil,
},
{
Version: stomp.Version("1.1"),
Err: nil,
},
{
Version: stomp.Version("1.2"),
Err: nil,
},
{
Version: stomp.Version("2.2"),
Err: stomp.ErrUnsupportedVersion,
},
}
for _, testCase := range testCases {
version := testCase.Version
expected := testCase.Err
actual := version.CheckSupported()
if expected != actual {
t.Errorf("Version %v: CheckSupported: expected %v, actual %v",
version, expected, actual)
}
}
}