Форум программистов, компьютерный форум, киберфорум
Наши страницы
Erlang, OTP
Войти
Регистрация
Восстановить пароль
 
Рейтинг 4.78/9: Рейтинг темы: голосов - 9, средняя оценка - 4.78
S_el
2233 / 1699 / 355
Регистрация: 15.12.2013
Сообщений: 6,794
1

[wxErlang] Пишем простой чатик

06.02.2016, 21:57. Просмотров 1880. Ответов 12

В качестве практики программирования на функциональном языке программирования Erlang решил написать простой чатик.
Так как это моя первая попытка написать приложение, использующее сеть, то у меня ушло немало времени на поиск и разбор полученной информации. Чтобы в дальнейшем новичкам было проще, а более опытные могли дать рекомендации и/или исправить возможные ошибки решил написать обзор по написанию чата. Задачу условно разделю на 3 части, написание каждой из которых разберу шаг за шагом:
1. Написание GUI на WxErlang со всеми обработчиками.
2. Функции клиента
3. Функции сервера
Я не буду детально описывать создание графического интерфейса, так как уже делал это в своей предыдущей теме, к тому же в коде нет ничего сложного. Если у меня будет желание продолжить тему, я добавлю те составляющие чата, которые не стал реализовывать для упрощения задачи. Это и хранение информации о пользователе и более качественная обработка ошибок и многое другое. Возможно, все эти вещи я опишу в дальнейшем, когда захочу поближе познакомится, с такой мощной и практически неотъемлемой составляющей программирования на Erlang – OTP фреймворком.
Также буду признателен за советы по другим хорошим тренировочным задачам.
Как обычно ниже привожу ряд полезных ссылок или названия книг, которые будут полезны желающим углубить свои знания.

Литература по Erlang
1. Learn You Some Erlang for great good! http://learnyousomeerlang.com/
http://www.ozon.ru/context/detail/id/28953563/
2. Джо Армстронг - Программирование на Эрланге
https://github.com/dyp2000/Russian-A...f/fullbook.pdf
3. Франческо Чезарини, Саймон Томпсон - Программирование в Erlang
http://www.ozon.ru/context/detail/id/30671701/

Литература для изучения сетей:
1. Таненбаум "Компьютерные сети",
2. Снейдер "Эффективное программирование TCP/IP",
3. Стивенс "Протоколы TCP/IP. Практическое руководство",
4. Оланд и Джонс "Программирование в сетях Windows".

Другие обзоры написания чатов:
1. Основы работы с сетью в Java на примере консольного чата
2. Клиент-серверный чат, используя сокеты Qt/C++

Результат вы видите на скриншоте:
[wxErlang] Пишем простой чатик
4
Similar
Эксперт
41792 / 34177 / 6122
Регистрация: 12.04.2006
Сообщений: 57,940
06.02.2016, 21:57
Ответы с готовыми решениями:

[wxErlang] Пишем простую игру на wxErlang
Продолжая изучать мир функционального программирования на Erlang, дошел до темы по созданию GUI . Я...

Какова практика распространения(дистрибуции) приложений WxErlang?
Признаюсь честно, знаком с erlang на уровне беглого чтения нескольких статей. Наличие wxerlang в...

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

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

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

12
S_el
2233 / 1699 / 355
Регистрация: 15.12.2013
Сообщений: 6,794
06.02.2016, 22:00  [ТС] 2
GUI
Первым делом реализовываем GUI. На форме создаем необходимые элементы управления: 2 кнопки, одна для установки соединения, вторая – для отправки сообщения. 3 однострочных поля для ввода адреса, порта и ника, а также 2 многострочных для ввода и отображения сообщений. Контролу для отображения принимаемых и отображаемых сообщений устанавливаем свойства для запрета ввода текста. Чтобы иметь возможность работать с интерпретатором после начала работы клиента, основную функцию запускаем в отдельном процессе. В дальнейшем в функции, реализующую цикл обработки событий графического интерфейса пользователя, понадобится еще и сокет. Вместо сокета параметром функции будем передавать пустой список. Это связано с тем, что для его создания требуется знать параметры соединения: хост и порт, которые определяются пользователем.
В результате получим следующий код:
Prolog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
-module(client).
-export([start/0]).
-include_lib("E:/Erlang/erl7.2.1/lib/wx-1.6/include/wx.hrl").
 
-define(Connect, 150).
-define(Send, 151).
 
start() -> spawn_link(fun() -> init() end).
 
