conn.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. package chromedp
  2. import (
  3. "bytes"
  4. "context"
  5. "io"
  6. "net"
  7. "github.com/gobwas/ws"
  8. "github.com/gobwas/ws/wsutil"
  9. "github.com/mailru/easyjson/jlexer"
  10. "github.com/mailru/easyjson/jwriter"
  11. "github.com/chromedp/cdproto"
  12. )
  13. // Transport is the common interface to send/receive messages to a target.
  14. //
  15. // This interface is currently used internally by Browser, but it is exposed as
  16. // it will be useful as part of the public API in the future.
  17. type Transport interface {
  18. Read(context.Context, *cdproto.Message) error
  19. Write(context.Context, *cdproto.Message) error
  20. io.Closer
  21. }
  22. // Conn implements Transport with a gobwas/ws websocket connection.
  23. type Conn struct {
  24. conn net.Conn
  25. // reuse the websocket reader and writer to avoid an alloc per
  26. // Read/Write.
  27. reader wsutil.Reader
  28. writer wsutil.Writer
  29. // reuse the easyjson structs to avoid allocs per Read/Write.
  30. decoder jlexer.Lexer
  31. encoder jwriter.Writer
  32. dbgf func(string, ...interface{})
  33. }
  34. // DialContext dials the specified websocket URL using gobwas/ws.
  35. func DialContext(ctx context.Context, urlstr string, opts ...DialOption) (*Conn, error) {
  36. // connect
  37. conn, br, _, err := ws.Dial(ctx, urlstr)
  38. if err != nil {
  39. return nil, err
  40. }
  41. if br != nil {
  42. panic("br should be nil")
  43. }
  44. // apply opts
  45. c := &Conn{
  46. conn: conn,
  47. // pass 0 to use the default initial buffer size (4KiB).
  48. // github.com/gobwas/ws will grow the buffer size if needed.
  49. writer: *wsutil.NewWriterBufferSize(conn, ws.StateClientSide, ws.OpText, 0),
  50. }
  51. for _, o := range opts {
  52. o(c)
  53. }
  54. return c, nil
  55. }
  56. // Close satisfies the io.Closer interface.
  57. func (c *Conn) Close() error {
  58. return c.conn.Close()
  59. }
  60. // Read reads the next message.
  61. func (c *Conn) Read(_ context.Context, msg *cdproto.Message) error {
  62. // get websocket reader
  63. c.reader = wsutil.Reader{Source: c.conn, State: ws.StateClientSide}
  64. h, err := c.reader.NextFrame()
  65. if err != nil {
  66. return err
  67. }
  68. if h.OpCode != ws.OpText {
  69. return ErrInvalidWebsocketMessage
  70. }
  71. var b bytes.Buffer
  72. if _, err := b.ReadFrom(&c.reader); err != nil {
  73. return err
  74. }
  75. buf := b.Bytes()
  76. if c.dbgf != nil {
  77. c.dbgf("<- %s", buf)
  78. }
  79. // unmarshal, reusing lexer
  80. c.decoder = jlexer.Lexer{Data: buf}
  81. msg.UnmarshalEasyJSON(&c.decoder)
  82. return c.decoder.Error()
  83. }
  84. // Write writes a message.
  85. func (c *Conn) Write(_ context.Context, msg *cdproto.Message) error {
  86. c.writer.Reset(c.conn, ws.StateClientSide, ws.OpText)
  87. // Chrome doesn't support fragmentation of incoming websocket messages. To
  88. // compensate this, they support single-fragment messages of up to 100MiB.
  89. //
  90. // See https://github.com/ChromeDevTools/devtools-protocol/issues/175.
  91. //
  92. // And according to https://bugs.chromium.org/p/chromium/issues/detail?id=1069431,
  93. // it seems like that fragmentation won't be supported very soon.
  94. // Luckily, now github.com/gobwas/ws will grow the buffer if needed.
  95. // The func name DisableFlush is a little misleading,
  96. // but it do make it grow the buffer if needed.
  97. c.writer.DisableFlush()
  98. // Reuse the easyjson writer.
  99. c.encoder = jwriter.Writer{}
  100. // Perform the marshal.
  101. msg.MarshalEasyJSON(&c.encoder)
  102. if err := c.encoder.Error; err != nil {
  103. return err
  104. }
  105. // Write the bytes to the websocket.
  106. // BuildBytes consumes the buffer, so we can't use it as well as DumpTo.
  107. if c.dbgf != nil {
  108. buf, _ := c.encoder.BuildBytes()
  109. c.dbgf("-> %s", buf)
  110. if _, err := c.writer.Write(buf); err != nil {
  111. return err
  112. }
  113. } else {
  114. if _, err := c.encoder.DumpTo(&c.writer); err != nil {
  115. return err
  116. }
  117. }
  118. return c.writer.Flush()
  119. }
  120. // DialOption is a dial option.
  121. type DialOption = func(*Conn)
  122. // WithConnDebugf is a dial option to set a protocol logger.
  123. func WithConnDebugf(f func(string, ...interface{})) DialOption {
  124. return func(c *Conn) {
  125. c.dbgf = f
  126. }
  127. }