cpp →
WebService c поддержкой gzip на C
В посте речь пойдет о моем опыте встраивания XML-RPC интерфейса в утилиту, написанную на C. Интерфейс должен предоставлять доступ к статитстике и результатам работы утилиты. Одно из требований к интерфейсу — поддержка ответов в формате gzip, в целях экономии трафика. Мне очень хотелось обойтись малой кровью и вот что из этого получилось.
Начнем с тестов. XML-RPC клиент на python умещается в 4 строчки. Кстати, он как раз понимает ответы в gzip формате.
Отлично! Теперь мы знаем какие HTTP заголовки получает клиент. И если формат некорректен — получаем исключение с подробным стеком вызовов. В случае ошибки, все это поможет нам пролить свет на причину ее возникновения.
В вики написано, что формат gzip основан на алгоритме сжатия deflate, который реализован в библиотеке zlib. В этой библиотеке есть отличный метод compress.
Обрадовавшись находке, я сразу решил попробовать этот метод и набросал простой костяк приложения, но этого оказалось не достаточно. Клиент отказывался понимать содержимое ответов сервера и вываливался с исключением. Пришлось изучить формат gzip продробнее.
Тут все довольно просто.
Сжатые данные обрамляются десятью байтами заголовка специального формата и восьмью байтами суффикса, содержащего контрольную сумму исходных данных и их длину.
Заголовок начинается с магических констант ID1 = 31 (0x1f, \037), ID2 = 139 (0x8b, \213), говорящих о начале данных в формате gzip. Далее идет метод сжатия CM (Compress Method), в случае deflate СM=8. Заним следуют флаги, в нашем случае FLG=1, что означает текстовые данные. Потом идут 4 байта даты последнего изменение исходных данных, в нашем случае MTIME=0. Затем идут дополнительные флаги XFL=2 (высокая степень сжатия). Имя операционной системы позволим себе оставить неопределенным OS=255.
Для вычисления контрольной суммы воспользуемся функцией из той же zlib
Но и этого оказывается мало. Нашего клиента все еще не устраивают ответы сервера.
Посмотрим, в каком формате возвращает нам данные zlib.
Оказалось, zlib добавляет специальные 2-байтный префикс и 4-байтный суффикс к сжатым данным (подробнее). Избавимся от них и добавим заголовок и суффикс формата gzip.
И, о чудо! Клиент наконец-то нас понял!
На заметку: в библиотеке Qt есть метод qCompress(), который возвращает данные сжатые библиотекой zlib, но еще и с 4-байтным префиксом длины сжатых данных.
Чтобы сформировать данные в формате gzip, сжимаем исходные данные функцией compress, в полученном массиве первые 2 байта заменяем 10-байтным заголовком gzip, вместо последних 4 байт ставим контрольную сумму и длину исходных данных.
Пример рабочего XML-RPC сервера, возвращающего данные в gzip формате приведен ниже.
Прежде всего тесты
Начнем с тестов. XML-RPC клиент на python умещается в 4 строчки. Кстати, он как раз понимает ответы в gzip формате.
import xmlrpclib
if __name__ == '__main__':
proxy = xmlrpclib.ServerProxy("http://localhost:8080/", verbose=True)
print proxy.sayHello()
Отлично! Теперь мы знаем какие HTTP заголовки получает клиент. И если формат некорректен — получаем исключение с подробным стеком вызовов. В случае ошибки, все это поможет нам пролить свет на причину ее возникновения.
Zlib
В вики написано, что формат gzip основан на алгоритме сжатия deflate, который реализован в библиотеке zlib. В этой библиотеке есть отличный метод compress.
int compress (Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen);
Обрадовавшись находке, я сразу решил попробовать этот метод и набросал простой костяк приложения, но этого оказалось не достаточно. Клиент отказывался понимать содержимое ответов сервера и вываливался с исключением. Пришлось изучить формат gzip продробнее.
Gzip
Тут все довольно просто.
Сжатые данные обрамляются десятью байтами заголовка специального формата и восьмью байтами суффикса, содержащего контрольную сумму исходных данных и их длину.
Заголовок начинается с магических констант ID1 = 31 (0x1f, \037), ID2 = 139 (0x8b, \213), говорящих о начале данных в формате gzip. Далее идет метод сжатия CM (Compress Method), в случае deflate СM=8. Заним следуют флаги, в нашем случае FLG=1, что означает текстовые данные. Потом идут 4 байта даты последнего изменение исходных данных, в нашем случае MTIME=0. Затем идут дополнительные флаги XFL=2 (высокая степень сжатия). Имя операционной системы позволим себе оставить неопределенным OS=255.
Для вычисления контрольной суммы воспользуемся функцией из той же zlib
uLong crc32 (uLong crc, const Bytef *buf, uInt len);
Но и этого оказывается мало. Нашего клиента все еще не устраивают ответы сервера.
И снова zlib
Посмотрим, в каком формате возвращает нам данные zlib.
Оказалось, zlib добавляет специальные 2-байтный префикс и 4-байтный суффикс к сжатым данным (подробнее). Избавимся от них и добавим заголовок и суффикс формата gzip.
И, о чудо! Клиент наконец-то нас понял!
На заметку: в библиотеке Qt есть метод qCompress(), который возвращает данные сжатые библиотекой zlib, но еще и с 4-байтным префиксом длины сжатых данных.
Итог
Чтобы сформировать данные в формате gzip, сжимаем исходные данные функцией compress, в полученном массиве первые 2 байта заменяем 10-байтным заголовком gzip, вместо последних 4 байт ставим контрольную сумму и длину исходных данных.
Пример рабочего XML-RPC сервера, возвращающего данные в gzip формате приведен ниже.
#include <zlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define PORT 8080
#define MAXCONN 5
#define BUF_SZ 1024
#define ZLIB_PREFIX_SZ 2
#define ZLIB_SUFFIX_SZ 4
#define GZIP_PREFIX_SZ 10
#define GZIP_SUFFIX_SZ 8
// Returns listen socket handle
int create_srvsock(int port, int maxconn);
// Returns response to be sent back
int get_response(int clisock, char *response);
// Writes given data range to socket
void write_range(int sock, const char *begin, const char *end);
// Write int value to socket
void write_int(int sock, int value);
// Prints error message and exit
void error(const char *msg);
int main(int argc, const char *argv[]) {
fprintf(stderr, "HTTP Server with gzip encoding support using zlib (%s)\r\n", ZLIB_VERSION);
char httpheaders[BUF_SZ] = {0,};
char response[BUF_SZ] = {0,};
char compressed[BUF_SZ] = {0,};
int srvsock = create_srvsock(PORT, MAXCONN);
fprintf(stderr, "Server is started on port %d\r\n", PORT);
while (true) {
struct sockaddr_in addr = {0,};
socklen_t addrlen = sizeof(addr);
// 1. Accepting connection
int clisock = accept(srvsock, (struct sockaddr *)&addr, &addrlen);
// 2. Retreiving response
int responselen = get_response(clisock, response);
// 3. Compressing response
long unsigned int compressedlen = BUF_SZ;
if (compress((unsigned char *)compressed, &compressedlen
, (const unsigned char *)response, responselen) != Z_OK)
error("Can not compress");
// substract zlib prefix and suffix: http://www.ietf.org/rfc/rfc1950.txt
compressedlen -= ZLIB_PREFIX_SZ + ZLIB_SUFFIX_SZ;
// 4. Writing HTTP headers
int contentlen = GZIP_PREFIX_SZ + compressedlen + GZIP_SUFFIX_SZ;
int httpheaderslen = sprintf(httpheaders,
"HTTP/1.1 200 OK\r\n"\
"Content-Type: text/xml\r\n"\
"Content-Encoding: gzip\r\n"\
"Content-Length: %d\r\n\r\n", contentlen);
write_range(clisock, httpheaders, httpheaders + httpheaderslen);
// 5. Writing gzip headers: http://www.gzip.org/zlib/rfc-gzip.html
const char gzipheader[] = {
0x1f, 0x8b // gzip magic number
, 8 // compress method "defalte"
, 1 // text data
, 0, 0, 0, 0 // timestamp is not set
, 2 // maximum compression flag
, 255 // unknown OS
};
write_range(clisock, gzipheader, gzipheader + sizeof(gzipheader));
// 6. Write compressed data
write_range(clisock, compressed + ZLIB_PREFIX_SZ
, compressed + ZLIB_PREFIX_SZ + compressedlen);
// 7. Append crc32
write_int(clisock, (int)crc32(0, (unsigned char *)response, responselen));
// 8. Append initial size
write_int(clisock, responselen);
}
return EXIT_SUCCESS;
}
// Returns listen socket handle
int create_srvsock(int port, int maxconn) {
int sock = 0;
struct sockaddr_in addr = {0,};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
error("Can not open socket");
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
error("Can not bind socket");
if (listen(sock, maxconn) < 0)
error("Can not listen socket");
return sock;
}
// Returns response to be sent back
int get_response(int clisock, char *response) {
return sprintf(response, "<?xml version=\"1.0\"?>\r\n"\
"<methodResponse>\r\n"\
" <params><param><value>Hello there!</value></param></params>\r\n"\
"</methodResponse>");
}
// Writes given data range to socket
void write_range(int sock, const char* begin, const char *end) {
for (const char *it = begin; it != end;) {
int written = write(sock, it, end - it);
if (written < 0)
error("Can not write to socket");
it += written;
}
}
// Write int value to socket
void write_int(int sock, int value) {
const char *data = (const char *)&value;
write_range(sock, data, data + sizeof(int));
}
// Prints error message and exit
void error(const char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}
22.11.2011 19:53+0400