init() ->
  wx:new(),
  Frame = wxFrame:new(wx:null(), ?wxID_ANY, "Simple chat client", [{size, {400, 400}},
    {style, ?wxDEFAULT_FRAME_STYLE band (bnot (?wxMAXIMIZE_BOX bor ?wxRESIZE_BORDER))}]),
  Panel = wxPanel:new(Frame),
  MainSizer = wxBoxSizer:new(?wxHORIZONTAL),
  wxSizer:addSpacer(MainSizer, 15),
  VSizer1 = wxBoxSizer:new(?wxVERTICAL),
  wxSizer:addSpacer(VSizer1, 15),
 
  OutputText = wxTextCtrl:new(Panel, ?wxID_ANY,
    [{size, {250, 250}},
      {style, ?wxTE_MULTILINE}]),
  wxTextCtrl:setEditable(OutputText, false),
  wxSizer:add(VSizer1, OutputText, []),
  wxSizer:addSpacer(VSizer1, 25),
  InputText = wxTextCtrl:new(Panel, ?wxID_ANY,
    [{size, {250, 50}}, {style, ?wxTE_MULTILINE}]),
  wxSizer:add(VSizer1, InputText, []),
  wxSizer:addSpacer(VSizer1, 10),
  wxSizer:add(MainSizer, VSizer1, []),
  wxSizer:addSpacer(MainSizer, 20),
 
  VSizer2 = wxBoxSizer:new(?wxVERTICAL),
  wxSizer:addSpacer(VSizer2, 15),
  BConnect = wxButton:new(Panel, ?Connect, [{label, "Connect"}, {size, {80, 30}}]),
  BSend = wxButton:new(Panel, ?Send, [{label, "Send"}, {size, {80, 50}}]),
  NickText = wxTextCtrl:new(Panel, ?wxID_ANY, [{value, "NickName"}, {size, {80, 25}}]),
  Ip = wxTextCtrl:new(Panel, ?wxID_ANY, [{value, "127.0.0.1"}, {size, {80, 25}}]),
  Port = wxTextCtrl:new(Panel, ?wxID_ANY, [{value, "6268"}, {size, {80, 25}}]),
  wxSizer:add(VSizer2, BConnect, []),
  wxSizer:addSpacer(VSizer2, 10),
  wxSizer:add(VSizer2, Ip, []),
  wxSizer:addSpacer(VSizer2, 10),
  wxSizer:add(VSizer2, Port, []),
  wxSizer:addSpacer(VSizer2, 50),
  wxSizer:add(VSizer2, NickText, []),
  wxSizer:addSpacer(VSizer2, 100),
  wxSizer:add(VSizer2, BSend),
 
  wxSizer:add(MainSizer, VSizer2, []),
  wxPanel:setSizer(Panel, MainSizer),
  wxPanel:connect(Panel, command_button_clicked),
  wxFrame:connect(Frame, close_window),
  wxFrame:show(Frame),
  Pid = self(),
  loop([Frame, OutputText, InputText, BConnect], {NickText, Ip, Port}, [], Pid).
 
loop(Widgets, Params, ClientSocket, Pid) ->
  receive
 
    #wx{id = ?Connect, event = #wxCommand{type = command_button_clicked}} ->
      io:format("Press button Connect~n"),
      loop(Widgets, Params, ClientSocket, Pid);
 
    #wx{id = ?Send, event = #wxCommand{type = command_button_clicked}} ->
      io:format("Press button Send~n"),
      loop(Widgets, Params, ClientSocket, Pid);
 
    #wx{event = #wxClose{}} ->
      wx:destroy()
 
  end.
Займемся установкой соединения. Клиентская часть, реализующая соединение будет написана во 2-ой части данного обзора, то пока ограничимся функцией-заглушкой, выводящей на экран Pid процесса и параметры соединения:
Prolog
1
2
client(Host, Port, Pid) ->
  io:format("Процесс ~p установил соединение с сервером по адресу ~p:~p~n", [Pid, Host, Port]).
И помещаем её в обработчик нажатия на Connect:
Prolog
1
2
3
4
5
6
…
client(
        wxTextCtrl:getValue(Ip),
        wxTextCtrl:getValue(Port),
        Pid),
Прежде чем перейти к отправке сообщения, следует заблокировать элементы управления для ввода адреса, порта, соединения и ника, а также саму кнопку Connect для того, чтобы после установки соединения эти параметры оставались неизменными.
Prolog
1
2
3
4
wxTextCtrl:setEditable(NickText, false),
wxTextCtrl:setEditable(Ip, false),
wxTextCtrl:setEditable(Port, false),
wxWindow:enable(B1, [{enable, false}]),
Функцию отправки создаем по такому же типу: выводим ник и сообщение в консоль. Первый аргумент оставляем для сокета.
Prolog
1
2
send(_, Message) ->
  io:format("Отправлено сообщение ~p~n", [Message]).
Так как сообщение будем передавать в бинарном виде, напишем вспомогательную функцию для преобразования текста из юникода в требуемую форму. А заодно объединим ник и сообщение:
Prolog
1
create_message(Nick, Text) -> unicode:characters_to_binary(Nick ++ " >> " ++ Text).
При отправке сообщения следует проверять, было ли установлено соединение, и в зависимости от этого отправлять введенный текст серверу или отображать информационное окно.
Prolog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 case wxWindow:isEnabled(B1) of
        false ->
          send(
            ClientSocket,
            create_message(
              wxTextCtrl:getValue(NickText),
              wxTextCtrl:getValue(InputText))),
          wxTextCtrl:setValue(InputText,"");
        true ->
          MD = wxMessageDialog:new(Frame, "Вначале установите соединение!",
            [{style, ?wxOK}, {caption, "Warning"}]),
          wxDialog:showModal(MD),
          wxDialog:destroy(MD)
      end,
В результате получим такой код:
Prolog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
-module(client).
 
%% API
-export([start/0]).
-include_lib("E:/Erlang/erl7.2.1/lib/wx-1.6/include/wx.hrl").
 
-define(Connect, 150).
-define(Send, 151).
 
start() -> spawn_link(fun() -> init() end).
 
