Appendix AQ

Overview

Oracle 8.1.6 introduced for the first time, the UTL_TCP package. This package allows PL/SQL to open a network socket connection over TCP/IP to any server accepting connections. Assuming you know the protocol of a server, you can now 'talk' to it from PL/SQL. For example, given that I know HTTP (Hyper Text Transfer Protocol), I can code in UTL_TCP the following:

test_jsock@DEV816> DECLARE   2     c utl_tcp.connection; -- TCP/IP connection to the web server   3     n number;   4     buffer varchar2(255);   5  BEGIN   6     c := utl_tcp.open_connection('proxy-server', 80);   7     n := utl_tcp.write_line(c, 'GET http://www.apress.com/ HTTP/1.0');   8     n := utl_tcp.write_line(c);   9     BEGIN  10       LOOP  11           n:=utl_tcp.read_text( c, buffer, 255 );  12           dbms_output.put_line( buffer );  13       END LOOP;  14     EXCEPTION  15       WHEN utl_tcp.end_of_input THEN  16        NULL; -- end of input  17     end;  18     utl_tcp.close_connection(c);  19  END;  20  / HTTP/1.1 200 OK Date: Tue, 30 Jan 2001 11:33:50 GMT Server: Apache/1.3.9 (Unix) mod_perl/1.21 ApacheJServ/1.1 Content-Type: text/html      <head> <title>Oracle Corporation</title> 

This lets me open a connection to a server, in this case a proxy server named proxy-server. It lets me get through our firewall to the outside Internet. This happens on line 6. I then request a web page on lines 7 and 8. On lines 10 through to 13, we receive the contents of the web page, including the all-important HTTP headers (something UTL_HTTP, another supplied package, won't share with us) and print it. When UTL_TCP throws the UTL_TCP.END_OF_INPUT exception, we are done, and we break out of the loop. We then close our connection and that's it.

This simple example demonstrates a majority of the functionality found in the UTL_TCP package. We didn't see functions such as AVAILABLE, which tells us if any data is ready to be received. We skipped FLUSH, which causes any buffered output to be transmitted (we didn't use buffering, hence did not need this call). Likewise, we did not use every variation of READ, WRITE, and GET to put and get data on the socket, but the example above shows how to use UTL_TCP fairly completely.

The one thing I don't necessarily like about the above is the speed at which it runs. It is in my experience that UTL_TCP, while functional, is not as high performing as it could be in this release (Oracle 8i). In Oracle 8.1.7.1, this performance issue is fixed (bug #1570972 corrects this issue).

So, how slow is slow? The above code, to retrieve a 16 KB document, takes anywhere from four to ten seconds, depending on platform. This is especially bad considering the native UTL_HTTP function can do the same operation with a sub-second response time. Unfortunately, UTL_HTTP doesn't permit access to cookies, HTTP headers, binary data, basic authentication, and the like, so using an alternative is many times useful. I think we can do better. To this end, we will implement our own UTL_TCP package. However, we will use the Object Type metaphor we discussed in Chapter 20 on Using Object Relational Features. What we will do is to implement a SocketType in PL/SQL with some of the underlying 'guts' in Java. In the UTL_HTTP section, we put this SocketType we created to use in building a better UTL_HTTP package for ourselves as well. Since our functionality will be modeled after the functionality available in the UTL_TCP package, when Oracle9i is released with native and faster UTL_TCP support, we can easily re-implement our type body using the real UTL_TCP package, and stop using our Java-supported one.

The SocketType

Our SocketType Object Type will use the following specification:

tkyte@TKYTE816> create or replace type SocketType   2  as object   3  (   4      -- 'Private data', rather than you   5      -- passing a context to each procedure, like you   6      -- do with UTL_FILE.   7      g_sock        number,   8   9      -- A function to return a CRLF. Just a convenience.  10      static function crlf return varchar2,  11  12      -- Procedures to send data over a socket.  13      member procedure send( p_data in varchar2 ),  14      member procedure send( p_data in clob ),  15  16      member procedure send_raw( p_data in raw ),  17      member procedure send_raw( p_data in blob ),  18  19      -- Functions to receive data from a socket. These return  20      -- Null on eof. They will block waiting for data. If  21      -- this is not desirable, use PEEK below to see if there  22      -- is any data to read.  23      member function recv return varchar2,  24      member function recv_raw return raw,  25  26      -- Convienence function. Reads data until a CRLF is found.  27      -- Can strip the CRLF if you like (or not, by default).  28      member function getline( p_remove_crlf in boolean default FALSE )  29             return varchar2,  30  31      -- Procedures to connect to a host and disconnect from a host.  32      -- It is important to disconnect, else you will leak resources  33      -- and eventually will not be able to connect.  34      member procedure initiate_connection( p_hostname in varchar2,  35                                               p_portno   in number ),  36      member procedure close_connection,  37  38      -- Function to tell you how many bytes (at least) might be  39      -- ready to be read.  40      member function peek return number  41  );  42  /      Type created. 

