• Erlang 入门——从普通tcp到OTP框架通信


    根据Erlang的语言特点,Erlang创建进程就如同Java创建对象那样简单。而Erlang的OTP框架,可以理解为是Java的Spring框架。

    刚入门Erlang的tcp通信,书上的写法是根据socket用gen_tcp:send和receive通信,到了OTP里用gen_server也是一样的原理,只不过在OTP框架下gen_server行为模式封装了一些方法使得写法更方便。

    首先是一般的写法。server端监听来自client的tcp连接。

    %% server.erl
    start(Port) ->
    	{ok, LSocket} = gen_tcp:listen(Port, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]),
    	do_accept(LSocket).
    	
    do_accept(LSocket) ->
    	{ok, Socket} = gen_tcp:accept(LSocket),
    	io:format("Socket ~p connnected. ~n", [Socket]).
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    %% client.erl
    start(Port) ->
    	{ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {packet, 4}]).
    
    • 1
    • 2
    • 3

    连接后生成的socket用于后续的交互通信,服务端端通常写个loop函数,监听来自客户端请求。而这个loop函数应该给它新建一进程,使其不影响服务端的tcp监听,通常这么写。

    %% server.erl
    	Pid = spawn(fun() -> loop(Socket) end),
    	gen_tcp:controlling_process(Socket, Pid).
    
    • 1
    • 2
    • 3

    gen_tcp:controlling_process的作用,我的理解是给loop函数创建一进程后,将这个进程号和Socket绑定,作用就是将tcp消息转换成进程消息,使其能通过{tcp, Socket, Bin}自动匹配接收。

    整个demo如下:

    -module(server).
    
    -export([start/1]).
    
    start(Port) ->
    	{ok, LSocket} = gen_tcp:listen(Port, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]),
    	do_accept(LSocket).
    	
    do_accept(LSocket) ->
    	{ok, Socket} = gen_tcp:accept(LSocket),
    	io:format("Socket ~p connnected. ~n", [Socket]),
    	Pid = spawn(fun() -> loop(Socket) end),
    	gen_tcp:controlling_process(Socket, Pid),
    	loop(Socket).
    	
    loop(Socket) ->
    	receive
    		{tcp, Socket, Bin} ->
    			Str = binary_to_term(Bin),
    			io:format("Server get the msg : ~p ~n", [Str]),
    			gen_tcp:send(Socket, term_to_binary(hi)),
    			loop(Socket);
    		{tcp_closed, Socket} ->
    			io:format("Socket ~p disconnected ~n", [Socket])
    	end.	
    
    • 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
    -module(client).
    -export([start/1, send/1]).
    
    start(Port) ->
    	ets:new(tcpname, [set, public, named_table]),
    	{ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {packet, 4}]),
    	ets:insert(tcpname, {socket, Socket}),
    	Pid = spawn(fun() -> loop(Socket) end),
    	gen_tcp:controlling_process(Socket, Pid).
    	
    send(Str) ->
    	Socket = ets:lookup_element(tcpname, socket, 2),
    	gen_tcp:send(Socket, term_to_binary(Str)).
    	
    loop(Socket) ->
    	receive
    		{tcp, Socket, Bin} ->
    			Str = binary_to_term(Bin),
    			io:format("Client get the msg : ~p ~n", [Str]),
    			loop(Socket)
    	end.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    效果如下:
    服务端:
    在这里插入图片描述
    客户端:
    在这里插入图片描述

    在OTP框架中,gen_server做了封装,新建进程不用spawn而是用gen_server:start_link,而接收来自tcp的消息用handle_info加字段匹配进行接收即可。

    根据gen_server:start_link的写法,第二个参数为Module,即在gen_server里新建的进程将是一个新的模块(新的erl文件)。

    %% server.erl
    	%%Pid = spawn(fun() -> handle_clients(Socket) end),
    	{ok, PidA} = gen_server:start_link(server_recv, Socket, []),
    
    • 1
    • 2
    • 3

    这么做的结果就是,server_recv将会接收来自client的tcp消息,并且不用receive,直接用handle_info即可接收,直接用字段匹配接收tcp消息将使得开发效率得到很大的提升。

    %% server_recv.erl
    handle_info({tcp, Socket, Bin}, State) ->
    	Msg = binary_to_term(Bin),
    	io:format("~p ~n", [Msg]),
    	{noreply, State};
    
    • 1
    • 2
    • 3
    • 4
    • 5

    而在gen_server里,State用来保存服务器的状态,那么你可以定义一个record,将tcp连接生成的Socket保存到State里,那么在整个服务器所有地方(handle_call、handle_cast、handle_info)里都能取出来用。

    (客户端将Socket保存到状态后,在需要发送消息时可从状态里取出来用)

    %% client.erl
    -record(state, {socket}).
    
    ...
    
    init(Port) ->
      {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {packet, 4}]),
      {ok, #state{socket = LSocket}}.
    
    ...
    
    handle_cast({login, Data}, State) ->
      Socket = State#state.socket,
      gen_tcp:send(Socket, term_to_binary(Data)),
      {noreply, State};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    完整demo如下:
    ① 服务端server:

    -module(server).
    -behaviour(gen_server).
    -export([start/1]).
    
    -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
    -define(SERVER, ?MODULE).
    -define(TCP_OPTIONS, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]).
    
    -record(state, {socket}).
    
    start(Port) ->
    	gen_server:start_link({local, ?SERVER}, ?MODULE, Port, []).
    	
    init(Port) ->
    	{ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
    	self() ! {to_accept, LSocket},
    	{ok, #state{socket = LSocket}}.
    	
    do_accept(LSocket) ->
    	{ok, Socket} = gen_tcp:accept(LSocket),
    	io:fwrite("Socket connected: ~w ~n", [Socket]),
    	%%Pid = spawn(fun() -> handle_clients(Socket) end),
    	{ok, Pid} = gen_server:start_link(server_recv, Socket, []),
    	gen_tcp:controlling_process(Socket, Pid),
    	do_accept(LSocket).
    	
    handle_info({to_accept, LSocket}, State) ->
    	do_accept(LSocket),
    	{noreply, State}.
    	
    handle_call(stop, _From, Tab) ->
    	{stop, normal, stopped, Tab}.
    	
    handle_cast(_Msg, State) ->	
    	{noreply, State}.
    	
    terminate(_Reason, _State) ->
    	ok.
    code_change(_OldVsn, State, _Extra) ->
    	{ok, State}.
    
    
    • 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

    ② 接收客户端消息的新进程server_recv:

    -module(server_recv).
    -behaviour(gen_server).
    
    -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
    -define(SERVER, ?MODULE).
    -define(TCP_OPTIONS, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]).
    
    -record(state, {socket}).
    
    init(Socket) ->
    	{ok, #state{socket = Socket}}.
    
    handle_info({hi, Data}, State) ->
    	Socket = State#state.socket,
    	gen_tcp:send(Socket, term_to_binary(Data)),
    	{noreply, State};
    
    handle_info({tcp, Socket, Bin}, State) ->
    	Msg = binary_to_term(Bin),
    	io:format("server get the msg : ~p ~n", [Msg]),
    	Msg1 = hi,
    	self() ! {hi, Msg1},	
    	{noreply, State};
    	
    handle_info({tcp_closed, Socket}, State) ->
    	io:fwrite("Socket disconnected: ~w ~n", [Socket]),
    	{noreply, State}.
    	
    handle_call(stop, _From, Tab) ->
    	{stop, normal, stopped, Tab}.
    
    handle_cast(_Msg, State) ->
    	{noreply, State}.
    
    terminate(_Reason, _State) ->
    	ok.
    code_change(_OldVsn, State, _Extra) ->
    	{ok, State}.
    
    
    • 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

    ③ 客户端client:

    -module(client).
    -behaviour(gen_server).
    -export([start/1, hello/0]).
    
    -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
    -define(SERVER, ?MODULE).
    
    -record(state, {socket}).
    
    start(Port) ->
    	gen_server:start_link({local, ?SERVER}, ?MODULE, Port, []).
    	
    init(Port) ->
    	{ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {packet, 4}]),
    	{ok, #state{socket = Socket}}.
    	
    hello() ->
    	Data = hello,
    	gen_server:cast(?MODULE, {hello, Data}).
    
    handle_info({tcp, Socket, Bin}, State) ->
    	Msg = binary_to_term(Bin),
    	io:format("client get the msg : ~p ~n", [Msg]),
    	{noreply, State};
    	
    handle_info({tcp_closed, Socket}, State) ->
    	io:fwrite("Socket disconnected: ~w ~n", [Socket]),
    	{noreply, State}.
    
    handle_cast({hello, Data}, State) ->
    	Socket = State#state.socket,
    	gen_tcp:send(Socket, term_to_binary(Data)),
    	{noreply, State}.
    
    handle_call(stop, _From, State) ->
    	{stop, normal, stopped, State}.
    
    terminate(_Reason, _State) ->
    	ok.
    code_change(_OldVsn, State, Extra) ->
    	{ok, State}.
    
    • 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

    小白入门分享,如有错误,欢迎指正。如有帮助,欢迎点赞收藏~

  • 相关阅读:
    (vue)el-descriptions 描述列表无效
    Cesium-动态绘制面
    基于PYTHON游乐场服务管理系统的设计与实现
    如何使ssh操作linux 更安全
    [附源码]SSM计算机毕业设计基于健身房管理系统JAVA
    【实例分享】访问后端服务超时,银河麒麟服务器操作系统分析及处理建议
    苹果笔记本电脑可以玩steam游戏吗 MacBook支持玩steam游戏吗 在Steam上玩黑神话悟空3A大作 苹果Mac怎么下载steam
    ADS-B显示软件
    Docker 安装zookeeper
    中介者模式
  • 原文地址:https://blog.csdn.net/weixin_42549874/article/details/126334996