gnet

package module
v0.0.0-...-16d1669 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 19, 2022 License: Apache-2.0 Imports: 26 Imported by: 0

README

gnet

English | 中文

📖 Introduction

gnet is an event-driven networking framework that is fast and lightweight. It makes direct epoll and kqueue syscalls rather than using the standard Go net package and works in a similar manner as netty and libuv, which makes gnet achieve a much higher performance than Go net.

gnet is not designed to displace the standard Go net package, but to create a networking client/server framework for Go that performs on par with Redis and Haproxy for networking packets handling (although it does not limit itself to these areas), therefore, gnet is not as comprehensive as Go net, it only provides the core functionalities (by a concise API set) of a networking application and it is not planned on being a full-featured networking framework, as I think net has done a good enough job in this area.

gnet sells itself as a high-performance, lightweight, non-blocking, event-driven networking framework written in pure Go which works on transport layer with TCP/UDP protocols and Unix Domain Socket , so it allows developers to implement their own protocols(HTTP, RPC, WebSocket, Redis, etc.) of application layer upon gnet for building diversified network applications, for instance, you get an HTTP Server or Web Framework if you implement HTTP protocol upon gnet while you have a Redis Server done with the implementation of Redis protocol upon gnet and so on.

gnet derives from the project: evio while having a much higher performance and more features.

🚀 Features

  • High-performance event-loop under networking model of multiple threads/goroutines
  • Built-in goroutine pool powered by the library ants
  • Lock-free during the entire runtime
  • Concise and easy-to-use APIs
  • Efficient, reusable and elastic memory buffer: (Elastic-)Ring-Buffer, Linked-List-Buffer and Elastic-Mixed-Buffer
  • Supporting multiple protocols/IPC mechanism: TCP, UDP and Unix Domain Socket
  • Supporting multiple load-balancing algorithms: Round-Robin, Source-Addr-Hash and Least-Connections
  • Supporting two event-driven mechanisms: epoll on Linux and kqueue on FreeBSD/DragonFly/Darwin
  • Flexible ticker event
  • Implementation of gnet Client
  • Windows platform support (gnet v1 is available on Windows, v2 not yet)
  • TLS support
  • io_uring support

🎬 Getting started

gnet is available as a Go module and we highly recommend that you use gnet via Go Modules, with Go 1.11 Modules enabled (Go 1.11+), you can just simply add import "github.com/panjf2000/gnet" to the codebase and run go mod download/go mod tidy or go [build|run|test] to download the necessary dependencies automatically.

With v2

go get -u github.com/Yajun312890225/gnet

With v1

go get -ugithub.com/panjf2000/gnet

🎡 Use cases

The following companies/organizations use gnet as the underlying network service in production.

          

If you have gnet integrated into projects, feel free to open a pull request refreshing this list.

📊 Performance

Benchmarks on TechEmpower

# Hardware Environment
CPU: 28 HT Cores Intel(R) Xeon(R) Gold 5120 CPU @ 2.20GHz
Mem: 32GB RAM
OS : Ubuntu 18.04.3 4.15.0-88-generic #88-Ubuntu
Net: Switched 10-gigabit ethernet
Go : go1.14.x linux/amd64

All language

This is the top 50 on the framework ranking of all programming languages consists of a total of 422 frameworks from all over the world where gnet is the runner-up.

Golang

This is the full framework ranking of Go and gnet tops all the other frameworks, which makes gnet the fastest networking framework in Go.

To see the full ranking list, visit TechEmpower Plaintext Benchmark.

Contrasts to the similar networking libraries

On Linux (epoll)

Test Environment
# Machine information
        OS : Ubuntu 20.04/x86_64
       CPU : 8 CPU cores, AMD EPYC 7K62 48-Core Processor
    Memory : 16.0 GiB

# Go version and settings
Go Version : go1.17.2 linux/amd64
GOMAXPROCS : 8

# Benchmark parameters
TCP connections : 1000/2000/5000/10000
Packet size     : 512/1024/2048/4096/8192/16384/32768/65536 bytes
Test duration   : 15s
Echo benchmark

On MacOS (kqueue)

Test Environment
# Machine information
        OS : MacOS Big Sur/x86_64
       CPU : 6 CPU cores, Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory : 16.0 GiB

