čtvrtek 8. prosince 2011

Multiplatformní Sockety a TCP pro Windows a Linux v C/C++

Úvod

Programování TCP v C je rozdílné ve Windows a Linuxu. Existují wrappery a různé knihovny, které funkcionalitu sjednocují a zlepšují. Rozdíly jsou ale tak malé, že by mohli být řešeny preprocesorem, které by mělo několik výhod:

  • Kód se může psát nativně pro jednu platformu.
  • Direktivy budou krátké a jednoduché.
  • Zkompilovaný bude rychlý, protože nebude žádná další režie (wrappery).

Hlavičkové soubory

Každý systém má vlastní sadu hlavičkových souborů, které jsou nutné pro funkcionalitu socketů:

Windows

Existuje několik možných hlavičkových souborů mimo tento (windows.h, Winsock.h).

#include <WinSock2.h>

Linux

#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

Porovnání funkcí

Porovnával jsem následující funkce:


  • socket
  • bind
  • listen
  • accept
  • send
  • recv
  • close (closesocket)

Funkce socket

Definice

Windows

SOCKET socket(int af, int type, int protocol);

Linux

int socket(int domain, int type, int protocol);

Parametry

Parametry jsou stejného datového typu.

Návratová hodnota

Z definice funkce se zdá, že jediným rozdílem je návratová hodnota, která je ve Windows typu SOCKET a linuxu typu int.
Ve winsock.h je SOCKET definován takto:

typedef u_int           SOCKET;
A u_int následně takto:
typedef unsigned int    u_int;
Je to tedy unsigned int a int.

Úspěch:
V případě úspěchu funkce vrací kladné číslo vyjadřující deskriptor daného socketu pro oba operační systémy.
Chybový stav:

Windows

Vrací konstantu INVALID_SOCKET, která je definována:
#define INVALID_SOCKET  (SOCKET)(~0)

Linux

Vrací -1, což je binární vyjádření hodnoty ~0

Funkce bind

Definice

Windows

int bind(SOCKET s, const struct sockaddr *name, int namelen);

Linux

int bind(int s, struct sockaddr *my_addr, socklen_t addrlen);

Parametry

První parametr je rozdílný - SOCKET vs int. Rozdíly byly popsány u návratové hodnoty funkce socket.
V linuxu je třetí parametr typu socklen_t, ve windows je to int. Parametr obsahuje velikost truktury sockaddr* druhého parametru. Pokud ho získáme pomocí funkce sizeof("2. parametr"), v linuxu se vrátí datový typ kompatibilní s socketlen_t (nenašel jsem přesnou dokumentaci) a program se bez problémů zkompiluje.

Návratová hodnota

Návratová hodnota je 0 v případě úspěchu. V případě neúspěchu je v linuxu vrácena hodnota -1 a ve windows konstanta SOCKET_ERROR, která je taky -1.

Funkce listen

Definice

Windows

int listen(SOCKET s, int backlog);

Linux

int listen(int s, int backlog);

Parametry

Podobně jako u funkce bind je zde rozdílný datový typ pro socket.

Funkce accept

Definice

Windows

SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen); 

Linux

int accept(int s, struct sockaddr *addr, socklen_t *address_len);

Parametry

Podobně jako u funkce bind je zde rozdílný datový typ pro socket. Další parametry jsou stejné jako u funkce bind s tím rozdílem, že tentokrát se posílají pointery.
Zde může nastat problém, protože některé překladače v linuxu nepovolí třetí parametr typu int bez explicitního přetypování (g++).

Návratová hodnota

Stejně jako u funkce socket.


Funkce send

Definice

Windows

int send(SOCKET s, const char *buf, int len, int flags);

Linux

int send(int s, const void *buf, size_t len, int flags); 

Parametry

Stejný problém s datovým typem socketu a datovým typem pro velikost (size_t vs int).

Návratová hodnota

Windows

Počet odeslaných bytů, případně SOCKET_ERROR (-1) pokud nastala chyba.

Linux

Počet odeslaných bytů, případně -1.


Funkce recv

Definice

Windows

int recv(SOCKET s, char *buf, int len, int flags);

Linux

int recv(int s, void *buf, size_t len, int flags);

Parametry

Stejný problém s datovým typem socketu a datovým typem pro velikost (size_t vs int).

Návratová hodnota

Počet přijatých bytů, případně SOCKET_ERROR (-1) pokud nastala chyba. 0 pokud byl socket ukončen standardně.


Funkce close

Definice

Tady je problém přímo v definici - názvy nesouhlasí.

Windows

int closesocket(SOCKET s);

Linux

int close(int fd);

Parametry

Stejný problém s datovým typem socketu.

Návratová hodnota

0 v případě úspěchu, SOCKET_ERROR (-1) pokud nastala chyba.

Windows - specifické funkce

Ve windows se navíc používají funkce na inicializaci a ukončení používání socketů, mezi základní patří funkce:
WORD MAKEWORD(BYTE bLow, BYTE bHigh); (makro)
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
int WSACleanup(void);
struktura WSADATA (LPWSADATA)
Toto je jen nejzákladnější výčet, funkcí existuje víc. Tyto funkce jsou platformně velmi specifické, ale v jednoduchých případech není potřeba udělat nic víc než startup a cleanup.

Řešení

Mým snažením je udělat řešení tak, aby se dala aplikace psát nativně pro jednu platformu a pomocí makra byla funkční i v druhé platformě. Protože na windows je nutné používat WSA funkce, rozhodl jsem se tuto platformu použít jako nativní.

Odstranění problémů s WSA funkcemi

V linuxu není nutné tyto funkce volat, proto stačí jen vytvořit funkce s prázdným tělem vracející defaultní hodnoty, případně návratové hodnoty úspěchu.

Odstranění rozdílných datových typů pro socket

Protože nativní platformou je Windows, nejjednodušším způsobem je dodefinováním datového typu SOCKET.

Odstranění problému s socklen_t*

Pomocí makra přejmenujeme veškeré výskyty funkce accept na funkci acceptSocket, která bude vnitřně volat funkci accept s explicitním přetypováním na socklen_t*

Vytvoření konstant

V linuxu nejsou standartně konstanty SOCKET_ERROR a INVALID_SOCKET, stačí je přidat z winsock.h

Zdrojový kód

networking.h

#pragma once



#ifdef __unix__ /* __unix__ is usually defined by compilers targeting Unix systems */
//g++
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define closesocket(socket) close(socket)

typedef int SOCKET;
typedef unsigned short WORD;
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)

struct WSADATA {
 int x;
};


WORD MAKEWORD(int a, int b);

int WSAStartup(int a, WSADATA* b);
int WSACleanup();

#define accept(socket,addr,addrlen) acceptSocket(socket,addr,addrlen)
int acceptSocket(int s, struct sockaddr *addr, int* addrlen);

#elif defined _WIN32 /* _Win32 is usually defined by compilers targeting 32 or 64 bit Windows systems */
//WIN
#include <WinSock2.h>

#endif

networking.cpp

#include "networking.h"


#ifdef __unix__
//g++

WORD MAKEWORD(int a, int b) {
 return 0;
}

int WSAStartup(int a, WSADATA* b) {
 return 0;
}

int WSACleanup() {
 return 0;
}

#define acceptSocket(socket,addr,addrlen) accept(socket,addr,addrlen)

int acceptSocket(int s, struct sockaddr *addr, int* addrlen) {
 return accept(s, addr, (socklen_t*) addrlen);
}
#define accept(socket,addr,addrlen) acceptSocket(socket,addr,addrlen)

#endif

Závěr

Výše uvedený hlavičkový soubor umožňuje mým C++ projektům ve Visual Studiu 2010 zkompilovatelost v linuxu pomocí g++ bez nutnosti provádění změn. Jsem céčkař začátečník a tak nevím jestli neexistuje podobné lepší řešení (žádné jsem nenašel). Některé věci by určitě šli udělat líp a věrohodněji (aby WSA api opravdu něco dělalo, např. vracelo chybové kódy).

Zdroje

http://en.wikipedia.org/wiki/C_preprocessor
http://www.allegro.cc/forums/thread/370013/370226#target
http://tangentsoft.net/wskfaq/articles/bsd-compatibility.html
http://www.builder.cz/art/cpp/tcp_server_linux.html
http://www.builder.cz/art/cpp/tcp_server_windows.html
http://research.microsoft.com/en-us/um/redmond/projects/invisible/include/winsock.h.htm
http://pubs.opengroup.org/onlinepubs/7908799/xns/syssocket.h.html