NETWORK. Асинхронные сокеты Tcl

Как я и обещал в прошлой статье про Tcl сокеты сегодня будем разбираться с тем, что такое асинхронные сокеты и как с ними работают. Но для начала, небольшая, но хорошая новость.

Этим летом вышла книга про сетевое программирование на Tcl, что не может остаться без внимания сообщества программистов. На русском книги этой нет и вряд ли будет :_(

Сетевое программирование на Tcl

Подумываю теперь, где бы сэкономить $50 :)

Далее по теме.

Чем отличаются асинхронные сокеты?

Иногда, соединение с сервером является достаточно длительным процессом. В этом случае команда открытия сокета в обычном синхронном режиме остановит исполнение всего скрипта до того момента, пока не будет выполнено подключение к серверу.

Использование асинхронного сокета позволяет скрипту исполняться, пока происходит подключение к серверу. Что бы открыть сокет в асинхронном режиме нужно использовать опцию ‘-async’.

set channel [socket -async $address $port]

Блокирующий режим

Опция -async относится только к подключению, но не к обмену данными с каналом. Сразу после создания канала он переводится в блокирующий режим, что эквивалентно синхронному обмену данными с каналом.

Это значит, что скрипт будет приостанавливаться, если:

- вызвана команда gets или read, но в канале нет данных
- вызвана команда flush, но еще нет соединения
- вызвана команда puts, буфер полон, но еще нет соединения

В случае gets и read скрипт будет приостанавливаться до появления данных в канале, или до разрыва соединения (тогда вернут ошибку).

В следующем примере соединение выполнено асинхронно, но из за того, что канал находится в блокирующем режиме, вызов gets, вызванный сразу после socket, приостановит исполнение скрипта.

set channel [socket -async yandex.ru 99]
set data [gets $channel]
... тут долго ждем ...

Правда, этот пример в конце ожидания еще выдаст ошибку :) т.к. я специально выбрал несуществующий сервер, что бы подольше подключалось, дабы имитировать сеть с низкой пропускной способностью.

Неблокирующий режим

В неблокирующем режиме вместо задержки выдается ошибка, которую нужно отловить и соответствующим образом обработать. Ошибки возникают в следующих конкретных случаях:

- Вызывается gets или read, когда в канале нет данных
- Вызывается flush, когда нет подключения
- Вызывается puts, когда нет подключения и буфер заполнен

Что бы перевести канал в неблокирующий режим, сразу после подключения нужно использовать команду fconfigure:

set channel [socket -async $address $port]
fconfig $channel -blocking false

В случае если из канала выбраны все данные, то команда fblocked будет возвращать 1.

В случае неудачного подключения

Если подключение завершилось неудачно, то команды чтения из канала и flush будут сразу возвращать ошибку, а команды записи в канал только по его заполнению.

ВАЖНО! Если еще нет соединения и буфер заполнен, команда после которой буфер был заполнен закончится ошибкой, а следующие команды записи будут перезаписывать буфер с начала! По этому важно отслеживать эти ошибки, что бы не допустить потери информации.

Пример использования fblocked

Сервер отсылает порцию данных и не закрывает канал. Такое возможно, когда сервер ожидает ответа от клиента.

proc connectionHandler {channel host port} {
	puts "Новое соединение с $host"
	puts $channel {
         (__)
         (oo)
  /-------\/
 / |     ||
*  ||----||
   ~~    ~~
	}
	flush $channel
}
 
socket -server connectionHandler 9909
 
puts "Hi coder :)"
puts "server address: localhost 9909"
 
vwait forever

Клиент работает в неблокирующем режиме. Так как канал не был закрыт, клиент может узнать о конце передачи данных только с помощью команды fblocked. Хотя, в общем случае, это означает только то, что данные во входном буфере были исчерпаны. Например, это может случиться при задержке очередного пакета.

(интерактивный режим tclsh)

% socket localhost 9909
sock224
% fconfig sock224 -blocking false
% while {[fblocked sock224] == 0} {
puts [gets sock224]
}
 
         (__)
         (oo)
  /-------\/
 / |     ||
*  ||----||
   ~~    ~~
 
 
% _

Конец

Следующий раз опишу полноценную реализацию небольшого протокола, которая будет полностью иллюстрировать описанные выше тонкости работы с сокетами.

