В этом разделе представлена сама программа, а также исходный код. В первую очередь хочу отметить, что программа не является мультипоточной, работает всего лишь в один поток. В программе нет конвертации кириллических доменов в punycode, поэтому программа не работает с доменами в зоне РФ, но вариант конверации приведу в данном документе.
Логика работы бота
Рефбот, как я писал в вводной статье, в первую очередь выполняет HTTP-запрос вэбсерверу. В заголовке запроса он передает данные и реферер. Приведу пример такого запроса:
GET / HTTP/1.1 Host: target_host.com Referer: our_site.ru Connection: close User-Agent: Refbot/1.0 (+http://our_site.ru) Accept-Encoding: gzip Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Cache-Control: no-cache
Это GET запрос к серверу, Host - запрашиваемый домен, Referer указывает откуда пришел бот, User-Agent сообщает серверу тип бота. Остальное это вид сжатия, который воспринимает бот. На самом деле в данном проекте бот не использует содержимое страниц, но для экономии трафика он "запрашивает" сжатые данные.
Данные о сайтах поступают из базы данных refbot, которую мы создали и загрузили, этот процесс описан в предыдущем документе из этого раздела. После выполнения запроса бот получает HTTP-код сервера. Эти коды описаны в той же предыдущей статье. Далее бот осуществляет запись в базу данных и получает следующий адрес для обработки. Весь процесс работы бота автоматический и циклический.
Внешний вид
Сама программа это диалоговое окно, с элементами управления и настройки. В первом гроупбоксе производится настройка базы данных MySQL, кнопка тест осуществляет проверку подключения к БД. Если настройки базы отличные от стандартных, то в программе запишите правильные. В случае успешной проверки рефбот сообщит, что тест пройден. После чего отобразит текущие статистические данные.
Во втором гроупбоксе указывается наименование бота, официально - Refbot/1.0 и "продвигаемый" сайт, кнопка старт (стоп).
В 3-ем боксе приводятся статистические данные. Здесь указано количество сайтов, сколько выполнено и сколько осталось. В последнем боксе ведется небольшой пятистрочный лог. Сам лог не записывается в БД или в файл, в основном будете использовать для мониторинга работы.
Запуск программы осуществляется с отдельном потоке, он не блокирует форму программы. Осуществляется блокирование первого гроупбокса. Остановка осуществляется той же кнопкой старт (после запуска она именуется как "стоп"), смотрите скриншот программы.
Файлы проекта
Это стандартный набор файлов проекта Refbot. Используются стандартные библиотеки, которые устанавливаются вместе Лазарусом. Для сохранения настроек программы используется файл-конфигурации config.ini. Для работы с БД MySQL используется клиент libmysql.dll версии 5.5, если Вы будете использовать другой коннектор например низкой версии, то необходимо менять dll на соответствующую версию. В Лазарус имеется несколько версии коннекторов: от 4.1 до 5.5.
Файл иконки refbot.ico. Refbot.exe - сама программа.
Исходный код проекта
Детально описывать весь код не буду, приведу лишь основные части кода.
Для основного потока создается класс-потока TMyThread = class(TThread). В TMyThread.Execute осуществляется основной цикл программы:
while (not Terminated) do
begin
try
Form1.MySQL55Connection1.Connected:=true;
except
status := 'ошибка подключения БД!';
Synchronize(@Showstatus);
Synchronize(@Exit);
Terminate;
end;
Form1.SQLQuery1.Close;
Form1.SQLQuery1.SQL.Text :=
'SELECT id,LOWER(site) AS domain FROM sites WHERE ready = FALSE LIMIT 1;';
Form1.SQLQuery1.Open;
if Form1.SQLQuery1.EOF then
begin
status := 'Работа завершена!';
Synchronize(@CountShow);
Synchronize(@Showstatus);
Synchronize(@Exit);
Terminate;
end;
id := Form1.SQLQuery1.FieldByName('id').AsString;
domain := Form1.SQLQuery1.FieldByName('domain').AsString;
Form1.SQLQuery1.Close;
temp := Form1.theget(domain);
if LeftStr(temp,1) = '2' then
Form1.SQLQuery1.SQL.Text:=
'UPDATE sites SET ready=TRUE, err=FALSE, code="200" WHERE id='+id;
if temp = 'error' then
Form1.SQLQuery1.SQL.Text:=
'UPDATE sites SET ready=TRUE, err=TRUE, code="err" WHERE id='+id;
if LeftStr(temp,1) = '4' then
Form1.SQLQuery1.SQL.Text:=
'UPDATE sites SET ready=TRUE, err=TRUE, code="'+temp+'" WHERE id='+id;
if LeftStr(temp,1) = '3' then
Form1.SQLQuery1.SQL.Text:=
'UPDATE sites SET ready=TRUE, err=TRUE, code="'+temp+'" WHERE id='+id;
if LeftStr(temp,1) = '5' then
Form1.SQLQuery1.SQL.Text:=
'UPDATE sites SET ready=TRUE, err=TRUE, code="'+temp+'" WHERE id='+id;
try
Form1.SQLQuery1.ExecSQL;
except
status := 'Ошибка записи в БД!';
Synchronize(@Showstatus);
Terminate;
end;
Synchronize(@CountShow);
status := domain+' - '+temp;
Synchronize(@Showstatus);
Form1.MySQL55Connection1.Connected:=false;
end;
Synchronize(@Exit);
Для работы с БД на форме присутствуют коннекторы базы, это MySQL55Connection1, SQLTransaction1, SQLQuery1. Эти элементы соединены между собой и посредством этой группы осуществляется работа с сервером MySQL. Бот с помощью запроса SELECT id,LOWER(site) AS domain FROM sites WHERE ready = FALSE LIMIT 1 получает следующий необработанный адрес сайта.
В файле конфигурации производится сохранение и восстановление настроек, используется компонент IniPropStorage1.
Процедура Counter осуществляет запрос в БД, выводит текущую информацию. Запрос выглядит следующим образом: SELECT COUNT(*) AS total, cpl, (COUNT(*)-cpl) AS balance FROM sites,(SELECT COUNT(*) AS cpl FROM sites WHERE ready = TRUE) AS t. Эта функция запускается всего лишь один раз, во время старта программы. Для дальнейшего подсчета используется встроенная процедура потока TMyThread.CountShow. Внутри потока имеется свой счетчик, переменная cnt. Значение этой переменной инкрементируется и осуществляется обычная математика из известных данных. Дело в том, что каждый раз осуществлять подсчет данных с помощью БД не самая правильная идея, так как сервер при подсчете затратит много времени и скорость работы программы существенно снизится.
Функция theget осуществляет соединение с WEB-сервером с помощью сокетов, передает HTTP запрос и получает ответ. Для таймаута функции установлен временной интервал 2 секунды на подключение и 3 секунды на обмен информацией:
fillchar(sock.recv,1024,#0);
fillchar(sock.send,1024,#0);
WSAStartup($101,wsa);
sock.host := domain;
sock.port := 80;
if sock.uri='' then sock.uri:='/';
sock.sock:=socket(AF_INET,SOCK_STREAM,0);
sock.time:=4;
temp:='GET '+sock.uri+' HTTP/1.1'+#13#10+
'Host: '+sock.host+#13#10+
'Referer: http://'+site+#13#10+
'Connection: close'+#13#10+
'User-Agent: '+bot+' (+http://'+site+')'+#13#10+
'Accept-Encoding: gzip'+#13#10+
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'+#13#10+
'Cache-Control: no-cache'+#13#10#13#10;
sock.send:=temp;
sock.size:=length(temp);
ioctlsocket(sock.sock,FIONBIO,on_sock);
addr.sin_family:=AF_INET;
addr.sin_port:=htons(sock.port);
addr.sin_addr.s_addr:=d_addr(sock.host);
connect(sock.sock,addr,sizeof(addr));
sock.stat:=CONNECTING_SOCK;
FD_ZERO(wfds);
wfds_empty:=true;
wait:=true;
while wait do
begin
if (sock.stat=CONNECTING_SOCK) and (sock.status='') then
begin
FD_SET(sock.sock,wfds);
wfds_empty:=false;
end;
if not wfds_empty then
begin
tv.tv_sec:=0;
tv.tv_usec:=500000;
select(0,nil,@wfds,nil,@tv);
end;
if (sock.stat<>CLOSE_SOCK) and (sock.status='') then
begin
sleep(5);
dec(sock.time);
if sock.time<=0 then
begin
sock.stat:=CLOSE_SOCK;
sock.status:='timeout';
ioctlsocket(sock.sock,FIONBIO,off_sock);
closesocket(sock.sock);
end;
end;
if (sock.stat=CONNECTING_SOCK) and (sock.status='') then
begin
if FD_ISSET(sock.sock,wfds) then
begin
sock.stat:=CONNECTED_SOCK;
end;
end;
if (sock.stat=CONNECTED_SOCK) and (sock.status='') then
begin
FD_CLR(sock.sock,wfds);
send(sock.sock,sock.send,sock.size,0);
sock.stat:=SENDED_SOCK;
end;
if (sock.stat=SENDED_SOCK) and (sock.status='') then
begin
temp:='';
read:=true;
a:=0;
b:=0;
recvwait:=false;
while read do
begin
if a=100 then read:=false;
inc(a);
fillchar(sock.recv,1024,#0);
recv(sock.sock,sock.recv,1024,0);
if sock.recv<>'' then recvwait:=true;
if temp2<>sock.recv then
begin
temp2:=sock.recv;
temp+=sock.recv;
end
else
if recvwait then
begin
if (b=1) then
read:=false
else inc(b);
end;
sleep(30);
end;
sock.status:='ok';
sock.stat:=CLOSE_SOCK;
ioctlsocket(sock.sock,FIONBIO,off_sock);
closesocket(sock.sock);
end;
wait:=false;
if sock.status='' then wait:=true;
end;
WSACleanup;
if (sock.status<>'ok') OR (temp = '') then
begin
result:='error';
exit;
end;
reg:=TRegExpr.Create;
reg.Expression:='([^"HTTP/1.1 "][0-9]+)';
reg.Exec(temp);
result:=reg.Match[0];
reg.Free;
Исходный код 1,25Мб и программа Refbot 4,6Мб (отдельно без исходного кода).
В следующей статье приведу немного информации о работе и результатах работы рефбота.