rpc

package module
v1.0.12 Latest Latest
Warning

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

Go to latest
Published: May 26, 2019 License: Apache-2.0 Imports: 11 Imported by: 3

README

GOPI Go Language Application Framework

CircleCI

This respository contains remote procedure call (RPC) and service discovery modules for gopi. It supports gRPC and mDNS at present. This README guide will walk you though:

  • Satisfying dependencies
  • Building the helloworld service and client
  • Understanding how to use an existing client in your application
  • Generating a protocol buffer file for your service
  • Creating a new service and client

Please also see documentation for:

Introduction

A "microservice" is a server-based process which can satisfy remote procedure calls, by accepting requests, processing the information within the service, and providing a response. A "simple" microservice might provide a single response to a request, a more complicated version will accept requests in a "stream" and may provide responses similarly in a "stream".

Protocol Buffers are an attractive mechanism for defining a schema for this request and response, and can generate both the client and server code programmatically in many languages using a compiler. The Google gRPC project is a useful counterpart for the compiler, providing supporting libraries, but there are others such as Twerp which can be used to provide a more traditional REST-based interface on compiling the protocol buffer code.

When you have your service running on your network, how do other processes discover it? This is where the discovery mechanisms come into play. For local area networks, discovery by DNS provides an easy mechansism, specifically using multicast DNS and the DNS-SD protocol. For cloud environents, service registration and discovery through services like Consul.

Dependencies

It is assumed you're using either a MacOS or Debian Linux machine. For MacOS, you should be using the Homebrew Package Manager:

bash% brew install protobuf
bash% go get -u github.com/golang/protobuf/protoc-gen-go

For Debian Linux:

bash# sudo apt install protobuf-compiler
bash# sudo apt install libprotobuf-dev
bash# go get -u github.com/golang/protobuf/protoc-gen-go

You can then use the protoc compiler command with the gRPC plugin to generate golang code for client and server.

The "helloworld" service

As an example, the Greeter service provides a call SayHello which takes a name parameter and returns a greeting message. The definition of the service is available in the folder rpc/protobuf/helloworld.proto:

package gopi;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}
message HelloReply {
  string message = 1;
}

The fully qualified name of the service is gopi.Greeter. The golang code to construct the client and service can be generated with the make protobuf command:

bash% make protobuf
go generate -x ./rpc/...
protoc helloworld/helloworld.proto --go_out=plugins=grpc:.

This will create the generated client and server code in the file rpc/protobuf/helloworld.pb.go. There are some other services defined in the rpc/protobuf folder:

  • gopi.Version returns version numbers of the running service, service and host uptime.
  • gopi.Discovery returns service records for any discovered and registered services on the local area network.

The make protobuf command generates the client and server code for these as well. In order to generate the helloworld client and service binaries, use the following commands:

bash% make helloworld-service
bash% make helloworld-client

You can run the helloworld service with unencrypted requests and responses:

bash% helloworld-service -rpc.port 8080 -verbose
[INFO] Waiting for CTRL+C or SIGTERM to stop server

Then to communicate with the service, use the following command in a separate terminal window:

bash% helloworld-client -addr localhost:8080 -rpc.insecure
Service says: "Hello, World"
bash% helloworld-client -addr localhost:8080 -rpc.insecure -name David
Service says: "Hello, David"

You can use encrypted communications if you provide an SSL key and certificate. In order to generate a self-signed certificate, use the following commands, replacing DAYS, OUT and ORG with appropriate values:

bash% DAYS=99999
bash% OUT="${HOME}/.ssl"
bash% ORG="mutablelogic"
bash% install -d ${OUT} && openssl req \
  -x509 -nodes \
  -newkey rsa:2048 \
  -keyout "${OUT}/selfsigned.key" \
  -out "${OUT}/selfsigned.crt" \
  -days "${DAYS}" \
  -subj "/C=GB/L=London/O=${ORG}"

Then the following commands are used to invoke the service:

bash% helloworld-service -rpc.port 8080 \
  -rpc.sslkey ${OUT}/selfsigned.key -rpc.sslcert  ${OUT}/selfsigned.crt \
  -verbose
[INFO] Waiting for CTRL+C or SIGTERM to stop server

You can then drop the -rpc.insecure flag when invoking the client.

