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)) writer.Write(f2) 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, } }
// 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()) writer.Write(f2) 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_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") writer.Write(connectedFrame) f2, err := reader.Read() c.Assert(err, IsNil) c.Assert(f2.Command, Equals, "DISCONNECT") receipt, _ := f2.Header.Contains("receipt") c.Check(receipt, Equals, "1") writer.Write(frame.New("RECEIPT", frame.ReceiptId, "1")) }() 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_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") writer.Write(f2) }() conn, err := Connect(fc1) c.Assert(conn, IsNil) c.Assert(err, ErrorMatches, "auth-failed") }
// 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 for { var timerChannel <-chan time.Time var timer *time.Timer if c.writeTimeout > 0 { 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 { log.Println("validation failed for", f.Command, "frame", 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: // write a heart-beat err := c.writer.Write(nil) if err != nil { return } } } }
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) } writer.Write(connectedFrame) f2, err := reader.Read() c.Assert(err, IsNil) c.Assert(f2.Command, Equals, "DISCONNECT") receipt, _ := f2.Header.Contains("receipt") c.Check(receipt, Equals, "1") writer.Write(frame.New("RECEIPT", frame.ReceiptId, "1")) }() 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 } }
// 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, readCh: make(chan *frame.Frame, 8), writeCh: make(chan writeRequest, 8), } options, err := newConnOptions(c, opts) if err != nil { return nil, err } 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.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 } } // TODO(jpj): make any non-standard headers in the CONNECTED // frame available. This could be implemented as: // (a) a callback function supplied as an option; or // (b) a property of the Conn structure (eg CustomHeaders) // Neither options are particularly elegant, so wait until // there is a real need for this. go readLoop(c, reader) go processLoop(c, writer) return c, nil }