13.3 The DatagramSocket Class

     

To send or receive a DatagramPacket , you must open a datagram socket. In Java, a datagram socket is created and accessed through the DatagramSocket class:

 public class DatagramSocket extends Object 

All datagram sockets are bound to a local port, on which they listen for incoming data and which they place in the header of outgoing datagrams. If you're writing a client, you don't care what the local port is, so you call a constructor that lets the system assign an unused port (an anonymous port). This port number is placed in any outgoing datagrams and will be used by the server to address any response datagrams. If you're writing a server, clients need to know on which port the server is listening for incoming datagrams; therefore, when a server constructs a DatagramSocket , it specifies the local port on which it will listen. However, the sockets used by clients and servers are otherwise identical: they differ only in whether they use an anonymous (system-assigned) or a well-known port. There's no distinction between client sockets and server sockets, as there is with TCP; there's no such thing as a DatagramServerSocket .

13.3.1 The Constructors

The DatagramSocket constructors are used in different situations, much like the DatagramPacket constructors. The first constructor opens a datagram socket on an anonymous local port. The second constructor opens a datagram socket on a well-known local port that listens to all local network interfaces. The third constructor opens a datagram socket on a well-known local port on a specific network interface. Java 1.4 adds a constructor that allows this network interface and port to be specified with a SocketAddress . Java 1.4 also adds a protected constructor that allows you to change the implementation class. All five constructors deal only with the local address and port. The remote address and port are stored in the DatagramPacket , not the DatagramSocket . Indeed, one DatagramSocket can send and receive datagrams from multiple remote hosts and ports.

13.3.1.1 public DatagramSocket( ) throws SocketException

