Skip to content
This repository has been archived by the owner on Jan 16, 2021. It is now read-only.

tutorial

Eddie Ng edited this page Dec 12, 2016 · 11 revisions

Tutorial on Building Web Services With OKWS

The gameplan for building and running a new service with OKWS is as follows:

  • Make a new standalone Unix executable that will be the Web service. The executable needs to follow the OKWS protocol for receving new connections from the demultiplexer, and also for interacting with the OKWS publishing system. Though it's possible to do this from scratch, it's much easier to use our supplied C++ libraries.
  • Install your new executable in the right place. If running OKWS as an unprivileged user, you can run in place, but if running with privileged (and binding to a low port) you must put the executable in a runtime jail along with all of the shared objects it needs to run in the jail.
  • Edit OKWS's configuration file. So that OKWS launches and looks for your new service.
  • Restart OKWS. When adding or removing services, you must restart OKWS.

Writing OKWS Services

To write an OKWS service with our given libraries, the programmer needs to implement two OKWS classes: one corresponding to the base service object (one per running service), and one corresponding to a per-HTTP-request object. We'll be explaining this process in reference to a standard static Web server included as part of the included examples (test/system/simple.T). This service just reads OKWS templates off the disk and serves them to the client.

In our example, we have the two classes as follows:

class oksrvc_simple_t : public oksrvc_t {
public:
  oksrvc_simple_t (int argc, char *argv[]) : oksrvc_t (argc, argv) {}
  okclnt_t *make_newclnt (ptr<ahttpcon> x);
  void post_launch_pub (evb_t ev) { post_launch_pub_T (ev); }
private:
  void post_launch_pub_T (evb_t ev, CLOSURE);
};

class okclnt_simple_t : public okclnt2_t {
public:
  okclnt_simple_t (ptr<ahttpcon> x, oksrvc_simple_t *o)
    : okclnt2_t (x, o), ok_simple (o) {}
  ~okclnt_simple_t () {}

  void process (proc_ev_t ev) { process_T (ev); }
protected:
  void process_T (proc_ev_t ev, CLOSURE);
  oksrvc_simple_t *ok_static;
}

We describe this code below:

Service Objects

The class oksrvc_simple_t corresponds to the long-lived HTTP service. There will be one object allocated when the service is started up and it will persist across all requests. The primary job of the service object is to allocate new client objects as new HTTP requests come in. It does this by implementing the oksrvc_t::make_newclnt virtual method. This is the service's opportunity to make a client object of its choosing that will give the Web logic needed for the given application. In this case:

okclnt_t *
oksrvc_simple_t::make_newclnt (ptr<ahttpcon> x)
{
  return New okclnt_simple_t (x, this);
}

This method is called once per incoming HTTP web request, and thus there's one okclnt_simple_t object per request.

From the code, another method of oksrvc_simple_t is the constructor, which just calls the superclass constructor with the arguments passed to main. This is crucial, since the underlying service object needs to parse options given to it by the OKWS launcher.

Finally, the oksrvc_simple_t class implements the virtual method oksrvc_t::post_pub_init. What's happening here is that the OKWS system is initializing a pub object, which is used for reading configuration files, and HTML templates off the file system and into OKWS. This virtual method gives this particular service opporunity to read in configuration data, perhaps specifying which databases to connect to, which language to run in, etc. This virtual method is called after the pub object is initialized, but before the service has started accepting incoming HTTP connections. Note the signature of this method:

void post_launch_pub (evb_t ev);

The idea is that the service does its thing, perhaps firing off some asynchronous calls and waiting for results. After all of that completes, it calls the cb callback to signify that it's done, providing true in the case of success and false in the case of failure.

To drill down a bit further, let's see how this particular OKWS service implements this method. To the class definition we add a helper method, since we're implementing a virtual method with the tame system.

  void post_launch_pub (evb_t ev) { post_launch_pub_T (ev); }
private:
  void post_launch_pub_T (evb_t ev, CLOSURE);

In post_launch_pub_T, the simple service is just reading a single configuration file off the file system:

tamed void
oksrvc_simple_t::post_launch_pub_T (evb_t ev)
{
  tvars {
    bool res1, res2;
  }
  twait {
    oksrvc_t::post_launch_pub (mkevent (res1));
    pub3 ()->run_cfg ("conf/intl.conf", mkevent (res2));
  }
  ev->trigger (res1 && res2);
}

Here's what's going on. The two operations oksrvc_t::post_launch_pub and run_cfg are launched parallely, and control blocks until both finish. The standard post_launch_pub method loads standard pub-related data, like the location of Error 400 and/or Error 404 documents. The run_cfg method reads a configuration file off the disk, parses it, and loads configuration file name/value pairs into the pub3() object. In this case, we're loading a file that defines which language our locale is (setting LANG equal to en).

Client Objects

Client objects inheret from OKWS's base class for handling and HTTP client connection, called okclnt_t. A client object is allocated with a new incoming HTTP network connection (ptr<ahttpcon> x) and a link to the service object. The client's constructor ought to call okclnt_t's constructor to initialize OKWS's internal data objects. It might also want to keep a pointer to the service object around, so that it's typed properly:

 okclnt_simple_t (ptr<ahttpcon> x, oksrvc_simple_t *o)
     : okclnt2_t (x, o), ok_simple (o) {}

All client classes must implement the abstract virtual method okclnt_t::process. What's going on behind the scenes is that the generic machinery reads the client's HTTP request from the network socket and parses its headers and body. Once all of that data is read in and understood (and no errors were encountered), it looks to subclasses to actually implement what to do with that data. We'll get to an example now:

tamed void
okclnt_simple_t::process_T (proc_ev_t ev)
{
  tvars {
    pub3::obj_dict_t d;     // a dictionary!
    bool rc (true);
    pub3::opts_t opts (pub3::P_IINFO|pub3::P_VERBOSE);
    str file;
  }
  file = cgi["file"];

  if (file) {
    // load all CGI values into the associative array
    cgi.load_dict (d.to_dict ());

    // publish the file 'file' to the output target &out.
    // 'out' is a member of okclnt_t, which is a buffer
    // that accepts output that will be sent to the client.
    // Return the output to the bool 'rc'.  When
    // publishing 'out', if values of the form ${X} are
    // encountered, substitute with value 'X' from aarr.
    twait { pub3 ()->run (&out, file, mkevent (rc), d.to_dict (), opts); }

    if (!rc) {
      aarr.add ("target", file);
      file = "/fnf.html";
    }
  } else {
    aarr.add ("target", "&lt; <i>no <tt>file</tt> specified</i> &gt;");
    file = "/fnf.html";
    rc = false;
  }
  if (!rc) {
    // publish some error document if failure.
    twait { pub3 ()->run (&out, file, mkevent (rc), d.to_dict (), opts|pub3::P_VISERR); }
  }
  // when everything is done, output 'out' buffer to the client,
  // deleting the 'this' object.  Thus, don't access class members
  // after calling 'output'
  twait { output (out, mkevent ()); }
  ev->trigger (true, HTTP_OK);
}
Clone this wiki locally