Skip to content

Commit 022e111

Browse files
committed
Finished 'src/appendix/ap02-a_socket_application.md'.
1 parent 513c405 commit 022e111

10 files changed

Lines changed: 305 additions & 3 deletions

File tree

Binary file not shown.
Binary file not shown.
Binary file not shown.

projects/appendix/_build/default/lib/math_server/ebin/math_server.app

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
{applications,[kernel,stdlib]},
66
{mod,{math_server_app,[]}},
77
{env,[]},
8-
{modules,[lib_chan,lib_chan_cs,lib_chan_mm,math_server,
9-
math_server_app,mod_math]}]}.
8+
{modules,[lib_chan,lib_chan_auth,lib_chan_cs,lib_chan_mm,
9+
lib_md5,math_server,math_server_app,mod_math]}]}.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
%% ---
2+
%% Excerpted from "Programming Erlang, Second Edition",
3+
%% published by The Pragmatic Bookshelf.
4+
%% Copyrights apply to this code. It may not be used to create training material,
5+
%% courses, books, articles, and the like. Contact us if you are in doubt.
6+
%% We make no guarantees that this code is fit for any purpose.
7+
%% Visit http://www.pragmaticprogrammer.com/titles/jaerlang2 for more book information.
8+
%%---
9+
-module(lib_chan_auth).
10+
-export([make_challenge/0, make_response/2, is_response_correct/3]).
11+
12+
make_challenge() ->
13+
random_string(25).
14+
make_response(Challenge, Secret) ->
15+
lib_md5:string(Challenge ++ Secret).
16+
is_response_correct(Challenge, Response, Secret) ->
17+
case lib_md5:string(Challenge ++ Secret) of
18+
Response -> true;
19+
_ -> false
20+
end.
21+
22+
%% random_string(N) -> a random string with N characters.
23+
random_string(N) -> random_seed(), random_string(N, []).
24+
random_string(0, D) -> D;
25+
random_string(N, D) ->
26+
random_string(N-1, [random:uniform(26)-1+$a|D]).
27+
random_seed() ->
28+
{_,_,X} = erlang:now(),
29+
{H,M,S} = time(),
30+
H1 = H * X rem 32767,
31+
M1 = M * X rem 32767,
32+
S1 = S * X rem 32767,
33+
put(random_seed, {H1,M1,S1}).

projects/appendix/src/lib_md5.erl

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
%% ---
2+
%% Excerpted from "Programming Erlang, Second Edition",
3+
%% published by The Pragmatic Bookshelf.
4+
%% Copyrights apply to this code. It may not be used to create training material,
5+
%% courses, books, articles, and the like. Contact us if you are in doubt.
6+
%% We make no guarantees that this code is fit for any purpose.
7+
%% Visit http://www.pragmaticprogrammer.com/titles/jaerlang2 for more book information.
8+
%%---
9+
-module(lib_md5).
10+
11+
%% modfied to use BIFs
12+
13+
-export([string/1, file/1, bin/1, binAsBin/1, digest2str/1]).
14+
15+
-define(BLOCKSIZE, 32768).
16+
-define(IN(X,Min,Max), X >= Min, X =< Max).
17+
18+
%% md5:string(string()) -> BinDigest
19+
%% md5:file(FileName) -> {ok, BinDigest} | {error, E}
20+
%% md5:digest2str(BinDigest) -> StringDigest
21+
22+
%% md5:file works with chunks so should work correctly with extremely
23+
%% large files
24+
25+
string(Str) -> digest2str(erlang:md5(Str)).
26+
27+
file(File) ->
28+
case file:open(File, [binary,raw,read]) of
29+
{ok, P} -> loop(P, erlang:md5_init());
30+
Error -> Error
31+
end.
32+
33+
loop(P, C) ->
34+
case file:read(P, ?BLOCKSIZE) of
35+
{ok, Bin} ->
36+
loop(P, erlang:md5_update(C, Bin));
37+
eof ->
38+
file:close(P),
39+
{ok, erlang:md5_final(C)}
40+
end.
41+
42+
digest2str(Digest) -> bin2str(binary_to_list(Digest)).
43+
44+
bin2str([H|T]) ->
45+
{H1, H2} = byte2hex(H),
46+
[H1,H2|bin2str(T)];
47+
bin2str([]) ->
48+
[].
49+
50+
byte2hex(X) ->
51+
{nibble2hex(X bsr 4), nibble2hex(X band 15)}.
52+
53+
nibble2hex(X) when ?IN(X, 0, 9) -> X + $0;
54+
nibble2hex(X) when ?IN(X, 10, 15) -> X - 10 + $a.
55+
56+
%% compute the md5 checksum of a binary
57+
bin(Bin) ->
58+
C1 = erlang:md5_init(),
59+
C2 = erlang:md5_update(C1, Bin),
60+
C3 = erlang:md5_final(C2),
61+
digest2str(C3).
62+
63+
binAsBin(Bin) ->
64+
C1 = erlang:md5_init(),
65+
C2 = erlang:md5_update(C1, Bin),
66+
erlang:md5_final(C2).
67+
68+
69+
70+
71+
72+
73+
74+
75+
76+
77+
78+
79+

