Sunday, October 31, 2010

C Socket Server example, with semaphores, signal handling

This is a quick program that combines various examples from the GNU/Linux Application Programming book I have been studying.

The program is a BSD Socket based server (ch. 13), that accepts connections on a specific port and sends over TCP a "temperature" value, which at the moment I am simply randomly generating for example. I have added the use of counting semaphores (ch 17) to demonstrate a critical section of code as well as used POSIX signal handling (ch. 14) as a mechanism to cleanly exit. I developed this in Eclipse CDT IDE. I am not a C expert, this is my first C program since approx 2002 but it runs and compiles without warning!

Therm- ServerProject set up and dependencies
I created two static libraries for generating the random number/temperature reading. This are linked in my Eclipse project. For the purposes of this example you only need to know about these function prototypes (from thermapi.h)
/*
* Initialization for Therm API. Call before using any other function
*/
extern void initTherm( void );

/*
* Returns the current temperature reading from sensor
*/
extern float getTemp( void );

Other than this, I depend on various standard libraries in Linux for sockets, signal handling, etc..

thermserv.c - Includes and Defines.
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

/* sockets and addresses */
#include <sys.h>
#include <arpa.h>
#include <netinet.h>
#include <netinet.h>
#include <netinet.h>
#include <netinet.h>
#include <netdb.h>

/* semaphores */
#include <sys.h>

/* signal handling */
#include <sys.h>
#include <sys.h>
#include <signal.h>

/* my own includes for custom functions */
#include <thermapi.h>
#include "thermserv.h"

/* magic number constants */
#define MAX_BUFFER 128
#define THERM_SERVER_PORT 6777
#define THERM_SEM_ID 6778

static int loopCounter = 1;

/* Exit handler function called by sigaction */
void exitHandler(int sig, siginfo_t *siginfo, void *ignore) {
printf("*** Got %d signal from %d\n", siginfo->si_signo, siginfo->si_pid);
loopCounter = 0;

return;
}


Print Server Info utility method just to clean up the main function a little. It still is too long.
/*
* Prints out basic server information for startup
*/
void printTServerInfo(struct sockaddr_in servaddr, int cliLen,
time_t startupTime) {

printf("*** Thermo Server initialized.\n");
printf("*** cliLen size: %d\n", cliLen);
printf("*** Server Listening as %s:%d\n", inet_ntoa(servaddr.sin_addr),
ntohs(servaddr.sin_port));
printf("*** Server PID: %d\n", getpid());
printf("*** Start up time: %s", ctime(&startupTime));

}

Note: I was getting warnings from gcc for this function.

../thermserv.c: In function ‘main’:
../thermserv.c:122: warning: implicit declaration of function ‘printTServerInfo’
../thermserv.c: At top level:
../thermserv.c:180: warning: conflicting types for ‘printTServerInfo’
../thermserv.c:122: note: previous implicit declaration of ‘printTServerInfo’ was here

To resolve these, I added thermserv.h:

#ifndef THERMSERV_H_
#define THERMSERV_H_

/*
* Prints out basic server information for startup
*/
void printTServerInfo(struct sockaddr_in servaddr,
int cliLen,
time_t startupTime);

#endif /* THERMSERV_H_ */

Warnings resolved!

back to the rest of thermserv.c:
main() - set up code

