| 
div.main {margin-left: 20pt; margin-right: 20pt}       С.Кадаков 
       sgerr@hotmail.com 
      Связь с SMSC.
      Глава 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 <winsock.h>
#else
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
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 <stdio.h>
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: %sn", 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 <winsock2.h>
#define socklen_t int
#else
#include "unisock.h"
#include <fcntl.h>
#include <sys/time.h>
#endif
#define _SMSCE_H_
#endif /* _SMSCE_H_ */
      
      smsce.cpp 
      
#include <iostream>
#include <list>
#include <stdio.h>
#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] = ' ';
        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<SOCKET> 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 <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
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 <winsock2.h>
#define socklen_t int
#else
#include "unisock.h"
#include <fcntl.h>
#include <sys/time.h>
#define Sleep(x) usleep((unsigned long )(x * 1000))
#endif
#define _SMSCE_H_
#endif /* _SMSCE_H_ */
      
      sockclient.cpp 
      
#include <iostream>
#include <stdio.h>
#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] = ' ';
        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 <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
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. 
       
      Мы же на этом закончим обсуждение вопроса, еще раз 
      напомнив о предложении внимательно его изучить, а то что-то мы увлеклись 
      сокетами; пора переходить к содержательной части дела. В следующей статье 
      мы попробуем построить наше первое "настоящее" SMS-приложение и 
      добавим функциональности нашему эмулятору. Оставайтесь с нами! 
 
  
 
 |