본문 바로가기

Software/Network

[RAS] How Work Ras Dial

March 1999
Version: 1.0, 3.0

Using RAS, part 1

by Kent Reisdorph

Microsoft's Remote Access Server (RAS) can be used to establish a connection to a remote RAS server. Typically, this means dialing a modem to connect to a remote network, such as an Internet Service Provider (ISP). RAS isn't a terribly complex API, but there are significant differences in the RAS implementation between Windows NT and Windows 95. This article will explain how to use RAS to establish a connection to a remote machine. In part two of this series, we'll discuss some of the more advanced RAS functions and the built-in RAS dialogs.

How RAS works

RAS uses Windows' dial-up networking settings to make a connection. This assumes, of course, that RAS has been installed on the user's machine. RAS is installed when a user sets up dial-up networking on their machine. RAS isn't automatically installed for all Windows installations, though. If, for example, you have a permanent Internet connection, then you may never need to install dial-up networking. In that case, RAS will likely not be installed on your machine. Differences exist between Windows 95 and NT regarding how RAS operates. On Windows 95, dial-up networking automatically starts when an application attempts to connect to a remote machine. If, for example, you start a Web browser and type in a URL, Windows will automatically invoke RAS and will begin dialing. Under NT, however, this doesn't occur. It's up to the programmer to explicitly dial with RAS on NT machines. It's probably best to explicitly dial RAS in your applications so that you don't have to write code for a specific platform. In other words, always assume the lowest common denominator and write your programs accordingly.

Most of the time when dealing with RAS, you're dealing with a modem dialing out over a phone line. That isn't always true, however. Users using ISDN, for example, don't have a modem per se. Windows handles these details for you and you don't have to worry too much about the specific hardware on a particular user's machine.

RAS phonebooks

Key to RAS operations is Windows' concept of a phonebook. The phonebook contains the user's dial-up networking connection settings. This includes the device to use (usually a modem), the phone number to dial, the network connection settings, and so on. Most of the time, the phonebook will only contain one entry. In some cases, however, the phonebook will contain multiple entries. A user might have multiple entries if they typically connect to more than one remote machine. Let's say, for example, that a user has phonebook entries for an ISP, a network at work, and one for CompuServe. The phonebook will then contain three entries. You must consider multiple entries when using RAS to dial a remote machine. We'll address retrieving the phonebook entries a bit later in the article. The phonebook is handled differently in NT and Windows 95. In Windows NT, phonebooks are contained in a phonebook file (a file with a .PBK extension). Complicating the issue is the fact that the user may have more than one phonebook file (although multiple phonebooks are rarely used). In Windows 95, there's only one phonebook and it's stored in the registry, as opposed to a separate file. Later, we'll look at how the phonebook comes into play when dialing with RAS.

Dialing with RAS

Before you can use RAS in your applications, you'll need to include the RAS.H and RASERROR.H headers to your source code. Once you've done that, you can get on with the business of dialing using RAS. Dialing is a multi-step process requiring these steps:
bullet Determine the phonebook entry that will be used to make the connection.
bullet Determine whether a connection already exists.
bullet Dial the modem (or other device).
We'll examine each of these steps in the following sections.

Getting the phonebook entry