This set of functionality is pretty much modeled after the UTL_TCP package, and provides much of the same interface. In fact, it could be implemented on top of that package if you wanted. We are going to implement it on top of a different package however, one which I call the SIMPLE_TCP_CLIENT. This is a ordinary PL/SQL package that the SocketType will be built on. This is really our specification of a UTL_TCP package:

tkyte@TKYTE816> CREATE OR REPLACE PACKAGE simple_tcp_client   2  as   3      -- A function to connect to a host. Returns a 'socket',   4      -- which is really just a number.   5      function connect_to( p_hostname in  varchar2,   6                           p_portno   in  number ) return number;   7   8      -- Send data. We only know how to send RAW data here. Callers   9      -- must cast VARCHAR2 data to RAW. At the lowest level, all  10      -- data on a socket is really just 'bytes'.  11  12      procedure send( p_sock     in  number,  13                      p_data     in  raw );  14  15      -- recv will receive data.  16      -- If maxlength is -1, we try for 4k of data. If maxlength  17      -- is set to anything OTHER than -1, we attempt to  18      -- read up to the length of p_data bytes. In other words,  19      -- I restrict the receives to 4k unless otherwise told not to.  20      procedure recv( p_sock      in   number,  21                      p_data      out  raw,  22                      p_maxlength in   number default -1 );  23  24      -- Gets a line of data from the input socket. That is, data  25      -- up to a \n.  26      procedure getline( p_sock          in number,  27                         p_data        out raw );  28  29  30      -- Disconnects from a server you have connected to.  31      procedure disconnect( p_sock     in number );  32  33      -- Gets the server time in GMT in the format yyyyMMdd HHmmss z  34      procedure get_gmt( p_gmt out varchar2 );  35  36      -- Gets the server's timezone. Useful for some Internet protocols.  37      procedure get_timezone( p_timezone   out varchar2 );  38  39      -- Gets the hostname of the server you are running on. Again,  40      -- useful for some Internet protocols.  41      procedure get_hostname( p_hostname   out varchar2 );  42  43      -- Returns the number of bytes available to be read.  44      function peek( p_sock in number ) return number;  45  46      -- base64 encodes a RAW. Useful for sending e-mail  47      -- attachments or doing HTTP which needs the user/password  48      -- to be obscured using base64 encoding.  49      procedure b64encode( p_data in raw, p_result out varchar2 );  50  end;  51  /      Package created. 

Now, as none of these functions can actually be written in PL/SQL, we will implement them in Java instead. The Java for doing this is surprisingly small. The entire script is only 94 lines long. We are using the native Socket class for Java, and will maintain a small array of them, allowing PL/SQL to have up to ten connections open simultaneously. If you would like more than ten, just make the socketUsed array larger in the code below. I've tried to keep this as simple, and as small as possible, preferring to do the bulk of any work in PL/SQL. I'll present the small class we need, and then comment on it:

