Erlang03_套接字编程(1)TCP

总篇:11

编辑于 2025/5/7 20:30

截稿于: 2025/5/7 22:12

上一篇“基于套接字的分布式模式”中直接使用了套接字,本片将详细介绍套接字编程。

简介

Erlang的套接字编程主要通过gen_tcp和gen_udp模块实现,支持TCP和UDP两种协议。

1. Socket是Port类型
    is_port(Socket) =:= true
2. 每个Socket一定不同
    Socket1 =/= Socket2
3. Socket只能在创建它的节点上使用,分布式下不能A节点无法使用B节点的Socket,
4. 如果拥有Socket的进程终止,Socket会自动关闭
5. Socket通常作为参数在gen_tcp模块函数中使用

以下主要介绍TCP套接字编程。

原语

以下原语来自gen_tcp模块

  1. {ok, ListenSocket} = gen_tcp:listen(Port,Options):开启一个监听Port端口的服务
Options =[
    binary,             % 以二进制形式处理数据
    {packet, 0},        % 原始数据模式,不自动处理包长度
    {active, true},     % 主动模式,自动将数据转为消息发送给进程
    {reuseaddr, true}   % 允许重用地址,快速重启服务器
],
{ok, ListenSocket} = gen_tcp:listen(Port,Options) %ListenSocket,称为监听Socket

在Socket中,服务器和客户端以binary格式数据传输,因此需要手动处理输入输出数据
  1. {ok, Socket} = gen_tcp:accept(ListenSocket):服务器等待接收一个客户端Socket连接
{ok, Socket} = gen_tcp:accept(ListenSocket)  % 返回一个通信Socket用于与客户端通信,
% 一个Socket对应一条服务器-客户端的连接,可以有多个客户端而只有一个服务器
  1. {ok, Data} = gen_tcp:recv(Socket, Length) :服务器接收客户端/客户端接受服务器发送的消息
{ok, Data} = gen_tcp:recv(Socket, Length)    % Length为0时接收所有可用数据
                                            % Length为N时接收N字节数据
  1. ok = gen_tcp:send(Socket, Data) :客户端向服务器/服务器向客户端发送数据
ok = gen_tcp:send(Socket, Data) % Data:binary/string
  1. {ok, Socket} = gen_tcp:connect(Host, Port,Options): 客户端连接服务器
Options= [
    binary,             % 以二进制形式处理数据
    {packet, 0},        % 原始数据模式
    {active, true}      % 主动模式
],
{ok, Socket} = gen_tcp:connect(Host, Port,Options)
  1. ok = gen_tcp:close(Socket) :服务器关闭监听Socket,若该Socket是监听Socket则服务器也无法accept,若该Socket是通信Socket则无法send。
ok = gen_tcp:close(Socket)                   

简单的服务器、客户端

本例为一个一次性的服务器,客户端,客户端在连接向服务器后发送依次消息接收到回复后就断开连接。

-module(socket_example).
-export([start_nano_server/0, nano_client_eval/1]).

start_nano_server() ->
    % 创建监听套接字,端口2345
    {ok, Listen} = gen_tcp:listen(2345, [
        % 二进制数据模式
        binary,
        % 4字节包长度头
        {packet, 4},
        % 允许重用地址
        {reuseaddr, true},
        % 主动模式
        {active, true}
    ]),

    % 接受一个连接
    {ok, Socket} = gen_tcp:accept(Listen),

    % 关闭监听套接字
    gen_tcp:close(Listen),

    % 进入循环处理
    loop(Socket).

loop(Socket) ->
    receive
        {tcp, Socket, Bin} ->
            % 接收到的二进制数据
            io:format("~nServer received binary = ~p~n", [Bin]),

            % 将二进制数据转换为原数据打印
            Data = binary_to_term(Bin),
            io:format("~n Server (unpacked) ~p~n", [Data]),

            % 生成回复数据
            Reply = io_lib:format("Server received data: ~p", [Data]),
            io:format("~nServer replying = ~p~n", [Reply]),

            % 发送回复
            gen_tcp:send(Socket, term_to_binary(Reply)),
            loop(Socket);
        {tcp_closed, Socket} ->
            io:format("~nServer socket closed~n")
    end.

nano_client_eval(Data) ->
    % 连接到服务器
    {ok, Socket} = gen_tcp:connect("localhost", 2345, [
        binary,
        {packet, 4}
    ]),

    % 发送数据
    ok = gen_tcp:send(Socket, term_to_binary(Data)),

    % 接收响应
    receive
        {tcp, Socket, Bin} ->
            io:format("Client received binary = ~p~n", [Bin]),
            Reply = binary_to_term(Bin),
            io:format("Client result = ~p~n", [Reply]),
            % 关闭套接字
            gen_tcp:close(Socket)
    end.

在两个erl终端中,先启动服务器:

再启动客户端:

服务端接受到第一个客户端连接后便关闭监听Socket,此后无法接受其他客户端连接,

客户端向服务器发送“hello”得到回复打印后,关闭通信Socket。此后必须重新启动服务器才能继续让客户端连接。下面改造这个简单服务器,让其变得不是一次性的。

顺序服务器

将以上服务器逻辑改造为:监听Socket不断开,不停的接受客户端连接,客户端连接上并发送消息后关闭通信连接,然后可以重新连接获得新的通信连接。

-module(socket_example2).
-export([start_nano_server/0, nano_client_eval/1]).

start_nano_server() ->
    % 创建监听套接字,端口2345
    {ok, Listen} = gen_tcp:listen(2345, [
        % 二进制数据模式
        binary,
        % 4字节包长度头
        {packet, 4},
        % 允许重用地址
        {reuseaddr, true},
        % 主动模式
        {active, true}
    ]),
    set_loop(Listen).

% 得到一个连接,将其放入loop,后继续监听下一个连接
set_loop(Listen) ->
    % 接受一个连接
    {ok, Socket} = gen_tcp:accept(Listen),
    % 进入循环处理
    loop(Socket),
    set_loop(Listen).

loop(Socket) ->
    receive
        {tcp, Socket, Bin} ->
            % 接收到的二进制数据
            io:format("~nServer received binary = ~p~n", [Bin]),

            % 将二进制数据转换为原数据打印
            Data = binary_to_term(Bin),
            io:format("~n Server (unpacked) ~p~n", [Data]),

            % 生成回复数据
            Reply = io_lib:format("Server received data: ~p", [Data]),
            io:format("~nServer replying = ~p~n", [Reply]),

            % 发送回复
            gen_tcp:send(Socket, term_to_binary(Reply)),
            loop(Socket);
        {tcp_closed, Socket} ->
            io:format("~nServer socket closed~n")
    end.

nano_client_eval(Data) ->
    % 连接到服务器
    {ok, Socket} = gen_tcp:connect("localhost", 2345, [
        binary,
        {packet, 4}
    ]),

    % 发送数据
    ok = gen_tcp:send(Socket, term_to_binary(Data)),

    % 接收响应
    receive
        {tcp, Socket, Bin} ->
            io:format("Client received binary = ~p~n", [Bin]),
            Reply = binary_to_term(Bin),
            io:format("Client result = ~p~n", [Reply]),
            % 关闭套接字
            gen_tcp:close(Socket)
    end.

服务器持续监听:

顺序两次连接服务器发送不同的消息

并行服务器

感觉这个才是真正使用场景,将监听Socket部分在新的进程运行,这样监听不会阻塞服务器进程, 在监听到客户端连接后新开一个监听进程,这样可以保证多个客户端连接时不必排队等待。

需要注意控制进程创建深度数量和生命周期,避免死进程和大量迸发进程超出服务器荷载。

-module(socket_example3).
-export([start_parallel_server/0, nano_client_eval/1]).

% 启动并行服务器
start_parallel_server() ->
    % 创建监听套接字,端口2345
    {ok, Listen} = gen_tcp:listen(2345, [
        binary,             % 二进制数据模式
        {packet, 4},        % 4字节包长度头
        {reuseaddr, true},  % 允许重用地址
        {active, true}      % 主动模式
    ]),
    spawn(fun() -> par_connect(Listen) end).

% 并行处理连接
par_connect(Listen) ->
    {ok, Socket} = gen_tcp:accept(Listen),
    spawn(fun() -> par_connect(Listen) end),
    loop(Socket).

loop(Socket) ->
    receive
        {tcp, Socket, Bin} ->
            % 接收到的二进制数据
            io:format("~nServer received binary = ~p~n", [Bin]),

            % 将二进制数据转换为原数据打印
            Data = binary_to_term(Bin),
            io:format("~n Server (unpacked) ~p~n", [Data]),

            % 生成回复数据
            Reply = io_lib:format("Server received data: ~p", [Data]),
            io:format("~nServer replying = ~p~n", [Reply]),

            % 发送回复
            gen_tcp:send(Socket, term_to_binary(Reply)),
            loop(Socket);
        {tcp_closed, Socket} ->
            io:format("~nServer socket closed~n")
    end.

nano_client_eval(Data) ->
    % 连接到服务器
    {ok, Socket} = gen_tcp:connect("localhost", 2345, [
        binary,
        {packet, 4}
    ]),

    % 发送数据
    ok = gen_tcp:send(Socket, term_to_binary(Data)),

    % 接收响应
    receive
        {tcp, Socket, Bin} ->
            io:format("Client received binary = ~p~n", [Bin]),
            Reply = binary_to_term(Bin),
            io:format("Client result = ~p~n", [Reply]),
            % 关闭套接字
            gen_tcp:close(Socket)
    end.

服务器在开启监听Socket后还可以做其他事情,而不用阻塞等待客户端连接

客户端保持原样,虽然单机无法测试多客户端同时连接,单明白就是那么回事就行。

以上,基于gen_tcp模块开发了三个不同类型的套接字服务器。

posted on 2025-05-07 22:13  依只  阅读(18)  评论(0)    收藏  举报

导航