TCP Chat Server in C mit Threads

Autor: Andre Adrian
Version 10.Juni.2007

Zusammenfassung

Ein Internet Relay Chat (IRC) Server ist ein gutes Beispielprogramm für Server und Netzwerk Programmierung. Die Businesslogik des IRC Servers ist sehr einfach: Textmeldungen (ASCII Strings) von einem Client entgegennehmen und an alle anderen Clients senden. Vom gleichen Autor gibt es schon einen Chat Server mit select() für Linux und MS-Windows. Die select() Lösung ist die einfachste Lösung.
Mit Threads (Programmfäden) kann ebenfalls ein Chat Server realisiert werden. Jede TCP Verbindung (Connection) wird durch einen Thread bedient. Die Zustandsvariable the_state wird über einen binären Semaphore (Mutex) geschützt.

Inhaltsverzeichnis

Threads und Mutex Variablen

Für korrekte Threads Programme werden Mutex Variablen oder Semaphore benötigt um eine Race Condition (Wettlaufsituation) zu vermeiden. Bei dem select() Beispiel gab es nur einen Thread und deshalb war eine Race Condition nicht möglich. Über select() können mehrere Events (Ereignisse) vom Betriebssystem an die Applikation gemeldet werden. So kann gleichzeitig ein neuer Client am Listen Port einen connect() ausführen, ein etablierter Client eine Textmeldung zum Server senden und ein weiterer etablierter Client einen close() ausführen. Im select() Beispiel werden diese drei Events nacheinander abgearbeitet. Dann wird erneut select() aufgerufen und die Applikation blockiert (schläft) bis neue Events eintreten.
Bei der Threads Lösung gibt es einen Thread für den Listen Port und je einen Thread für jeden etablierten Client. Die Zustandsvariablen the_state und maxfd müssen bei der Thread Lösung durch eine Mutex Variable geschützt werden. Sonst kann unter folgenden Bedingungen eine Race Condition auftreten: Ein etablierter Client schließt die Verbindung, ein anderer Client sendet eine Textmeldung an den Server. Der eine Thread schließt seine TCP Verbindung, der andere Thread führt ein write() auf diese TCP Verbindung aus. Manchmal wird der close() nach dem write() ausgeführt, dann wird der write() erfolgreich sein. Manchmal wird der close() vor dem write() ausgeführt - dann wird der write() einen Fehler zurückliefern. Im schlimmsten Fall führt die Race Condition zum Server Programmabsturz oder zu einer Datenverfälschung. Beim Chat Server sind die Auswirkungen klein. Dies kann durch Auskommentieren der Mutex lock und unlock Aufrufe leicht überprüft werden.

Threads und Speicher

Lokale Variablen und Funktionsparameter gehören einem einzelnen Thread. Globale und statische Variablen sind für alle Threads gemeinsam. Mit malloc() oder calloc() wird Speicher aus dem globalen Heap (Haufen) reserviert. Globale, statische und Heap Variablen welche von verschiedenen Threads benutzt werden sollten mit dem Type Attribute volatile versehen werden.
Die read() Aufrufe arbeiten mit einem lokalen Speicher zusammen. Die Mutex Variable mutex_state sowie die Zustandvariablen the_state, minfd und maxfd sind globale Variablen.

POSIX Threads (Pthreads), MS-Windows Threads

Unter Linux werden die POSIX Threads (IEEE POSIX 1003.1c Standard von 1995) eingesetzt. Unter MS-Windows gibt es die Microsoft C Run-Time Threads.

Funktion
Pthreads
MS-Windows
Erzeuge Thread
pthread_create() _beginthread()
Beende Thread
pthread_exit() _endthread()
Erzeuge Mutex
pthread_mutex_init()
InitializeCriticalSection()
Lösche Mutex
pthread_mutex_destroy() DeleteCriticalSection()
P Operation (Resource belegen)
pthread_mutex_lock() EnterCriticalSection()
V Operation (Resource freigeben)
pthread_mutex_unlock() LeaveCriticalSection()

Compile und Ausführen

Der Quelltext wird unter Linux mit  cc -o chat1d chat1d.c -lpthread  übersetzt. Die Programmdatei wird mit   ./chat1d &   im Hintergrund gestartet. Für den ersten Test können zwei oder mehr telnet Programme mit  telnet 127.0.0.1 51234  auf dem gleichen Rechner gestartet werden. Eine Eingabe im telnet Programm wird per TCP Verbindung zum chatd Programm gesendet. Von dort wird die Eingabe an alle anderen Clients weitergesendet und ausgegeben.

Der Quelltext

Der Quelltext besteht aus den Funktionen
Die genaue Funktion des Quelltextes erschließt sich durch aufmerksames Lesen des Quelltextes. Das   man   UNIX Kommando ist sehr hilfreich beim Verständnis der Betriebssystem Aufrufe. Falls Lesen nicht mehr weiterhilft, sollte man mit dem Programm "spielen": Quelltext ändern, printf() Kommandos einfügen, das Programm im Debugger ausführen, usw.
Falls auch Spielen nicht dem Verständnis hilft dann sollte man vielleicht das select() Beispiel durcharbeiten.

/* chat1d.c
 * Chat Server (Daemon) mit Threads fuer Linux
 * Autor: Andre Adrian
 * Version: 09jun2007
 *
 * Chat Server wartet auf TCP Port 51234 auf einen Verbindungsaufbau
 * von telnet Clients. Der Client sendet ASCII Meldungen an den Server.
 * Die Meldungen werden vom Server an alle anderen Clients gesendet.
 *
 * Aufruf Server:
 *  ./chat1d &
 *
 * Aufruf Client:
 *  telnet IP_Adresse_Server 51234
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <syslog.h>
#include <fcntl.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>

/* Die Makros exit_if() und return_if() realisieren das Error Handling
 * der Applikation. Wenn die exit_if() Bedingung wahr ist, wird
 * das Programm mit Fehlerhinweis Datei: Zeile: Funktion: errno beendet.
 * Wenn die return_if() Bedingung wahr ist, wird die aktuelle Funktion
 * beendet. Dabei wird der als Parameter 2 angegebene Returnwert benutzt.
 */

#define exit_if(expr) \
if(expr) { \
  syslog(LOG_WARNING, "exit_if() %s: %d: %s: Error %s\n", \
  __FILE__, __LINE__, __PRETTY_FUNCTION__, strerror(errno)); \
  exit(1); \
}

#define return_if(expr, retvalue) \
if(expr) { \
  syslog(LOG_WARNING, "return_if() %s: %d: %s: Error %s\n\n", \
  __FILE__, __LINE__, __PRETTY_FUNCTION__, strerror(errno)); \
  return(retvalue); \
}

#define MAXLEN 1024
#define MAXFD 20

#define OKAY 0
#define ERROR (-1)

volatile fd_set the_state;
pthread_mutex_t mutex_state = PTHREAD_MUTEX_INITIALIZER;

int tcp_server_init(int port)
/* Server (listen) Port oeffnen - nur einmal ausfuehren
 * in port: TCP Server Portnummer
 * return: Socket Filedescriptor zum Verbindungsaufbau vom Client
 */
{
  int listen_fd;
  int ret;
  struct sockaddr_in sock;
  int yes = 1;

  listen_fd = socket(PF_INET, SOCK_STREAM, 0);
  exit_if(listen_fd < 0);

  /* vermeide "Error Address already in use" Fehlermeldung */
  ret = setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
  exit_if(ret < 0);

  memset((char *) &sock, 0, sizeof(sock));
  sock.sin_family = AF_INET;
  sock.sin_addr.s_addr = htonl(INADDR_ANY);
  sock.sin_port = htons(port);

  ret = bind(listen_fd, (struct sockaddr *) &sock, sizeof(sock));
  exit_if(ret != 0);

  ret = listen(listen_fd, 5);
  exit_if(ret < 0);

  return listen_fd;
}

int tcp_server_init2(int listen_fd)
/* communication (connection) oeffnen - fuer jeden neuen client ausfuehren
 * in listen_fd: Socket Filedescriptor zum Verbindungsaufbau vom Client
 * return: wenn okay Socket Filedescriptor zum lesen vom Client, ERROR sonst
 */
{
  int fd;
  struct sockaddr_in sock;
  socklen_t socklen;

  socklen = sizeof(sock);
  fd = accept(listen_fd, (struct sockaddr *) &sock, &socklen);
  return_if(fd < 0, ERROR);

  return fd;
}

int tcp_server_write(int fd, char buf[], int buflen)
/* Schreibe auf die Client Socket Schnittstelle
 * in fd: Socket Filedescriptor zum Schreiben zum Client
 * in buf: Meldung zum Schreiben
 * in buflen: Meldungslaenge
 * return: OKAY wenn Schreiben vollstaendig, ERROR sonst
 */
{
  int ret;

  ret = write(fd, buf, buflen);
  return_if(ret != buflen, ERROR);
  return OKAY;
}

void *tcp_server_read(void *arg)
/* Thread fuer einen CONNECT Port
 * Lese von der Client Socket Schnittstelle, schreibe an alle anderen Clients
 * in arg: Socket Filedescriptor zum lesen vom Client
 * return:
 */
{
  int rfd;
  char buf[MAXLEN];
  int buflen;
  int wfd;
 
  rfd = (int)arg;
  for(;;) {
    /* lese Meldung */
    buflen = read(rfd, buf, sizeof(buf));
    if (buflen <= 0) {
      /* End of TCP Connection */
      pthread_mutex_lock(&mutex_state);
      FD_CLR(rfd, &the_state);      /* toten Client rfd entfernen */
      pthread_mutex_unlock(&mutex_state);
      close(rfd);
      pthread_exit(NULL);
    }
   
    /* Meldung an alle anderen Clients schreiben */
    pthread_mutex_lock(&mutex_state);
    for (wfd = 3; wfd < MAXFD; ++wfd) {
      if (FD_ISSET(wfd, &the_state) && (rfd != wfd)) {
        tcp_server_write(wfd, buf, buflen);
      }
    }
    pthread_mutex_unlock(&mutex_state);
  }
  return NULL;
}

void loop(int listen_fd)
/* Server Endlosschleife - accept
 * in listen_fd: Socket Filedescriptor zum Verbindungsaufbau vom Client
 */
{
  pthread_t threads[MAXFD];
 
  FD_ZERO(&the_state);
  for (;;) {                    /* Endlosschleife */
    int rfd;
    void *arg;
   
    /* TCP Server LISTEN Port (Client connect) pruefen */
    rfd = tcp_server_init2(listen_fd);
    if (rfd >= 0) {
      if (rfd >= MAXFD) {
        close(rfd);
        continue;
      }
      pthread_mutex_lock(&mutex_state);
      FD_SET(rfd, &the_state);        /* neuen Client fd dazu */
      pthread_mutex_unlock(&mutex_state);
      arg = (void *) rfd;
      pthread_create(&threads[rfd], NULL, tcp_server_read, arg);
    }
  }
}

int main(int argc, char *argv[])
{
  /* Fehler Logging einschalten */
  openlog(NULL, LOG_PERROR, LOG_WARNING);

  /* open Chat as TCP server */
  loop(tcp_server_init(51234));

  return OKAY;
}

MS-Windows TCP Threads Chat Server Quelltext

Unter Microsoft Windows sind Threads und Sockets ebenfalls verfügbar. Es gibt einige Unterschiede zwischen der Linux und der Microsoft Version. Für Details siehe das select() Beispiel.

/* winchat1d.c
 * Chat Server (Daemon) mit Threads fuer MS-Windows
 * Autor: Andre Adrian
 * Version: 10jun2007
 *
 * Chat Server wartet auf TCP Port 51234 auf einen Verbindungsaufbau
 * von telnet Clients. Der Client sendet ASCII Meldungen an den Server.
 * Die Meldungen werden vom Server an alle anderen Clients gesendet.
 *
 * Aufruf Server:
 *  winchat1d
 *
 * Aufruf Client:
 *  telnet IP_Adresse_Server 51234
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <process.h>

/* Die Makros exit_if() und return_if() realisieren das Error Handling
 * der Applikation. Wenn die exit_if() Bedingung wahr ist, wird
 * das Programm mit Fehlerhinweis Datei: Zeile: Funktion: errno beendet.
 * Wenn die return_if() Bedingung wahr ist, wird die aktuelle Funktion
 * beendet. Dabei wird der als Parameter 2 angegebene Returnwert benutzt.
 */

#define exit_if(expr) \
if(expr) { \
  fprintf(stderr, "exit_if() %s: %d: %s: WinError %d\n", \
  __FILE__, __LINE__, __FUNCTION__, WSAGetLastError()); \
  exit(1); \
}

#define return_if(expr, retvalue) \
if(expr) { \
  fprintf(stderr, "return_if() %s: %d: %s: WinError %d\n\n", \
  __FILE__, __LINE__, __FUNCTION__, WSAGetLastError()); \
  return(retvalue); \
}

#define MAXLEN 1024

#define OKAY 0

volatile fd_set the_state;
volatile SOCKET minfd;
volatile SOCKET maxfd;
CRITICAL_SECTION mutex_state;

SOCKET tcp_server_init(int port)
/* Server (listen) Port oeffnen - nur einmal ausfuehren
 * in port: TCP Server Portnummer
 * return: Socket Filedescriptor zum Verbindungsaufbau vom Client
 */
{
  SOCKET listen_fd;
  int ret;
  struct sockaddr_in sock;

  listen_fd = socket(PF_INET, SOCK_STREAM, 0);
  exit_if(listen_fd < 0);

  memset((char *) &sock, 0, sizeof(sock));
  sock.sin_family = AF_INET;
  sock.sin_addr.s_addr = htonl(INADDR_ANY);
  sock.sin_port = htons(port);

  ret = bind(listen_fd, (struct sockaddr *) &sock, sizeof(sock));
  exit_if(ret != 0);

  ret = listen(listen_fd, 5);
  exit_if(ret < 0);

  return listen_fd;
}

SOCKET tcp_server_init2(SOCKET listen_fd)
/* communication (connection) oeffnen - fuer jeden neuen client ausfuehren
 * in listen_fd: Socket Filedescriptor zum Verbindungsaufbau vom Client
 * return: wenn okay Socket Filedescriptor zum lesen vom Client, SOCKET_ERROR sonst
 */
{
  SOCKET fd;
  struct sockaddr_in sock;
  int socklen;

  socklen = sizeof(sock);
  fd = accept(listen_fd, (struct sockaddr *) &sock, &socklen);
  return_if(fd < 0, SOCKET_ERROR);

  return fd;
}

int tcp_server_write(SOCKET fd, char buf[], int buflen)
/* Schreibe auf die Client Socket Schnittstelle
 * in fd: Socket Filedescriptor zum Schreiben zum Client
 * in buf: Meldung zum Schreiben
 * in buflen: Meldungslaenge
 * return: OKAY wenn Schreiben vollstaendig, SOCKET_ERROR sonst
 */
{
  int ret;

  ret = send(fd, buf, buflen, 0);
  return_if(ret != buflen, SOCKET_ERROR);
  return OKAY;
}

void tcp_server_read(void *arg)
/* Thread fuer einen CONNECT Port
 * Lese von der Client Socket Schnittstelle, schreibe an alle anderen Clients
 * in arg: Socket Filedescriptor zum lesen vom Client
 * return:
 */
{
  SOCKET rfd;

  rfd = (SOCKET)arg;
  for(;;) {
    char buf[MAXLEN];
    int buflen;
    SOCKET wfd;
 
    /* lese Meldung */
    buflen = recv(rfd, buf, sizeof(buf), 0);
    if (buflen <= 0) {
      /* TCP Connection wurde vom Client beendet */
      EnterCriticalSection(&mutex_state);
      FD_CLR(rfd, &the_state);      /* toten Client rfd entfernen */
      LeaveCriticalSection(&mutex_state);
      closesocket(rfd);
      _endthread();
    }
   
    /* Meldung an alle anderen Clients schreiben */
    EnterCriticalSection(&mutex_state);
    for (wfd = minfd + 1; wfd <= maxfd; ++wfd) {
      if (FD_ISSET(wfd, &the_state) && (rfd != wfd)) {
        tcp_server_write(wfd, buf, buflen);
      }
    }
    LeaveCriticalSection(&mutex_state);
  }
}

void loop(SOCKET listen_fd)
/* Server Endlosschleife - accept
 * in listen_fd: Socket Filedescriptor zum Verbindungsaufbau vom Client
 */
{
  FD_ZERO(&the_state);
  minfd = maxfd = listen_fd;
  InitializeCriticalSection(&mutex_state);
  for (;;) {                    /* Endlosschleife */
    SOCKET rfd;
    void *arg;
   
    /* TCP Server LISTEN Port (Client connect) pruefen */
    rfd = tcp_server_init2(listen_fd);
    if (rfd >= 0) {
      EnterCriticalSection(&mutex_state);
      if (rfd > maxfd) {
        maxfd = rfd;
      }
      FD_SET(rfd, &the_state);        /* neuen Client fd dazu */
      LeaveCriticalSection(&mutex_state);
      arg = (void *) rfd;
      _beginthread(tcp_server_read, 0, arg);
    }
  }
  DeleteCriticalSection(&mutex_state);
}

int main(int argc, char *argv[])
{
  WSADATA wsaData;
  int ret;

  /* MS-Windows spezielle Socket Initialisierung */
  ret = WSAStartup(MAKEWORD(2, 2), &wsaData);
  exit_if(ret != 0);
 
  /* open Chat as TCP server */
  loop(tcp_server_init(51234));

  /* MS-Windows spezielle Socket Terminierung */
  WSACleanup();

  return OKAY;
}

Literatur

Vom Lawrence Livermore National Laboratory gibt es ein gutes POSIX Threads Programming Tutorial.

POSIX ist definiert in IEEE Std 1003.1. Diese Definition wurde nach ISO/IEC 9945 übernommen. Die POSIX Standard Dokumente gibt es von IEEE oder online - im Moment kostenfrei nach Registrierung - von The Open Group als Single UNIX Specification.