tkyte@TKYTE816> set define off      tkyte@TKYTE816> CREATE or replace and compile JAVA SOURCE   2  NAMED "jsock"   3  AS   4  import java.net.*;   5  import java.io.*;   6  import java.util.*;   7  import java.text.*;   8  import sun.misc.*;   9  10  public class jsock  11  {  12  static int           socketUsed[] = { 0,0,0,0,0,0,0,0,0,0 };  13  static Socket        sockets[] = new Socket[socketUsed.length];  14  static DateFormat    tzDateFormat = new SimpleDateFormat( "z" );  15  static DateFormat    gmtDateFormat =  16                          new SimpleDateFormat( "yyyyMMdd HHmmss z" );  17  static BASE64Encoder encoder = new BASE64Encoder();  18 

This class has some static variables - the two arrays, socketUsed and sockets are the main ones. When returns are called from PL/SQL, we must return to it something it can send to us on subsequent calls, to identify the socket connection it wants to use. We cannot return the Java Socket class to PL/SQL, so I am using an array in which to store them, and will return to PL/SQL an index into that array. If you look at the java_connect_to method, it looks in the socketsUsed array for an empty slot, and allocates this to the connection. That index into socketsUsed is what PL/SQL will see. We use this in the remaining sockets routines to access the actual Java class that represents a socket.

The other static variables are there for reasons of performance. I needed some date format objects, and rather than NEW them each time you call java_get_gmt or java_get_timezone, I allocate them once, and just reuse them. Lastly is the base 64 encoder object. For the same reason I allocate the date formatter objects, I allocate the encoder.

Now for the routine that connects over TCP/IP, to a server. This logic loops over the socketUsed array looking for an empty slot (where socketUsed[I] is not set to 1). If it finds one, it uses the Java Socket class to create a connection to the host/port combination that was passed in, and sets the socketUsed flag for the array slot to 1. It then returns a -1 on error (no empty slots), or a non-negative number upon success:

 19  static public int java_connect_to( String p_hostname, int p_portno )  20  throws java.io.IOException  21  {  22  int    i;  23  24      for( i = 0; i < socketUsed.length && socketUsed[i] == 1; i++ );  25      if ( i < socketUsed.length )  26      {  27          sockets[i] = new Socket( p_hostname, p_portno );  28          socketUsed[i] = 1;  29      }  30      return i<socketUsed.length?i:-1;  31  }  32  33 

The next routines are the two most frequently called Java routines. They are responsible for sending and receiving data on a connected TCP/IP socket. The java_send_data routine is straightforward - it simply gets the output stream associated with the socket, and writes the data. The java_recv_data is slightly more complex. It uses OUT parameters, hence the use of int[] p_length for example, in order to return data. This routine inspects the length that was sent in by the caller, and if the length was -1, it will allocate a 4 KB buffer to read into, else it will allocate a buffer of the size specified. It will then try to read that much data from the socket. The actual amount of data read (which will be less than or equal to the amount requested) is placed in p_length as a return value:

 34  static public void java_send_data( int p_sock, byte[] p_data )  35  throws java.io.IOException  36  {  37      (sockets[p_sock].getOutputStream()).write( p_data );  38  }  39  40  static public void java_recv_data( int p_sock,  41                                     byte[][] p_data, int[] p_length)  42  throws java.io.IOException  43  {  44      p_data[0] = new byte[p_length[0] == -1 ? 4096:p_length[0] ];  45      p_length[0] = (sockets[p_sock].getInputStream()).read( p_data[0] );  46  }  47 

java_getline is a convenience function. Many Internet protocols respond to operations 'a line at a time', and being able to get a simple line of text is very handy. For example, the headers sent back in the HTTP protocol are simply lines of ASCII text. This routine works by using the DataInputStream.readLine method, and if a line of text is read in, it will return it (putting the new line, which readLine strips off, back on). Otherwise, the data will be returned as Null:

 48  static public void java_getline( int p_sock, String[] p_data )  49  throws java.io.IOException  50  {  51      DataInputStream d =  52          new DataInputStream((sockets[p_sock].getInputStream()));  53      p_data[0] = d.readLine();  54      if ( p_data[0] != null ) p_data[0] += "\n";  55  }  56 

java_disconnect is very straightforward as well. It simply sets the socketUsed array flag back to zero, indicating we can reuse this slot in the array, and closes the socket down for us:

 57  static public void java_disconnect( int p_sock )  58  throws java.io.IOException  59  {  60      socketUsed[p_sock] = 0;  61      (sockets[p_sock]).close();  62  }  63 

The java_peek_sock routine is used to see if data on a socket is available to be read. This is useful for times when the client does not want to block on a receive of data. If you look to see if anything is available, you can tell if a receive will block, or return right away:

 64  static public int java_peek_sock( int p_sock )  65  throws java.io.IOException  66  {  67      return (sockets[p_sock].getInputStream()).available();  68  }  69 

Now we have our two time functions. java_get_timezone is used to return the time zone of the database server. This is particularly useful if you need to convert an Oracle DATE from one time zone to another using the NEW_TIME built-in function, or if you just need know the time zone in which the server is operating. The second function, java_get_gmt, is useful for getting the server's current date and time in GMT (Greenwich Mean Time):

 70  static public void java_get_timezone( String[] p_timezone )  71  {  72      tzDateFormat.setTimeZone( TimeZone.getDefault() );  73      p_timezone[0] = tzDateFormat.format(new Date());  74  }  75  76  77  static public void java_get_gmt( String[] p_gmt )  78  {  79      gmtDateFormat.setTimeZone( TimeZone.getTimeZone("GMT") );  80      p_gmt[0] = gmtDateFormat.format(new Date());  81  }  82 

The b64encode routine will base 64 encode a string of data. Base 64 encoding is an Internet-standard method of encoding arbitrary data into a 7bit ASCII format, suitable for transmission. We will use this function in particular when implementing our HTTP package, as it will support basic authentication (used by many web sites that require you to log in via a username and password).

 83  static public void b64encode( byte[] p_data, String[] p_b64data )  84  {  85      p_b64data[0] = encoder.encode( p_data );  86  }  87 

The last routine in this class simply returns the hostname of the database server. Some Internet protocols request that you transmit this information (for example, SMTP - simple mail transfer protocol):

 88  static public void java_get_hostname( String[] p_hostname )  89  throws java.net.UnknownHostException  90  {  91      p_hostname[0] = (InetAddress.getLocalHost()).getHostName();  92  }  93  94  }  95  /      Java created. 

The Java methods themselves are rather straightforward. If you recall from Chapter 19 on Java Stored Procedures, in order to get OUT parameters, we must send, what appears to be an array, to Java. Hence, most of the procedures above take the form of:

 40  static public void java_recv_data( int p_sock,  41                                     byte[][] p_data, int[] p_length) 

This allows me to return a value in p_data, and return a value in p_length. Now that we have our Java class, we are ready to build our package body for the SIMPLE_TCP_CLIENT package. It consists almost entirely of bindings to Java:

tkyte@TKYTE816> CREATE OR REPLACE PACKAGE BODY simple_tcp_client   2  as   3   4      function connect_to( p_hostname in  varchar2,   5                           p_portno   in  number ) return number   6      as language java   7      name 'jsock.java_connect_to( java.lang.String, int ) return int';   8   9  10      procedure send( p_sock in number, p_data in raw )  11      as language java  12      name 'jsock.java_send_data( int, byte[] )';  13  14      procedure recv_i ( p_sock      in number,  15                        p_data      out raw,  16                        p_maxlength in out number )  17      as language java  18      name 'jsock.java_recv_data( int, byte[][], int[] )';  19  20      procedure recv( p_sock      in   number,  21                      p_data      out  raw,  22                      p_maxlength in   number default -1 )  23      is  24          l_maxlength    number default p_maxlength;  25      begin  26          recv_i( p_sock, p_data, l_maxlength );  27          if ( l_maxlength <> -1 )  28          then  29              p_data := utl_raw.substr( p_data, 1, l_maxlength );  30          else  31              p_data := NULL;  32          end if;  33      end; 

Here, I have a RECV_I and a RECV procedure. RECV_I is a private procedure (the _I stands for internal), not directly callable out of this package. It is called by RECV. RECV provides a 'friendly' internal on top of RECV_I - it checks to see if any data was read from the socket and if so, it sets the length correctly. If you recall from the Java code above, we allocated a fixed size buffer in the RECV routine, and read up to that many bytes from the socket. We need to resize our buffer to be exactly that size here, and this is the purpose of the UTL_RAW.SUBSTR function. Otherwise, if no data was read, we simply return Null.

 34  35      procedure getline_i( p_sock      in number,  36                           p_data      out varchar2 )  37      as language java  38      name 'jsock.java_getline( int, java.lang.String[] )';  39  40      procedure getline( p_sock      in number,  41                         p_data        out raw )  42      as  43          l_data    long;  44      begin  45          getline_i( p_sock, l_data );  46          p_data := utl_raw.cast_to_raw( l_data );  47      end getline; 