# Go version and settings
Go Version : go1.16.5 darwin/amd64
GOMAXPROCS : 12

# Benchmark parameters
TCP connections : 300/400/500/600/700
Packet size     : 512/1024/2048/4096/8192 bytes
Test duration   : 15s
Echo benchmark

⚠️ License

Source code of gnet should be distributed under the Apache-2.0 license.

👏 Contributors

Please read the Contributing Guidelines before opening a PR and thank you to all the developers who already made contributions to gnet!

⚓ Relevant Articles

💰 Backers

Support us with a monthly donation and help us continue our activities.

💎 Sponsors

Become a bronze sponsor with a monthly donation of $10 and get your logo on our README ongithub.

☕️ Buy me a coffee

Please be sure to leave your name,github account or other social media accounts when you donate by the following means so that I can add it to the list of donors as a token of my appreciation.

        

💴 Patrons

Patrick Othmer Jimmy ChenZhen Mai Yang 王开帅 Unger Alejandro Swaggadan Weng Wei

🔑 JetBrains OS licenses

gnet had been being developed with GoLand IDE under the free JetBrains Open Source license(s) granted by JetBrains s.r.o., hence I would like to express my thanks here.

🔋 Sponsorship

This project is supported by:

Documentation

Index

Constants

This section is empty.

Variables

View Source
var MaxStreamBufferCap = 64 * 1024 // 64KB

MaxStreamBufferCap is the default buffer size for each stream-oriented connection(TCP/Unix).

Functions

func Run

func Run(eventHandler EventHandler, protoAddr string, opts ...Option) (err error)

Run starts handling events on the specified address.

Address should use a scheme prefix and be formatted like `tcp://192.168.0.10:9851` or `unix://socket`. Valid network schemes:

tcp   - bind to both IPv4 and IPv6
tcp4  - IPv4
tcp6  - IPv6
udp   - bind to both IPv4 and IPv6
udp4  - IPv4
udp6  - IPv6
unix  - Unix Domain Socket

The "tcp" network scheme is assumed when one is not specified.

func Stop

func Stop(ctx context.Context, protoAddr string) error

Stop gracefully shuts down the engine without interrupting any active event-loops, it waits indefinitely for connections and event-loops to be closed and then shuts down. Deprecated: The global Stop only shuts down the last registered Engine with the same protocol and IP:Port as the previous Engine's, which can lead to leaks of Engine if you invoke gnet.Run multiple times using the same protocol and IP:Port under the condition that WithReuseAddr(true) and WithReusePort(true) are enabled. Use Engine.Stop instead.

Types

type Action

type Action int

Action is an action that occurs after the completion of an event.

const (
	// None indicates that no action should occur following an event.
	None Action = iota

	// Close closes the connection.
	Close

	// Shutdown shutdowns the engine.
	Shutdown
)

type AsyncCallback

type AsyncCallback func(c Conn, err error) error

AsyncCallback is a callback which will be invoked after the asynchronous functions has finished executing.

Note that the parameter gnet.Conn is already released under UDP protocol, thus it's not allowed to be accessed.

type BuiltinEventEngine

type BuiltinEventEngine struct{}

BuiltinEventEngine is a built-in implementation of EventHandler which sets up each method with a default implementation, you can compose it with your own implementation of EventHandler when you don't want to implement all methods in EventHandler.

func (*BuiltinEventEngine) OnBoot

func (*BuiltinEventEngine) OnBoot(_ Engine) (action Action)

OnBoot fires when the engine is ready for accepting connections. The parameter engine has information and various utilities.

func (*BuiltinEventEngine) OnClose

func (*BuiltinEventEngine) OnClose(_ Conn, _ error) (action Action)

OnClose fires when a connection has been closed. The parameter err is the last known connection error.

func (*BuiltinEventEngine) OnOpen

func (*BuiltinEventEngine) OnOpen(_ Conn) (out []byte, action Action)

OnOpen fires when a new connection has been opened. The parameter out is the return value which is going to be sent back to the peer.

func (*BuiltinEventEngine) OnShutdown

func (*BuiltinEventEngine) OnShutdown(_ Engine)

OnShutdown fires when the engine is being shut down, it is called right after all event-loops and connections are closed.