src/appendix/ap02-a_socket_application.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,192 @@ true
109109
> 然后即可执行命令 `erl -boot start_sasl -config elog4 -smp +S 12 -pa _build/default/lib/math_server/ebin` 启动 Erlang shell,并在其下执行上面的命令。
110110

111111

112+
### 透过网络访问服务器
112113

113114

115+
我们可以一台机器上,测试此代码。
114116

115117

118+
```erlang
119+
2> {ok, S} = lib_chan:connect("localhost",2233,math,"qwerty",{yes,go}).
120+
{ok,<0.100.0>}
121+
mod_math: run starting
122+
ArgC = {yes,go} ArgS = []
123+
3> lib_chan:rpc(S, {factorial,20}).
124+
2432902008176640000
125+
4> lib_chan:rpc(S, {fibonacci,15}).
126+
610
127+
5> lib_chan:disconnect(S).
128+
close
129+
mod_math stopping
130+
```
131+
132+
## `lib_chan` 工作原理
133+
134+
135+
`lib_chan` 是使用四个模组中的代码构建的。
136+
137+
- `lib_chan` 充当 “主模组”。程序员需要了解的例程,仅限 `lib_chan` 中导出那些。其他三个模组(将在下文讨论)在 `lib_chan` 实现内部用到;
138+
- `lib_chan_mm` 会编码及解码 Erlang 消息,并管理套接字通信;
139+
- `lib_chan_cs` 会建立该服务器,并管理客户端连接。他的主要任务之一,是限制客户端同时连接的最大数量;
140+
- `lib_chan_auth` 包含着简单挑战/响应式验证的代码。
116141

117142

