Erlang TCP Socket
- Learning about TCP socket services
- Erlang TCP service development overview
Resources
- Building a TCP server based on Erlang OTP
- ranch
- Ranch code walkthrough
- Introduction
- Reading the code documentation
- Enter_loop method handling
- ranch Notes
- A wheel written for ranch; see its design differences.
- Introduction
- esockd
- gen_socket
- gen_socket is an Erlang socket interface that can be used to create gen_tcp and gen_udp based sockets with special properties.
- Existing use cases are:
- gen_udp socket that talks to the Linux kernel via netlink
- gen_tcp socket that is a Unix domain socket
- Building A Non-Blocking TCP Server Using OTP Principles
- The child process uses gen_fsm, which cleverly combines state machines and message events and is worth learning.
Comparison
gen_tcp:accept/2 vs. prim_inet:async_accept/2
- prim_inet:async_accept/2 is an asynchronous accept
- gen_tcp:accept/2 is a synchronous accept
- Asynchronous accept is fast, but prim_inet:async_accept/2 is undocumented and subject to variability.
- Using asynchronous accept to handle TCP connections allows you to control the system’s concurrent processing capabilities by controlling the number of accept processes.
- Ranch uses a synchronous approach, while other socket programming styles often use guided non-blocking accept.
Ranch Overview
- ranch.erl – API, start and stop interfaces
- ranch_server.erl – gen_server process, mainly manages configuration, data mapping, and process mapping. Ranch products only have one ranch_server process, and content uses ETS to store data.
- ranch_listener_sup.erl – One listener sup for each port
- ranch_conns_sup.erl – Creates a message processing process
- ranch_protocol.erl – Implements the message processing process described in ranch_protocol.erl
- N
- ranch_acceptors_sup – Connection process supervisor
- ranch_acceptor.erl – Socket port accept process, which handles port connections
- N
- Port connection request processing flow:
client –> Initiates a socket port connection –> ranch_acceptor.erl –> ranch_conns_sup.erl –> ranch_protocol.erl
- This process involves two transfers of control over the socket port.
- From accept to ranch_conns_sup.erl
- From ranch_conns_sup.erl to the specific message processing process, ranch_protocol.erl
- When a port connection request is received, the message processing process [ranch_protocol.erl] is directly created. It then retrieves data using gen_tcp:recv(Socket, Length, Timeout). and determines whether the connection is valid based on the data.
- ranch_conns_sup.erl is responsible for starting the message processing process and managing the number of connections. If there is a single point of failure, i.e., a high level of concurrent connections, the system’s port connection establishment speed will be affected.
- Process Types:
- ranch_acceptors_sup is the supervisor process.
- ranch_acceptor.erl A general process supervised by the ranch_acceptors_sup process.
- ranch_conns_sup.erl: Does not implement a supervisory process.
- ranch_protocol.erl: Message processing process, no specified process type.
TCP Server
- Multiple port services can also be started, but the design of tcp_listener is less rational. The gen_server only listens on a port, essentially opening it, without requiring a dedicated process.
- tcp_listener.erl
gen_tcp:listen(Port, SockOpts ++ [{active, false}])
- Suggested change to:
gen_tcp:listen(Port, [{active, false} | proplists:delete(active, SockOpts)])
TCP Socket
- {active, true}
- The default setting of gen_tcp is {active, true}, which means that when gen_tcp receives a network packet, it sends the packet to its host process by default. gen_tcp:recv is a user-initiated mode for receiving data. These two modes are mutually exclusive.
The default listening TCP port service uses {active,false}, a blocking mode.
{active,true} has security issues, and {active,once} is too slow. We set {active,N} to receive N packets at a time, spreading the cost of epoll_ctl and significantly reducing performance pressure.
{exit_on_close,false}
Set inets:setopts(Socket, [{exit_on_close,false}]). This prevents forced exits.
When the TCP peer calls shutdown (RD/WR), the host process will receive a {tcp_closed,Socket} message by default.
{delay_send,true}
The delay_send option prevents immediate data transmission. Instead, data is temporarily stored in the driver’s send queue until it is available for writing. This allows for batched data transmission and improves efficiency.
inet:getstat(Socket, [send_pend]) Get the current send buffer length.
If there is still data in your receive buffer, the protocol stack will send RST instead of FIN.
{ok,Sock} = gen_tcp:connect("baidu.com", 80, [{active,false}]).
gen_tcp:send(Sock, "GET / HTTP/1.1\r\n\r\n").
gen_tcp:close(Sock).
or
{ok,Sock} = gen_tcp:connect("baidu.com", 80, [{active,false}]).
gen_tcp:send(Sock, "GET / HTTP/1.1\r\n\r\n").
gen_tcp:recv(Sock,0).
gen_tcp:close(Sock).
- Port busy can be set.
- This is undocumented; see below for usage:
inet:setopts(Socket, [{high_watermark, 131072}]).
inet:setopts(Socket, [{low_watermark, 65536}]).
Each socket in inet_drv has a message queue that stores messages pushed from the upper layer. This message queue has upper and lower watermarks. When the number of bytes in a message exceeds the high watermark, inet_drv marks the socket as busy. This busy status is not released until the number of bytes in the queue falls below the low watermark.
{packet, raw | 0 | 1 | 2 | 4}
TCP is a stream protocol, so when sending messages, we usually need to add a header to the front of the message, such as a 4-byte length. Manually doing this is very inefficient. tcp_driver supports automatically adding the message length to handle this. > - It is recommended to use {packet,4} for processing, which guarantees the message data length.
Summary
- Use prim_inet:async_accept/2 for asynchronous accept
- {active,false}
- {delay_send, true}
- {exit_on_close, false}
- After accept receives a product connection request, it directly creates a process to handle it.
proc_lib:start_link/3 %% starts the gen_server process
ok = proc_lib:init_ack({ok, self()}), %% returns, handles controlling_process
gen_server:enter_loop/3 %% enters the gen_server loop
Note:
- Disadvantages: A process is created as soon as a connection is made, resulting in the overhead and management of process creation caused by invalid connections.
- Advantages: Improves the ability to handle parallel connections, meaning that simultaneous requests exceeding the number of accept processes can be easily handled.