Cross-Platform GUI Programming with wxWidgets

As an introduction to socket programming with wxWidgets, let's jump right in to an event-based client/server example. The code is fairly readable with just a basic background in socket programming. For brevity, the GUI elements of the program are omitted, and we focus only on the socket functions; the complete application is available on the CD-ROM in examples/chap18. The detailed socket API reference follows the order of the code in the example.

The program performs a very simple task. The server listens for connections, and when a connection is made, the server reads ten characters from the client and then sends those same ten characters back to the client. Likewise, the client creates a connection, sends ten characters, and then receives ten characters in return. The string sent by the client is hard-coded in the example to 0123456789. The server and client programs are illustrated in Figure 18-1.

Figure 18-1. Socket server and client programs

The Client

This is the code for the client program.

BEGIN_EVENT_TABLE(MyFrame, wxFrame) EVT_MENU(CLIENT_CONNECT, MyFrame::OnConnectToServer) EVT_SOCKET(SOCKET_ID, MyFrame::OnSocketEvent) END_EVENT_TABLE() void MyFrame::OnConnectToServer(wxCommandEvent& WXUNUSED(event)) { wxIPV4address addr; addr.Hostname(wxT("localhost")); addr.Service(3000); // Create the socket wxSocketClient* Socket = new wxSocketClient(); // Set up the event handler and subscribe to most events Socket->SetEventHandler(*this, SOCKET_ID); Socket->SetNotify(wxSOCKET_CONNECTION_FLAG | wxSOCKET_INPUT_FLAG | wxSOCKET_LOST_FLAG); Socket->Notify(true); // Wait for the connection event Socket->Connect(addr, false); } void MyFrame::OnSocketEvent(wxSocketEvent& event) { // The socket that had the event wxSocketBase* sock = event.GetSocket(); // Common buffer shared by the events char buf[10]; switch(event.GetSocketEvent()) { case wxSOCKET_CONNECTION: { // Fill the arry with the numbers 0 through 9 // as characters char mychar = '0'; for (int i = 0; i < 10; i++) { buf[i] = mychar++; } // Send the characters to the server sock->Write(buf, sizeof(buf)); break; } case wxSOCKET_INPUT: { sock->Read(buf, sizeof(buf)); break; } // The server hangs up after sending the data case wxSOCKET_LOST: { sock->Destroy(); break; } } }

The Server

This is the code for the server program.

BEGIN_EVENT_TABLE(MyFrame, wxFrame) EVT_MENU(SERVER_START, MyFrame::OnServerStart) EVT_SOCKET(SERVER_ID, MyFrame::OnServerEvent) EVT_SOCKET(SOCKET_ID, MyFrame::OnSocketEvent) END_EVENT_TABLE() void MyFrame::OnServerStart(wxCommandEvent& WXUNUSED(event)) { // Create the address - defaults to localhost:0 initially wxIPV4address addr; addr.Service(3000); // Create the socket. We maintain a class pointer so we can // shut it down m_server = new wxSocketServer(addr); // We use Ok() here to see if the server is really listening if (! m_server->Ok()) { return; } // Set up the event handler and subscribe to connection events m_server->SetEventHandler(*this, SERVER_ID); m_server->SetNotify(wxSOCKET_CONNECTION_FLAG); m_server->Notify(true); } void MyFrame::OnServerEvent(wxSocketEvent& WXUNUSED(event)) { // Accept the new connection and get the socket pointer wxSocketBase* sock = m_server->Accept(false); // Tell the new socket how and where to process its events sock->SetEventHandler(*this, SOCKET_ID); sock->SetNotify(wxSOCKET_INPUT_FLAG | wxSOCKET_LOST_FLAG); sock->Notify(true); } void MyFrame::OnSocketEvent(wxSocketEvent& event) { wxSocketBase *sock = event.GetSocket(); // Process the event switch(event.GetSocketEvent()) { case wxSOCKET_INPUT: { char buf[10]; // Read the data sock->Read(buf, sizeof(buf)); // Write it back sock->Write(buf, sizeof(buf)); // We are done with the socket, destroy it sock->Destroy(); break; } case wxSOCKET_LOST: { sock->Destroy(); break; } } }

Connecting to a Server

This section explains how to initiate a client connection to a server using the wxSockAddress and wxSocketClient classes.

Socket Addresses

All socket address classes derive from the abstract base class wxSockAddress, providing a common parameter type for socket methods regardless of the address protocol being used. The wxIPV4address class provides all of the methods necessary for specifying a remote host using the current standard Internet address scheme, IPv4. A wxIPV6address class is partially implemented and will certainly be completed when IPv6 is more widely available.

Note: When representing addresses as unsigned longs, network order is expected, and network order is always returned. Network order corresponds to big endian (Intel or AMD x86 architecture is little endian; Apple's architecture is big endian). Depending on how the unsigned long addresses are stored or entered, you can probably use the byte-order macro wxINT32_SWAP_ON_LE, which will swap the byte order only on little endian platforms. For example:

IPV4addr.Hostname(wxINT32_SWAP_ON_LE(longAddress));

Hostname takes either a wxString for a string address (for example, www. wxwidgets.org) or an IP address in 4-byte unsigned long format (in big endian, as noted previously). Without any parameters, Hostname returns the name of the currently specified host.

Service sets the remote port, using either a wxString description for a well-known port or an unsigned short for any port. Without any parameters, Service returns the port number currently chosen.

IPAddress provides a dotted-decimal notation representation in a wxString of the remote host.

AnyAddress sets the address to any of the addresses of the current machine. This is the same as setting an address to INADDR_ANY.

Socket Clients

The wxSocketClient class derives from wxSocketBase and inherits all of the common socket methods. The only methods added to the client class are those necessary to initiate and establish a connection to a remote server.

Connect takes a wxSockAddress parameter telling the socket client the address and port for the connection. As mentioned earlier, you would use a class such as wxIPV4address rather than wxSockAddress directly. The second parameter, a boolean, defaults to true, indicating that the call to Connect should block until the connection is established. If this is done from the main GUI thread, the GUI will block while connecting.

WaitOnConnect can be used after a call to Connect if Connect was told not to block. The first parameter is the number of seconds to wait, and the second parameter is the number of milliseconds to wait. If the connection succeeds or definitively fails (for example, if the host does not exist), true is returned. If a timeout occurs, false is returned. Passing -1 for the number of seconds specifies the default timeout value, which is 10 minutes unless overridden with a call to SetTimeout.

Socket Events

All socket events are filtered through one event, EVT_SOCKET.

EVT_SOCKET(identifier, function) sends socket events for the socket identifier to the specified function. The function should take a wxSocketEvent parameter.

The wxSocketEvent class is by itself very simple, but by providing both the event type and the socket for which the event was generated, the need to manually store socket pointers is reduced.

Socket Event Types

Table 18-1 lists the event types that are returned from GetSocketEvent.

Table 18-1. Socket Event Types

wxSOCKET_INPUT

Issued whenever there is data available for reading. This will be the case if the input queue was empty and new data arrives, or if the application has read some data but there is still more data available.

wxSOCKET_OUTPUT

Issued when a socket is first connected with Connect or accepted with Accept. After that, new events will be gen-erated only after an output operation fails and buffer space becomes available again.

wxSOCKET_CONNECTION

Issued when a delayed connection request completes successfully (client) or when a new connection arrives at the incoming queue (server).

wxSOCKET_LOST

Issued when a close indication is received for the socket. This means that the connection broke down or that the peer closed it. This event will also be issued if a connection request fails.

wxSocketEvent Major Member Functions

wxSocketEvent is used as a parameter to socket event handlers.

GetSocket returns a wxSocketBase pointer to the socket that generated this event.

GetSocketEvent returns the event type of this socket event, as per Table 18-1.

Using Socket Events

In order to use socket events, you must provide an event handler and specify which events you want to receive for processing. The wxSocketBase class gives you several methods for using events, which you can see being used in the server example program after the socket listener is created. Note that the event handling parameters affect only the socket on which they are set, so you need to specify the events you want to receive for each socket.

SetEventHandler takes a reference to an event handler and an event identifier. The event identifier should correspond to an entry in the event table for the event handler class.

SetNotify takes a bit-list of the socket events for which you want to be notified. For example, wxSOCKET_INPUT_FLAG | wxSOCKET_LOST_FLAG would send an event when there is data to read on the socket or when the socket is closed.

Notify takes a boolean indicating whether you want to receive events. This allows you to enable or disable events as needed without reconfiguring the events that you want to receive.

Socket Status and Error Notifications

Before discussing sending and receiving data, we describe the auxiliary methods for status and error notification so that we can refer to them from the data methods' descriptions.

Close shuts down the socket, disabling further data transmission. The peer is explicitly notified that you have closed the socket. Note that events might have been queued already when you close the socket, so you must be prepared to continue receiving socket events even after closing the socket.

Destroy is used instead of the delete operator because events might reach the socket after it has been deleted if delete were used. Destroy closes the socket and adds the socket to the list of objects to be deleted on idle time, after all events have been processed.

Error returns TRue if an error occurred in the last operation.

GetPeer returns a wxSockAddress reference containing information about the peer side of the connection, such as IP address and port.

IsConnected returns true if the socket is connected and false otherwise.

LastCount returns the number of bytes read or written by the last I/O call.

LastError returns the last error. Note that a successful operation does not update the error code, so Error must be used first to determine whether an error occurred. Table 18-2 lists the error code values.

Table 18-2. Socket Error Codes

wxSOCKET_INVOP

Invalid operation, such as using an invalid address type.

wxSOCKET_IOERR

I/O error, such as being unable to initialize a socket.

wxSOCKET_INVADDR

Invalid address, which will occur when trying to connect without specifying an address or when the address is malformed.

wxSOCKET_INVSOCK

A socket was used in an invalid way or wasn't properly initialized.

wxSOCKET_NOHOST

The specified address does not exist.

wxSOCKET_INVPORT

An invalid port was specified.

wxSOCKET_WOULDBLOCK

The socket is non-blocking, and the operation would block (see the discussion of socket modes).

wxSOCKET_TIMEDOUT

The socket operation exceeded the timeout.

wxSOCKET_MEMERR

Memory could not be allocated for the socket operation.

Ok returns true for a socket client only when the client is connected to server, and it only returns true for a socket server if the socket could bind to the port and is listening.

SetTimeout specifies how long to wait, in seconds, before a blocking socket operation times out. The default value is 10 minutes.

Sending and Receiving Socket Data

wxSocketBase provides a variety of basic and advanced methods for reading and writing socket data. All of the read and write operations store the results of the operation and enable you to access the number of bytes read with LastCount and the last error with LastError.

Reading

Discard deletes all incoming data from the socket buffer.

Peek enables you to copy data from the socket buffer without removing the data from the buffer. You must provide a buffer for the data and the maximum number of bytes to peek.

Read pulls data from the socket buffer and copies it to the specified buffer, up to the maximum size specified.

ReadMsg reads data sent by WriteMsg into the specified buffer, up to the maximum size specified. If the buffer becomes full, the rest of the data is discarded. ReadMsg always waits for the full message sent with WriteMsg to arrive unless an error occurs.

Unread copies data from the data buffer back into the socket buffer. You must also specify how many bytes to put back.

Writing

Write sends data over the socket connection; you must specify a pointer to the data and the number of bytes to send.

WriteMsg is similar to Write, except that WriteMsg adds a header to the data being sent so that the call to ReadMsg on the other end will know exactly how much data to read. Note that data sent with WriteMsg should always be read by a call to ReadMsg.

Creating a Server

The wxSocketServer class adds only a few methods to the wxSocketBase class for creating a listener and accepting connections. In order to create a server, you must specify what port to listen on for incoming connections. wxSocketServer uses the same wxIPV4address class used by wxSocketClient, except without specifying a remote host. In most cases, you should call Ok after creating a socket server to verify that the socket is bound and listening.

wxSocketServer Major Member Functions

wxSocketServer accepts an address object specifying the listen port, and optional socket flags (see the "Socket Flags" section later in this chapter).

Accept returns a new socket connection if one is available, optionally waiting for the connection to be made or returning NULL immediately if no connections are pending. If the wait flag is specified, the GUI will block.

AcceptWith works just like Accept, but you must pass in an already existing wxSocketBase object (by reference), and a boolean is returned indicating whether a connection was accepted.

WaitForAccept takes a seconds parameter and a milliseconds parameter for how long to wait for a connection, returning TRue when a connection is available, or false if the time period elapses without a connection arriving.

Handling a New Connection Event

When the listening socket detects an incoming connection, a connection event is sent for processing. From the event handler, you can accept the connection and perform any necessary immediate processing. Assuming that the connection has some longevity and isn't immediately closed, you also need to specify an event handler for the new socket. Remember that a listening socket continues to listen until closed, and new sockets are created for each new connection. In the lifetime of a server program, the same listening socket can spawn thousands of new sockets.

Socket Event Recap

From the programmer's standpoint, event-based sockets are a boon for easily processing socket data, eliminating the need for creating and shutting down threads. The example program doesn't use threads, but the GUI will never block waiting for data. Because read commands are not issued until there is data to read, calls to read will immediately succeed and return the available data. If larger amounts of data need to be read, the data can be read in pieces and added to a buffer. Alternatively, a call can be made to Peek to determine how much data is available, and if not enough data has arrived, the application can simply wait for the next input event to arrive.

Next, we will look at how to use different socket flags to change a socket's behavior.

    Категории