143+
### `lib_chan`
144+
145+
146+
`lib_chan` 有着如下结构:
147+
148+
149+
```erlang
150+
-module(lib_chan).
151+
152+
153+
start_server(ConfigFile) ->
154+
%% read configuration file - check syntax
155+
%% call start_port_server(Port, ConfigData)
156+
%% where Port is the required Port and ConfigData
157+
%% contains the configuration data
158+
159+
start_port_server(Port, ConfigData) ->
160+
lib_chan_cs:start_raw_server(Port,
161+
fun(Socket) ->
162+
start_port_instance(Socket, ConfigData) end,
163+
%% lib_chan_cs manages the connection
164+
%% when a new connection comes the fun which is an
165+
%% argument to start_raw_server will be called
166+
167+
168+
start_port_instance(Socket, ConfigData) ->
169+
%% this is spawned when the client connects
170+
%% to the server. Here we setup a middle man,
171+
%% then perform authentication. If everything works call
172+
%% really_start(MM, ArgC, {Mod, Func, ArgS})
173+
%% (the last three arguments come from the configuration file
174+
175+
176+
really_start(MM, ArgC, {Mod, Func, ArgS}) ->
177+
apply(Mod, Func, [MM, ArgC, ArgS]).
178+
179+
180+
connect(Host, Port, Service, Secret, ArgC) ->
181+
%% client side code
182+
```
183+
118184
### `lib_chan_mm`:中间人
185+
186+
`lib_chan_mm` 实现了一个中间人。他对应用隐藏了套接字通信,将 TCP 套接字上的数据流转换成 Erlang 消息。中间人负责组装消息(其可能已被分片),以及将 Erlang 项编码和解码为可在套接字上发送和接收的字节流。
187+
188+
189+
![中间人下的套接字通信](../images/lib_chan_mm.png)
190+
191+
<a name="fig-8"></a>
192+
**8** -- **中间人下的套接字通信**
193+
194+
195+
现在快速浏览一下 [图 8*中间人下的套接字通信](#fig-8) 的好时机,其展示了我们的中间人架构。当机器 `M1` 上的某个进程 `P1` 想要发送一条消息 `T` 到机器 `M2` 上的进程 `P2`时,他会执行 `MM1 !{send, T}`。`MM1` 充当 `P2` 的 *代理*。发送到 `MM1` 的任何消息,都会被编码并写入套接字,并被发送到 `MM2`。`MM2` 会解码他在套接字上接收到的任何东西,并将消息 `{chan, MM2, T}` 发送给 `P2`。
196+
197+
198+
在机器 `M1` 上,进程 `MM1` 充当了 `P2` 的代理,而在 `M2` 上,进程 `MM2` 则会充当 `P1` 的代理。
199+
200+
`MM1` 和 `MM2` 是两个中间人进程的 PID。中间人进程的代码,看起来像下面这样:
201+
202+
```erlang
203+
loop(Socket, Pid) ->
204+
receive
205+
{tcp, Socket, Bin} ->
206+
Pid ! {chan, self(), binary_to_term(Bin)},
207+
loop(Socket, Pid);
208+
{tcp_closed, Socket} ->
209+
Pid ! {chan_closed, self()};
210+
close ->
211+
gen_tcp:close(Socket);
212+
{send, T} ->
213+
gen_tcp:send(Socket, [term_to_binary(T)]),
214+
loop(Socket, Pid)
215+
end.
216+
```
217+
218+
这个循环被用作套接字数据世界,与 Erlang 消息传递世界的接口。咱们可在 [`lib_chan_mm`](#lib_chan_mm) 中,找到 `lib_chan_mm` 的完整代码。相比这里给出的代码,完整代码稍微复杂一些,但原理是一样的。唯一的区别在与,我们添加了一些用于跟踪消息的代码,以及一些接口例程。
219+
220+
221+
### `lib_chan_cs`
222+
223+
224+
`lib_chan_cs` 负责建立客户端与服务器的通信。他导出的两个重要例程如下:
225+
226+
227+
- `start_raw_server(Port, Max, Fun, PacketLength)`
228+
229+
这个例程会在 `Port` 上启动一个监听连接的监听器。最多 `Max` 个同时会话被允许。`Fun` 是一个 1 元函数;当某个连接启动时,`Fun(Socket)` 会被执行。套接字通信会假设一个长度为 `PacketLength` 的数据包。
230+
231+
232+
- `start_raw_client(Host, Port, PacketLength) -> {ok, Socket} | {error, Why}`
233+
234+
这个例程会尝试连接到某个以 `start_raw_server` 打开的端口。
235+
236+
237+
`lib_chan_cs` 的代码,遵循了 [并行服务器](../part-iv/Ch17-programming_with_sockets.md#parall_server) 中描述的模式,但此外他还会追踪同时打开连接的最大数量。这个小细节虽然在概念上很简单,却增加了二十多行看起来相当奇怪,捕获退出等的代码。像这样的代码一团糟,不过不用担心;他完成了他的工作,并对该模组的用户隐藏了复杂性。
238+
239+
### `lib_chan_auth`
240+
241+
242+
该模组实现了一种简单形式的挑战/响应认证。挑战/响应的认证,基于与服务名名字相关联的共享秘密概念。要演示其工作原理,我们将假定有项名为 `math`,有着共享秘密 `qwerty` 的服务。
243+
244+
当某个客户端打算使用服务 `math` 时,那么该客户端就必须向服务器证明,他们知道共享秘密。这个过程如下进行:
245+
246+
1. 客户端发送一次请求到服务器,表明他想要使用 `math` 服务;
247+
248+
2. 服务器会计算出一个随机字符串 `C` 并将其发送给客户端。这就是 *挑战*。该字符串由 `lib_chan_ auth:make_challenge()` 函数计算得出。我们可以交互式地使用他,看看他做了些什么。
249+
250+
```erlang
251+
1> lib_chan_auth:make_challenge().
252+
"ynsbrxyuavrknszkretvjvqao"
253+
```
254+
255+
3. 客户端收到这个字符串 (`C`) ,并计算出响应 (`R`),其中 `R = MD5(C ++ Secret)`。`R` 是使用 `lib_chan_auth:make_response` 计算出的。下面是个示例:
256+
257+
```erlang
258+
2> R = lib_chan_auth:make_response(C, "qwerty").
259+
"58565dbdf3328f7d9b9869a2c47eb699"
260+
```
261+
262+
4. 响应会被发回服务器。服务器收到该响应,并通过计算该响应的预期值,检查其是否正确。这是在 `lib_chan_auth:is_response_correct` 中完成的。
263+
264+
```erlang
265+
3> lib_chan_auth:is_response_correct(C, R, "qwerty").
266+
true
267+
```
268+
269+
## `lib_chan` 的代码
270+
271+
272+
现在是代码时间。
273+
274+
275+
### `lib_chan`
276+
277+
```erlang
278+
{{#include ../../projects/appendix/src/lib_chan.erl}}
279+
```
280+
281+
### `lib_chan_cs`
282+
283+
```erlang
284+
{{#include ../../projects/appendix/src/lib_chan_cs.erl}}
285+
```
286+
287+
288+
### `lib_chan_mm`
289+
290+
```erlang
291+
{{#include ../../projects/appendix/src/lib_chan_mm.erl}}
292+
```
293+
294+
295+
### `lib_chan_auth`
296+
297+
```erlang
298+
{{#include ../../projects/appendix/src/lib_chan_auth.erl}}
299+
```
300+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<comment version="3.0">
3+
<caption/>
4+
<note/>
5+
<place/>
6+
<categories/>
7+
</comment>

src/images/lib_chan_mm.png

55.8 KB
Loading

src/part-iv/Ch17-programming_with_sockets.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ ok
226226
Server socket closed
227227
```
228228

229-
### 顺序与并行服务器
229+
### 顺序服务器与并行服务器
230230

231231
在上一小节中,我们构造了个只接受一个连接并随后终止的服务器。稍微修改一下这些代码,我们就可以构造出两种不同类型的服务器。
232232

@@ -273,6 +273,7 @@ loop(..) -> %% as before
273273
我们只给出的是启动服务器的代码。停止服务器很简单(停止并行服务器也很简单);只要杀死启动服务器的进程。`gen_tcp` 会将自身链接到控制进程,并在控制进程死亡时,他就会关闭套接字。
274274

275275

276+
<a name="parall_server"></a>
276277
**并行服务器**
277278

278279
构造并行服务器的诀窍在于,当每次 `gen_tcp:accept` 获取一个新连接时,都立即生成一个新的进程。

0 commit comments

Comments
 (0)