init() ->
  wx:new(),
  Frame = wxFrame:new(wx:null(), ?wxID_ANY, "Simple chat client", [{size, {400, 400}},
    {style, ?wxDEFAULT_FRAME_STYLE band (bnot (?wxMAXIMIZE_BOX bor ?wxRESIZE_BORDER))}]),
  Panel = wxPanel:new(Frame),
  MainSizer = wxBoxSizer:new(?wxHORIZONTAL),
  wxSizer:addSpacer(MainSizer, 15),
  VSizer1 = wxBoxSizer:new(?wxVERTICAL),
  wxSizer:addSpacer(VSizer1, 15),
 
  OutputText = wxTextCtrl:new(Panel, ?wxID_ANY,
    [{size, {250, 250}},
      {style, ?wxTE_MULTILINE}]),
  wxTextCtrl:setEditable(OutputText, false),
  wxSizer:add(VSizer1, OutputText, []),
  wxSizer:addSpacer(VSizer1, 25),
  InputText = wxTextCtrl:new(Panel, ?wxID_ANY,
    [{size, {250, 50}}, {style, ?wxTE_MULTILINE}]),
  wxSizer:add(VSizer1, InputText, []),
  wxSizer:addSpacer(VSizer1, 10),
  wxSizer:add(MainSizer, VSizer1, []),
  wxSizer:addSpacer(MainSizer, 20),
 
  VSizer2 = wxBoxSizer:new(?wxVERTICAL),
  wxSizer:addSpacer(VSizer2, 15),
  BConnect = wxButton:new(Panel, ?Connect, [{label, "Connect"}, {size, {80, 30}}]),
  BSend = wxButton:new(Panel, ?Send, [{label, "Send"}, {size, {80, 50}}]),
  NickText = wxTextCtrl:new(Panel, ?wxID_ANY, [{value, "NickName"}, {size, {80, 25}}]),
  Ip = wxTextCtrl:new(Panel, ?wxID_ANY, [{value, "127.0.0.1"}, {size, {80, 25}}]),
  Port = wxTextCtrl:new(Panel, ?wxID_ANY, [{value, "6268"}, {size, {80, 25}}]),
  wxSizer:add(VSizer2, BConnect, []),
  wxSizer:addSpacer(VSizer2, 10),
  wxSizer:add(VSizer2, Ip, []),
  wxSizer:addSpacer(VSizer2, 10),
  wxSizer:add(VSizer2, Port, []),
  wxSizer:addSpacer(VSizer2, 50),
  wxSizer:add(VSizer2, NickText, []),
  wxSizer:addSpacer(VSizer2, 100),
  wxSizer:add(VSizer2, BSend),
 
  wxSizer:add(MainSizer, VSizer2, []),
  wxPanel:setSizer(Panel, MainSizer),
  wxPanel:connect(Panel, command_button_clicked),
  wxFrame:connect(Frame, close_window),
  wxFrame:show(Frame),
  Pid = self(),
  loop([Frame, OutputText, InputText, BConnect], {NickText, Ip, Port}, [], Pid).
 
loop([Frame, OutputText, InputText, B1] = Widgets, {NickText, Ip, Port} = Params, ClientSocket, Pid) ->
  receive
 
    #wx{id = ?Connect, event = #wxCommand{type = command_button_clicked}} ->
      client(
        wxTextCtrl:getValue(Ip),
        wxTextCtrl:getValue(Port),
        Pid),
      wxTextCtrl:setEditable(NickText, false),
      wxTextCtrl:setEditable(Ip, false),
      wxTextCtrl:setEditable(Port, false),
      wxWindow:enable(B1, [{enable, false}]),
      loop(Widgets, Params, ClientSocket, Pid);
 
    #wx{id = ?Send, event = #wxCommand{type = command_button_clicked}} ->
      case wxWindow:isEnabled(B1) of
        false ->
          send(
            ClientSocket,
            create_message(
              wxTextCtrl:getValue(NickText),
              wxTextCtrl:getValue(InputText))),
          wxTextCtrl:setValue(InputText,"");
        true ->
          MD = wxMessageDialog:new(Frame, "Вначале установите соединение!",
            [{style, ?wxOK}, {caption, "Warning"}]),
          wxDialog:showModal(MD),
          wxDialog:destroy(MD)
      end,
      loop(Widgets, Params, ClientSocket, Pid);
 
    #wx{event = #wxClose{}} ->
      wx:destroy()
 
  end.
 
client(Host, Port, Pid) ->
  io:format("Процесс ~p установил соединение с сервером по адресу ~p:~p~n", [Pid, Host, Port]).
 
send(_, Message) ->
  io:format("Отправлено сообщение ~p~n", [Message]).
 
create_message(Nick, Text) -> unicode:characters_to_binary(Nick ++ " >> " ++ Text).
3
S_el
2233 / 1699 / 355
Регистрация: 15.12.2013
Сообщений: 6,794
06.02.2016, 22:01  [ТС] 3
Клиентская часть
Все функции для работы с протоколом Transmission Control Protocol (TCP), а именно его мы будем использовать, предоставляет Erlang в библиотеке gen_tcp. Подробности о реализации этого протокола читайте по приведенным в первом посте ссылкам.
Клиентская функция принимает адрес и порт, по которым должно быть установлено соединение и возвращает сокет.
Prolog
1
2
client(Host, Port) ->
  gen_tcp:connect(Host, Port, [binary,{active, false}, {packet, 0}]).
Как видите, здесь нет ничего сложного. С помощью библиотечной функции пробуем подключиться и возвращаем результат. За обработку будет отвечать вызывающая сторона. Остановимся на параметрах:
binary – обозначает передачу всех сообщений в бинарном виде. По умолчанию сообщения передаются в виде списка целых чисел вне зависимости от того, в каком виде они отправляются, поэтому этот параметр указывают отдельно. Используем так как считается, что с двоичными данными Erlang работает быстрее, чем со списками.
{active, false} - устанавливает пассивный режим. Сообщения, поступающие в сокет, сохраняются в буфер, процесс может извлечь их вызовом функций gen_tcp: recv/2 и gen_tcp:recv/3. Это параметр рекомендуется применять из-за использования tcp/ip flow control при больших объемах данных и высоких скоростях; в случаях, когда разная скорость работы сети у клиента и сервера; чтобы клиент не завалил сообщениями сервер. Нам это не грозит, но будем придерживаться принципов хорошего стиля.
{packet, 0} - данные не пакуются, а передаются на обработку как есть.
Информацию по другим опциональным параметрам можно прочесть в документации:
http://erlang.org/doc/man/gen_tcp.html
Можем переходить к следующей функции? Опытный читатель (или внимательный) скажет - нет, и будет прав. Если бы наш клиент работал только на отправку сообщений, тогда можно было бы остановиться. Но для правильной работы чата необходимо также получать сообщения от других участников. Для этого запускаем отдельный поток создаем процесс, вызывающий функцию для приёма таких данных. Кроме возможности принимать сообщения их надо еще и отображать на форме. Поэтому нужно добавить еще один аргумент - Pid соответствующего процесса. Ему будем передавать принятые сообщения. Можно было бы зарегистрировать этот процесс, но так есть возможность запускать несколько клиентов с одного вычислителя Erlang. Теперь мы не можем игнорировать успешность установки соединения, поэтому, используя case-выражение, разделяем эти варианты.
Prolog
1
2
3
4
5
client(Host, Port, Pid) ->
  case gen_tcp:connect(Host, Port, [binary, {active, false}, {packet, 0}]) of
    {ok, Socket} -> spawn(fun() -> recv(Socket, Pid) end), {ok, Socket};
    Other -> Other
  end.
Функция recv будет использовать библиотечную функцию gen_tcp: recv/2 для извлечения входных данных. В случае успеха – передаем их. В случае неудачи было бы правильным также обрабатывать такой случай, но я не стал рассматривать этот вариант.
Prolog
1
2
3
4
5
recv(Socket, Pid) ->
  case gen_tcp:recv(Socket, 0) of
    {ok, Data} -> Pid ! {add_message, Data}, recv(Socket, Pid);
    {error, Reason} -> Reason
  end.
Сразу добавим обработчик, отвечающий за отображение текстовых сообщений, для этого используем функцию добавления текста виджета wxTextCtrl:
Prolog
1
2
3
{add_message, Answer} ->
      wxTextCtrl:appendText(OutputText, "\n" ++ Answer),
      loop(Widgets, Params, ClientSocket, Pid);
Функции отсоединения(вызываем при закрытии клиентской программы) и отправки сообщения – просто обертки над библиотечными функциями. Выносим отдельно для упрощения в случае дальнейшего их расширения.
Prolog
1
2
3
4
5
send(Socket, Message) ->
  gen_tcp:send(Socket, Message).
 
disconnect(Socket) ->
  gen_tcp:close(Socket).
Я не буду описывать вспомогательные функции для преобразования строковых представлений адреса и порта в форму, требуемую для передачи в функцию gen_tcp:connect/3 и изменения обработки выхода, т.к. в них нет ничего интересного. Для завершения этой части, осталось изменить обработчик подключения, чтобы учитывать случай, когда соединение установить не удалось. В таких ситуациях ограничимся простым выводом сообщения в MessageBox с указанием причины, также как мы делали при обработке попытки отправить сообщение до успешной установки соединения с сервером. Кроме того, при соединении следует отправить серверу специальное сообщение, чтобы он добавил подключенный клиент в список рассылки сообщений чата.
Prolog
1
2
3
…
send(Socket, term_to_binary({add, new})),
В результате получим такой код:
Prolog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
-module(client).
-export([start/0]).
-include_lib("E:/Erlang/erl7.2.1/lib/wx-1.6/include/wx.hrl").
 
-define(Connect, 150).
-define(Send, 151).
 
start() -> spawn_link(fun() -> init() end).
 