func (*BuiltinEventEngine) OnTick

func (*BuiltinEventEngine) OnTick() (delay time.Duration, action Action)

OnTick fires immediately after the engine starts and will fire again following the duration specified by the delay return value.

func (*BuiltinEventEngine) OnTraffic

func (*BuiltinEventEngine) OnTraffic(_ Conn) (action Action)

OnTraffic fires when a local socket receives data from the peer.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client of gnet.

func NewClient

func NewClient(eventHandler EventHandler, opts ...Option) (cli *Client, err error)

NewClient creates an instance of Client.

func (*Client) Dial

func (cli *Client) Dial(network, address string) (Conn, error)

Dial is like net.Dial().

func (*Client) Enroll

func (cli *Client) Enroll(c net.Conn) (Conn, error)

Enroll converts a net.Conn to gnet.Conn and then adds it into Client.

func (*Client) Start

func (cli *Client) Start() error

Start starts the client event-loop, handing IO events.

func (*Client) Stop

func (cli *Client) Stop() (err error)

Stop stops the client event-loop.

type Conn

type Conn interface {
	Reader
	Writer
	Socket

	// Context returns a user-defined context.
	Context() (ctx interface{})

	// SetContext sets a user-defined context.
	SetContext(ctx interface{})

	// LocalAddr is the connection's local socket address.
	LocalAddr() (addr net.Addr)

	// RemoteAddr is the connection's remote peer address.
	RemoteAddr() (addr net.Addr)

	// SetDeadline implements net.Conn.
	SetDeadline(t time.Time) (err error)

	// SetReadDeadline implements net.Conn.
	SetReadDeadline(t time.Time) (err error)

	// SetWriteDeadline implements net.Conn.
	SetWriteDeadline(t time.Time) (err error)

	// Wake triggers a OnTraffic event for the connection.
	Wake(callback AsyncCallback) (err error)

	// CloseWithCallback closes the current connection, usually you don't need to pass a non-nil callback
	// because you should use OnClose() instead, the callback here is only for compatibility.
	CloseWithCallback(callback AsyncCallback) (err error)

	// Close closes the current connection, implements net.Conn.
	Close() (err error)
}

Conn is an interface of underlying connection.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine represents an engine context which provides some functions.

func (Engine) CountConnections

func (s Engine) CountConnections() (count int)

CountConnections counts the number of currently active connections and returns it.

func (Engine) Dup

func (s Engine) Dup() (dupFD int, err error)

Dup returns a copy of the underlying file descriptor of listener. It is the caller's responsibility to close dupFD when finished. Closing listener does not affect dupFD, and closing dupFD does not affect listener.

func (Engine) Stop

func (s Engine) Stop(ctx context.Context) error

Stop gracefully shuts down this Engine without interrupting any active event-loops, it waits indefinitely for connections and event-loops to be closed and then shuts down.

type EventHandler

type EventHandler interface {
	// OnBoot fires when the engine is ready for accepting connections.
	// The parameter engine has information and various utilities.
	OnBoot(eng Engine) (action Action)

	// OnShutdown fires when the engine is being shut down, it is called right after
	// all event-loops and connections are closed.
	OnShutdown(eng Engine)

	// OnOpen fires when a new connection has been opened.
	//
	// The Conn c has information about the connection such as its local and remote addresses.
	// The parameter out is the return value which is going to be sent back to the peer.
	// Sending large amounts of data back to the peer in OnOpen is usually not recommended.
	OnOpen(c Conn) (out []byte, action Action)

	// OnClose fires when a connection has been closed.
	// The parameter err is the last known connection error.
	OnClose(c Conn, err error) (action Action)

	// OnTraffic fires when a socket receives data from the peer.
	//
	// Note that the []byte returned from Conn.Peek(int)/Conn.Next(int) is not allowed to be passed to a new goroutine,
	// as this []byte will be reused within event-loop after OnTraffic() returns.
	// If you have to use this []byte in a new goroutine, you should either make a copy of it or call Conn.Read([]byte)
	// to read data into your own []byte, then pass the new []byte to the new goroutine.
	OnTraffic(c Conn) (action Action)

	// OnTick fires immediately after the engine starts and will fire again
	// following the duration specified by the delay return value.
	OnTick() (delay time.Duration, action Action)
}