/* start main */
int main(void) {

/*
* TODO implement server as daemon, per best
* practices, Advanced Programming in Unix Environment (ch 13, p417)
*/

int serverFd, connectionFd;
struct sockaddr_in servaddr;
char tempBuffer[MAX_BUFFER + 1];
time_t currentTime;
time_t startupTime;
float fread;
socklen_t cliLen;
struct sockaddr_in cliaAddr;
int therm_sem_id;
struct sembuf semAcquireBuffer;
struct sembuf semReleaseBuffer;
struct sigaction act;

/*init socket*/
serverFd = socket(AF_INET, SOCK_STREAM, 0);

/* set up semaphore */
therm_sem_id = semget(THERM_SEM_ID, 1, 0666 | IPC_CREAT);
if (therm_sem_id >= 0) {
printf("*** Semaphore %d\n", therm_sem_id);
/* init semaphore count to 1 */
semctl(therm_sem_id, 0, SETVAL, 1);
} else {
printf("Could not create/get semaphore with errno: %d %s", errno,
strerror(errno));
exit(-1);
}

semAcquireBuffer.sem_flg = 0;
semAcquireBuffer.sem_op = -1;
semAcquireBuffer.sem_num = 0;

semReleaseBuffer.sem_flg = 0;
semReleaseBuffer.sem_op = 1;
semReleaseBuffer.sem_num = 0;

/* Set exit handler function for SIGUSR1 , SIGINT (ctrl+c) */
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = exitHandler;
sigaction(SIGUSR1, &act, 0);
sigaction(SIGINT, &act, 0);

/* Set socket server */
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(THERM_SERVER_PORT);

bind(serverFd, (struct sockaddr *) &servaddr, sizeof(servaddr));

listen(serverFd, 5);

/* initialize thermometer api (required by rand api) */
initTherm();

/* get set up to capture client socket address */
cliLen = sizeof(struct sockaddr_in);
startupTime = time(NULL);
printTServerInfo(servaddr, cliLen, startupTime);



Now the block with all the hot action, an infinite while loop for the socket server to accept connections, get the current temperature value and then write to the socket. If a SIGUSR1 or SIGINT (ctrl+c) is received by the handlers the loop value loopCounter will be set to false and the while loop will end allowing for some clean up at the end to close the sockets, semaphores, etc...

/* infinite loop until SIGUSR1 or SIGINT is handled */
while (loopCounter) {

printf("Waiting for Client connection\n");
connectionFd = accept(serverFd, (struct sockaddr*) &cliaAddr, &cliLen);
/* valid connection to client accepted */
if (connectionFd >= 0) {

currentTime = time(NULL);
/*print client address info and time of connection*/
/* http://www.cs.odu.edu/~cs476/fall03/lectures/sockets.htm
*
* Note ctime() seems to build in trailing \n
* */
printf("Client Connected %s:%d at %s",
inet_ntoa(cliaAddr.sin_addr), ntohs(cliaAddr.sin_port),
ctime(&currentTime));

/*Begin Critical Section acquire Semaphore*/
if (semop(therm_sem_id, &semAcquireBuffer, 1) == -1) {
printf("Critical Section Begin failed semid: %d\n",
therm_sem_id);
} else {
printf("Critical Section Begin\n");
/*write current temp in F to connected socket*/
fread = getTemp();
snprintf(tempBuffer, MAX_BUFFER, "%f\n", fread);

/*End Critical Section Release Semaphore*/
if (semop(therm_sem_id, &semReleaseBuffer, 1) == -1) {
printf("Critical Section End failed semid: %d\n",
therm_sem_id);
}
printf("Critical Section End\n");
}
/*write string tempBuffer to connected socket, close connection*/
write(connectionFd, tempBuffer, strlen(tempBuffer));
close(connectionFd);

}

}


The clean up code is reached by the signal handler function changing the loopCounter to allow the while loop to end. Other signals could cause the process to exit, I am not sure what that really means for the still open socket, and more importantly the semaphore. Might there be a better facility in C to handle this case?

/* clean up socket, semaphore before process exits*/
/* TODO better way to do this? ie Java finally{}?*/
printf("Shutting down server\n");
close(serverFd);
semctl(therm_sem_id, 0, IPC_RMID);

return 0;

}


Building therm-server

Invoking: GCC C Compiler - note libtherm and libexp are my custom static libraries
gcc -I"/home/ammianus/workspace/libtherm" -I"/home/ammianus/workspace/libexp" -O0 -g3 -pedantic -Wall -c -fmessage-length=0 -MMD -MP -MF"thermserv.d" -MT"thermserv.d" -o"thermserv.o" "../thermserv.c

Invoking: GCC C Linker
gcc -static -L"/home/ammianus/workspace/libtherm/Debug" -L"/home/ammianus/workspace/libexp/Debug" -o"therm-server" ./thermserv.o -ltherm -lexp

Running therm-server
For the fun part. Open a terminal and navigate to /home/ammianus/workspace/libtherm/Debug
./therm-serv
This launches the server, it acquires the semaphore and inits the socket listening on port 6777

*** Semaphore 131075
*** Thermo Server initialized.
*** cliLen size: 16
*** Server Listening as 0.0.0.0:6777
*** Server PID: 4790
*** Start up time: Sun Oct 31 12:27:11 2010
Waiting for Client connection



To connect to the socket with a client, use telnet or a web browser and connect to 0.0.0.0:6777

ammianus@Hadrian:~$ telnet 0.0.0.0 6777
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.
72.720497
Connection closed by foreign host.

Getting 72.720497 is the "temperature" that was our goal from the start!

On the server side it logged this connection attempt:
Waiting for Client connection
Client Connected 127.0.0.1:57090 at Sun Oct 31 12:30:17 2010
Critical Section Begin
Critical Section End
Waiting for Client connection

Etc..

The server will continue to run and accept connections, (try connecting with a browser and holding down 'F5').

To shutdown the server, you can send a signal to its PID
ammianus@Hadrian:~$ kill -s SIGUSR1 4790
And server handles this signal gracefully
*** Got 10 signal from 4794
Shutting down server


References
http://www.suite101.com/content/c-header-files-a2936
http://stackoverflow.com/questions/2406986/c-warning-implicit-declaration-of-function-exit
http://stackoverflow.com/questions/4009090/what-is-correct-way-to-have-single-signal-handler-function-for-multiple-signals
http://www.cs.odu.edu/~cs476/fall03/lectures/sockets.htm
Jones, M. Tim. GNULinux application programming. 2nd ed. Cengage Learning, 2005. Print.
Richard, W., and Stephen A. Advanced Programming in the UNIX Environment. Addison-Wesley Professional, 2008. Print.

No comments:

Post a Comment