Getting service information

The helloworld client can also report what services are running remotely, using an argument. For example,

bash% helloworld-client -addr rpi3plus:44453 services
+------------------------------------------+
|                 SERVICE                  |
+------------------------------------------+
| gopi.Greeter                             |
| gopi.Version                             |
| grpc.reflection.v1alpha.ServerReflection |
+------------------------------------------+

To invoke the gopi.Version service with the client (which returns some information about the remote server):

bash% helloworld-client -addr rpi3plus:44453 version
+---------------+------------------------------------------+
|      KEY      |                  VALUE                   |
+---------------+------------------------------------------+
| goversion     | go1.12.1                                 |
| gittag        | v1.0.5-7-gb433a3b                        |
| hostname      | rpi3plus                                 |
| execname      | helloworld-service                       |
| servicename   | gopi                                     |
| githash       | b433a3bed938a452d874dc6df4c50ab8ba15e036 |
| serviceuptime | 14m2s                                    |
| gobuildtime   | 2019-04-28T16:43:54Z                     |
| gitbranch     | v1                                       |
+---------------+------------------------------------------+

The "dns-discovery" command

Often microservices are "discovered" on the network, rather than known about in advance. Discovery can be through DNS or through cached knowledge of what microservices are running. The dns-discovery command allows you to retrieve information about services through DNS. To make the command and discover services on the local network:

bash% make dns-discovery
bash% dns-discovery -timeout 2s
+---------------------+
|       SERVICE       |
+---------------------+
| _adisk._tcp         |
| _apple-mobdev2._tcp |
| _http._tcp          |
| _googlerpc._tcp     |
| _googlecast._tcp    |
| _googlezone._tcp    |
| _sftp-ssh._tcp      |
+---------------------+

This command returns any discovered service names provided within two seconds, for example, _sftp-ssh._tcp indicates there are SFTP services on the network. Service instances can then be looked up:

bash% dns-discovery -timeout 2s _sftp-ssh._tcp
+----------------+--------------+-------------------+--------------------------------+
|    SERVICE     | NAME         | HOST              |               IP               |
+----------------+--------------+-------------------+--------------------------------+
| _sftp-ssh._tcp | MacBook      | MacBook.local.:22 | 192.168.1.1                    |
|                |              |                   | fe80::10f7:b0b1:81cb:3e7b      |
|                |              |                   | fd00::1:1c20:4b27:3d42:1082    |
+----------------+--------------+-------------------+--------------------------------+

In this example there is a host called "MacBook" providing a service instance on port 22. The IP addresses listed can be used to connect to SFTP. You can also keep the command running to cache records from the network. For example:

bash% dns-discovery -timeout 2s -watch -dns-sd.db cache.json
ADDED      _ipp._tcp                      Brother\ HL-3170CDW\ series (brother-eth.local.:631)

The command will stream the records onto your screen, either ADDED, UPDATED, EXPIRED or REMOVED. Press CTRL+C to interrupt. A file cache.json will be maintained in your home folder which contains discovered service instances on the network, whilst the command is running.

Using a client in your own application

If you want to use a client in your own application, you'll need to do the following:

  1. Know the DNS service name or address & port you want to connect to;
  2. Use a "client pool" to lookup and return a service record;
  3. Connect to the remote service instance using the service record;
  4. If you're connecting to a gRPC service instance, know the name of the gRPC service;
  5. Create a gRPC client with the connection;
  6. Use the client to call remote service methods.

The RPCClientPool interface provides you with all the method required to do this:

package gopi

type RPCClientPool interface {
  // Lookup one or more service records
  Lookup(ctx context.Context, name, addr string, max int) ([]RPCServiceRecord, error)

  // Connect to a service instance
  Connect(service RPCServiceRecord, flags RPCFlag) (RPCClientConn, error)

  // Create an RPCClient
  NewClient(string, RPCClientConn) RPCClient
}

For example, here is a function in helloworld-client which creates a new connection:

func Conn(service,addr string,timeout time.Duration) (gopi.RPCClientConn, error) {
  pool := app.ModuleInstance("rpc/clientpool").(gopi.RPCClientPool)
  ctx, cancel := context.WithTimeout(context.Background(),timeout)
  defer cancel()

  if records, err := pool.Lookup(ctx,service,addr, 1); err != nil {
    return nil, err
  } else if len(records) == 0 {
    return nil, gopi.ErrNotFound
  } else if conn, err := pool.Connect(records[0],gopi.RPC_FLAG_NONE); err != nil {
    return nil, err
  } else {
    return conn, nil
  }
}