init() ->
  wx:new(),
  Frame = wxFrame:new(wx:null(), ?wxID_ANY, "Simple chat client", [{size, {400, 400}},
    {style, ?wxDEFAULT_FRAME_STYLE band (bnot (?wxMAXIMIZE_BOX bor ?wxRESIZE_BORDER))}]),
  Panel = wxPanel:new(Frame),
  MainSizer = wxBoxSizer:new(?wxHORIZONTAL),
  wxSizer:addSpacer(MainSizer, 15),
  VSizer1 = wxBoxSizer:new(?wxVERTICAL),
  wxSizer:addSpacer(VSizer1, 15),
 
  OutputText = wxTextCtrl:new(Panel, ?wxID_ANY,
    [{size, {250, 250}},
      {style, ?wxTE_MULTILINE}]),
  wxTextCtrl:setEditable(OutputText, false),
  wxSizer:add(VSizer1, OutputText, []),
  wxSizer:addSpacer(VSizer1, 25),
  InputText = wxTextCtrl:new(Panel, ?wxID_ANY,
    [{size, {250, 50}}, {style, ?wxTE_MULTILINE}]),
  wxSizer:add(VSizer1, InputText, []),
  wxSizer:addSpacer(VSizer1, 10),
  wxSizer:add(MainSizer, VSizer1, []),
  wxSizer:addSpacer(MainSizer, 20),
 
  VSizer2 = wxBoxSizer:new(?wxVERTICAL),
  wxSizer:addSpacer(VSizer2, 15),
  BConnect = wxButton:new(Panel, ?Connect, [{label, "Connect"}, {size, {80, 30}}]),
  BSend = wxButton:new(Panel, ?Send, [{label, "Send"}, {size, {80, 50}}]),
  NickText = wxTextCtrl:new(Panel, ?wxID_ANY, [{value, "NickName"}, {size, {80, 25}}]),
  Ip = wxTextCtrl:new(Panel, ?wxID_ANY, [{value, "127.0.0.1"}, {size, {80, 25}}]),
  Port = wxTextCtrl:new(Panel, ?wxID_ANY, [{value, "6268"}, {size, {80, 25}}]),
  wxSizer:add(VSizer2, BConnect, []),
  wxSizer:addSpacer(VSizer2, 10),
  wxSizer:add(VSizer2, Ip, []),
  wxSizer:addSpacer(VSizer2, 10),
  wxSizer:add(VSizer2, Port, []),
  wxSizer:addSpacer(VSizer2, 50),
  wxSizer:add(VSizer2, NickText, []),
  wxSizer:addSpacer(VSizer2, 100),
  wxSizer:add(VSizer2, BSend),
 
  wxSizer:add(MainSizer, VSizer2, []),
  wxPanel:setSizer(Panel, MainSizer),
  wxPanel:connect(Panel, command_button_clicked),
  wxFrame:connect(Frame, close_window),
  wxFrame:show(Frame),
  Pid = self(),
  loop([Frame, OutputText, InputText, BConnect], {NickText, Ip, Port}, [], Pid).
 
loop([Frame, OutputText, InputText, B1] = Widgets, {NickText, Ip, Port} = Params, ClientSocket, Pid) ->
  receive
 
    {add_message, Answer} ->
      wxTextCtrl:appendText(OutputText, "\n" ++ Answer),
      loop(Widgets, Params, ClientSocket, Pid);
 
    #wx{id = ?Connect, event = #wxCommand{type = command_button_clicked}} ->
      Answer = client(
        iptotuple(wxTextCtrl:getValue(Ip)),
        get_port(wxTextCtrl:getValue(Port)),
        Pid),
      case Answer of
        {ok, Socket} ->
          wxTextCtrl:setEditable(NickText, false),
          wxTextCtrl:setEditable(Ip, false),
          wxTextCtrl:setEditable(Port, false),
          wxWindow:enable(B1, [{enable, false}]),
          send(Socket, term_to_binary({add, new})),
          loop(Widgets, Params, Socket, Pid);
        {error, Reason} ->
          MD = wxMessageDialog:new(Frame, "Невозможно установить соединение : " ++ atom_to_list(Reason),
            [{style, ?wxOK}, {caption, "Warning"}]),
          wxDialog:showModal(MD),
          wxDialog:destroy(MD),
          loop(Widgets, Params, ClientSocket, Pid)
      end;
 
    #wx{id = ?Send, event = #wxCommand{type = command_button_clicked}} ->
      case wxWindow:isEnabled(B1) of
        false ->
          send(
            ClientSocket,
            create_message(
              wxTextCtrl:getValue(NickText),
              wxTextCtrl:getValue(InputText))),
          wxTextCtrl:setValue(InputText,"");
        true ->
          MD = wxMessageDialog:new(Frame, "Вначале установите соединение!",
            [{style, ?wxOK}, {caption, "Warning"}]),
          wxDialog:showModal(MD),
          wxDialog:destroy(MD)
      end,
      loop(Widgets, Params, ClientSocket, Pid);
 
    #wx{event = #wxClose{}} ->
      case is_list(ClientSocket) of
        true -> ok;
        false -> disconnect(ClientSocket)
      end,
      wx:destroy()
 
  end.
 
client(Host, Port, Pid) ->
  case gen_tcp:connect(Host, Port, [binary, {active, false}, {packet, 0}]) of
    {ok, Socket} -> spawn(fun() -> recv(Socket, Pid) end), {ok, Socket};
    Other -> Other
  end.
 
recv(Socket, Pid) ->
  case gen_tcp:recv(Socket, 0) of
    {ok, Data} -> Pid ! {add_message, Data}, recv(Socket, Pid);
    {error, Reason} -> Reason
  end.
 
send(Socket, Message) ->
  gen_tcp:send(Socket, Message).
 
disconnect(Socket) ->
  gen_tcp:close(Socket).
 
create_message(Nick, Text) -> unicode:characters_to_binary(Nick ++ " >> " ++ Text).
 
iptotuple(Ip) ->
  list_to_tuple([fun() -> {N, _} = string:to_integer(X), N end()
    || X <- string:tokens(Ip, ".")]).
 
get_port(Str) -> element(1, string:to_integer(Str)).
[wxErlang] Пишем простой чатик
3
S_el
2233 / 1699 / 355
Регистрация: 15.12.2013
Сообщений: 6,794
06.02.2016, 22:08  [ТС] 4
Серверная часть.
Осталось самое сложное, хотя и не самое длинное – серверная часть.
Реализацию начнем с функции запуска, которая принимает 1 параметр – порт, на котором будет запускаться сокет, обеспечивающий соединение. В нашем случае, одного слушающего процесса, ожидающего от клиента запроса на соединение, будет недостаточно, так как надо где-то хранить информацию обо всех установленных соединениях. На мой взгляд, для этой задачи лучше всего подходит множество, которое в Erlang реализовано в модуле sets. Поэтому начинаем выполнение этой функции в новом процессе, который регистрируем с именем модуля. Затем, исходя из принципа 1 действие – 1 функция, вызываем и возвращаем результат функции listen/1. Она должна создать и вернуть слушающий сокет.
Prolog
1
2
3
start(Port) ->
  register(?MODULE, spawn(fun() ->server(sets:new()) end)),
  listen(Port).
Прежде чем перейти к рассмотрению функции listen/1 напишем функцию server/1, которая должна обрабатывать случаи добавления клиента, отправки сообщения и удаления клиента, при его отсоединении. Реализация достаточно очевидна:
Prolog
1
2
3
4
5
6
7
8
9
10
11
12
server(Set) ->
  receive
    {add, Socket} ->
      NewSet = sets:add_element(Socket,Set),
      server(NewSet);
    {remove,Socket}->
      NewSet = sets:del_element(Socket,Set),
      server(NewSet);
    {send, Message} ->
      [gen_tcp:send(Socket, Message) || Socket <- sets:to_list(Set)],
      server(Set)
  end.
Теперь определим функцию listen/1. Начнем с того, что создадим слушающий сокет, используя библиотечную функцию gen_tcp:listen/2 с теми-же параметрами, которые использовались для создания соединения.
Prolog
1
2
listen(Port) ->
{ok, Socket} = gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]),
Далее создаем принимающий процесс, выполняющий функцию для приёма данных, чтобы не блокировать вызывающий процесс и возвращаем слушающий сокет.
Prolog
1
2
spawn_link(fun() -> accept(Socket) end),
  Socket.
Переходим к принимающему процессу (функции accept/1). Слушающий сокет, возвращенный gen_tcp:listen/2, может использоваться только в парной функции gen_tcp:accept/1 для приёма входного соединения:
Prolog
1
{ok, Socket} = gen_tcp:accept(LSocket),
При каждом поступающем запросе будем создавать новый процесс и продолжаем ожидать поступления новых запросов на соединение. О другом способе определения принимающего процесса можно прочить в 15 главе книги Чезарини и Томпсона. В качестве управляющего процесса обычно выступает процесс, установивший соединение вызовом gen_tcp: accept или gen_tcp: connect.
Prolog
1
Pid = spawn(fun() -> loop(Socket) end),
Для перенаправления сообщений другому процессу используем библиотечную функцию:
Prolog
1
gen_tcp:controlling_process(Socket, Pid),
и продолжаем “слушать”:
Prolog
1
accept(LSocket).
Осталась последняя функция – та, что отвечает за обработку принимаемых по сокету данных. Модуль inet содержит обобщённые функции для работы с сокетами. Для настройки различных параметров открытого сокета существует функция inet:setopts/2. Мы будем её использовать, чтобы иметь возможность получать данные сокета в виде сообщений и в то же время использовать flow control. Для этого устанавливаем параметр {active, once}, устанавливающий сокет в активный режим, но после приёма первого сообщения режим переключается на пассивный.
Prolog
1
  inet:setopts(Sock, [{active, once}]),
Как нам известно, данные могут поступать к нам либо сигнализируя о подключении нового клиента, либо о приеме сервером очередного текстового сообщения.
В активном режиме процесс будет получать такие сообщения:
{tcp, Socket, Data} – пришли новые данные;
{tcp_closed, Socket} – соединение закрылось.
В зависимости от принятого сообщения отправляем запрос передаваемый главному процессу (который мы регистрировали). С закрытием – все понятно, но как различить сигнал о новом соединении от простой переписки?
В коде клиента при установке соединения передавался кортеж {add,new}. Преобразуем его в бинарную последовательность, чтобы получить тот вид, в котором приходят данные. Для этого вызываем функцию term_to_binary/1 в интерпретаторе и копируем результат в код (жертвуем читабельностью в пользу быстродействия). Теперь используя сопоставление с образцом легко определить нужную нам ветку и, соответственно, отправить нужный запрос.
Prolog
1
2
3
4
5
6
7
8
9
10
11
  receive
    {tcp, Socket, Data} ->
      case Data of
        <<131, 104, 2, 100, 0, 3, 97, 100, 100, 100, 0, 3, 110, 101, 119>> ->
          ?MODULE ! {add,  Socket};
        _ -> ?MODULE ! {send, Data}
      end,
      loop(Socket);
    {tcp_closed, Socket} ->
      ?MODULE ! {remove,Socket}
  end.
В результате код нашего сервера примет такой вид:
Prolog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
-module(server).
-export([start/1]).
 
start(Port) ->
  register(?MODULE, spawn(fun() ->server(sets:new()) end)),
  listen(Port).
 
listen(Port) ->
  {ok, Socket} = gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]),
  spawn_link(fun() -> accept(Socket) end),
  Socket.
 
accept(LSocket) ->
  {ok, Socket} = gen_tcp:accept(LSocket),
  Pid = spawn(fun() -> loop(Socket) end),
  gen_tcp:controlling_process(Socket, Pid),
  accept(LSocket).
 
loop(Sock) ->
  inet:setopts(Sock, [{active, once}]),
  receive
    {tcp, Socket, Data} ->
      case Data of
        <<131, 104, 2, 100, 0, 3, 97, 100, 100, 100, 0, 3, 110, 101, 119>> ->
          ?MODULE ! {add,  Socket};
        _ -> ?MODULE ! {send, Data}
      end,
      loop(Socket);
    {tcp_closed, Socket} ->
      ?MODULE ! {remove,Socket}
  end.
 
server(Set) ->
  receive
    {add, Socket} ->
      NewSet = sets:add_element(Socket,Set),
      server(NewSet);
    {remove,Socket}->
      NewSet = sets:del_element(Socket,Set),
      server(NewSet);
    {send, Message} ->
      [gen_tcp:send(Socket, Message) || Socket <- sets:to_list(Set)],
      server(Set)
  end.
Как видите суммарно получилось меньше 200 строк кода, что довольно мало.
4
06.02.2016, 22:08
S_el
2233 / 1699 / 355
Регистрация: 15.12.2013
Сообщений: 6,794
24.03.2016, 16:17  [ТС] 5
Я показал код более опытным Erlang-программистам, и они указали на ряд небольших недостатков в реализации, которые приведу ниже вместе с исправленной версией.
1. Для обхода sets не нужно переводить его в список, а использовать специальную функцию.
Prolog
1
sets:fold(fun(Socket, _) -> gen_tcp:send(Socket, Message) end, 0, Set),
вместо
Prolog
1
[gen_tcp:send(Socket, Message) || Socket <- sets:to_list(Set)]
2. Параметры отвечающие за значения хоста, порта и ника, лучше помещать в список вместо кортежа, что позволит вынести блокирование и разблокирование этих виджетов в отдельную функцию.
Также передается кнопка, отвечающая за установку соединения
Prolog
1
2
3
setEditable({TextControls,Button},State)->
 lists:foreach(fun(El)->wxTextCtrl:setEditable(El, State) end,TextControls),
 wxWindow:enable(Button, [{enable, State}]).
3. Для превращения строки с ip в кортеж, вместо самописной, можно использовать функцию из модуля inet:
Prolog
1
iptotuple(Ip) ->element(2,inet:parse_address(Ip)).
вместо
Prolog
1
2
3
iptotuple(Ip) ->
  list_to_tuple([fun() -> {N, _} = string:to_integer(X), N end()
    || X <- string:tokens(Ip, ".")]).
В завершении приведу куски кода, добавляющие возможность отключатся и подключатся к серверу, меняя параметры.
Добавляем кнопку
Prolog
1
2
3
4
5
6
7
8
-define(Disconnect, 152).
…
BDisconnect = wxButton:new(Panel, ?Disconnect, [{label, "Disconnect"}, {size, {80, 30}}]),
…
wxSizer:addSpacer(VSizer2, 10),
wxSizer:add(VSizer2, BDisconnect),
wxSizer:addSpacer(VSizer2, 60),
...
Обработчик:
Prolog
1
2
3
4
5
6
7
#wx{id = ?Disconnect, event = #wxCommand{type = command_button_clicked}} ->
      case is_list(ClientSocket) of
        true -> ok;
       false -> disconnect(ClientSocket)
     end,
     setEditable({Params,B1},true),
loop(Widgets, Params, [], Pid);
3
budvox
0 / 0 / 0
Регистрация: 30.12.2016
Сообщений: 4
30.12.2016, 05:44 6
Спасибо большое за тему, для таких начинающих как я, она очень полезна.Не совсем понятно как работает
регистрация : ....->register(?MODULE, spawn(fun() ->server(sets:new()) end)) в серверной части и зависит
ли она как -то от клиентской части, так как у меня не работает вызов server(NewSet); а предыдущий вызов :
NewSet = sets:add_eleent(Socket,SSet) работает нормально. Вероятно и удаление Set не работает тоже
Клиент у меня свой а сервер скопировал точно, но программа как целое не работает из за данной проблемки (вызов server(NewSet) не работает). Буду признателен за помощь и объяснения.

С наступающим новым годом !!
0
S_el
2233 / 1699 / 355
Регистрация: 15.12.2013
Сообщений: 6,794
30.12.2016, 13:12  [ТС] 7
Цитата Сообщение от budvox Посмотреть сообщение
Не совсем понятно как работает регистрация
Регистрируем процесс с идентификатором, передаваемым вторым аргументом с именем,задаваемым вторым.
макрос ?MODULE соответствует названию модуля.
Документация.

Цитата Сообщение от budvox Посмотреть сообщение
в серверной части и зависит
ли она как -то от клиентской части, так как у меня не работает вызов server(NewSet);
Не зависит, вероятно уже есть зарегистрированный процесс с таким именем.
Попробуйте добавить проверку:

Prolog
1
2
3
4
5
6
 case whereis(?MODULE) of
        undefined ->
                       register(?MODULE, spawn(fun() ->server(sets:new()) end)),
                       listen(Port)
        _Exist -> io:format("Alias ~p already exist~n",[?MODULE]),ok
      end
Если дело в этом, тогда
a) Определите другой макрос с именем и замените ?MODULE на него.
б) предварительно разрегистрируйте процесс.

Хотя при неудачной регистрации должно бросаться исключение.

И, я так понял, первый вызов функции server проходит успешно? Тогда убедитесь, что клиент отправляет данные в нужном формате.

Edit: Я имел ввиду, что обычно клиент пишется под сервер, поэтому мог не совсем корректно ответить на вопрос по поводу зависимости.
Цитата Сообщение от S_el Посмотреть сообщение
Кроме того, при соединении следует отправить серверу специальное сообщение, чтобы он добавил подключенный клиент в список рассылки сообщений чата.
Prolog
1
2
3
…
send(Socket, term_to_binary({add, new})),
Если в нужном,то давайте начнем с очевидного шага:
1. Добавьте вывод логирующих сообщений и определите где поведение отличается от ожидаемого.

Или уберите лишний функционал и постепенно наращивайте.

С наступающим!
1
budvox
0 / 0 / 0
Регистрация: 30.12.2016
Сообщений: 4
02.01.2017, 13:15 8
Спасибо! С предыдущей проблемой разобрался, все работает - это была моя невнимательность и отсутствие опыта , спасибо Вам за ценную помощь. Но теперь у меня иное затруднение: после закрытия сокета в клиенте функцией
gen_tcp:close(Socket) повторное подключение к серверу (сервер практически Ваш) не устанавливается, получился такой одноразовый чат, опыта работы с сокетами не имеется. Попробую, конечно, сам поискать в книжках и нете , но буду признателен за помощь и объяснения.

Спасибо Вам за ценный ресурс
0
S_el
2233 / 1699 / 355
Регистрация: 15.12.2013
Сообщений: 6,794
02.01.2017, 16:52  [ТС] 9
Цитата Сообщение от budvox Посмотреть сообщение
Но теперь у меня иное затруднение: после закрытия сокета в клиенте функцией
gen_tcp:close(Socket) повторное подключение к серверу (сервер практически Ваш) не устанавливается, получился такой одноразовый чат
Без кода клиента(функций подключения и отключения) ничего сказать не могу. Единственное, что могу предположить - новое подключение пытается использовать старый, уже закрытый, сокет.
0
budvox
0 / 0 / 0
Регистрация: 30.12.2016
Сообщений: 4
02.01.2017, 17:16 10
Закрывается в клиенте просто вызовом функции ccltt:disconnect(P), где Р. -сокет а cltt - модуль клиента,
сама функция имеет в модуле клиента вид disconnect( Socket) ->gen_tcp:close(Socket).
Р (сокет) получаем так: P = ccltt:con("ALEX"), где ALEX имя клиента, а con - функция connect, имеющая вид :

con(UserName) ->
{ok, Socket} = gen_tcp:connect("localhost", 2345,[binary,{active, false},

{packet, 2},{reuseaddr, true}]), spawn(fun() -> loop(Socket, UserName) end),
F = term_to_binary("add"), gen_tcp:send(Socket,list_to_binary(F)),

gen_tcp:send(Socket, UserName ++ " joined the chat"),Socket. - где loop функция цикла обработки

Извиняюсь за бестолковые вопросы
0
S_el
2233 / 1699 / 355
Регистрация: 15.12.2013
Сообщений: 6,794
02.01.2017, 17:35  [ТС] 11
budvox, похоже, в этой части кода все нормально. Есть возможность привести код для воспроизведения ошибки?
0
budvox
0 / 0 / 0
Регистрация: 30.12.2016
Сообщений: 4
02.01.2017, 17:46 12
Хотел добавить в предыдущий пост, но время ответа превысил .
Извиняюсь за бестолковые вопросы, второй раз подключиться используя переменную Р (сокет) вызовом функции
Р =ccltt:con("ALEX") после вызова disconnect не удается, но если ее переименовать в Р1 , то установить соединение можно
но как -то некрасиво получается плодить переменные, хотя это тренировочная программа. Понимаю ,что -это Erlang и значение переменной нельзя изменить , но понимаю , что делаю что-то не так, а вообще закрывать сокет можно со стороны клиента и/или сервера ? Видел программы в сети где gen_tcp : close (Socket) есть и на сервере и на стороне клиента
0
S_el
2233 / 1699 / 355
Регистрация: 15.12.2013
Сообщений: 6,794
02.01.2017, 19:49  [ТС] 13
Цитата Сообщение от budvox Посмотреть сообщение
второй раз подключиться используя переменную Р (сокет) вызовом функции
Я так и предполагал:
Цитата Сообщение от S_el Посмотреть сообщение
Единственное, что могу предположить - новое подключение пытается использовать старый, уже закрытый, сокет.
Цитата Сообщение от budvox Посмотреть сообщение
но как -то некрасиво получается плодить переменные, хотя это тренировочная программа.
посмотрите, как это сделано у меня - сокет хранится как параметр функции, имитирующей рабочий цикл.

Цитата Сообщение от budvox Посмотреть сообщение
а вообще закрывать сокет можно со стороны клиента и/или сервера ? Видел программы в сети где gen_tcp : close (Socket) есть и на сервере и на стороне клиента
Не специалист, поэтому сказать с определенностью не могу. Полагаю это зависит от задач и вариантов закрытия сокета. Я ориентировался на пример из книги Чезарини. Хотя мне следовало бы добавить возможность останавливать сервер.


P.S. Обрамляйте код в специальный тег, например [PROLOG][/PROLOG], это повышает читабельность.
1
02.01.2017, 19:49
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
02.01.2017, 19:49

Чатик на ActiveMQ
Если опыт есть покритикуйте решение или может оно нормальное :) В общем надо сделать чатик по...

Как модифицировать данную прогу, чтоб чатик получился многопользовательским
Народ, как модифицировать данную прогу, чтоб чатик получился многопользовательским? (более 2х...

Пишем Секундомер
Привет. необходимо сделать такую штуку. По нажатию клавиши старт должен включиться секундомер(...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
13
Ответ Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru