С.Кадаков
Глава 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: %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 <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] = '\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<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] = '\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 <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-приложение и добавим
функциональности нашему эмулятору. Оставайтесь с нами!
Языки программирования: разное
|