Transport Interfaces Programming Guide

Data Transfer

After the connection is established, both the client and the server can transfer data through the connection using t_snd() and t_rcv(). XTI/TLI does not differentiate the client from the server from this point on. Either user can send data, receive data, or release the connection.

There are two classes of data on a transport connection:

  1. Normal data

  2. Expedited data

Expedited data is for urgent data. The exact semantics of expedited data vary between transport providers. Not all transport protocols support expedited data (see t_open(3N)).

Most connection-oriented mode protocols transfer data in byte streams. "Byte stream" implies no message boundaries in data sent over a connection. Some transport protocols preserve message boundaries over a transport connection. This service is supported by XTI/TLI, but protocol-independent software must not rely on it.

The message boundaries are invoked by the T_MORE flag of t_snd() and t_rcv(). The messages, called transport service data units (TSDU), can be transferred between two transport users as distinct units. The maximum message size is defined by the underlying transport protocol. Get the message size through t_open() or t_getinfo().

You can send a message in multiple units. Set the T_MORE flag on every t_snd() call, except the last to send a message in multiple units. The flag specifies that the data in the current and the next t_snd() calls are a logical unit. Send the last message unit with T_MORE turned off to specify the end of the logical unit.

Similarly, a logical unit can be sent in multiple units. If t_rcv() returns with the T_MORE flag set, the user must call t_rcv() again to receive the rest of the message. The last unit in the message is identified by a call to t_rcv() that does not set T_MORE.

The T_MORE flag implies nothing about how the data is packaged below XTI/TLI or how the data is delivered to the remote user. Each transport protocol, and each implementation of a 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 delivers the data in a single unit to the receiving user. Similarly, a message transmitted in two units can be delivered in a single unit to the remote transport user.

If supported by the transport, the message boundaries are preserved only by setting the value of T_MORE for t_snd() and testing it after t_rcv(). This guarantees that the receiver sees a message with the same contents and message boundaries as was sent.

Client

The example server transfers a log file to the client over the transport connection. The client receives the data and writes it to its standard output file. A byte stream interface is used by the client and server, with no message boundaries. The client receives data by the following:


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

The client repeatedly calls t_rcv() to receive incoming data. t_rcv() blocks until data arrives. t_rcv() writes up to nbytes of the data available into buf and returns the number of bytes buffered. The client writes the data to standard output and continues. The data transfer loop ends when t_rcv() fails. t_rcv() fails when an orderly release or disconnect request arrives. If fwrite() fails for any reason, the client exits, which closes the transport endpoint. If the transport endpoint is closed (either by exit() or t_close()) during data transfer, the connection is aborted and the remote user receives a disconnect request.

Server

The server manages its data transfer by spawning a child process to send the data to the client. The parent process continues the loop to listen for more connect requests. run_server() is called by the server to spawn this child process, as shown in Example 3-7.


Example 3-7 Spawning Child Process to Loopback and Listen


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 request - normal exit */
   exit(0);
}
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) == -1) {
         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) == -1) {
         t_error("t_close failed for listen_fd");
         exit(22);
      }
      if ((logfp = fopen("logfile", "r")) == (FILE *) NULL) {
         perror("cannot open logfile");
         exit(23);
      }
      signal(SIGPOLL, connrelease);
      if (ioctl(conn_fd, I_SETSIG, S_INPUT) == -1) {
         perror("ioctl I_SETSIG failed");
         exit(24);
      }
      if (t_look(conn_fd) != 0){      /*disconnect 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) == -1) {
            t_error("t_snd failed");
            exit(26);
         }

After the fork, the parent process returns to the main listening loop. The child process manages the newly established transport connection. If the fork fails, exit() closes both transport endpoints, sending a disconnect request 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 the data to the client using t_snd(). buf points to the start of the data buffer, and nbytes specifies the number of bytes to transmit. The fourth argument can be zero or one of the two optional flags below:

Neither flag is set by the server in this example.

If the user floods the transport provider with data, t_snd() blocks until enough data is removed from the transport.

t_snd() does not look for a disconnect request (showing that the connection was broken). If the connection is aborted, the server should be notified, since data can be lost. One solution is to call t_look() to check for incoming events before each t_snd() call or after a t_snd() failure. The example has a cleaner solution. The I_SETSIG ioctl() lets a user request a signal when a specified event occurs. See the streamio(7I) manpage. S_INPUT causes a signal to be sent to the user process when any input arrives at the endpoint conn_fd. If a disconnect request arrives, the signal-catching routine (connrelease()) prints an error message and exits.

If the server alternates t_snd() and t_rcv() calls, it can use t_rcv() to recognize an incoming disconnect request.