EventHandler represents the engine events' callbacks for the Run call. Each event has an Action return value that is used manage the state of the connection and engine.

type LoadBalancing

type LoadBalancing int

LoadBalancing represents the type of load-balancing algorithm.

const (
	// RoundRobin assigns the next accepted connection to the event-loop by polling event-loop list.
	RoundRobin LoadBalancing = iota

	// LeastConnections assigns the next accepted connection to the event-loop that is
	// serving the least number of active connections at the current time.
	LeastConnections

	// SourceAddrHash assigns the next accepted connection to the event-loop by hashing the remote address.
	SourceAddrHash
)

type Option

type Option func(opts *Options)

Option is a function that will set up option.

func WithLoadBalancing

func WithLoadBalancing(lb LoadBalancing) Option

WithLoadBalancing sets up the load-balancing algorithm in gnet engine.

func WithLockOSThread

func WithLockOSThread(lockOSThread bool) Option

WithLockOSThread sets up LockOSThread mode for I/O event-loops.

func WithLogLevel

func WithLogLevel(lvl logging.Level) Option

WithLogLevel is an option to set up the logging level.

func WithLogPath

func WithLogPath(fileName string) Option

WithLogPath is an option to set up the local path of log file.

func WithLogger

func WithLogger(logger logging.Logger) Option

WithLogger sets up a customized logger.

func WithMulticastInterfaceIndex

func WithMulticastInterfaceIndex(idx int) Option

WithMulticastInterfaceIndex sets the interface name where UDP multicast sockets will be bound to.

func WithMulticore

func WithMulticore(multicore bool) Option

WithMulticore sets up multi-cores in gnet engine.

func WithNumEventLoop

func WithNumEventLoop(numEventLoop int) Option

WithNumEventLoop sets up NumEventLoop in gnet engine.

func WithOptions

func WithOptions(options Options) Option

WithOptions sets up all options.

func WithReadBufferCap

func WithReadBufferCap(readBufferCap int) Option

WithReadBufferCap sets up ReadBufferCap for reading bytes.

func WithReuseAddr

func WithReuseAddr(reuseAddr bool) Option

WithReuseAddr sets up SO_REUSEADDR socket option.

func WithReusePort

func WithReusePort(reusePort bool) Option

WithReusePort sets up SO_REUSEPORT socket option.

func WithSocketRecvBuffer

func WithSocketRecvBuffer(recvBuf int) Option

WithSocketRecvBuffer sets the maximum socket receive buffer in bytes.

func WithSocketSendBuffer

func WithSocketSendBuffer(sendBuf int) Option

WithSocketSendBuffer sets the maximum socket send buffer in bytes.

func WithTCPKeepAlive

func WithTCPKeepAlive(tcpKeepAlive time.Duration) Option

WithTCPKeepAlive sets up the SO_KEEPALIVE socket option with duration.

func WithTCPNoDelay

func WithTCPNoDelay(tcpNoDelay TCPSocketOpt) Option

WithTCPNoDelay enable/disable the TCP_NODELAY socket option.

func WithTicker

func WithTicker(ticker bool) Option

WithTicker indicates that a ticker is set.

func WithWriteBufferCap

func WithWriteBufferCap(writeBufferCap int) Option

WithWriteBufferCap sets up WriteBufferCap for pending bytes.

type Options

type Options struct {

	// Multicore indicates whether the engine will be effectively created with multi-cores, if so,
	// then you must take care with synchronizing memory between all event callbacks, otherwise,
	// it will run the engine with single thread. The number of threads in the engine will be automatically
	// assigned to the value of logical CPUs usable by the current process.
	Multicore bool

	// NumEventLoop is set up to start the given number of event-loop goroutine.
	// Note: Setting up NumEventLoop will override Multicore.
	NumEventLoop int

	// LB represents the load-balancing algorithm used when assigning new connections.
	LB LoadBalancing

	// ReuseAddr indicates whether to set up the SO_REUSEADDR socket option.
	ReuseAddr bool

	// ReusePort indicates whether to set up the SO_REUSEPORT socket option.
	ReusePort bool

	// MulticastInterfaceIndex is the index of the interface name where the multicast UDP addresses will be bound to.
	MulticastInterfaceIndex int

	// ReadBufferCap is the maximum number of bytes that can be read from the peer when the readable event comes.
	// The default value is 64KB, it can either be reduced to avoid starving the subsequent connections or increased
	// to read more data from a socket.
	//
	// Note that ReadBufferCap will always be converted to the least power of two integer value greater than
	// or equal to its real amount.
	ReadBufferCap int

	// WriteBufferCap is the maximum number of bytes that a static outbound buffer can hold,
	// if the data exceeds this value, the overflow will be stored in the elastic linked list buffer.
	// The default value is 64KB.
	//
	// Note that WriteBufferCap will always be converted to the least power of two integer value greater than
	// or equal to its real amount.
	WriteBufferCap int

	// LockOSThread is used to determine whether each I/O event-loop is associated to an OS thread, it is useful when you
	// need some kind of mechanisms like thread local storage, or invoke certain C libraries (such as graphics lib: GLib)
	// that require thread-level manipulation via cgo, or want all I/O event-loops to actually run in parallel for a
	// potential higher performance.
	LockOSThread bool

	// Ticker indicates whether the ticker has been set up.
	Ticker bool

	// TCPKeepAlive sets up a duration for (SO_KEEPALIVE) socket option.
	TCPKeepAlive time.Duration

	// TCPNoDelay controls whether the operating system should delay
	// packet transmission in hopes of sending fewer packets (Nagle's algorithm).
	//
	// The default is true (no delay), meaning that data is sent
	// as soon as possible after a write operation.
	TCPNoDelay TCPSocketOpt

	// SocketRecvBuffer sets the maximum socket receive buffer in bytes.
	SocketRecvBuffer int

	// SocketSendBuffer sets the maximum socket send buffer in bytes.
	SocketSendBuffer int

	// LogPath the local path where logs will be written, this is the easiest way to set up logging,
	// gnet instantiates a default uber-go/zap logger with this given log path, you are also allowed to employ
	// you own logger during the lifetime by implementing the following log.Logger interface.
	//
	// Note that this option can be overridden by the option Logger.
	LogPath string

	// LogLevel indicates the logging level, it should be used along with LogPath.
	LogLevel logging.Level

	// Logger is the customized logger for logging info, if it is not set,
	// then gnet will use the default logger powered by go.uber.org/zap.
	Logger logging.Logger
}

Options are configurations for the gnet application.

type Reader

type Reader interface {
	io.Reader
	io.WriterTo // must be non-blocking, otherwise it may block the event-loop.

	// Next returns a slice containing the next n bytes from the buffer,
	// advancing the buffer as if the bytes had been returned by Read.
	// If there are fewer than n bytes in the buffer, Next returns the entire buffer.
	// The error is ErrBufferFull if n is larger than b's buffer size.
	//
	// Note that the []byte buf returned by Next() is not allowed to be passed to a new goroutine,
	// as this []byte will be reused within event-loop.
	// If you have to use buf in a new goroutine, then you need to make a copy of buf and pass this copy
	// to that new goroutine.
	Next(n int) (buf []byte, err error)

	// Peek returns the next n bytes without advancing the reader. The bytes stop
	// being valid at the next read call. If Peek returns fewer than n bytes, it
	// also returns an error explaining why the read is short. The error is
	// ErrBufferFull if n is larger than b's buffer size.
	//
	// Note that the []byte buf returned by Peek() is not allowed to be passed to a new goroutine,
	// as this []byte will be reused within event-loop.
	// If you have to use buf in a new goroutine, then you need to make a copy of buf and pass this copy
	// to that new goroutine.
	Peek(n int) (buf []byte, err error)

	// Discard skips the next n bytes, returning the number of bytes discarded.
	//
	// If Discard skips fewer than n bytes, it also returns an error.
	// If 0 <= n <= b.Buffered(), Discard is guaranteed to succeed without
	// reading from the underlying io.Reader.
	Discard(n int) (discarded int, err error)

	// InboundBuffered returns the number of bytes that can be read from the current buffer.
	InboundBuffered() (n int)
}

Reader is an interface that consists of a number of methods for reading that Conn must implement.

type Socket