The Lookup function arguments are:

  • The service name to connect to, or if empty will connect to any remote service instance;
  • The address of the instance including the port. If empty, will connect to any service instance;
  • The maximum number of service records to return, or zero for unlimited.

The Connect function can be used to return an RPCClientConn object from a service record. The second argument can be RPC_FLAG_INET_V4 or RPC_FLAG_INET_V6 if you want to select one protocol or the other:

package gopi

type RPCClientConn interface {
  // Remote address and port
  Addr() string

  // Return list of services published by remote service instance
  Services() ([]string, error)
}

Once you have a client connection, you can create an RPC client, which can then be used to call remote methods. In order to do this, a client module needs to be registered. For example,

package main

import (
  gopi  "github.com/djthorpe/gopi"
  hw    "github.com/djthorpe/gopi-rpc/rpc/grpc/helloworld"
)

////////////////////////////////////////////////////////////////////////////////

func Main(app *gopi.AppInstance, done chan<- struct{}) error {  
  if conn, err := Conn(app); err != nil {
    return err
  } else {
    client_ := pool.NewClient("gopi.Greeter", conn).(*hw.Client)
    if reply, err := client.SayHello(name); err != nil {
      return err
    } else {
      fmt.Println("Service says:",reply)
    }
  }
  return nil
}

////////////////////////////////////////////////////////////////////////////////

func main() {
  // Create the configuration
  config := gopi.NewAppConfig("rpc/helloworld:client")

  // Set flags
  config.AppFlags.FlagString("addr", "localhost:8080", "Gateway address")

  // Run the command line tool
  os.Exit(gopi.CommandLineTool(config, Main))
}

Note that the client module for the helloworld service is called rpc/helloworld:client and in order to register it you need to import the module from github.com/djthorpe/gopi-rpc/rpc/grpc/helloworld.

Writing a service command

A server is a long-running process which can be composed of one or more RPC services. If you have already created the service definitions (which is described in the sections below) you'll simply need to import the service module files and use the gopi.RPCServerTool function to start the server. For example, the following command publishes the gopi.Greeter and gopi.Version services:

package main

import (
  "os"

  // Frameworks
  gopi "github.com/djthorpe/gopi"

  // Modules
  _ "github.com/djthorpe/gopi-rpc/sys/grpc"
  _ "github.com/djthorpe/gopi/sys/logger"

  // Services
  _ "github.com/djthorpe/gopi-rpc/rpc/grpc/helloworld"
  _ "github.com/djthorpe/gopi-rpc/rpc/grpc/version"
)

////////////////////////////////////////////////////////////////////////////////

func main() {
  // Create the configuration
  config := gopi.NewAppConfig("rpc/helloworld:service", "rpc/version:service")

  // Run the server and register all the services
  os.Exit(gopi.RPCServerTool(config))
}

Creating a new service and client

In order to create your own microservice and clients, you'll need to do the following:

  1. Define your service methods and messages in a protocol buffer file;
  2. Generate the gRPC client and service stubs;
  3. Write the module code to interface to the service stub (service.go);
  4. Write the module code to interface to the client stub (client.go);
  5. Write the initialization code to register the modules (init.go);
  6. Optionally, write some serialization logic to translate between native types & protobuf types.

It may seem daunting, and ultimately it's a lot of work and quite a bit of boilerplate. To make it even more daunting, the folder and file structure could look like this, if you were to create a new microservice called foobar for example:

foobar/
  -> foobar.go
  -> rpc/
    -> protobuf/
      -> protobuf.go
      -> foobar/
        -> foobar.proto
    -> gprc/
      -> foobar/
        -> client.go
        -> service.go
        -> init.go
        -> serialize.go
  -> sys/
    -> foobar/
      -> foobar.go
      -> init.go

