Глава 1 Установка связи с SMSC. В предыдущей статье мы вкратце
остановились на описании общего механизма работы SMS, упомянули
некоторые протоколы и наметили основные задачи, которые придется решить
при написании SMS клиента. Однако прежде, чем приступить к обсуждению
данных вопросов, вернемся ненадолго к терминологии. В тот момент, когда
предыдущая статья уже версталась, к нам поступило ценное замечание. В
статье мы (произвольно!) использовали аббревиатуру ``MT'' для
обозначения сотового телефона, приравняв ее к MS (Mobile Station).
Однако, в стандарте ``MT'' используется применительно к сервисам и
обозначает Mobile Terminated (в противоположность Mobile Originated).
Мы принимаем это замечание и в дальнейшем будем использовать MS для
данных целей (в литературе также встречается аббревиатура SMT -- Short
Messages Terminal -- для обозначения MS и ESME). Итак, мы выделили следующие задачи: Установка соединения по TCP/IP с сервис-центром. Формирование пакетов в формате выбранного нами протокола. «Разбор» (parse) пакетов в формате выбранного протокола.
В данной статье мы сосредоточимся на первой задаче. Вообще-то, мы не
собирались здесь вдаваться в детали программирования сокетов (sockets),
полагая, что читатели знакомы с данным вопросом. Однако думается, что
несколько слов сказать все же стоит. Тем не менее мы настоятельно (а
как же советуем тем, кто не знаком с данным вопросом, изучить его
подробнее применительно к той ОС под которой придется программировать. (для UNIX см. например http://world.std.com/~jimf/papers/sockets/sockets.html)
мы же приведем простую реализацию, которая нам понадобится в
дальнейшем. Те же, кто уже сталкивался с программированием сокетов
могут запросто пропустить данную статью, обратившись, может быть, к
нескольким последним абзацам. Глава 2 Использование сокетов. 2.2 Общие принципы.
Связь по TCP/IP устанавливается по принципу "точка-точка"; инициирующая
сторона называется клиентом, принимающая -- сервером. Сервер постоянно
находится в ожидании входящих соединений (как говорят, "слушает" --
listening), клиент же посылает запрос на установление связи, используя
IP-номер (IP-адрес) сервера и номер порта. IP-адрес это
тридцатидвухразрядное число, представляемое обычно в т. н. dotted
нотации: XXX.XXX.XXX.XXX (байты разделены точками, кажда
из групп XXX может принимать значения от 0 до 255). Номер же прота
можно рассматривать как указание на конкретный сервис данного узла.
Таким образом, для установки соединения клиенту необходимо знать пару
чисел IP-адрес:порт (например 192.18.97.241:80 дает нам www-сервер
компании Sun Microsystems :). Мы не станем здесь останавливаться на
службе доменных имен (предыдущий пример можно записать проще:
http://www.sun.com:80), URL и прочем, полагая, что читателю это
знакомо. Заметим только, что существуют стандартные соглашения на
присваивание номеров портов сервисам (в предыдущем примере использован
порт 80 -- http; можно упомянуть порт 21 -- ftp, 23 -- telnet и 25 --
smpt), посему для "нестандартных" сервисов рекомендуется брать
"большие" номера (мы предпочитаем номера начиная с 8100). Кстати, из
вышесказанного видно, что работа с сокетами на клиентской и серверной
сторонах различна. Мы начнем (сюрприз!) с серверной части. 2.2 Сервер. Простейшая реализация TCP/IP сервера может быть представлена следующим кодом (socktest.c): -------------------------------------------------------------------------------- #ifdef _WIN32 #include #else #include #include #include #include #include #include typedef struct sockaddr SOCKADDR; typedef struct sockaddr_in SOCKADDR_IN; #define SOCKET_ERROR -1 #define INVALID_SOCKET -1 #define closesocket(s) close(s) #endif /* _WIN32 */ #include int main(int argc, char** argv) { SOCKADDR_IN sockaddr; SOCKADDR_IN descr; int addr_len = sizeof(SOCKADDR_IN); #ifdef _WIN32 SOCKET sock; SOCKET newsock; WSADATA WSAData; /* Startup socket library */ if (WSAStartup(MAKEWORD(1,1), &WSAData) != 0) perror("Can't initialize socket library"); #else int sock; int newsock; #endif /* _WIN32 */ /* create socket */ sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == INVALID_SOCKET) { perror("Can't open socket"); return 1; } /* filling up sockaddr structure */ sockaddr.sin_family = AF_INET; sockaddr.sin_addr.s_addr = INADDR_ANY; sockaddr.sin_port = htons(8100); /* bind socket */ if (bind(sock, (const SOCKADDR *)&sockaddr, sizeof(SOCKADDR_IN))) { perror("Can't bind socket"); closesocket(sock); return 1; } /* start listening */ if (listen(sock, 1) == SOCKET_ERROR) { perror("Can't start listening"); closesocket(sock); return 1; } else { /* accept connection (note that accept() is the blocking call) */ newsock = accept(sock, (SOCKADDR *)&descr, &addr_len); if (newsock != INVALID_SOCKET) { printf("Connection is accepted. Peer: %s\n", inet_ntoa(descr.sin_addr)); if (send(newsock, "Hello from server", strlen("Hello from server"), 0) == SOCKET_ERROR) perror("Send operation failed"); } else perror("Can't accept connection"); closesocket(sock); closesocket(newsock); } return 0; } --------------------------------------------------------------------------------
Мы постарались сделать код переносимым (по крайней мере между Windows и
Linux. Для того, чтобы собрать данный пример под Windows мы должны
указать компоновщику на библиотеку wsock32.lib). Как видно из
предыдущего примера, "открытие порта на прослушку" -- операция
достаточно простая: необходимо создать сокет (socket(2)), заполнить и
связать с сокетом структуру sockaddr_in (bind(2)), после чего вызвать
listen(2). В данном примере сервер начинает "слушать" по порту 8100. По
приходу запроса отрабатывает функция accept(2), которая создает новый
сокет, оставляя "старый" готовым к приему нового соединения. Новый
сокет готов к приему-передаче данных, мы посылаем приветствие и
закрываем оба сокета (тонко, правда? ;). Обратим внимание на
то, что accept является блокирующим вызовом, т. е. поток исполнения не
проходит ниже этой строчки, пока не принято входящее соединение, и наша
программа не может в это время делать ничего, кроме как "болтаться в
accept'е". Кроме того, данный пример написан так, что принимает только
одно соединение. Мы могли бы не закрывать первый сокет, а снова вызвать
с ним accept для приема второго соединения, однако проблема блокировки
вызовом accept все равно не была бы решена (несколько забегая вперед,
заметим, что и функция приема данных из сокета recv(2) также является
блокирующей). Часто данную проблему снимают организуя многопоточное
(multithreaded) приложение, в котором каждое соединение обрабатывается
в собственном потоке или, под UNIX, используют вызов разделения
процесса fork(2) (кстати, ежели кто не понял, зачем двойки в скобках,
-- это означает вторую секцию руководства). Добиться переносимости
такого кода -- задача совсем нетривиальная, мы же пока не хотим
привязываться к платформе, насколько это возможно, и потому
воспользуемся вызовом select(2), который присутствует и в UNIX и в
Windows. Функция select ожидает изменения статуса набора дескрипторов
(в Windows поддерживаются только сокеты, а в UNIX -- файловые
дескрипторы, коими сокеты и являются). Кроме того, нам потребуется
перевести наши сокеты в неблокирующее состояние (non-blocking mode).
Все вышесказанное отражено в следующем примере, состоящем из трех
файлов (по прежнему, в Windows следует подключать библиотеку
wsock32.lib): smsce.h -------------------------------------------------------------------------------- #ifndef _SMSCE_H_ #ifdef _WIN32 #include #define socklen_t int #else #include "unisock.h" #include #include #endif #define _SMSCE_H_ #endif /* _SMSCE_H_ */ -------------------------------------------------------------------------------- smsce.cpp -------------------------------------------------------------------------------- #include #include #include #include "smsce.h" #define SERVER_PORT 8200 #define RECVBUFSIZ 4096 using namespace std; bool process_data(SOCKET sock) { static char buf[RECVBUFSIZ]; int received; if ((received = recv(sock, buf, RECVBUFSIZ, 0)) != SOCKET_ERROR) { buf[received] = '\0'; cout << (char *)buf << flush; return true; } return false; } static void shutdown_socket(SOCKET *s) { if (*s != INVALID_SOCKET) { shutdown (*s, SD_BOTH); closesocket(*s); *s = INVALID_SOCKET; } } int main(int argc, char **argv) { #ifdef _WIN32 WSADATA WSAData; /* Startup socket library */ if (WSAStartup(MAKEWORD(1,1), &WSAData) != 0) perror("Can't initialize socket library");#endif // list for server clients typedef list CL; CL clients; CL::iterator ii; struct timeval tv; fd_set readfds; fd_set exfds; SOCKET ssocket; SOCKET accepted; SOCKET maxfd; bool true_value = true; struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(SERVER_PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); // Creating the socket and setting it's optioins ssocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if (ssocket == INVALID_SOCKET) { perror("Can't create socket"); return 1; } setsockopt(ssocket, SOL_SOCKET, SO_REUSEADDR, (char *) &true_value, sizeof (true_value)); #ifdef _WIN32 ioctlsocket(ssocket, FIONBIO, (unsigned long *)&true_value); // Set to non-block mode #else fcntl(ssocket, F_SETFL, O_NONBLOCK); // Set to non-block mode #endif // _WIN32 // Binding if (bind(ssocket, (struct sockaddr *) &addr, sizeof (addr)) == SOCKET_ERROR) { perror("Can't start listening"); return 1; } // sockaddr for client socket struct sockaddr_in ca; int cal = sizeof(ca); // Start listening if (listen (ssocket, SOMAXCONN) == SOCKET_ERROR) { perror("Can't start listening"); return 1; } while (true) { // Trying to accept connection (non-blocking mode) // Please note that if no incoming connection presents at non-blocking // socket accept returns with some error like EAGAIN or EWOULDBLOCK if ((accepted = accept(ssocket, (struct sockaddr *) &ca, (socklen_t *)&cal)) != SOCKET_ERROR) clients.push_back(accepted); // Preparing descriptor sets FD_ZERO(&readfds); FD_ZERO(&exfds); FD_SET(ssocket, &exfds); tv.tv_sec = 1; tv.tv_usec = 0; maxfd = ssocket; for (ii = clients.begin(); ii != clients.end(); ++ii) { FD_SET((SOCKET )*ii, &readfds); FD_SET((SOCKET )*ii, &exfds); maxfd = max(maxfd, (SOCKET )*ii); } // select failing breaks the work if (select(maxfd + 1, &readfds, NULL, &exfds, &tv) == -1) break; // On exception in server socket also breaks immediately if(FD_ISSET(ssocket, &exfds)) break; // Test events on client sockets for (ii = clients.begin(); ii != clients.end(); ++ii) { if (FD_ISSET(*ii, &exfds)) { if (*ii != INVALID_SOCKET) shutdown_socket(&(*ii)); if ((ii = clients.erase(ii)) == clients.end()) break; } if (FD_ISSET(*ii, &readfds) && !process_data(*ii)) { if (*ii != INVALID_SOCKET) shutdown_socket(&(*ii)); if ((ii = clients.erase(ii)) == clients.end()) break; } // Send data if (*ii != INVALID_SOCKET) if (send(*ii, "Connection is established ", strlen("Connection is established "), 0) == SOCKET_ERROR) if ((ii = clients.erase(ii)) == clients.end()) break; } } for (ii = clients.begin(); ii != clients.end(); ++ii) if (*ii != INVALID_SOCKET) shutdown_socket(&(*ii)); shutdown_socket(&ssocket); return 0; } -------------------------------------------------------------------------------- unisock.h -------------------------------------------------------------------------------- #ifndef _UNISOCK_H_ #ifndef _WIN32 #include #include #include #include #include #include typedef int SOCKET; typedef struct sockaddr SOCKADDR; typedef struct sockaddr_in SOCKADDR_IN; #define SOCKET_ERROR -1 #define INVALID_SOCKET -1 #define SD_RECEIVE 0x0 #define SD_SEND 0x1 #define SD_BOTH 0x2 #define closesocket(s) close(s) #endif /* _WIN32 */ #define _UNISOCK_H_ #endif /* _UNISOCK_H_ */ --------------------------------------------------------------------------------
В этом примере мы получили возможность обрабатывать несколько входящих
соединений (хотя, если в канале нет данных от клиента, то select ждет 1
секунду; таким образом, мы не можем отправлять данные клиентам чаще, но
этого нам в дальнейшем будет достаточно) и не останавливаться на
блокирующих вызовах. Интервал в 1 секунду выбран произвольно. Мы можем
испытать наш сервер, набрав команду: telnet localhost 8200
Остановить выполнение сервера можно с помощью Ctrl-C :). Разумеется, в
приведенном примере еще многое можно "подрихтовать" (например, можно
проверять, доступен ли сокет для записи перед вызовом send или
проверять код ошибки accept), но мы объявим серверную часть готовой и
перейдем, наконец, к клиенту. 2.3 Клиент.
Программирование клиентских сокетов несколько проще, чем серверных. На
клиенте достаточно создать сокет с помощью socket(2) и соединить с
удаленной стороной с помощью connect(2). После этого сокет готов к
приему и передаче данных. Просто приведем пример. sockclient.h -------------------------------------------------------------------------------- #ifndef _SMSCE_H_ #ifdef _WIN32 #include #define socklen_t int #else #include "unisock.h" #include #include #define Sleep(x) usleep((unsigned long )(x * 1000)) #endif #define _SMSCE_H_ #endif /* _SMSCE_H_ */ -------------------------------------------------------------------------------- sockclient.cpp -------------------------------------------------------------------------------- #include #include #include "sockclient.h" #define SERVER_ADDR "127.0.0.1" // localhost #define SERVER_PORT 8200 #define RECVBUFSIZ 4096 using namespace std; int main(int argc, char **argv) { #ifdef _WIN32 WSADATA WSAData; // Startup socket library if (WSAStartup(MAKEWORD(1,1), &WSAData) != 0) perror("Can't initialize socket library"); #endif SOCKET soc; struct sockaddr_in addr; static char buf[RECVBUFSIZ]; int received; addr.sin_family = AF_INET; // Server address addr.sin_addr.s_addr = inet_addr(SERVER_ADDR); // Server port addr.sin_port = htons(SERVER_PORT); // Creating socket if ((soc = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) { perror("Can't create socket"); return 1; } // Perform connection if (connect(soc, (struct sockaddr *) &addr, sizeof(addr)) == SOCKET_ERROR) { perror("Can't connect"); return 1; } cout << "Connection is established" << endl; // Try to receive greeting. // Note thar receive is the blocking call if ((received = recv(soc, buf, RECVBUFSIZ, 0)) != SOCKET_ERROR) { buf[received] = '\0'; cout << (char *)buf << flush; } else { perror("Receive operation failed"); closesocket(soc); return 1; } // Try to send greeting if (send(soc, "Hello from client ", strlen("Connection is established "), 0) == SOCKET_ERROR) { perror("Hello from client "); closesocket(soc); return 1; } closesocket(soc); return 0; } -------------------------------------------------------------------------------- unisock.h остался без изменений: -------------------------------------------------------------------------------- #ifndef _UNISOCK_H_ #ifndef _WIN32 #include #include #include #include #include #include typedef int SOCKET; typedef struct sockaddr SOCKADDR; typedef struct sockaddr_in SOCKADDR_IN; #define SOCKET_ERROR -1 #define INVALID_SOCKET -1 #define SD_RECEIVE 0x0 #define SD_SEND 0x1 #define SD_BOTH 0x2 #define closesocket(s) close(s) #endif /* _WIN32 */ #define _UNISOCK_H_ #endif /* _UNISOCK_H_ */ --------------------------------------------------------------------------------
В этом примере мы устанавливаем соединение с нашим сервером, дожидаемся
приветствия, посылаем ответное и закрываем соединение. Напомним, что
recv(2) является блокирующим вызовом, что нас, вообще говоря, не
устраивает. Тем не менее, мы снова можем перевести наш сокет в
неблокирующее состояние и воспользоваться select. Мы так и поступим в
дальнейшем, а этот пример просто показывает технику написания
простейшего клиента, и мы с удовольствием обнаруживаем, что это не
слишком сложно. В завершение обратим внимание на вызовы inet_addr(3) и
htons(3). Первая функция дает IP-адрес по символьному его
представлению, а вторая переводит short int в целое с порядком байтов,
принятых в сети. Часто этот порядок совпадает с порядком байтов в
машинном представлении, но может и не совпадать (имеется ввиду т. н.
LSB и FSB представления). Впрочем, это уже тонкости, о которых можно
почитать и в другом месте :). И наконец, можно на досуге взглянуть на
функцию gethostbyname(3), которая выполняет т. н. разрешение
(resolving) по имени хоста. Используя ее, мы могли бы обратиться к
нашему серверу не по IP-адресу, а по его имени ("localhost"). Глава 3 Заключение.
Итак, в данной статье мы выяснили, как обращаться с сокетами. Те, кто
уже имел с ними дело (и набрался терпения дочитать до этого места),
наверное обратили внимание на то, что мы использовали "классическую"
Берклиевскую реализацию. Она хороша тем, что в большинстве случаев
переносима между платформами, однако нам бы не хотелось подталкивать
разработчиков к использованию именно такого подхода, тем более, что,
как мы в дальнейшем увидим, для работы с SMS-протоколами это совсем
необязательно, ибо они абстрагированы от деталей установки соединения.
Например, те, кто программирует под Windows, могут воспользоваться
функциями из семейства WSA* (если, конечно, не уснут, читая их перечень
:), а программисты, привыкшие работать с MFC, возможно найдут полезным
класс CSocket (правда, если Вы собираетесь использовать его в
мультипоточном приложении с CWinThread, не забудьте включить
заклинание: -------------------------------------------------------------------------------- #ifndef _AFXDLL #define _AFX_SOCK_THREAD_STATE AFX_MODULE_THREAD_STATE #define _afxSockThreadState AfxGetModuleThreadState() _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState; if (pState->m_pmapSocketHandle == NULL) pState->m_pmapSocketHandle = new CMapPtrToPtr; if (pState->m_pmapDeadSockets == NULL) pState->m_pmapDeadSockets = new CMapPtrToPtr; if (pState->m_plistSocketNotifications == NULL) pState->m_plistSocketNotifications = new CPtrList; #endif --------------------------------------------------------------------------------
в код thread'а до самой первой сокетной операции; возможно, это
сэкономит Вам выходные ;). И, в конце концов, Вы можете воспользоваться
компонентами (Привет, Михаил! ;), которых достаточно много и которые
достаточно "бросить" на форму, особенно это касается поклонников
продуктов от Borland.
|