Tagged with:
 

как стать программистом

Когда-то давным давно нам с младшим братом пришлось убегать от здоровенного быка. На нашем пути лежала речка, которая казалась единственным спасением. И только переплыв ее мы вспомнили, что не умеем плавать.

Можно долго хотеть плавать. Можно долго учиться плавать. Можно долго пробовать плавать на мели. Можно даже научиться плавать таким образом!!! Но пока за тобой не гонится разъяренный бык, ты не узнаешь, что на самом деле ты всегда умел плавать.

Ты можешь долго хотеть стать программистом, потом долго учиться программировать, но единственное, что сделает тебя программистом – написание кода.

Tagged with:
 

Не следует использовать в качестве имени перемнной имя, уже использованное в более глобальном по отношению к текущему пространству имен. При таком переопределении имя глобальной переменной будет перекрыто локальной и доступа к глобальной получить будет нельзя.

Пример с ошибкой

В следующем примере внутри функции повторно использовано имя глобальной переменной. Это одновременно приводит к невозможности доступа к локальной переменной и переполнению буфера в локальной переменной, т.к. для указания размера этого буфера используется глобальная переменная msgsize, которая указывает больший размер.

static char msg[100];
static const size_t msgsize = sizeof( msg);
 
void report_error(const char *str) {
  char msg[80];
  snprintf(msg, msgsize, "Error: %s\n", str);
  /* ... */
}
 
int main() {
  /* ... */
  report_error("some error");
}

Правильное решение

Правильным решением будет использовать различные имена локальных и глобальных переменных. Кроме того, для глобальных желательно выбирать более понятные.

static char message[100];
static const size_t message_size = sizeof( message);
 
void report_error(const char *str) {
  char msg[80];
  snprintf(msg, sizeof( msg), "Error: %s\n", str);
  /* ... */
}
 
int main() {
  /* ... */
  report_error("some error");
}

Исключение

Аргументы функции при ее объявлении (но не определении) могут вступать в конфликт с именами глобальных переменных:

extern int name;
void f(char *name);   // объявление: тут нет проблем
// ...
void f(char *arg) {   // определение: имя должно отличаться!
  // use arg
}

Оценка риска

Строгость – низкая = 1
Вероятность – маловероятно = 1
Цена соблюдения – средняя = 2

Итого:
Приоритет: P2
Уровень: L3

Автоматическое обнаружение

Обнаруживать нарушение этой рекомендации умеют следующие тулзы:

LDRA tool suite V. 7.6.0
Splint V. 3.1.1
Compass/ROSE
Klocwork V. 9.1 (IF_MULTI_DECL, IF_MULTI_DEF, IF_MULTI_KIND)

02. Declarations and Initialization / DCL01-C

Tagged with:
 

Рекомендация элементарная, но справедливая: все объекты, которые должны быть неизменяемыми следует объявлять как const. Кроме собственно защиты переменных это дает возможность отслеживать участки кода, которые по ошибке хотят изменить содержимое переменной. В следующих статьях мы еще обратимся к более частным случаям использования const.

Однако, константные значения в Си можно задавать еще двумя способами: макроподстановками и перечислениями enum. Оба они имеют свои недостатки. Так макросы видны только в пределах компилируемого модуля, при отладке выглядят как константы, разбросанные по исходнику, а так же нет возможности явно указать их тип.

В случае перечислений – тип только int, зато не выделяется память. Отладчики представляют их как переменные, что тоже удобно.

Собственно и все. Не буду оскорблять достоинство читателя примерами определения константных переменных :) Хотя, если они вам необходимы, можете заглянуть на страницу стандарта, там они есть. Ссылка ниже.

Оценка риска

Строгость – низкая = 1
Вероятность – маловероятно = 1
Цена соблюдения – высокая = 1

Итого:
Приоритет: P1
Уровень: L3

Автоматическое обнаружение

Compass/ROSE умеет это

02. Declarations and Initialization / DCL00-C

Tagged with:
 

Оказывается очень просто. Правда метод работает только для Windows. Зато от 95-той до последних. Пример на тикле:

exec sndrec32 /play /close /embedding file.wav &

Программа sndrec32 – это стандартная программа звукозаписи, которая есть в любой венде.

Более полезный пример проговаривает слова из коллекции вафок StarDict:

