26 февраля 2011 г.

Сокеты. Пример создания простого сетевого приложения (клиента и сервера) с использованием TCP.

Почти все приложения которые взаимодействуют с сетью используют библиотеку, поддерживаемую операционной системой, - сокеты (sockets). Ее реализации бывают разными, но в целом принцип работы везде одинаков.
Далее в качестве примера рассмотрим сервер получения времени и клиент к нему. Скорее всего, вам уже известно, что такое клиент и сервер, но вкратце объясню. Грубо говоря,  клиент - сетевое приложение которое обращается к серверу с каким то запросом. А сервер - сетевое приложение, которое обрабатывает запросы от клиентов. Зачастую, сервер - приложение которое запущено как демон, то есть после загрузки в память операционной системы, не завершается, а ждет какого-то события и потом его обрабатывает. Так же следует знать, что обычно один клиент обращается всего лишь к одному серверу, но сервер, может одновременно обрабатывать запросы нескольких клиентов, хотя в этой схеме бывают и исключения (например, adc-клиент).

Сервер времени и даты использует протокол TCP. Вот код такого простого сервера:
#include <sys/socket.h> // собственно, сокеты
#include <string.h> // отсюда мы берем memset()
#include <resolv.h> // тут объявлены структуры sockaddr
#include <stdio.h> // snprintf()
         
#define BUFSIZE 128 // максимальный размер буфера под выводимую строку
#define LISTENQ 256 // максимальный размер очереди клиентов
         
#define bzero(x,y) memset((x),0,(y)) // макрос позволяющий пользоваться bzero вместо memset
         
int main()
{        
    int listenfd, connfd; // дескрипторы сокетов
    struct sockaddr_in servaddr; // структура, которой задается адрес сокета
    char buff[BUFSIZE]; // массив чаров. Туда будет помещаться строка, которую сервер отдаст клиенту
    time_t ticks; // сюда будет помещено время в секундах
         
    listenfd = socket(AF_INET, SOCK_STREAM, 0); // просим операционную систему создать для нас сокет протокола IPv4 (AF_INET) / TCP (SOCK_STREAM). Эта функция вернет дескриптор сокета.
    bzero(&servaddr, sizeof(servaddr)); // зануляем память структуры, это необходимо. Использовать bzero вместо memset сложилось исторически, еще с появления сокетов на BSD. Хотя разницы нет.
    servaddr.sin_family = AF_INET; // структура для IPv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // задаем адрес
    servaddr.sin_port = htons(1313); // задаем порт. Стандартно для сервера времени - 13, но его от обычного пользователя мы не сможем использовать.
         
    bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); // "привязываем" сокет к адресу и порту
         
    listen(listenfd, LISTENQ); // говорим операционной системе ожидать подключений к нашему сокету
         
    for(;;) { 
        connfd = accept(listenfd, (struct sockaddr*) NULL, NULL); // когда клиент подключается к серверу, то операционная система создаст сокет. Собственно этой функцией и задаем подключение клиентов, она возвращает дескриптор на новый сокет клиента
        ticks = time(NULL); // получаем текущее время в секундах
        snprintf(buff, sizeof(buff), "%.24s\er\en\n", ctime(&ticks)); // переводим секунды в удобный для восприятия формат
        send(connfd, buff, strlen(buff), 0); // отправляем данные (на самом деле ничего не отправляется тут)
        close(connfd); // закрываем соединение
    }    
    return 0;
}
Предупреждаю, что возвращаемые значения функций там не обрабатываются, а они возвращают 0 или номер ошибки. Поэтому, если у вас эта программа не будет работать, то скорее всего в работе какой-то функции возникает ошибка. Хорошим тоном, будет создать для всех функций обвертки и проверять в них возвращаемые значения, если функция вернет не ноль, то выводить ошибку и завершать программу.
Еще хочу сказать про функцию send, казалось бы, по названию можно подумать что она что-то там куда-то отправляет, но это не так. На самом деле она всего лишь помещает данные в буфер TCP (этот буфер уникальный для каждого порта) операционной системы, а уже она будет что-то там пытаться отправить. То есть, ничто не может гарантировать успешную отправку данных.