In short,

  • The foobar/foobar.go file may comtain interface and type definitions when you want to import foobar elsewhere;
  • The foobar/sys/foobar folder contains the module code for your foobar business logic, including the foobar.go driver and the module initialization code init.go;
  • The foobar/rpc/protobuf/foobar/foobar.proto contains your service definition;
  • The foobar/rpc/protobuf/protobuf.go contains a single generate directive to create the client and server stubs;
  • The foobar/grpc/foobar folder contains the client, server and serialization code and also the module initialization code in init.go.

The next few subsections describes what you need to put in all the files.

Generating a protocol buffer file for your service definition

TODO

Generating the stubs

TODO

Writing the service module

TODO

Writing the client module

TODO

The initalization code

TODO

Serialization code

TODO

Documentation

Index

Constants

View Source
const (
	DISCOVERY_SERVICE_QUERY = "_services._dns-sd._udp"
)

Variables

This section is empty.

Functions

func MainTask added in v1.0.11

func MainTask(app *gopi.AppInstance, done chan<- struct{}) error

func ProcessEvent added in v1.0.11

func ProcessEvent(app *gopi.AppInstance, server gopi.RPCServer, discovery gopi.RPCServiceDiscovery, evt gopi.RPCEvent) error

func RegisterTask added in v1.0.11

func RegisterTask(app *gopi.AppInstance, start chan<- struct{}, stop <-chan struct{}) error

func Server added in v1.0.11

func Server(config gopi.AppConfig, background_tasks ...gopi.BackgroundTask) int

func ServerTask added in v1.0.11

func ServerTask(app *gopi.AppInstance, start chan<- struct{}, stop <-chan struct{}) error

Types

type DiscoveryClient added in v1.0.7

type DiscoveryClient interface {
	gopi.RPCClient

	// Ping the remote service instance
	Ping() error

	// Register a service record
	Register(gopi.RPCServiceRecord) error

	// Enumerate service names
	Enumerate(DiscoveryType, time.Duration) ([]string, error)

	// Lookup service instances
	Lookup(string, DiscoveryType, time.Duration) ([]gopi.RPCServiceRecord, error)

	// Stream discovery events. filtering by service name
	StreamEvents(string, chan<- gopi.RPCEvent) error
}

type DiscoveryType added in v1.0.6

type DiscoveryType uint

DiscoveryType is either DNS (using DNS-SD) or DB (using internal database)

const (
	DISCOVERY_TYPE_NONE DiscoveryType = 0
	DISCOVERY_TYPE_DNS  DiscoveryType = 1
	DISCOVERY_TYPE_DB   DiscoveryType = 2
)

type Gaffer added in v1.0.12

type Gaffer interface {
	gopi.Driver
	gopi.Publisher

	// Return all services, groups, instances and executables
	GetServices() []GafferService
	GetGroups() []GafferServiceGroup
	GetInstances() []GafferServiceInstance
	GetExecutables(recursive bool) []string

	// Services
	AddServiceForPath(string) (GafferService, error)
	GetServiceForName(string) GafferService
	RemoveServiceForName(string) error
	SetServiceNameForName(service string, new string) error
	SetServiceModeForName(string, GafferServiceMode) error
	SetServiceInstanceCountForName(service string, count uint) error
	SetServiceGroupsForName(service string, groups []string) error

	// Groups
	GetGroupsForNames([]string) []GafferServiceGroup
	AddGroupForName(string) (GafferServiceGroup, error)
	SetGroupNameForName(group string, new string) error
	RemoveGroupForName(string) error

	// Tuples
	SetServiceFlagsForName(string, Tuples) error
	SetGroupFlagsForName(string, Tuples) error
	SetGroupEnvForName(string, Tuples) error

	// Instances
	GetInstanceForId(id uint32) GafferServiceInstance
	GenerateInstanceId() uint32
	StartInstanceForServiceName(service string, id uint32) (GafferServiceInstance, error)
	StopInstanceForId(id uint32) error
}

type GafferClient added in v1.0.12