As we said earlier, you're probably only dealing with one phonebook entry. However, you must account for the possibility of multiple phonebook entries. If only one entry exists in the phonebook, then you can simply use that entry to establish the connection. If multiple entries exist in the phonebook, then you should give the user the option of selecting the phonebook entry used to dial. Phonebook entries are obtained using the RasEnumEntries function. This function retrieves the entries contained in the phonebook. RasEnumEntries is one of those interesting API functions that can return a variable number of entries. It does this by filling a buffer with a variable number of RASENTRYNAME structures. Your code needs to take that fact into account. The RASENTRYNAME structure is very simple. Here's how it's defined in RAS.H (for single-byte character versions of Windows):
RASENTRYNAMEA
{
		DWORD dwSize;
		CHAR  szEntryName[RAS_MaxEntryName + 1];
};
As you can see, the szEntryName member is the only one of significance. When RasEnumEntries returns, this member will contain the name of a specific phonebook entry. At this point an example might help. Here's the first step in retrieving phonebook entries using RasEnumEntries:
RASENTRYNAME* entries = new RASENTRYNAME[1];
entries[0].dwSize = sizeof(RASENTRYNAME);
DWORD numEntries;
DWORD size = entries[0].dwSize;
The first line in this code snippet declares an array of RASENTRYNAME structures. We initially assume there's only one entry in the phonebook so the array size is 1. Following that, we set the dwSize member of the structure to the size of a RASENTRYNAME structure. This is a necessary step and is typical of many Windows API functions. Then we declare a variable called numEntries. This variable will contain the number of entries in the phonebook after RasEnumEntries returns. Finally, we declare a variable called size. This variable must initially be set to the size of the RASENTRYNAME structure. When RasEnumEntries returns, this variable will be set to the size of the buffer required to hold all of the entry names. Now we can actually call the RasEnumEntries function. Here's the code:
DWORD res = RasEnumEntries(
	0, 0, entries, &size, &numEntries);
The first parameter to RasEnumEntries is reserved and must be set to 0. The second parameter is used to specify the phonebook to enumerate. Under Windows 95, this parameter is ignored. In Windows NT you can set this parameter to the path and filename of a particular phonebook. If you specify 0 for this parameter under NT, then Windows will use the current default phonebook. Most of the time, this is sufficient. Some applications, however, need to take multiple phonebooks into account. If you're writing an application that must take multiple phonebooks into account, then you need to write code to handle multiple phonebooks. The third parameter in RasEnumEntries is the address of the buffer that will contain the list of RASENTRYNAME structures if RasEnumEntries returns successfully. The final two parameters are the buffer size and number of entries parameters. Notice that we pass the addresses of our size and numEntries variables for these parameters.

One of two scenarios will develop when you call RasEnumEntries. First, (and most likely) the function may return success on the first call (a return value of 0 indicates success). If this happens, then you know there was only one entry in the phonebook. The numEntries variable will contain the value 1 and the entries array will contain the entry. In this instance, you can simply use the value of entries[0].szEntryName to dial RAS.

In the second scenario, RasEnumEntries will return ERROR_BUFFER_TOO_SMALL. This return value indicates more than one entry in the phonebook. You need to re-allocate the buffer and call RasEnumEntries again.

You can re-allocate the buffer in one of two ways. The size variable will contain the required size of the buffer. Using the size variable you can re-allocate the buffer like this:

delete[] entries;
entries = (RASENTRYNAME*)new char[size];
Or, if you prefer, you can use the numEntries to re-allocate the buffer like this:
delete[] entries;
entries = new RASENTRYNAME[numEntries];
Once you've re-allocated the buffer, you can call RasEnumEntries again. Before you do, however, you must set the dwSize member of the first structure in the array. For example:
entries = new RASENTRYNAME[numEntries];
entries[0].dwSize = sizeof(RASENTRYNAME);
res = RasEnumEntries(
	0, 0, entries, &size, &numEntries);
If RasEnumEntries returns 0 you can enumerate the array to retrieve the list of entry names. Typically, you'd place this list in a combo box to allow the user to select the entry he wants to use to dial. The code might look like this:
for (int i=0;i<(int)numEntries;i++)
	EntriesCb->Items->Add(entries[i].szEntryName);
Now you can move on to the second step, determining whether a connection currently exists.

Detecting an active connection

Sometimes a user might connect to the Internet using one application and then expect any other Internet-based applications to use the existing connection. Put another way, if the user is already connected to a remote machine, your application might as well use that connection. The RasEnumConnections function is used to detect active connections. Here again, there might be more than one active connection. While this situation is fairly uncommon, you need to understand that such a condition might exist.

Detecting an active connection is a two-step operation:

  1. Check to see whether there are active connections.
  2. Check the status of any active connections.
Here's the code for enumerating active connections:
RASCONN rc;
rc.dwSize = sizeof(rc);
DWORD numConns;
DWORD size;
DWORD res =
	RasEnumConnections(&rc, &size, &numConns);
This code should look familiar, as it's very similar to the code we used when we called RasEnumEntries. We're technically cheating by assuming that there will only be one active connection. If you need to account for more than one active connection, then you should use the allocate-check-re-allocate method described earlier for RasEnumEntries. If RasEnumConnections returns 0 (success), then you can check the value of the numConns variable. If numConns is 0, you know that there are no active connections. Then you can move directly to dialing RAS. If numConns is 1, then there's one active connection and you should check that connection's status. If numConns is greater than 1, you should enumerate the connections and look for the connection that matches the phonebook entry name obtained earlier.

The RASCONN structure has two members of note. The hrasconn member contains a handle to an active connection. The szEntryName member contains the phonebook entry used to make the connection.

Once you've detected an active connection, you need to check the status of that connection. This is accomplished using the RasGetConnectStatus function. Here's the code:

RASCONNSTATUS status;
status.dwSize = sizeof(status);
res = RasGetConnectStatus(rc.hrasconn, &status);
In this code snippet, we're calling RasGetConnectStatus passing the hrasconn parameter of the RASCONN structure obtained earlier with the call to RasEnumConnections and the address of a RASCONNSTATUS structure. If RasGetConnectStatus returns 0 (indicating success), you should check the rasconnstate member of the RASCONNSTATUS structure to determine the status. The rasconnstate member can contain either RASCS_Connected or RASCS_Disconnected. If rasconnstate is RASCS_Connected, then you can use the active connection. A value of RASCS_Disconnected indicates an active connection that, for whatever reason, is no longer valid. If the status is RASCS_Connected, you can save the hrascon parameter of the RASCONN structure to be used later. For example:
if (status.rasconnstate == RASCS_Connected)
	// Good connection handle, save it
	TheHandle = rc.hrasconn;
else 
	// A connection was detected but its
	// status is RASCS_Disconnected.
	TheHandle = 0;
Now we can move on to actually dialing RAS.

Dialing RAS

Now that you've determined the phonebook entry to use and the existence of active connections, you can dial. Again, dialing is a multi-step operation.

Setting the RAS dialing parameters

The first step in dialing is filling out a RASDIALPARAMS structure. Consider this code:
RASDIALPARAMS params;
params.dwSize = sizeof(params);
strcpy(params.szEntryName, PBEntry.c_str());
strcpy(params.szPhoneNumber, "");
strcpy(params.szCallbackNumber, "");
strcpy(params.szUserName, "");
strcpy(params.szPassword, "");
strcpy(params.szDomain, "TURBOPOWER");
First, note how we assign the szEntryName member. In this code example, PBEntry is an AnsiString that contains the name of the phonebook entry to use. This value is obtained as described earlier in the section, "Getting the phonebook entry." Alternatively, you can specify an empty string for the szEntryName member. When you do that, Windows will establish a simple modem connection on the first available port. If you use this method, you must provide a phone number to dial in the szPhoneNumber member. Note that in this example, the szPhoneNumber member is set to a blank string. This causes Windows to use the phone number as determined by the phonebook entry. If you want to override the phonebook entry's phone number, you can specify the new number here. The szCallbackNumber is set to an empty string, as well. This member is used when the caller requests the remote machine to call back the local machine (Windows NT only).

The szUserName and szPassword members are used to log on to the remote machine. When these members contain an empty string, Windows behaves in two different ways depending on whether the operating system is Windows NT or Windows 95. With NT, Windows uses the current user's logon credentials to log on to the remote server and no further user input is required. Windows 95, on the other hand, can't use the current logon credentials. Under Windows 95, the user will be presented with a log on dialog where they can specify their user name and password. In either situation, you can specifically set the szUserName and szPassword in code if you prefer.

The szDomain member of the RASDIALPARAMS structure can be set to the domain name of the machine to which you are connecting or to one of the following values:

"" The domain in which the remote access server is a member
"*" The domain specified in the phonebook entry settings

In most situations, you should set this member to an asterisk so the phonebook entry settings are used.

Synchronous vs. asynchronous dialing

The call to RASDial can be either synchronous or asynchronous. When using synchronous mode, the call to RASDial doesn't return until the connection operation has been carried out. If the connection was successful, RasDial will return 0. If an error occurred during dialing, RasDial will return one of the RAS error codes (defined in RASERROR.H). While synchronous mode is fairly simple to implement, it suffers from one major drawback: your application can't provide any status information to the user when in synchronous mode. When RasDial is called in asynchronous mode, RasDial begins dialing and then immediately returns control to the application. In asynchronous mode, a callback function is called at various stages of the dialing operation (the RAS callback function is described in the next section). The callback function can be used to provide feedback on the dialing operation to the user (connecting, logging on, authenticating, authenticated, etc.). In addition, you can provide error information to the user if an error occurs while connecting to the remote machine. Asynchronous mode isn't difficult to implement, and provides more control over the dialing operation. You should use asynchronous mode most of the time.

Synchronous or asynchronous operation is determined by the way RasDial is called. If the address of a callback function is provided, then RasDial operates asynchronously. If no callback function is provided, then RasDial operates synchronously.

Providing a RAS callback function

Much of the time, you'll execute RasDial asynchronously. When operating in asynchronous mode, RAS will notify your application when RAS events occur. RAS can use several methods of providing your application with status information. The RAS messaging system type is determined by the value of the dwNotifierType parameter of RasDial. Table A shows the possible values for this parameter and the type of messaging system they represent.

Table A: Possible dwNotifierType values

0xFFFFFFFF
A window handle which receives WM_RASDIALEVENT messages
0
A RasDialFunc callback function
1
A RasDialFunc1 callback function
2
A RasDialFunc2 callback function

You'll probably use a RasDialFunc1 callback function. This type of callback provides both status and error messages (the RasDialFunc callback doesn't provide error messages). In situations where multilink connections are possible, you may elect to use the RasDialFunc2. This callback provides more information than RasDialFunc1 does. A typical RAS callback function might look like this:

VOID WINAPI RasCallback(HRASCONN hrasconn,
	UINT unMsg, RASCONNSTATE rascs,
	DWORD dwError, DWORD dwExtendedError)
{
	String S = "";
	if (dwError) {
		char buff[256];
		RasGetErrorString(
			dwError, buff, sizeof(buff));
		// display error message 
		return;
	}
	switch (rascs) {
		case RASCS_DeviceConnected : 
			S = "Connected..."; break;
		case RASCS_Authenticate : 
			S = "Logging on..."; break;
		case RASCS_Connected :
			S = "Logon Complete"; break;
		// additional case statements
	}
	Form1->Memo1->Lines->Add(S);
}
This callback function provides error and status information. You can make your callback as simple or as complex as you like, as determined by the type of application you're writing. A common mistake is to try to make the callback function a member of your main form's class. A callback must be a stand-alone function and cannot be a class member function.

Calling RasDial

Finally, you're ready to call RasDial. A typical call to RasDial looks like this:
DWORD res = RasDial(
		0, 0, &params, 1, RasCallback, &hRas);
The first parameter of RasDial is used to specify extended RAS features. The extended RAS features are only available under Windows NT. We aren't using the extended features here, so we set this parameter to 0.

The second parameter is used to specify the phonebook to be used for dialing. We set this parameter to 0 so Windows will use the default phonebook. Under Windows NT, you can specify a phonebook other than the default if multiple phonebooks are defined.

The third parameter is a pointer to a RASDIALPARAMS structure. We filled this structure out previously in step one of the dialing operation.

The fourth parameter is used to specify which RAS messaging mechanism to use. Now, we pass the value 1 to indicate that we're using a RasDialFunc1 callback type. The fourth parameter is the address of the callback function itself. In this example, the name of the callback function is RasCallback. The fourth parameter is also the address of a variable that will contain a RAS connection handle. This variable will contain the connection handle when RasDial returns. If RasDial returns 0, the call was successful. If a non-zero value is returned, an error occurred. If that happens, you can use RasGetErrorString to obtain an error message to display to the user.

Terminating the connection

If you've established a new connection, then you should terminate that connection before your application terminates. Terminating a RAS connection is achieved with the RasHangUp function. The following code illustrates the proper method of calling RasHangUp:
RasHangUp(hRas);
DWORD res = 0;
while (res != ERROR_INVALID_HANDLE) {
	RASCONNSTATUS status;
	status.dwSize = sizeof(status);
	res = RasGetConnectStatus(hRas, &status);
	Sleep(0);
}
Notice that we call RasHangUp, passing the handle to the RAS connection we're terminating. The while loop insures that RAS has completed the disconnect process before allowing the application to continue on its way. Listing A shows the code for the main unit of an example program that illustrates the concepts discussed in this article. This application establishes a RAS connection on a button click. It then downloads a file from TurboPower Software's FTP site on a second button click. Finally, it hangs up the connection on a third button click. Status messages are shown in a memo on the form. We don't show the main form's header or the code for a second unit, which allows the user to select a phonebook entry. You can download the complete example program from our Web site.

Conclusion

RAS is a fairly complex and somewhat intimidating API. Establishing a simple RAS connection is involved, certainly, but once you understand how RAS works, it isn't so daunting. Next month, we'll examine some of the more advanced aspects of RAS, including connection paused states and the built-in RAS dialogs.

Listing A: RASEXU.CPP


#include <vcl.h>
#pragma hdrstop

#include "RasExU.h"
#include "EntriesU.h"

#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{
}

// The RAS callback function
VOID WINAPI RasCallback(HRASCONN hrasconn,
	UINT unMsg, RASCONNSTATE rascs,
	DWORD dwError, DWORD dwExtendedError)
{
	String S = "";
	if (dwError) {
		// Error occurred, show the error string.
		char buff[256];
		RasGetErrorString(
			dwError, buff, sizeof(buff));
		Form1->Memo1->Lines->Add(buff);
		return;
	}
	switch (rascs) {
		// Build a status string based on the
		// status message.
		case RASCS_PortOpened :
			S = "Port opened..."; break;
		case RASCS_DeviceConnected :
			S = "Connected..."; break;
		case RASCS_Authenticate :
			S = "Logging on..."; break;
		case RASCS_Authenticated :
			S = "Authenticated"; break;
		case RASCS_Connected : {
			S = "Logon Complete";
			Form1->DownloadBtn->Enabled = true;
			break;
		}
		case RASCS_Disconnected :
			S = "Disconnected"; break;
	}
	// Show the status message in the memo.
	if (S != "")
		Form1->Memo1->Lines->Add(S);
}

void __fastcall
TForm1::ConnectBtnClick(TObject *Sender)
{
	// Get the phonebook entry to use to dial.
	String PBEntry = GetPhoneBookEntry();
	if (PBEntry == "") {
		ShowMessage("Operation Cancelled");
		return;
	}
	// Check for existing connections.
	hRas = CheckForConnections();
	if (hRas) {
		Memo1->Lines->Add(
			"Using existing connection...");
		Memo1->Lines->Add("Click the "
			"Download button to transfer files.");
		DownloadBtn->Enabled = true;
		hRas = 0;
		// No need to call RasDial for an
		// existing connection.
		return;
	}
	Memo1->Lines->Add(
		"No current connection, dialing...");
	// Set up the connection params.
	RASDIALPARAMS params;
	params.dwSize = sizeof(params);
	strcpy(params.szEntryName, PBEntry.c_str());
	strcpy(params.szPhoneNumber, "");
	strcpy(params.szCallbackNumber, "");
	strcpy(params.szUserName, "");
	strcpy(params.szPassword, "");
	strcpy(params.szDomain, "*");
	// Dial.
	DWORD res = RasDial(
		0, 0, &params, 1, RasCallback, &hRas);
	if (res) {
		char buff[256];
		RasGetErrorString(res, buff, sizeof(buff));
		Memo1->Lines->Add(buff);
	}
}

HRASCONN TForm1::CheckForConnections()
{
	char buff[256];
	RASCONN rc;
	rc.dwSize = sizeof(rc);
	DWORD numConns;
	DWORD size;
	// Enumerate the connections.
	DWORD res =
		RasEnumConnections(&rc, &size, &numConns);
	if (!res) {
		// No connections, return 0.
		if (numConns == 0)
			return 0;
		// Multiple connections. Should add code to
		// handle multiple connections and decide
		// which one to use.
		if (numConns > 1)
			Memo1->Lines->Add("Multiple connections");
	}
	if (res) {
		// Error. Report it.
		RasGetErrorString(res, buff, sizeof(buff));
		Memo1->Lines->Add(buff);
	} else {
		// Get the connection status.
		RASCONNSTATUS status;
		status.dwSize = sizeof(status);
		res = RasGetConnectStatus(
			rc.hrasconn, &status);
		if (res) {
			// Error. Report it.
			RasGetErrorString(
				res, buff, sizeof(buff));
			Memo1->Lines->Add(buff);
			return 0;
		} else {
			// Found connection, show details.
			if (status.rasconnstate
					== RASCS_Connected) {
				Memo1->Lines->Add("Device type: " +
				    String(status.szDeviceType));
				Memo1->Lines->Add("Device name: " +
				    String(status.szDeviceName));
				Memo1->Lines->Add("Connected to: " +
				    String(rc.szEntryName));
				return rc.hrasconn;
			} else {
				// A connection was detected but its
				// status is RASCS_Disconnected.
				Memo1->Lines->Add
					("Connection Error");
				return 0;
			}
		}
	}
	return 0;
}

String TForm1::GetPhoneBookEntry()
{
	RASENTRYNAME* entries = new RASENTRYNAME[1];
	entries[0].dwSize = sizeof(RASENTRYNAME);
	DWORD numEntries;
	DWORD size = entries[0].dwSize;
	DWORD res = RasEnumEntries(0, 0, entries, &size, &numEntries);
	if (numEntries == 1)
		return entries[0].szEntryName;
	if (res == ERROR_BUFFER_TOO_SMALL) {
		// allocate enough memory to get all the phonebook entries
		delete[] entries;
		entries = new RASENTRYNAME[numEntries];
		entries[0].dwSize = sizeof(RASENTRYNAME);
		res = RasEnumEntries(0, 0, entries, &size, &numEntries);
		if (res) {
			char buff[256];
			RasGetErrorString(res, buff, sizeof(buff));
			ShowMessage(buff);
		}
	}
	TPBEntriesForm* form = new TPBEntriesForm(this);
	for (int i=0;i<(int)numEntries;i++)
		form->EntriesCb->Items->Add(entries[i].szEntryName);
	form->EntriesCb->ItemIndex = 0;
	String S;
	if (form->ShowModal() == mrCancel)
		S = "";
	else
		S = form->EntriesCb->Text;
	delete form;
	delete[] entries;
	return S;
}

void __fastcall
TForm1::HangUpBtnClick(TObject *Sender)
{
	if (!hRas) return;
	// Hang up.
	RasHangUp(hRas);
	// Be sure the RAS state machine has cleared.
	DWORD res = 0;
	while (res != ERROR_INVALID_HANDLE) {
		RASCONNSTATUS status;
		status.dwSize = sizeof(status);
		res = RasGetConnectStatus(hRas, &status);
		Sleep(0);
	}
	hRas = 0;
}

void __fastcall
TForm1::DownloadBtnClick(TObject *Sender)
{
	FTP->Connect();
}

void __fastcall
TForm1::FTPConnect(TObject *Sender)
{
	Memo1->Lines->Add(
		"Connected to TurboPower FTP Site");
	FTP->ChangeDir("pub");
}

void __fastcall
TForm1::FTPSuccess(TCmdType Trans_Type)
{
	switch (Trans_Type) {
		case cmdChangeDir : {
			Memo1->Lines->Add("Changed directory");
			FTP->Download(
				"00index.txt", "00index.txt");
			break;
		}
		case cmdDownload : {
			Memo1->Lines->Add("File downloaded!");
			break;
		}
	}
}

void __fastcall
TForm1::FormCreate(TObject *Sender)
{
	hRas = 0;
}

void __fastcall
TForm1::FormDestroy(TObject *Sender)
{
	// Terminate the connection if necessary
	if (hRas)
		HangUpBtnClick(this);
}