Is socket programming safe?

In contrast to the other sections of UNIX programming, socket programming will be explained using the example of a client socket pair. The structure should first be explained and then the details should be discussed.

Sockets are treated like files

Socket programming is the basis for programming distributed applications under TCP / IP in commercial client server architectures as well as in Internet applications. A socket is a connection end point that can be written and read by the program like a normal file with read () and write (). A socket is also closed with close (). However, it is not opened with open (), but with the socket () call. The following figure shows the process of a typical server on the left and a corresponding client on the right. The arrows in between point to the synchronization points. The direction of the arrow should show where the resolution of waiting at this point comes from.

Start the server

The server process must be clearly addressed from the outside. To do this, it uses the bind () call to bind itself to a fixed socket, the so-called well-known port, via which it can be reached. The port number is linked to a service name in the / etc / services file. In the program, the service name can be converted back into a number by calling getservbyname (). Then the server prepares the accept () with listen (). Calling accept () blocks the process until a request is received. Immediately afterwards the server will call read () or alternatively recv () to read the content of the request. It processes the request and sends the response to the currently waiting client. The server then loops to accept () to wait for further requests.

Start the client

The client does not need a fixed port. It fetches a normal socket to which the system assigns a free number. The server learns the number of the client from the request and can answer it at this port. In the next step, the client calls connect () to establish a connection with the server that is described in the parameters. As soon as the connection is established, the client sends its request via write () or alternatively send () and waits for the server's response via read () or recv (). After receiving the data, the client closes its connection.

Overview of the system calls

The table summarizes the system calls that affect socket programming.
call purpose
socket Request for a communication end point
bind Set the port number
list Specifying the number of buffers for requests
accept Wait for inquiries
connect Request connection
send Sending data
recv Receiving data
close Closing the socket

read or recv

If the functions are written in such a way that they alternatively have to process files or sockets, read () and write () should be used for sending and receiving. However, if the functions are only intended for use in the network, the use of recv () and send () is recommended, since other operating systems do not recognize this close connection between file and socket and the use of read () and write () on sockets allow. If you use recv () and send () the portability of the programs is higher.

Communication end point: socket and close

To work with sockets, it must first be opened. The socket () function creates the socket, the close () function closes it. Calling socket () returns the number of the new socket. If something went wrong, the call returns -1.
#include int IDMySocket; IDMySocket = socket (AF_INET, SOCK_STREAM, 0); ... if (IDMySocket> 0) close (IDMySocket);

Every socket that is opened must also be closed again. This in itself is a truism. Carelessness at this point can take bitter revenge, since connections are opened very often, especially with stateless server processes, and the lack of further sockets leads to the entire system coming to a standstill.

Close the socket under UNIX with close

The socket is closed under UNIX by calling close (). This does not normally work with other operating systems, since the analogy of the sockets to files does not exist there. Mostly names like closesocket, socketclose or soclose are used by the TCP / IP libraries.

Server calls: bind, listen and accept

bind

The server process must be accessible from outside. For this he gets a so-called well known port. This number is therefore well known to the client processes. The bind () call is used to bind a socket to this number. It uses the socket and a sockaddr_in structure that describes this port as parameters. If everything is OK, bind () returns a 0 as the return value, and -1 in the event of an error.

listen ()

The listen () call specifies how many requests can be buffered while the server is not in accept. In almost all programs a 5 is used as a parameter, as this is the maximum of some BSD systems. Listen () also delivers a 0 as a return value if everything went smoothly, otherwise a -1.

accept ()

accept () waits for a request from a client. The return value of the call is the socket with which the server subsequently exchanges data with the client. It does not use the bound socket for sending. In the event of an error, accept () returns -1.
struct sockaddr_in AdrMySock, AdrPartnerSocket; ... AdrMySock.sin_family = AF_INET; AdrMySock.sin_addr.s_addr = INADDR_ANY; / * accept. each * / AdrMySock.sin_port = PortNr; / * determined by getservbyname * / bind (IDMySocket, & AdrMySock, sizeof (AdrMySock)); listen (IDMySock, 5); do {IDPartnerSocket = accept (IDMySocket, & AdrPartnerSocket, & len);

Not to be forgotten: the IDPartnerSocket must be closed after communication has ended, although it was not explicitly opened.

Client call: connect

connect fails the connection to the server

As soon as the server has started up, the client can establish a connection to the well known port of the server. The corresponding call is connect (). The server computer is determined by its IP number. As is well known, this is a 4-byte value and is in the sockaddr_in structure in the sin_addr element. You can usually get this number by calling gethostbyname (). The second component is the destination port. This is determined by calling getservbyname (). The conversion of names to numbers is covered in more detail later.
struct sockaddr_in AdrSock; AdrSock.sin_addr = HostID; AdrSock.sin_port = PortNr; connect (IDSocket, (struct sockaddr *) & AdrSock, sizeof (AdrSock));

connect () returns a 0 if everything works and a -1 in the event of an error.

Data exchange: send and recv

The two calls send () and recv () transport data over the existing connections. Under UNIX, the file calls read () and write () can also be used for this. Unless functions should work with files as well as sockets, it is advisable to stick to send () and recv (). Firstly, it is easier to see that these are network connections; secondly, there are advantages when porting to other platforms. On other platforms, read () and write () only work on files. To turn a read () into a recv (), you just have to add another parameter 0 to the end. The same applies to write () and send (). The recv () function supplies the size of the memory area received or a -1 in the event of an error as a return value. Since the return value does not say anything about the size of the packet actually sent, this must be regulated by the program. If the packets are not always the same size, the packet length is usually encoded in the first bytes of the first packet. When transmitting with character strings, the length results from the end of the line.

Name resolution

Computers and services are actually addressed with numbers under TCP / IP. However, there are mechanisms for name resolution for both. So that they can also be used in the program, you call the functions gethostbyname () to determine an IP address based on the host name and getservbyname () to determine the service number based on the service name.
struct hostent * ComputerID; struct servent * service; / * Determine the computer named server * / ComputerID = gethostbyname ("server"); / * Determine the port for help * / Service = getservbyname ("help", "tcp");

gethostbyname ()

gethostbyname () simply receives the character string with the name of the server it is looking for as a parameter and returns the IP number in the form of a pointer to a structure hostent. The most important element of the hostent structure is the h_addr_list field. This is where the array of the computer's IP numbers is located. The macro h_addr supplies the number as it was common in older versions. The h_length field supplies the size of an IP number.

getservbyname ()

The getservbyname () function returns a pointer to a structure called servent for the two strings that describe the service. The most important element of the servent structure is the s_port field. This contains the number of the port as used by the connect () function.

Twisted numbers ntoh and hton

The byte sequence is defined differently on the different computers. A short type variable consists of two bytes. On a machine with an Intel CPU, the least significant byte comes first, while it is exactly the other way around on a 68000. There must be a standard for this in a heterogeneous network. Under TCP / IP the most significant byte is first (Big Endian \ gpFussnote {According to the legend, this name comes from the book >> Gullivers Reisen <<, in which two peoples argue about whether to get the big end or open the little end first.}). The macros ntoh () (Net to Host) and hton () (Host to Net) are used to bring the numbers of the machine into the network form and to keep the programs portable. Both act on short variables. The analog macros htonl () and ntohl () are used to process long variables.

For example, to write the port of POP3 (110) into the sock_add_in structure, one would use hton. If getservbyname is used at this point, the need for hton is eliminated.

struct sockaddr_in AdrSock; AdrSock.sin_port = hton (110);

Framework program of a client-server pair

The server answers client requests in an endless loop. Before he gets into this endless loop, he has to register his service. It blocks for the first time with accept (), which is released by the connect () of the client. recv () blocks the process for a short time, but since the client will send its request immediately, this will only last for a short time. He sends the answer and turns to the next inquirer. #include #ifdef WIN32 #include // link with Ws2_32.lib #pragma comment (lib, "Ws2_32.lib") #else #include # include #include #include #include #include #define closesocket close #endif #define MAXPUF 1024 #define WELLKNOWNPORT 5000 #include int main () {#ifdef WIN32 // Windows has to initialize first! WSADATA wsaData; int iResult = WSAStartup (MAKEWORD (2, 2), & wsaData); if (iResult! = 0) {printf ("WSAStartup failed:% d \ n", iResult); return 1; } #endif char buffer [MAXPUF]; int IDMySocket = socket (AF_INET, SOCK_STREAM, 0); if (IDMySocket == 0) {std :: cout << "socket returns 0" << std :: endl; return 1; } // Set socket parameters struct sockaddr_in AdrMySock = {0}; AdrMySock.sin_family = AF_INET; AdrMySock.sin_addr.s_addr = INADDR_ANY; // accept. every AdrMySock. Otherwise the sockets go out} while (1); // up to St. Nimmerlein closesocket (IDMySocket); }

The listing has been updated a little because a few small things in the syntax of C and in the includes have changed. The outputs (std :: cout, std :: endl, include ) are C ++. The rest is still pure C. Since some people also have to program under Windows, the special features of Windows are built in.

Sequential processing

This server sequentially processes every request that is made to it via the port >> help <<. After each request, the connection is released and another client can request. Such a server should be able to work on any operating system that supports TCP / IP. The associated client prepares the connection in the AdrSocket variable and thus calls the connect () function. This blocks until the server has called accept () on the other side. The client continues by sending its request. The sending process never blocks, but the subsequent receipt of the response does. As soon as the server has sent its response, the client can exit. #include #ifdef WIN32 #undef UNICODE #include // getaddinfo #include // link with Ws2_32.lib #pragma comment (lib, "Ws2_32.lib" ) #else #include #include #include #include #include #define closesocket close #include // memcpy #endif #include #define MAXPUF 1024 #define WELLKNOWNPORT 5000 #define TARGET SERVER "127.0.0.1" int main () {struct sockaddr_in AdrSock; // struct servent * Service; // for access to the / etc / services char buffer [MAXPUF]; int error; #ifdef WIN32 // Windows has to initialize first! WSADATA wsaData; int iResult = WSAStartup (MAKEWORD (2, 2), & wsaData); if (iResult! = 0) {printf ("WSAStartup failed:% d \ n", iResult); return 1; } AdrSock.sin_addr.s_addr = inet_addr (TARGET SERVER); #else struct hostent * ComputerID; ComputerID = gethostbyname (TARGET SERVER); if (ComputerID == 0) {std :: cout << "Computer" << TARGET SERVER << "Unknown" << std :: endl; return 1; } // bcopy (src, dest, len) -> memcpy (dest, src, len) memcpy (& AdrSock.sin_addr, ComputerID-> h_addr, ComputerID-> h_length); #endif // Determine the port by name from the / etc / services // Service = getservbyname ("servicename", "tcp"); //AdrSock.sin_port = Service-> s_port; // ... or directly ... AdrSock.sin_port = htons (WELLKNOWNPORT); AdrSock.sin_family = AF_INET; int IDSocket = socket (AF_INET, SOCK_STREAM, 0); if (IDSocket <= 0) {std :: cout << "Socket error - IDSocket:" << IDSocket << std :: endl; } error = connect (IDSocket, (struct sockaddr *) & AdrSock, sizeof (AdrSock)); if (error <0) {std :: cout << "connect error" << std :: endl; } error = send (IDSocket, "Uhu", 4, 0); if (error <0) {std :: cout << "send error" << std :: endl; } int len ​​= recv (IDSocket, buffer, MAXPUF, 0); std :: cout << buffer [0] << "/" << len << std :: endl; closesocket (IDSocket); }

variables

There are two variables per socket. As with file access, one is a simple handle (marked here with ID), the other holds the address of the connection, i.e. the Internet number of the computer and the number of the port. The server does not determine the number of the computer by using the constant INADDR_ANY. The client, on the other hand, specifies the address of the server to be addressed. The recv () function supplies the size of the memory area sent as a return value. The recv () function reads the transmission in packets of 1KB maximum. If larger parcels have been sent, they have to be read bit by bit. Sending is not restricted.

parallelism

The server is supplemented so that it can use the advantages of a multitasking environment and process several requests in parallel. To do this, a fork () must be installed at a suitable point:
do {IDPartnerSocket = accept (IDMySocket, & AdrPartnerSocket, & len); if (fork () == 0) {MsgLen = recv (IDPartnerSocket, buffer, MAXPUF, 0); / * do something with the data * / send (IDPartnerSocket, buffer, MsgLen, 0); close (IDPartnerSocket); / * Son kills himself * / exit (0); } / * if fork .. * / close (IDPartnerSocket); / * the father closes the connection * /} while (1);

Parallel processing with little effort

You can see what small changes can be made to implement a multitasking server. When fork () is called, the process creates a child process that owns all of the parent's resources. In this way, he can further process the connection with the inquirer. He completely takes the place of the father, who in turn can close the connection and wait for a new request. As soon as this arrives, a child is generated again, who may work in parallel with the other child if that child is not yet finished.

Stateless server

The server as it is now is a stateless server. This means that he cannot remember the status of a communication. If the same client asks again, it will anonymously treat it like a completely new request. This is how a web server works. Every request is new to him.Other servers, for example POP3 servers, maintain the connection with their client until both agree to end the connection. In such a case, a loop in the child process would run via recv (), processing and send () until a defined end of communication takes place. Of course, the client would then also have a loop that is only ended when an agreement has been reached about the disconnection of the connection.

Thwart zombies

As shown in connection with the signals, the creation of zombies should be prevented. Zombies arise when a son ends but the father isn't waiting for him. This leaves an entry in the process table with the exit value of the son. The effort is extremely low:

Query several sockets in parallel

Another topic of socket programming is the parallel processing of several ports by a single process. An example of such a program is inetd, the so-called Internet daemon, which accepts requests for all ports listed in inetd.conf and calls up the required server.
#include #include #include int select (int MaxHandle}, fd_set *Read, fd_set *Write}, fd_set *OutOfBand, struct timeval *Time-out);

Central objects in this context are the file descriptor arrays. Of these, assume three. There is one sentence each to read, to write or to exceptions, like observing messages out of sequence. If you don't want to observe all categories, enter NULL for the uninteresting parameters. If sockets a and b are to be monitored for reading, an fd_set must be created and filled with it:

fd_set read sockets; FD_ZERO (& readsockets); FD_SET (a, & read sockets); FD_SET (b, & readsockets); maxHandle = max (a, b) + 1; select (maxHandle, & readsockets, NULL, NULL, NULL);

blocks until data arrives on one of the sockets. If a timeout is to be defined, i.e. a period of time after which the user should give up, the last parameter must be assigned.

struct timeval myTime; ... myTime.tv_sec = 0; myTime.tv_usec = 100; select (maxHandle, & lesesockets, NULL, NULL, & myTime);

Replacement for sleep

The ability of select () to set a timeout in the millisecond range is sometimes misused to put a process to sleep for a short time. The actually responsible function sleep () puts a process to sleep for at least one second, which is sometimes too much.

IPv6 from a programmer's point of view

The next generation of IP addresses, which now use 16 instead of 4 bytes as addresses, naturally also have consequences for programming. There are new constants, structures and functions that have to be used instead of the old versions. The concepts of how client and server programs are structured will not change anything. In this respect, the administrators are more affected by the innovations than the programmers. The new address family AF_INET6 has been defined instead of AF_INET. Instead of the in_addr structure, there is the in6_addr structure, which is defined as follows:
struct in6_addr {uint8_t s6_addr [16];

This change also affects the sockaddr_in structure, which is now defined as sockaddr_in6 as follows:

struct sockadd_in6 {sa_family_t sin6_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_address sin6_addr; }

The conversion between host name and IP number is no longer done with the functions gethostbyname () or gethostbyaddr (), but with the new functions getnodebyname () and getnodebyaddr (). \ GpFussnote {cf. Santifaller, Michael: TCP / IP and ONC / NFS - Internetworking with UNIX. Addison-Wesley, 1998. p. 365}

Client-server from a performance perspective

The goal of a good client-server architecture is to use the performance that would otherwise be idle in the local computer for things that do not have to run centrally, thereby relieving the central computer.

The Terminal model

If one considers software that is to be used by several users at the same time, three architecture models result. The classic variant provides each participant with a terminal and the central computer carries out all requirements on its central processor and its disk. This also includes user guidance, for example setting up the masks. Navigating the program in search of the right mask is a burden for the general public.

The disk server model

In the case of the solution with a disk server, the processor load is distributed among the workstations. Since the server does not have to have any intelligence of its own, a standard PC is usually sufficient. However, it is easy to overlook the fact that all disk accesses also strain the network, which is typically slower than disk access. If, for example, a specific customer is to be searched for, a binary search is carried out for sorted or indexed data. This means that the first access is to the middle data set. If the name found is higher in the alphabet, the lower half is halved, otherwise the upper half. In this way you can find the correct sentence in 1024 sentences with 10 accesses. 11 accesses are required for 2048 records, 12 for 4096 and so on. However, these accesses all run over the network and if the network is heavily loaded, the overall behavior is significantly affected.

Find the division point

A client-server solution can be shared anywhere. The division point will be set above the binary search so that the binary search accesses take place locally on the server and thus do not burden the network. On the other hand, the user control will be left on the local workstation, so that only lean packets with requests are sent to the server.