type GafferClient interface {
	gopi.RPCClient

	// Ping remote microservice
	Ping() error

	// Return list of executables which can be used as microservices
	ListExecutables() ([]string, error)

	// Return services
	ListServices() ([]GafferService, error)
	ListServicesForGroup(string) ([]GafferService, error)
	GetService(string) (GafferService, error)

	// Return groups
	ListGroups() ([]GafferServiceGroup, error)
	ListGroupsForService(string) ([]GafferServiceGroup, error)
	GetGroup(string) (GafferServiceGroup, error)

	// Return instances
	ListInstances() ([]GafferServiceInstance, error)

	// Add services and groups
	AddServiceForPath(path string, groups []string) (GafferService, error)
	AddGroupForName(string) (GafferServiceGroup, error)

	// Remove services and groups
	RemoveServiceForName(string) error
	RemoveGroupForName(string) error

	// Start instances
	GetInstanceId() (uint32, error)
	StartInstance(string, uint32) (GafferServiceInstance, error)
	StopInstance(uint32) (GafferServiceInstance, error)

	// Set flags and env
	SetFlagsForService(string, Tuples) (GafferService, error)
	SetFlagsForGroup(string, Tuples) (GafferServiceGroup, error)
	SetEnvForGroup(string, Tuples) (GafferServiceGroup, error)

	// Set other service parameters
	SetServiceGroups(string, []string) (GafferService, error)

	// Stream Events
	StreamEvents(chan<- GafferEvent) error
}

type GafferEvent added in v1.0.12

type GafferEvent interface {
	gopi.Event

	Type() GafferEventType
	Service() GafferService
	Group() GafferServiceGroup
	Instance() GafferServiceInstance
	Data() []byte
}

type GafferEventType added in v1.0.12

type GafferEventType uint
const (
	GAFFER_EVENT_NONE GafferEventType = iota
	GAFFER_EVENT_SERVICE_ADD
	GAFFER_EVENT_SERVICE_CHANGE
	GAFFER_EVENT_SERVICE_REMOVE
	GAFFER_EVENT_GROUP_ADD
	GAFFER_EVENT_GROUP_CHANGE
	GAFFER_EVENT_GROUP_REMOVE
	GAFFER_EVENT_INSTANCE_ADD
	GAFFER_EVENT_INSTANCE_START
	GAFFER_EVENT_INSTANCE_RUN
	GAFFER_EVENT_INSTANCE_STOP_OK
	GAFFER_EVENT_INSTANCE_STOP_ERROR
	GAFFER_EVENT_INSTANCE_STOP_KILLED
	GAFFER_EVENT_LOG_STDOUT
	GAFFER_EVENT_LOG_STDERR
)

func (GafferEventType) String added in v1.0.12

func (t GafferEventType) String() string

type GafferService added in v1.0.12

type GafferService interface {
	Name() string
	Path() string
	Groups() []string
	Mode() GafferServiceMode
	InstanceCount() uint
	RunTime() time.Duration
	IdleTime() time.Duration
	Flags() Tuples
	IsMemberOfGroup(string) bool
}

type GafferServiceGroup added in v1.0.12

type GafferServiceGroup interface {
	Name() string
	Flags() Tuples
	Env() Tuples
}

type GafferServiceInstance added in v1.0.12

type GafferServiceInstance interface {
	Id() uint32
	Service() GafferService
	Flags() Tuples
	Env() Tuples
	Start() time.Time
	Stop() time.Time
	ExitCode() int64
}

type GafferServiceMode added in v1.0.12

type GafferServiceMode uint
const (
	GAFFER_MODE_NONE GafferServiceMode = iota
	GAFFER_MODE_MANUAL
	GAFFER_MODE_AUTO
)

func (GafferServiceMode) MarshalJSON added in v1.0.12

func (m GafferServiceMode) MarshalJSON() ([]byte, error)

func (GafferServiceMode) String added in v1.0.12

func (m GafferServiceMode) String() string

func (*GafferServiceMode) UnmarshalJSON added in v1.0.12

func (m *GafferServiceMode) UnmarshalJSON(data []byte) error

type GoogleCast added in v1.0.11

type GoogleCast interface {
	gopi.Driver
	gopi.Publisher

	Devices() []GoogleCastDevice
}

type GoogleCastClient added in v1.0.11

type GoogleCastClient interface {
	// Ping remote service
	Ping() error

	// Return devices from the remote service
	Devices() ([]GoogleCastDevice, error)

	// Stream discovery events
	StreamEvents(string, chan<- GoogleCastEvent) error
}

type GoogleCastDevice added in v1.0.11

type GoogleCastDevice interface {
	Id() string
	Name() string
	Model() string
	Service() string
	State() uint
}

