|
|
Function | Event Description |
OnAccept | This function is called on a listening socket to signal that a connection request from another application is waiting to be accepted. |
OnClose | This function is called on a socket to signal that the application on the other end of the connection has closed its socket or that the connection has been lost. This should be followed by closing the socket that received this notification. |
OnConnect | This function is called on a socket to signal that the connection with another application has been completed and that the application can now send and receive messages through the socket. |
OnReceive | This function is called to signal that data has been received through the socket connection and that the data is ready to be retrieved by calling the Receive function. |
OnSend | This function is called to signal that the socket is ready and available for sending data. This function is called right after the connection has been completed. Usually, the other time that this function is called is when your application has passed the Send function more data than can be sent in a single packet. In this case, this is a signal that all of the data has been sent, and the application can send the next buffer-full of data. |
Whenever any of the CAsyncSocket member functions return an error, either FALSE for most functions or SOCKET_ERROR on the Send and Receive functions, you can call the GetLastError method to get the error code. This function returns only error codes, and you have to look up the translation yourself. All the Winsock error codes are defined with constants, so you can use the constants in your code to determine the error message to display for the user, if any. You can use the GetLastError function as follows:
int iErrCode; iErrCode = m_sMySocket.GetLastError(); switch (iErrCode) { case WASNOTINITIALISED: . . . }
For the sample application that you will build today, you'll create a simple dialog application that can function as either the client or server in a Winsock connection. This will allow you to run two copies of the sample application, one for each end of the connection, on the same computer or to copy the application to another computer so that you can run the two copies on separate computers and see how you can pass messages across a network. Once the application has established a connection with another application, you will be able to enter text messages and send them to the other application. When the message has been sent, it will be added to a list of messages sent. Each message that is received will be copied into another list of all messages received. This will allow you to see the complete list of what is sent and received. It will also allow you to compare what one copy of the application has sent and what the other has received. (The two lists should be the same.)
For today's sample application, just to keep things simple, you'll create a dialog-style application. Everything that you are doing in today's application can be done in an SDI or MDI application just as easily as with a dialog-style application. By using a dialog-style application today, we are getting everything that might distract from the basic socket functionality (such as questions about whether the socket variable belongs in the document or view class, how much of the application functionality belongs in which of these two classes, and so on) away from the sample application.
To start today's sample application, create a new MFC AppWizard project, giving the project a suitable name, such as Sock. On the first step of the AppWizard, specify that the application will be a dialog-based application. On the second step of the AppWizard, specify that the application should include support for Windows Sockets, as in Figure 20.3. You can accept the default settings for the rest of the options in the AppWizard.
Once you create your application shell, you can lay out the main dialog for your application. On this dialog, you'll need a set of radio buttons to specify whether the application is running as the client or server. You'll also need a couple of edit boxes for the computer name and port that the server will be listening on. Next, you'll need a command button to start the application listening on the socket or opening the connection to the server, and a button to close the connection. You'll also need an edit box for entering the message to be sent to the other application and a button to send the message. Finally, you'll need a couple of list boxes into which you can add each of the messages sent and received. Place all these controls on the dialog, as shown in Figure 20.4, setting all of the control properties as specified in Table 20.2.
FIGURE 20.3. Including sockets support.
FIGURE 20.4. The main dialog layout.
Object | Property | Setting |
Group Box | ID | IDC_STATICTYPE |
|
Caption | Socket Type |
Radio Button | ID | IDC_RCLIENT |
|
Caption | &Client |
|
Group | Checked |
Radio Button | ID | IDC_RSERVER |
|
Caption | &Server |
Static Text | ID | IDC_STATICNAME |
|
Caption | Server &Name: |
Edit Box | ID | IDC_ESERVNAME |
Static Text | ID | IDC_STATICPORT |
|
Caption | Server &Port: |
Edit Box | ID | IDC_ESERVPORT |
Command Button | ID | IDC_BCONNECT |
|
Caption | C&onnect |
Command Button | ID | IDC_BCLOSE |
|
Caption | C&lose |
|
Disabled | Checked |
Static Text | ID | IDC_STATICMSG |
|
Caption | &Message: |
|
Disabled | Checked |
Edit Box | ID | IDC_EMSG |
|
Disabled | Checked |
Command Button | ID | IDC_BSEND |
|
Caption | S&end |
|
Disabled | Checked |
Static Text | ID | IDC_STATIC |
|
Caption | Sent: |
List Box | ID | IDC_LSENT |
|
Tab Stop | Unchecked |
|
Sort | Unchecked |
|
Selection | None |
Static Text | ID | IDC_STATIC |
|
Caption | Received: |
List Box | ID | IDC_LRECVD |
|
Tab Stop | Unchecked |
|
Sort | Unchecked |
|
Selection | None |
Once you have the dialog designed, open the Class Wizard to attach variables to the controls on the dialog, as specified in Table 20.3.
Object | Name | Category | Type |
IDC_BCONNECT | m_ctlConnect | Control | CButton |
IDC_EMSG | m_strMessage | Value | CString |
IDC_ESERVNAME | m_strName | Value | CString |
IDC_ESERVPORT | m_iPort | Value | int |
IDC_LRECVD | m_ctlRecvd | Control | CListBox |
IDC_LSENT | m_ctlSent | Control | CListBox |
IDC_RCLIENT | m_iType | Value | int |
So that you can reuse the Connect button to place the server application into listen mode, you'll add a function to the clicked event message for both radio buttons, changing the text on the command button depending on which of the two is currently selected. To add this functionality to your application, add a function to the BN_CLICKED event message for the IDC_RCLIENT control ID, naming the function OnRType. Add the same function to the BN_CLICKED event message for the IDC_RSERVER control ID. Edit this function, adding the code in Listing 20.1.
1: void CSockDlg::OnRType() 2: { 3: // TODO: Add your control notification handler code here 4: // Sync the controls with the variables 5: UpdateData(TRUE); 6: // Which mode are we in? 7: if (m_iType == 0) // Set the appropriate text on the button 8: m_ctlConnect.SetWindowText("C&onnect"); 9: else 10: m_ctlConnect.SetWindowText("&Listen"); 11: }
Now, if you compile and run your application, you should be able to select one and then the other of these two radio buttons, and the text on the command button should change to reflect the part the application will play, as in Figure 20.5.
FIGURE 20.5. Changing the button text.
So that you will be able to capture and respond to the socket events, you'll create your own descendent class from CAsyncSocket. This class will need its own versions of the event functions, as well as a means of passing this event to the dialog that the object will be a member of. So that you can pass each of these events to the dialog-class level, you'll add a pointer to the parent dialog class as a member variable of your socket class. You'll use this pointer to call event functions for each of the socket events that are member functions of the dialog, after checking to make sure that no errors have occurred (of course).
To create this class in your application, select Insert | New Class from the menu. In the New Class dialog, leave the class type with the default value of MFC Class. Enter a name for your class, such as CMySocket, and select CAsyncSocket from the list of available base classes. This is all that you can specify on the New Class dialog, so click the OK button to add this new class to your application.
Once you have created the socket class, add a member variable to the class to serve as a pointer to the parent dialog window. Specify the variable type as CDialog*, the variable name as m_pWnd, and the access as private. You also need to add a method to the class to set the pointer, so add a member function to your new socket class. Specify the function type as void, the declaration as SetParent(CDialog* pWnd), and the access as public. Edit this new function, setting the pointer passed as a parameter to the member variable pointer, as in Listing 20.2.
1: void CMySocket::SetParent(CDialog *pWnd) 2: { 3: // Set the member pointer 4: m_pWnd = pWnd; 5: }
The only other thing that you need to do to your socket class is add the event functions, which you'll use to call similarly named functions on the dialog class. To add a function for the OnAccept event function, add a member function to your socket class. Specify the function type as void, the function declaration as OnAccept(int nErrorCode), and the access as protected and check the virtual check box. Edit this function, adding the code in Listing 20.3.
1: void CMySocket::OnAccept(int nErrorCode) 2: { 3: // Were there any errors? 4: if (nErrorCode == 0) 5: // No, call the dialog's OnAccept function 6: ((CSockDlg*)m_pWnd)->OnAccept(); 7: }
Add similar functions to your socket class for the OnConnect, OnClose, OnReceive, and OnSend functions, calling same-named functions in the dialog class, which you'll add later. After you've added all these functions, you'll need to include the header file for your application dialog in your socket class, as in line 7 of Listing 20.4.
1: // MySocket.cpp: implementation file 2: // 3: 4: #include "stdafx.h" 5: #include "Sock.h" 6: #include "MySocket.h" 7: #include "SockDlg.h"
Once you've added all the necessary event functions to your socket class, you'll add a variable of your socket class to the dialog class. For the server functionality, you'll need two variables in the dialog class, one to listen for connection requests and the other to be connected to the other application. Because you will need two socket objects, add two member variables to the dialog class (CSockDlg). Specify the type of both variables as your socket class (CMySocket) and the access for both as private. Name one variable m_sListenSocket, to be used for listening for connection requests, and the other m_sConnectSocket, to be used for sending messages back and forth.
Once you've added the socket variables, you'll add the initialization code for all the variables. As a default, set the application type to client, the server name as loopback, and the port to 4000. Along with these variables, you'll set the parent dialog pointers in your two socket objects so that they point to the dialog class. You can do this by adding the code in Listing 20.5 to the OnInitDialog function in the dialog class.
NOTE: The computer name loopback is a special name used in the TCP/IP network protocol to indicate the computer you are working on. It's an internal computer name that is resolved to the network address 127.0.0.1. This is a computer name and address that is commonly used by applications that need to connect to other applications running on the same computer.
1: BOOL CSockDlg::OnInitDialog() 2: { 3: CDialog::OnInitDialog(); 4: 5: // Add "About..." menu item to system menu. 6: . . . 26: SetIcon(m_hIcon, FALSE); // Set small icon 27: 28: // TODO: Add extra initialization here 29: // Initialize the control variables 30: m_iType = 0; 31: m_strName = "loopback"; 32: m_iPort = 4000; 33: // Update the controls 34: UpdateData(FALSE); 35: // Set the socket dialog pointers 36: m_sConnectSocket.SetParent(this); 37: m_sListenSocket.SetParent(this); 38: 39: return TRUE; // return TRUE unless you set the focus to a Âcontrol 40: }
When the user clicks the Connect button, you'll disable all the top controls on the dialog. At this point, you don't want the user to think that she is able to change the settings of the computer that she's connecting to or change how the application is listening. You'll call the Create function on the appropriate socket variable, depending on whether the application is running as the client or server. Finally, you'll call either the Connect or Listen function to initiate the application's side of the connection. To add this functionality to your application, open the Class Wizard and add a function to the BN_CLICKED event message for the Connect button (ID IDC_BCONNECT). Edit this function, adding the code in Listing 20.6.
1: void CSockDlg::OnBconnect() 2: { 3: // TODO: Add your control notification handler code here 4: // Sync the variables with the controls 5: UpdateData(TRUE); 6: // Disable the connection and type controls 7: GetDlgItem(IDC_BCONNECT)->EnableWindow(FALSE); 8: GetDlgItem(IDC_ESERVNAME)->EnableWindow(FALSE); 9: GetDlgItem(IDC_ESERVPORT)->EnableWindow(FALSE); 10: GetDlgItem(IDC_STATICNAME)->EnableWindow(FALSE); 11: GetDlgItem(IDC_STATICPORT)->EnableWindow(FALSE); 12: GetDlgItem(IDC_RCLIENT)->EnableWindow(FALSE); 13: GetDlgItem(IDC_RSERVER)->EnableWindow(FALSE); 14: GetDlgItem(IDC_STATICTYPE)->EnableWindow(FALSE); 15: // Are we running as client or server? 16: if (m_iType == 0) 17: { 18: // Client, create a default socket 19: m_sConnectSocket.Create(); 20: // Open the connection to the server 21: m_sConnectSocket.Connect(m_strName, m_iPort); 22: } 23: else 24: { 25: // Server, create a socket bound to the port specified 26: m_sListenSocket.Create(m_iPort); 27: // Listen for connection requests 28: m_sListenSocket.Listen(); 29: } 30: }
Next, to complete the connection, you'll add the socket event function to the dialog class for the OnAccept and OnConnect event functions. These are the functions that your socket class is calling. They don't require any parameters, and they don't need to return any result code. For the OnAccept function, which is called for the listening socket when another application is trying to connect to it, you'll call the socket object's Accept function, passing in the connection socket variable. Once you've accepted the connection, you can enable the prompt and edit box for entering and sending messages to the other application.
To add this function to your application, add a member function to the dialog class (CSockDlg). Specify the function type as void, the declaration as OnAccept, and the access as public. Edit the function, adding the code in Listing 20.7.
1: void CSockDlg::OnAccept() 2: { 3: // Accept the connection request 4: m_sListenSocket.Accept(m_sConnectSocket); 5: // Enable the text and message controls 6: GetDlgItem(IDC_EMSG)->EnableWindow(TRUE); 7: GetDlgItem(IDC_BSEND)->EnableWindow(TRUE); 8: GetDlgItem(IDC_STATICMSG)->EnableWindow(TRUE); 9: }
For the client side, there's nothing to do once the connection has been completed except enable the controls for entering and sending messages. You'll also enable the Close button so that the connection can be closed from the client side (but not the server side). To add this functionality to your application, add another member function to the dialog class (CSockDlg). Specify the function type as void, the function declaration as OnConnect, and the access as public. Edit the function, adding the code in Listing 20.8.
1: void CSockDlg::OnConnect() 2: { 3: // Enable the text and message controls 4: GetDlgItem(IDC_EMSG)->EnableWindow(TRUE); 5: GetDlgItem(IDC_BSEND)->EnableWindow(TRUE); 6: GetDlgItem(IDC_STATICMSG)->EnableWindow(TRUE); 7: GetDlgItem(IDC_BCLOSE)->EnableWindow(TRUE); 8: }
If you could compile and run your application now, you could start two copies, put one into listen mode, and then connect to it with the other. Unfortunately, you probably can't even compile your application right now because your socket class is looking for several functions in your dialog class that you haven't added yet. Add three member functions to the dialog class (CSockDlg). Specify all of them as void functions with public access. Specify the first function's declaration as OnSend, the second as OnReceive, and the third as OnClose. You should now be able to compile your application.
Once you've compiled your application, start two copies of the application, side-by-side. Specify that one of these two should be the server, and click the Listen button to put it into listen mode. Leave the other as the client and click the Connect button. You should see the connection controls disable and the message sending controls enable as the connection is made, as in Figure 20.6.
FIGURE 20.6. Connecting the two applications.
TIP: Be sure that you have the server application listening before you try to connect it to the client application. If you try to connect to it with the client application before the server is listening for the connection, the connection will be rejected. Your application will not detect that the connection was rejected because you haven't added any error handling to detect this event.
TIP: To run these applications and get them to connect, you'll need TCP/IP running on your computer. If you have a network card in your computer, you may already have TCP/IP running. If you do not have a network card, and you use a modem to connect to the Internet, then you will probably need to be connected to the Internet when you run and test these applications. When you connect to the Internet through a modem, your computer usually starts running TCP/IP once the connection to the Internet is made. If you do not have a network card in your computer, and you do not have any means of connecting to the Internet, or any other outside network that would allow you to run networked applications, you may not be able to run and test today's applications on your computer.
Now that you are able to connect the two running applications, you'll need to add functionality to send and receive messages. Once the connection is established between the two applications, the user can enter text messages in the edit box in the middle of the dialog window and then click the Send button to send the message to the other application. Once the message is sent, it will be added to the list box of sent messages. To provide this functionality, when the Send button is clicked, your application needs to check whether there is a message to be sent, get the length of the message, send the message, and then add the message to the list box. To add this functionality to your application, use the Class Wizard to add a function to the clicked event of the Send (IDC_BSEND) button. Edit this function, adding the code in Listing 20.9.
1: void CSockDlg::OnBsend() 2: { 3: // TODO: Add your control notification handler code here 4: int iLen; 5: int iSent; 6: 7: // Sync the controls with the variables 8: UpdateData(TRUE); 9: // Is there a message to be sent? 10: if (m_strMessage != "") 11: { 12: // Get the length of the message 13: iLen = m_strMessage.GetLength(); 14: // Send the message 15: iSent = m_sConnectSocket.Send(LPCTSTR(m_strMessage), iLen); 16: // Were we able to send it? 17: if (iSent == SOCKET_ERROR) 18: { 19: } 20: else 21: { 22: // Add the message to the list box. 23: m_ctlSent.AddString(m_strMessage); 24: // Sync the variables with the controls 25: UpdateData(FALSE); 26: } 27: } 28: }
When the OnReceive event function is triggered, indicating that a message has arrived, you'll retrieve the message from the socket using the Receive function. Once you've retrieved the message, you'll convert it into a CString and add it to the message-received list box. You can add this functionality by editing the OnReceive function of the dialog class, adding the code in Listing 20.10.
1: void CSockDlg::OnReceive() 2: { 3: char *pBuf = new char[1025]; 4: int iBufSize = 1024; 5: int iRcvd; 6: CString strRecvd; 7: 8: // Receive the message 9: iRcvd = m_sConnectSocket.Receive(pBuf, iBufSize); 10: // Did we receive anything? 11: if (iRcvd == SOCKET_ERROR) 12: { 13: } 14: else 15: { 16: // Truncate the end of the message 17: pBuf[iRcvd] = NULL; 18: // Copy the message to a CString 19: strRecvd = pBuf; 20: // Add the message to the received list box 21: m_ctlRecvd.AddString(strRecvd); 22: // Sync the variables with the controls 23: UpdateData(FALSE); 24: } 25: }
At this point, you should be able to compile and run two copies of your application, connecting them as you did earlier. Once you've got the connection established, you can enter a message in one application and send it to the other application, as shown in Figure 20.7.
FIGURE 20.7. Sending messages between the applications.
To close the connection between these two applications, the client application user can click the Close button to end the connection. The server application will then receive the OnClose socket event. The same thing needs to happen in both cases. The connected socket needs to be closed, and the message sending controls need to be disabled. On the client, the connection controls can be enabled because the client could change some of this information and open a connection to another server application. Meanwhile, the server application continues to listen on the port that it was configured to listen to. To add all this functionality to your application, edit the OnClose function, adding the code in Listing 20.11.
1: void CSockDlg::OnClose() 2: { 3: // Close the connected socket 4: m_sConnectSocket.Close(); 5: // Disable the message sending controls 6: GetDlgItem(IDC_EMSG)->EnableWindow(FALSE); 7: GetDlgItem(IDC_BSEND)->EnableWindow(FALSE); 8: GetDlgItem(IDC_STATICMSG)->EnableWindow(FALSE); 9: GetDlgItem(IDC_BCLOSE)->EnableWindow(FALSE); 10: // Are we running in Client mode? 11: if (m_iType == 0) 12: { 13: // Yes, so enable the connection configuration controls 14: GetDlgItem(IDC_BCONNECT)->EnableWindow(TRUE); 15: GetDlgItem(IDC_ESERVNAME)->EnableWindow(TRUE); 16: GetDlgItem(IDC_ESERVPORT)->EnableWindow(TRUE); 17: GetDlgItem(IDC_STATICNAME)->EnableWindow(TRUE); 18: GetDlgItem(IDC_STATICPORT)->EnableWindow(TRUE); 19: GetDlgItem(IDC_RCLIENT)->EnableWindow(TRUE); 20: GetDlgItem(IDC_RSERVER)->EnableWindow(TRUE); 21: GetDlgItem(IDC_STATICTYPE)->EnableWindow(TRUE); 22: } 23: }
Finally, for the Close button, call the OnClose function. To add this functionality to your application, use the Class Wizard to add a function to the clicked event for the Close button (IDC_BCLOSE). Edit the function to call the OnClose function, as in Listing 20.12.
1: void CSockDlg::OnBclose() 2: { 3: // TODO: Add your control notification handler code here 4: // Call the OnClose function 5: OnClose(); 6: }
If you compile and run your application, you can connect the client application to the server, send some messages back and forth, and then disconnect the client by clicking the Close button. You'll see the message-sending controls disable themselves in both applications, as in Figure 20.8. You can reconnect the client to the server by clicking the Connect button again and then pass some more messages between the two, as if they had never been connected in the first place. If you start a third copy of the application, change its port number, designate it as a server, and put it into listening mode, you can take your client back and forth between the two servers, connecting to one, closing the connection, changing the port number, and then connecting to the other.
FIGURE 20.8. Closing the connection between the applications.
Today, you learned how you can enable your applications to communicate with others across a network or across the Internet by using the MFC Winsock classes. You took a good look at the CAsyncSocket class and learned how you could create your own descendent class from it that would provide your applications with event-driven network communications. You learned how to create a server application that can listen for and accept connections from other applications. You also learned how to build a client application that can connect to a server. You learned how to send and receive messages over a socket connection between two applications. Finally, you learned how to close the connection and how to detect that the connection has been closed.
The Workshop provides quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you've learned. The answers to the quiz questions and exercises are provided in Appendix B, "Answers."
The server application that you wrote can handle only a single connection at a time. If a second application tries to open a connection to it while it's got an existing connection to an application, the server application will crash. The server tries to accept the second connection into the socket that is already connected to the first client application. Add a third socket object to the application that will be used to reject additional client connections until the first client closes the connection.