TCP半开的几个测试,比较简单都在Erlang Shell中完成.立此存照,备忘.
gen_tcp提供了shutdown来实现这个功能,下面官方文档中提到了{exit_on_close,false}参数,如果要实现半开,无论是Read Write都要添加这个参数. 官网文档 Doc Ref
shutdown(Socket, How) -> ok | {error, Reason}
Types:
Socket = socket()
How = read | write | read_write
Reason = posix()
Immediately close a socket in one or two directions.
How == write means closing the socket for writing, reading from it is still possible.
To be able to handle that the peer has done a shutdown on the write side, the {exit_on_close, false} option is useful.
shutdown read
%% Server Eshell V5.9 (abort with ^G) 1> {ok,S0}=gen_tcp:listen(5678,[]). {ok,#Port<0.506>} 2> {ok,S2}=gen_tcp:accept(S0). {ok,#Port<0.512>} 3> flush(). Shell got {tcp,#Port<0.512>, [104,105,44,121,111,117,32,104,97,118,101,32,110,101,119,32, 109,115,103,32,0]} ok 4> gen_tcp:shutdown(S2,read). ok 5> flush(). Shell got {tcp_closed,#Port<0.512>} ok 6> gen_tcp:send(S2,"are you alive?"). {error,closed} 7> %% Client Eshell V5.9 (abort with ^G) 1> {ok,S1}= gen_tcp:connect("localhost",5678,[]). {ok,#Port<0.511>} 2> gen_tcp:send(S1,"hi,you have new msg \0"). ok 3> gen_tcp:send(S1,"hi,you have new msg \0"). %% 发送这条消息导致server端 接受到tcp_closed的消息 ok 4> gen_tcp:send(S1,"hi,you have new msg \0"). %% 服务器端宕掉之后再发送消息 Client也会出现{error,closed}错误 {error,closed}
Client添加一下选项 {exit_on_close, false} 试试
%% Server Eshell V5.9 (abort with ^G) 1> {ok,S0}=gen_tcp:listen(5678,[]). {ok,#Port<0.506>} 2> {ok,S2}=gen_tcp:accept(S0). {ok,#Port<0.512>} 3> gen_tcp:send(S2,"are you alive?"). ok 4> flush(). Shell got {tcp,#Port<0.512>, [104,105,44,121,111,117,32,104,97,118,101,32,110,101,119,32, 109,115,103,32,0]} ok 5> gen_tcp:shutdown(S2,read). ok 6> inet:getstat(S2). %%这时候检查一下Socket的状态 {ok,[{recv_oct,21}, {recv_cnt,1}, {recv_max,21}, {recv_avg,21}, {recv_dvi,0}, {send_oct,14}, {send_cnt,1}, {send_max,14}, {send_avg,14}, {send_pend,0}]} 7> gen_tcp:send(S2,"are you alive?===="). %% 由于server只关闭了read 所以发送消息还可以发送出去 ok 8> flush(). %% Client尝试发送消息 Shell got {tcp_closed,#Port<0.512>} ok 9> inet:getstat(S2). {error,einval} 10> %% Client Eshell V5.9 (abort with ^G) 1> {ok,S1} = gen_tcp:connect("localhost",5678,[{exit_on_close, false} ]). {ok,#Port<0.511>} 2> gen_tcp:send(S1,"hi,you have new msg \0"). ok 3> flush(). Shell got {tcp,#Port<0.511>,"are you alive?"} ok 4> flush(). %% 服务器端只关闭了read 还可以发送消息 这条消息还可以收到 Shell got {tcp,#Port<0.511>,"are you alive?===="} ok 5> gen_tcp:send(S1,"hi,you have new msg \0"). %% Client向Server发送消息 会导致Server收到tcp_closed消息 ok 6> gen_tcp:send(S1,"hi,you have new msg \0"). {error,closed} 7>
shutdown write
下面的测试关注点是shutdown write,过程和上面类似:
%%Server Eshell V5.9 (abort with ^G) 1> {ok,S0}=gen_tcp:listen(5678,[]). {ok,#Port<0.506>} 2> {ok,S2}=gen_tcp:accept(S0). {ok,#Port<0.512>} 3> flush(). Shell got {tcp,#Port<0.512>, [104,105,44,121,111,117,32,104,97,118,101,32,110,101,119,32, 109,115,103,32,0]} ok 4> gen_tcp:send(S2,"are you alive?"). ok 5> gen_tcp:shutdown(S2,write). %% 执行完这一句 检查Socket已经是einval ok 6> inet:getstat(S2). {error,einval} 7> %%Client Eshell V5.9 (abort with ^G) 1> {ok,S1}= gen_tcp:connect("localhost",5678,[]). {ok,#Port<0.511>} 2> gen_tcp:send(S1,"hi,you have new msg \0"). ok 3> flush(). Shell got {tcp,#Port<0.511>,"are you alive?"} ok 4> gen_tcp:send(S1,"hi,you have new msg \0"). {error,closed} 5>
调整实验 添加
exit_on_close选项
%%Server Eshell V5.9 (abort with ^G) 1> {ok,S0}=gen_tcp:listen(5678,[]). {ok,#Port<0.506>} 2> {ok,S2}=gen_tcp:accept(S0). {ok,#Port<0.512>} 3> flush(). Shell got {tcp,#Port<0.512>, [104,105,44,121,111,117,32,104,97,118,101,32,110,101,119,32, 109,115,103,32,0]} ok 4> gen_tcp:send(S2,"are you alive?"). ok 5> gen_tcp:shutdown(S2,write). ok 6> inet:getstat(S2). %% 对比上面的测试结果 这里是正常的状态 {ok,[{recv_oct,21}, {recv_cnt,1}, {recv_max,21}, {recv_avg,21}, {recv_dvi,0}, {send_oct,14}, {send_cnt,1}, {send_max,14}, {send_avg,14}, {send_pend,0}]} 7> flush(). %% 由于只关闭了写,读还是正常的,接受到的消息 Shell got {tcp,#Port<0.512>, [104,105,44,121,111,117,32,104,97,118,101,32,110,101,119,32, 109,115,103,32,50,32,0]} ok 8> gen_tcp:send(S2,"are you alive?"). %% 关闭了写 这里的调用就会报错了 {error,closed} 9> inet:getstat(S2). {error,einval} 10> %% Client Eshell V5.9 (abort with ^G) 1> {ok,S1} = gen_tcp:connect("localhost",5678,[{exit_on_close, false} ]). {ok,#Port<0.511>} 2> gen_tcp:send(S1,"hi,you have new msg \0"). ok 3> flush(). Shell got {tcp,#Port<0.511>,"are you alive?"} ok 4> gen_tcp:send(S1,"hi,you have new msg 2 \0"). ok 5>
gen_tcp shutdown
看看gen_tcp 的shutdown的逻辑:
shutdown(S, How) when is_port(S) -> case inet_db:lookup_socket(S) of {ok, Mod} -> Mod:shutdown(S, How); Error -> Error end.
inet_db:lookup_socket(S) 的结果是什么?在Shell里面测试一下:
Eshell V5.9 (abort with ^G) 1> {ok,S0}=gen_tcp:listen(5678,[]). {ok,#Port<0.506>} 2> inet_db:lookup_socket(S0). {ok,inet_tcp} 3>
|--> inet_tcp
%% %% Shutdown one end of a socket %% shutdown(Socket, How) -> prim_inet:shutdown(Socket, How).
|--> prim_inet.erl
shutdown(S, read) when is_port(S) -> shutdown_2(S, 0); shutdown(S, write) when is_port(S) -> shutdown_1(S, 1); shutdown(S, read_write) when is_port(S) -> shutdown_1(S, 2). shutdown_1(S, How) -> case subscribe(S, [subs_empty_out_q]) of {ok,[{subs_empty_out_q,N}]} when N > 0 -> shutdown_pend_loop(S, N); %% wait for pending output to be sent _Other -> ok end, shutdown_2(S, How). shutdown_2(S, How) -> case ctl_cmd(S, ?TCP_REQ_SHUTDOWN, [How]) of {ok, []} -> ok; {error,_}=Error -> Error end. shutdown_pend_loop(S, N0) -> receive {empty_out_q,S} -> ok after ?INET_CLOSE_TIMEOUT -> case getstat(S, [send_pend]) of {ok,[{send_pend,N0}]} -> ok; {ok,[{send_pend,N}]} -> shutdown_pend_loop(S, N); _ -> ok end end.
上面处理考虑的相当周全,如果shutdown的时候有消息还没有发送完成,就会先完成暂存数据的发送,霸爷有一篇很详细的分析: gen_tcp调用进程收到{empty_out_q, Port}消息奇怪行为分析
最后小图一张 Natalie Portman :