variable t
 
proc say {word} {
	exec -ignorestderr sndrec32 /play /close /embedding ./sw/[string index $word 0]/[set word].wav &
}
 
pack [entry .e -textvar t]
pack [button .b -text Сказать -command {say $t}]

Ключь /embedding скрывает окно программы, остальные в комментариях не нуждаются.

Tagged with:
 

Приветствую всех, кто заглядывает в мой блог!

Рад сообщить, что эта статья последняя из серии про препроцессор языка Си. Был переведен практически весь этот раздел стандарта безопасного программирования, за исключением двух статей, одна из которых пока в разработке, а вторая чуть менее чем полностью повторяет уже переведенную.

Теперь найти бы время, для творческой переработки всего этого материала, что бы сделать его более удобочитаемым…

В следующих статьях речь пойдет об ошибках объявлений и инициализации.


***


Аргументы макросов не должны включать директив #define, #ifdef и #include. Это приведет к неопределенному поведению, в соответствии со стандартом C99 (ISO/IEC 9899:1999, секция 6.10.3, параграф 11):

Последовательность препроцессорных токенов, ограниченных самыми внешними скобками, формирует список аргументов, для функционального макроса. Отдельные аргументы в списке параметров разделяются запятой, но запятая во внутренних парных скобках не разделяет аргументов. Если есть последовательности токенов в списке аргументов, которые могли бы выступать в качестве директив препроцессора, поведение не определено.

Тут особо стоит отметить, что многие функции стандартной библиотеки (и прочих библиотеки сторонних разработчиков) на самом деле являются макросами. Например, memcpy(), printf(), и assert() могут быть макросами. По этому данное правило следует распространить и на использование препроцессорных директив в списках параметров функций.

Пример с ошибкой

В следующем примере автор использовал директивы препроцессора для определения платформенно-зависимого параметра в вызове memcpy(). Однако, если memcpy() является макросом, то этот участок кода приведет к неопределенному поведению.

memcpy(dest, src,
#ifdef PLATFORM1
  12
#else
  24
#endif
);

Правильное решение

#ifdef PLATFORM1
  memcpy(dest, src, 12);
#else
  memcpy(dest, src, 24);
#endif

Оценка риска

Строгость – низкая = 1
Вероятность – маловероятно = 1
Цена соблюдения – средняя = 2

Итого:
Приоритет: P2
Уровень: L3

01. Preprocessor / PRE32-C

Tagged with:
 

Контрол с Drag&Drop в WPF

Наконец-то свершилось чудо! Доделал таки хитрый контрол, для своей программы :) Программа правда сверхсекретная, по тому полностью ее окно не показываю. И исходник тоже. Такой вот я жмот… Но это все временно.

Если будет время, то обязательно напишу статью о Drag&Drop в WPF. Пока лишь скажу, что засада там полная. По крайней мере в .NET Framework 3.5. Да и материала по этой теме – кот наплакал.

Так что, если кому-то нужно срочно разработать, что-то с Drag&Drop в WPF, то обращайтесь :)

За что мне нравится WPF, так это за безграничные возможности настройки и расширения элементов GUI.

Например, элементом списка, в данном случае, может быть абсолютно любой пользовательский элемент управления. К контролам самих списков легко добавить необходимые свойства, кнопки… И мой фирменный дизайн может быть легко переработан :)

Tagged with:
 

С99 поддерживает универсальные имена символов, которые могут использоваться в идентификаторах, символьных константах и строковых литералах для обозначения символов не входящих в базовый набор. Универсальное имя символа начинается с \U или \u и последующих восьми, или четырех шестнадцатеричных цифр соответственно. Более подробно о стандарте UTF можно почитать в википедии, или в стандарте ISO/IEC 10646.

В стандарте C99, секция 5.1.1.2, параграф 4, говорится:

Если последовательность символов, синтаксически соответствующая универсальному имени символа образуется в результате конкатенации (6.10.3.3), поведение не определено.

Вообще, следует избегать применения универсальных имен символов в идентификаторах без крайней на то необходимости. Базового набора символов должно хватать практически для любого идентификатора.

Пример с ошибкой

#define assign(uc1, uc2, val) uc1##uc2 = val;
 
int \u0401;
assign( \u04, 01, 4);

Правильное решение

#define assign(ucn, val) ucn = val;
 
int \u0401;
assign( \u0401, 4);

Оценка риска

Строгость – низкая = 1
Вероятность – маловероятно = 1
Цена соблюдения – средняя = 2

Итого:
Приоритет: P2
Уровень: L3

01. Preprocessor / PRE30-C

Tagged with:
 

Вообще-то странно конечно, что данная статья стандарта перекрывается с вот этой – “CERT. static и inline функции вместо макросов” чуть менее, чем полностью. Конечно, раз она есть совсем ее пропускать не хорошо, но сократить, думаю, не грех.


***

Раньше в этой серии статей уже говорилось, что в отличие от функций, макросы могут рассчитывать переданное в качестве параметра выражение по нескольку раз. И говорилось о приемуществе inline функций, перед функциональными макросами, сдесь же дается еще одно решение данной проблемы.

Пример с ошибкой (множественное вычисление аргумента)

1
2
3
4
5
6
7
#define ABS(x) (((x) < 0) ? -(x) : (x))
 
void f(int n) {
  int m;
  m = ABS(++n); // получится явно не то, что задумывалось
  /* ... */
}

После подстановки макроса исходный текст будет содержать:

5
m = (((++n) < 0) ? -(++n) : (++n));

Правильное решение

Решение с использованием inline функций описано в этой статье. Некоторые компиляторы поддерживают расширения, которые позволяют определять безопасные функциональные макросы. Например, в GCC безопасный макрос можно определить так:

#define ABS(x) ({int tmp = (x); tmp < 0 ? -tmp : tmp; })

Правда, при использовании таких расширений теряется портируемость кода и скомпилировать его компилятором не поддерживающим это расширение невозможно.

Оценка риска

Строгость – низкая = 1
Вероятность – вероятно = 2
Цена соблюдения – низкая = 3

Итого:
Приоритет: P6
Уровень: L2

01. Preprocessor / PRE12-C

Tagged with:
 

NETWORK. Tcl команда socket

Привет всем, кто читает этот блог! Это не первая статья моего блога из серии NETWORK, однако сейчас встала острая необходимость перетрясти все свои знания по сетевому программированию и я решил совместить приятное с полезным – написать несколько статеек на эту тему и все вспомнить.

Начать я решил с простого – TCP сокетов в Tcl. Хотя, примеры исходников выглядят очень простыми – это реально работающие кусочки кода, которые при минимальных трудозатратах можно адаптировать под собственные нужды. Например, в разрабатываемом в данный момент мною проекте примерно такой же код используется, для одного очень хитрого и приятного глазу костыля с говорящим названием – RemoteExec. Может и про него как-нибудь поговорим.


***


Сетевое программирование в tcl задача не легкая… а очень легкая :) Как язык системной интеграции, он отлично заточен под задачи межпроцессного и сетевого взаимодействия. Для него имеется множество библиотек, реализующих различные сетевые протоколы, а базовые возможности работы с протоколом TCP включены в сам язык и реализуются командой socket, на которой мы и заострим внимание.

Команда socket открывает сетевое соединение по протоколу TCP, и создает канал, с которым можно работать так же как с каналом любого обычного файла. Проще говоря, после открытия сетевого соединения в Tcl, с ним можно работать точно так же как с обычным файлом, используя команды puts, gets, flush…

Серверная сторона

В Tcl работа с сетью пользуется общим механизмом обработки событий. По этому, вам не придется как-то вручную отслеживать сетевые события. Для того, что бы отследить запросы на соединения и поступление новых данных достаточно “повесить” на эти события свои процедуры и они будут вызваны как callback, при поступлении этих событий.

Для начала рассмотрим работу простейшего сервера, который при подключении к нему клиента просто передает ему текущее время и тут же закрывает соединение:

 proc Server {channel clientaddr clientport} {
     puts "Зарегистрировано соединение с ${clientaddr}:$clientport"
     puts $channel [clock format [clock seconds]]
     close $channel
 }
 
 socket -server Server 9900
 vwait forever

Начнем с конца :) Команда vwait forever – запускает цикл обработки событий Tcl. Тут все просто, ну и это тема для другой статьи.

Команда socket – открывает сокет в режиме сервера, сама налаживает все и вешает на прослушку 9900-го порта процедуру Server, с помощью опции -server. Именно по этой опции команда socket и узнает, что сокет открывается в режиме сервера (другими словами, в режиме ожидающем подключения от клиентов).

Определенная в начале исходника процедура Server вызывается в случае приема соединения от клиента. Она получает три параметра:

channel – имя канала, ассоциированного с сокетом
clientaddr – адрес клиента
clientport – порт клиента

Наша процедура отписывается о том, что произошло соединение, затем отправляет в канал текущее время и закрывает канал командой close.

Хотя, она может и не закрывать соединение, а повесить на канал событие, отслеживающее получение данных. Тогда соединенные по сети программы смогут вести диалог. Ниже мы рассмотрим такой пример, сейчас важно понять общий механизм.

Клиентская сторона

Собственно, сабж:

 set server localhost ;# Адрес сервера
 
 set channel [socket $server 9900]
 
 gets $channel line
 
 close $channel
 puts "Время на $server - $line"

В первой строчке просто заводим переменную с адресом сервера. Адрес сервера может быть задан его доменом (в виде www.mydomain.ru, или localhost), либо IP адресом. Порт, естественно, должен совпадать с тем, который прослушивает сервер :)

Команда socket в любом случае возвращает идентификатор канала сокета, но если в случае сервера он нам был не нужен, то в случае клиента без него не обойтись. Так что запоминаем его в переменную channel.

Как уже упоминалось выше, – с каналом сокета можно работать как с обычным каналом, связанным с консолью, или файлом. По этому команда gets читает полученную от сервера строку и записывает ее в переменную line.

Далее, просто закрываем сокет командой close.

Обработка события поступления данных

В Tcl есть замечательная команда fileevent, которая позволяет задавать обработчики событий каналов, в том числе: файловых, стандартного ввода-вывода (stdin, stdio, stderr) и сокетов.

Собственно, события, на которые можно повесить свои обработчики, всего два – readable и writable. Readable сообщает нам, что в канале есть данные, которые можно прочитать, а writable – что канал готов к записи в него данных.

Ниже – пример сервера, который отслеживает поступление данных от клиента и его отключение.

# Сервер
 
proc connectionHandler {channel host port} {
	puts "Новое соединение с $host"
	fileevent $channel readable [list reciveDataFrom $channel]
}
 
proc reciveDataFrom {channel} {
	if {[catch {gets $channel txt} error]} {
		CloseConnection $channel
		puts "Error $error"
	} elseif {[eof $channel]} {
		CloseConnection $channel
		puts "User disconnected"
	} else {
		puts $txt
	}
}
 
proc closeConnection {channel} {
	close $channel
}
 
socket -server connectionHandler 9900
vwait forever
# Клиент
 
set channel [socket localhost 9900]
fconfigure $channel -buffering line
 
while {[set txt [gets stdin]] ne "q"} {
    puts $channel $txt
}
 
close $channel

Сохраняем эти куски кода в файлы (скажем, server.tcl и client.tcl) и запускаем в tclsh.

tcl tcp server + client

Остановимся на наиболее важных моментах.

Команда fileevent, устанавливает процедуру reciveDataFrom в качестве обработчика события поступления данных. Команда list при этом используется в нестандартной, но очень распространенной и уже ставшей традиционной среди Tcl программистов роли сборщика команды.

Процедура reciveDataFrom читает данные из канала и проверяет не вываливается ли при этом какое-нибудь исключение. В случае исключения и достижения конца файла (что обычно соответствует отсоединению клиента) вызывается процедура closeConnection, которая закрывает канал сокета.

В исходнике клиента так же есть один примечательный момент – команда “fconfigure $channel -buffering line” устанавливает каналу построчную буферизацию. Это значит, что когда в канал помещается символ перевода строки, то все накопившиеся данные тут же пересылаются TCP серверу.

Далее просто в цикле считывается текст с консоли, если строка содержит только один символ q, то происходит завершение работы клиента и закрытие канала сокета.


***

На этом пока остановимся, хотя, у этой команды есть и еще пара тонких моментов. Но я думаю, что про них лучше написать отдельную статью.

Ссылки:
Доки. Раскуриваемся.
Простой чат клиент и пара серверов:
http://wiki.tcl.tk/10968
http://wiki.tcl.tk/11005
http://wiki.tcl.tk/4448

Tagged with: