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 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)) } f, err := reader.Read() if err != nil { if err == io.EOF { log.Println("connection closed:", c.rw.RemoteAddr()) } else { log.Println("read failed:", 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 } }
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 }