Create a client/server socket-based application that allows an Active Server Page .NET application to communicate with the iSeries.
You're probably already familiar with the
concept of using TCP/IP sockets to communicate between a client and a server
application. You may even have a socket server application on your iSeries now
that communicates with client apps running on another iSeries or PC. In this
article, I'll explain what it takes to create a client/server socket-based
application that allows an Active Server Page .NET (ASP.NET) application to
communicate with the iSeries.
Socket to Me
I'll start by taking a look at what TCP/IP sockets are
and how they work. All applications running over TCP/IP communicate through the
use of sockets. Sockets allow you to identify individual server applications at
a specific IP address. Think of it this way: If you were comparing TCP/IP
applications to a standard telephone call, the server's IP address would be the
phone number, and the socket (or port number) would be the telephone extension.
When people call your office, they probably dial a main number that gets them to
a receptionist or automated attendant that can transfer them to your phone by
identifying an extension. A TCP/IP server application simply waits in "listen"
mode for a client application to try to connect to it. Once a connection is
made, the server application will send data back and forth to the client until
the socket is closed. On the client side, the socket is defined using the
server's IP address and port number. The client application will then connect to
the server and send data back and forth until the connection is closed. Figure
1 shows a graphic example of how typical client and server socket applications
interact with each other.
Figure 1: This illustration shows how socket applications
communicate. (Click images to enlarge.)
As I mentioned, all TCP/IP applications communicate using
sockets. When you connect to a Web server on the Internet, your browser acts as
the client and connects to port 80 on the server. When you connect to an FTP
server running on the iSeries, your PC's FTP command is the client and is able
to communicate with the iSeries FTP server through port 21. When you use iSeries
Access to start a terminal session on the iSeries, PC5250 acts as the client and
communicates with the iSeries server over port 23. There are many ports like
these that are used for standard applications. The table in figure 2 contains a
list of ports used by TCP/IP applications.
Port Number
Description
13
Network Time Protocol (NTP)
21
File Transfer Protocol (FTP)
23
Telnet Protocol
25
Simple Mail Transfer Protocol (SMTP)
53
Domain Name Service (DNS)
80
Hypertext Transfer Protocol (HTTP)
119
Network News Transfer Protocol (NNTP)
161
Simple Network Management Protocol (SNMP)
443
Hypertext Transfer Protocol over SSL (HTTPS)
563
Network News Transfer Protocol over SSL
(NNTPS)
Figure 2: This table lists some commonly used TCP/IP
ports.
When you are creating a custom socket application, it's best
to avoid using any of these "well-known" port numbers to prevent conflicts with
any of these applications.
ASPX to iSeries
The big draw to creating socket-based applications is
that they allow applications written in different languages and running on
different platforms to communicate with one another. This means that a socket
server that is running on a Linux box can accept connections from and
communicate with a client application running on Windows or even on the iSeries.
To illustrate this, I've created a sample socket server application running on
the iSeries that will interact with an ASP.NET client application.
First, take a look at the iSeries server application. This sample
application will accept an iSeries user ID and return the TEXT value from the
corresponding user profile. Figure 3 contains the ILE RPG source code for the
socket server application.
H DFTACTGRP(*NO) ACTGRP('QILE') BNDDIR('QC2LE') ***************************************************************** * Compile Command: * * CRTBNDRPG PGM(XXX/SKTSERVER) SRCFILE(XXX/QRPGLESRC) * ***************************************************************** * Copy Prototypes. ***************************************************************** /COPY QRPGLESRC,RPGSOCKCPY **************************************************************** * SOCKET DATA STRUCTURES ***************************************************************** D sockaddr DS D sa_family 5u 0 D sa_data 14a D serveraddr DS D sin_family 5i 0 D sin_port 5u 0 D sin_addr 10u 0 D sin_zero 8a **************************************************************** * Miscellaneous Field Definitions ***************************************************************** D Msg S 80a Based(ptrMsg) D ErrMsg S 52a INZ D Errno S 10i 0 Based(ptrErn) D UserName S 30a INZ D svaddrlen S 10u 0 INZ D On S 10u 0 INZ(1) D SDID S 10i 0 INZ D SDID2 S 10i 0 INZ D TotCharRead S 10i 0 INZ D rc S 10i 0 INZ(0) D BufferIn S 10A INZ(' ') D BufferOut S 50A INZ(*ALL' ') D DataLen S 5P 0 INZ D XLateTable S 10A INZ D XLateTblLib S 10A INZ C********************************************************************** * Obtain a socket descriptor ID C EVAL SDID = Socket(AF_INET:SOCK_STREAM:UNUSED) C IF (SDID < 0 ) C EVAL ptrErn = GetErrNo C EVAL ptrMsg = StrError(Errno) C EVAL ErrMsg = %Char(Errno) + ' ' + Msg C ErrMsg DSPLY * C EVAL *INLR = *ON C RETURN C ENDIF
* * Allow the socket descriptor to be reusable. * C EVAL rc = SetSockOpt(SdId:SOL_SOCKET: C SOL_REUSEADDR:%ADDR(On):%SIZE(On)) C IF (rc < 0 ) C EVAL ptrMsg = StrError(Errno) C EVAL ErrMsg = %Char(Errno) + ' ' + Msg C ErrMsg DSPLY C EVAL *INLR = *ON C RETURN C ENDIF
* * Bind the socket to the defined IP Address/Port * C EVAL sin_family = AF_INET C EVAL sin_port = 2834 * retrieves your as/400's ip address C EVAL sin_addr = GetHostId C EVAL sin_zero = X'0000000000000000' C EVAL svaddrlen = %SIZE(serveraddr) C EVAL rc = Bind(SDID:%ADDR(serveraddr): C svaddrlen) C IF (rc < 0 ) C EVAL ptrErn = GetErrNo C EVAL ptrMsg = StrError(Errno) C EVAL ErrMsg = %Char(Errno) + '-' + Msg C ErrMsg DSPLY C EVAL *INLR = *ON C RETURN C ENDIF
* * Listen on the socket for up to 5 clients * C EVAL rc = Listen(SdId:5) C IF (rc < 0 ) C EVAL ptrErn = GetErrNo C EVAL ptrMsg = StrError(Errno) C EVAL ErrMsg = %Char(Errno) + '-' + Msg C ErrMsg DSPLY C EVAL *INLR = *ON C RETURN C ENDIF * * Accept new connection * C DOW ( BufferIn <> '*END' ) C EVAL rc = Accept(SDID:%ADDR(serveraddr):%ADDR( C svaddrlen)) C IF ( rc < 0 ) C EVAL ptrErn = GetErrNo C EVAL ptrMsg = StrError(Errno) C EVAL ErrMsg = %Char(Errno) + '-' + Msg C ErrMsg DSPLY C EVAL *INLR = *ON C RETURN C ELSE C EVAL SdId2 = rc C ENDIF
C DOW ( rc > 0 ) * * Read an process data from socket client * C EVAL TotCharRead = *ZERO C EVAL TotCharRead = Read(SdId2:%ADDR(BufferIn): C BufferLen) * * Convert incomming data from ASCII to EBCDIC * C IF ( TotCharRead > *ZERO ) C CALL 'QDCXLATE' 45 C PARM 10 DataLen C PARM BufferIn C PARM 'QEBCDIC' XLateTable C PARM 'QSYS' XLateTblLib
C IF ( *IN45 = *OFF AND BufferIn <> '*END') * * Call CL Program to get the defined system value * C Call 'GETUSRNAME' C Parm BufferIn C Parm UserName
* * Convert outgoing data from EBCDIC to ASCII and send back * C Eval DataLen=%Len(%Trim(UserName)) C Eval BufferOut=UserName
C CALL 'QDCXLATE' 45 C PARM DataLen C PARM BufferOut C PARM 'QASCII' XLateTable C PARM 'QSYS' XLateTblLib
C EVAL rc = Write(SdId2:%ADDR(BufferOut): DataLen) C IF ( rc < 0 ) C EVAL ptrErn = GetErrNo C EVAL ptrMsg = StrError(Errno) C EVAL ErrMsg = %Char(Errno) + '-' + Msg C ErrMsg DSPLY C EVAL *INLR = *ON C RETURN C ENDIF C ELSE * * Close socket * C Eval rc = Close(SdId2) C LEAVE C ENDIF
C ELSE * * Close socket * C Eval rc = Close(SdId2) C LEAVE C ENDIF C ENDDO C ENDDO
* * Close socket * C Eval rc = Close(SdId) C EVAL *INLR = *ON C RETURN C*********************************************************************
Figure 3: This is the ILE RPG socket server application.
This
application uses the copy file source member RPGSOCKCPY, which is included with
this article's source code.
This member defines the prototypes for the functions used within the program as
well as the constants used here. The functions defined here are contained within
the QC2LE binding directory, which gives you access to C functions from within
an ILE application. Figure 4 contains a list of the functions used here along
with a description of their use.
Function
Returns
Description
Socket()
Socket Identifier
Creates a new socket
SetSockOpt()
Status Code
Sets options for the defined socket
Bind()
Status Code
Binds to the specified socket (port)
Listen()
Status Code
Waits for a connection request on the defined
socket
Accept()
Incoming Socket ID
Accepts an incoming connection attempt
Read()
Characters Read
Reads in characters from the specified
socket
Write()
Status Code
Writes data back to the client
Close()
Status Code
Ends the client connection
Figure 4: The functions listed here are used to create the socket server
application.
As the table above explains, the socket() function
creates a new socket. The socket identifier value returned when the function is
called is used to identify the socket in the functions that follow. The first
parameter supplied here is used to define the address family. AF_INET identifies
that the address specified uses Internet Protocol (IP). The second parameter
identifies the type of socket being defined. The SOCK_STREAM constant identifies
that type to be socket streaming (TCP) protocol. The final option defines the
protocol to be used with the socket. Since the other two parameters have defined
that, set this option to UNUSED to cause the default for this value to be used.
The SetSockOpt() function defines options related to the socket. The
first parameter on this function is socket ID of the socket created with the
socket() function. The second parameter identifies the level at which the
option is being set. Data communications using TCP/IP are processed on multiple
levels or layers. The IP layer handles the lower-level network communications.
This application uses the TCP layer to send data back and forth. In this case,
the constant SOL_SOCKET identifies that the option being defined is at the
socket level. The next parameter identifies the option being changed. In this
case, the constant identifies that the socket definition is to be reusable. The
next two parameters define the option data and the length of that
data.
The bind() function binds the socket identifier defined on the
first parameter to the IP address and port identified through the
serveraddr variable. In this example, the IP address will be the IP
address of your iSeries as returned by the GetHostId function. The port number
is defined as 2834. The third parameter for the bind function is used to
identify the length of the values stored in the serveraddr variable.
The listen() function instructs the program to wait for a connection
attempt on the socket defined by the socket identifier on the first parameter.
The second parameter defines the number of connections to be accepted at one
time. Program execution will stall at this statement until a client application
attempts to make a connection to this socket.
The accept() function is
executed once a connection attempt is made. This function accepts the same three
parameters used for the bind() function: the socket ID, the server address, and
the length of the server address value. The value returned by this function
uniquely identifies each of the possible incoming connections for future
statements.
The read() function reads data coming in through the socket.
The first parameter is the socket identifier returned by the accept() function.
The second parameter is the pointer to the BufferIn variable, which will contain
the incoming data. This function returns a value that contains the number of
characters read into the BufferIn variable. The sample application then uses the
QDCXLATE API to convert the incoming data from ASCII to EBCDIC. The value from
BufferIn, which should contain an iSeries user ID, is then sent to the CL
program GetUserName. This program uses RTVUSRPRF to retrieve the TEXT value
related to the user profile. This value is sent back to the client socket
application. The QDCXLATE API is again used to translate the EBCDIC data back to
ASCII.
The write() function returns data to the client application. The
first parameter used with this function is the socket ID created by the accept()
function. The second parameter is the pointer to the BufferOut variable,
which contains the outgoing data. The third parameter defines the length of the
outgoing data. The sample application continues this process until there is no
more data to be read in. At that point, the remote socket is closed.
The
close() function ends the socket. The single parameter on this function is the
socket identifier for the socket to be closed. In this sample application, both
the primary socket identifier and the secondary identifiers that relate to each
client connection are used along with this function.
The server
application will remain active until it receives a value of *END from the
client. At that point, the server application will close the socket and end. Use
the CRTBNDRPG program to create this program, as shown here:
You'll also need to create the CL Program GETUSRNAME, which can be found
with this article's companion code.
Now that the socket server
application is in place, I'll explain what you need to do on the client side.
ASP.NET Socket Client
If you're unfamiliar with ASP.NET, let me explain:
This extension to Microsoft's Internet Information Server (IIS) is part of the
.NET (that's "dot net") framework, which can be downloaded from Microsoft's web
site here.
ASP.NET extends Microsoft's Active Server Pages (ASP) by extending the language
options available. Within ASP, you have the option of using Visual Basic
Scripting (VBScript) language or JavaScript. ASP.NET supports any language that
is compliant with the .NET framework. This currently includes Visual Basic.NET,
Visual C#, Visual C++, and Visual J#. Once you have downloaded the .NET
framework from the link above, install the framework on a machine running
Microsoft IIS. This sample client application allows the user to key in an
iSeries user ID and receive back the corresponding user's name. Figure 5
contains the source for the Web page that contains the client
application.
<%@ Page Language="VisualBasic" Debug="true" %> Socket Client ASP.NET Application
Value="Get User Name" onserverclick="GetUserName">
Dim sock As New System.Net.Sockets.Socket(Net.Sockets.AddressFamily.InterNetwork, _ Net.Sockets.SocketType.Stream, Net.Sockets.ProtocolType.Tcp) Dim RecvBytes(256) As Byte, x AS Long Dim hostadd As System.Net.IPAddress Dim bytes As Int32 Dim Str As String Sub GetUserName(sender As Object, e As System.EventArgs) If UserID.Value = "" Then Response.Write("Please Enter The User ID") Else hostadd = System.Net.IPAddress.Parse("192.168.0.1") Dim EPhost As New System.Net.IPEndPoint(hostadd, 2834) Dim UIDBytes(11) AS Byte sock.Connect(EPhost) Response.Write("Connecting! ") Response.Flush If Not sock.Connected() Response.Write("Unable to Connect! ") Response.End End If Response.Write("Connected! ") Response.Flush UIDBytes = Encoding.ASCII.GetBytes(LEFT(UserID.Value & " ",10)) Response.Write("Sending " & UIDBytes.Length & " bytes! ") Response.Flush sock.Send(UIDBytes,10,System.Net.Sockets.SocketFlags.None) bytes = sock.Receive(RecvBytes, RecvBytes.Length, 0) If UserID.Value="*END" Then Response.Write("Socket Server Ended! ") Response.Flush Else
Response.Write(bytes & " bytes received! ") Response.Flush For x = 0 To bytes - 1 Str = Str & Chr(RecvBytes.GetValue(x)) Next Response.Write("User Name=" & Str & " ") str="" End If Response.Write("Complete! ") Response.Flush sock.close End If End Sub