Creating the BSDSocketServer header file
The BSDSocketServer
header file looks like the following code:
The header file of the BSDSocketServer
class starts off by defining the LISTENQ
constant as 1024
. This constant will be the maximum number of pending connections that can be queued up at any given time before the sockets stop accepting new connection requests.
We also define the maximum length of the inbound string for the echo server, which we will set as 4096
characters.
We then define an ENUM
with our five error conditions:
NOERROR
: This determines that no errors occurred while performing the socket, bind, and listen steps
SOCKETERROR
: This determines that the error occurred while creating the socket
BINDERROR
: This determines that the error occurred while binding the sockaddr
family of structures with the socket
LISTENERROR
: This determines that the error occurred while preparing to listen on the socket
ACCEPTINGERROR
: This determines that the error occurred while accepting a connection
The BSDSocketServer
has two properties. The
errorCode
property will contain the error code if any of the functions fails, while the listenfd
property will contain the socket descriptor. This descriptor can be used outside the BSDSocketServer
object to create your server if you want to have your server code outside the BSDSocketServer
class.
The header defines one constructor called initWithPort:
, which has one parameter to define the port number to listen on. The header file also defines one method that sets up the echo server once we initialize the server within the initWithPort:
constructor. As you build your own servers, you will want to add separate methods such as the echoServerListenWithDescriptor:
method, to handle them while using the initWithPort:
constructor to initialize the server.
Creating the BSDSocketServer implementation file
Now let's look at the
BSDSocketServer
implementation file. The code for this implementation file is as follows:
We begin the implementation file by importing the header files needed to implement our echo server. Let's look at the initOnPort:
constructor:
The BSDSocketSever.m
class has a single constructor called initWithPort:
. This constructor will take a single parameter named port
of type int
. This port
parameter is the port number that we want our server to bind to. This number can range from 0-65535; however, you will need to have the root access to bind to ports below 1024,
so I recommend you to use port numbers greater than 1024
.
We define a sockaddr_in
structure (remember, sockaddr_in
is for IPv4 and sockaddr_in6
is for IPv6) named servaddr
. To begin with, we set the errorCode
variable to NOERROR
.
To set up a socket, we will need to call the socket()
, bind()
, and listen()
functions. If any of these functions fail, we will want to set the errorCode
variable and skip the rest of the initialization.
We use the socket()
function to create our socket using the AF_INET
(IPv4) and SOCK_STREAM
(TCP) parameters. If you would like to use IPv6, you would change AF_INET
to AF_INET6
. If you would like to use UDP instead of TCP, you would change SOCK_STREAM
to SOCK_DGRAM
.
Prior to calling the bind()
function, we need to set up a sockaddr
structure that contains the IP version, interface, and port number that we will be binding the socket to. Before populating the sockaddr
structure with the information, we would want to clear the memory to make sure there is no stale information that may cause our bind
function to fail. We do this using the memset()
function.
After we clear the memory of the sockaddr
structure, we set the values. The sin_family
address family is set to AF_INET
, which sets the IP version to IPv4. The sin_addr.s_addr
address is set using htonl(INADDR_ANY)
to let the socket bind to any interface on the device. The sin_port
number is set to the port number using the htons(port)
function.
The htonl()
and
htons()
functions convert the byte order of the values passed in from the host byte order to the network byte order, so the values can be properly interpreted when making network calls. If you are unsure what byte order is, you can refer to the Finding the byte order of your device recipe of this chapter.
After we have our sockaddr
structure set, we use it to bind the socket to the address specified in the servaddr
structure.
If our bind()
function call is successful, we attempt to listen to the socket for new connections. We set the maximum number of backlog connection attempts to the LISTENQ
constant, which is defined as 1024
.
After we initiate the BSDSocketServer
object using the initOnPort:
constructor, we will have a server that is actively listening for new connections on the port, but now we need to do something when the connection comes in. That is where the echoServerListenWithDescriptor:
method comes in. The echoServerListenWithDescriptor:
method will listen for new connections and when one comes in, it will start a new thread to handle the connection, as shown in the following code:
The echoServerListenWithDescriptor:
method will use the accept()
function to accept incoming connections on the supplied socket descriptor.
Within the echoServerListenWithDescriptor:
method, we create a for
loop that will loop forever because each time a new connection is accepted, we will want to pass the control of that connection to a separate thread and then come back and wait for the next connection.
The accept()
function detects and initializes incoming connections on the listening socket. When a new connection is made, it will return a new socket descriptor. If there is a problem initializing the connection, the accept()
function will return -1
. If the connection is successfully initialized, we determine the IP address and port number from where the client is connecting and log it.
Finally, we use dispatch_async()
to add our strEchoServer()
method to the dispatch queue. If we simply called the method directly without dispatch_async()
, the server would only be able to handle one incoming connection at a time. With dispatch_async()
, each time a new connection comes in, the strEchoServer()
method gets passed to the queue and then the server can go back to listening for new connections. The strEchoServer()
method listens to establish connections for incoming text and then echoes that text back to the client. Refer to the following code:
The strEchoServer:
method has one parameter that is a socket descriptor to read from. We set up the while
loop that will loop each time data comes in on the socket. When the data is received, the recv()
function will put the incoming bytes into the buffer pointed to by buf
. The
recv()
function will then return the number of bytes that are read. If the number of bytes is zero, the client is disconnected; if it is less than zero, there is an error. For the purpose of this recipe, we will close the socket if the number of bytes returned is zero or less.
As soon as the data is read from the socket, we call the written:char:size:
function to write the data back to the client. This essentially is our echo server; however, we want to perform some additional steps so we can see when the data is received.
We will want to terminate the buf
character array with a NULL
terminator prior to converting it to NSString
, so we do not get any additional garbage in our string. After we terminate the character array, we post a notification named posttext
with the text from the socket. This will allow us to set an observer within our program that will receive all incoming text from the socket. In our example code, this notification will be used to display the incoming text to the screen, but it can also be used for logging or anything else we think of. If you do not want to do anything with the text that is sent, you can safely ignore the notification.
Once the client closes the connection, we will want to close the socket on our end. The close()
function at the end of the strEchoServer:
method does this for us if the number of bytes returned from the recv()
function is zero or less:
The written:char:size:
method is used to write the text back to the client and has three parameters. These parameters are: sockfdNum
, which is the socket descriptor to write to; the vptr
pointer, which points to the text to be written; and n
, which is the length of the text to be written.
The written:char:size:
method uses the write()
function to write the text back to the client. This method returns the number of bytes written, which may be less than the total number of bytes you told it to write. When that happens, we will need to make multiple write calls until everything is written back to the client.
We set ptr
to point to the beginning of the text to send back and then set nleft
to the size of the text to write. If the write
function does not send all the text to the client, ptr
will be moved to point to where we will begin the next write from and nleft
will be set to the number of remaining bytes to write. The while
loop will continue to loop until all text is written back to the client. If the write
function returns a number less than 0, it means that there was a problem writing to the socket, so we return -1
.