![]() |
|
| << Previous | Index | Next >> | |
| | |
TCP (Transmission Control Protocol) and UDP (User Datagram Protocol) are both transport layer protocols. TCP is used when a reliable, stream-oriented, transport is required for data flowing between two hosts on a network. UDP is a record-oriented protocol which is used when lower overhead is more important than reliability. The acronym UDP is sometimes expanded as "unreliable datagram protocol" although, in practice, UDP is quite reliable especially over a local Ethernet LAN segment.
The Dynamic C TCP/IP libraries implement TCP and UDP over IP (Internet Protocol). IP is a network layer protocol, that in turn uses lower levels known as "link layer" protocols, such as Ethernet and PPP (Point-to-Point Protocol). The link-layer protocols depend on a physical layer, such as 10BaseT for Ethernet, or asynchronous RS232 for PPP over serial.
In the other direction, various protocols use TCP. This includes the familiar protocols HTTP, SMTP (mail) and FTP. Other protocols use UDP: DNS and SNMP to name a couple. TCP handles a lot of messy details which are necessary to ensure reliable data flow in spite of possible deficiencies in the network, such as lost or re-ordered packets. For example, TCP will automatically retransmit data that was not acknowledged by the peer within a reasonable time. TCP also paces data transmission so that it does not overflow the peer's receive buffers (which are always finite) and does not overload intermediate nodes (routers) in the network. UDP leaves all of these details to the application, however UDP has some benefits that TCP cannot provide: one benefit is that UDP can "broadcast" to more than one peer, and another is that UDP preserves the concept of "record boundaries" which can be useful for some applications.
TCP is a connection-oriented protocol. Two peers establish a TCP connection, which persists for the exclusive use of the two parties until it is mutually closed (in the usual case). UDP is connectionless. There is no special start-up or tear-down required for UDP communications. You can send a UDP packet at any time to any destination. Of course, the destination may not be ready to receive UDP packets, so the application has to handle this possibility. (In spite of being "connectionless," we still sometimes refer to UDP "connections" or "sessions" with the understanding that the connection is a figment of your application's imagination.)
This chapter describes how to implement your own application level protocols on top of TCP or UDP. The Dynamic C TCP/IP libraries can also be examined for further hints as to how to code your application. For example,
HTTP.LIBcontains the source for an HTTP web server.3.1 What is a Socket?
Both TCP and UDP make extensive use of the term "socket." A TCP socket represents the connection state between the local host and the remote peer. When talking about TCP connections that traverse the Internet, a socket is globally unique because it is described by 4 numbers: the local and remote IP addresses (32 bits each), and the local and remote port numbers (16 bits each).
Connections that do not traverse the Internet (e.g., between two hosts on an isolated LAN) are still unique within the attached network.
UDP sockets do not have the global uniqueness property, since they are not connection-oriented. For UDP, a socket really refers to just the local side.
For practical purposes, a socket is a structure in RAM that contains all the necessary state information. TCP sockets are considerably larger than UDP sockets since there is more connection state information to maintain. TCP sockets also require both a receive and a transmit buffer, whereas UDP sockets require only a receive buffer.
With Dynamic C version 6.57, each socket must have an associated
tcp_Socketstructure of 145 bytes or audp_Socketstructure of 62 bytes. The I/O buffers are in extended memory. For Dynamic C 7.30 these sizes are 136 bytes and 44 bytes, respectively.For earlier versions of Dynamic C (than 6.57), each socket must have a
tcp_Socketdata structure that holds the socket state and I/O buffers. These structures are, by default, around 4200 bytes each. The majority of this space is used by the input and output buffers.3.1.1 Port Numbers
Both TCP and UDP sockets make use of port numbers. Port numbers are a convenient method of allowing several simultaneous connections to exist between the same two hosts. Port numbers are also used to provide "well-known" starting points for common protocols. For example, TCP port number 23 is used for standard telnet connections. In general, port numbers below 1024 are used for standard services. Numbers between 1024 and 65535 are used for connections of a temporary nature. Often, the originator of a connection will select one of the temporary port numbers for its end of the connection, with the well-known number for the other end (which is often some sort of "server").
TCP and UDP port numbers are not related and operate in an independent "space." However, the well-known port numbers for TCP and UDP services often match if the same sort of protocol can be made to run over TCP or UDP.
When you open a socket using the TCP/IP libraries, you can specify a particular port number to use, or you can allow the library to pick a temporary port number for an "ephemeral" connection.
3.2 Allocating TCP and UDP Sockets
In all versions of Dynamic C, TCP and UDP socket structures
mustbe allocated in static data storage. This is simply accomplished by declaring a static variable of type tcp_Socket or udp_Socket:static tcp_Socket my_sock;
static udp_Socket my_udp_sock_array[20];3.2.1 Allocating Socket Buffers
Starting with Dynamic C version 7.05, there are two macros that define the number of sockets available. These macros do not determine how many sockets you can allocate, but they do limit how many sockets you can successfully use. Each socket requires some resources which are not automatically available just because you declare a tcp_Socket structure. The additional resources are receive/transmit buffers (which are allocated in extended memory), and also socket semaphores if you are using µC-OS/II. The relevant macros are:
MAX_TCP_SOCKET_BUFFERS
Determines the maximum number of TCP sockets with preallocated buffers. The default is 4. A buffer is tied to a socket with the first call to
tcp_open()ortcp_listen(). If you usetcp_extopen()ortcp_extlisten()then these buffer resources are not used up, but only if you allocate your own buffers usingxalloc().MAX_UDP_SOCKET_BUFFERS
Determines the maximum number of UDP sockets with preallocated buffers. The default is 0. A buffer is tied to a socket with the first call to
udp_open(). If you useudp_extopen()then these buffer resources are not used up, but only if you allocate your own buffers usingxalloc().Note that DNS does not need a UDP socket buffer since it manages its own buffer. Prior to version 7.30, DHCP and
TFTP.LIBeach need one UDP socket buffer. Starting with version 7.30, DHCP manages its own socket buffers.Prior to Dynamic C version 7.05,
MAX_SOCKETSdefined the number of sockets that could be allocated, not including the socket for DNS lookups. If you use libraries such asHTTP.LIBorFTP_SERVER.LIB, you must provide enough sockets inMAX_SOCKETSfor them also.In Dynamic C 7.05 (and later), if
MAX_SOCKETSis defined in an application program,MAX_TCP_SOCKET_BUFFERSwill be assigned the value ofMAX_SOCKETS.If you are using µC-OS/II then there is a further macro which must be set to the correct value:
MAX_SOCKET_LOCKS. This must count every socket (TCP plus UDP), including those used internally by the libraries. If you cannot calculate this exactly, then it is best to err on the side of caution by overestimating. The actual socket semaphore structure is not all that big (less than 70 bytes).The default value for
MAX_SOCKET_LOCKSis the sum ofMAX_TCP_SOCKET_BUFFERSandMAX_UDP_SOCKET_BUFFERS(plus 1 if DNS is being used).3.2.2 Socket Buffer Sizes
Starting with Dynamic C version 7.05, TCP and UDP I/O buffers are sized separately using:
TCP_BUF_SIZE
Determines the TCP buffer size. Defaults to 4096 bytes.
UDP_BUF_SIZE
Determines the UDP buffer size. Defaults to 4096 bytes.
Compatibility is maintained with earlier versions of Dynamic C. If
SOCK_BUF_SIZEis defined,TCP_BUF_SIZEandUDP_BUF_SIZEwill be assigned the value ofSOCK_BUF_SIZE. IfSOCK_BUF_SIZEis not defined, buttcp_MaxBufSizeis, thenTCP_BUF_SIZEandUDP_BUF_SIZEwill be assigned the value oftcp_MaxBufSize* 2.3.2.2.1 User-Supplied Buffers
Starting with Dynamic C version 7.05, a user can associate his own buffer with a TCP or UDP socket. The memory for the buffer must be allocated by the user. This can be done with
xalloc(), which returns a pointer to the buffer. This buffer will be tied to a socket by a call to an extended open function:tcp_extlisten(),tcp_extopen()orudp_extopen(). Each function requires a long pointer to the buffer and its length be passed as parameters.3.3 Opening TCP Sockets
There are two ways to open a TCP socket, passive and active. Passive open means that the socket is made available for connections originated from another host. This type of open is commonly used for Internet servers that listen on a well-known port, like 80 for HTTP (Hypertext Transfer Protocol) servers. Active open is used when the controller board is establishing a connection with another host which is (hopefully) listening on the specified port. This is typically used when the controller board is to be a "client" for some other server.
The distinction between passive and active open is lost as soon as the connection is fully established. When the connection is established, both hosts operate on a peer-to-peer basis. The distinction between who is "client" and who is "server" is entirely up to the application. TCP itself does not make a distinction.
3.3.1 Passive Open
To passively open a socket, call
tcp_listen()ortcp_extlisten(); then wait for someone to contact your device. You supply the listen function with a pointer to atcp_Socketdata structure, the local port number others will be contacting on your device, and possibly the IP address and port number that will be acceptable for the peer. If you want to be able to accept connections from any IP address or any port number, set one or both to zero.To handle multiple simultaneous connections, each new connection will require its own
tcp_Socketand a separate call to one of the listen functions, but using the same local port number (lportvalue). The listen function will immediately return, and you must poll for the incoming connection. You can manually poll the socket usingsock_established(). The proper procedure for fielding incoming connections is described below.3.3.2 Active Open
When your Web browser retrieves a page, it actively opens one or more connections to the server's passively opened sockets. To actively open a connection, call
tcp_open()ortcp_extopen(), which use parameters that are similar to the ones used in the listen functions. Supply exact parameters forremipandport, which are the IP address and port number you want to connect to; thelportparameter can be zero, causing an unused local port between 1024 and 65535 to be selected.If the open function returns zero, no connection was made. This could be due to routing difficulties, such as an inability to resolve the remote computer's hardware address with ARP. Even if non-zero is returned, the connection will not be immediately established. You will need to check the socket status as described in the next section.
3.3.3 Waiting for Connection Establishment
When you open a TCP socket either passively or actively, you must wait for a complete TCP connection to be established. This is technically known as the "3-way handshake." As the name implies, at least 3 packets must be exchanged between the peers. Only after completion of this process, which takes at least one round-trip time, does the connection become fully established such that application data transfer can proceed.
Unfortunately, the 3-way handshake may not always succeed: the network may get disconnected; the peer may cancel the connection; or the peer might even crash. The handshake may also complete, but the peer could immediately close or cancel the connection. These possibilities need to be correctly handled in a robust application. The consequences of not doing this right include locked-up sockets (i.e., inability to accept further connections) or protocol failures.
The following code outlines the correct way to accept connections, and to recover in case of errors.
Notice the
tcp_tick(&my_socket)call inside the while loop. This is necessary in order to test whether the handshake was aborted by the peer, or timed out. At the end of the loop,sock_established()tests whether the handshake did indeed complete. If so, then the socket is ready for data flow. Otherwise, the socket should be re-opened. The same basic procedure applies for passively opened sockets (i.e.,tcp_listen()).3.3.4 Specifying a Listen Queue
A
tcp_Socketstructure can handle only a single connection at any one time. However, a passively opened socket may be required to handle many incoming connection requests without undue delay. To help smoothly process successive connection requests with a single listening socket, you can specify that certain TCP port numbers have an associated "pending connection" queue. If there is no queue, then incoming requests will be cancelled if the socket is in use. If there is a queue, then the new connections will be queued until the current active connection is terminated.To accept new connection requests when the passively opened socket is currently connected, use the function
tcp_reserveport(). It takes one parameter, the port number where you want to accept connections. When a connection to that port number is requested, the 3-way handshaking is done even if there is not yet a socket available. When replying to the connection request, the window parameter in the TCP header is set to zero, meaning, "I can take no bytes of data at this time." The other side of the connection will wait until the value in the window parameter indicates that data can be sent. Using the companion function,tcp_clearreserve(port number), causes TCP/IP to treat a connection request to the port in the conventional way. The macroUSE_RESERVEDPORTSis defined by default. It allows the use of these two functions.When using
tcp_reserveport(), the 2MSL (maximum segment lifetime) waiting period for closing a socket is avoided.3.4 TCP Socket Functions
There are many functions that can be applied to an open TCP socket. They fall into three main categories: Control, Status, and I/O.
3.4.1 Control Functions for TCP Sockets
These functions change the status of the socket or its I/O buffer.
The open and listen functions have been explained in previous sections.
Call
sock_close()to end a connection. This call may not immediately close the connection because it may take some time to send the request to end the connection and receive the acknowledgements. If you want to be sure that the connection is completely closed before continuing, calltcp_tick()with the socket structure's address. Whentcp_tick()returns zero, then the socket is completely closed. Please note that if there is data left to be read on the socket, the socket will not completely close.Call
sock_abort()to cancel an open connection. This function will cause a TCP reset to be sent to the other end, and all future packets received on this connection will be ignored.For performance reasons, data may not be immediately sent from a socket to its destination. If your application requires the data to be sent immediately, you can call
sock_flush(). This function will try sending any pending data immediately. If you know ahead of time that data needs to be sent immediately, callsock_flushnext()on the socket. This function will cause the next set of data written to the socket to be sent immediately, and is more efficient thansock_flush().3.4.2 Status Functions for TCP Sockets
These functions return useful information about the status of either a socket or its I/O buffers.
tcp_tick()is the daemon that drives the TCP/IP stack, but it also returns status information. When you supplytcp_tick()with a pointer to atcp_Socket(a structure that identifies a particular socket), it will first process packets and then check the indicated socket for an established connection.tcp_tick()returns zero when the socket is completely closed. You can use this return value after callingsock_close()to determine if the socket is completely closed.
sock_close(&my_socket);while(tcp_tick(&my_socket)) {// you can do other things here while waiting for the socket to be completely closed
}
The status functions can be used to avoid blocking when using
sock_write()and some of the other I/O functions. As illustrated in the following code, you can make sure that there is enough room in the buffer before adding data with a blocking function.
if(sock_tbleft(&my_socket,size)) {
sock_write(&my_socket,buffer,size);
}
The following block of code ensures that there is a string terminated with a new line in the buffer, or that the buffer is full before calling
sock_gets():
sock_mode(&my_socket,TCP_MODE_ASCII);if(sock_bytesready(&my_socket) != -1) {
sock_gets(buffer,MAX_BUFFER);
}3.4.3 I/O Functions for TCP Sockets
These functions handle all I/O for a TCP socket.
There are two modes of reading and writing to TCP sockets: ASCII and binary. By default, a socket is opened in binary mode, but you can change the mode with a call to
sock_mode().When a socket is in ASCII mode, it is assumed that the data is an ASCII stream with record boundaries on the newline characters for some of the functions. This behavior means
sock_bytesready()will return ≥0 only when a complete newline-terminated string is in the buffer or the buffer is full. Thesock_puts()function will automatically place a newline character at the end of a string, and thesock_gets()function will strip the newline character.Do not use
sock_gets()in binary mode.3.5 UDP Socket Overview
The UDP protocol is useful when sending messages where either a lost message does not cause a system failure or is handled by the application. Since UDP is a simple protocol and you have control over the retransmissions, you can decide if you can trade low latency for high reliability.
Broadcast Packets
UDP can send broadcast packets (i.e., to send a packet to a number of computers on the same network). This is accomplished by setting the remote IP address to -1, in either a call to
udp_open()or a call toudp_sendto(). When used properly, broadcasts can reduce overall network traffic because information does not have to be duplicated when there are multiple destinations.Checksums
There is an optional checksum field inside the UDP header. This field verifies the header and the data. This feature can be disabled on a reliable network where the application has the ability to detect transmission errors. Disabling the UDP checksum can increase the performance of UDP packets moving through the TCP/IP stack. This feature can be modified by:
sock_mode(s, UDP_MODE_CHK); // enable checksums
sock_mode(s, UDP_MODE_NOCHK); // disable checksumsThe first parameter is a pointer to the socket's data structure, either
tcp_Socketorudp_Socket.In Dynamic C version 7.20, some convenient macros offer a safer, faster alternative to using
sock_mode(). They areudp_set_chk(s)andudp_set_nochk(s).Improved Interface
With Dynamic C version 7.05 there is a redesigned UDP API. The new interface is incompatible with the previous one. Section 3.6 covers the new interface and Section 3.7 covers the previous one. See Section 3.7.5 for information on porting an older program to the new UDP interface.
3.6 UDP Socket Functions (7.05 and later)
Starting with Dynamic C 7.05, the UDP implementation is a true record service. It receives distinct datagrams and passes them as such to the user program. The socket I/O functions available for TCP sockets will no longer work for UDP sockets.
3.6.1 Control Functions for UDP Sockets
These functions change the status of the socket or its I/O buffer.
3.6.2 Status Function for UDP Sockets
These functions return useful information about the status of either a socket or its I/O buffers.
For a UDP socket,
sock_bytesready()returns the number of bytes in the next datagram in the socket buffer, or -1 if no datagrams are waiting. Note that a return of 0 is valid, since a datagram can have 0 bytes of data.3.6.3 I/O Functions for UDP Sockets
These functions handle datagram-at-a-time I/O:
The write function,
udp_sendto(), allows the remote IP address and port number to be specified. The read function,udp_recvfrom(), identifies the IP address and port number of the host that sent the datagram. There is no longer a UDP read function that blocks until data is ready.3.7 UDP Socket Functions (pre 7.05)
This interface is basically the TCP socket interface with some additional functions for simulating a record service. Some of the TCP socket functions work differently for UDP because of its connectionless state. The descriptions for the applicable functions detail these differences.
3.7.1 I/O Functions for UDP Sockets
Prior to Dynamic C 7.05, the functions that handle UDP socket I/O are mostly the same functions that handle TCP socket I/O.
Notice that there are three additional I/O functions that are only available for use with UDP sockets:
sock_recv(),sock_recv_from()andsock_recv_init(). The status and control functions that are available for TCP sockets also work for UDP sockets, with the exception of the open functions,tcp_listen()andtcp_open().3.7.2 Opening and Closing a UDP Socket
udp_open()takes a remote IP address and a remote port number. If they are set to a specific value, all incoming and outgoing packets are filtered on that value (i.e., you talk only to the one remote address).If the remote IP address is set to -1, the UDP socket receives packets from any valid remote address, and outgoing packets are broadcast. If the remote IP address is set to 0, no outgoing packets may be sent until a packet has been received. This first packet completes the socket, filling in the remote IP address and port number with the return address of the incoming packet. Multiple sockets can be opened on the same local port, with the remote address set to 0, to accept multiple incoming connections from separate remote hosts. When you are done communicating on a socket that was started with a 0 IP address, you can close it with
sock_close()and reopen to make it ready for another source.3.7.3 Writing to a UDP Socket
Prior to Dynamic C 7.05, the normal socket functions used for writing to a TCP socket will work for a UDP socket, but since UDP is a significantly different service, the result could be different. Each atomic write--
sock_putc(),sock_puts(),sock_write(), orsock_fastwrite()--places its data into a single UDP packet. Since UDP does not guarantee delivery or ordering of packets, the data received may be different either in order or content than the data sent. Packets may also be duplicated if they cross any gateways. A duplicate packet may be received well after the original.3.7.4 Reading From a UDP Socket
There are two ways to read UDP packets prior to Dynamic C 7.05. The first method uses the same read functions that are used for TCP:
sock_getc(),sock_gets(),sock_read(), andsock_fastread(). These functions will read the data as it came into the socket, which is not necessarily the data that was written to the socket.The second mode of operation for reading uses the
sock_recv_init(),sock_recv(), andsock_recv_from()functions. Thesock_recv_init()function installs a large buffer area that gets divided into smaller buffers. Whenever a datagram arrives, it is stuffed into one of these new buffers. Thesock_recv()andsock_recv_from()functions scan these buffers. After callingsock_recv_initon the socket, you should not usesock_getc(),sock_read(), orsock_fastread().The
sock_recv()function scans the buffers for any datagrams received by that socket. If there is a datagram, the length is returned and the user buffer is filled, otherwisesock_recv()returns zero.The
sock_recv_from()function works likesock_recv(), but it allows you to record the IP address where the datagram originated. If you want to reply, you can open a new UDP socket with the IP address modified bysock_recv_from().3.7.5 Porting Programs from the older UDP API to the new UDP API
To update applications written with the older-style UDP API, use the mapping information in the following table.
3.8 Skeleton Program
The following program is a general outline for a Dynamic C TCP/IP program. The first couple of defines set up the default IP configuration information. The "memmap" line causes the program to compile as much code as it can in the extended code window. The "use" line causes the compiler to compile in the Dynamic C TCP/IP code using the configuration data provided above it.
Program Name: Samples\tcpip\icmp\pingme.c
To run this program, start Dynamic C and open the
Samples\TCPIP\ICMP\PINGME.Cfile. If you are using a Dynamic C version prior to 7.30, edit theMY_IP_ADDRESS,MY_NETMASK, andMY_GATEWAYmacros to reflect the appropriate values for your device. Otherwise, edit yourtcpconfig.lib(orcustom_config.lib) file with appropriate network addresses for your device and defineTCPCONFIGto access the desired configuration information.Run the program and try to run
ping 10.10.6.101from a command line on a computer on the same physical network, replacing10.10.6.101with your value forMY_IP_ADDRESS.3.8.1 TCP/IP Stack Initialization
The
main()function first initializes the TCP/IP stack with a call tosock_init(). This call initializes internal data structures and enables the Ethernet chip, which will take a couple of seconds with the RealTek chip. At this point, the TCP/IP stack is ready to handle incoming packets.3.8.2 Packet Processing
Incoming packets are processed whenever
tcp_tick()is called. The user-callable functions that calltcp_tick()are:tcp_open,udp_open,sock_read,sock_write,sock_close, andsock_abort. Some of the higher-level protocols, e.g.,HTTP.LIBwill calltcp_tick()automatically.Call
tcp_tick()periodically in your program to ensure that the TCP/IP stack has had a chance to process packets. A rule of thumb is to calltcp_tick()around 10 times per second, although slower or faster call rates should also work. The Ethernet interface chip has a large buffer memory, and TCP/IP is adaptive to the data rates that both ends of the connection can handle; thus the system will generally keep working over a wide variety of tick rates.3.9 TCP/IP Daemon: tcp_tick()
tcp_tick()is a fundamental function for the TCP/IP library. It has two uses: it drives the "background" processing necessary to maintain up-to-date information; and it may also be used to test TCP socket state. The latter use is described in the next section.Note that
tcp_tick()does more than just TCP processing: it is also necessary for UDP and other internal protocols such as ARP and ICMP. It also (as of Dynamic C 7.30) controls interface status.The computing time consumed by each call to
tcp_tick()varies. Rough numbers are less than a millisecond if there is nothing to do, tens of milliseconds for typical packet processing, and hundreds of milliseconds under exceptional circumstances. In general, the more active sockets that are in use simultaneously, the longer it will take fortcp_tick()to complete, however there is not much increase for reasonable numbers of sockets.It is recommended that you call
tcp_tick()at the head of the main application processing loop. If you have any other busy-wait loops in your application, you should arrange fortcp_tick() to be called in each such loop. TCP/IP library functions that are documented as "blocking" will always include calls totcp_tick(), so you do not have to worry about it. Library functions which are documented as "non-blocking" (e.g.,sock_fastread()) do not in general calltcp_tick(), so your application will need to do it.Some of the provided application protocols (such as HTTP and FTP) have their own "tick" functions (e.g.,
http_handler()andftp_tick()). When you call such a function, there is no need to calltcp_tick()since the other tick function will always do this for you.3.9.1 tcp_tick() for Robust Applications
It goes without saying that your application should be designed to be robust. You should be aware that an open TCP socket may become disconnected at any time. The disconnection can arise because of a time-out (caused by network problems), or because the peer application sent a RST (reset) flag to abort the connection, the interface went down, or even because another part of your application called
sock_abort(). Your application should check for this condition, preferably in the main socket processing loop, by callingtcp_tick()with the socket address. Sincetcp_tick()needs to be called regularly, this does not add much overhead if you have a single socket. For applications which manage multiple sockets, you can use thesock_alive()function (new for Dynamic C 7.30). Iftcp_tick()orsock_alive()returns zero for a socket, then the socket may be re-opened after your application recovers.Regular checking of socket status is also convenient in that it can simplify the rest of your application. In effect, checking socket status in your main application loop concentrates socket error handling at a single point in the code. There is less need to perform error handling after other calls to TCP/IP functions. For example, the
sock_fastread()function normally returns a non-negative value, but it can return -1 if there is a problem with the socket. An application function which callssock_fastread()needs to check for this code, however it can choose to merely return to the caller (the main loop) if this code is detected, rather than handling the error at the point where it was first detected. This works because ifsock_fastread()returns -1,tcp_tick()will return zero for that socket.3.9.2 Global Timer Variables
The TCP/IP stack depends on the values for
MS_TIMER, andSEC_TIMER. Problems may be encountered if the application program changes these values during execution.3.10 State-Based Program Design
An efficient design strategy is to create a state machine within a function and pass the socket's data structure as a function parameter. This method allows you to handle multiple sockets without the services of a multitasking kernel. This is the way the
HTTP.LIBfunctions are organized. Many of the common Internet protocols fit well into this state machine model.
An example of state-based programming is
SAMPLES\TCPIP\STATE.C. This program is a basic Web server that should work with most browsers. It allows a single connection at a time, but can be extended to allow multiple connections.In general, when defining the set of states for a socket connection, you will need to define a state for each point where the application needs to wait for some external event. At a minimum, this will include states when waiting for:
For non-trivial application protocols, the states in-between session establishment and session termination may need to be embellished into a set of sub-states which reflect the stage of processing of input or output. Sometimes, input and output states may need to overlap. If they do not, then you typically have a step-by-step protocol. Otherwise, you have an application that uses receive and transmit independently. Step-by-step protocols are easier to implement, since there is no need to be able to overlap two (or more) sets of state.
For read states, which are waiting for some data to come in from the peer, you will typically call one of the non-blocking socket read functions to see if there is any data available. If you are expecting a fixed length of data (e.g., a C structure encoded in the TCP data stream), then it is most convenient to use the
sock_aread()function which was introduced in Dynamic C 7.30. Otherwise, if you cannot tell how much data will be required to go to the next state, then you will have to callsock_preread()to check the current data, without prematurely extracting it from the socket receive buffer.For write states, you can just keep calling
sock_fastwrite()until all the data for this state is written. If you have a fixed amount of data,sock_awrite()is more convenient since you do not have to keep track of partially written data.3.10.1 Blocking vs. Non-Blocking
There is a choice between blocking and non-blocking functions when doing socket I/O.
3.10.1.1 Non-Blocking Functions
The
sock_fastread()andsock_preread()functions read all available data in the buffers, and return immediately. Similarly, thesock_fastwrite()function fills the buffers and returns the number of characters that were written. When using these functions, you must ensure that all of the data were written completely.
offset=0;while(offset<len) {bytes_written = sock_fastwrite(&s, buf+offset, len-offset);if(bytes_written < 0) {// error handling}offset += bytes_written;
}3.10.1.2 Blocking Functions
The other functions (
sock_getc(),sock_gets(),sock_putc(),sock_puts(),sock_read()andsock_write()) do not return until they have completed or there is an error. If it is important to avoid blocking, you can check the conditions of an operation to ensure that it will not block.
sock_mode(socket,TCP_MODE_ASCII);
// ...
if (sock_bytesready(&my_socket) != -1){
sock_gets(buffer, MAX_BUFFER);
}In this case
sock_gets()will not block because it will be called only when there is a complete new line terminated record to read.3.11 TCP and UDP Data Handlers
Starting with Dynamic C 7.301, your application can specify data handler callback functions for TCP and UDP sockets. The data handler callback may be specified as a parameter to the
tcp_open(),tcp_extopen(),tcp_listen(),tcp_extlisten(),udp_open(),udp_extopen()andudp_waitopen()functions.The UDP data handler callback is always available. The TCP handler is only available if you
#define TCP_DATAHANDLERbefore includingdcrtcp.lib. Both types of callback use the same function prototype, however, the parameters are interpreted slightly differently.The prototype for a suitable callback function is:
int my_data_handler(
int event,
void * socket,
ll_Gather * g,
void * info
);"event" indicates the type of callback. It is one of a predefined set of constants specified in the table below.
"socket" is a pointer to the socket structure (TCP or UDP). "g" contains a number of fields which may be accessed to find additional information, including the data stream or packet. "info" points to a structure which depends on the type of socket:
_udp_datagram_infoif the socket is UDP, orNULLfor TCP sockets.The ll_Gather structure is defined and documented in
NET.LIB. It is printed here for reference:typedef struct {
byte iface; // Destination interface
byte spare;
word len1; // Length of root data section
void * data1; // Root data (e.g., link, IP, transport headers)
word len2; // Length of first xmem section
long data2; // First xmem data extent (physical address)
word len3; // Length of second xmem section
long data3; // Second xmem data extent (physical address)
} ll_Gather;The
_udp_datagram_infois defined inUDP.LIB.It is documented with theudp_peek()function.For UDP sockets, the callback is invoked for each packet received by the socket. For TCP sockets, the callback is invoked whenever
newdata is available that could otherwise be returned bysock_fastread().The advantages of using the data handler callback are:
Less application overhead calling
sock_dataready()orsock_fastread()Ability to transform data in the socket buffer (e.g., decryption)
For UDP, may avoid the need to copy incoming data into the socket receive buffer
Minimizes latency between
tcp_tick()receive processing, and application processingThe following table lists the parameters to the callback for each event type.
3.11.1 UDP Data Handler
For UDP sockets, the callback is invoked as soon as a new datagram is demultiplexed to the socket. For event type
UDP_DH_INDATA,thell_Gatherstruct is set up with the interface number and pointers to the data in the receive buffers (not the UDP socket receive buffer, since the data has not yet been copied there). The info structure is a pointer to_udp_datagram_info (UDI), which is set up with the usualudp_peekinformation such as the host IP address and port number, and whether the datagram is in fact an ICMP error message. If an ICMP message is received, the event type is set toUDP_DH_ICMPMSG. The callback should return 0 to continue with normal processing (i.e., add the datagram to the socket buffer), or 1 to indicate that the datagram has been processed and should not be added to the socket bufferThe data pointers in the
ll_Gatherstructure are the physical address (and length) of one or two datagram fragments in the main network receive buffers. (Currently, only one address will be provided, since datagrams are reassembled before passing to the UDP handler). There is also a root data pointer in thell_Gatherstructure, that is set to point to the IP and UDP headers of the datagram.3.11.2 TCP Data Handler
The TCP data handler is only available if you #define
TCP_DATAHANDLER. It is invoked with a large number of different event types. Most of the events are for significant changes in the TCP socket state. You can use these events to perform customized handling of socket open and close. Apart fromTCP_DH_INDATAandTCP_DH_ICMPMSG, thell_Gatherstructure is not passed (g is set toNULL). Currently, the info parameter is always null for TCP sockets.If your callback function does not understand a particular event type, or is not interested, it should return zero. This will allow for upward compatibility if new callback events are introduced.
For convenience in coding the callback, you can use the user_data field in the tcp_Socket structure to hold some application-specific data which is to be associated with a socket instance. There is no API for accessing this field; just use
s->user_data. This field is only available if you have definedTCP_DATAHANDLER, and only for TCP sockets (not UDP).There is no guarantee on the order in which events will arrive for a socket. The exceptions are that
TCP_DH_LISTENorTCP_DH_OPENwill always be first, andTCP_DH_CLOSEDwill always be last. There is no guarantee that the callback will be invoked withTCP_DH_INCLOSEorTCP_DH_OUTCLOSEbeforeTCP_DH_CLOSED.
TCP_DH_OUTBUFindicates that some previously transmitted data has been acknowleged by the peer. Generally, this means that there is more space available in the transmit buffer. The callback can write data to the socket usingsock_fastwrite()and other non-blocking write functions. The available transmit buffer space may be determined bysock_tbleft()function. WhenTCP_DH_ESTABis invoked, the transmit buffer is normally completely empty, so the callback can write a reasonable amount of data to start with.The
TCP_DH_INDATAevent callback is invoked after the incoming data has been stored in the socket buffer. It is only invoked if there isnewdata available from the peer. Thell_Gatherstructure is set up with one or two physical address pointers to the new data, and the logical pointer points to the IP header of the most recent datagram which provided the new data. Usually there will be only one physical address, however there may be two if the socket buffer happens to wrap around at that point. The callback will need to be coded to handle this possibility if it is accessing the data directly out of the xmem buffer.The
TCP_DH_INDATAcallback is allowed to modify the new data in-place, if desired. This may be used to provide "transparent decryption" or similar services.There are some restrictions which apply to callback code. Primarily, it is not allowed to invoke
tcp_tick()directly or indirectly, since that will cause recursion intotcp_tick(). It will be possible to callsock_fastwrite()orudp_sendto()e.g., to generate some sort of response. Sincesock_fastwrite()needs to buffer data, there is a possibility that there may be insufficient room in the transmit buffer for the generated response. Thus the callback will need to be carefully coded to avoid getting into a buffer deadlock situation if it generates responses. It will also need to co-ordinate with the rest of the application, since the application will otherwise have to contend with the possibility of arbitrary data being inserted in the write stream by the callback.3.12 Multitasking and TCP/IP
Dynamic C's TCP/IP implementation is compatible with both µC/OS-II and with the language constructs that implement cooperative multitasking: costatements and cofunctions. Note that TCP/IP is not compatible with the slice statement.
3.12.1 µC/OS-II
The TCP/IP stack may be used with the µC/OS-II real-time kernel. The line
#use ucos2.lib#use dcrtcp.libin the application program. Also be sure to call
OSInit()before callingsock_init().Dynamic C version 7.05 and later requires the macro
MAX_SOCKET_LOCKSfor µC/OS-II support. If it is not defined, it will default toMAX_TCP_SOCKET_BUFFERS+TOTAL_UDP_SOCKET_BUFFERS(which isMAX_UDP_SOCKET_BUFFERS+ 1 if there are DNS lookups).Buffers
xalloc'd for socket I/O should be accounted for inMAX_SOCKET_LOCKS.3.12.2 Cooperative Multitasking
The following program demonstrates the use of multiple TCP sockets with costatements.
Program Name: costate_tcp.c
// #define MY_IP_ADDRESS "10.10.6.11"
// #define MY_NETMASK "255.255.255.0"
// #define MY_GATEWAY "10.10.6.1"#define TCPCONFIG 1#define PORT1 8888
#define PORT2 8889#define SOCK_BUF_SIZE 2048
#define MAX_SOCKETS 2#memmap xmem
#use "dcrtcp.lib"tcp_Socket Socket_1;
tcp_Socket Socket_2;#define MAX_BUFSIZE 512
char buf1[MAX_BUFSIZE], buf2[MAX_BUFSIZE];
// The function that actually does the TCP work
cofunc int basic_tcp[2](tcp_Socket *s, int port, char *buf){
auto int length, space_avaliable;tcp_listen(s, port, 0, 0, NULL, 0);// wait for a connection
while((-1 == sock_bytesready(s)) && (0 == sock_established(s)))
// give other tasks time to do things while we are waiting
yield;while(sock_established(s)) {
space_avaliable = sock_tbleft(s);// limit transfer size to MAX_BUFSIZE, leave room for '\0'
if(space_avaliable > (MAX_BUFSIZE-1))
space_avaliable = (MAX_BUFSIZE-1);// get some data
length = sock_fastread(s, buf, space_avaliable);
if(length > 0) { // did we receive any data?
buf[length] = '\0'; // print it to the Stdio window
printf("%s",buf);// send it back out to the user's telnet session
// sock_fastwrite will work-we verified the space beforehand
sock_fastwrite(s, buf, length);
}
yield; // give other tasks time to run
}
sock_close(s);
return 1;
}
main() {
sock_init();
while (1) {
costate {
// Go do the TCP/IP part, on the first socket
wfd basic_tcp[0](&Socket_1, PORT1, buf1);
}
costate {
// Go do the TCP/IP part, on the second socket
wfd basic_tcp[1](&Socket_2, PORT2, buf2);
}
costate {
// drive the tcp stack
tcp_tick(NULL);
}
costate {
// Can insert application code here!
waitfor(DelayMs(100));
}
}
}
1 Data handler pointers were provided to the tcp_open etc. functions prior to this release, however the interface was not documented, and does not work in the way described herein.
| TCP/IP Manual Vol 1 |
<<Previous | Index | Next>> | rabbit.com |