Up to: Mika Raento's Symbian Programming pages

Symbian Programming - using the WAP stack

Note: I don't recommend using the WAP stack unless you really have to. Write your own HTTP client instead, it'll be easier.

I needed to upload some data to a server and in my naivete I though an easy way to do this would be using the WAP stack (since it's the only higher level stack available on the Series 60 v.1). It turned out to be the most painful experience in Symbian programming so far. I hope this explanation and the code examples will allow you to avoid at least some of the pain.

Code

All the snippets below are taken from the classes in wap.h and wap.cpp.

Additionally you'll need the supporting code in pointer.h and symbian_auto_ptr.h. Some of the classes derive from MContextBase. You can see it's definition in app_context.h, but you'll have to implement the functions yourself if you want to use the code as it is.

The libraries you need (I might have accidentally left something out, grep is your friend) are:

All of the code works both on the emulator (as long as you've got the emulator network connection set up and the right WAP access point IP defined) and a 7650.

The way I use the WAP client to upload files assumes a cgi-bin program that accepts a POST body and the following kind of URL:
http://<server>/<path-to-script>?<OP>;<offset>;<filename-and-path>
where OP is 'C' for creating a file and 'A' for appending to file at the specified offset. A simple script that does this is put.pl.

Establishing a connection (CConnectionOpener)

There seems to be (at least) two APIs you can use to establish a network connection: RGenericAgent and CIntConnectionInitiator. I opted for the latter, as it provides a higher level interface. It proved to be a bit unintuitive but it does work.

A word of warning. In my opinion there is no reasonable way of knowing what access point to use for WAP automatically, so you'll have to ask the user for that. My class wants an IAP (Internet Access Point) id as a parameter. Getting that from the user is left as an exercise to the reader (accessing the comm db is shown later, though.

Basically you use CIntConnectionInitiator by calling ConnectL in your CActive-derived class with the right parameters like this:

	CCommsDbConnectionPrefTableView::TCommDbIapConnectionPref iConnPref;
	iConnPref.iRanking = 1; 
	iConnPref.iDirection = ECommDbConnectionDirectionOutgoing; 
	iConnPref.iDialogPref = ECommDbDialogPrefDoNotPrompt; 
	CCommsDbConnectionPrefTableView::TCommDbIapBearer bearer; 
	bearer.iBearerSet = KMaxTUint32;
	bearer.iIapId = IapID; 
	iConnPref.iBearer = bearer;

	CIntConnectionInitiator *iInitiator;
	iInitiator=CIntConnectionInitiator::NewL();
	iInitiator->ConnectL(iConnPref, iStatus);
	SetActive();

If another connection already exists, you'll have to close it first (or wait until it is closed). You can do this by calling TerminateActiveConnection() on the initiator. You can't use the same initiator instance after that though, it'll just return an error if you try to connect with it. So you'll have to create a new instance after closing the existing connection.

The CConnectionOpener class does all this, and includes retries and timeouts.

Connecting to the WAP gateway (CWap)

Before actually making any WAP requests, you have to establish a connection to a WAP gateway (this is called a WAP Session). The gateway is a server that takes WAP requests and transforms them to HTTP requests, and is normally provided by your mobile operator. It's the IP that you set up as 'Gateway IP address' in the phones connection settings (Access points).

The basic sequence of establishing the session is:

/* in class definition */
	RWAPServ	server;
	RWSPCOConn	connection;
	RWSPCOTrans	transaction;
	RWSPCOTrans	event_tx;
	RWSPCOConn::TEvent event;

/* in Connect() function */
	User::LeaveIfError(server.Connect());

	_LIT8(headers, "");
	iCap = CCapCodec::NewL();
	iCap->SetServerSDUSize(MAXSIZE+3000);
	iCap->SetClientSDUSize(MAXSIZE+3000);

	TBuf8<100> host;
	GetGwAddress(host);
	TInt port=9201;
	User::LeaveIfError(connection.Open(server, host, port, 0, EIP, EFalse));
	User::LeaveIfError(connection.Connect(headers, iCap));

	connection.GetEvent(event, event_tx, iStatus);
	SetActive();

The first thing is the connection capabilities. Here you say how big the requests can be. It is used both in the actual WAP Session negotiation but also to set up internal buffers in the WAP stack. So even if you know for sure that the gateway allows requests as big as you need, you still have to set up a big enough space so that the stack can handle it. Here MAXSIZE is maximum POST body size I'll be using and 3000 seems to be enough for the rest of the request data.

The next things you need are the gateway IP and port number. Unless you are using a very non-standard setup the port number will be 9201 (UDP is used for WAP, BTW). The gateway host IP you'll have to get from the communications database (shown in a moment). Then you open the RWSPCOConn instance, request a connection to the gateway and wait for it to complete. The GetEvent() method always needs a reference to a RWSPCOTrans. If the event that you get has something to do with a transaction (as it will later) this will hold a reference to that transaction. It doesn't have to be initialised, and doesn't hold any interesting information when just establishing the connection.

The documentation for RWSPCOConn lists the possible values event can become. For the session establishment the interesting ones are RWSPCOConn::EConnect_cnf_s (the session was established) and RWSPCOConn::EDisconnect_ind_s, RWSPCOConn::EAbort_ind_t and RWSPCOConn::EException_ind_e (something went wrong). Look at the CWap::RunL to see how the event handling is done.

One thing to note is that you cannot call Close() on the RWSPCOConn unless it has been opened successfully. So you have to keep a flag that tells you whether it is open or not (trying to close a connection that hasn't been opened will result in a panic). The same holds for RWSPCOTrans and Release().

The gateway IP

Getting the gateway IP from the communications database is fairly straightforward (having stored the IAP id in iIapId):

void CWap::GetGwAddress(TDes8& addr)
{
	bool found=false;

	CCommsDatabase* db;
	db=CCommsDatabase::NewL(EDatabaseTypeIAP);
	CleanupStack::PushL(db);

	CCommsDbTableView *view=db->OpenTableLC(TPtrC(WAP_IP_BEARER));
	
	while (view->GotoNextRecord()==KErrNone) {
		TUint32 currid;
		view->ReadUintL(TPtrC(WAP_IAP), currid);
		if (currid==iIapId) {
			found=true;
			TBuf<100> tmp;
			
			view->ReadTextL(TPtrC(WAP_GATEWAY_ADDRESS), tmp);

			CC()->ConvertFromUnicode(addr, tmp);
			break;
		}
	}
	
	CleanupStack::PopAndDestroy(2); //db, view

	if (!found || !addr.Compare(_L8("0.0.0.0")) || addr.Length()==0) {
		User::Leave(-1004);
	} 

}

Making a POST request (CWap)

So this is WAP (1.2 in Series 60 v.1), not HTTP. The protocol and headers are not the same. Go read the WSP spec if you want to understand what's going on (note that the links on the wapforum specs page are wrong. They point to www1.wapforum.org, which asks for a login and password. By replacing www1 with www you can access the docs (you have to do this twice, first for the license and then for the doc).

After establishing a connection, you can create a new transaction (request) with the following code:

	head.Zero();
	// Content-type first in POST
	// our upload doesn't care what it is, let's use image/gif since it's binary
	head.Append('\x9d');
	// Accept: */*
	head.Append('\x80'); head.Append('\x80');
	// Content-length:
	head.Append('\x8d');
	MakeContentLength(size, head);

	User::LeaveIfError(connection.CreateTransaction(RWAPConn::EPost, 
		*iURL, head, *iContents, transaction));

	connection.GetEvent(event, event_tx, iStatus);
	SetActive();

The most important part is the header. A WAP POST request 'header' (actually PDU, but the Symbian stack calls everyhing just headers) starts with the content type (just the value, not a complete header). I'm using the binary WAP header encoding here. The WAP spec states that you MUST use the binary encoding for all fields and values that an encoding exists for. I guess in practice gateways allow textual headers, but this should work even with a strict gateway. (Note that even textual headers don't look quite like HTTP headers, read the spec.)

After the content-type come actual headers. Again many gateways might work without an 'Accept' header, but some tend to refuse to handle responses for types the client has not told it can accept. The content-length is probably pretty much mandatory. The function MakeContentLength encodes the size into a WAP integer (variable length, most significant bytes first, minimal number of bytes).

After setting up the header you call CreateTransaction. Here you again pass a reference to a RWSPCOTrans. This will get filled immediately with the created transaction. You should probably always keep this separate from the one you use with GetEvent so that you can call Release on it at the right time. After creating the request you wait for it to be completed with GetEvent.

A transaction goes through two states before being completed: RWSPCOConn::EMethodInvoke_cnf_t and EMethodResult_ind_t. When it gets to the latter, you can ask for the result body with RWSPCOTrans::GetData(buffer, EResultBody).

That's pretty much it for a single request. Read through the whole CWap class to see all the gory details. Read on for what additional issues there are for multiple requests.

Making multiple POST requests in succession (CWap)

The whole POST request process goes like:

StepClient Server
1method invoke request->method invoke indication
2method invoke conf<-method invoke response
3method result indication<-method result request
4method result response->method result conf

So the client (the Symbian WAP stack) acknowledges the response to the server. This step (4) is not visible outside the stack, but seems to happen after some timeout. If the WAP request size is more than a single UDP packet can hold (about 1400 bytes of data), it gets split into multiple packets. Now if we send a new request that gets split and in the middle of sending these split up packets the stack also realises it has to send an ack for the previous response it gets confused and forgets about the request in progress.

The only way I've found to get this working without having to re-establish the session is to wait for some time between requests. The time needed seems to depend on the size of the requests. I've expirementally determined a combination of request size 25 kB and wait time 10 seconds. YMMV.

This happens both on the emulator and the target device and is quite easy to see when running against Kannel and looking at the debug information.

Tools

In addition to the docs I used some other things to figure all this out.

CommDB dump

To get to understand the communication database I wrote a class that walks through the whole DB and prints out the contents. You can download cdb.h and cdb.cpp to get the information. The code works both on the emulator and on a 7650.

Using WAP on the emulator

You can read the excellent NewLC article about setting up the emulator to connect to the network. I'm not using quite this setup though. I haven't got the Virtual Serial port software but I do have a desktop linux machine available for connecting to. So I use a null-modem cable from my laptop to the desktop and pppd in linux. I run pppd with the command while(true) do pppd call epocemu debug nodetach; done and my /etc/ppp/peers/epocemu looks like:

connect "/usr/sbin/chat -t 3600 -v -f /etc/ppp/peers/epocemu.chat"
noauth
user ppp
crtscts
lock
passive
silent

ipcp-accept-local
ipcp-accept-remote

115200
/dev/ttyS0
169.254.1.68:169.254.1.1

And /etc/ppp/peers/epocemu.chat looks like:

"" "." "CLIENT" "SERVER"

This setup should be enough to connect to The Kannel open source WAP gateway for testing. You must set up 169.254.1.68 as the WAP gateway IP with setupcomms (or hardcode in your app). If in addition to the WAP stuff want to do general IP stuff as well, you must set up NAT on your linux box like this (on kernel 2.4):

modprobe ip_tables
modprobe ip_conntrack
modprobe iptable_nat
modprobe ipt_MASQUERADE
modprobe ip_conntrack_ftp
modprobe ip_nat_ftp
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING -o eth0 -s 169.254.1.0/24 -j MASQUERADE

Although the Nokia Gateway Simulator (part of the Nokia Mobile Internet Toolkit) isn't too bad either I highly recommend Kannel for its superior debug output.

Setupcomms note

I had a lot of problems using setupcomms on my laptop. It doesn't have a numpad and the app uses the numpad for numerical input. So I had to use the numlock to switch parts of the keyboard between numpad and normal and the emulator was picking the numlock up as NULLs!. So the values I was inserting ended up having NULLs in them and of course nothing worked... I fixed this by writing a program to do the changes instead.


Mika Raento, mikie(at)iki.fi