Again, much like RECV_I/RECV above, GETLINE_I is an internal function called only by GETLINE. The external PL/SQL interface exposes all data as the RAW type, and the GETLINE function here simply converts the VARCHAR2 data into a RAW for us.

 48  49      procedure disconnect( p_sock     in number )  50      as language java  51      name 'jsock.java_disconnect( int )';  52  53      procedure get_gmt( p_gmt   out varchar2 )  54      as language java  55      name 'jsock.java_get_gmt( java.lang.String[] )';  56  57      procedure get_timezone( p_timezone   out varchar2 )  58      as language java  59      name 'jsock.java_get_timezone( java.lang.String[] )';  60  61      procedure get_hostname( p_hostname   out varchar2 )  62      as language java  63      name 'jsock.java_get_hostname( java.lang.String[] )';  64  65      function peek( p_sock   in  number ) return number  66      as language java  67      name 'jsock.java_peek_sock( int ) return int';  68  69      procedure b64encode( p_data in raw, p_result out varchar2 )  70      as language java  71      name 'jsock.b64encode( byte[], java.lang.String[] )';  72  end;  73  /      Package body created. 

We are now ready to test some of our functions to see that they are installed, and actually work:

tkyte@TKYTE816> declare   2    l_hostname varchar2(255);   3    l_gmt      varchar2(255);   4    l_tz       varchar2(255);   5  begin   6    simple_tcp_client.get_hostname( l_hostname );   7    simple_tcp_client.get_gmt( l_gmt );   8    simple_tcp_client.get_timezone( l_tz );   9  10    dbms_output.put_line( 'hostname ' || l_hostname );  11    dbms_output.put_line( 'gmt time ' || l_gmt );  12    dbms_output.put_line( 'timezone ' || l_tz );  13  end;  14  / hostname tkyte-dell gmt time 20010131 213415 GMT timezone EST      PL/SQL procedure successfully completed. 

An important point for running the TCP/IP components of this package is that we need special permission to use TCP/IP in the database. For more information on the DBMS_JAVA package and privileges associated with Java, please see the DBMS_JAVA section in this appendix. In this case, we specifically we need to execute:

sys@TKYTE816> begin   2  dbms_java.grant_permission(   3  grantee => 'TKYTE',   4  permission_type => 'java.net.SocketPermission',   5  permission_name => '*',   6  permission_action => 'connect,resolve' );   7  end;   8  /      PL/SQL procedure successfully completed. 

Refer to the section on DBMS_JAVA for more details on what, and how, this procedure works. In a nutshell, it allows the user TKYTE to create connections and resolve hostnames to IP addresses to any host (that's the '*' above). If you are using Oracle 8.1.5, you will not have the DBMS_JAVA package. Rather, in this version you would grant the JAVASYSPRIV to the owner of jsock. You should be aware that the JAVASYSPRIV is a very 'broad' privilege. Whereas DBMS_JAVA.GRANT_PERMISSION is very granular, JAVASYSPRIV is very broad, and conveys a lot of privileges at once. Now that I have this permission, we are ready to implement and test our SocketType, similar to the way we tested in which we tested UTL_TCP initially. Here is the body of SocketType. The type body contains very little actual code, and is mostly a layer on the SIMPLE_TCP_CLIENT package we just created. It hides the 'socket' from the caller:

tkyte@TKYTE816> create or replace type body SocketType   2  as   3   4  static function crlf return varchar2   5  is   6  begin   7      return chr(13)||chr(10);   8  end;   9  10  member function peek return number  11  is  12  begin  13      return simple_tcp_client.peek( g_sock );  14  end;  15  16  17  member procedure send( p_data in varchar2 )  18  is  19  begin  20      simple_tcp_client.send( g_sock, utl_raw.cast_to_raw(p_data) );  21  end;  22  23  member procedure send_raw( p_data in raw )  24  is  25  begin  26      simple_tcp_client.send( g_sock, p_data );  27  end;  28  29  member procedure send( p_data in clob )  30  is  31      l_offset number default 1;  32      l_length number default dbms_lob.getlength(p_data);  33      l_amt    number default 4096;  34  begin  35      loop  36          exit when l_offset > l_length;  37          simple_tcp_client.send( g_sock,  38                  utl_raw.cast_to_raw(  39                      dbms_lob.substr(p_data,l_amt,l_offset) ) );  40          l_offset := l_offset + l_amt;  41      end loop;  42  end; 

The SEND routine is overloaded for various data types, and takes a CLOB of arbitrary length. It will break the CLOB into 4 KB chunks for transmission. The SEND_RAW routine below is similar, but performs the operation for a BLOB:

 43  44  member procedure send_raw( p_data in blob )  45  is  46      l_offset number default 1;  47      l_length number default dbms_lob.getlength(p_data);  48      l_amt    number default 4096;  49  begin  50      loop  51          exit when l_offset > l_length;  52          simple_tcp_client.send( g_sock,  53                     dbms_lob.substr(p_data,l_amt,l_offset) );  54          l_offset := l_offset + l_amt;  55      end loop;  56  end;  57  58  member function recv return varchar2  59  is  60      l_raw_data    raw(4096);  61  begin  62      simple_tcp_client.recv( g_sock, l_raw_data );  63      return utl_raw.cast_to_varchar2(l_raw_data);  64  end;  65  66  67  member function recv_raw return raw  68  is  69      l_raw_data    raw(4096);  70  begin  71      simple_tcp_client.recv( g_sock, l_raw_data );  72      return l_raw_data;  73  end;  74  75  member function getline( p_remove_crlf in boolean default FALSE )  76  return varchar2  77  is  78      l_raw_data    raw(4096);  79  begin  80      simple_tcp_client.getline( g_sock, l_raw_data );  81  82      if ( p_remove_crlf ) then  83          return rtrim(  84              utl_raw.cast_to_varchar2(l_raw_data), SocketType.crlf );  85      else  86          return utl_raw.cast_to_varchar2(l_raw_data);  87      end if;  88  end;  89  90  member procedure initiate_connection( p_hostname in varchar2,  91                                 p_portno   in number )  92  is  93      l_data    varchar2(4069);  94  begin  95      -- we try to connect 10 times and if the tenth time  96      -- fails, we reraise the exception to the caller  97      for i in 1 .. 10 loop  98      begin  99          g_sock := simple_tcp_client.connect_to( p_hostname, p_portno ); 100          exit; 101      exception 102          when others then 103              if ( i = 10 ) then raise; end if; 104      end; 105      end loop; 106  end; 

We try the connection ten times in order to avoid issues with 'server busy' type messages. It is not entirely necessary, but makes it so the caller doesn't get errors as often as it otherwise might on a busy web server, or some other service.

107 108  member procedure close_connection 109  is 110  begin 111      simple_tcp_client.disconnect( g_sock ); 112      g_sock := NULL; 113  end; 114 115  end; 116  /      Type body created. 

As you can see, these are mostly convenience routines layered on top of SIMPLE_TCP_CLIENT to make this package easier to use. It also serves as a nice way to encapsulate the functionality of the SIMPLE_TCP_CLIENT in an object type. Using SocketType instead of UTL_TCP, our simple 'get a web page via a proxy' routine looks like this:

tkyte@TKYTE816> declare   2      s      SocketType := SocketType(null);   3      buffer varchar2(4096);   4  BEGIN   5      s.initiate_connection( 'proxy-server', 80 );   6      s.send( 'GET http://www.oracle.com/ HTTP/1.0'||SocketType.CRLF);   7      s.send( SocketType.CRLF);   8   9      loop  10          buffer := s.recv;  11          exit when buffer is null;  12          dbms_output.put_line( substr( buffer,1,255 ) );  13      end loop;  14      s.close_connection;  15  END;  16  / HTTP/1.1 200 OK Date: Thu, 01 Feb 2001 00:16:05 GMT Server: Apache/1.3.9 (Unix) mod_perl/1.21 ApacheJServ/1.1 yyyyyyyyyy: close Content-Type: text/html           <head> <title>Oracle Corporation</title> 

This code is not radically different from using UTL_TCP directly, but it does show how encapsulating your packages with an Object Type can add a nice feeling of object-oriented programming to your PL/SQL. If you are a Java or C++ programmer, you feel very comfortable with the above code, declaring a variable of type SocketType and then calling methods against that type. This is as opposed to declaring a variable of some record type that you pass down to each routine as UTL_TCP does. The above is more object-oriented than the procedural method first shown.

Summary

In this section we looked at the new functionality provided by the UTL_TCP package. We also investigated an alternative implementation in Java. Additionally, we packaged this functionality into a new Object Type for PL/SQL, fully encapsulating the capabilities of the TCP/IP socket nicely. We saw how easy it is to integrate networking functionality into our PL/SQL applications using this facility, and in an earlier section on UTL_HTTP, we saw how we can make use of this to provide full access to the HTTP protocol.



Expert One on One Oracle
Full Frontal PR: Getting People Talking about You, Your Business, or Your Product
ISBN: 1590595254
EAN: 2147483647
Year: 2005
Pages: 41
Authors: Richard Laermer, Michael Prichinello
BUY ON AMAZON

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