This constructor creates a socket that is bound to an anonymous port. For example:

 try {   DatagramSocket client = new DatagramSocket( );   // send packets... } catch (SocketException ex) {   System.err.println(ex); } 

You would use this constructor in a client that initiates a conversation with a server. In this scenario, you don't care what port the socket is bound to, because the server will send its response to the port from which the datagram originated. Letting the system assign a port means that you don't have to worry about finding an unused port. If for some reason you need to know the local port, you can find out with the getLocalPort( ) method described later in this chapter.

The same socket can receive the datagrams that a server sends back to it. A SocketException is thrown if the socket can't be created. It's unusual for this constructor to throw an exception; it's hard to imagine situations in which the socket could not be opened, since the system gets to choose the local port.

13.3.1.2 public DatagramSocket(int port) throws SocketException

This constructor creates a socket that listens for incoming datagrams on a particular port, specified by the port argument. Use this constructor to write a server that listens on a well-known port; if servers listened on anonymous ports, clients would not be able to contact them. A SocketException is thrown if the socket can't be created. There are two common reasons for the constructor to fail: the specified port is already occupied, or you are trying to connect to a port below 1,024 and you don't have sufficient privileges (i.e., you are not root on a Unix system; for better or worse , other platforms allow anyone to connect to low-numbered ports).

TCP ports and UDP ports are not related . Two unrelated servers or clients can use the same port number if one uses UDP and the other uses TCP. Example 13-2 is a port scanner that looks for UDP ports in use on the local host. It decides that the port is in use if the DatagramSocket constructor throws an exception. As written, it looks at ports from 1,024 and up to avoid Unix's requirement that it run as root to bind to ports below 1,024. You can easily extend it to check ports below 1,024, however, if you have root access or are running it on Windows.

Example 13-2. Look for local UDP ports
 import java.net.*; public class UDPPortScanner {   public static void main(String[] args) {          for (int port = 1024; port <= 65535; port++) {       try {         // the next line will fail and drop into the catch block if         // there is already a server running on port i         DatagramSocket server = new DatagramSocket(port);         server.close( );       }       catch (SocketException ex) {         System.out.println("There is a server on port " + port + ".");       } // end try     } // end for   }    } 

The speed at which UDPPortScanner runs depends strongly on the speed of your machine and its UDP implementation. I've clocked Example 13-2 at as little as two minutes on a moderately powered SPARCstation, under 12 seconds on a 1Ghz TiBook, about 7 seconds on a 1.4GHz Athlon system running Linux, and as long as an hour on a PowerBook 5300 running MacOS 8. Here are the results from the Linux workstation on which much of the code in this book was written:

 %  java UDPPortScanner  There is a server on port 2049. There is a server on port 32768. There is a server on port 32770. There is a server on port 32771. 

The first port, 2049, is an NFS server. The high-numbered ports in the 30,000 range are Remote Procedure Call (RPC) services. Along with RPC, common protocols that use UDP include NFS, TFTP, and FSP.

It's much harder to scan UDP ports on a remote system than to scan for remote TCP ports. Whereas there's always some indication that a listening port, regardless of application layer protocol, has received your TCP packet, UDP provides no such guarantees . To determine that a UDP server is listening, you have to send it a packet it will recognize and respond to.

13.3.1.3 public DatagramSocket(int port, InetAddress interface) throws SocketException

This constructor is primarily used on multihomed hosts; it creates a socket that listens for incoming datagrams on a specific port and network interface. The port argument is the port on which this socket listens for datagrams. As with TCP sockets, you need to be root on a Unix system to create a DatagramSocket on a port below 1,024. The address argument is an InetAddress object matching one of the host's network addresses. A SocketException is thrown if the socket can't be created. There are three common reasons for this constructor to fail: the specified port is already occupied, you are trying to connect to a port below 1,024 and you're not root on a Unix system, or address is not the address of one of the system's network interfaces.

13.3.1.4 public DatagramSocket(SocketAddress interface) throws SocketException // Java 1.4

This constructor is similar to the previous one except that the network interface address and port are read from a SocketAddress . For example, this code fragment creates a socket that only listens on the local loopback address:

 SocketAddress address = new InetSocketAddress("127.0.0.1", 9999); DatagramSocket socket = new DatagramSocket(address); 

13.3.1.5 protected DatagramSocket(DatagramSocketImpl impl) throws SocketException // Java 1.4

This constructor enables subclasses to provide their own implementation of the UDP protocol, rather than blindly accepting the default. Unlike sockets created by the other four constructors, this socket is not initially bound to a port. Before using it you have to bind it to a SocketAddress using the bind( ) method, which is also new in Java 1.4:

 public void bind(SocketAddress addr) throws SocketException 

You can pass null to this method, binding the socket to any available address and port.

13.3.2 Sending and Receiving Datagrams

The primary task of the DatagramSocket class is to send and receive UDP datagrams. One socket can both send and receive. Indeed, it can send and receive to and from multiple hosts at the same time.

13.3.2.1 public void send(DatagramPacket dp) throws IOException

Once a DatagramPacket is created and a DatagramSocket is constructed , send the packet by passing it to the socket's send() method. For example, if theSocket is a DatagramSocket object and theOutput is a DatagramPacket object, send theOutput using theSocket like this:

 theSocket.send(theOutput); 

If there's a problem sending the data, an IOException may be thrown. However, this is less common with DatagramSocket than Socket or ServerSocket , since the unreliable nature of UDP means you won't get an exception just because the packet doesn't arrive at its destination. You may get an IOException if you're trying to send a larger datagram than the host's native networking software supports, but then again you may not. This depends heavily on the native UDP software in the OS and the native code that interfaces between this and Java's DatagramSocketImpl class. This method may also throw a SecurityException if the SecurityManager won't let you communicate with the host to which the packet is addressed. This is primarily a problem for applets and other remotely loaded code.

Example 13-3 is a UDP-based discard client. It reads lines of user input from System.in and sends them to a discard server, which simply discards all the data. Each line is stuffed in a DatagramPacket . Many of the simpler Internet protocols, such as discard, have both TCP and UDP implementations .

Example 13-3. A UDP discard client
 import java.net.*; import java.io.*; public class UDPDiscardClient {   public final static int DEFAULT_PORT = 9;   public static void main(String[] args) {     String hostname;     int port = DEFAULT_PORT;     if (args.length > 0) {       hostname = args[0];       try {               port = Integer.parseInt(args[1]);       }       catch (Exception ex) {         // use default port       }     }     else {       hostname = "localhost";     }     try {       InetAddress server = InetAddress.getByName(hostname);       BufferedReader userInput         = new BufferedReader(new InputStreamReader(System.in));       DatagramSocket theSocket = new DatagramSocket( );       while (true) {         String theLine = userInput.readLine( );         if (theLine.equals(".")) break;         byte[] data = theLine.getBytes( );         DatagramPacket theOutput           = new DatagramPacket(data, data.length, server, port);         theSocket.send(theOutput);       }  // end while     }  // end try     catch (UnknownHostException uhex) {       System.err.println(uhex);     }       catch (SocketException sex) {       System.err.println(sex);     }     catch (IOException ioex) {       System.err.println(ioex);     }   }  // end main } 

The UDPDiscardClient class should look familiar. It has a single static field, DEFAULT_PORT , which is set to the standard port for the discard protocol (port 9), and a single method, main( ) . The main() method reads a hostname from the command line and converts that hostname to the InetAddress object called server . A BufferedReader is chained to System.in to read user input from the keyboard. Next, a DatagramSocket object called theSocket is constructed. After creating the socket, the program enters an infinite while loop that reads user input line by line using readLine() . We are careful, however, to use only readLine( ) to read data from the console, the one place where it is guaranteed to work as advertised. Since the discard protocol deals only with raw bytes, we can ignore character encoding issues.

In the while loop, each line is converted to a byte array using the getBytes( ) method, and the bytes are stuffed in a new DatagramPacket , theOutput . Finally, theOutput is sent over theSocket , and the loop continues. If at any point the user types a period on a line by itself, the program exits. The DatagramSocket constructor may throw a SocketException , so that needs to be caught. Because this is a discard client, we don't need to worry about data coming back from the server.

13.3.2.2 public void receive(DatagramPacket dp) throws IOException

This method receives a single UDP datagram from the network and stores it in the preexisting DatagramPacket object dp . Like the accept( ) method in the ServerSocket class, this method blocks the calling thread until a datagram arrives. If your program does anything besides wait for datagrams, you should call receive() in a separate thread.

The datagram's buffer should be large enough to hold the data received. If not, receive( ) places as much data in the buffer as it can hold; the rest is lost. It may be useful to remember that the maximum size of the data portion of a UDP datagram is 65,507 bytes. (That's the 65,536-byte maximum size of an IP datagram minus the 20-byte size of the IP header and the 8-byte size of the UDP header.) Some application protocols that use UDP further restrict the maximum number of bytes in a packet; for instance, NFS uses a maximum packet size of 8,192 bytes.

If there's a problem receiving the data, an IOException may be thrown. In practice, this is rare. Unlike send( ) , this method does not throw a SecurityException if an applet receives a datagram from other than the applet host. However, it will silently discard all such packets. (This behavior prevents a denial-of-service attack against applets that receive UDP datagrams.)

Example 13-4 shows a UDP discard server that receives incoming datagrams. Just for fun, it logs the data in each datagram to System.out so that you can see who's sending what to your discard server.

Example 13-4. The UDPDiscardServer
 import java.net.*; import java.io.*; public class UDPDiscardServer {   public final static int DEFAULT_PORT = 9;   public final static int MAX_PACKET_SIZE = 65507;   public static void main(String[] args) {     int port = DEFAULT_PORT;     byte[] buffer = new byte[MAX_PACKET_SIZE];     try {       port = Integer.parseInt(args[0]);     }     catch (Exception ex) {       // use default port     }     try {       DatagramSocket server = new DatagramSocket(port);       DatagramPacket packet = new DatagramPacket(buffer, buffer.length);       while (true) {         try {           server.receive(packet);           String s = new String(packet.getData( ), 0, packet.getLength( ));           System.out.println(packet.getAddress( ) + " at port "             + packet.getPort( ) + " says " + s);           // reset the length for the next packet           packet.setLength(buffer.length);         }         catch (IOException ex) {           System.err.println(ex);         }              } // end while     }  // end try     catch (SocketException  ex) {       System.err.println(ex);     }  // end catch   }  // end main } 

This is a simple class with a single method, main() . It reads the port the server listens to from the command line. If the port is not specified on the command line, it listens on port 9. It then opens a DatagramSocket on that port and creates a DatagramPacket with a 65,507-byte bufferlarge enough to receive any possible packet. Then the server enters an infinite loop that receives packets and prints the contents and the originating host on the console. A high-performance discard server would skip this step. As each datagram is received, the length of packet is set to the length of the data in that datagram. Consequently, as the last step of the loop, the length of the packet is reset to the maximum possible value. Otherwise, the incoming packets would be limited to the minimum size of all previous packets. You can run the discard client on one machine and connect to the discard server on a second machine to verify that the network is working.

13.3.2.3 public void close( )

Calling a DatagramSocket object's close( ) method frees the port occupied by that socket. For example:

 try {   DatagramSocket server = new DatagramSocket( );   server.close( ); } catch (SocketException ex) {   System.err.println(ex); } 

It's never a bad idea to close a DatagramSocket when you're through with it; it's particularly important to close an unneeded socket if the program will continue to run for a significant amount of time. For example, the close() method was essential in Example 13-2, UDPPortScanner : if this program did not close the sockets it opened, it would tie up every UDP port on the system for a significant amount of time. On the other hand, if the program ends as soon as you're through with the DatagramSocket , you don't need to close the socket explicitly; the socket is automatically closed upon garbage collection. However, Java won't run the garbage collector just because you've run out of ports or sockets, unless by lucky happenstance you run out of memory at the same time. Closing unneeded sockets never hurts and is good programming practice.

13.3.2.4 public int getLocalPort( )

A DatagramSocket 's getLocalPort( ) method returns an int that represents the local port on which the socket is listening. Use this method if you created a DatagramSocket with an anonymous port and want to find out what port the socket has been assigned. For example:

 try {   DatagramSocket ds = new DatagramSocket( );   System.out.println("The socket is using port " + ds.getLocalPort( )); } catch (SocketException ex) {   ex.printStackTrace( ); } 

13.3.2.5 public InetAddress getLocalAddress( )

A DatagramSocket 's getLocalAddress( ) method returns an InetAddress object that represents the local address to which the socket is bound. It's rarely needed in practice. Normally, you either know or don't care which address a socket is listening to.

13.3.2.6 public SocketAddress getLocalSocketAddress( ) // Java 1.4

The getLocalSocketAddress() method returns a SocketAddress object that wraps the local interface and port to which the socket is bound. Like getLocalAddress( ) , it's a little hard to imagine a realistic use case here. This method probably exists mostly for parallelism with setLocalSocketAddress() .

13.3.3 Managing Connections

Unlike TCP sockets, datagram sockets aren't very picky about whom they'll talk to. In fact, by default they'll talk to anyone, but this is often not what you want. For instance, applets are only allowed to send datagrams to and receive datagrams from the applet host. An NFS or FSP client should accept packets only from the server it's talking to. A networked game should listen to datagrams only from the people playing the game. In Java 1.1, programs must manually check the source addresses and ports of the hosts sending them data to make sure they're who they should be. However, Java 1.2 adds four methods that let you choose which host you can send datagrams to and receive datagrams from, while rejecting all others' packets.

13.3.3.1 public void connect(InetAddress host, int port) // Java 1.2

The connect( ) method doesn't really establish a connection in the TCP sense. However, it does specify that the DatagramSocket will send packets to and receive packets from only the specified remote host on the specified remote port. Attempts to send packets to a different host or port will throw an IllegalArgumentException . Packets received from a different host or a different port will be discarded without an exception or other notification.

A security check is made when the connect( ) method is invoked. If the VM is allowed to send data to that host and port, the check passes silently. Otherwise, a SecurityException is thrown. However, once the connection has been made, send( ) and receive( ) on that DatagramSocket no longer make the security checks they'd normally make.

13.3.3.2 public void disconnect( ) // Java 1.2

The disconnect( ) method breaks the "connection" of a connected DatagramSocket so that it can once again send packets to and receive packets from any host and port.

13.3.3.3 public int getPort( ) // Java 1.2

If and only if a DatagramSocket is connected, the getPort( ) method returns the remote port to which it is connected. Otherwise, it returns -1.

13.3.3.4 public InetAddress getInetAddress( ) // Java 1.2

If and only if a DatagramSocket is connected, the getInetAddress( ) method returns the address of the remote host to which it is connected. Otherwise, it returns null.

13.3.3.5 public InetAddress getRemoteSocketAddress( ) // Java 1.4

If a DatagramSocket is connected, the getRemoteSocketAddress( ) method returns the address of the remote host to which it is connected. Otherwise, it returns null.

13.3.4 Socket Options

The only socket option supported for datagram sockets in Java 1.1 is SO_TIMEOUT. Java 1.2 adds SO_SNDBUF and SO_RCVBUF. Java 1.4 adds SO_REUSEADDR and SO_BROADCAST and enables the specification of the traffic class.

13.3.4.1 SO_TIMEOUT

SO_TIMEOUT is the amount of time, in milliseconds , that receive( ) waits for an incoming datagram before throwing an InterruptedIOException (a subclass of IOException ). Its value must be nonnegative. If SO_TIMEOUT is 0, receive( ) never times out. This value can be changed with the setSoTimeout( ) method and inspected with the getSoTimeout( ) method:

 public synchronized void setSoTimeout(int timeout)   throws SocketException public synchronized int getSoTimeout( ) throws IOException 

The default is to never time out, and indeed there are few situations in which you would need to set SO_TIMEOUT. You might need it if you were implementing a secure protocol that required responses to occur within a fixed amount of time. You might also decide that the host you're communicating with is dead (unreachable or not responding) if you don't receive a response within a certain amount of time.

The setSoTimeout( ) method sets the SO_TIMEOUT field for a datagram socket. When the timeout expires , an InterruptedIOException is thrown. (In Java 1.4 and later, SocketTimeoutException , a subclass of InterruptedIOException , is thrown instead.) Set this option before you call receive( ) . You cannot change it while receive( ) is waiting for a datagram. The timeout argument must be greater than or equal to zero; if it is not, setSoTimeout( ) throws a SocketException . For example:

 try {   buffer = new byte[2056];   DatagramPacket dp = new DatagramPacket(buffer, buffer.length);   DatagramSocket ds = new DatagramSocket(2048);   ds.setSoTimeout(30000); // block for no more than 30 seconds   try {    ds.receive(dp);     // process the packet...   }   catch (InterruptedIOException ex) {     ss.close( );     System.err.println("No connection within 30 seconds");   } catch (SocketException ex) {   System.err.println(ex); } catch (IOException ex) {   System.err.println("Unexpected IOException: " + ex); } 

The getSoTimeout( ) method returns the current value of this DatagramSocket object's SO_TIMEOUT field. For example:

 public void printSoTimeout(DatagramSocket ds) {   int timeout = ds.getSoTimeOut( );   if (timeout > 0) {     System.out.println(ds + " will time out after "       + timeout + "milliseconds.");   }   else if (timeout == 0) {     System.out.println(ds + " will never time out.");   }   else {     System.out.println("Something is seriously wrong with " + ds);   } } 

13.3.4.2 SO_RCVBUF

The SO_RCVBUF option of DatagramSocket is closely related to the SO_RCVBUF option of Socket . It determines the size of the buffer used for network I/O. Larger buffers tend to improve performance for reasonably fast (say, Ethernet-speed) connections because they can store more incoming datagrams before overflowing. Sufficiently large receive buffers are even more important for UDP than for TCP, since a UDP datagram that arrives when the buffer is full will be lost, whereas a TCP datagram that arrives at a full buffer will eventually be retransmitted. Furthermore, SO_RCVBUF sets the maximum size of datagram packets that can be received by the application. Packets that won't fit in the receive buffer are silently discarded.

DatagramSocket has methods to get and set the suggested receive buffer size used for network input:

 public void setReceiveBufferSize(int size) throws SocketException // Java 1.2 public int getReceiveBufferSize( ) throws SocketException          // Java 1.2 

The setReceiveBufferSize( ) method suggests a number of bytes to use for buffering input from this socket. However, the underlying implementation is free to ignore this suggestion. For instance, many 4.3 BSD-derived systems have a maximum receive buffer size of about 52K and won't let you set a limit higher than this. My Linux box was limited to 64K. Other systems raise this to about 240K. The details are highly platform-dependent. Consequently, you may wish to check the actual size of the receive buffer with getReceiveBufferSize( ) after setting it. The getReceiveBufferSize( ) method returns the number of bytes in the buffer used for input from this socket.

Both methods throw a SocketException if the underlying socket implementation does not recognize the SO_RCVBUF option. This might happen on a non-POSIX operating system. The setReceiveBufferSize( ) method throws an IllegalArgumentException if its argument is less than or equal to zero.

13.3.4.3 SO_SNDBUF

DatagramSocket has methods to get and set the suggested send buffer size used for network output:

 public void setSendBufferSize(int size)  throws SocketException // Java 1.2 public int getSendBufferSize( ) throws SocketException           // Java 1.2 

The setSendBufferSize( ) method suggests a number of bytes to use for buffering output on this socket. Once again, however, the operating system is free to ignore this suggestion. Consequently, you'll want to check the result of setSendBufferSize( ) by immediately following it with a call to getSend BufferSize( ) to find out the real the buffer size.

Both methods throw a SocketException if the underlying native network software doesn't understand the SO_SNDBUF option. The setSendBufferSize( ) method also throws an IllegalArgumentException if its argument is less than or equal to zero.

13.3.4.4 SO_REUSEADDR

The SO_REUSEADDR option does not mean the same thing for UDP sockets as it does for TCP sockets. For UDP, SO_REUSEADDR can control whether multiple datagram sockets can bind to the same port and address at the same time . If multiple sockets are bound to the same port, received packets will be copied to all bound sockets. This option is controlled by these two methods:

 public void setReuseAddress(boolean on) throws SocketException  // Java 1.4 public boolean getReuseAddress( ) throws SocketException         // Java 1.4 

For this to work reliably, setReuseAddress( ) must be called before the new socket binds to the port. This means the socket must be created in an unconnected state using the protected constructor that takes a DatagramImpl as an argument. In other words, it won't work with a plain vanilla DatagramSocket . Reusable ports are most commonly used for multicast sockets, which will be discussed in the next chapter. Datagram channels also create unconnected datagram sockets that can be configured to reuse ports, as you'll see later in this chapter.

13.3.4.5 SO_BROADCAST

The SO_BROADCAST option controls whether a socket is allowed to send packets to and receive packets from broadcast addresses such as 192.168.254.255, the local network broadcast address for the network with the local address 192.168.254.*. UDP broadcasting is often used for protocols like the JXTA Peer Discovery Protocol and the Service Location Protocol that need to communicate with servers on the local net whose addresses are not known in advance. This option is controlled with these two methods:

 public void setBroadcast(boolean on) throws SocketException  // Java 1.4 public boolean getBroadcast( ) throws SocketException         // Java 1.4 

Routers and gateways do not normally forward broadcast messages, but they can still kick up a lot of traffic on the local network. This option is turned on by default, but if you like you can disable it thusly:

 socket.setBroadcast(false); 

This option can be changed after the socket has been bound.

On some implementations, sockets bound to a specific address do not receive broadcast packets. In other words, use the DatagramPacket(int port) constructor, not the DatagramPacket(InetAddress address , int port) constructor to listen to broadcasts. This is necessary in addition to setting the SO_BROADCAST option to true.


13.3.4.6 Traffic class

Traffic class is essentially the same for UDP as it is for TCP. After all, packets are actually routed and prioritized according to IP, which both TCP and UDP sit on top of. There's really no difference between the setTrafficClass( ) and getTrafficClass( ) methods in DatagramSocket and those in Socket . They just have to be repeated here because DatagramSocket and Socket don't have a common superclass. These two methods let you inspect and set the class of service for a socket using these two methods:

 public int getTrafficClass( ) throws SocketException // Java 1.4 public void setTrafficClass(int trafficClass) throws SocketException                                                        // Java 1.4 

The traffic class is given as an int between 0 and 255. (Values outside this range cause IllegalArgumentException s.) This int is a combination of bit-flags. Specifically:

  • 0x02: Low cost

  • 0x04: High reliability

  • 0x08: Maximum throughput

  • 0x10: Minimum delay

Java always sets the lowest order, ones bit to zero, even if you try to set it to one. The three high-order bits are not yet used. For example, this code fragment requests a low cost connection:

 DatagramSocket s = new DatagramSocket ( ); s.setTrafficClass(0x02); 

This code fragment requests a connection with maximum throughput and minimum delay:

 DatagramSocket s = new DatagramSocket ( ); s.setTrafficClass(0x08  0x10); 

The underlying socket implementation is not required to respect any of these requests. They hint at the policy that is desired. Probably most current implementations will ignore these values completely. If the local network stack is unable to provide the requested class of service, it may throw a SocketException , but it's not required to and truth be told, it probably won't.



Java Network Programming
Java Network Programming, Third Edition
ISBN: 0596007213
EAN: 2147483647
Year: 2003
Pages: 164

Similar book on Amazon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net