See also:
The communication link between the two processes is made possible by software called a ``transport provider.'' The transport provider is responsible for moving data over the network between processes running on different computers. It is also responsible for monitoring error conditions and reporting them appropriately. Examples of transport providers are TCP/IP, IPX/SPX, NetBEUI, and OSI.
One advantage to using XTI is that this interface is not tied to a single transport provider. You can write programs that will run over any one of the transport providers listed above, then port the programs to run over one of the other transport providers with only modest modifications. See ``Transport-specific issues'' for information about those areas that affect how a network program is written that are outside the scope of XTI.
Note that for two processes on two different computers to
communicate, compatible implementations of the same transport
provider must be running on both systems.
What XTI can do
XTI allows a process to perform a
variety of functions associated with network communication.
All of these functions operate on a ``transport endpoint.''
A transport endpoint is a link between a transport
provider and the process that wants to communicate over the network
using that provider.
The transport endpoint is always identified
by a file descriptor
(an integer index into the system file descriptor table).
Specifically, a process can call XTI functions (listed in parentheses) to
For example, a file server is a process that offers a service to clients, namely, it transfers files from the server to the client machine. A time-of-day server, on the other hand, performs a computation, namely, it determines the current time of day and sends the result back to the client.
The above usage is more precise than the common practice of confusing a server or client process with the computer on which it is running. For example, the term ``file server'' is often used to refer to the computer on which the file server process is running. However, the same computer can run not only the file server process, but several other kinds of servers (such as a time-of-day server) as well.
Similarly, a computer on which one kind of client is running can also run many other clients. In fact, a server process on one computer can, in order to perform its task on behalf of the client that contacted it, enlist the assistance of yet another server on a different computer. It does this using the same mechanisms that the client on the first computer used. The result is that, while the first server is communicating with the second server, it is actually functioning as a client.
In this model, server processes can be running on many different
computers that are attached to many different networks. Whenever a
process needs some service or computation performed that it either
cannot, should not, or need not do, it simply contacts the appropriate
server (wherever it is on the network). Moreover, servers can connect
to each other to request services or computations. The entire network
then becomes an interconnected set of building blocks that can be
called upon in an organized fashion to accomplish complex tasks.
How clients and servers communicate
To contact a particular server, a client first creates a transport
endpoint by calling t_open. In this call, the client
specifies the transport provider it will use. Each transport provider
available is designated by the pathname of a device in the UNIX
filesystem. For example, the TCP transport is identified by
the pathname /dev/inet/tcp.
The client then calls t_alloc to allocate a data structure to contain address information. The client places into the data structure the address information needed by the transport provider to locate the desired server. The client then calls t_connect to contact the server, passing the data structure to the function as an argument. The next section, ``Transport addresses'', has more information about the address information required.
Before clients can successfully connect to a server, however, the server must be waiting for an incoming connection request. The server first calls t_alloc to allocate a data structure to contain address information. It then calls t_listen (with the allocated data structure as one of its arguments), and waits for a client to contact it. When a connection request arrives, t_listen returns with the client address information in the allocated data structure. The server can either reject the request by calling t_snddis (send disconnect) or accept the request by calling t_accept.
When a client is finished communicating over the network, it calls
t_snddis to abort the connection,
t_unbind to disassociate address information from the
transport endpoint,
then t_close to delete
the transport endpoint.
If the client is simply finished with the one server and
wants to connect to another, it calls
t_snddis to abort the connection, followed by
t_connect to send a connection
request to the new server.
Transport addresses
A network consists of multiple computers
(also called ``hosts'') connected to each
other in a way that allows one host to communicate
with any of the other hosts on the network.
Furthermore, a network may be
connected to other networks, either directly or indirectly.
Note that a single host can be directly connected to more than one
network.
The transport provider running on a given host must be able to identify both that host and the network to which the host is attached. Generically, this information is referred to as the ``network address'' and the ``host id.'' Because each kind of transport provider has its own scheme for what network addresses and host ids look like, refer to the chapter in this Guide that discusses the specific transport provider you will be using in your application for more information.
Being able to identify a particular host and the network to which it is attached is not enough, however. With a multi-tasking operating system such as UNIX, many processes (whether clients, servers, or both) can be running on the same host simultaneously. As a result, the transport provider must be able to uniquely identify every process on the host that communicates with the transport provider. Each kind of transport provider defines its own identifier (the ``local process id'') for this purpose. Refer to the chapter that talks specifically about the transport provider you will be using for more information.
Once a process (whether client or server) has opened a transport endpoint with a specific transport provider, it needs to establish the network address and host id of the machine it is running on, as well as the local process id by which it will be known. A process associates this information with the transport endpoint by calling the t_bind function.
The process can either select the network address, host id, and local process id to be used, or it can allow the transport provider to select them. Even if the process makes the selection, the transport provider can override it. In either case, the process can, if it wants, find out the network address, host id, and local process id selected by the transport provider. It does this by providing the appropriate argument to t_bind.
Usually, servers specify their local
process id and clients do not.
This is because clients are written to contact
a particular server using the predetermined local
process id of that server.
It is crucial, therefore, that the server use
the predetermined local process id and no other.
On the other hand, when a client connects to a server, the local
process id of the client is passed to the server when the server
accepts the connection.
The actual value of the local process id
being used by the client is of no importance.
Modes of service
A transport provider typically offers two different modes of service:
``connection-oriented'' and ``connectionless.''
With ``connection-oriented'' service, a client and a server establish a communication path over which they send and receive data. This kind of service is useful when a client and server expect to have an extended dialogue with each other.
One of the characteristics of connection-oriented service is that the communication is sequenced, reliable, and error-free. This means that all data sent by one process is guaranteed to be received by the destination process, that the data arrives exactly in the order it was sent, and that the data arrives without having been altered or corrupted.
A client and a server must cooperate to set up a connection. The server prepares to receive an incoming connection request by calling t_listen. The client then calls t_connect to request a connection with that server. Finally, the server accepts the connection request by calling t_accept.
With ``connectionless'' service, the client and server exchange individual messages. This is often appropriate when the service or computation provided by the server requires very little interaction between the server and the client.
For example, a time-of-day server can compute the current time of day and return the result in a single message. Using the connectionless service, the client and server avoid the extra processing performed by the transport provider at both the server and the client to set up a connection.
When no connection is established, each message sent must contain the address of the destination network, host id, and local process id. Similarly, each message received must contain the address of the network, the host id, and local process id of the process that sent the message.
In a typical scenario, the client and server begin by calling t_open and t_bind. The client then calls t_alloc to allocate space for the message to be sent, fills in the data area with the message, then sends the message to the server by calling t_sndudata (send unit data). Meanwhile, the server awaits the incoming message by calling t_rcvudata (receive unit data). After the client sends its message, it calls t_rcvudata and waits for the server to respond. When the client's message arrives at the server, the server's call to t_rcvudata completes. The server can then examine the message, perform whatever computation it was designed to do, and send its reply by first calling t_alloc, filling the data area with its reply, then sending it off with t_sndudata.
The client and server continue to exchange messages this way until they are finished.
Unlike the connection-oriented service, a connectionless
service does not guarantee delivery of messages,
does not ensure that messages arrive at their
destination in the same order in which they were sent,
and does not ensure that data arrives error-free.
Synchronous and asynchronous operation
Some of the functions in XTI can operate
either synchronously or asynchronously.
In synchronous mode, a function call does not return
until the operation can be completed (or until an error is detected).
For example, if a process calls t_rcv
in synchronous mode and no data is available,
the call blocks until data arrives at the transport endpoint.
On the other hand, if the process
calls t_rcv in asynchronous mode and no data is available,
the call returns immediately with a value of -1 and the global
variable t_errno is set to TNODATA.
It is then up to the process to decide when
and how to try again and call t_rcv later.
The functions that can operate either synchronously or asynchronously are:
If a transport endpoint is operating in synchronous mode and flow control is in effect, a call to t_snd or t_sndudata will block until the transport provider has freed up enough internal storage (buffers) to accept more data. The transport provider frees buffers by transmitting data to the destination transport endpoint.
If a transport endpoint is operating in asynchronous mode and flow
control is in effect, a call to t_snd or
t_sndudata returns -1 and the global variable
t_errno is set to TFLOW.
The process can either call the t_look
function to detect when it can send more data
(t_look returns either
T_GODATA or T_GOEXDATA when
flow control has been lifted)
or it can use an event management facility to be notified
when flow control has been lifted. For information about using an
event management facility, see
``Event management''.
Structure of transmitted data
When a process reads data from a transport endpoint in
connection-oriented mode,
the data can appear to be either a continuous stream of bytes
(stream-oriented input)
or a sequence of messages with message boundaries preserved
(record-oriented input).
In the case of a continuous byte stream, the reading process cannot tell how many individual messages were sent to it, nor where one message stops and the next one begins. If message boundaries are preserved, however, the reading process can detect the end of one message and the start of another. Depending on the transport provider being used, a process reading incoming data on a transport endpoint will see one or the other of these two forms.
When a process reads data from a transport endpoint in connectionless mode, it always receives a datagram (also known as a ``message''). A datagram is a self-contained unit with a start and an end indicator for the data. When a process sends datagrams (by calling t_sndudata), each datagram is sent individually to the remote process. The receiving process then retrieves each datagram (by calling t_rcvudata) one at a time.
This is different from sending data over a connection.
In connection-oriented mode, the sending process
calls t_snd to transmit data.
A process may call t_snd several times
before it turns around and waits to
receive data from the remote process
(it does this by calling t_rcv).
At the other end, the receiving process may get
all of the data that was sent to it with a single call to t_rcv.
Because the receiving process sees a byte stream as it reads data,
it has no idea how many times the sending process called
t_snd, nor what data was sent with each of those calls.
Priority of transmitted data
Some transport providers support the idea of ``expedited data''
(also called ``urgent'' or ``out-of-band'' data).
In general, expedited data is to be sent
immediately to the remote process. This can be important if, for
example, the client process is interactive, and the user hits a
special key (such as ``quit'' or ``interrupt'').
In this case, the
server process needs to get this data
as quickly as possible so it can respond promptly.
Expedited data is useful because transport providers operating in connection-oriented mode typically don't send data in small chunks. To improve efficiency, the transport provider waits until the sending process has written enough data (by collecting the data passed through the transport endpoint over several calls to t_snd) before actually transmitting it over the network. This widely used technique is called ``buffering.''
To make sure that a piece of data is not buffered for some unknown period of time, the sending process can mark it as ``expedited.'' This is done by calling t_snd and passing in the value T_EXPEDITED as the flags argument. In this case, the transport provider transmits the expedited data ahead of all data currently being buffered without delay.
There are a few problems with expedited data, however.
One is that not all transport providers support this.
Another is that the way expedited
data actually works on transport providers that do support it (that
is, the semantics of expedited data) is not exactly the same
for all transports. Consequently, X/Open recommends that programs
seeking to be as independent as possible of the underlying transport
provider should avoid sending expedited data.
Transport provider states
XTI defines various states that a transport provider can be
in. It also defines how calling the different functions in
XTI can change the state of the transport provider.
XTI defines the following states:
Orderly release of a connection means that all data ``in the pipeline'' is transmitted to the destination transport endpoint before the connection is terminated. Note that some transport providers do not support orderly release. If you want to maximize the portability of your programs across different transport providers, you should not make use of orderly release. If you need the functionality of orderly release, you should establish your own protocol between the client and the server to verify that all data has been sent before performing a standard (abortive) release.
All transports support abortive release.
With abortive release, the connection is terminated immediately.
Consequently, data still ``in the pipeline'' may
not be delivered to the destination transport endpoint.
Error conditions
Most of the XTI functions return
a value of -1 if they encounter an error.
In that case the global variable t_errno
is set to one of errors defined for the function.
These errors are all defined in the header file xti.h.
The manual page for each function also lists the
errors that the function can return.
One of the special errors defined is TLOOK. When a function returns this ``error'', an asynchronous event has occurred that the process should investigate and respond to. An asynchronous event occurs when the transport provider has detected something that is unrelated to what the process is doing at the moment. The process can determine what event has occurred by calling the t_look function. This function returns one of the events listed at the end of this section.
For example, assume a process attempts to read data over an established connection with the t_rcv call. Normally, this call returns with data sent by a remote process. However, instead of data, the transport provider may have received a signal from the remote process to disconnect. In this case, the t_rcv call returns with a value of -1. The process should now examine the variable t_errno, which contains the value TLOOK. The process can then call the function t_look, which returns the name of the event that occurred, namely T_DISCONNECT. At this point, the process can perform actions appropriate to this event (such as calling t_rcvdis to acknowledge the disconnect request).
One other error to note is TSYSERR. This means that a UNIX system error has occurred. In this case, the process should examine the global variable errno for the precise error.
The events defined in XTI are:
Check whether the options negotiated are supported on the particular implementation of the transport provider being used by examining the error code returned by t_optmgmt.
For more information on the use of options, read the t_optmgmt manual page.
See also:
XTI does not define its own portable event management facility. Instead, a process must make use of either the poll(S) or select(S) system calls. The process uses these system calls to list the transport endpoints and the events to be monitored. The process can then simply wait until the transport provider notifies it that an event has occurred on one of those transport endpoints. The process checks to see on which endpoint the event has occurred, and executes the code written to handle that kind of event.
See the CAE Specification: X/Open Transport Interface
for a fuller discussion of event management.
The CAE Specification also
includes two code samples, one using poll
and the other using select, to
illustrate the use of these two system calls as the heart of an
event-driven server.
Compiling and linking with XTI
To compile and link a program that uses XTI, do
the following:
#include <xti.h>
From the standpoint of syntax and semantics, the two libraries are nearly identical. The name of the header file used for TLI is tiuser.h. The syntax of the include preprocessor directive to use is
#include <sys/tiuser.h>
The name of the library to
be searched when compiling and linking a program that uses
TLI is nsl (Network Services Library).
The syntax of the cc command to use is
cc option file -lnsl
Applications written to use TLI can be ported relatively easily to XTI. You should note the points listed below when porting your application.
The list below calls out some of the differences between XTI and sockets. This information may be useful to those who are already familiar with socket programming or who have programs they wish to port from sockets to XTI. Note that a sockets library is available for use on SCO systems with the TCP and UDP protocols.
The above list is not exhaustive. Compare the manual pages
for both the sockets library (SSC and SLIB)
and XTI for more detailed information.
Transport-specific issues
Although XTI is a transport-independent programming
interface, some areas that a network program must confront
lie outside the scope of XTI.
Two such areas are transport addresses and options management.
Address formats and available options vary from transport provider to
transport provider.
While XTI provides a generic mechanism
for assigning and retrieving address and options information,
it is up to the
software developer to code the specific details appropriate to each
transport provider.
These details will have to be recoded when the program is
ported to use a different transport provider.
Similarly, each transport provider protocol structures internal information differently. For example, a transport provider stores network addresses, host ids, and local process ids as integers. The byte order used by the transport protocol need not be the same as that used by the host machine. Consequently, the transport provider may support transport-specific library routines to convert integers from the host format to the transport provider's format and back again. The socket library functions htons and ntohs are two examples of this.
The transport provider may also support various utility functions that are specific to that transport protocol. For example, the TCP/IP protocol encodes network addresses and host ids as four bytes, where each byte is an unsigned integer. However, programs typically denote these addresses as dotted quads (four decimal numbers separated by periods). A utility function is available for use with TCP/IP to convert a dotted quad into the integer encoding.
Each transport provider can also define its own mechanism to allow programs to refer to host ids or local process ids with symbolic names, rather than numbers. The defined mechanism converts the symbolic name into the appropriate numeric value. For example, the TCP/IP protocol defines a file called /etc/services. A server process that is to be known on the network by a certain name can look up that name in this file to determine the local process id (in this case, the port number) it should specify when calling t_bind. Again, a utility function exists to facilitate this lookup.
It is the responsibility of the software developer to include the appropriate transport-specific header files and libraries. These files and libraries define transport-specific address formats and utility functions. To obtain this kind of information, read the appropriate chapter in this Guide that describes how to use XTI or TLI over the specific transport provider you will use in your application
Client pseudo-code
This client sends data to and receives data from the selected server.
#include <xti.h> #include other needed header filesextern int t_errno;
main (int argc, char *argv[]) {
/* Declare data structures needed for operations on the transport provider endpoint. */
/* Declare the data structure that contains the well-known address of the server. Both the type and the value of "server_address" must be transport-specific. This example assumes that they have already been defined appropriately (using "typedef" and "#define", for example). TRANSPORT_ADDRESS server_address = WELL_KNOWN_SERVER_ADDRESS;
/* Declare other data structures. */
/* Open the transport provider, using the appropriate device name, and receive a file descriptor in return that denotes that endpoint. */ fd = t_open(device_name, ... )
/* Have the transport provider bind an arbitrary transport address (which is transport-specific) to the endpoint. */ t_bind(fd, NULL, NULL);
/* Allocate the data structure needed in the call to connect to the server. */ connection_info = t_alloc(fd, T_CALL, T_ADDR);
/* Fill in the data structure with the well-known address of the server. */ connection_info->addr.len = sizeof(server_address); connection_info->addr.buf = &server_address;
/* Connect to the server, passing in its well-known address. */ t_connect(fd, connection_info, NULL);
while (true) {
/* Send data to the server. */ t_snd(fd, &data_to_send, sizeof(data_to_send), &flags);
/* Receive data from the server. */ t_rcv(fd, &data_to_send, sizeof(data_to_send), &flags);
/* Time to exit? */ if (time_to_exit == /* some suitable expression that returns zero if it's not time to disconnect and non-zero otherwise */) break; }
/* Disconnect */ t_snddis(fd, NULL);
/* Close the transport endpoint. */ t_close(fd); }
#include <xti.h> #include other needed header filesextern int t_errno;
main (int argc, char *argv[]) {
/* Declare data structures needed for operations on the transport provider endpoint. */
/* Declare the data structure that contains the well-known address of the server. Both the type and the value of "server_address" must be transport-specific. This example assumes that they have already been defined appropriately (using "typedef" and "#define", for example). TRANSPORT_ADDRESS server_address = WELL_KNOWN_SERVER_ADDRESS;
/* Declare other data structures. */
/* Open the transport provider, using the device name assigned to that transport provider, and receive in return a file descriptor denoting that endpoint. */ fd = t_open(device_name, ... );
/* Allocate the bind data structure that will contain the well-known transport-specific address of the server. */ requested_binding = t_alloc(fd, T_BIND, T_ADDR);
/* Allocate the bind data structure that will contain the actual address assigned by the transport provider. */ actual_binding = t_alloc(fd, T_BIND, T_ADDR);
/* Fill in the bind data structure with the well-known transport-specific address of the server, and the length of the queue to hold incoming connection requests. */ requested_binding->addr.len = sizeof(server_address); requested_binding->addr.buf = &server_address; requested_binding->qlen = MAX_QUEUE_LENGTH;
/* Bind the well-known transport-specific address of the server to the transport endpoint. */ t_bind(fd, requested_binding, actual_binding);
/* Compare the requested address in "requested_binding" against the actual assigned address in "actual binding". If they don't match, issue an error and exit. */ if (memcmp(requested_binding->addr.buf, actual_binding->addr.buf, ADDRESS_LENGTH) != 0 ) { perror("Unable to bind correct server address."); exit(1); }
/* Allocate the data structure needed to record the address of the next client requesting a connection. */ call = t_alloc(fd, T_CALL, T_ADDR);
/* This server runs forever. */ while (true) {
/* Listen forever for the next connection request from a client. */ t_listen(fd, call);
/* Open a new file descriptor through which the server will complete the connection from the client. By completing the connection through "resfd", the server leaves "fd" available to receive additional connection requests from other clients. */ resfd = t_open(device_name, ... );
/* Let the transport provider select an arbitrary transport address for the new file descriptor. */ t_bind(resfd, NULL, NULL);
/* Accept the connection request that arrived at "fd" and attach the server-side of the connection to the responding file descriptor "resfd". The file descriptor "fd" continues to be available to the server to listen for new incoming connection requests from clients (see "t_listen" at the top of the loop). The "call" data structure has the information that the child server process (see the "switch" statement below) needs to communicate with the client at the other end of the connection. */ t_accept(fd, resfd, call);
/* Spawn a child server process. The parent process will continue to listen on the original file descriptor "fd" (which is still bound to the server's well-known address) for additional connection requests. The child process will communicate with the client using the "resfd" file descriptor. */ switch (fork()) {
case -1: perror("Fork of server process to respond to client request has failed. Server aborting..."); exit(1);
default: /* This is the code executed by the parent process that continues to listen on the well-known transport address for additional incoming connection requests from clients. The parent process has no use for the file descriptor to be used to communicate with the client (resfd), so it closes it. Doing so does not close this file descriptor for the child process, however, which can still use it. */ t_close(resfd);
case 0: /* This is the code executed by the child process that services the request of the client. The child process has no further use for the original file descriptor (fd) on which the connection request arrived, so it closes it. This does not close the file descriptor for the parent process, however, which will continue to listen on the "fd" file descriptor for new connection requests from clients. t_close(fd);
/* The child process can now perform some useful service for the client. In this example, the server has a very accurate clock, so it returns the correct time of day to the client, then exits. */ gettimeofday(&time_value, &timezone); t_snd(resfd, &time_value, sizeof(struct timeval), flags); t_close(resfd); exit(0); } /* end switch */
} /* end while */
} /* end main */
In the following example, a server first pushes tirdwr onto a stream before running cat(C) so that a client can read from or write to it over the transport connection.
#include <stropts.h>The server invokes the read/write interface by pushing the tirdwr module onto the stream head associated with the transport endpoint created when the connection was established. For a description of I_PUSH, see streamio(M). With tirdwr in place, the server calls close and dup(S) to establish the transport endpoint as its standard input, and uses cat to process the input.. /* . * connection requested and accepted . */
if (ioctl(fd, I_PUSH, "tirdwr") < 0) { perror("I_PUSH of tirdwr failed"); exit(5); } close(0); dup(fd); execl("/bin/cat", "/bin/cat", 0); perror("execl of /bin/cat failed"); exit(6);
Because the transport layer is implemented using STREAMS, the facilities of this character I/O mechanism can be used to provide enhanced user services. Note the following limitations on the use of this interface:
With tirdwr pushed onto a stream, an application can send and receive data over the transport connection for the duration of the connection. Either end of a connection can terminate it by closing the file descriptor associated with the transport endpoint or by popping the tirdwr module off the stream. In either case, tirdwr takes the following actions:
For the specifics of the XTI API implemented in the XTI libraries provided with the SCO OpenServer Development System, read the X/Open Developer's Specification (1990), Revised XTI (X/Open Transport Interface), ISBN 1-872630-05-7, and the X/Open Addendum (August 1991), Addendum to Revised XTI, ISBN 1-872630-21-9.
For the specifics of the TLI API implemented in the TLI libraries provided with the SCO OpenServer Development System, read the AT&T SVID Issue 3.