Sunday, January 30, 2011

Программирование сокетов в Linux

При написании приложения для Андроид иногда (очень редко) возникает необходимость пообщаться с сервером на уровне более низком, чем Java SDK. Например, если вы создаете низкоуровневый сервис, работающий напрямую с данными оператора (загрузка обновления системы, активация телефонного девайса в сети оператора и т.д.), приходится иметь дело с сокетами (в грубом приближении - инетрфейсами между железом и програмной частью).

Работать с сокетами на низком уронде в Андроиде (т.е. на ядре линукс) можно с помощью следующего набора заклинаний.

#define WIN32

// заголовки стандартного C
#include <string.h>
#include <stdio.h>

// подключаем заголовки для разных OS
#if defined WIN32
#include <winsock.h>  // WinSock
#elif defined LINUX
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#endif

// переопределяем некоторые типы и константы
// в зависимости от OS
#if defined WIN32
typedef int socklen_t;  // размер сокета в Unix
#elif defined LINUX
typedef int SOCKET;
#define INVALID_SOCKET -1  // костанта WinSock
#define SOCKET_ERROR   -1  // стандартная ошибка WinSock

// WinSock, в отличие от Unix, не использует файл-дескрипторы
// поэтому переопределяем наименование метода
#define closesocket(s) close(s);  
#endif

int init_sockaddr_in(char *hostname, struct sockaddr_in *sin);
int client_process(int fd, char *hostname);
int send_request(int fd, char *hostname);
int recv_reply(int fd);


int main()
{
    struct sockaddr_in  server;
    SOCKET              sockfd;
    int                    recvMsgSize;
    
    // имя сервера, может быть вида "www."""".com" и т.д.
    char * srv =        "localhost"; 

#if defined WIN32
    // для Windows мы должны инициализировать Winsock
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(1,1), &wsa_data);
#endif

    // создаем сокет
    sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(sockfd == INVALID_SOCKET) {
        perror("Could not create socket.\n");
        return(SOCKET_ERROR);
    }

    // пробуем создать метаданные сервера
    recvMsgSize = init_sockaddr_in(srv, &server);
    if(recvMsgSize < 0)
    {
        perror("Could not initialize address.\n" );
        return(SOCKET_ERROR);
    }
    
    // пытаемся соединиться через сокет
    recvMsgSize = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
    if(recvMsgSize < 0)
    {
        perror("Error while connecting to server.\n");
        return(SOCKET_ERROR);
    }
    
    // посылаем сообщения на сервер и принимаем респонс
    recvMsgSize = client_process(sockfd, srv);
    
    printf("Program has ended successfully.");
    
#if defined WIN32
    // для Windows мы должны явно указать, что завершаем работу Winsock
    WSACleanup();
#endif
    
    return 0;
}

int init_sockaddr_in(char *hostname, struct sockaddr_in *sin)
{
    struct servent *serv;

    // утсанавливаем поинтер в памяти
    memset(sin, 0, sizeof(*sin));
    sin->sin_family = AF_INET;

    // проверяем, в каком виде записан адрес сервера: IP или "www.****.com"
#if defined LINUX
    if( !inet_aton( hostname, &sin->sin_addr ) )
#endif
    {
        // если запись в виде "www.****.com", ищем IP адрес
        struct hostent *host = gethostbyname(hostname);
        if(!host)
            return SOCKET_ERROR;

        sin->sin_addr.s_addr = *(unsigned long*)host->h_addr_list[0];
    }

    // получаем дефолтный порт для выбранного протокола
    serv = getservbyname("http", "tcp");
    if(!serv)
        return SOCKET_ERROR;

    sin->sin_port = serv->s_port;

    return EXIT_SUCCESS;
}

int client_process(int socketId, char *hostname)
{
    // посылаем данные    
    printf("Sending...\n");
    if(send_request(socketId, hostname))
    {
        // неудачно скастовали заклинание
        return SOCKET_ERROR;
    }
    
    // получаем ответ от сервера
    printf("Receiving reply...\n");
    if(recv_reply(socketId))
    {
        // ошибка магии
        return SOCKET_ERROR;
    }
    return EXIT_SUCCESS;
}

int send_request(int fd, char *hostname)
{
    // здесь мы должны сформировать запрос к серверу
    // запрос должен содержать стандартные заголовки и константы
    // типа таких: "GET /foo/bar/baz/ HTTP/1.1\r\nHost: www.myfoo.com Connection: close"
    // 
    char format[150] = "GET /foo/bar/baz/ HTTP/1.1\r\nHost: ";
    int length = 0;
    int ret = 0;

    strcat(format, hostname);
    strcat(format, "\r\nConnection: close\r\n\r\n");

    length = strlen(format);

    // посылаем запрос format длиной length через сокет fd  
    ret = send(fd, format, length, 0);
    if(ret != length )
        return SOCKET_ERROR;

    return EXIT_SUCCESS;
}

int recv_reply(int fd)
{
    fd_set readfds;
    
    // определяем буффер для входящих данных
    char buf[256];
    int length;

    while(1)
    {
        // инициализируем буффер
        FD_ZERO(&readfds);
        FD_SET(fd, &readfds);
        memset(buf, 0, sizeof(buf));

        // ждем прибытия данных с сервера
        if(select(fd+1, &readfds, NULL, NULL, NULL) < 0 )
        {
            // ошибочка в заклинании
            return SOCKET_ERROR;
        }
        
        // проверяем, если ли в дескрипторе переданные данные
        if(FD_ISSET(fd, &readfds))
        {
            // получаем их
            length = recv(fd, buf, sizeof(buf)-1, 0);
            if(length < 0)
                return SOCKET_ERROR;
            else if(length == 0)    // данные закончились
                break;
            
            // вываливаем полученный буфер куда хотим
            printf("%s\n", (char*)buf);
        }
    }
    return EXIT_SUCCESS;
}


Немного примечаний к волшебству.

Отношения с сокетами на этом уровне примерно одинаковые в Linux и в Windows. Я по долгу работы и в силу личных предпочтений пишу код в Windows (и чтобы не запусать лишний раз ресурсоемкий билд). Для этого в коде введен переключатель для определения условия компиляции. При отсутствии необходимости в нем, его легко можно удалить и компилироваться исключительно под выбранную ОС.