Chapter 9. Transport Layer Interface

This chapter provides detailed information, with various examples, on X/Open's Transport Layer Interface. This chapter describes the more important and common facilities of TLI, but is not meant to be exhaustive.


Note: Silicon Graphics does not encourage use of the TLI model; its inclusion is for compatibility with interfaces used by other vendors. The sockets interface, with its ease of use and compatibility with industry standardization is the preferred interface.

Topics covered in this chapter include:

Introduction

The Transport Layer Interface is a programming interface to the transport layer of ISO's Open Systems Interconnection Reference Model. It is a subset of the X/Open Transport Interface (XTI), and is implemented within the STREAMS framework. TLI is media- and protocol-independent; it allows applications to run across any transport protocol that supports the interface.

Network Selection and Name-to-Address Mapping facilities have been added to TLI to provide a means of guaranteeing media and protocol independence for transport applications. Network Selection and Name-to-Address Mapping allow network applications to acquire transport-specific information in a transport-independent way.

The following discussion assumes that you have a working knowledge of IRIX, C language programming, and data communication concepts. You should also be familiar with ISO-OSI before reading this chapter.

Network Selection and Name-to-Address Mapping

If TLI applications are to be media- and protocol-independent, they require an understanding of Network Selection and Name-to-Address Mapping facilities. The Network Selection routines in libnsl.so provide a standard interface to the networks available in any environment. Name-to-Address Mapping allows applications to translate transport-specific addresses. The following manual pages give more information on these topics:

getnetconfig() 

describes the libnsl.so routines that manipulate the network configuration administrative file, netconfig. For more information, see getnetconfig(3N).

getnetpath() 

describes the routines that manipulate the NETPATH variable, allowing you to specify which networks in the netconfig file to try. For more information, see getnetpath(3N).

netconfig() 

describes the network configuration database file. For more information, see netconfig(4).

netdir() 

describes the Name-to-Address Mapping library functions. For more information, see netdir(3N).

rpcbind() 

allows client C programs to make procedure calls to the RPC binder service. For more information, see rpcbind(3N).

rpc_svc_calls() 

library routines for registering servers: rpc_reg, svc_reg, svc_unreg, xprt_register, xprt_unregister. For more information, see rpcbind(3N).

rpc_svc_reg() 

library routines for RPC servers: svc_freeargs, svc_getargs, svc_getreqset, svc_getrpccaller, svc_run, svc_sendreply. For more information, see rpcbind(3N).

rpc() 

library routines that allow C language programs to make procedure calls on other machines across a network. For more information, see rpcbind(3N).

rpc_svc_err() 

library routines for server side remote procedure call errors: svcerr_auth, svcerr_decode, svcerr_noproc, svcerr_noprog, svcerr_progvers, svcerr_systemerr, svcerr_weakauth. For more information, see rpcbind(3N).


Note: libnsl, the Network Selection library, should not be confused with libnls, the network license server library.


OSI Reference Model

This section discusses the Reference Model to place the Transport Interface in perspective. The Reference Model partitions networking functions into seven layers, as depicted in Figure 9-1.

Figure 9-1. OSI Reference Model

OSI Reference Model

Layer 1 

The physical layer is responsible for the transmission of raw data over a communication medium.

Layer 2 

The data-link layer provides the exchange of data between network layer entities. It detects and corrects any errors that may occur in the physical layer transmission.

Layer 3 

The network layer manages the operation of the network. In particular, it is responsible for the routing and management of data exchange between transport layer entities within the network.

Layer 4 

The transport layer provides transparent data transfer services between session layer entities, thereby relieving them of concerns about how to achieve reliable and cost-effective transfer of data.

Layer 5 

The session layer provides the services needed by presentation layer entities that enable them to organize and synchronize their dialogue and manage their data exchange.

Layer 6 

The presentation layer manages the representation of information that application layer entities either communicate or reference in their communication.

Layer 7 

The application layer serves as the window between corresponding application processes that are exchanging information.

A basic principle of the Reference Model is that each layer provides services needed by the next higher layer in a way that frees the upper layer from concern about how these services are provided. This approach simplifies the design of each particular layer.

Industry standards have been defined (or are being defined) at each layer of the Reference Model. Two standards are defined at each layer: one that specifies an interface to the services of the layer (to be used when interacting with other layers) and one that defines the protocol by which services are provided (used by each instance of the current layer, as shown in Figure 9-1). A service interface standard at any layer frees users of the service from details of how that layer's protocol is implemented, or even which protocol is used to provide the service.

The transport layer is important because it is the lowest layer in the Reference Model that provides the basic service of reliable, end-to-end data transfer needed by applications and higher-layer protocols. In doing so, this layer hides the topology and characteristics of the underlying network from its users. More important, however, the transport layer defines a set of services common to layers of many contemporary protocol suites, including the International Standards Organization (ISO) protocols, the Transmission Control Protocol and Internet Protocol (TCP/IP) of the Internet, and the Systems Network Architecture (SNA).

A transport service interface, then, enables applications and higher layer protocols to be implemented without knowledge of the underlying protocol stack. That is a principal goal of the Transport Interface. Also, because an inherent characteristic of the transport layer is that it hides details of the physical medium being used, the Transport Interface offers both protocol and medium independence to networking applications and higher layer protocols.

The Transport Interface was modeled after the industry standard ISO Transport Service Definition (ISO 8072). As such, it is intended for those applications and protocols that require transport services. Because the Transport Interface provides reliable data transfer, and because its services are common to several protocol suites, many networking applications will find these services useful.

The Transport Interface is implemented as a user library using the STREAMS input/output mechanism. Therefore, many services available to STREAMS applications are also available to users of the Transport Interface. These services will be highlighted throughout this guide. For detailed information about STREAMS, refer to any generic UNIX SVR4 document set.

Overview of the Transport Interface

This section presents a high-level overview of the services of the Transport Interface, which supports the transfer of data between two user processes. Figure 9-2 illustrates the Transport Interface.

Figure 9-2. Transport Interface

Transport Interface

The transport provider is the entity that provides the services of the Transport Interface, and the transport user is the entity that requires these services. An example of a transport provider is ISO 8073 (the OSI transport protocol), while a transport user can be a networking application or session layer protocol.

The transport user accesses the services of the transport provider by issuing the appropriate service requests. One example is a request to transfer data over a connection. Similarly, the transport provider notifies the user of various events, such as the arrival of data on a connection.

The Network Services Library includes a set of functions that support the services of the Transport Interface for user processes.

These functions enable a user to make requests to the provider and process incoming events. Programs using the Transport Interface can link the appropriate routines from the Network Services Library by using the –lnsl command-line option to cc.

Modes of Service

The Transport Interface provides two modes of service: connection mode and connectionless mode.

Connection mode is circuit-oriented and enables the transmission of data over an established connection in a reliable, sequenced manner. It also provides an identification procedure that avoids the overhead of address resolution and transmission during the data transfer phase. This service is attractive for applications that require relatively long-lived, datastream-oriented interactions. Connection-mode service is analogous to BSD's “stream sockets” (as opposed to datagram sockets), which provide a stream of data instead of isolated data units.

Connectionless mode, by contrast, is message-oriented and supports data transfer in self-contained units with no logical relationship required among multiple units. This service requires only a preexisting association between the peer users involved, which determines the characteristics of the data to be transmitted. This mode corresponds to sending datagrams in the sockets paradigm. All the information required to deliver a unit of data (for example, the destination address) is presented to the transport provider, together with the data to be transmitted, in one service access (which need not relate to any other service access). Each unit of data transmitted is entirely self-contained, like datagrams transmitted through BSD datagram sockets. Connectionless-mode service is attractive for applications that:

  • involve short-term request/response interactions

  • exhibit a high level of redundancy

  • are dynamically reconfigurable

  • do not require guaranteed, in-sequence delivery of data

Connection-Mode Service

The connection-mode transport service is characterized by four phases:

  • local management

  • connection establishment

  • data transfer

  • connection release

Local Management

The local management phase defines local operations between a transport user and a transport provider. For example, a user must establish a channel of communication with the transport provider, as illustrated in Figure 9-3. Each channel between a transport user and transport provider is a unique endpoint of communication, and is called the transport endpoint. The t_open() routine (see t_open(3)) enables a user to choose a particular transport provider that supplies the connection-mode service, and establishes the transport endpoint.

Figure 9-3. Channel between User and Provider

Channel between User and Provider

Another necessary local function for each user is to establish an identity with the transport provider. Each user is identified by a transport address. More accurately, a transport address is associated with each transport endpoint, and one user process can manage several transport endpoints. In connection-mode service, one user requests a connection to another user by specifying that user's address. The structure of a transport address is defined by the address space of the transport provider. An address can be anything from a simple character string (such as “file_server”) to an encoded bit pattern that specifies all information needed to route data through a network. Each transport provider defines its own mechanism for identifying users. Addresses can be assigned to each transport endpoint by t_bind().

In addition to t_open() and t_bind(), several routines are available to support local operations. Table 9-1 summarizes all local management routines of the Transport Interface.

Table 9-1. Local Management Routines for the Transport Interface

Routine

Description

t_alloc()

Allocates Transport Interface data structures

t_bind()

Binds a transport address to a transport endpoint

t_close()

Closes a transport endpoint

t_error()

Prints a Transport Interface error message

t_free()

Frees structures allocated using t_alloc()

t_getinfo()

Returns a set of parameters associated with a particular transport provider

t_getstate()

Returns the state of a transport endpoint

t_look()

Returns the current event on a transport endpoint

t_open()

Establishes a transport endpoint connected to a chosen transport provider

t_optmgmt()

Negotiates protocol-specific options with the transport provider

t_sync()

Synchronizes a transport endpoint with the transport provider

t_unbind()

Unbinds a transport address from a transport endpoint


Connection Establishment

The connection establishment phase enables two users to create a connection, or virtual circuit, between them, as demonstrated in Figure 9-4.

Figure 9-4. Transport Connection

Transport Connection

This phase is illustrated in the following description of a client/server relationship. The client application and the server application are users of their respective transport providers. One user, the server, typically advertises some service to a group of users, and then listens for requests from those users. When a client requires the service, the client attempts to connect itself to the server using the server's advertised transport address. The t_connect() routine (see t_connect(3N)) initiates the connect request. One argument to t_connect(), the transport address, identifies the server the client wishes to access. The server is notified of each incoming request using t_listen() and can call t_accept() (see t_listen(3N) and t_accept(3N)) to accept the client's request for access to the service. If the request is accepted, the transport connection is established.

Table 9-2 summarizes all routines available for establishing a transport connection.

Table 9-2. Routines for Establishing a Transport Connection

Routine

Description

t_accept()

Accepts a request for a transport connection

t_connect()

Establishes a connection with the transport user at a specified destination

t_listen()

Retrieves an indication of a connect request from another transport user

t_rcvconnect ()

Completes connection establishment if t_connect() was called in asynchronous mode (see “Advanced Topics”

)


Data Transfer

The data transfer phase enables users to transfer data in both directions over an established connection. Two routines, t_snd() and t_rcv(), send and receive data over this connection. All data sent by a user is guaranteed to be delivered to the user on the other end of the connection in the order in which it was sent. Table 9-3 summarizes the connection-mode data transfer routines.

Table 9-3. Connection-Mode Data Transfer Routines

Routine

Description

t_rcv()

Retrieves data that has arrived over a transport connection

t_snd()

Sends data over an established transport connection


Connection Release

The connection release phase allows you to break an established connection. When you decide that a conversation should end, you can request that the provider release the transport connection. Two types of connection release are supported by the Transport Interface. The first is an abortive release, which directs the transport provider to release the connection immediately. Any previously sent data that has not yet reached the other transport user can be discarded by the transport provider. The t_snddis() routine initiates this abortive disconnect, and t_rcvdis() processes the incoming indication for an abortive disconnect.

All transport providers must support the abortive release procedure. In addition, some transport providers can also support an orderly release facility that enables users to terminate communication gracefully with no data loss. The functions t_sndrel() and t_rcvrel() support this capability. Table 9-4 summarizes the connection release routines.

Table 9-4. Connection Release Routines

Routine

Description

t_rcvdis()

Returns an indication of an aborted connection, including a reason code and user data

t_rcvrel()

Returns an indication that the other transport user has requested an orderly release of a connection

t_snddis()

Aborts a connection or rejects a connect request

t_sndrel()

Requests the orderly release of a connection


Connectionless-Mode Service

The connectionless-mode transport service is characterized by two phases: local management and data transfer. The local management phase defines the same local operations described above for the connection-mode service.

The data transfer phase enables a user to transfer data units (sometimes called datagrams) to the specified peer user. Each data unit must be accompanied by the transport address of the receiver. Two routines, t_sndudata() and t_rcvudata(), support this message-based data transfer facility, while another routine, t_rcvuderr(), allows retrieval of error messages. (For more information, see t_sndudata(3N), t_rcvudata(3N), and t_rcvuderr(3N).) Table 9-5 summarizes all routines associated with connectionless-mode data transfer.

Table 9-5. Routines for Connectionless-Mode Data Transfer

Routine

Description

t_rcvudata()

Retrieves a message sent by another transport user

t_rcvuderr()

Retrieves error information associated with a previously sent message

t_sndudata()

Sends a message to the specified destination user


State Transitions

The Transport Interface has two components:

  • the library routines that provide the transport services to users

  • the state transition rules that define the sequence in which the transport routines can be invoked

The state transition rules can be found in the state tables under “State Transitions”. The state tables define the legal sequence of library calls based on state information and the handling of events. These events include user-generated library calls, as well as provider-generated event indications.


Note: Any user of the Transport Interface must completely understand all possible state transitions before writing software using the interface.


Introduction to Connection-Mode Service

This section describes the connection-mode service of the Transport Interface. As discussed in the previous section, the connection-mode service can be illustrated using a client/server paradigm. The important concepts of connection-mode service are presented using two programming examples. The examples are related: Example 9-1 illustrates how a client establishes a connection to a server and then communicates with it, while Example 9-2 shows the server's side of the interaction. All code-fragment examples discussed in this chapter are presented as complete programs in “Some Examples”.

In the examples, the client establishes a connection with a server process. The server then transfers a file to the client. The client, in turn, receives the data from the server and writes it to its standard output file.

Local Management

Before the client and server can establish a transport connection, each must first establish a local channel (the transport endpoint) to the transport provider using t_open() and establish its identity (or address) using t_bind().

The set of services supported by the Transport Interface may not be implemented by all transport protocols. Each transport provider has a set of characteristics associated with it that determines the services it offers and the limits associated with those services. This information is returned to the user by t_open() and consists of the following:

addr 

maximum size of a transport address

options 

maximum bytes of protocol-specific options that can be passed between the transport user and transport provider

tsdu 

maximum message size that can be transmitted

etsdu 

maximum expedited data message size that can be sent over a transport connection

connect 

maximum number of bytes of user data that can be passed between users during connection establishment

discon 

maximum number of bytes of user data that can be passed between users during the abortive release of a connection

servtype 

type of service supported by the transport provider

The three service types (servtype) defined by the Transport Interface are as follows:

T_COTS 

The transport provider supports connection-mode service but does not provide the optional orderly release facility.

T_COTS_ORD 

The transport provider supports connection-mode service with the optional orderly release facility.

T_CLTS 

The transport provider supports connectionless-mode service. Only one such service can be associated with the transport provider identified by t_open().


Note: t_open() returns the default provider characteristics associated with a transport endpoint. However, some characteristics can change after an endpoint has been opened. This occurs if the characteristics are associated with negotiated options (option negotiation is described later in this section). For example, if the support of expedited data transfer is a negotiated option, the value of this characteristic can change. t_getinfo() can be called to retrieve the current characteristics of a transport endpoint.


Once a user establishes a transport endpoint with the chosen transport provider, it must establish its identity. As mentioned earlier, t_bind() does this by binding a transport address to the transport endpoint. In addition, for servers, this routine informs the transport provider that the endpoint will be used to listen for incoming connect indications, also called connect requests.

An optional facility, t_optmgmt() (see t_optmgmt(3N)), is also available during the local management phase. It enables a user to negotiate the values of protocol options with the transport provider. Each transport protocol is expected to define its own set of negotiable protocol options, which can include such information as Quality-of-Service parameters. Because of the protocol-specific nature of options, only applications written for a particular protocol environment are expected to use this facility.

The Client

The local management requirements of the example client and server are used to discuss details of these facilities following each example. The following are the definitions needed by the client program, followed by its necessary local management steps:

Example 9-1. The Connection-Mode Client Definitions and Local Management

#include <stdio.h>
#include <tiuser.h>
#include <fcntl.h>

#define SRV_ADDR  1     /* server's well-known address */

void main() 
{
    int fd;
    int nbytes;
    int flags = 0;
    char buf[1024];
    struct t_call *sndcall;
    extern int t_errno;
if ((fd = t_open("/dev/ticotsord", O_RDWR, NULL)) < 0) {
        t_error("t_open failed");
        exit(1);
    } 

    if (t_bind(fd, NULL, NULL) < 0) {
        t_error("t_bind failed");
        exit(2);
    }

The first argument to t_open() is the pathname of a filesystem node that identifies the transport protocol that supplies the transport service. In this example, /dev/ticotsord is a STREAMS clone device node that identifies a generic, connection-based transport protocol (see clone(7)). The clone device finds an available minor device of the transport provider for the user. It is opened for both reading and writing, as specified by the O_RDWR flag passed as the second argument. The third argument can be used to return the service characteristics of the transport provider to the user. This information is useful when writing protocol-independent software (discussed in “Guidelines for Protocol Independence”). For simplicity, the client and server in this example ignore this information and assume the transport provider has the following characteristics:

  • The transport address is an integer value that uniquely identifies each user.

  • The transport provider supports the T_COTS_ORD service type, and the example uses the orderly release facility to release the connection.

  • User data cannot be passed between users during either connection establishment or abortive release.

  • The transport provider does not support protocol-specific options.

Because these characteristics are not needed by the user, NULL is specified in the third argument to t_open(). If the user were to require a service other than T_COTS_ORD, another transport provider would be opened. An example of the T_CLTS service invocation is presented in “Introduction to Connectionless-Mode Service”.

The return value of t_open() is an identifier for the transport endpoint that is used by all subsequent Transport Interface function calls. This identifier is actually a file descriptor obtained by opening the transport protocol file (see open(2)). The significance of this fact is highlighted in “A Read/Write Interface”.

After the transport endpoint is created, the client calls t_bind() to assign an address to the endpoint. The first argument identifies the transport endpoint. The second argument describes the address the user would like to bind to the endpoint, and the third argument is set on return from t_bind() to specify the address that the provider bound.

The address associated with a server's transport endpoint is important, because that is the address used by all clients to access the server. However, the typical client does not care what its own address is, because no other process tries to access it. That is the case in this example, where the second and third arguments to t_bind() are set to NULL. A NULL second argument directs the transport provider to choose an address for the user. A NULL third argument specifies that the user does not care what address was assigned to the endpoint.

If either t_open() or t_bind() fails, the program calls t_error() (see t_error(3N)) to print an appropriate error message to stderr. If any Transport Interface routine fails, the global integer t_errno is assigned a transport error value. A set of error values is defined (in <tiuser.h>) for the Transport Interface, and t_error() prints an error message corresponding to the value in t_errno. This routine is analogous to perror(), which prints an error message based on the value of errno (see perror(3)). If the error associated with a transport function is a system error, t_errno is set to TSYSERR, and errno is set to the appropriate value.

The Server

The server in Example 9-2 must take similar local management steps before communication can begin. The server must establish a transport endpoint through which it listens for connect indications. The necessary definitions and local management steps are shown in Example 9-2.

Example 9-2. The Connection-Mode Server Definitions and Local Management

#include <tiuser.h>
#include <stropts.h>
#include <fcntl.h>
#include <stdio.h>
#include <signal.h>

#define DISCONNECT -1
#define SRV_ADDR  1   /* server's well-known address */

int conn_fd;             /* connection established here */
extern int t_errno;

void main()
{
    int listen_fd;        /* listening transport endpoint */
    struct t_bind *bind;
    struct t_call *call;

    if ((listen_fd = t_open("/dev/ticotsord", O_RDWR, NULL))
        < 0) {
        t_error("t_open failed for listen_fd");
        exit(1);
    }

    /*
     * By assuming that the address is an integer value,
     * this program may not run over another protocol.
     */

    if ((bind = (struct t_bind *)t_alloc(listen_fd,
        T_BIND, T_ALL))== NULL) {
        t_error("t_alloc of t_bind structure failed");
        exit(2);
    } 

    bind->qlen = 1;
    bind->addr.len = sizeof(int);

    *(int *)bind->addr.buf = SRV_ADDR;

    if (t_bind(listen_fd, bind, bind) < 0)  {
        t_error("t_bind failed for listen_fd");
        exit(3);
    } 

    /* Was the correct address bound? */
    if (*(int *)bind->addr.buf != SRV_ADDR)  {
        fprintf(stderr, "t_bind bound wrong address\n");
        exit(4);
    }

As with the client, the first step is to call t_open() to establish a transport endpoint with the desired transport provider. This endpoint, listen_fd, is used to listen for connect indications. Next, the server must bind its well-known address to the endpoint. This address is used by each client to access the server. The second argument to t_bind() requests that a particular address be bound to the transport endpoint. This argument points to a t_bind structure with the following format:

struct t_bind {
    struct netbuf addr;
    unsigned qlen;
}

addr describes the address to be bound, and qlen specifies the maximum outstanding connect indications that can arrive at this endpoint. All Transport Interface structure and constant definitions are found in <tiuser.h>.

The address is specified using a netbuf structure that contains the following members:

struct netbuf {
    unsigned int maxlen;
    unsigned int len;
    char *buf;
}

buf points to a buffer containing the data, len specifies the number of bytes of data in the buffer, and maxlen specifies the maximum number of bytes the buffer can hold (and need only be set when data is returned to the user by a Transport Interface routine). For the t_bind structure, the data pointed to by buf identifies a transport address. The structure of addresses is likely to vary between protocol implementations under the Transport Interface; the netbuf structure is intended to support any address structure.

If the value of qlen is greater than 0, the transport endpoint can be used to listen for connect indications. In such cases, t_bind() directs the transport provider to begin queueing connect indications destined for the bound address immediately. Furthermore, the value of qlen specifies the maximum outstanding connect indications the server wishes to process. The server must respond to each connect indication, either accepting or rejecting the request for connection. An outstanding connect indication is one to which the server has not yet responded. Often, a server fully processes a single connect indication and responds to it before receiving the next indication. When this occurs, a value of 1 is appropriate for qlen. However, some servers may wish to retrieve several connect indications before responding to any of them. In such cases, qlen specifies the maximum number of outstanding indications the server processes. An example of a server that manages multiple outstanding connect indications is presented in “Advanced Topics”.

t_alloc() is called to allocate the t_bind structure needed by t_bind(). t_alloc() takes three arguments. The first is a file descriptor that references a transport endpoint. This is used to access the characteristics of the transport provider (see t_open(3N)). The second argument identifies the appropriate Transport Interface structure to be allocated. The third argument specifies which, if any, netbuf buffers should be allocated for that structure. T_ALL specifies that all netbuf buffers associated with the structure should be allocated, and causes the addr buffer to be allocated in this example. The size of this buffer is determined from the transport provider characteristic that defines the maximum address size. The maxlen field of this netbuf structure is set to the size of the buffer allocated by t_alloc(). The use of t_alloc() helps ensure the compatibility of user programs with future releases of the Transport Interface.

The server in this example processes connect indications one at a time, so qlen is set to 1. The address information is then assigned to the newly allocated t_bind structure. This t_bind structure is passed to t_bind() as both the second and third arguments; as the second argument, it contains information for t_bind(), while as the third argument, it returns information to the user.

On return, the t_bind structure contains whatever address was bound to the transport endpoint. If the provider can't bind the requested address (perhaps because it's already bound to another transport endpoint), it returns another appropriate address.


Note: Each transport provider manages its address space differently. Some transport providers can allow a single transport address to be bound to several transport endpoints, while others can require a unique address per endpoint. The Transport Interface supports either choice. Based on its address management rules, a provider determines if it can bind the requested address. If not, it chooses another valid address from its address space and binds it to the given transport endpoint.

The server must check the bound address to ensure that it is the one previously advertised to clients. Otherwise, the clients are unable to reach the server.

If t_bind() succeeds, the provider begins queueing connect indications, entering the next phase of communication, connection establishment.

Connection Establishment

The connection establishment procedures highlight the distinction between clients and servers. The Transport Interface imposes a different set of procedures in this phase for each type of transport user. The client starts the connection establishment procedure by requesting a connection to a particular server using t_connect(). The server is then notified of the client's request by calling t_listen(). The server can either accept or reject the client's request. It calls t_accept() to establish the connection, or calls t_snddis() to reject the request. The client is notified of the server's decision when t_connect() completes. For more information, see t_connect(3N), t_listen(3N), t_accept(3N), and t_snddis(3N).

The Transport Interface supports two facilities during connection establishment that are not necessarily supported by all transport providers:

  • The ability to transfer data between the client and server when establishing the connection.

    The client can send data to the server when it requests a connection. This data is passed to the server by t_listen(). Similarly, the server can send data to the client when it accepts or rejects the connection. The connect characteristic returned by t_open() determines how much data, if any, two users can transfer during connect establishment.

  • The negotiation of protocol options.

    The client can specify protocol options that it would like the transport provider and/or the other user to support. The Transport Interface supports both local and remote option negotiation. As discussed earlier, option negotiation is inherently a protocol-specific function. If you want protocol-independent software, you should not use this facility (see “Guidelines for Protocol Independence”).

The Client

Continuing with the client/server example, the steps needed by the client to establish a connection are as follows:

/* Since it assumes that the address is an integer value,
 * this program may not run over another protocol.
 */ 
if ((sndcall = (struct t_call *)t_alloc(fd, T_CALL, T_ADDR)) == NULL) {
    t_error("t_alloc failed");
    exit(3);
}
sndcall->addr.len = sizeof(int);
*(int *)sndcall->addr.buf = SRV_ADDR;

if (t_connect(fd, sndcall, NULL) < 0) {
    t_error("t_connect failed for fd");
    exit(4);
}

The t_connect() call establishes the connection with the server. The first argument to t_connect() identifies the transport endpoint through which the connection is established, and the second argument identifies the destination server. This argument is a pointer to a t_call structure with the following format:

struct t_call  {
    struct netbuf addr;
    struct netbuf opt;
    struct netbuf udata;
    int sequence;
}

addr identifies the address of the server, opt can be used to specify protocol-specific options that the client would like to associate with the connection, and udata identifies user data that can be sent with the connect request to the server. The sequence field has no meaning for t_connect().

t_alloc() is called to allocate the t_call structure dynamically. Once allocated, the appropriate values are assigned. In this example, no options or user data items are associated with the t_connect() call, but the server's address must be set. The third argument to t_alloc() is set to T_ADDR to specify that an appropriate netbuf buffer should be allocated for the address. The server's address is then assigned to buf, and len is set accordingly.

The third argument to t_connect() can be used to return information to the user about the newly established connection, and can be used to retrieve any user data sent by the server in its response to the connect request. It is set to NULL by the client here to indicate that this information is not needed. The connection is established on the successful return of t_connect(). If the server rejects the connect request, t_connect() fails and sets t_errno to TLOOK.

Event Handling

The TLOOK error has special significance in the Transport Interface. TLOOK notifies the user if a Transport Interface routine is interrupted by an unexpected asynchronous transport event on the given transport endpoint. As such, TLOOK does not report an error with a Transport Interface routine, but the normal processing of that routine is not performed because of the pending event. The events defined by the Transport Interface are listed as follows:

T_LISTEN 

A request for a connection, called a connect indication, has arrived at the transport endpoint.

T_CONNECT 

The confirmation of a previously sent connect request, called a connect confirmation, has arrived at the transport endpoint. The confirmation is generated when a server accepts a connect request.

T_DATA 

User data has arrived at the transport endpoint.

T_EXDATA 

Expedited user data has arrived at the transport endpoint. Expedited data is discussed later in this section.

T_DISCONNECT 

A notification that the connection was aborted or that the server rejected a connect request, called a disconnect indication, has arrived at the transport endpoint.

T_ERROR 

A notification that a fatal error has occurred.

T_UDERR 

A notification of an error in a previously sent datagram, called a unitdata error indication, has arrived at the transport endpoint (see “Introduction to Connectionless-Mode Service”).

T_ORDREL 

A request for the orderly release of a connection, called an orderly release indication, has arrived at the transport endpoint.

It is possible in some states to receive one of several asynchronous events, as described in the state tables of “State Transitions”. The t_look() routine enables a user to determine what event has occurred if a TLOOK error is returned. The user can then process that event accordingly. In the example, if a connect request is rejected, the event passed to the client is a disconnect indication. The client exits if its request is rejected.

The Server

Returning to the example, when the client calls t_connect(), a connect indication is generated on the server's listening transport endpoint. The steps required by the server to process the event are discussed below. For each client, the server accepts the connect request and spawns a server process to manage the connection as follows:

if ((call = (struct t_call *)t_alloc(listen_fd, T_CALL,
                                     T_ALL)) == NULL) {
    t_error("t_alloc of t_call structure failed");
    exit(5);
} 
while (1) {
    if (t_listen(listen_fd, call) < 0) {
        t_error("t_listen failed for listen_fd");
        exit(6);
    } 
    if ((conn_fd = accept_call(listen_fd, call))
        != DISCONNECT)
        run_server(listen_fd);
}

The server loops forever, processing each connect indication. First, the server calls t_listen() to retrieve the next connect indication. When one arrives, the server calls accept_call() to accept the connect request. accept_call() accepts the connection on an alternate transport endpoint (as discussed below) and returns the value of that endpoint. conn_fd is a global variable that identifies the transport endpoint where the connection is established. Because the connection is accepted on an alternate endpoint, the server can continue listening for connect indications on the endpoint that was bound for listening. If the call is accepted without error, run_server() spawns a process to manage the connection.

The server allocates a t_call structure to be used by t_listen(). The third argument to t_alloc(), T_ALL, specifies that all necessary buffers should be allocated for retrieving the caller's address, options, and user data. As mentioned earlier, the transport provider in this example does not support the transfer of user data during connection establishment, and also does not support any protocol options. Therefore, t_alloc() does not allocate buffers for the user data and options. It must, however, allocate a buffer large enough to store the address of the caller. t_alloc() determines the buffer size from the addr characteristic returned by t_open(). The maxlen field of each netbuf structure is set to the size of the buffer allocated by t_alloc() (maxlen is 0 for the user data and options buffers).

Using the t_call structure, the server calls t_listen() to retrieve the next connect indication. If one is currently available, it is returned to the server immediately. Otherwise, t_listen() blocks until a connect indication arrives.

The Transport Interface supports an asynchronous mode for these routines, which prevents a process from blocking. This feature is discussed in “Advanced Topics”.

When a connect indication arrives, the server calls accept_call() to accept the client's request, as follows:

int accept_call(listen_fd, call)
int listen_fd;
struct t_call *call;
{
    int resfd;

    if ((resfd = t_open("/dev/ticotsord", O_RDWR, NULL))
        < 0) {
        t_error("t_open for responding fd failed");
        exit(7);
    }

    if (t_bind(resfd, NULL, NULL) < 0) {
        t_error("t_bind for responding fd failed");
        exit(8);
    } 
    if (t_accept(listen_fd, resfd, call) < 0) {
        if (t_errno == TLOOK) {  /* must be a disconnect */
            if (t_rcvdis(listen_fd, NULL) < 0) {
                t_error("t_rcvdis failed for listen_fd");
                exit(9);
            }
            if (t_close(resfd) < 0) {
                t_error("t_close failed for responding fd");
                exit(10);
            } 			/* go back up and listen for other calls */
            return(DISCONNECT);
        }
        t_error("t_accept failed");
        exit(11);
    }
    return(resfd);
}

accept_call() takes two arguments:

  • listen_fd identifies the transport endpoint where the connect indication arrived

  • call is a pointer to a t_call structure that contains all information associated with the connect indication.

The server first establishes another transport endpoint by opening the clone device node of the transport provider and binding an address. As with the client, a NULL value is passed to t_bind() to specify that the user does not care what address is bound by the provider. The newly established transport endpoint, resfd, is used to accept the client's connect request.

The first two arguments of t_accept() specify the listening transport endpoint and the endpoint where the connection is accepted, respectively. A connection can be accepted on the listening endpoint, but this prevents other clients from accessing the server for the duration of the connection.

The third argument of t_accept() points to the t_call structure associated with the connect indication. This structure should contain the address of the calling user and the sequence number returned by t_listen(). The sequence number is significant if the server manages multiple outstanding connect indications. “Advanced Topics” presents an example of this situation. Also, the t_call structure should identify protocol options the user has requested and user data that can be passed to the client. Because the transport provider in this example does not support protocol options or the transfer of user data during connection establishment, the t_call structure returned by t_listen() can be passed without change to t_accept().

For simplicity in the example, the server exits if either the t_open() or t_bind() call fails. exit() closes the transport endpoint associated with listen_fd, causing the transport provider to pass a disconnect indication to the client that requested the connection. This disconnect indication notifies the client that the connection was not established; t_connect() fails, setting t_errno to TLOOK.

t_accept() can fail if an asynchronous event has occurred on the listening transport endpoint before the connection is accepted, and t_errno is set to TLOOK. The state transition table in “State Transitions” shows that the only event that can occur in this state with only one outstanding connect indication is a disconnect indication. This event can occur if the client decides to undo the connect request it had previously sent. If a disconnect indication arrives, the server must retrieve the disconnect indication using t_rcvdis(). This routine takes a pointer to a t_discon structure as an argument, which is used to retrieve information associated with a disconnect indication. In this example, however, the server does not care to retrieve this information, so it sets the argument to NULL. After receiving the disconnect indication, accept_call() closes the responding transport endpoint and returns DISCONNECT, which informs the server that the connection was disconnected by the client. The server then listens for further connect indications.

Figure 9-5 illustrates how the server establishes connections.

Figure 9-5. Listening and Responding Transport Endpoints

Listening and Responding Transport Endpoints

The transport connection is established on the newly created responding endpoint, and the listening endpoint is freed to retrieve further connect indications.

Data Transfer

Once the connection is established, both the client and server can begin transferring data over the connection using t_snd() and t_rcv(). The Transport Interface does not differentiate the client from the server from this point on. Either user can send and receive data or release the connection. The Transport Interface guarantees reliable, sequenced delivery of data over an existing connection.

Two classes of data can be transferred over a transport connection: normal data and expedited data.

Expedited data is typically associated with urgent information. The exact semantics of expedited data are subject to the interpretations of the transport provider. Furthermore, not all transport protocols support the notion of an expedited data class (see t_open(3N)).

All transport protocols support the transfer of data in byte stream mode, where byte stream implies no concept of message boundaries on data that's transferred over a connection. However, some transport protocols support the preservation of message boundaries over a transport connection. This service is supported by the Transport Interface, but protocol-independent software must not rely on its existence.

The message interface for data transfer is supported by a special flag of t_snd() and t_rcv() called T_MORE. The messages, called Transport Service Data Units ( TSDU), can be transferred between two transport users as distinct units. The maximum TSDU size is a characteristic of the underlying transport protocol. This information is available to the user from t_open() and t_getinfo(). Because the maximum size can be large (possibly unlimited), the Transport Interface allows a user to transmit a message in multiple units.

To send a message in multiple units over a transport connection, the user must set the T_MORE flag on every t_snd() call except the last. This flag specifies that the user will send more data associated with the message in a subsequent call to t_snd(). The last message unit should be transmitted with T_MORE turned off to specify that this is the end of the TSDU.

Similarly, a TSDU can be passed in multiple units to the receiving user. Again, if t_rcv() returns with the T_MORE flag set, the user should continue calling t_rcv() to retrieve the remainder of the message. The last unit in the message is identified by a call to t_rcv() that does not set T_MORE.


Note: The T_MORE flag implies nothing about how the data can be packaged below the Transport Interface or how the data can be delivered to the receiver. Each transport protocol, and each implementation of that protocol, can package and deliver the data differently.

For example, if a user sends a complete message in a single call to t_snd(), there is no guarantee that the transport provider will deliver the data in a single unit to the remote transport user. Similarly, a message transmitted in two message units can be delivered in a single unit to the remote transport user. The message boundaries can only be preserved by noting the value of the T_MORE flag on t_snd() and t_rcv(). This guarantees that the receiving user sees a message with the same contents and message boundaries as that sent by the sender.

The Client

Continuing with the client/server example, the server transfers a log file to the client over the transport connection. The client receives this data and writes it to its standard output file. A byte stream interface is used by the client and server, where message boundaries (that is, the T_MORE flag) are ignored. The client receives data using the following instructions:

while ((nbytes = t_rcv(fd, buf, 1024, &flags)) != -1) {
    if (fwrite(buf, 1, nbytes, stdout) < 0) {
        fprintf(stderr, "fwrite failed\n");
        exit(5);
    }
}

The client continuously calls t_rcv() to process incoming data. If no data is currently available, t_rcv() blocks until data arrives. t_rcv() retrieves the available data up to 1024 bytes, which is the size of the client's input buffer, and returns the number of bytes received. The client then writes this data to standard output and continues. The data transfer phase completes when t_rcv() fails. t_rcv() fails if an orderly release or disconnect indication arrives, as discussed later in this section. If the fwrite() call (see fwrite(3S)) fails for any reason, the client exits, closing the transport endpoint. If the transport endpoint is closed (either by exit() or t_close()) during the data transfer phase, the connection is aborted and the other user receives a disconnect indication.

The Server

Looking now at the other side of the connection, the server manages its data transfer by spawning a child process to send the data to the client. The parent process then loops back to listen for further connect indications.

 run_server() is called by the server to spawn this child process as shown in Example 9-3.

Example 9-3. Sending Data to a Client

void connrelease()
{
    /* conn_fd is global because needed here */
    if (t_look(conn_fd) == T_DISCONNECT) {
        fprintf(stderr, "connection aborted\n");
        exit(12);
    }     /* else orderly release indication - normal exit */
    exit(0);
 }

int run_server(listen_fd)
int listen_fd;
{
    int nbytes;
    FILE *logfp;           /* file pointer to log file */
    char buf[1024];

    switch (fork()) {

    case -1:
        perror("fork failed");
        exit(20);

    default:   /* parent */ 

        /* close conn_fd and then go up and listen again */ 
        if (t_close(conn_fd) < 0) {
            t_error("t_close failed for conn_fd");
            exit(21);
        } 
        return;

    case 0:     /* child */ 

        /* close listen_fd and do service */ 
        if (t_close(listen_fd) < 0) {
            t_error("t_close failed for listen_fd");
            exit(22);
        } 
        if ((logfp = fopen("logfile", "r")) == NULL) {
            perror("cannot open logfile");
            exit(23);
        }
        signal(SIGPOLL, connrelease);
        if (ioctl(conn_fd, I_SETSIG, S_INPUT) < 0) {
            perror("ioctl I_SETSIG failed");
            exit(24);
        }
        /* was disconnect there? */
        if (t_look(conn_fd) != 0) {
            fprintf(stderr, "t_look: unexpected event\n");
            exit(25);
        } 
        while ((nbytes = fread(buf, 1, 1024, logfp)) > 0) 
            if (t_snd(conn_fd, buf, nbytes, 0) < 0) {
                t_error("t_snd failed");
                exit(26);
            }

After the fork(), the parent process returns to the main processing loop and listens for further connect indications. Meanwhile, the child process manages the newly established transport connection. If the fork() call fails, exit() closes the transport endpoint associated with listen_fd, sending a disconnect indication to the client, and the client's t_connect() call fails.

The server process reads 1024 bytes of the log file at a time and sends that data to the client using t_snd(). buf points to the start of the data buffer, and nbytes specifies the number of bytes to be transmitted. The fourth argument can contain one of the two optional flags as follows:

  • T_EXPEDITED specifies that the data is expedited.

  • T_MORE defines message boundaries when transmitting messages over a connection.

Neither flag is set by the server in this example.

If the user floods the transport provider with data, the provider can exert back pressure to provide flow control. In such cases, t_snd() blocks until the flow control is relieved, and then resumes its operation. t_snd() does not complete until nbyte bytes have been passed to the transport provider.

The t_snd() routine does not look for a disconnect indication (showing that the connection was broken) before passing data to the provider. Also, because the data traffic flows in one direction, the user never looks for incoming events. If the connection is aborted, the user should be notified since data can be lost. The user can invoke t_look(), which checks for incoming events before each t_snd() call. A more efficient solution is presented in Example 9-3. The STREAMS I_SETSIG ioctl() enables a user to request a signal when a given event occurs (see streamio(5) and signal(2)). S_INPUT causes a signal to be sent to the user if any input arrives on the Stream referenced by conn_fd. If a disconnect indication arrives, the signal catching routine (connrelease()) prints an error message and then exits.

If the data traffic flowed in both directions in this example, the user would not have to monitor the connection for disconnects. If the client alternated t_snd() and t_rcv() calls, it could rely on t_rcv() to recognize an incoming disconnect indication.

Connection Release

At any point during data transfer, either user can release the transport connection and end the conversation. As mentioned earlier, two forms of connection release are supported by the Transport Interface:

  • Abortive release breaks a connection immediately and can result in the loss of any data that has not yet reached the destination user.

    Either user can call t_snddis() to generate an abortive release. Also, the transport provider can abort a connection if a problem occurs below the Transport Interface. t_snddis() enables a user to send data to the receiver when aborting a connection. Although the abortive release is supported by all transport providers, the ability to send data when aborting a connection is not.

    When a user receives notification of the aborted connection, t_rcvdis() must be called to retrieve the disconnect indication. This call returns a reason code that identifies why the connection was aborted, and returns any user data that accompanied the disconnect indication (if the abortive release was initiated by the other user). This reason code is specific to the underlying transport protocol and should not be interpreted by protocol-independent software.

  • Orderly release gracefully terminates a connection and guarantees that no data is lost.

All transport providers must support the abortive release procedure, but orderly release is an optional facility that is not supported by all transport protocols.

The Server

The client/server example in this section assumes that the transport provider supports the orderly release of a connection. When all the data has been transferred by the server, the connection can be released as follows:

        if (t_sndrel(conn_fd) < 0) {
            t_error("t_sndrel failed");
            exit(27);
        } 
        pause();
    /* until orderly release indication arrives */
    }

The orderly release procedure consists of two steps by each user. The first user to complete data transfer can initiate a release using t_sndrel(), as illustrated in the example. This routine informs the client that no more data will be sent by the server. When the client receives this indication, it can continue sending data back to the server if desired. When all data has been transferred, however, the client must also call t_sndrel() to indicate that it is ready to release the connection. The connection is released only after both users have requested an orderly release and received the corresponding indication from the other user.

In this example, data is transferred in one direction from the server to the client, so the server does not expect to receive data from the client after it has initiated the release procedure. Thus, the server simply calls pause() (see pause(2))after initiating the release. Eventually, the client responds with its orderly release request, which generates a signal that is caught by connrelease(). Remember that the server earlier issued an I_SETSIG ioctl() call to generate a signal on any incoming event. Since the only possible Transport Interface events that can occur in this situation are a disconnect indication or an orderly release indication, connrelease() terminates normally when the orderly release indication arrives. The exit() call in connrelease() closes the transport endpoint, freeing the bound address for another user. If a user process wants to close a transport endpoint without exiting, it can call t_close().

The Client

The client's view of connection release is similar to that of the server. As mentioned earlier, the client continues to process incoming data until t_rcv() fails. If the server releases the connection (using either t_snddis() or t_sndrel()), t_rcv() fails and sets t_errno to TLOOK. The client then processes the connection release as follows:

    if ((t_errno == TLOOK)  &&  (t_look(fd) == T_ORDREL)) {
        if (t_rcvrel(fd) < 0) {
            t_error("t_rcvrel failed");
            exit(6);
        }
        if (t_sndrel(fd) < 0) {
            t_error("t_sndrel failed");
            exit(7);
        }
        exit(0);
    }
    t_error("t_rcv failed");
    exit(8);

When an event occurs on the client's transport endpoint, the client checks whether the expected orderly release indication has arrived. If so, it proceeds with the release procedures by calling t_rcvrel() to process the indication and t_sndrel() to inform the server that it is also ready to release the connection. At this point the client exits, closing its transport endpoint.

Because not all transport providers support the orderly release facility just described, users may have to use the abortive release facility provided by t_snddis() and t_rcvdis(). However, steps must be taken by each user to prevent data loss. For example, a special byte pattern can be inserted in the datastream to indicate the end of a conversation. There are many possible routines for preventing data loss. Each application and high-level protocol must choose an appropriate routine given the target protocol environment and requirements.

Introduction to Connectionless-Mode Service

This section describes the connectionless-mode service of the Transport Interface. Connectionless-mode service is appropriate for short-term request/response interactions, such as transaction-processing applications. Data is transferred in self-contained units with no logical relationship required among multiple units.

The connectionless-mode service is described using a transaction server as an example. This server waits for incoming transaction queries, and processes and responds to each query.

Local Management

Just as with connection-mode service, the transport users must do appropriate local management steps before transferring data. A user must choose the appropriate connectionless service provider using t_open() and establish its identity using t_bind(). See the t_open(3N) man page or the other “Local Management” section of this chapter (under “Introduction to Connection-Mode Service”) for information about what t_open() returns.

t_optmgmt() can be used to negotiate protocol options associated with the transfer of each data unit. As with the connection-mode service, each transport provider specifies the options, if any, that it supports. Option negotiation is therefore a protocol-specific activity.

The definitions and local management calls needed by the transaction server are shown in Example 9-4:

Example 9-4. The Transaction Server Definitions and Local Management

#include <stdio.h>
#include <fcntl.h> 
#include <tiuser.h> 

#define SRV_ADDR  2     /* server's well-known address */

void main()
{
    int fd;
    int flags;

    struct t_bind *bind;
    struct t_unitdata *ud;
    struct t_uderr *uderr;

    extern int t_errno;

    if ((fd = t_open("/dev/ticlts", O_RDWR, NULL)) < 0) {
        t_error("unable to open /dev/provider");
        exit(1);
    }

    if ((bind = (struct t_bind *)t_alloc(fd, T_BIND,
                T_ADDR)) == NULL)   {
        t_error("t_alloc of t_bind structure failed");
        exit(2);
    } 

    bind->addr.len = sizeof(int);
    *(int *)bind->addr.buf = SRV_ADDR;
    bind->qlen = 0;

    if (t_bind(fd, bind, bind) < 0)  {
        t_error("t_bind failed");
        exit(3);
    } 

    /*
     * is the bound address correct?
     */ 

    if (*(int *)bind->addr.buf != SRV_ADDR)
    {
        fprintf(stderr, "t_bind bound wrong address\n");
        exit(4);
    }

The local management steps should look familiar by now. The server establishes a transport endpoint with the desired transport provider using t_open(). Each provider has an associated service type, so the user can choose a particular service by opening the appropriate transport provider file. This connectionless-mode server ignores the characteristics of the provider returned by t_open() in the same way as the users in the connection-mode example, by setting the third argument to NULL. For simplicity, the transaction server assumes the transport provider has the following characteristics:

  • The transport address is an integer value that uniquely identifies each user.

  • The transport provider supports the T_CLTS service type (connectionless transport service, or datagram).

  • The transport provider does not support any protocol-specific options.

The connectionless server also binds a transport address to the endpoint so that potential clients can identify and access the server. A t_bind structure is allocated using t_alloc(), and the buf and len fields of the address are set accordingly.

One important difference between the connection-mode server and this connectionless-mode server is that the qlen field of the t_bind structure has no meaning for connectionless-mode service, since all users are capable of receiving datagrams once they have bound an address. The Transport Interface defines an inherent client/server relationship between two users while establishing a transport connection in the connection-mode service. However, no such relationship exists in the connectionless-mode service. It is the context of this example, not the Transport Interface, that defines one user as a server and another as a client.

Because the address of the server is known by all potential clients, the server checks the bound address returned by t_bind() to ensure it is correct.

Data Transfer

Once a user has bound an address to the transport endpoint, datagrams can be sent or received over that endpoint. Each outgoing message is accompanied by the address of the destination user. In addition, the Transport Interface enables a user to specify protocol options that should be associated with the transfer of the data unit (for example, transit delay). As discussed earlier, each transport provider defines the set of options, if any, that can accompany a datagram. When the datagram is passed to the destination user, the associated protocol options can be returned as well.

The sequence of calls in Example 9-5 illustrates the data transfer phase of the connectionless-mode server:

Example 9-5. The Data Transfer Phase of a Connectionless-Mode Server

if ((ud = (struct t_unitdata *)t_alloc(fd, T_UNITDATA,
                                       T_ALL)) == NULL)  {
        t_error("t_alloc of t_unitdata structure failed");
        exit(5);
} 

if ((uderr = (struct t_uderr *)t_alloc(fd, T_UDERROR,
                                       T_ALL)) == NULL)  {
        t_error("t_alloc of t_uderr structure failed");
        exit(6);
} 

    while (1)  {
        if (t_rcvudata(fd, ud, &flags) < 0)  {
            if (t_errno == TLOOK)  {
                /* Error on previously sent datagram */
                if (t_rcvuderr(fd, uderr) < 0)  {
                    exit(7);
                }
                fprintf(stderr, "bad datagram, \
                 error = %d\n", uderr->error);
                continue;
            }
            t_error("t_rcvudata failed");
            exit(8);
        } 

        /*
         * Query() processes the request and places the
         * response in ud->udata.buf, setting ud->udata.len
         */ 

        query(ud);

        if (t_sndudata(fd, ud < 0)  {
            t_error("t_sndudata failed");
            exit(9);
        }
    }
}
query()
{
    /* Merely a stub for simplicity */ 
}

The server must first allocate a t_unitdata structure for storing datagrams, which has the following format:

struct t_unitdata {
    struct netbuf addr;
    struct netbuf opt;
    struct netbuf udata;
}

addr holds the source address of incoming datagrams and the destination address of outgoing datagrams, opt identifies any protocol options associated with the transfer of the datagram, and udata holds the data itself. The addr, opt, and udata fields must all be allocated with buffers large enough to hold any possible incoming values. As described in the previous section, the T_ALL argument to t_alloc() ensures this and sets the maxlen field of each netbuf structure accordingly. Because the provider does not support protocol options in this example, no options buffers are allocated, and maxlen is set to zero in the netbuf structure for options. The server also allocates a t_uderr structure for processing any datagram errors, as discussed later in this section.

The transaction server loops forever, receiving queries, processing the queries, and responding to the clients. It first calls t_rcvudata() to receive the next query. t_rcvudata() retrieves the next available incoming datagram. If none is currently available, t_rcvudata() blocks, waiting for a datagram to arrive. The second argument of t_rcvudata() identifies the t_unitdata structure in which the datagram should be stored.

The third argument, flags, must point to an integer variable and can be set to T_MORE on return from t_rcvudata() to specify that the user's udata buffer was not large enough to store the full datagram. In this case, subsequent calls to t_rcvudata() retrieve the remainder of the datagram. Because t_alloc() allocates a udata buffer large enough to store the maximum datagram size, the transaction server does not have to check the value of flags.

If a datagram is received successfully, the transaction server calls the query() routine to process the request. This routine stores the response in the structure pointed to by ud, and sets ud–>udata.len to the number of bytes in the response. The source address returned by t_rcvudata() in ud–>addr is used as the destination address by t_sndudata().

When the response is ready, t_sndudata() is called to return the response to the client. The Transport Interface prevents a user from flooding the transport provider with datagrams using the same flow control mechanism described for the connection-mode service. In such cases, t_sndudata() blocks until the flow control is relieved, and then resumes its operation.

Datagram Errors

If the transport provider cannot process a datagram that was passed to it by t_sndudata(), it returns a unit data error event, T_UDERR, to the user. This event includes the destination address and options associated with the datagram, plus a protocol-specific error value that describes what could be wrong with the datagram. The reason a datagram could not be processed is protocol-specific. One reason can be that the transport provider could not interpret the destination address or options. Each transport protocol is expected to specify all reasons why it is unable to process a datagram.


Note: The unit data error indication is not necessarily intended to indicate success or failure in delivering the datagram to the specified destination. The transport protocol decides how the indication is used. Remember, the connectionless service does not guarantee reliable delivery of data.

The transaction server is notified of this error event when it attempts to receive another datagram. In this case, t_rcvudata() fails, setting t_errno to TLOOK. If TLOOK is set, the only possible event is T_UDERR, so the server calls t_rcvuderr() to retrieve the event. The second argument to t_rcvuderr() is the t_uderr structure that was allocated earlier. This structure is filled in by t_rcvuderr() and has the following format:

struct t_uderr  {
    struct netbuf addr;
    struct netbuf opt;
    long error;
 }

addr and opt identify the destination address and protocol options as specified in the bad datagram, and error is a protocol-specific error code that specifies why the provider could not process the datagram. The transaction server prints the error code and then continues by entering the processing loop again.

A Read/Write Interface

A user may wish to establish a transport connection and then exec() (see exec(2)) an existing user program such as cat to process the data as it arrives over the connection. However, existing programs use read() and write() for their input/output needs. The Transport Interface does not directly support a read/write interface to a transport provider, but one is available with IRIX. This interface enables a user to issue read() and write() calls over a transport connection that is in the data transfer phase. This section describes the read/write interface to the connection-mode service of the Transport Interface. This interface is not available with the connectionless-mode service.

The read/write interface is presented using the client example of “Introduction to Connection-Mode Service” with some minor modifications. The clients are identical until the data transfer phase is reached. At that point, this client uses the read/write interface and cat to process incoming data. cat() can be run without change over the transport connection. Only the differences between this client and that of the example in “Introduction to Connection-Mode Service” are shown below:

#include <stropts.h>
    ...
    /*
     * Same local management and connection
     * establishment steps.
     */
    ...
    if (ioctl(fd, I_PUSH, "tirdwr") < 0) {
        perror("I_PUSH of tirdwr failed");
        exit(5);
    }

    close(0);
    dup(fd);
    execl("/usr/bin/cat", "/usr/bin/cat", 0);
    perror("execl of /usr/bin/cat failed");
    exit(6);
 }

The client invokes the read/write interface by pushing the tirdwr (see tirdwr(7)) module onto the Stream associated with the transport endpoint where the connection was established (see I_PUSH in streamio(5)). This module converts the Transport Interface above the transport provider into a pure read/write interface. With the module in place, the client calls close() and dup() (see close(2) and dup(2)) to establish the transport endpoint as its standard input file, and uses /usr/bin/cat to process the input. Because the transport endpoint identifier is a file descriptor, the facility for dup()ing the endpoint is available to users.

Because the Transport Interface uses STREAMS, the facilities of this character input/output mechanism can be used to provide enhanced user services. By pushing the tirdwr module above the transport provider, the user's interface is effectively changed. The semantics of read() and write() must be followed, and message boundaries are not preserved.


Note: The tirdwr module can only be pushed onto a Stream when the transport endpoint is in the data transfer phase. Once the module is pushed, the user cannot call any Transport Interface routines. If a Transport Interface routine is invoked, tirdwr generates a fatal protocol error, EPROTO, on that Stream, rendering it unusable. Furthermore, if the user pops the tirdwr module off the Stream (see I_POP in streamio(5)), the transport connection is aborted.

The exact semantics of write(), read(), and close() using tirdwr are described below. To summarize, tirdwr enables a user to send and receive data over a transport connection using read() and write(). This module translates all Transport Interface indications into the appropriate actions. The connection can be released with the close() system call.

write()

The user can transmit data over the transport connection using write(). The tirdwr module passes data through to the transport provider. However, if a user attempts to send a zero-length data packet, which the STREAMS mechanism allows, tirdwr discards the message. If the transport connection is aborted (for example, because the other user aborts the connection using t_snddis()), a STREAMS hangup condition is generated on that Stream, and further write() calls fail and set errno to ENXIO. The user can still retrieve any available data after a hangup.

read()

read() can be used to retrieve data that has arrived over the transport connection. The tirdwr module passes data through to the user from the transport provider. However, any other event or indication passed to the user from the provider is processed by tirdwr as follows:

  • read() cannot process expedited data because it cannot distinguish expedited data from normal data for the user. If an expedited data indication is received, tirdwr generates a fatal protocol error, EPROTO, on that Stream. This error causes further system calls to fail. You should therefore not communicate with a process that is sending expedited data.

  • If an abortive disconnect indication is received, tirdwr discards it and generates a hangup condition on that Stream. Subsequent read() calls retrieve any remaining data, and then read() returns 0 for all further calls (indicating end-of-file).

  • If an orderly release indication is received, tirdwr discards the indication and delivers a zero-length message to the user. As described in read(2), this notifies the user of end-of-file by returning 0.

  • If any other Transport Interface indication is received, tirdwr generates a fatal protocol error, EPROTO, on that Stream. This causes further system calls to fail. If a user pushes tirdwr onto a Stream after the connection has been established, no indication is generated.

close()

With tirdwr on a Stream, the user can send and receive data over a transport connection for the duration of that connection. Either user can terminate the connection 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:

  • If an orderly release indication was previously received by tirdwr, an orderly release request is passed to the transport provider to complete the orderly release of the connection. The user who initiated the orderly release procedure receives the expected indication when data transfer completes.

  • If a disconnect indication was previously received by tirdwr, no special action is taken.

  • If neither an orderly release indication nor a disconnect indication was previously received by tirdwr, a disconnect request is passed to the transport provider to abort the connection.

  • If an error previously occurred on the Stream and a disconnect indication has not been received by tirdwr, a disconnect request is passed to the transport provider.

A process cannot initiate an orderly release after tirdwr is pushed onto a Stream, but tirdwr handles an orderly release properly if it is initiated by the user on the other side of a transport connection. If the client described in this section is communicating with the server program in “Introduction to Connection-Mode Service”, that server terminates the transfer of data with an orderly release request. The server then waits for the corresponding indication from the client. At that point, the client exits and the transport endpoint is closed. When the file descriptor is closed, as explained in the first bulleted item above, tirdwr initiates the orderly release request from the client's side of the connection. This generates the indication that the server is expecting, and the connection is released properly.

Advanced Topics

This section presents the following important concepts of the Transport Interface that have not been covered in the previous section:

  • An optional nonblocking (asynchronous) mode for some library calls

  • An advanced programming example that defines a server supporting multiple outstanding connect indications and operating in an event-driven manner

Asynchronous Execution Mode

Many Transport Interface library routines can block waiting for an incoming event or the relaxation of flow control. However, some time-critical applications should not block for any reason. Similarly, an application may wish to do local processing while waiting for some asynchronous transport interface event.

Support for asynchronous processing of Transport Interface events is available to applications using a combination of the STREAMS asynchronous features and the nonblocking mode of the Transport Interface library routines. Earlier examples in this chapter have illustrated the use of the poll() system call and the I_SETSIG ioctl() command for processing events asynchronously.

In addition, any Transport Interface routine that can block while waiting for some event can be run in a special nonblocking mode. For example, t_listen() normally blocks waiting for a connect indication. However, a server can periodically poll a transport endpoint for existing connect indications by calling t_listen() in the nonblocking (or asynchronous) mode. The asynchronous mode is enabled by setting O_NDELAY or O_NONBLOCK on the file descriptor. These can be set as a flag on t_open() or by calling fcntl() (see fcntl(2)) before calling the Transport Interface routine. fcntl() can be used to enable or disable this mode at any time. All programming examples in this chapter use the default synchronous processing mode.

O_NDELAY or O_NONBLOCK affect each Transport Interface routine differently. To determine the exact semantics of O_NDELAY or O_NONBLOCK for a particular routine, see the relevant manual pages.

Advanced Programming Example

The example in Example 9-6 demonstrates two important concepts. The first is a server's ability to manage multiple outstanding connect indications. The second is an illustration of the ability to write event-driven software using the Transport Interface and the system call interface.

The server example in Example 9-6 is capable of supporting only one outstanding connect indication, but the Transport Interface supports the ability to manage multiple outstanding connect indications. One reason a server might wish to receive several simultaneous connect indications is to impose a priority scheme on each client. A server can retrieve several connect indications, and then accept them in an order based on a priority associated with each client. A second reason for handling several outstanding connect indications is that the single-threaded scheme has some limitations. Depending on the implementation of the transport provider, it is possible that while the server is processing the current connect indication, other clients will find it busy. If, however, multiple connect indications can be processed simultaneously, the server will be found to be busy only if the maximum allowed number of clients attempt to call the server simultaneously.

The server example in Example 9-6 is event-driven: the process polls a transport endpoint for incoming Transport Interface events, and then takes the appropriate actions for the current event. The example demonstrates the ability to poll multiple transport endpoints for incoming events.

The definitions and local management functions needed by the example in Example 9-6 are similar to those of the server example in Example 9-4.

Example 9-6. An Advanced Server

#include <tiuser.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <stropts.h>
#include <signal.h>

#define NUM_FDS        1
#define MAX_CONN_IND   4
#define SRV_ADDR       1    /* server's well-known address */

int conn_fd;                /* server connection here */
extern int t_errno;

/* holds connect indications */
struct t_call  *calls[NUM_FDS][MAX_CONN_IND];

void main()
{
    struct pollfd pollfds[NUM_FDS];
    struct t_bind *bind;
    int i;

    /*
     * Only opening and binding one transport endpoint,
     * but more could be supported
     */
    if ((pollfds[0].fd = t_open("/dev/ticotsord", O_RDWR,
                                NULL)) < 0) {
        t_error("t_open failed");
        exit(1);
    } 

    if ((bind = (struct t_bind *)t_alloc(pollfds[0].fd,
          T_BIND, T_ALL)) == NULL) {
        t_error("t_alloc of t_bind structure failed");
        exit(2);
    }
    bind->qlen = MAX_CONN_IND;
    bind->addr.len = sizeof(int);
    *(int *)bind->addr.buf = SRV_ADDR;

    if (t_bind(pollfds[0].fd, bind, bind) < 0)  {
        t_error("t_bind failed");
        exit(3);
    }

    /* Was the correct address bound? */ 
    if (*(int *)bind->addr.buf != SRV_ADDR)  {
        fprintf(stderr, "t_bind bound wrong address\n");
        exit(4);
    }

The file descriptor returned by t_open() is stored in a pollfd structure (see poll(2)) that polls the transport endpoint for incoming data. Notice that only one transport endpoint is established in this example. However, the remainder of the example is written to manage multiple transport endpoints. Several endpoints could be supported with minor changes to the above code.

An important aspect of this server is that it sets qlen to a value greater than 1 for t_bind(). This specifies that the server is willing to handle multiple outstanding connect indications. Remember that the earlier examples single-threaded the connect indications and responses. The server accepted the current connect indication before retrieving additional connect indications. This example, however, can retrieve up to MAX_CONN_IND connect indications at one time before responding to any of them. The transport provider can negotiate the value of qlen downward if it cannot support MAX_CONN_IND outstanding connect indications.

Once the server has bound its address and is ready to process incoming connect requests, it performs the following:

    pollfds[0].events = POLLIN;
    while (1) {
        if (poll(pollfds, NUM_FDS, -1) < 0)  {
            perror("poll failed");
            exit(5);
        }
        for (i = 0; i < NUM_FDS; i++)  {

            switch (pollfds[i].revents)  {

            default:
                perror("poll returned error event");
                exit(6);

            case 0:
                continue;

            case POLLIN:
                do_event(i, pollfds[i].fd);
                service_conn_ind(i, pollfds[i].fd);
            }
        }
    }
}

The events field of the pollfd structure is set to POLLIN, which notifies the server of any incoming Transport Interface events. The server then enters an infinite loop, in which it poll()s the transport endpoint(s) for events, and then processes those events as they occur.

The poll() call blocks indefinitely, waiting for an incoming event. On return, each entry (corresponding to each transport endpoint) is checked for an existing event. If revents is set to 0, no event has occurred on that endpoint. In this case, the server continues to the next transport endpoint. If revents is set to POLLIN, an event does exist on the endpoint. In this case, do_event() is called to process the event. If revents contains any other value, an error must have occurred on the transport endpoint, and the server exits.

For each iteration of the loop, if any event is found on the transport endpoint, service_conn_ind() is called to process any outstanding connect indications. However, if another connect indication is pending, service_conn_ind() saves the current connect indication and responds to it later. This routine is explained shortly. If an incoming event is discovered, the routine in Example 9-7 is called to process it.

Example 9-7. Processing an Incoming Event

do_event(slot, fd)
{
    struct t_discon *discon;
    int i;

    switch (t_look(fd))  {

    default:
        fprintf(stderr,"t_look: unexpected event\n");
        exit(7);

    case T_ERROR:
        fprintf(stderr,"t_look returned T_ERROR event\n");
        exit(8);

    case -1:
        t_error("t_look failed");
        exit(9);

    case 0:
        /* since POLLIN returned, this should not happen */
        fprintf(stderr, "t_look returned no event\n");
        exit(10);

    case T_LISTEN:
         /* find free element in calls array */

        for (i = 0; i < MAX_CONN_IND; i++)  {
            if (calls[slot][i] == NULL)
                break;
        } 

        if ((calls[slot][i] = (struct t_call *)t_alloc(fd,
               T_CALL, T_ALL)) == NULL)   {
            t_error("t_alloc of t_call structure failed");
            exit(11);
        } 

        if (t_listen(fd, calls[slot][i]) < 0)  {
            t_error("t_listen failed");
            exit(12);
        }
        break;
    case T_DISCONNECT:
        discon = (struct t_discon *)t_alloc(fd,T_DIS,T_ALL);
        if (t_rcvdis(fd, discon) < 0)  {
            t_error("t_rcvdis failed");
            exit(13);
        }
          /* find call ind in array and delete it  */

        for (i = 0; i < MAX_CONN_IND; i++)  {
            if (discon->sequence==calls[slot][i]->sequence) {
                t_free((char*)calls[slot][i], T_CALL);
                calls[slot][i] = NULL;
            }
        }
        t_free(discon, T_DIS);
        break;
    }
}

This routine takes a number, slot, and a file descriptor, fd, as arguments. slot is used as an index into the global array calls. This array contains an entry for each polled transport endpoint, where each entry consists of an array of t_call structures that hold incoming connect indications for that transport endpoint. The value of slot is used to identify the transport endpoint.

do_event() calls t_look() to determine the Transport Interface event that has occurred on the transport endpoint specified by fd. If a connect indication (T_LISTEN event) or disconnect indication (T_DISCONNECT event) has arrived, the event is processed. Otherwise, the server prints an appropriate error message and exits.

For connect indications, do_event() scans the array of outstanding connect indications looking for the first free entry. A t_call structure is then allocated for that entry, and the connect indication is retrieved using t_listen(). There must always be at least one free entry in the connect indication array, because the array is large enough to hold the maximum number of outstanding connect indications as negotiated by t_bind(). The processing of the connect indication is deferred until later.

If a disconnect indication arrives, it must correspond to a previously received connect indication. This occurs if a client attempts to undo a previous connect request. In this case, do_event() allocates a t_discon structure to retrieve the relevant disconnect information. This structure has the following members:

struct t_discon  {
    struct netbuf udata;
    int reason;
    int sequence;
 }

udata identifies any user data that might have been sent with the disconnect indication, reason contains a protocol-specific disconnect reason code, and sequence identifies the outstanding connect indication that matches this disconnect indication.

Next, t_rcvdis() is called to retrieve the disconnect indication. The array of connect indications for slot is then scanned for one that contains a sequence number that matches the sequence number in the disconnect indication. When the connect indication is found, it is freed and the corresponding entry is set to NULL.

As mentioned earlier, if any event is found on a transport endpoint, service_conn_ind() is called to process all currently outstanding connect indications associated with that endpoint as follows:

service_conn_ind(slot, fd)
{
    int i;

    for (i = 0; i < MAX_CONN_IND; i++) {
        if (calls[slot][i] == NULL)
            continue;
        if ((conn_fd = t_open("/dev/ticotsord", O_RDWR,
                              NULL)) < 0) {
            t_error("open failed");
            exit(14);
        }
        if (t_bind(conn_fd, NULL, NULL) < 0) {
            t_error("t_bind failed");
            exit(15);
        } 
        if (t_accept(fd, conn_fd, calls[slot][i]) < 0) {
            if (t_errno == TLOOK) {
                t_close(conn_fd);
                return;
            }
            t_error("t_accept failed");
            exit(16);
        }
        t_free((char*)calls[slot][i], T_CALL);
        calls[slot][i] = NULL;

        run_server(fd);
    }
}

For the given slot (the transport endpoint), the array of outstanding connect indications is scanned. For each indication, the server opens a responding transport endpoint, binds an address to the endpoint, and then accepts the connection on that endpoint. If another event (connect indication or disconnect indication) arrives before the current indication is accepted, t_accept() fails and sets t_errno to TLOOK.


Note: The user cannot accept an outstanding connect indication if any pending connect indication events or disconnect indication events exist on that transport endpoint.

If this error occurs, the responding transport endpoint is closed and service_conn_ind() returns immediately (saving the current connect indication for later processing). This causes the server's main processing loop to be entered, and the new event is discovered by the next call to poll(). In this way, multiple connect indications can be queued by the user.

Eventually, all events are processed, and service_conn_ind() is able to accept each connect indication in turn. Once the connection is established, the run_server() routine used by the server in “Introduction to Connection-Mode Service” is called to manage the data transfer.

State Transitions

These tables describe all state transitions associated with the Transport Interface. First, however, the states and events are described.

Transport Interface States

Table 9-6 defines the states used to describe the Transport Interface state transitions.

Table 9-6. States Describing Transport Interface State Transitions

State

Description

Service Type

T_UNINIT

Uninitialized—initial and final state of interface

T_COTS, T_COTS_ORD, T_CLTS

T_UNBND

Initialized but not bound

T_COTS, T_COTS_ORD, T_CLTS

T_IDLE

No connection established

T_COTS, T_COTS_ORD, T_CLTS

T_OUTCON

Outgoing connection pending for client

T_COTS, T_COTS_ORD

T_INCON

Incoming connection pending for server

T_COTS, T_COTS_ORD

T_DATAXFER

Data transfer

T_COTS, T_COTS_ORD

T_OUTREL

Outgoing orderly release (waiting for orderly release indication)

T_COTS_ORD

T_INREL

Incoming orderly release (waiting to send orderly release request)

T_COTS_ORD


Outgoing Events

The outgoing events described in Table 9-7 correspond to the return of the specified transport routines, where these routines send a request or response to the transport provider.

In the table, some events (such as acceptn) are distinguished by the context in which they occur. The context is based on the values of the following variables:

ocnt  

count of outstanding connect indications

fd  

file descriptor of the current transport endpoint

resfd  

file descriptor of the transport endpoint where a connection is accepted

Table 9-7. Outgoing Events

Event

Description

Service Type

open

Successful return of t_open()

T_COTS, T_COTS_ORD, T_CLTS

bind

Successful return of t_bind()

T_COTS, T_COTS_ORD, T_CLTS

optmgmt

Successful return of t_optmgmt()

T_COTS, T_COTS_ORD, T_CLTS

unbind

Successful return of t_unbind()

T_COTS, T_COTS_ORD, T_CLTS

close

Successful return of t_close()

T_COTS, T_COTS_ORD, T_CLTS

sndudata

Successful return of t_sndudata()

T_CLTS

connect1

Successful return of t_connect() in synchronous mode

T_COTS, T_COTS_ORD

connect2

TNODATA error on t_connect() in asynchronous mode, or TLOOK error due to a disconnect indication arriving on the transport endpoint

T_COTS, T_COTS_ORD

accept1

Successful return of t_accept() with ocnt == 1, fd == resfd

T_COTS, T_COTS_ORD

accept2

Successful return of t_accept() with ocnt == 1, fd != resfd

T_COTS, T_COTS_ORD

accept3

Successful return of t_accept() with ocnt > 1

T_COTS, T_COTS_ORD

snd

Successful return of t_snd()

T_COTS, T_COTS_ORD

snddis1

Successful return of t_snddis() with
ocnt <= 1

T_COTS, T_COTS_ORD

snddis2

Successful return of t_snddis() with ocnt > 1

T_COTS, T_COTS_ORD

sndrel

Successful return of t_sndrel()

T_COTS_ORD


Incoming Events

The incoming events correspond to the successful return of the specified routines, where these routines retrieve data or event information from the transport provider. The only incoming event not associated directly with the return of a routine is pass_conn, which occurs when a user transfers a connection to another transport endpoint. This event occurs on the endpoint that is being passed the connection, despite the fact that no Transport Interface routine is issued on that endpoint. pass_conn is included in the state tables to describe the behavior when a user accepts a connection on another transport endpoint.

In Table 9-8, the rcvdis events are distinguished by the context in which they occur. The context is based on the value of ocnt, which is the count of outstanding connect indications on the transport endpoint.

Table 9-8. Incoming Events

Event

Description

Service Type

rcvudata

Successful return of t_rcvudata()

T_CLTS

rcvuderr

Successful return of t_rcvuderr()

T_CLTS

rcvconnect

Successful return of t_rcvconnect()

T_COTS, T_COTS_ORD

listen

Successful return of t_listen()

T_COTS, T_COTS_ORD

rcv

Successful return of t_rcv()

T_COTS, T_COTS_ORD

rcvdis1

Successful return of t_rcvdis() with ocnt <= 0

T_COTS, T_COTS_ORD

rcvdis2

Successful return of t_rcvdis() with ocnt == 1

T_COTS, T_COTS_ORD

rcvdis3

Successful return of t_rcvdis() with ocnt > 1

T_COTS, T_COTS_ORD

rcvrel

Successful return of t_rcvrel()

T_COTS_ORD

pass_conn

Receive a passed connection

T_COTS, T_COTS_ORD


Transport User Actions

In the state tables that follow, some state transitions are accompanied by a list of actions the transport user must take. These actions are represented by the notation [x], where x is a mnemonic for the specific action:

[0] 

Set the count of outstanding connect indications to zero.

[+] 

Increment the count of outstanding connect indications.

[-] 

Decrement the count of outstanding connect indications.

[–>] 

Pass a connection to another transport endpoint as indicated in t_accept().

State Tables

Table 9-9, Table 9-10, and Table 9-11 describe the Transport Interface state transitions. Given a current state and an event, the transition to the next state is shown, as well as any actions that must be taken by the transport user (indicated by [x]). The state is that of the transport provider as seen by the transport user.

To see what the next state will be in a given situation, find the table cell at the intersection of the column headed by the current state and the row labeled with the current incoming or outgoing event. An empty cell represents a state/event combination that is invalid. Along with the next state, each cell can indicate one or more actions from among those listed in the previous section. The transport user must take the specific actions in the order specified in the state table.

The following should be understood when studying the state tables:

  • The t_close() routine is referenced in the state tables (see close event in Table 9-9) but can be called from any state to close a transport endpoint. If t_close() is called when a transport address is bound to an endpoint, the address is unbound. Also, if t_close() is called when the transport connection is still active, the connection is aborted.

  • If a transport user issues a routine out of sequence, the transport provider recognizes this and the routine fails, setting t_errno to TOUTSTATE. The state does not change.

  • If any other transport error occurs, the state does not change unless explicitly stated on the manual page for that routine. The exception to this is a TLOOK or TNODATA error on t_connect(), as described in Table 9-7 under “connect2.” The state tables assume correct use of the Transport Interface.

  • The support routines t_getinfo(), t_getstate(), t_alloc(), t_free(), t_sync(), t_look(), and t_error() are excluded from the state tables because they do not affect the state.

Here are the state-transition tables: one for common local management steps; one for data transfer in connectionless mode; and one for connection establishment, connection release, and data transfer in connection mode.

Table 9-9. Common Local Management State Table

 

T_UNINIT

T_UNBND

T_IDLE

open

T_UNBND

 

 

bind

 

T_IDLE[0]

 

optmgmt

 

 

T_IDLE

unbind

 

 

T_UNBND

close

 

T_UNINIT

 


Table 9-10. Connectionless-Mode State Table

 

T_IDLE

sndudata

T_IDLE

rcvudata

T_IDLE

rcvuderr

T_IDLE


Table 9-11. Connection-Mode State Table

 

T_IDLE

T_OUTCON

T_INCON

T_DATAXFER

T_OUTREL

T_INREL

connect1

T_DATAXFER

 

 

 

 

 

connect2

T_OUTCON

 

 

 

 

 

rcvconnet

 

T_DATAXFR

 

 

 

 

listen

T_INCON [+]

 

T_INCON [+]

 

 

 

accept1

 

 

T_DATAXFER [-]

 

 

 

accept2

 

 

T_IDLE [-] [–>]

 

 

 

accept3

 

 

T_INCON [-] [–>]

 

 

 

snd

 

 

 

T_DATAXFR

 

T_INRL

rcv

 

 

 

T_DATAXFR

T_OUTRL

 

snddis1

 

T_IDLE

T_IDLE [-]

T_IDLE

T_IDLE

T_IDLE

snddis2

 

 

T_INCON [-]

 

 

 

rcvdis1

 

T_IDLE

 

T_IDLE

T_IDLE

T_IDLE

rcvdis2

 

 

T_IDLE [-]

 

 

 

rcvdis3

 

 

T_INCON [-]

 

 

 

sndrel

 

 

 

T_OUTREL

 

T_IDLE

rcvrel

 

 

 

T_INREL

T_IDLE

 

pass_conn

T_DATAXFER

 

 

 

 

 


Guidelines for Protocol Independence

By defining a set of services common to many transport protocols, the Transport Interface provides many opportunities for protocol independence. However, there exist some transport protocols that do not support all of the services supported by the Transport Interface. If software must be run in a variety of protocol environments, only the common services should be accessed.

The following guidelines highlight services that may not be common to all transport protocols:

  • In the connection-mode service, the concept of a transport service data unit (TSDU) may not be supported by all transport providers. The user should make no assumptions about the preservation of logical data boundaries across a connection. If messages must be transferred over a connection, a protocol should be implemented above the Transport Interface to support message boundaries.

  • Protocol- and implementation-specific service limits are returned by the t_open() and t_getinfo() routines. These limits are useful when allocating buffers to store protocol-specific transport addresses and options. It is the responsibility of the user to access these limits and then adhere to the limits throughout the communication process.

  • User data should not be transmitted with connect requests or disconnect requests (see t_connect(3N) and t_snddis(3N)). Not all transport protocols support this capability.

  • The buffers in the t_call structure used for t_listen() must be large enough to hold any information passed by the client during connection establishment. The server should use the T_ALL argument to t_alloc(), which determines the maximum buffer sizes needed to store the address, options, and user data for the current transport provider.

  • The user program should not look at or change options that are associated with any Transport Interface routine. These options are specific to the underlying transport protocol. The user should not pass options with t_connect() or t_sndudata(). In such cases, the transport provider uses default values. Also, a server should use the options returned by t_listen() when accepting a connection.

  • Protocol-specific addressing issues should be hidden from the user program. A client should not specify any protocol address on t_bind(), but instead should allow the transport provider to assign an appropriate address to the transport endpoint. Similarly, a server should retrieve its address for t_bind() in such a way that it does not require knowledge of the transport provider's address space. Such addresses should not be hard-coded into a program. A name server procedure could be useful in this situation, but the details for providing this service are outside the scope of the Transport Interface. The reason codes associated with t_rcvdis() are protocol-dependent. The user should not interpret this information if protocol independence is important.

  • The error codes associated with t_rcvuderr() are protocol-dependent. The user should not interpret this information if protocol independence is a concern.

  • The names of devices should not be hard-coded into programs, because the device node identifies a particular transport provider and is not protocol independent.

  • The optional orderly release facility of the connection-mode service (provided by t_sndrel() and t_rcvrel()) should not be used by programs targeted for multiple protocol environments. This facility is not supported by all connection-based transport protocols.

Some Examples

This section contains examples of complete client and server programs mentioned earlier in the chapter.

Connection-Mode Client

The code in Example 9-8 represents the connection-mode client program described in “Introduction to Connection-Mode Service”.

This client establishes a transport connection with a server, and then receives data from the server and writes that data to the client's standard output. The connection is released using the orderly release facility of the Transport Interface. This client communicates with each of the connection-mode servers presented in the guide.

Example 9-8. A Connection-Mode Client

#include <stdio.h>
#include <tiuser.h>
#include <fcntl.h>

#define SRV_ADDR 1   /* server's well-known address */ 

void main()
{
    int fd;
    int nbytes;
    int flags = 0;
    char buf[1024];
    struct t_call *sndcall;
    extern int t_errno;

    if ((fd = t_open("/dev/ticotsord", O_RDWR, NULL)) < 0) {
        t_error("t_open failed");
        exit(1);
    }

    if (t_bind(fd, NULL, NULL) < 0) {
        t_error("t_bind failed");
        exit(2);
    }

    /* By assuming that the address is an integer value,
     * this program may not run over another protocol. */

    if ((sndcall = (struct t_call *)t_alloc(fd, T_CALL,
        T_ADDR)) == NULL) {
        t_error("t_alloc failed");
        exit(3);
    }
    sndcall->addr.len = sizeof(int);
    *(int *)sndcall->addr.buf = SRV_ADDR;

    if (t_connect(fd, sndcall, NULL) < 0) {
        t_error("t_connect failed for fd");
        exit(4);
    } 
    while ((nbytes = t_rcv(fd, buf, 1024, &flags)) != -1){
        if (fwrite(buf, 1, nbytes, stdout) < 0){
            fprintf(stderr, "fwrite failed\n");
            exit(5);
        }
    } 
    if ((t_errno == TLOOK)  &&  (t_look(fd) == T_ORDREL)) {
        if (t_rcvrel(fd) < 0) {
            t_error("t_rcvrel failed");
            exit(6);
        }
        if (t_sndrel(fd) < 0) {
            t_error("t_sndrel failed");
            exit(7);
        }
        exit(0);
    }
    t_error("t_rcv failed");
    exit(8);
 }


Connection-Mode Server

The code in Example 9-9 represents the connection-mode server program described in “Introduction to Connection-Mode Service”. This server establishes a transport connection with a client, and then transfers a log file to the client on the other side of the connection. The connection is released using the orderly release facility of the Transport Interface. The connection-mode client presented earlier communicates with this server.

Example 9-9. A Connection-Mode Server

#include <tiuser.h>
#include <stropts.h>
#include <fcntl.h>
#include <stdio.h>
#include <signal.h>

#define DISCONNECT -1
#define SRV_ADDR  1  /* server's well-known address */

int conn_fd;         /* connection established here */
extern int t_errno;

void main()
{
    int listen_fd;   /* listening transport endpoint */
    struct t_bind *bind;
    struct t_call *call;
    if ((listen_fd = t_open("/dev/ticotsord", O_RDWR, NULL))
        < 0) {
        t_error("t_open failed for listen_fd");
        exit(1);
    }
    /*
     * By assuming that the address is an integer value,
     * this program may not run over another protocol.
     */
    if ((bind = (struct t_bind *)t_alloc(listen_fd, T_BIND,
        T_ALL)) == NULL) {
        t_error("t_alloc of t_bind structure failed");
        exit(2);
    }
    bind->qlen = 1;
    bind->addr.len = sizeof(int);
    *(int *)bind->addr.buf = SRV_ADDR;

    if (t_bind(listen_fd, bind, bind) < 0) {
        t_error("t_bind failed for listen_fd");
        exit(3);
    }

    /* Was the correct address bound? */
    if (*(int *)bind->addr.buf != SRV_ADDR) {
        fprintf(stderr, "t_bind bound wrong address\n");
        exit(4);
    }
    if ((call = (struct t_call *)t_alloc(listen_fd, T_CALL,
                                         T_ALL)) == NULL) {
        t_error("t_alloc of t_call structure failed");
        exit(5);
    }
    while (1) {
        if (t_listen(listen_fd, call) < 0) {
            t_error("t_listen failed for listen_fd");
            exit(6);
        }
        if ((conn_fd = accept_call(listen_fd, call))
            != DISCONNECT)
            run_server(listen_fd);
    }
}
 
int accept_call(listen_fd, call)
int listen_fd; struct t_call *call;
{
    int resfd;

    if ((resfd = t_open("/dev/ticotsord", O_RDWR, NULL))
        < 0) {
        t_error("t_open for responding fd failed");
        exit(7);
    }
    if (t_bind(resfd, NULL, NULL) < 0) {
        t_error("t_bind for responding fd failed");
        exit(8);
    }
    if (t_accept(listen_fd, resfd, call) < 0) {
        if (t_errno == TLOOK) {  /* must be a disconnect */
            if (t_rcvdis(listen_fd, NULL) < 0) {
                t_error("t_rcvdis failed for listen_fd");
                exit(9);
            }
            if (t_close(resfd) < 0) {
                t_error("t_close failed for responding fd");
                exit(10);
            }
            /* go back up and listen for other calls */
            return(DISCONNECT);
        }
        t_error("t_accept failed");
        exit(11);
    }
    return(resfd);
}

void connrelease()
 {
    /* conn_fd is global because needed here */
    if (t_look(conn_fd) == T_DISCONNECT) {
        fprintf(stderr, "connection aborted\n");
        exit(12);
    }
    /* else orderly release indication - normal exit */
    exit(0);
 }

int run_server(listen_fd)
int listen_fd;
{
    int nbytes;
    FILE *logfp;        /* file pointer to log file */
    char buf[1024];

    switch (fork()) {

    case -1:
        perror("fork failed");
        exit(20);

    default:    /* parent */
        /* close conn_fd and then go up and listen again */
        if (t_close(conn_fd) < 0) {
            t_error("t_close failed for conn_fd");
            exit(21);
        }
        return;

    case 0:     /* child */
        /* close listen_fd and do service */
        if (t_close(listen_fd) < 0) {
            t_error("t_close failed for listen_fd");
            exit(22);
        }

        if ((logfp = fopen("logfile", "r")) == NULL) {
            perror("cannot open logfile");
            exit(23);
        }

        signal(SIGPOLL, connrelease);
        if (ioctl(conn_fd, I_SETSIG, S_INPUT) < 0) {
            perror("ioctl I_SETSIG failed");
            exit(24);
        }
        if (t_look(conn_fd) != 0) {
            /* disconnect wasn't there */
            fprintf(stderr, "t_look: unexpected event\n");
            exit(25);
        }

        while ((nbytes = fread(buf, 1, 1024, logfp)) > 0)
            if (t_snd(conn_fd, buf, nbytes, 0) < 0) {
                t_error("t_snd failed");
                exit(26);
            }

        if (t_sndrel(conn_fd) < 0) {
            t_error("t_sndrel failed");
            exit(27);
        }
        pause(); /*until orderly release indication arrives*/
    }
 }


Connectionless-Mode Transaction Server

The code in Example 9-10 represents the connectionless-mode transaction server program described in “Introduction to Connectionless-Mode Service”.

This server waits for incoming datagram queries, and then processes each query and sends a response.

Example 9-10. A Connectionless-Mode Transaction Server

#include <stdio.h>
#include <fcntl.h>
#include <tiuser.h>

#define SRV_ADDR  2   /* server's well-known address */

void main()
{
    int fd;
    int flags;
    struct t_bind *bind;
    struct t_unitdata *ud;
    struct t_uderr *uderr;
    extern int t_errno;

    if ((fd = t_open("/dev/ticlts", O_RDWR, NULL)) < 0) {
        t_error("unable to open /dev/provider");
        exit(1);
    } 

    if ((bind = (struct t_bind *)t_alloc(fd, T_BIND,
                                         T_ADDR)) == NULL) {
        t_error("t_alloc of t_bind structure failed");
        exit(2);
    }
    bind->addr.len = sizeof(int);
    *(int *)bind->addr.buf = SRV_ADDR;
    bind->qlen = 0;

    if (t_bind(fd, bind, bind) < 0) {
        t_error("t_bind failed");
        exit(3);
    } 

    /* is the bound address correct? */
    if (*(int *)bind->addr.buf != SRV_ADDR) {
        fprintf(stderr, "t_bind bound wrong address\n");
        exit(4);
    }

    if ((ud = (struct t_unitdata *)t_alloc(fd, T_UNITDATA,
        T_ALL)) == NULL) {
        t_error("t_alloc of t_unitdata structure failed");
        exit(5);
    }
    if ((uderr = (struct t_uderr *)t_alloc(fd, T_UDERROR,
                                           T_ALL)) == NULL) {
        t_error("t_alloc of t_uderr structure failed");
        exit(6);
    } 

    while (1) 
        {
        if (t_rcvudata(fd, ud, &flags) < 0) {
            if (t_errno == TLOOK) {
                /* Error on previously sent datagram */ 
                if (t_rcvuderr(fd, uderr) < 0) {
                    t_error("t_rcvuderr failed");
                    exit(7);
                }
                fprintf(stderr, "bad datagram, \
                  error = %d\n", uderr->error);
                continue;
            } 
            t_error("t_rcvudata failed");
            exit(8);
        } 

        /* 
         * Query() processes the request and places the 
         * response in ud->udata.buf, setting ud->udata.len 
         */ 
        query(ud);

        if (t_sndudata(fd, ud < 0) {
            t_error("t_sndudata failed");
            exit(9);
        }
    }
}

query()
{
    /* Merely a stub for simplicity */
}


Read/Write Client

The code in Example 9-11 represents the connection-mode read/write client program described in “A Read/Write Interface”. This client establishes a transport connection with a server, and then uses cat to retrieve the data sent by the server and write that data to the client's standard output. This client communicates with each of the connection-mode servers presented in the guide.

Example 9-11. A Connection-Mode Read/Write Client

#include <stdio.h>
#include <tiuser.h>
#include <fcntl.h>
#include <stropts.h>

#define SRV_ADDR  1   /* server's well-known address */

void main()
{
    int fd;
    int nbytes;
    int flags = 0;
    char buf[1024];
    struct t_call *sndcall;
    extern int t_errno;

    if ((fd = t_open("/dev/ticotsord", O_RDWR, NULL)) < 0) {
        t_error("t_open failed");
        exit(1);
    } 

    if (t_bind(fd, NULL, NULL) < 0) {
        t_error("t_bind failed");
        exit(2);
    } 

     /* By assuming that the address is an integer value,
      * this program may not run over another protocol. */ 

    if ((sndcall = (struct t_call *)t_alloc(fd, T_CALL,
        T_ADDR)) == NULL) {
        t_error("t_alloc failed");
        exit(3);
    } 

    sndcall->addr.len = sizeof(int);
    *(int *)sndcall->addr.buf = SRV_ADDR;

    if (t_connect(fd, sndcall, NULL) < 0) 
{
        t_error("t_connect failed for fd");
        exit(4);
    }
    if (ioctl(fd, I_PUSH, "tirdwr") < 0) {
        perror("I_PUSH of tirdwr failed");
        exit(5);
    } 

    close(0);
    dup(fd);

    execl("/usr/bin/cat", "/usr/bin/cat", 0);
    perror("execl of /usr/bin/cat failed");
    exit(6);
}


Event-Driven Server

The code in Example 9-12 represents the connection-mode server program described in “Advanced Topics”. This server manages multiple connect indications in an event-driven manner. Either connection-mode client presented earlier communicates with this server.

Example 9-12. A Connection-Mode Server

#include <tiuser.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <stropts.h>
#include <signal.h>

#define NUM_FDS         1
#define MAX_CONN_IND    4
#define SRV_ADDR        1 /* server's well-known address */

int conn_fd;        /* server connection here */
extern int t_errno;

/* holds connect indications */
struct t_call *calls[NUM_FDS][MAX_CONN_IND];

void main()
{
    struct pollfd pollfds[NUM_FDS];
    struct t_bind *bind;
    int i;

    / * Only opening and binding one transport endpoint,
      * but more could be supported  */

    if ((pollfds[0].fd = t_open("/dev/ticotsord", O_RDWR,
        NULL)) < 0) {
        t_error("t_open failed");
        exit(1);
    }

    if ((bind = (struct t_bind *)t_alloc(pollfds[0].fd,
        T_BIND, T_ALL)) == NULL) {
        t_error("t_alloc of t_bind structure failed");
        exit(2);
    }
    bind->qlen = MAX_CONN_IND;
    bind->addr.len = sizeof(int);
    *(int *)bind->addr.buf = SRV_ADDR;

    if (t_bind(pollfds[0].fd, bind, bind) < 0) {
        t_error("t_bind failed");
        exit(3);
    }

    /* Was the correct address bound? */
    if (*(int *)bind->addr.buf != SRV_ADDR) {
        fprintf(stderr, "t_bind bound wrong address\n");
        exit(4);
    } 

    pollfds[0].events = POLLIN;

    while (1) {
        if (poll(pollfds, NUM_FDS, -1) < 0) {
            perror("poll failed");
            exit(5);
        }

        for (i = 0; i < NUM_FDS; i++) {

            switch (pollfds[i].revents) {

            default:
                perror("poll returned error event");
                exit(6);

            case 0:
                continue;

            case POLLIN:
                do_event(i, pollfds[i].fd);
                service_conn_ind(i, pollfds[i].fd);
            }
        }
    } 
} 
do_event(slot, fd) 
{
    struct t_discon *discon;
    int i;

    switch (t_look(fd)) {

    default: 
        fprintf(stderr,"t_look: unexpected event\n");
        exit(7);

    case T_ERROR:
        fprintf(stderr,"t_look returned T_ERROR event\n");
        exit(8);

    case -1:
        t_error("t_look failed");
        exit(9);

    case 0:
        /* since POLLIN returned, this should not happen */
        fprintf(stderr,"t_look returned no event\n");
        exit(10);
        case T_LISTEN:
        /* find free element in calls array */

        for (i = 0; i < MAX_CONN_IND; i++) {
            if (calls[slot][i] == NULL)
                break;
        }
        if ((calls[slot][i] = (struct t_call *)t_alloc(fd,
            T_CALL, T_ALL)) == NULL)  {
            t_error("t_alloc of t_call structure failed");
            exit(11);
        }
        if (t_listen(fd, calls[slot][i]) < 0) {
            t_error("t_listen failed");
            exit(12);
        }
        break;

    case T_DISCONNECT:
        discon = (struct t_discon *)t_alloc(fd,T_DIS,T_ALL);

        if (t_rcvdis(fd, discon) < 0) {
            t_error("t_rcvdis failed");
            exit(13);
        }
         /* find call ind in array and delete it  */

        for (i = 0; i < MAX_CONN_IND; i++) {
            if (discon->sequence==calls[slot][i]->sequence) {
                t_free((char*)calls[slot][i], T_CALL);
                calls[slot][i] = NULL;
            }
        } 
        t_free((char*)discon, T_DIS);
        break;
    }
 } 
service_conn_ind(slot, fd) 
{
    int i;
    for (i = 0; i < MAX_CONN_IND; i++) {
        if (calls[slot][i] == NULL) 
            continue;
        if ((conn_fd = t_open("/dev/ticotsord", O_RDWR,
                              NULL)) < 0){
            t_error("open failed");
            exit(14);
        }

        if (t_bind(conn_fd, NULL, NULL) < 0)  {
            t_error("t_bind failed");
            exit(15);
        }
        if (t_accept(fd, conn_fd, calls[slot][i]) < 0) {
            if (t_errno == TLOOK) {
                t_close(conn_fd);
                return;
            }
            t_error("t_accept failed");
            exit(16);
        }
        t_free((char*)calls[slot][i], T_CALL);
        calls[slot][i] = NULL;

        run_server(fd);
    }
 } 

void connrelease() 
{
    /* conn_fd is global because needed here */
    if (t_look(conn_fd) == T_DISCONNECT) {
        fprintf(stderr, "connection aborted\n");
        exit(12);
    } 

    /* else orderly release indication - normal exit */
    exit(0);
} 

int run_server(listen_fd) 
int listen_fd;
{
    int nbytes;
    FILE *logfp;        /* file pointer to log file */ 
    char buf[1024];

    switch (fork()) 
{

    case -1: 
        perror("fork failed");
        exit(20);

    default:     /* parent */

        /* close conn_fd and then go up and listen again */
        if (t_close(conn_fd) < 0) {
            t_error("t_close failed for conn_fd");
            exit(21);
        }
        return;

    case 0:     /* child */

        /* close listen_fd and do service */
        if (t_close(listen_fd) < 0) {
            t_error("t_close failed for listen_fd");
            exit(22);
        }
        if ((logfp = fopen("logfile", "r")) == NULL) {
            perror("cannot open logfile");
            exit(23);
        }

        signal(SIGPOLL, connrelease);
        if (ioctl(conn_fd, I_SETSIG, S_INPUT) < 0) {
            perror("ioctl I_SETSIG failed");
            exit(24);
        }
        /* disconnect already there? */
        if (t_look(conn_fd) != 0) {
            fprintf(stderr, "t_look: unexpected event\n");
            exit(25);
        }

        while ((nbytes = fread(buf, 1, 1024, logfp)) > 0)
            if (t_snd(conn_fd, buf, nbytes, 0) < 0){
                t_error("t_snd failed");
                exit(26);
            }

        if (t_sndrel(conn_fd) < 0) {
            t_error("t_sndrel failed");
            exit(27);
        }
        pause();
    /* until orderly release indication arrives */
    }
 }


Error Messages

The following errors have been added to TLI for X/Open Transport Interface (XTI) compatibility.


Note:  XTI has changed the names specified by struct_type by appending _STR to the end. For example, to allocate a t_bind structure, the argument struct_type is T_BIND_STR instead of T_BIND. The old names will continue to be supported. t_free() supports the new names for the struct_type argument.

Upon failure, t_accept() sets t_errno to TBADADDR if the specified protocol address was in an incorrect format or contained illegal information.

Upon failure, t_alloc() sets t_errno to TNOSTRUCTYPE if the struct_type parameter is invalid.

Upon failure, t_bind() sets t_errno to TADDRBUSY if the requested address is in use and the transport provider can't allocate an new address.

t_look() includes two new events, and an existing event was removed. The T_ERROR event was removed because it can be handled by setting t_errno to TSYSERR. The new events, T_GODATA and T_GOEXDATA, are returned to indicate that flow-control restrictions on normal data flow (T_GODATA) or expedited data flow (T_GOEXDATA) have been lifted and data may be sent again.

Upon failure, t_open() sets t_errno to:

TBADFLAG 

if oflag is invalid.

TBADNAME 

if name is not a valid transport provider.

Upon failure, t_rcv(), t_rcvconnect(), t_rcvdis(), t_rcvrel(), and t_rcvudata() set t_errno to TOUTSTATE if the function was issued in the wrong sequence on this transport endpoint.

Upon failure, t_snd() sets t_errno to:

TBADFLAG  

if oflag is invalid.

TLOOK  

if an asynchronous event has occurred on this transport endpoint and requires immediate attention.

TOUTSTATE  

if the function was issued in the wrong sequence on this transport endpoint.

Upon failure, t_sndrel() and t_sndudata() set t_errno to:

 TLOOK  

if an asynchronous event has occurred on this transport endpoint and requires immediate attention.

 TOUTSTATE  

if the function was issued in the wrong sequence on this transport endpoint.