type GoogleCastEvent added in v1.0.11

type GoogleCastEvent interface {
	gopi.Event

	Type() gopi.RPCEventType
	Device() GoogleCastDevice
	Timestamp() time.Time
}

type GreeterClient added in v1.0.7

type GreeterClient interface {
	gopi.RPCClient

	// Ping the remote service instance
	Ping() error

	// Return a message from the remote service
	SayHello(name string) (string, error)
}

type ServiceRecord

type ServiceRecord interface {
	gopi.RPCServiceRecord

	// Key returns the PTR record for the service record
	Key() string

	// Expired returns true if TTL has been reached
	Expired() bool

	// Source returns the source of the record
	Source() DiscoveryType

	// Get DNS answers
	PTR(zone string, ttl time.Duration) *dns.PTR
	SRV(zone string, ttl time.Duration) *dns.SRV
	TXT(zone string, ttl time.Duration) *dns.TXT
	A(zone string, ttl time.Duration) []*dns.A
	AAAA(zone string, ttl time.Duration) []*dns.AAAA

	// Set parameters
	SetService(service, subtype string) error
	SetName(name string) error
	SetAddr(addr string) error
	SetPTR(zone string, rr *dns.PTR) error
	SetSRV(zone string, rr *dns.SRV) error
	SetTTL(time.Duration) error
	AppendIP(...net.IP) error
	AppendTXT(...string) error
}

type Tuples added in v1.0.12

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

func (Tuples) Copy added in v1.0.12

func (this Tuples) Copy() Tuples

Copy returns a copy of the tuples

func (Tuples) Env added in v1.0.12

func (this Tuples) Env() []string

Env returns tuples as a set of key value parameters

func (Tuples) Equals added in v1.0.12

func (this Tuples) Equals(that Tuples) bool

Equals returns true if the tuples are identical

func (*Tuples) ExistsForKey added in v1.0.12

func (this *Tuples) ExistsForKey(k string) bool

ExistsForKey returns true if a key is present

func (Tuples) Flags added in v1.0.12

func (this Tuples) Flags() []string

Flags returns tuples as a set of flags, including the initial '-' character

func (*Tuples) Keys added in v1.0.12

func (this *Tuples) Keys() []string

Keys returns an array of keys

func (*Tuples) Len added in v1.0.12

func (this *Tuples) Len() int

Len returns the number of tuples

func (Tuples) MarshalJSON added in v1.0.12

func (t Tuples) MarshalJSON() ([]byte, error)

func (*Tuples) RemoveAll added in v1.0.12

func (this *Tuples) RemoveAll()

RemoveAll removes all tuples

func (*Tuples) SetStringForKey added in v1.0.12

func (this *Tuples) SetStringForKey(k, v string) error

SetStringForKey sets a tuple key-value pair. Returns error if a key is invalid

func (Tuples) String added in v1.0.12

func (this Tuples) String() string

String returns the string representation of the tuples

func (*Tuples) StringForKey added in v1.0.12

func (this *Tuples) StringForKey(k string) string

StringForKey returns the string value for a key or an empty string if a keyed tuple was not found

func (*Tuples) UnmarshalJSON added in v1.0.12

func (t *Tuples) UnmarshalJSON(data []byte) error

type Util added in v1.0.7

type Util interface {
	gopi.Driver

	// NewEvent creates a new event from source, type and service record
	NewEvent(gopi.Driver, gopi.RPCEventType, gopi.RPCServiceRecord) gopi.RPCEvent

	// NewServiceRecord creates an empty service record
	NewServiceRecord(source DiscoveryType) ServiceRecord

	// Read and write array of service records
	Writer(fh io.Writer, records []ServiceRecord, indent bool) error
	Reader(fh io.Reader) ([]ServiceRecord, error)
}

type VersionClient added in v1.0.7

type VersionClient interface {
	gopi.RPCClient

	// Ping the remote service instance
	Ping() error

	// Return version parameters from the remote service
	Version() (map[string]string, error)
}

Directories

Path Synopsis
cmd
helloworld-client
An example RPC Client tool
An example RPC Client tool
helloworld-service
An RPC Server tool, import the services as modules
An RPC Server tool, import the services as modules
rpc
sys

Jump to

Keyboard shortcuts

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