Fun with SocketsI will be honest with you. I hate programming sockets. I would much rather simply use a library. If you are like this, just download the source code and do it from there, you won't really be losing out on much. Although I do have a good explanation of pure virtual functions here. If you really want to learn, then here we go. Sockets are the computer's representation of their connections to each other. Basically there are two types of sockets, a listening socket and a normal socket. A listening socket is also known as a server socket, as it takes incoming connections. When the listening socket gets an incoming connection (on a multi-user server), then it passes that connection on to a normal socket so it is free for other users to connect to. Apart from taking a connection from a listening socket, a normal socket can also be used to make a connection to a server's listening socket. While we will not be doing this at this point in time, the socket classes I will present here have this functionality so they can be reused in any program you like. As they will be sharing some functionality, we will be creating a pure virtual base class to contain the shared functionality. A pure virtual base class is a class that has at least one pure virtual function. You cannot create an instance of a pure virtual class. A pure virtual function is defined as a virtual function that has no contents in that particular class. Your compiler will prevent you from creating any instances of a pure virtual class; however like any inherited class you can reference any of the child classes using the parent's type. You create a pure virtual function by tagging = 0 onto the end like so: class parent { virtual int foo() = 0; }; class child : public parent { virtual int foo() {return 1234;} }; int function() { parent* bar = new child; bar->foo(); } As you can see in this example, we create an instance of the class child, but it is referenced as a pointer to the class parent. When you call the function foo, it in fact resolves that the class is of type child and calls the appropriate version of foo. Now I will show you the socket base class. I am just going to shove it all here and then explain it in detail if you want to read it. class CSocket { public: inline int GetStatus() {return status;} inline SOCKET GetSocket() {return sock;} // Send text to the socket inline void SendText(char* message) { send(sock, message, strlen(message), 0); } // Pure Virtual to be defined by child classes. virtual void GetText(char* dest, int length) = 0; // Disconnect the socket void CloseSocket(); protected: // The Winsock representation of a socket SOCKET sock; // The address of the socket (on TCP/IP this is hostname/ip and port) SOCKADDR_IN addr; // Connected? int status; // binds the address to the socket int BindSocket(); }; Note that we use protected instead of private, as we will be using this as a base class, and we want the child classes to have access to items in this section. This piece of code and its comments should be fairly self-explanatory. I will go through the function bodies in a moment. First I will explain the keyword inline for those who don't know how it works. Say I created two functions like so: inline int foo(int n) { output(n); return n+5; } int bar() { int temp = foo(6)+100; return temp*12; } The compiler would compile the code like so (before optimisations) int bar() { int temp; output(6); temp = 6+5; return temp*12; } This saves the overhead required to call a function. It should be used on small and frequently used functions to gain a speed increase. Getting back to the functions. GetText has no function body; it is a pure virtual function as I explained before. SendText is self-explanatory; it calls the Winsock function, send, with the appropriate arguments. GetStatus and GetSocket simply return the values of the appropriate variables. This is an important aspect of object-oriented programming. It hides the variables away in the private or protected section, but allows you to read their value using the interface functions. Notice that there is no way to set these variables externally. This helps prevent potentially buggy code and more importantly, if a bug is introduced, it is contained to one class, as only the owner class is allowed to access its variables. While some programmers, especially new ones, do not like the extra work in creating interface functions, it helps save much work later and also improves code reusability. The function CloseSocket has the following body: void CSocket::CloseSocket(void) { shutdown(sock, SD_SEND); // Stop socket from sending any more data closesocket(sock); // Cancel any pending transfers and release sock status = 0; } You can probably work out this function from the comments. The other function that needs a body is BindSocket. int CSocket::BindSocket(void) { if(bind(sock, (LPSOCKADDR)&addr, sizeof(addr)) == SOCKET_ERROR) { return 0; } return 1; } This function calls the Winsock function bind. When you create a socket, you give it an address you want it to connect to (or if it is a listening socket, an address where you want it to accept connections from). The function, bind, will bind the address structure, addr, to the actual socket. Let me tell you a little more about addresses. You may have read the comment that was on the addr variable in the class body above. TCP/IP is a type of communications protocol. A hostname is what a computer is called. It corresponds to an IP address. Take www.google.com, it is a hostname, where as 123.12.31.23 is an IP address. An IP address is how computers identify themselves on a network. A hostname is something that people can understand easily, it will resolve to an IP address, which computers can understand easily. A port is how a computer can sustain many connections at once. Each connection is given a different port. A listen socket opens itself on a port and waits for incoming connections. A normal socket will usually be on a high numbered port. Any port can be connected to any other port on any other computer. Let us now create the listening port. First create the class definition, inheriting the base class. If you don't know how this is done, here it is. class CListenSocket : public CSocket You will need to add the definition of the function GetText. Remember this is the pure virtual function from before. Now we will give it a body. void CListenSocket::GetText(char* dest, int length) { int bytes = recv(sock, dest, length - 1, 0); if(bytes == SOCKET_ERROR) { dest[0] = '\0'; return; } dest[bytes] = '\0'; } This function uses the Winsock function recv to fill the buffer passed to it in the argument dest, up to the length of the buffer specified. It then appends the nul character to indicate the end of the string of text. To create a buffer, you would do the following: char* buffer = new char[1024]; // creates a buffer 1024 charcters long lstnsock->GetText(buffer, 1024); // fills that buffer // use the buffer here delete[] buffer; // do this when you are finished with the buffer Note that it is probably a good idea to #define how long the buffer is, instead of using the value every time. This is to prevent one value from changing and forgetting to set the other. If the buffer size and the argument given for length differed, then it may cause a buffer overrun. If this occurs, then it is possible to overwrite other variables in the program (Not a good thing. Buffer over running is a major security hazard for a server.) You may be thinking, "where is the function to open a connection?" well, here it is. Because it is different between each type of socket, we have not added it to the parent class. As such, if you had a socket referenced as CSocket and not a specific type, you would have to know what sort of socket it was and cast it to the correct type before you could call (drum roll) CreateSocket: int CListenSocket::CreateSocket(HWND hWnd, UINT message, int port) { if(status) return 0; sock = socket(AF_INET, SOCK_STREAM, 0); if(!sock) return 0; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = htonl(INADDR_ANY); if(!BindSocket()) return 0; if(!MakeListenSocket(hWnd, message)) return 0; status = 1; return 1; } Going down through the code, we first come to a call to the function socket. This creates the socket. Next we start adding values to the addr structure. This tells our socket what sort of a connection we are creating and to where. The INADDR_ANY means that our listening socket can accept a connection from anywhere. The port is which port the socket will listen on. Next we call BindSocket to bind the address to the socket. Then we call MakeListenSocket. You will notice that I haven't provided this function yet. Here it is: int CListenSocket::MakeListenSocket(HWND hWnd, UINT message) { if(listen(sock, CSOCK_BACKLOG) == SOCKET_ERROR) return 0; if(WSAAsyncSelect(sock, hWnd, message, (FD_ACCEPT | FD_READ | FD_CLOSE)) == SOCKET_ERROR) return 0; return 1; } The function listen creates a listening socket, while the function WSAAsyncSelect sets it up to be asynchronous. You may have been wondering why we needed a window handle and a window message type as arguments. The reason for this is that Windows will send us messages whenever there is a connection, a disconnection, or waiting data on a socket. Later we will add handling of these messages to our WndProc function from before. Finally we make the deconstructor close the socket if it is connected. CListenSocket::~CListenSocket(void) { if(status) CloseSocket(); } Before we do anything else we will need our normal socket. It should be created like our listening socket class, inherited from CSocket. I have called it (obviously) CNormalSocket. To begin with, quickly copy the deconstructor and the function GetText from the CListenSocket. We still want to disconnect when we delete the socket, and we a still getting text in the same way. What will be different about this socket is the way we connect. This socket will have two connection types. First we will put in the connection we use if we use the socket to connect to the server (a client connection). Copy the function CreateSocket from the listening socket. It will be almost the same. Now we do not want to be connecting to any IP, so remove htonl(INADDR_ANY) and replace it with inet_addr(ip). The argument of that function needs to be a nul terminated string of characters so add "char* ip" (no inverted commas) to the arguments section of the function. Finally, remove the call to MakeListenSocket (that function does not exist in this class) and replace it with the following: if(hWnd) if(WSAAsyncSelect(sock, hWnd, message, (FD_READ | FD_CLOSE)) == SOCKET_ERROR) return 0; This works in the same way as the call in MakeListenSocket, it makes the socket send messages when it either has data waiting, or it is disconnected. That was easy. The next way to create this sort of socket is to accept a connection from a listen socket. So we create the following function: int CNormalSocket::CreateSocket(CListenSocket* serversock) { if(status) // Don't create a socket if one already exists return 0; int addrsize = sizeof(SOCKADDR); sock = accept(serversock->GetSocket(), &asyncaddr, &addrsize); if(sock == INVALID_SOCKET) return 0; status = 1; return 1; } The important line there is where it calls accept. This takes the connection to the listening socket and transfers it to the normal socket. Then communication can take place via the normal socket. It requires the piece of data asyncaddr, which is of type SOCKADDR. Add this to the protected section of the class. This variable is used to contain the address data of an accepted connection. Well, that's it. We have created our socket classes. If you want more information, look up those data types and function calls in the Winsock2 API.
|
|