type Socket interface {
	// Fd returns the underlying file descriptor.
	Fd() int

	// Dup returns a copy of the underlying file descriptor.
	// It is the caller's responsibility to close fd when finished.
	// Closing c does not affect fd, and closing fd does not affect c.
	//
	// The returned file descriptor is different from the
	// connection's. Attempting to change properties of the original
	// using this duplicate may or may not have the desired effect.
	Dup() (int, error)

	// SetReadBuffer sets the size of the operating system's
	// receive buffer associated with the connection.
	SetReadBuffer(bytes int) error

	// SetWriteBuffer sets the size of the operating system's
	// transmit buffer associated with the connection.
	SetWriteBuffer(bytes int) error

	// SetLinger sets the behavior of Close on a connection which still
	// has data waiting to be sent or to be acknowledged.
	//
	// If sec < 0 (the default), the operating system finishes sending the
	// data in the background.
	//
	// If sec == 0, the operating system discards any unsent or
	// unacknowledged data.
	//
	// If sec > 0, the data is sent in the background as with sec < 0. On
	// some operating systems after sec seconds have elapsed any remaining
	// unsent data may be discarded.
	SetLinger(sec int) error

	// SetKeepAlivePeriod tells operating system to send keep-alive messages on the connection
	// and sets period between TCP keep-alive probes.
	SetKeepAlivePeriod(d time.Duration) error

	// SetNoDelay controls whether the operating system should delay
	// packet transmission in hopes of sending fewer packets (Nagle's
	// algorithm).
	// The default is true (no delay), meaning that data is sent as soon as possible after a Write.
	SetNoDelay(noDelay bool) error
}

Socket is a set of functions which manipulate the underlying file descriptor of a connection.

type TCPSocketOpt

type TCPSocketOpt int

TCPSocketOpt is the type of TCP socket options.

const (
	TCPNoDelay TCPSocketOpt = iota
	TCPDelay
)

Available TCP socket options.

type Writer

type Writer interface {
	io.Writer
	io.ReaderFrom // must be non-blocking, otherwise it may block the event-loop.

	// Writev writes multiple byte slices to peer synchronously, you must call it in the current goroutine.
	Writev(bs [][]byte) (n int, err error)

	// WriteToUDP writes multiple byte slices to peer synchronously, you must call it in the current goroutine.
	WriteToUDP(p []byte, sa unix.Sockaddr) (int, error)

	// Flush writes any buffered data to the underlying connection, you must call it in the current goroutine.
	Flush() (err error)

	// OutboundBuffered returns the number of bytes that can be read from the current buffer.
	OutboundBuffered() (n int)

	// AsyncWrite writes one byte slice to peer asynchronously, usually you would call it in individual goroutines
	// instead of the event-loop goroutines.
	AsyncWrite(buf []byte, callback AsyncCallback) (err error)

	// AsyncWritev writes multiple byte slices to peer asynchronously, usually you would call it in individual goroutines
	// instead of the event-loop goroutines.
	AsyncWritev(bs [][]byte, callback AsyncCallback) (err error)
}

Writer is an interface that consists of a number of methods for writing that Conn must implement.

Directories

Path Synopsis
internal
bs
io
queue
Package queue delivers an implementation of lock-free concurrent queue based on the algorithm presented by Maged M. Michael and Michael L. Scot.
Package queue delivers an implementation of lock-free concurrent queue based on the algorithm presented by Maged M. Michael and Michael L. Scot.
socket
Package socket provides functions that return fd and net.Addr based on given the protocol and address with a SO_REUSEPORT option set to the socket.
Package socket provides functions that return fd and net.Addr based on given the protocol and address with a SO_REUSEPORT option set to the socket.
pkg
logging
Package logging provides logging functionality for gnet server, it sets up a default logger (powered by go.uber.org/zap) which is about to be used by gnet server, it also allows users to replace the default logger with their customized logger by just implementing the `Logger` interface and assign it to the functional option `Options.Logger`, pass it to `gnet.Serve` method.
Package logging provides logging functionality for gnet server, it sets up a default logger (powered by go.uber.org/zap) which is about to be used by gnet server, it also allows users to replace the default logger with their customized logger by just implementing the `Logger` interface and assign it to the functional option `Options.Logger`, pass it to `gnet.Serve` method.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL