| = yaml = |
| title: Client/Server Basics |
| layout: tutorial |
| wherein: you build a fortune teller service and a client to talk to it. |
| prerequisites: {completer: basics, scenario: a} |
| sort: 13 |
| toc: true |
| = yaml = |
| |
| # Introduction |
| |
| In the [hello world tutorial], you got a client and server running. |
| |
| In this tutorial, you'll build a slightly more complex client and |
| server that will serve as the framework for deeper explorations of |
| Vanadium _security_ and _service discovery_. |
| |
| This time, the server code is split into _multiple files_. The |
| new code structure lets later tutorials change only the parts that |
| need to change (e.g., a dispatcher, an authorizer), keeping later |
| code shorter and clearer. That means doing a few odd things here, like |
| writing whole code files that, for the time being, only return |
| __nil__. |
| |
| This tutorial will otherwise repeat the structure of the [hello world |
| tutorial]. |
| |
| # Terminology |
| |
| A Vanadium program can hold any number of __servers__ and __clients__. |
| When the distinction between these roles isn't important to the |
| context, a program may be called a __peer__. |
| |
| A server consists of an __endpoint__, a Vanadium address encapsulating |
| a hostname, port and other information, and a __dispatcher__. A |
| dispatcher maps an incoming request to an __authorizer__ and a |
| __service__. The request only makes it to the service if the |
| authorizer approves. A dispatcher holds an authorizer |
| and one or more services. |
| |
| A client is just stub code, typed to match a particular service |
| interface, bound to one or more endpoints on the network via a name |
| (more on that in the [naming tutorials]). It's the boilerplate |
| used to contact remote services. |
| |
|  |
| |
| A service has no knowledge of the authorizer protecting it. Neither |
| the services nor the authorizers are aware of the dispatcher holding |
| them. |
| |
| A typical program will have one server and many clients. The program |
| will offer some services to others, and contact other programs in an |
| effort to provide these services (e.g. a television might contact |
| several content providers). |
| |
| A network of programs looks like this: |
| |
|  |
| |
| In this tutorial you'll build two programs. The first holds one |
| server and no clients, and - at the risk of overloading terms - is simply |
| called _the server_. The second, via similar reasoning, is called |
| _the client_. |
| |
| # Define a service |
| |
| A service is an object that responds to remote procedure calls (RPCs). |
| The service you'll build here stores and offers up fortunes. |
| |
| ## The interface |
| |
| The first step to making a Vanadium service is to define it using the |
| [Vanadium Definition Language][vanadium-definition-language] (VDL). |
| |
| The following command (read more about `cat` usage |
| [here][cat command]) will put the VDL in the right spot: |
| |
| <!-- @defineService @buildjs @test @testui @completer --> |
| ``` |
| mkdir -p $V_TUT/src/fortune/ifc |
| cat - <<EOF >$V_TUT/src/fortune/ifc/fortune.vdl |
| package ifc |
| |
| type Fortune interface { |
| // Returns a random fortune. |
| Get() (wisdom string | error) |
| // Adds a fortune to the set used by Get(). |
| Add(wisdom string) error |
| } |
| EOF |
| ``` |
| |
| This file defines the [Go] package `ifc` (an abbreviation of |
| _interface_). You'll soon define the packages `service`, `util` and |
| `main`. Package symbols will be isolated from each other, per Go |
| namespace rules, making it easier to reason about code dependencies. |
| |
| ## Stub code |
| |
| |
| Use the `fortune.vdl` you just made to create the file |
| `fortune.vdl.go`. This Go code provides stubbed attachment points |
| that will soon be linked into a client and server. |
| |
| <!-- @compileInterface @buildjs @test @testui @completer --> |
| ``` |
| VDLROOT=$VANADIUM_RELEASE/src/v.io/v23/vdlroot \ |
| VDLPATH=$V_TUT/src \ |
| $V_BIN/vdl generate --lang go $V_TUT/src/fortune/ifc |
| go build fortune/ifc |
| ``` |
| |
| The `go build` command here is just a check for errors. In this |
| case, the command doesn't create object or executable |
| files. In these tutorials, `cat` commands that _create code files_ |
| are immediately followed by commands that attempt compilation as a |
| quick check for errors. When the code file created is a main program, |
| `go install` is used to install (or replace) an executable in |
| `$V_TUT/bin`. |
| |
| ## Implementation |
| |
| The following implementation stores fortune strings in memory, |
| choosing one randomly when `Get` is called. A client can add more |
| fortunes via `Add`. |
| |
| <!-- @serviceImpl @test @testui @completer --> |
| ``` |
| mkdir -p $V_TUT/src/fortune/service |
| cat - <<EOF >$V_TUT/src/fortune/service/service.go |
| package service |
| |
| import ( |
| "math/rand" |
| "fortune/ifc" |
| "sync" |
| "v.io/v23/context" |
| "v.io/v23/rpc" |
| ) |
| |
| type impl struct { |
| wisdom []string // All known fortunes. |
| random *rand.Rand // To pick a random index in 'wisdom'. |
| mu sync.RWMutex // To safely enable concurrent use. |
| } |
| |
| // Makes an implementation. |
| func Make() ifc.FortuneServerMethods { |
| return &impl { |
| wisdom: []string{ |
| "You will reach the heights of success.", |
| "Conquer your fears or they will conquer you.", |
| "Today is your lucky day!", |
| }, |
| random: rand.New(rand.NewSource(99)), |
| } |
| } |
| |
| func (f *impl) Get(_ *context.T, _ rpc.ServerCall) (blah string, err error) { |
| f.mu.RLock() |
| defer f.mu.RUnlock() |
| if len(f.wisdom) == 0 { |
| return "[empty]", nil |
| } |
| return f.wisdom[f.random.Intn(len(f.wisdom))], nil |
| } |
| |
| func (f *impl) Add(_ *context.T, _ rpc.ServerCall, blah string) error { |
| f.mu.Lock() |
| defer f.mu.Unlock() |
| f.wisdom = append(f.wisdom, blah) |
| return nil |
| } |
| EOF |
| go build fortune/service |
| ``` |
| |
| # Build a server |
| |
| Service in hand, we need a place to put it. |
| |
| [Recall](#terminology) that a server associates an endpoint with a |
| dispatcher, and a dispatcher contains one or more services. This |
| tutorial covers the simple case of a program with just _one_ server - |
| just one active port. Further, the server has just _one_ service and |
| _one_ authorizer. |
| |
| ## Authorizer |
| |
| Vanadium checks every request via a policy defined in an |
| implementation of `security.Authorizer`. |
| |
| The tutorials will use several authorizer implementations. |
| |
| At any given time, one implementation will be provided by a factory |
| function called `util.MakeAuthorizer` in a file called |
| `authorizer.go`. To keep code simple (no branches, no signature |
| changes, etc.), switching to a new implementation means replacing the |
| file and recompiling. |
| |
| The first version of `authorizer.go` invokes the default Vanadium |
| authorization policy (to be discussed [later][default-auth]): |
| |
| <!-- @authorizer @test @testui @completer --> |
| ``` |
| mkdir -p $V_TUT/src/fortune/server/util |
| cat - <<EOF >$V_TUT/src/fortune/server/util/authorizer.go |
| package util |
| |
| import ( |
| "v.io/v23/security" |
| ) |
| |
| // Returns Vanadium's default authorizer. |
| func MakeAuthorizer() security.Authorizer { |
| return security.DefaultAuthorizer() |
| } |
| EOF |
| go build fortune/server/util |
| ``` |
| |
| ## Dispatcher |
| |
| Dispatcher implementations will be provided by a factory function |
| called `util.MakeDispatcher` in a file called `dispatcher.go`, an |
| arrangement allowing for low tech implementation swaps as described |
| above for the authorizer. |
| |
| The version of `dispatcher.go` defined here results in the use of a |
| dispatcher built into Vanadium. It's initialized with only _one_ |
| service and uses reflection to invoke server methods. |
| |
| <!-- @dispatcher @test @testui @completer --> |
| ``` |
| mkdir -p $V_TUT/src/fortune/server/util |
| cat - <<EOF >$V_TUT/src/fortune/server/util/dispatcher.go |
| package util |
| |
| import ( |
| "v.io/v23/rpc" |
| ) |
| |
| // Returns nil to trigger use of the default dispatcher. |
| func MakeDispatcher() (d rpc.Dispatcher) { |
| return nil |
| } |
| EOF |
| go build fortune/server/util |
| ``` |
| |
| ## Initializer |
| |
| The following utility package allows endpoints to be written to a flag |
| controlled file. |
| |
| In this tutorial, rather than specify a port as was done in the |
| [hello world tutorial], we let the system pick a free one, and from it |
| generate an _endpoint specification_. |
| |
| Every time the server starts, the endpoint address changes. The |
| address is written to a file named via a flag. A client must know |
| this address in order to connect to the server, so it reads it from |
| the file. |
| |
| The [mount table tutorial] describes how a _mount table_ replaces this |
| file-based exchange with a service. |
| |
| |
| <!-- @intializer @test @testui @completer --> |
| ``` |
| mkdir -p $V_TUT/src/fortune/server/util |
| cat - <<EOF >$V_TUT/src/fortune/server/util/initializer.go |
| package util |
| |
| import ( |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "log" |
| |
| "v.io/v23/naming" |
| ) |
| |
| var ( |
| fileName = flag.String( |
| "endpoint-file-name", "", |
| "Write endpoint address to given file.") |
| ) |
| |
| func SaveEndpointToFile(e naming.Endpoint) { |
| if *fileName == "" { |
| return |
| } |
| contents := []byte( |
| naming.JoinAddressName(e.String(), "") + "\n") |
| if ioutil.WriteFile(*fileName, contents, 0644) != nil { |
| log.Panic("Error writing ", *fileName) |
| } |
| fmt.Printf("Wrote endpoint name to %v.\n", *fileName) |
| } |
| |
| EOF |
| go build fortune/server/util |
| ``` |
| |
| ## Installation |
| |
| Construct a server executable by defining a `main` function. |
| |
| The server's custom behavior will be encapsulated by the authorizer |
| and dispatcher defined above. |
| |
| <!-- @server @test @testui @completer --> |
| ``` |
| mkdir -p $V_TUT/src/fortune/server |
| cat - <<EOF >$V_TUT/src/fortune/server/main.go |
| package main |
| |
| import ( |
| "flag" |
| "fmt" |
| "log" |
| "fortune/ifc" |
| "fortune/server/util" |
| "fortune/service" |
| |
| "v.io/v23" |
| "v.io/v23/rpc" |
| "v.io/x/ref/lib/signals" |
| _ "v.io/x/ref/runtime/factories/generic" |
| ) |
| |
| var ( |
| serviceName = flag.String( |
| "service-name", "", |
| "Name for service in default mount table.") |
| ) |
| |
| func main() { |
| ctx, shutdown := v23.Init() |
| defer shutdown() |
| |
| // Attach the 'fortune service' implementation |
| // defined above to a queriable, textual description |
| // of the implementation used for service discovery. |
| fortune := ifc.FortuneServer(service.Make()) |
| |
| // If the dispatcher isn't nil, it's presumed to have |
| // obtained its authorizer from util.MakeAuthorizer(). |
| dispatcher := util.MakeDispatcher() |
| |
| // Start serving. |
| var err error |
| var server rpc.Server |
| if dispatcher == nil { |
| // Use the default dispatcher. |
| _, server, err = v23.WithNewServer( |
| ctx, *serviceName, fortune, util.MakeAuthorizer()) |
| } else { |
| _, server, err = v23.WithNewDispatchingServer( |
| ctx, *serviceName, dispatcher) |
| } |
| if err != nil { |
| log.Panic("Error serving service: ", err) |
| } |
| endpoint := server.Status().Endpoints[0] |
| util.SaveEndpointToFile(endpoint) |
| fmt.Printf("Listening at: %v\n", endpoint) |
| |
| // Wait forever. |
| <-signals.ShutdownOnSignals(ctx) |
| } |
| EOF |
| go install fortune/server |
| ``` |
| |
| Successful installation places a new executable in the bin directory. |
| |
| ``` |
| ls $V_TUT/bin |
| ``` |
| The server is now ready to go. Next, make a client to work with it. |
| |
| # Build a client |
| |
| This client is short-lived; it starts, makes one call (`Get` or `Add`) |
| depending on a flag value, then exits. |
| |
| <!-- @client @test @testui @completer --> |
| ``` |
| mkdir -p $V_TUT/src/fortune/client |
| cat - <<EOF >$V_TUT/src/fortune/client/main.go |
| package main |
| |
| import ( |
| "flag" |
| "fmt" |
| "time" |
| |
| "fortune/ifc" |
| |
| "v.io/v23" |
| "v.io/v23/context" |
| "v.io/x/lib/vlog" |
| _ "v.io/x/ref/runtime/factories/generic" |
| ) |
| |
| var ( |
| server = flag.String( |
| "server", "", "Name of the server to connect to") |
| newFortune = flag.String( |
| "add", "", "A new fortune to add to the server's set") |
| ) |
| |
| func main() { |
| ctx, shutdown := v23.Init() |
| defer shutdown() |
| |
| if *server == "" { |
| vlog.Error("--server must be specified") |
| return |
| } |
| f := ifc.FortuneClient(*server) |
| ctx, cancel := context.WithTimeout(ctx, time.Minute) |
| defer cancel() |
| |
| if *newFortune == "" { // --add flag not specified |
| fortune, err := f.Get(ctx) |
| if err != nil { |
| vlog.Errorf("error getting fortune: %v", err) |
| return |
| } |
| fmt.Println(fortune) |
| } else { |
| if err := f.Add(ctx, *newFortune); err != nil { |
| vlog.Errorf("error adding fortune: %v", err) |
| return |
| } |
| } |
| } |
| EOF |
| go install fortune/client |
| ``` |
| |
| If that succeeds, a second binary will appear at |
| |
| ``` |
| ls $V_TUT/bin |
| ``` |
| |
| # Run the binaries |
| |
| ## First run |
| |
| This first run will demonstrate that we don't yet have enough |
| things defined to make a successful request. |
| |
| Start the server in the background: |
| |
| ``` |
| kill_tut_process TUT_PID_SERVER |
| $V_TUT/bin/server --endpoint-file-name $V_TUT/server.txt & |
| TUT_PID_SERVER=$! |
| ``` |
| |
| Before entering its event loop, the server writes the endpoint address |
| to `$V_TUT/server.txt`. The client will read it shortly. |
| |
| |
| {{# helpers.warning }} |
| ## Warning: The next command demonstrates failure! |
| |
| The correct pieces are in place to attempt a request - but the next command |
| shows that you've not yet established the conditions for authorization. |
| |
| {{/ helpers.warning }} |
| |
| |
| Now run the client, feeding it the server's endpoint, and attempt to |
| get a fortune: |
| |
| ``` |
| $V_TUT/bin/client --server `cat $V_TUT/server.txt` |
| ``` |
| |
| The client will report an authorization error. |
| |
| Let's change things to allow the call to succeed, then compare the two |
| situations. |
| |
| |
| ## Second run |
| |
| The simplest thing to do to allow a call to succeed under the [default |
| authorization policy][default-auth] is to run the client and server as |
| the same [principal] (think of this as an _identity_ for now). This |
| makes the two processes indistinguishable from an authorization point |
| of view. |
| |
| To do this, create a principal called `tutorial`. The next command |
| writes credentials associated with the name `tutorial` into the |
| `$V_TUT/cred/basics` directory: |
| |
| <!-- @principalTutorial @test @testui @completer --> |
| ``` |
| $V_BIN/principal create \ |
| --with-passphrase=false \ |
| --overwrite $V_TUT/cred/basics tutorial |
| ``` |
| |
| The `--overwrite` flag is optional. Omit it if you want a warning if |
| you attempt to overwrite an existing principal. |
| |
| Now kill the server and restart it, telling it to run with those |
| credentials: |
| |
| <!-- @runServerAsPrincipal @test @testui @sleep --> |
| ``` |
| kill_tut_process TUT_PID_SERVER |
| $V_TUT/bin/server \ |
| --v23.credentials $V_TUT/cred/basics \ |
| --endpoint-file-name $V_TUT/server.txt & |
| TUT_PID_SERVER=$! |
| ``` |
| |
| Run the client with the same credentials: |
| |
| <!-- @runClientAsPrincipal @test @testui --> |
| ``` |
| $V_TUT/bin/client \ |
| --v23.credentials $V_TUT/cred/basics \ |
| --server `cat $V_TUT/server.txt` |
| ``` |
| |
| This time, the `Get` RPC will succeed and a fortune will be displayed. |
| |
| Likewise, `Add` works: |
| |
| <!-- @clientGet @test @testui --> |
| ``` |
| $V_TUT/bin/client \ |
| --v23.credentials $V_TUT/cred/basics \ |
| --server `cat $V_TUT/server.txt` \ |
| --add 'Fortune favors the bold.' |
| ``` |
| |
| Feel free to rerun this without the `--add` flag until you |
| randomly recover the new fortune. |
| |
| ## Authorization |
| |
| Vanadium is secure by default. All communication channels are |
| encrypted and authenticated and all communication must satisfy an |
| authorization policy. |
| |
| In the first run above, no credentials were specified, so the server's |
| Vanadium runtime assigned *randomly generated* credentials, including |
| a characteristic name generated from the environment, e.g. |
| |
| > `{user}@{hostname}_{randomNumber}` |
| |
| Likewise, the client ran with randomly generated credentials. Random |
| credentials accompany requests, appear in logs, etc. and are |
| preferrable to building a policy around _missing_ credentials. |
| |
| In the first run, the server didn't recognize the client's randomly |
| different credentials and thus didn't authorize it. |
| |
| In the second run, the server effectively recognized the client as |
| itself, and authorized the call. The client and server ran with the |
| same credentials - they had the same principal and blessings. |
| |
| In practice, it is absolutely *not normal* to use the same principal |
| for distinct processes. Usually the client and server processes would |
| be on different machines, so running them as the same principal would |
| imply copying private keys around the network, completely defeating |
| the concept of _private_. |
| |
| ## The vrpc client |
| |
| Before continuing with more advanced tutorials, let's first introduce |
| a generally useful generic client called [vrpc]. |
| |
| It can talk to any Vanadium service. Try it with yours: |
| |
| <!-- @checkMethods @test @testui --> |
| ``` |
| $V_BIN/vrpc --v23.credentials $V_TUT/cred/basics \ |
| call `cat $V_TUT/server.txt` Get |
| $V_BIN/vrpc --v23.credentials $V_TUT/cred/basics \ |
| call `cat $V_TUT/server.txt` Add \"More cowbell.\" |
| ``` |
| |
| It is also possible to inspect the services offered by your server: |
| |
| <!-- @checkTheSignature @test @testui --> |
| ``` |
| $V_BIN/vrpc --v23.credentials $V_TUT/cred/basics \ |
| signature `cat $V_TUT/server.txt` |
| ``` |
| |
| That output includes methods you wrote, plus built-in methods that |
| support Vanadium security and service discovery. |
| |
| Report single methods like this: |
| |
| <!-- @checkMethods @test @testui --> |
| ``` |
| $V_BIN/vrpc --v23.credentials $V_TUT/cred/basics \ |
| signature `cat $V_TUT/server.txt` Add |
| $V_BIN/vrpc --v23.credentials $V_TUT/cred/basics \ |
| signature `cat $V_TUT/server.txt` Get |
| ``` |
| |
| ## Clean up |
| |
| <!-- @killServer @test @testui --> |
| ``` |
| kill_tut_process TUT_PID_SERVER |
| ``` |
| |
| |
| # Summary |
| |
| * You created a server that hosted a Fortune service defined using VDL |
| and made RPCs to it with a simple command line client. |
| |
| * The [default security policy][default-auth] prevented your initial |
| RPCs from working. |
| |
| * You 'fixed' this by running the client and server as the _same_ |
| principal, allowing requests to succeed. |
| |
| * You were introduced to the generic client `vrpc`. |
| |
| [hello world tutorial]: /tutorials/hello-world.html |
| [install]: /installation/ |
| [default-auth]: /tutorials/security/principals-and-blessings.html#default-authorization-policy |
| [vanadium-definition-language]: /glossary.html#vanadium-definition-language-vdl- |
| [blessing]: /glossary.html#blessing |
| [naming tutorials]: /tutorials/naming/ |
| [mount table tutorial]: /tutorials/naming/mount-table.html |
| [suffix tutorial]: /tutorials/naming/suffix.html |
| [principal]: /glossary.html#principal |
| [classic-fortune]: http://en.wikipedia.org/wiki/Fortune_(Unix) |
| [concepts-security]: /concepts/security.html |
| [vrpc]: /glossary.html#vrpc |
| [cat command]: /tutorials/faq.html#where-is-the-source-code- |
| [Go]: /tutorials/faq.html#do-i-need-to-know-go- |