This section provides a detailed description of the separate phases of the connection-oriented service using an example program for the following simple client/server application:
The client and server carry out their local management tasks.
A connection is set up between the client and server.
The server transfers a file to the client. The client receives the file from the server and outputs it to its standard output.
The client and server shut the connection down.
The example program is described in separate program sections, where two program sections explain each phase of the connection-oriented service. One program section takes on the role of the client and the other the role of the server.
The program code used in the examples in this section is shown completely and coherently in the sections "Client in the connection-oriented service" and "Server in the connection-oriented service".
Local management using the example client/server model
Before the client and server can set up a communications connection, they must first each set up a local channel to the transport provider with t_open(). After this, they must each use t_bind() to make a local address known under which each can be reached via its assigned transport endpoint.
The user gets the various services offered by the transport interface with the t_open() call.
The services are built as follows: | |
Address | Maximum size of an address |
Options | Maximum number of bytes for protocol-specific options which the user can exchange with the transport provider |
tsdu | Maximum message size which can be transferred in the connection-oriented or connectionless services |
etsdu | Maximum number of bytes for expedited data which can be sent over a connection |
Connection setup (connect) | Maximum number of bytes for user data which can be exchanged during connection setup |
Connection shutdown | Maximum number of bytes of user data which can be transferred during connection shutdown |
Service type | Type of the service supported by the transport provider |
Three service types are defined: | |
T_COTS | The transport provider supports the connection-oriented service but does not allow an orderly connection shutdown. The connection can only be aborted. |
T_COTS_ORD | The transport provider supports the connection-oriented service and provides the option of an orderly connection shutdown (standard for XTI(POSIX) in the connection-oriented service). |
T_CLTS | The transport provider supports the connectionless service. |
The user receives the preset features of the transport endpoint with t_open(). If these are dynamic features, they may subsequently change. The user can obtain information on the current features of the transport endpoint with t_getinfo().
Once a user has set up a transport endpoint, he must pass the transport provider the address under which he can be reached via this endpoint. As described above, the user passes the transport endpoint address to the transport provider with t_bind(). With server stations, t_bind() also ensures that incoming connection requests can be processed by the transport provider and forwarded to the transport endpoint.
One additional function is available while the transport endpoint is being set up: the user can change features with t_optmgmt(). Each transport protocol is expected to provide its own set of changeable features. These can, for example, be parameters that affect the service quality. Because of the protocol-specific nature of these parameters, only applications for a special protocol environment will use this option.
The local management tasks are shown below using a client and a server as examples. The two examples contain the definitions and calls.
Local management by the client
#include <xti.h> #include <stdio.h> #include <fcntl.h> #include <netinet/in.h> #include <sys/socket.h> #define SRV_ADDR 0x7F000001 #define SRV_PORT 8888 main() { int fd; int nbytes; int flags = 0; char buf[1024]; struct t_call *sndcall; struct sockaddr_in *sin; if ((fd = t_open("/dev/tcp", O_RDWR, NULL)) < 0) { t_error("t_open() failed"); exit(1); } if (t_bind(fd, NULL, NULL) < 0) { t_error("t_bind() unsuccessful"); exit(2); }
The first parameter of t_open() is the path name of the device which provides the requested transport service. In this example, /dev/tcp is a device file and provides a connection-oriented transport protocol. This transport protocol is opened for read/write accesses by the second parameter. The user can employ the third parameter to get information on the available features. This information is required to create programs that are independent of protocols. To keep the example simple, this information is not accessed.
The client and server assume that the transport provider has the following features:
Support for service type T_COTS_ORD, which is used in the example for the orderly connection shutdown.
User data cannot be exchanged during connection setup or connection shutdown.
No protocol-specific features are provided.
Since these features are not needed by the user, NULL is passed as the third parameter in the t_open() call. A different device file must be opened if the user requires a service type other than T_COTS_ORD. An example for T_CLTS is shown in section "Connectionless service using an example transaction system".
t_open() returns an integer value, which is required in all further transport provider calls to identify the transport endpoint set up with t_open(). This integer value is a file descriptor.
After the transport endpoint has been set up, the user calls t_bind() to assign the transport endpoint an address. The first parameter of t_bind() identifies the transport endpoint and the second parameter describes the address which is to be bound to the transport endpoint. When t_bind() returns, the third parameter contains the actually bound address.
In contrast to the address of a server transport endpoint, which is needed by all clients to access the server, the address of a client does not have to be generally known. As no other process will try and access the address of a client, the client does not normally bother with its own address. This is shown in the above example in the t_bind() call where NULL is passed as the second and third parameters. If the second parameter is NULL, the transport provider assigns an address. The third NULL parameter means that the client is “not interested” in the address assigned by the transport provider.
If either t_open() or t_bind() is unsuccessful, t_error() is called to output an appropriate error message to stderr. If any of the transport provider functions should fail, the global integer variable t_errno is set to a corresponding value that indicates the error more closely. A number of such error values, and the t_errno variable itself, are defined in <xti.h> for the transport provider. t_error() outputs an error message according to the value of t_errno. This function works in the same way as the perror() function which outputs an error message according to the value of errno. If the error in the transport provider is a system error, t_errno is set to the value TSYSERR and errno is set to the appropriate system error value.
Local management by the server
The server in this example has to proceed in a similar manner before communications can be started. The server has to set up a transport endpoint which waits continuously for connection requests.
The definitions and calls required are as follows:
#include <xti.h> #include <stropts.h> #include <fcntl.h> #include <stdio.h> #include <signal.h> #include <netinet.in.h> #include <sys/socket.h> #define FILENAME "/etc/services" #define DISCONNECT -1 #define SRV_ADDR 0x7F000001 #define SRV_PORT 8888 int conn_fd; /* For the connection file descriptor */ main() { int listen_fd; /* File descriptor for * connection request */ struct t_bind *bind; struct t_call *call; struct sockaddr_in *sin; if ((listen_fd = t_open("/dev/tcp", O_RDWR, NULL)) < 0) { t_error("t_open() call for listen_fd failed."); exit(1); } if ((bind = (struct t_bind *)t_alloc(listen_fd, T_BIND, T_ALL)) == NULL) { t_error("t_alloc() for t_bind structure failed."); exit(2); } bind->qlen = 1; bind->addr.len=sizeof(struct sockaddr_in); sin=(struct sockaddr_in *)bind->addr.buf; sin->sin_family=AF_INET; sin->sin_port=htons(SRV_PORT); sin->sin_addr.s_addr=htonl(SRV_ADDR); if (t_bind(listen_fd, bind, bind) < 0) { t_error("t_bind() for listen_fd failed."); exit(3); }
Analogous to the client, the server also calls t_open() to set up a connection to the desired transport provider, i.e. the server sets up a transport endpoint (listen_fd). The server will use this transport endpoint listen_fd later when it calls the t_listen() function to wait for connection requests.
Before the server can use the t_bind() function to bind an address to the transport endpoint listen_fd, the server has to provide this address. The address is passed with the second parameter (bind) when t_bind() is called.
The bind parameter is a pointer to an object of data type struct t_bind. All structures and constants of the transport provider are declared/defined in <xti.h>.
The t_bind structure is declared in <xti.h> as follows:
struct t_bind { struct netbuf addr; unsigned qlen; };
bind->qlen defines the maximum number of allowed connection requests. If the value of bind->qlen is greater than 0, incoming connection requests can be processed with this transport endpoint. The server then puts incoming connection requests for the address provided in bind->addr into a queue. bind->qlen also defines the maximum number of requests that the server can process simultaneously. The server must reply to all requests by either accepting or refusing them. A connection request is pending if the server has not replied to it.
A server will often completely process one connection request and then the next. In this case, qlen should be set to the value 1. If a server wants to process several requests simultaneously, bind->qlen specifies the maximum number of requests which can be processed simultaneously.
Since the server in the example processes one connection request after the other, bind->qlen must be assigned the value 1. An example of a server that processes several requests simultaneously is shown in section "Managing multiple connections simultaneously and event-controlled operation".
addr has the data type struct netbuf and describes the address to be bound.
The netbuf structure is declared in <xti.h> as follows:
struct netbuf { unsigned int maxlen; unsigned int len; char *buf; };
buf is a pointer to a data buffer, len specifies the number of bytes in the buffer and maxlen specifies the maximum number of bytes that can be written into the buffer. The last entry is only required if data is transported from the transport provider to the user.
Calling t_alloc() reserves memory dynamically for a t_bind object. The first parameter of t_alloc() names the file descriptor which identifies the transport endpoint. The second parameter specifies the transport provider structure to be created, i.e. t_bind in this case. The third parameter specifies which components of this structure are to be created. T_ALL means that memory is to be reserved for all the components of the structure. This creates the addr buffer in the above example. The size of the buffer is determined by the transport provider, who defines a maximum address length. This length is in the maxlen component of the netbuf structure. Using t_alloc() ensures compatibility with future versions of the transport provider.
The data is interpreted as an address with objects of type struct t_bind. It is generally assumed that the structure of an address differs from protocol to protocol. The structure of netbuf is created such that all protocols can be supported.
Finally, the address information is assigned to the new t_bind object. In the example, the address itself is structured according to the Internet communications domain address structure (see struct sockaddr_in in section "sockaddr_in address structure of the AF_INET address family").
The server now binds the address created above to the transport endpoint listen_fd with the t_bind() function. After the t_bind() call has been successfully executed, the server can be accessed by any client via this address. The transport provider puts incoming connection requests into a queue and this initiates the next phase of the connection setup protocol, the actual connection setup.
Connection setup using the example client/server model
The connection setup illustrates the difference between the client and server. The transport provider makes different, special functions available to each of them. The client calls t_connect() to request a connection while the server uses t_listen() to wait for connection requests. The server can either accept a connection with the t_accept() function or refuse it with t_snddis(). The client is informed of the decision of the transport provider when the t_connect() function terminates.
Connection request by the client
To continue with the client/server example, the following steps are required for connection setup from the viewpoint of the client:
if ((sndcall = (struct t_call *)t_alloc(fd, T_CALL, T_ADDR)) == NULL) { t_error("t_alloc() failed"); exit(3); } sndcall->addr.len=sizeof(struct sockaddr_in); sin=(struct sockaddr_in *)sndcall->addr.buf; sin->sin_family=AF_INET; sin->sin_port=htons(SRV_PORT); sin->sin_addr.s_addr=htonl(SRV_ADDR); if (t_connect(fd, sndcall, NULL) < 0) { t_error("t_connect() for fd failed"); exit(4); }
Before the client can send a connection request to the server with t_connect(), the client must specify the address of the server. This address is then passed as the second parameter (sndcall) with the t_connect() call.
The sndcall parameter is a pointer to an object of data type struct t_call.
The t_call structure is declared in <xti.h> as follows:
struct t_call { struct netbuf addr; struct netbuf opt; struct netbuf udata; int sequence; };
t_alloc() is used in the example to set up a t_call object dynamically. No features or user data are specified in the above example. Only the server address is used. T_ADDR is selected as the third parameter of t_alloc() to set up an appropriate buffer for the address information.
After t_alloc() has been successfully executed, the server deposits the server address and its length into the memory area reserved by t_alloc(). The server address is thereby structured according to the address structure of the Internet communications domain (see struct sockaddr_in in section "sockaddr_in address structure of the AF_INET address family").
The t_connect() call sends a connection request to the server. The first parameter of the call is the transport endpoint over which the connection is to be set up. The address of the desired server is passed with the second parameter (sndcall). The third parameter is also a pointer to an object of type struct t_call. This t_connect() parameter is used to get information on the established connection. Since this information is not needed here, NULL is passed as the third parameter in the example. If t_connect() is successful, the connection is set up. If the server refuses the connection request, t_errno is set to the value TLOOK.
The TLOOK error has a special significance for the transport interface: TLOOK informs the user if an interface function was interrupted by an unexpected asynchronous event on the specified transport endpoint. TLOOK therefore does not indicate an interface error, but only that the called function is not executed because of the pending event. The defined transport interface events are described in section"States and state transitions".
The user can determine which event has occurred when a TLOOK error is reported, with the t_look() function. If the connection request is refused in the above example, the client receives a message about the aborted connection. The program is terminated in this case.
Connection acceptance by the server
When the client requests a connection with t_connect(), a corresponding event is set at the transport endpoint of the server. The steps required for handling this event are shown below. For each client, the server accepts the request and creates a new process to manage the connection.
if ((call = (struct t_call *)t_alloc(listen_fd, T_CALL, T_ADDR)) == NULL){ t_error("t_alloc() for t_call structure failed"); exit(5); } while (1) { if (t_listen(listen_fd, call) < 0) { t_error("t_listen for listen_fd failed"); exit(6); } if ((conn_fd = accept_call(listen_fd, call)) != DISCONNECT) run_service(listen_fd); } }
The server uses t_alloc() to set up an object of type struct t_call that is required by t_listen(). The third parameter of t_alloc(), T_ADDR, causes the buffer for the address of the client to be created.
The value of maxlen in a netbuf object specifies the actual length of the created buffer.
The server runs in an endless loop and processes one incoming connection request in each loop run. The server thereby proceeds as follows:
The server calls the t_listen() function to wait for connection requests that arrive at the transport endpoint listen_fd. The transport address of the sender of a connection request is stored by t_listen() in the t_call object to which the pointer variable call points.If no connection requests are pending, the t_listen() function blocks the process until a connection request arrives.
When a connection request arrives, the server calls the user-defined accept_call() function to confirm the connection. accept_call() accepts the connection request on a new transport endpoint and returns the relevant file descriptor as the result. This file descriptor is stored in the global conn_fd variable. Since the connection is set up on a new transport endpoint, the server can wait for further requests on the old transport endpoint. The accept_call() function is described in detail below.
If the connection acceptance was successful, the run_service() function creates a new process to manage the connection. The user-defined run_service() function is described in detail in section "Connection-oriented client/server model".
The transport interface supports an asynchronous mode and this is described in section "Advanced XTI(POSIX) concepts".
The accept_call() function, which the server calls to accept a connection request, is defined as follows:
accept_call(listen_fd, call) int listen_fd; struct t_call *call; { int resfd; struct t_call *refuse_call; if ((resfd = t_open("/dev/tcp", O_RDWR, NULL)) < 0) { t_error(„t_open() call for accept failed”); exit(7); } while (t_accept(listen_fd, resfd, call) < 0) { if (t_errno == TLOOK) { if (t_look(listen_fd) == T_DISCONNECT) { /* Connection abort */ 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); } /* Terminate call and wait for further calls */ return(DISCONNECT); } else { /* new T_LISTEN; delete event */ if ((refuse_call = (struct t_call *)t_alloc(listen_fd,T_CALL,0)) == NULL) { t_error("t_alloc() for refuse_call failed"); exit(11); } if (t_listen(listen_fd, refuse_call) < 0) { t_error("t_listen() for refuse_call failed"); exit(12); } if (t_snddis(listen_fd, refuse_call) < 0) { t_error("t_snddis() for refuse_call failed"); exit(13); } if (t_free((char *)refuse_call, T_CALL) < 0) { t_error("t_free() for refuse_call failed"); exit(14); } } } else { t_error("t_accept() failed"); exit(15); } } return(resfd); }
The accept_call() call needs two parameters:
listen_fd specifies the transport endpoint on which the connection request arrived.
call is a pointer to an object of data type struct t_call that contains all the information for these requests.
The t_call() function first creates an additional transport endpoint. This new transport endpoint resfd is used to accept the connection request.
The t_accept() function accepts the connection request. The first parameter of the t_accept() function specifies the transport endpoint on which the request was received. The second parameter specifies the transport endpoint on which the request is to be confirmed.
A request can be confirmed on the same transport endpoint on which it was received. In this case, other clients cannot make any requests for the duration of this connection.
The third parameter of t_accept() points to the t_call object of the currently processed connection request. This object should contain the address of the calling client and the sequential number of the t_listen() call. The value of call->sequence is significant if the server manages several connections. You will find an appropriate example in section "Event-controlled server".
To keep this example simple, the server terminates the program if the t_open() call fails.exit(2) closes the transport endpoint assigned to listen_fd. The transport provider thereby sends the client a message to the effect that the connection was aborted and the connection request was unsuccessful. The t_connect() call fails and t_errno is set to TLOOK.
t_accept() execution can fail if an asynchronous event occurs on the receiving transport endpoint before the connection is accepted. t_errno is then set to TLOOK. The table "TLOOK error events" in section "States and state transitions" shows that precisely one of the two following events can arrive:
An abort message has arrived for the previously reported connection request, i.e. the client who sent the connection request wants to abort the connection.
When an abort request arrives, the server must immediately use a t_rcvdis() call to analyze the reason for the request. The t_rcvdis() function has a parameter which is a pointer to an object of data type t_discon (see "t_rcvdis() - get the cause of a connection shutdown"). The t_discon object is required to store the abort condition. The reason for the abort is not queried in this example and the parameter is therefore set to NULL. After the abort condition is received, accept_call() closes the transport endpoint and returns a DISCONNECT as its result. This informs the server that the connection was closed by the client.
A new connection request arrived during execution of t_accept().
In this example, the server refuses this connection request in order to be able to accept the currently processed connection request without interruption. The server thereby proceeds as follows:
The server creates a new object of type struct t_call with t_alloc() .
The server then accepts the new connection request with t_listen() which returns a unique ID for the new connection request in the refuse_call->sequence field.
The server refuses the new connection request with t_snddis().
The server repeats the t_accept() call after releasing the t_call object referenced by refuse_call.
The transport connection has been set up with the newly created transport endpoint. This allows the receive endpoint to handle new connection requests.
Data transfer using the example client/server model
Once the connection has been set up, the client and server can start exchanging data. They use the t_snd() and t_rcv() functions for this. From this point on, the transport provider does not distinguish between the client and server. Each user can send and receive data or close the connection. The transport provider offers secured data transfer and maintains the order of sending over an established connection.
In the example, the server sends one file to the client over the established connection.
Data sending by the server
The server organizes the data transfer by creating a new process which sends the data to the client. The parent process waits for further connection requests while the child process transfers the data.
The run_service() function is called to create this child process. The following extract from the definition of run_service() illustrates this procedure:
run_service(listen_fd) int listen_fd; { int nbytes; FILE *logfp; /* Pointer to the protocol file */ char buf[1024]; switch (fork()) { case -1: perror("fork failed"); exit(20); break; default: /* Parent process */ /* Close conn_fd and terminate the function */ if (t_close(conn_fd) < 0) { t_error("t_close() failed for conn_fd"); exit(21); } return; case 0: /* Child */ /* Close listen_fd and transfer the file */ if (t_close(listen_fd) < 0) { t_error("t_close() failed for listen_fd"); exit(22); } if (t_look(conn_fd) != 0) { /* Has connection abort arrived? */ 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 loop and waits for new connection requests.
The child process manages the new connection in the meantime. If the fork() call fails, exit() closes the established connection and sends an abort message to the client. This causes the t_connect() call of the client to fail.
The child process reads 1024 bytes of the protocol file and sends the data with the t_snd() call to the client. buf points to the start of the data buffer and nbytes defines the number of characters to be transferred.
If the user makes too much data available to the transport provider for transfer, the transport provider can refuse acceptance to ensure correct flow control. In this case, the t_snd() call blocks until the flow control is released again and the transfer can proceed. The t_snd() call is then not terminated until the transport provider is passed as many characters as defined by the nbytes variable.
The t_snd() function does not check whether an abort request arrived until the data is passed to the transport provider. Because the data flow is in just one direction it is also not possible for the user to handle incoming events. If, for example, the connection is interrupted, the user should be informed that data could be lost. The user can call t_look() before each t_snd() call to check whether incoming events arrived.
Data reception by the client
In the example, the server transfers a file to the client over the established connection. The client receives the file and directs it to the standard output. The client uses the following program section to receive the data:
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 calls the t_rcv() function to receive the incoming data. If no data is available, the process is blocked by the t_rcv() call until data is available. t_rcv() then returns the number of bytes in the receive buffer buf (maximum 1024). The client then writes the received data to the standard output. The data transfer is terminated when the t_rcv() call fails, which happens if a connection shutdown request is received. This is explained in more detail on the following page.
If the fwrite() call fails, the program is terminated and the transport endpoint is closed. Closing a transport endpoint (with exit() or t_close()) in the data transfer phase causes a connection abort and the communications partner receives an abort message.
Connection shutdown using the example client/server model
As already mentioned, there are two different forms of connection shutdown that can be supported by the transport provider.
The abortive connection release terminates a connection immediately. This can lead to loss of data if all data has not reached the receiver.
Any user can initiate such an abort by calling the t_snddis() function. If problems occur within the transport provider, the transport provider can also initiate a connection abort.
When the abort message reaches the receiver, he has to call the t_rcvdis() function to receive the message. t_rcvdis() returns a value which defines the reason for the abort as a result. This value is dependent on the transport provider used and should not be interpreted by protocol-independent programs.
The orderly connection shutdown terminates a connection only after all data has been transferred.
All transport providers must support the first variant, i.e. abortive connection release. In the example, it is implied that the transport provider also allows the orderly connection shutdown.
Connection shutdown by the server
Once all data has been transferred, the server can initiate an orderly connection shutdown as follows:
if (t_sndrel(conn_fd) < 0) { t_error("t_sndrel() failed"); exit(27); }
The connection is only shut down after both ends have sent a shutdown request and each has received a confirmation (see section "Connection-oriented service phases").
Connection shutdown by the client
The connection shutdown progresses in the same way from the viewpoint of the client as it does from the viewpoint of the server. As already mentioned, the client receives data until the t_rcv() call fails. If the server calls either t_snddis() or t_sndrel(), the t_rcv() call fails and t_errno is set to T_LOOK. The client handles this condition 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 arrives at the transport endpoint of the client, the client checks whether the expected request for orderly shutdown has arrived. If it has, the client calls t_rcvrel() to receive the request. The client then calls t_sndrel(). This indicates to the server that the client is also ready to shut the connection down. At this point, the client program is terminated, also causing the transport endpoint to be closed.
If the transport provider does not support the orderly connection shutdown discussed above, the users must employ the abortive connection release. The users themselves are then responsible for ensuring that the connection shutdown does not cause data to be lost. For example, a specific combination of bytes can be used to indicate that the connection is to be terminated. There are many ways of preventing data loss. Each application and each higher protocol must have a mechanism that adjusts itself to the prevailing transport environment.