Ну что, компилируем (обратите внимание, никакие динамические библиотеки подключать не надо), запускаем, программа "висит". Хорошо. Пробуем подключиться с помощью telnet:
$ telnet 127.0.0.1 1313
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Sat Feb 26 15:46:12 2011
Connection closed by foreign host.
Все работает!


Далее напишем клиент:
#include <sys/socket.h>
#include <string.h>
#include <resolv.h>
#include <stdio.h>
     
#define BUFSIZE 128
     
#define bzero(x,y) memset((x),0,(y))
     
int main()
{    
    int connfd;
    struct sockaddr_in servaddr;
    char buff[BUFSIZE];
    char servIP[] = "127.0.0.1"; // задаем IP адрес сервера
     
    connfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, servIP, &servaddr.sin_addr); // заполняем поле адреса (32 бита) с помощью функции inet_pton
    servaddr.sin_port = htons(1313);
     
    connect(connfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); // подключаемся к серверу
    recv(connfd, buff, BUFSIZE, 0); // получаем данные от сервера (вернее получаем данные из буфера TCP)
     
    printf("%s", buff);
     
    close(connfd);
    return 0;
}
Здесь, так же, обработки ошибок нет. Я не стал их делать, чтобы не загромождать код, ведь, это всего лишь примеры.

Кстати, IP адрес и порт которые помещаются в struct sockaddr_in должны иметь сетевой порядок следования байтов. Для этого пользуются специальными функциями преобразования:
htons (host TO net short) - для преобразования порта.
htonl (host TO net long) - для преобразования адресов.
Функция inet_pton преобразует массив символов (строку) в адрес (32 бита для IPv4) с сетевым порядком.
В функциях connect, bind и accept делается преобразование типа к struct sockaddr для их совместимости с различными протоколами IP. Подробно об этом не буду говорить, скажу лишь, то, что это преобразование нужно делать.
Возможно, вы заметили что в коде клиента нет функции bind. Но ведь клиенту тоже нужен порт, с которого он будет отправлять данные? Дело в том, что bind можно и не делать, если так, то операционная система назначит порт для клиента автоматически.
Кстати, полное описание, прототипы, возвращаемые и значения всех функций можно найти в man.



Это были всего лишь основы. Если вам интересно сетевое программирование, то очень советую  почитать книгу UNIX Network Programming (рус. UNIX. Разработка сетевых приложений), автор W. R. Stevens. В ней все очень подробно расписывается, как про сокеты так и про устройство основных протоколов интернета.

16 февраля 2011 г.

[bash] Выбор случайного файла из директории.

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

На для начала разберемся как будем это делать. Итак, на входе подается список директорий, в которых лежат нужные нам файлы. Собираем файлы и пихаем в массив. Перемешиваем все файлы в массиве (тогда при последовательном обходе они станут случайными). И все. Очень просто. На самом деле мой скрипт немного сложнее, но весь алгоритм такой.

Вот код:
#!/bin/bash

onSIGINT() { # создаем функцию которая будет завершать вечный цикл
    echo "SIGINT received, bye bye!"
    exit 0
}
trap 'onSIGINT' INT # трап, вызывающий эту функцию на SIGINT

regexType="posix-egrep" # переменная указывающая на то, какие регулярные выражения будет использовать утилита find
regex=".*\.(mp3|flac)$" # сам шаблон регулярного выражения
depth="u" # глубина поиска. u [unlimited] - не ограничена, может быть числом

if [[ $# -eq 0 ]]; then # проверка. Если не получили аргументов, то выводим хелп и выходим
    echo "Usage: $0 [-d DEPTH] DIR1 [DIR2 [DIR3 [...]]]"
    exit 0
fi

index=0
for i in `seq 1 $#`; do # цикл обработки аргументов командной строки
    arg=${!i}
    if [[ $bNextDepth -eq 1 ]]; then 
        bNextDepth=0
        if [[ $arg =~ ^[0-9]+$ ]]; then # по сути проверка - число ли depth или нет
            depth=$arg
            continue
        else
            echo "Missing value for -d."  # ошибка. Юзер написал -d но не написал значение для него
            exit 1
        fi
    fi
    if [ "$arg" = "-d" ]; then
        bNextDepth=1
        if [[ $# -eq $i ]]; then
            echo "Missing value for -d."
            exit 1
        fi
        continue
    fi
    if [ ! -d "$arg" ]; then # проверяем, действительно ли нам подали директорию?
        echo "Directory $arg does not exists."
    else
        dirList[$index]=$arg # сохраняем ее в массив
        let "index++"
    fi
done # конец обработки аргументов командной строки

if [[ ${#dirList[@]} -eq 0 ]]; then # если юзер глупый и не ввел не одну правильную директорию выходим, сообщая ошибку
    echo "No one correct directory has been found."
    exit 2
fi

index=0
if [ "$depth" != "u" ]; then # если у нас в depth число
    depthArg="-maxdepth $depth"
fi

while read line
do
    filesArray[$index]="$line"
    let "index++"
done < <(find "${dirList[@]}" $depthArg -regextype "$regexType" -iregex "$regex" -type f) # когда я впервые увидел эту строку в одном примере, желание писать скрипты на баше у меня почти отпало. Спасибо людям на канале #gentoo-ru которые мне объяснили ее смысл. А про find я расскажу ниже.

filesNum=${#filesArray[@]}
echo "Found $filesNum files."

let "filesNum--" # уменьшаем на 1. Потому что for в баше (в отличии от Си) выберет последний элемент тоже.
while [ true ]; do
    for i in `seq 0 "$filesNum"`; do # цикл который перемешает массив
        let "rndPos = RANDOM % $filesNum"
        temp="${filesArray[$i]}" # в этих 3 строках меняем местами элементы
        filesArray[$i]="${filesArray[$rndPos]}"
        filesArray[$rndPos]="$temp"
    done
    for i in `seq 0 "$filesNum"`; do # собственно, цикл запускающий нашу программу с нужным аргументом
        /usr/bin/mplayer "${filesArray[$i]}"
    done
done

exit 0 # все

find "${dirList[@]}" $depthArg -regextype "$regexType" -iregex "$regex" -type f
  • Здесь ${dirList[@]} - массив корректных директорий который мы составили ранее. 
  • $depthArg (без кавычек) - два слова, первое - аргумент (-maxdepth), второе - его значение. 
  • $regexType - тип регулярного выражения. 
  • $regex - шаблон регулярного выражения по которому мы выбираем файлы из директории. 
  • -type f - указывает find'у то, что нас интересуют только реальные файлы. 
Как вы уже могли заметить, этот скрипт подбирает рандомные файлы mp3 или flac и запускает mplayer от них. Реализовано все так, что, пока все файлы не пройдут, они повторятся не будут (по сути, мы просто воспроизводим все по порядку из массива filesArray, но перед началом вложенного цикла перемешиваем массив случайным образом).
-maxdepth N это параметр указывающий find'у, сколько раз надо проходить во вложенные директории (1 - ни разу не переходить).
Кстати, вместо блока кода     
for i in `seq 0 "$filesNum"`; do
        let "rndPos = RANDOM % $filesNum"
        temp="${filesArray[$i]}" 
        filesArray[$i]="${filesArray[$rndPos]}"
        filesArray[$rndPos]="$temp"
done
можно использовать утилиту shuf. То есть весь этот блок кода заменить на строку:
shuf -e ${filesArray[@]}
Проблема в том, что shuf не входит в POSIX.
Еще можно попробовать делать это через sort -R.

Вот и все.

12 февраля 2011 г.

Yeahconsole.

Вы можете работать в unix-like операционной системе без консоли? Например, я не люблю работать с файловой системой, некоторыми сетевыми протоколами, и так далее через оболчки с GUI. Возможности консоли почти безграничны, есть даже консольный графический редактор ImageMagick, о котором, возможно, я когда-нибудь расскажу. Все это я говорю к тому, что зачастую не очень удобно переключаться между каким-либо активным окном (например, браузером) и часто используемой консолью.
Поэтому представляю вашему вниманию yeahconsole. Что это? Собственно, довольно удобная выезжающая консоль. Что-то подобное вы скорее всего видели в играх, например, в Quake.
Выглядит это так:











Достоинства:
  • Удобное использование по хоткею
  • Самая легковесная и, следовательно быстрая, по сравнению с аналогами (если не считать скрипты с wmctr). На мой взгляд, это самое большое преимущество.
  • Сама по сути не является эмулятором терминала, а использует существующие (xterm, и мой любимый urxvt)
  • Вылезает поверх всех окон. Сразу становится зафокусированной window менеджером.
  • Настройка очень проста.
Теперь про настройку. Она осуществляется в файле ~/.Xdefaults. 
Конфиги записываются построчно так: yeahconsole*параметр: значение
Как всегда, весь набор можно посмотреть в man yeahconsole.
Некоторые параметры:
! клавиша включения yeahconsole*toggleKey: None+F1    ! включить полноэкранный режим у активной 
yeahconsole*keyFull: Alt+F1     ! клавиша увеличения высоты
yeahconsole*keyBigger: Control+KP_Add     ! уменьшения высоты
yeahconsole*keySmaller: Control+KP_Subtract     ! задержка появления
yeahconsole*aniDelay: 30   ! перезагружать терминал
yeahconsole*restart: 0     ! отступ от левого края
yeahconsole*xOffset: 0     ! ширина. По дефолту - ширина экрана.
yeahconsole*screenWidth: 650   ! высота в строках
yeahconsole*consoleHeight: 15     ! ширина полоски изменения размера
yeahconsole*handleWidth: 1     ! ее цвет
yeahconsole*handleColor: rgb:8/8/7     ! ее цвет без фокуса
yeahconsole*handleColor.unfocus: rgb:4/4/38     ! цвет фона
yeahconsole*background: black     ! цвет текста
yeahconsole*foreground: grey70     ! шрифт
yeahconsole*font: -*-terminus-medium-*-*-*-*-*-100-100-*-*-iso10646-1     ! количество строк которые запоминает
yeahconsole*savelines: 10000     ! используемый эмулятор терминала
yeahconsole*term: urxvt    
Вот и все. Удачи в настройке.


Настройка VPN в Linux с использованием PPTP.

Всем известно, что некоторые провайдеры раздают интернет через VPN. Именно VPN, а не просто PPTP мотивируется, наверное, наличием шифрования, при такой же легкой настройке. Но настройка легка только в Windows.

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

Решение все-таки было найдено. Как оказалось со стороны конфигов ppp все было правильно, не правильными были роуты (route, маршруты). Дело в том, что ppp требует ставить себе default route (однако, на debian'е у ppp есть конфиг replacedefaultroute. Как раз 70% того что выдает гугл, содержит эту опцию, следовательно конфиг работает только под Debian-Ubuntu-... ). Решение было очевидно - прописывать все необходимые роуты при старте системы.



Теперь расскажу всю настройку VPN в целом. Нам потребуются пакеты:
  • ppp
  • pptp
А так же необходима поддержка ppp в ядре. Если вы не собирали ядро сами, то скорее всего ppp в ядре у вас есть.
В файл /etc/ppp/options.pptp пишем (под рутом):

noauth    # не авторизовываться пока не будет получен gateway
refuse-eap           # Не использовать проверки
refuse-pap           ## Возможно, что на стороне провайдера стоит
refuse-chap         ## обязательным проверка по некоторым из этих
refuse-mschap    ## алгоритмов
nobsdcomp    # в этой и следующей строке отключаем компрессию
nodeflate
persist    # восстановление связи при обрыве
maxfail 10    # десять попыток восстановления
defaultroute   # подменяем дефолтный роут (о проблемах с этим я писал выше)
#replacedefaultroute    # раскоментить если у вас Debian-like дистрибутив (например, Ubuntu)

Далее нам потребуется еще один файл, в этом файле описываются все особенности касательно провайдера. Создаем /etc/ppp/peers/название_соединения (тоже под рутом):

pty "pptp Адрес_VPN_провайдера --nolaunchpppd"    # собственно адрес VPN-сервера
connect /bin/true    
name Ваш_Юзернейм    
password "Ваш_Пароль"
remotename название_соединения   # имя данного файла
file /etc/ppp/options.pptp   # указываем путь до конфига который создали выше

Далее, если у нас не Debian-like дистрибутив (у нас закоменчена опция replacedefaultroute) следует прописать роуты локальной сети (об этом я говорил выше), обычно их можно найти на сайте провайдера.

Напомню что список текущих роутов получается командой route -n. Прописываются и удаляются роуты через команду route. Более подробно написано в man route (который я очень рекомендую почитать). Так же следует помнить, что прописав роуты они скинутся после перезагрузки системы. Так что, лучше создать скрипт который будет добавлять роуты и добавить его в автозагрузку (у разных дистрибутивов автозагрузка в разных местах)

Собственно, настройка закончена. VPN включается по команде pon название_соединения, а выключается по команде poff название_соединения.



Приведу альтернативный пример настройки всего этого под Gentoo, соответственно с использованием конфигов Gentoo. Файл /etc/conf.d/net:

config_eth0=( "айпи_локальной_сети netmask маска_локальной_сети" )
gateways_eth0="адрес_шлюза"     # gateway
routes_eth0=( 
        # прописываем роуты локальной сети. Пример ниже:
        "-net айпи_адрес_роута netmask маска_сети_роута gw адрес_шлюза"
 )
dns_servers_eth0="днс_сервер"
config_ppp0=( "ppp" )
link_ppp0="pty 'адрес_VPN_провайдера --nolaunchpppd --nobuffer'"
username_ppp0='юзернейм'
password_ppp0='пароль'
pppd_ppp0=(
        noauth    # узнаете эти строки? :3
        refuse-eap
        refuse-pap
        refuse-chap
        refuse-mschap
        debug
        nobsdcomp
        nodeflate
        mtu 1474
        mru 1474
        persist
        logfile /var/log/pppd.log   # можно так же добавить файл для логов pppd
        maxfail 10
        defaultroute
)
depend_ppp0() {
        need net.eth0
}
Скрипт будет запускаться при старте системы. Или это можно делать руками, как с любым скриптом в Gentoo:   /etc/init.d/net.ppp0 start (или restart, stop).
Еще я забыл сказать, что нужно создать файл /etc/init.d/net.ppp0. Это делается простым копированием net.lo:    cp /etc/init.d/net.lo /etc/init.d/net.ppp0 (и все из под рута).

11 февраля 2011 г.

Проба пера.

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

Как можно догадаться по названию, основной тематикой моего блога будет изучение unix (в частности *nix, т.к., например, linux - Linux Is Not UniX, что уже веет каким-то маразмом), а так же программирование.

Коротко расскажу о себе: учусь на втором курсе (на момент 2010-2011 года) НГУ на факультете информационных технологий. Собственно, на данный момент у нас преподаются предметы: операционные системы, сети и ООП. Возможно, об ихних лабораторных работах и пойдет речь в моем блоге, так или иначе, я бы хотел затронуть эту тему.

Собственно сейчас сижу и думаю о чем бы написать первым делом.