diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1128021..9d3cc96 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,10 +1,39 @@ -FROM asciidoctor/docker-asciidoctor:latest +FROM erlang:27 -RUN apk add --no-cache\ - git\ - rsync\ - erlang +ENV DEBIAN_FRONTEND=noninteractive -RUN git config --global --add safe.directory /documents -WORKDIR /documents -CMD ["make"] +# Install dependencies +RUN apt-get update && apt-get install -y \ + git \ + gcc \ + rsync \ + postgresql \ + postgresql-contrib \ + wget \ + curl \ + make \ + build-essential \ + ruby \ + ruby-dev \ + asciidoctor \ + && apt-get clean + +# Install rebar3 +RUN git clone https://github.com/erlang/rebar3.git && \ + cd rebar3 && \ + ./bootstrap && \ + ./rebar3 local install + +# Set environment variable for Erlang shell history +ENV ERL_AFLAGS "-kernel shell_history enabled" + +# Initialize PostgreSQL database for examples +USER postgres +RUN /etc/init.d/postgresql start && \ + psql --command "CREATE USER myuser WITH SUPERUSER PASSWORD 'mypassword';" && \ + createdb -O myuser mydb + +USER root + +# Set the default command to start PostgreSQL and keep the container running +CMD service postgresql start && tail -f /var/lib/postgresql/data/logfile \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 18994f9..422704e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,19 +1,30 @@ { - "name": "BeamBook Dev Container", - "dockerFile": "Dockerfile", - - "customizations": { - "vscode": { - "extensions": [ - "ms-vscode.cpptools", - "ms-python.python", - "rebornix.Ruby" - ] - }, - "settings": { - "terminal.integrated.shell.linux": "/bin/bash" - } + "name": "BeamBook Dev Container", + "dockerFile": "Dockerfile", + "customizations": { + "vscode": { + "extensions": [ + "asciidoctor.asciidoctor-vscode", + "github.copilot", + "github.copilot-chat", + "ms-azuretools.vscode-docker", + "ms-python.debugpy", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-vscode.cmake-tools", + "ms-vscode.cpptools", + "ms-vscode.cpptools-extension-pack", + "ms-vscode.cpptools-themes", + "ms-vscode.makefile-tools", + "pgourlain.erlang", + "rebornix.ruby", + "twxs.cmake", + "wingrunr21.vscode-ruby", + ] }, - "postCreateCommand": ". /workspace/venv/bin/activate && pip install -r requirements.txt" -} - \ No newline at end of file + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + } + }, + "postCreateCommand": ". /workspace/venv/bin/activate && pip install -r requirements.txt" +} \ No newline at end of file diff --git a/chapters/c.asciidoc b/chapters/c.asciidoc index 50127af..a34b9f3 100644 --- a/chapters/c.asciidoc +++ b/chapters/c.asciidoc @@ -6,7 +6,7 @@ === Introduction Interfacing C, C++, Ruby, or assembler provides an opportunity to extend the capabilities of the BEAM. In this chapter, we will use C for most examples, but the methods described can be used to interface almost any other programming language. We will give some examples of Ruby and Java in this chapter. In most cases, you can replace C with any other language in the rest of this chapter, but we will just use C for brevity. -By integrating C code, developers can enhance their Erlang applications' performance, especially for computationally intensive tasks requiring direct access to system-level resources. Additionally, interfacing with C allows Erlang applications to interact directly with hardware and system-level resources. This capability is crucial for applications that require low-level operations, such as manipulating memory, accessing specialized hardware, or performing real-time data processing. Another advantage of integrating C with Erlang is using existing C libraries and codebases. Many powerful libraries and tools are available in C, and by interfacing with them, Erlang developers can incorporate these functionalities without having to reimplement them in Erlang. +By integrating C code, developers can enhance their Erlang applications' performance, especially for computationally intensive tasks requiring direct access to system-level resources. Additionally, interfacing with C allows Erlang applications to interact directly with hardware and system-level resources. This capability is crucial for applications that require low-level operations, such as manipulating memory, accessing specialized hardware, or performing real-time data processing. Another advantage of integrating C with Erlang is using existing C libraries and codebases. Many powerful libraries and tools are available in C, and by interfacing with them, Erlang developers can incorporate these functionalities without having to reimplement them in Erlang. Furthermore, interfacing with C can help when precise control over execution is necessary. While Erlang's virtual machine provides excellent concurrency management, certain real-time applications may require more deterministic behavior that can be better achieved with C. By integrating C code, developers can fine-tune the performance and behavior of their applications to meet specific requirements. @@ -15,10 +15,54 @@ C code can also extend ERTS and BEAM since they are written in C. In previous chapters, we have seen how you can safely interface other applications and services over sockets or ports. This chapter will look at ways to interface low-level code more directly, which also means using it more unsafely. +The official documentation contains a tutorial on interoperability, see link:https://www.erlang.org/doc/tutorial/introduction[Interoperability Tutorial]. + ==== Safe Ways of Interfacing C Code Interfacing C code with Erlang can be done safely using several mechanisms that minimize the risk of destabilizing the BEAM virtual machine. Here are the primary methods. +===== os:cmd +The `os:cmd` function allows Erlang processes to execute shell commands and retrieve their output. This method is safe because it runs the command in a separate OS process, isolating it from the BEAM VM. By using `os:cmd`, developers can interact with external C programs without directly affecting the Erlang runtime environment. It comes with an overhead and the C program is expected to be a standalone program that can be run from the command line and return the result on standard output. + +Example: +```C +// system_time.c +#include +#include + +void get_system_time() +{ + time_t rawtime; + struct tm *timeinfo; + + time(&rawtime); + timeinfo = localtime(&rawtime); + + printf("Current Date and Time: %s", asctime(timeinfo)); +} + +int main() +{ + get_system_time(); + return 0; +} +``` + +```erlang +> os:cmd("./time"). +"Current Date and Time: Mon May 20 04:46:37 2024\n" +``` + +===== 'open_port' 'spawn_executable' +An even safer way to interact with a program, especially when arguments are based on user input, is to use open_port with the spawn_executable argument. This method mitigates the risk of argument injection by passing the arguments directly to the executable without involving an operating system shell. This direct passing prevents the shell from interpreting the arguments, thus avoiding potential injection attacks that could arise from special characters or commands in the user input. + +```erlang +1> Port = open_port({spawn_executable, "./time"}, [{args, []}, exit_status]). +#Port<0.7> +2> receive {Port, {data, R}} -> R after 1000 -> timeout end. +"Current Date and Time: Mon May 20 13:59:32 2024\n" +``` + ===== Sockets Sockets provide a straightforward way to enable communication between Erlang and external C programs. By using TCP or UDP sockets, C applications can exchange data with Erlang processes over the network, ensuring that both systems remain isolated. This method is particularly useful for distributed systems and allows for asynchronous communication. @@ -29,15 +73,16 @@ The next level is to use pure socket communication, which can be more efficient See chapter xref:CH-IO[] for details on how sockets works. ===== Open Ports -The `open_port` function in Erlang creates a communication channel between an Erlang process and an external C program. This method involves starting the C program as a separate OS process and communicating with it via standard input and output. This approach encapsulates the C code, preventing it from directly affecting the Erlang VM's stability. +The `open_port` function in Erlang creates a communication channel between an Erlang process and an external C program. This method involves starting the C program as a separate OS process and communicating with it via standard input and output. This approach encapsulates the C code, preventing it from directly affecting the Erlang VM's stability. Example: + ```erlang -Port = open_port({spawn, "./my_c_program"}, [binary]), -port_command(Port, <<"Hello, C program!">>). +Port = open_port({spawn, "./system_time"}, [binary]), +port_command(Port, <<"get_time\n">>). ``` -2. Overview of BIFs, NIFs, and Linked-in Drivers +=== Overview of BIFs, NIFs, and Linked-in Drivers * Definitions and primary uses. * Comparison of safety and complexity. diff --git a/chapters/io.asciidoc b/chapters/io.asciidoc index 3e93f50..861f08c 100644 --- a/chapters/io.asciidoc +++ b/chapters/io.asciidoc @@ -1,5 +1,6 @@ [[CH-IO]] -== IO, Ports and Networking (10p) +== IO, Ports and Networking +// First Draft Within Erlang, all communication is done by asynchronous signaling. The communication between an Erlang node and the outside world is done @@ -15,13 +16,194 @@ how Erlang processes communicate with ports. But first we will look at how standard IO works on a higher level. === Standard IO === +Understanding standard I/O in Erlang helps with debugging and interaction with external programs. This section will cover the I/O protocol, group leaders, how to use 'erlang:display', 'io:format', and methods to redirect standard I/O. + +==== I/O Protocol ==== + +Erlang's I/O protocol handles communication between processes and I/O devices. The protocol defines how data is sent to and received from devices like the terminal, files, or external programs. +The protocol includes commands for reading, writing, formatting, and handling I/O control operations. +These commands are performed asynchronously, maintaining the concurrency model of Erlang. +For more detailed information, refer to the official documentation at link:https://www.erlang.org/doc/apps/stdlib/io_protocol.html[erlang.org:io_protocol]. + +The I/O protocol is used to communicate with the group leader, which is responsible for handling I/O requests. The group leader is the process that receives I/O requests from other processes and forwards them to the I/O server. The I/O server is responsible for executing the requests and sending the responses back to the group leader, which then forwards them to the requesting process. + +We will discuss group leaders later, but lets look at the actual I/O protocol first. The protocol has the following messages: + +===== Basic Messages + +* `{io_request, From, ReplyAs, Request}`: + - `From`: `pid()` of the client process. + - `ReplyAs`: Identifier for matching the reply to the request. + - `Request`: The I/O request. + +* `{io_reply, ReplyAs, Reply}`: + - `ReplyAs`: Identifier matching the original request. + - `Reply`: The response to the I/O request. + +===== Output Requests + +* `{put_chars, Encoding, Characters}`: + - `Encoding`: `unicode` or `latin1`. + - `Characters`: Data to be written. + +* `{put_chars, Encoding, Module, Function, Args}`: + - `Module, Function, Args`: Function to produce the data. + +===== Input Requests + +* `{get_until, Encoding, Prompt, Module, Function, ExtraArgs}`: + - `Encoding`: `unicode` or `latin1`. + - `Prompt`: Data to be output as a prompt. + - `Module, Function, ExtraArgs`: Function to determine when enough data is read. + +* `{get_chars, Encoding, Prompt, N}`: + - `Encoding`: `unicode` or `latin1`. + - `Prompt`: Data to be output as a prompt. + - `N`: Number of characters to be read. + +* `{get_line, Encoding, Prompt}`: + - `Encoding`: `unicode` or `latin1`. + - `Prompt`: Data to be output as a prompt. + +===== Server Modes + +* `{setopts, Opts}`: + - `Opts`: List of options for the I/O server. + +* `getopts`: + - Requests the current options from the I/O server. + +===== Multi-Request and Optional Messages + +* `{requests, Requests}`: + - `Requests`: List of valid `io_request` tuples to be executed sequentially. + +* `{get_geometry, Geometry}`: + - `Geometry`: Requests the number of rows or columns (optional). + +===== Unimplemented Request Handling + +If an I/O server encounters an unrecognized request, it should respond with: +- `{error, request}`. + +===== Example of a Custom I/O Server + +Here's a simplified example of an I/O server that stores data in memory: + +[source,erlang] +---- +include::../code/io_chapter/pg_example/src/custom_io_server.erl[] +---- + +We can now use this memory store as an I/O device for example using +the file interface. + + +[source,erlang] +---- +include::../code/io_chapter/pg_example/src/file_client.erl[] +---- + +Now we can use this memory store through the file_client interface: + +```erlang +shell V14.2.1 (press Ctrl+G to abort, type help(). for help) +1> {ok, Pid} = file_client:open(). +{ok,<0.219.0>} +2> file_client:write(Pid, "Hello, world!\n"). +ok +3> R = file_client:close(Pid). +{ok,<<"Hello, world!\n">>} +4> +``` + +==== Group Leader +Group leaders allow you to redirect I/O to the appropriate endpoint. This property is inherited by child processes, creating a chain. By default, an Erlang node has a group leader called 'user' that manages communication with standard input/output channels. All input and output requests go through this process. + +Each shell started in Erlang becomes its own group leader. This means functions run from the shell will send all I/O data to that specific shell process. When switching shells with ^G and selecting a shell (e.g., `c `), a special shell-handling process directs the I/O traffic to the correct shell. + +In distributed systems, slave nodes or remote shells set their group leader to a foreign PID, ensuring I/O data from descendant processes is rerouted correctly. This setup is crucial for interactive use cases. + +Each OTP application has an application master process acting as a group leader. This has two main uses: +1. It allows processes to access their application's environment configuration using `application:get_env(Var)`. +2. During application shutdown, the application master scans and terminates all processes with the same group leader, effectively garbage collecting application processes. + +Group leaders are also used to capture I/O during tests by `common_test` and `eunit`, and the interactive shell sets the group leader to manage I/O. + +===== Group Leader Functions + +- `group_leader() -> pid()`: Returns the PID of the process's group leader. +- `group_leader(GroupLeader, Pid) -> true`: Sets the group leader of `Pid` to `GroupLeader`. + +The group leader of a process is typically not changed in applications with a supervision tree, as OTP assumes the group leader is the application master. + +The 'group_leader/2' function uses the 'group_leader' signal to set the group leader of a process. This signal is sent to the process, which then sets its group leader to the specified PID. The group leader can be any process, but it is typically a shell process or an application master. + +===== Example Usage + +```erlang +1> group_leader(). +<0.24.0> + +2> self(). +<0.42.0> + +3> group_leader(self(), <0.43.0>). +true +``` + +Understanding group leaders and the difference between the bif 'display' and the function 'io:format' will help you manage basic I/O in Erlang. +* 'erlang:display/1': A BIF that writes directly to the standard output, bypassing the Erlang I/O system. +* 'io:format/1,2': Sends I/O requests to the group leader. If performed via rpc:call/4, the output goes to the calling process's standard output. + +==== Redirecting Standard IO at Startup (Detached Mode) + +In Erlang, redirecting standard I/O (stdin, stdout, and stderr) at startup, especially when running in detached mode, allows you to control where the input and output are directed. This is particularly useful in production environments where you might want to log output to files or handle input from a different source. + +===== Detached Mode + +Detached mode in Erlang can be activated by using the `-detached` flag. This flag starts the Erlang runtime system as a background process, without a connected console. Here’s how to start an Erlang node in detached mode: + +```sh +erl -sname mynode -setcookie mycookie -detached +``` +When running in detached mode, you need to redirect standard I/O manually, as there is no attached console. This can be done by specifying redirection options for the Erlang runtime. + +===== Redirecting Standard Output and Error + +To redirect standard output and standard error to a file, use shell redirection or Erlang’s built-in options. Here’s an example of redirecting stdout and stderr to separate log files: + +```sh +erl -sname mynode -setcookie mycookie -detached > mynode_stdout.log 2> mynode_stderr.log +``` +This command starts an Erlang node in detached mode and redirects standard output to `mynode_stdout.log` and standard error to `mynode_stderr.log`. + +Alternatively, you can configure this within an Erlang script or startup configuration: + +```erlang +init() -> + % Redirect stdout and stderr + file:redirect(standard_output, "mynode_stdout.log"), + file:redirect(standard_error, "mynode_stderr.log"). +``` + +===== Redirecting Standard Input + +Redirecting standard input involves providing an input source when starting the Erlang node. For example, you can use a file as the input source: + +```sh +erl -sname mynode -setcookie mycookie -detached < input_commands.txt +``` + +==== Standard Input and Output Summary + +Standard output ('stdout') is managed by the group leader and is typically directed to the console or a specified log file. Functions such as io:format/1 and io:put_chars/2 send output to the group leader, which handles writing it to the designated output device or file. Standard error ('stderr') is similarly managed by the group leader and can also be redirected to log files or other output destinations. Standard input ('stdin') is read by processes from the group leader. In detached mode, input can be redirected from a file or another input source. + +You can implement your own I/O server using the I/O protocol and you can +use that I/O server as a file descriptor or set it as the group leader of +a process and redirect I/O through that server. + -* IO protocol -* group leader -* erlang:display -- a BIF that sends directly to node's std out -* io:format -- sends through io protocol and the group leader -* Redirecting standard IO at startup (detached mode) -* Standard in and out === Ports === @@ -64,7 +246,6 @@ handle to the port and can send messages to the port. The processes and the port reside in an Erlang node. The file lives in the file and operating system on the outside of the Erlang node. - If the port owner dies or is terminated the port is also killed. When a port terminates all external resources should also be cleaned up. This is true for all ports that come with Erlang and if you @@ -85,7 +266,9 @@ PortSettings)+. A file descriptor port is opened with +{fd, In, Out}+ as the +PortName+. This class of ports is used by some internal ERTS servers like the old shell. They are considered to not be very efficient and -hence seldom used. +hence seldom used. Also the filedescriptors are non negative intgers +representing open file descriptors in the OS. The file descriptor can +not be an erlang I/O server. An external program port can be used to execute any program in the native OS of the Erlang node. To open an external program port you @@ -131,46 +314,207 @@ driver like: `multi_drv` and `sig_drv`. +--------------------------------------------------+---------+-------+-----------+++-------+++--------+ ---- +Data sent to and from a port are byte streams. The packet size can be specified in the 'PortSettings' when opening a port. Since R16, ports support truly asynchronous communication, improving efficiency and performance. -// Mention that data to from the port are byte streams -// Packet size -// R16 truly asynchronous. +Ports can be used to replace standard IO and polling. This is useful when you need to interact with external programs or devices. By opening a port to a file descriptor, you can read and write data to the file. Similarly, you can open a port to an external program and communicate with it using the port interface. -// Replacing the standard IO, Poll. -// How ports are implemented. -// How ports communicate. +===== Ports to file descriptors ===== +File descriptor ports in Erlang provide an interface to interact with already opened file descriptors. Although they are not commonly used due to efficiency concerns, they can provide an easi interface to external resources. -===== Ports to file descriptors ===== +To create a file descriptor port, you use the 'open_port/2' function with the '{fd, In, Out}'' tuple as the 'PortName'. Here, 'In' and 'Out' are the file descriptors for input and output, respectively. -Creating +Bad example: +```erlang +Port = open_port({fd, 0, 1}, []). +``` +This opens a port that reads from the standard input (file descriptor 0) and writes to the standard output (file descriptor 1). Don't try this example +since it will steal the IO from your erlang shell. -Commands +File descriptor ports are implemented using the 'open_port/2' function, which creates a port object. The port object handles the communication between the Erlang process and the file descriptor. -Examples +Internally, when 'open_port/2' is called with '{fd, In, Out}'', the Erlang runtime system sets up the necessary communication channels to interact with the specified file descriptors. The port owner process can then send and receive messages to/from the port, which in turn interacts with the file descriptor. -Implementation ===== Ports to Spawned OS Processes ===== -Creating - -Commands +To create a port to a spawned OS process, you use the 'open_port/2' function with the '{spawn, Command}'' or '{spawn_executable, FileName}'' tuple as the 'PortName'. This method allows Erlang processes to interact with external programs by spawning them as separate OS processes. -Implementation +The primary commands for interacting with a port include: +* '{command, Data}'': Sends 'Data' to the external program. +* '{control, Operation, Data}'': Sends a control command to the external program. +* '{exit_status, Status}'': Receives the exit status of the external program. -Windows +See xref:CH-C[] for examples of how to spawn a and externa program as a port, +you can also look at the official documentation: link:https://www.erlang.org/doc/system/c_port.html#content[erlang.org:c_port]. ===== Ports to Linked in Drivers ===== -Creating +Linked-in drivers in Erlang are created using the 'open_port/2' function with the '{spawn_driver, Command}'' tuple as the 'PortName'. This method requires the first token of the command to be the name of a loaded driver. + +```erlang +Port = open_port({spawn_driver, "my_driver"}, []). +``` + +Commands for interacting with a linked-in driver port typically include: + +* '{command, Data}'': Sends 'Data' to the driver. +* '{control, Operation, Data}'': Sends a control command to the driver. +Example: + +```erlang +Port ! {self(), {command, <<"Hello, Driver!\n">>}}. +``` + +See xref:CH-C[] for examples of how to implement and spawn a linked in driver as a port, you can also look at the official documentation: link:https://www.erlang.org/doc/system/c_portdriver.html#content[erlang.org:c_portdriver]. + +==== Flow Control in Erlang Ports + +Ports implement flow control mechanisms to manage backpressure and ensure efficient resource usage. One primary mechanism is the busy port functionality, which prevents a port from being overwhelmed by too many simultaneous operations. When a port is in a busy state, it can signal to the Erlang VM that it cannot handle more data until it processes existing data. + +When the port's internal buffer exceeds a specified high-water mark, the port enters a busy state. In this state, it signals to the VM to stop sending new data until it can process the buffered data. + +Processes attempting to send data to a busy port are suspended until the port exits the busy state. This prevents data loss and ensures that the port can handle all incoming data efficiently. + +Once the port processes enough data to fall below a specified low-water mark, it exits the busy state. Suspended processes are then allowed to resume sending data. + +By scheduling signals to/from the port asynchronously, Erlang ensures that processes sending data can continue executing without being blocked, improving system parallelism and responsiveness. + +Now this means that the erlang send operation is not always asyncronous. If the port is busy the send operation will block until the port is not busy anymore. This is a problem if you have a lot of processes sending data to the same port. The solution is to use a port server that can handle the backpressure and make sure that the send operation is always asyncronous. + +Lets do an example based on the offical port driver example: +link:https://www.erlang.org/doc/system/c_portdriver.html#c-driver[erlang.org:c-driver]. -Commands +Let us use the original complex c function, adding 1 or multiplying by 2. +[source, c] +---- +include::../code/io_chapter/pg_example/src/example.c[] +---- + +And a slighly modified port driver. We have added an id to each message +and return the id with the result. We also have a function +that simulats a busy port by sleeping for a while and setting +the busy port status if the id of the message is below 14 and the call is to the 'bar' function. + +[source, c] +---- +include::../code/io_chapter/pg_example/src/busy_port.c[] +---- + +Now we can call this function from Erlang syncrounusly, or send +asyncronus messages. In our port handler we have also added +some tests that sends 10 messages to the port and then recives +the results both syncronously and asyncronously. -Implementation +[source,erlang] +---- +include::../code/io_chapter/pg_example/src/busy_port.erl[] +---- -See chapter xref:CH-C[] for details of how to implement your own -linked in driver. +Lets try this in the shell: + +```erlang +1> c(busy_port). +{ok,busy_port} +2> busy_port:start("busy_port_drv"). +<0.89.0> +3> busy_port:test_sync_foo(). +Call: {foo,1} +Received data: 2 +Call: {foo,2} +Received data: 3 +Call: {foo,3} +Received data: 4 +Call: {foo,4} +Received data: 5 +Call: {foo,5} +Received data: 6 +Call: {foo,6} +Received data: 7 +Call: {foo,7} +Received data: 8 +Call: {foo,8} +Received data: 9 +Call: {foo,9} +Received data: 10 +Call: {foo,10} +Received data: 11 +[2,3,4,5,6,7,8,9,10,11] +``` +That worked as expected; we did a syncrouns call and immediatley +got a response. Now lets try the asyncronous call: + +```erlang +4> busy_port:test_async_foo(). +Send: {foo,1} +Send: {foo,2} +Send: {foo,3} +Send: {foo,4} +Send: {foo,5} +Send: {foo,6} +Send: {foo,7} +Send: {foo,8} +Send: {foo,9} +Send: {foo,10} +Received data: 2 +Received data: 3 +Received data: 4 +Received data: 5 +Received data: 6 +Received data: 7 +Received data: 8 +Received data: 9 +Received data: 10 +Received data: 11 +[2,3,4,5,6,7,8,9,10,11] +``` +That also worked as expected we sent 10 messages and got the results back in the same order also immediately. Now lets try the busy port: + +```erlang +5> busy_port:test_async_bar(). +Send: {bar,1} +Shouldnt ! be async... +Send: {bar,2} +Shouldnt ! be async... +Send: {bar,3} +Shouldnt ! be async... +Send: {bar,4} +Shouldnt ! be async... +Send: {bar,5} +Shouldnt ! be async... +Send: {bar,6} +Send: {bar,7} +Send: {bar,8} +Send: {bar,9} +Send: {bar,10} +Received data: 2 +Received data: 4 +Received data: 6 +Received data: 8 +Received data: 10 +Received data: 12 +Received data: 14 +Received data: 16 +Received data: 18 +Received data: 20 +[timepout,2,4,6,8,10,12,14,16,18] +``` +We see that the first 5 messages are not asyncronous, but the last 5 are. This is because the port is busy and the send operation is blocking. The port is busy because the id of the message is below 14 and the call is to the 'bar' function. The port is busy for 5 seconds and then the last 5 messages are sent asyncronously. + +==== Port Scheduling ==== + +Erlang ports, similar to processes, execute code (drivers) to handle external communication, such as TCP. Originally, port signals were handled synchronously, causing issues with I/O event parallelism. This was problematic due to heavy lock contention and reduced parallelism potential. + +To address these issues, Erlang schedules all port signals, ensuring sequential execution by a single scheduler. This eliminates contention and allows processes to continue executing Erlang code in parallel. + +Ports have a task queue managed by a "semi-locked" approach with a public locked queue and a private lock-free queue. Tasks are moved between these queues to avoid lock contention. This system handles I/O signal aborts by marking tasks as aborted using atomic operations, ensuring tasks are safely deallocated without lock contention. + +Ports can enter a busy state when overloaded with command signals, suspending new signals until the queue is manageable. This ensures flow control, preventing the port from being overwhelmed before it can process signals. + +Signal data preparation occurs before acquiring the port lock, reducing latency. Non-contended signals are executed immediately, maintaining low latency, while contended signals are scheduled for later execution to preserve parallelism. + +See Chapter xref:CH-Scheduling[] for details of how the scheduler works +and how ports fit into the general scheduling scheme. === Distributed Erlang === @@ -178,6 +522,65 @@ See chapter xref:CH-Distribution[] for details on the built in distribution layer. === Sockets, UDP and TCP === -// Sockets. -// Getting info on ports and sockets. -// Tweaking. +Sockets are a fundamental aspect of network communication in Erlang. They allow processes to communicate over a network using protocols such as TCP and UDP. Here, we will explore how to work with sockets, retrieve information about sockets, and tweak socket behavior. + +Erlang provides a robust set of functions for creating and managing sockets. The gen_tcp and gen_udp modules facilitate the use of TCP and UDP protocols, respectively. Here is a basic example of opening a TCP socket: + +```erlang +% Open a listening socket on port 1234 +{ok, ListenSocket} = gen_tcp:listen(1234, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]), + +% Accept a connection +{ok, Socket} = gen_tcp:accept(ListenSocket), + +% Send and receive data +ok = gen_tcp:send(Socket, <<"Hello, World!">>), +{ok, Data} = gen_tcp:recv(Socket, 0). +``` + +For UDP, the process is similar but uses the 'gen_udp' module: + +```erlang +% Open a UDP socket on port 1234 +{ok, Socket} = gen_udp:open(1234, [binary, {active, false}]), + +% Send and receive data +ok = gen_udp:send(Socket, "localhost", 1234, <<"Hello, World!">>), +receive + {udp, Socket, Host, Port, Data} -> io:format("Received: ~p~n", [Data]) +end. + +``` + + +Erlang provides several functions to retrieve information about sockets. For instance, you can use inet:getopts/2 and inet:setopts/2 to get and set options on sockets. Here's an example: + +```erlang +% Get options on a socket +{ok, Options} = inet:getopts(Socket, [recbuf, sndbuf, nodelay]), + +% Set options on a socket +ok = inet:setopts(Socket, [{recbuf, 4096}, {sndbuf, 4096}, {nodelay, true}]). +``` +Additionally, you can use inet:peername/1 and inet:sockname/1 to get the remote and local addresses of a socket: + +```erlang +% Get the remote address of a connected socket +{ok, {Address, Port}} = inet:peername(Socket), + +% Get the local address of a socket +{ok, {LocalAddress, LocalPort}} = inet:sockname(Socket). +``` +To optimize socket performance and behavior, you can tweak various socket options. Commonly adjusted options include buffer sizes, timeouts, and packet sizes. Here's how you can tweak some of these options: + +```erlang +% Set socket buffer sizes +ok = inet:setopts(Socket, [{recbuf, 8192}, {sndbuf, 8192}]), + +% Set a timeout for receiving data +ok = inet:setopts(Socket, [{recv_timeout, 5000}]), + +% Set packet size for a TCP socket +ok = inet:setopts(Socket, [{packet, 4}]). + +``` diff --git a/code/io_chapter/pg_example/.gitignore b/code/io_chapter/pg_example/.gitignore new file mode 100644 index 0000000..3a55f93 --- /dev/null +++ b/code/io_chapter/pg_example/.gitignore @@ -0,0 +1,21 @@ +.rebar3 +_build +_checkouts +_vendor +.eunit +*.o +*.so +*.beam +*.plt +*.swp +*.swo +.erlang.cookie +ebin +log +erl_crash.dump +.rebar +logs +.idea +*.iml +rebar3.crashdump +*~ diff --git a/code/io_chapter/pg_example/rebar.config b/code/io_chapter/pg_example/rebar.config new file mode 100644 index 0000000..608df54 --- /dev/null +++ b/code/io_chapter/pg_example/rebar.config @@ -0,0 +1,11 @@ +{erl_opts, [debug_info]}. +{deps, [ + {epgsql, "4.7.0"}, + {cowboy, "2.12.0"}, + {ranch, "2.0.0"} +]}. + +{shell, [ + % {config, "config/sys.config"}, + {apps, [pg_example]} +]}. diff --git a/code/io_chapter/pg_example/src/busy_port.c b/code/io_chapter/pg_example/src/busy_port.c new file mode 100644 index 0000000..35fcf1c --- /dev/null +++ b/code/io_chapter/pg_example/src/busy_port.c @@ -0,0 +1,103 @@ +/* Derived from port_driver.c + https://www.erlang.org/doc/system/c_portdriver.html + +*/ + +#include "erl_driver.h" +#include +#include // Include for sleep function + +int foo(int x); +int bar(int y); + +typedef struct +{ + ErlDrvPort port; +} example_data; + +static ErlDrvData bp_drv_start(ErlDrvPort port, char *buff) +{ + example_data *d = (example_data *)driver_alloc(sizeof(example_data)); + d->port = port; + return (ErlDrvData)d; +} + +static void bp_drv_stop(ErlDrvData handle) +{ + driver_free((char *)handle); +} + +static void bp_drv_output(ErlDrvData handle, char *buff, + ErlDrvSizeT bufflen) +{ + example_data *d = (example_data *)handle; + char fn = buff[0], arg = buff[1], id = buff[2]; + static char res[2]; + + if (fn == 1) + { + res[0] = foo(arg); + } + else if (fn == 2) + { + res[0] = bar(arg); + if (id > 14) + { + // Signal that the port is free + set_busy_port(d->port, 0); + } + else + { + // Signal that the port is busy + // This is not essential for this example + // However, if multiple processes attempted to use the port + // in parallel, we would need to signal that the port is busy + // This would make even the foo function block. + set_busy_port(d->port, 1); + // Simulate processing delay + sleep(1); + set_busy_port(d->port, 0); + } + } + res[1] = id; + driver_output(d->port, res, 2); +} + +ErlDrvEntry bp_driver_entry = { + NULL, /* F_PTR init, called when driver is loaded */ + bp_drv_start, /* L_PTR start, called when port is opened */ + bp_drv_stop, /* F_PTR stop, called when port is closed */ + bp_drv_output, /* F_PTR output, called when erlang has sent */ + NULL, /* F_PTR ready_input, called when input descriptor ready */ + NULL, /* F_PTR ready_output, called when output descriptor ready */ + "busy_port_drv", /* char *driver_name, the argument to open_port */ + NULL, /* F_PTR finish, called when unloaded */ + NULL, /* void *handle, Reserved by VM */ + NULL, /* F_PTR control, port_command callback */ + NULL, /* F_PTR timeout, reserved */ + NULL, /* F_PTR outputv, reserved */ + NULL, /* F_PTR ready_async, only for async drivers */ + NULL, /* F_PTR flush, called when port is about + to be closed, but there is data in driver + queue */ + NULL, /* F_PTR call, much like control, sync call + to driver */ + NULL, /* unused */ + ERL_DRV_EXTENDED_MARKER, /* int extended marker, Should always be + set to indicate driver versioning */ + ERL_DRV_EXTENDED_MAJOR_VERSION, /* int major_version, should always be + set to this value */ + ERL_DRV_EXTENDED_MINOR_VERSION, /* int minor_version, should always be + set to this value */ + 0, /* int driver_flags, see documentation */ + NULL, /* void *handle2, reserved for VM use */ + NULL, /* F_PTR process_exit, called when a + monitored process dies */ + NULL /* F_PTR stop_select, called to close an + event object */ +}; + +DRIVER_INIT(busy_port_drv) /* must match name in driver_entry */ +{ + return &bp_driver_entry; +} \ No newline at end of file diff --git a/code/io_chapter/pg_example/src/busy_port.erl b/code/io_chapter/pg_example/src/busy_port.erl new file mode 100644 index 0000000..40ffe41 --- /dev/null +++ b/code/io_chapter/pg_example/src/busy_port.erl @@ -0,0 +1,116 @@ +%% Derived from https://www.erlang.org/doc/system/c_portdriver.html + +-module(busy_port). +-export([start/1, stop/0, init/1]). +-export([foo/1, bar/1, async_foo/1, async_bar/1, async_receive/0]). +-export([test_sync_foo/0, + test_async_foo/0, + test_async_bar/0]). + +start(SharedLib) -> + case erl_ddll:load_driver(".", SharedLib) of + ok -> ok; + {error, already_loaded} -> ok; + _ -> exit({error, could_not_load_driver}) + end, + spawn(?MODULE, init, [SharedLib]). + +init(SharedLib) -> + register(busy_port_example, self()), + Port = open_port({spawn, SharedLib}, []), + loop(Port, [], 0). + +test_sync_foo() -> + [foo(N) || N <- lists:seq(1, 10)]. + +test_async_foo() -> + [async_receive() || _ <- [async_foo(N) || N <- lists:seq(1, 10)]]. + +test_async_bar() -> + [async_receive() || _ <- [async_bar(N) || N <- lists:seq(1, 10)]]. + + + +stop() -> + busy_port_example ! stop. + +foo(X) -> + call_port({foo, X}). +bar(Y) -> + call_port({bar, Y}). + +async_foo(X) -> + send_message({foo, X}). +async_bar(Y) -> + send_message({bar, Y}). + +async_receive() -> + receive + {busy_port_example, Data} -> + Data + after 2000 -> timepout + end. + +call_port(Msg) -> + busy_port_example ! {call, self(), Msg}, + receive + {busy_port_example, Result} -> + Result + end. + +send_message(Message) -> + busy_port_example ! {send, self(), Message}. + + +reply(Id, [{Id, From}|Ids], Data) -> + From ! {busy_port_example, Data}, + Ids; +reply(Id, [Id1|Ids], Data) -> + [Id1 | reply(Id, Ids, Data)]; +reply(_Id, [], Data) -> %% oops, no id found + io:format("No ID found for data: ~p~n", [Data]), + []. + + +loop(Port, Ids, Id) -> + receive + {call, Caller, Msg} -> + io:format("Call: ~p~n", [Msg]), + Port ! {self(), {command, encode(Msg, Id)}}, + receive + {Port, {data, Data}} -> + Res = decode_data(Data), + io:format("Received data: ~w~n", [Res]), + Caller ! {busy_port_example, Res} + end, + loop(Port, Ids, Id); + {Port, {data, Data}} -> + {Ref, Res} = decode(Data), + io:format("Received data: ~w~n", [Res]), + NewIds = reply(Ref, Ids, Res), + loop(Port, NewIds, Id); + {send, From, Message} -> + T1 = os:system_time(millisecond), + io:format("Send: ~p~n", [Message]), + Port ! {self(), {command, encode(Message, Id)}}, + T2 = os:system_time(millisecond), + if (T2 - T1) > 500 -> io:format("Shouldnt ! be async...~n", []); + true -> ok + end, + loop(Port, [{Id, From} | Ids], Id + 1); + stop -> + Port ! {self(), close}, + receive + {Port, closed} -> + exit(normal) + end; + {'EXIT', Port, Reason} -> + io:format("~p ~n", [Reason]), + exit(port_terminated) + end. + +encode({foo, X}, Id) -> [1, X, Id]; +encode({bar, X}, Id) -> [2, X, Id]. + +decode([Int, Id]) -> {Id, Int}. +decode_data([Int,_Id]) -> Int. \ No newline at end of file diff --git a/code/io_chapter/pg_example/src/custom_io_server.erl b/code/io_chapter/pg_example/src/custom_io_server.erl new file mode 100644 index 0000000..9243745 --- /dev/null +++ b/code/io_chapter/pg_example/src/custom_io_server.erl @@ -0,0 +1,58 @@ +-module(custom_io_server). +-export([start_link/0, stop/1, init/0, loop/1, handle_request/2]). + +-record(state, {buffer = <<>>, pos = 0}). + +start_link() -> + {ok, spawn_link(?MODULE, init, [])}. + +init() -> + ?MODULE:loop(#state{}). + +stop(Pid) -> + Pid ! {io_request, self(), Pid, stop}, + receive + {io_reply, _, {ok, State}} -> + {ok, State#state.buffer}; + Other -> + {error, Other} + end. + +loop(State) -> + receive + {io_request, From, ReplyAs, Request} -> + case handle_request(Request, State) of + {ok, Reply, NewState} -> + From ! {io_reply, ReplyAs, Reply}, + loop(NewState); + {stop, Reply, _NewState} -> + From ! {io_reply, ReplyAs, Reply}, + exit(normal); + {error, Reply, NewState} -> + From ! {io_reply, ReplyAs, {error, Reply}}, + loop(NewState) + end + end. + +handle_request({put_chars, _Encoding, Chars}, State) -> + Buffer = State#state.buffer, + NewBuffer = <>, + {ok, ok, State#state{buffer = NewBuffer}}; +handle_request({get_chars, _Encoding, _Prompt, N}, State) -> + Part = binary:part(State#state.buffer, State#state.pos, N), + {ok, Part, State#state{pos = State#state.pos + N}}; +handle_request({get_line, _Encoding, _Prompt}, State) -> + case binary:split(State#state.buffer, <<$\n>>, [global]) of + [Line|_Rest] -> + {ok, <>, State}; + _ -> + {ok, State#state.buffer, State} + end; +handle_request(getopts, State) -> + {ok, [], State}; +handle_request({setopts, _Opts}, State) -> + {ok, ok, State}; +handle_request(stop, State) -> + {stop, {ok, State}, State}; +handle_request(_Other, State) -> + {error, {error, request}, State}. \ No newline at end of file diff --git a/code/io_chapter/pg_example/src/example.c b/code/io_chapter/pg_example/src/example.c new file mode 100644 index 0000000..079b34b --- /dev/null +++ b/code/io_chapter/pg_example/src/example.c @@ -0,0 +1,11 @@ +/* example.c */ + +int foo(int x) +{ + return x + 1; +} + +int bar(int y) +{ + return y * 2; +} \ No newline at end of file diff --git a/code/io_chapter/pg_example/src/file_client.erl b/code/io_chapter/pg_example/src/file_client.erl new file mode 100644 index 0000000..8489bdc --- /dev/null +++ b/code/io_chapter/pg_example/src/file_client.erl @@ -0,0 +1,18 @@ +-module(file_client). +-export([open/0, close/1, write/2, read/2, read_line/1]). + +open() -> + {ok, Pid} = custom_io_server:start_link(), + {ok, Pid}. + +close(Device) -> + custom_io_server:stop(Device). + +write(Device, Data) -> + file:write(Device, Data). + +read(Device, Length) -> + file:read(Device, Length). + +read_line(Device) -> + file:read_line(Device). diff --git a/code/io_chapter/pg_example/src/index.html b/code/io_chapter/pg_example/src/index.html new file mode 100644 index 0000000..7cc5f85 --- /dev/null +++ b/code/io_chapter/pg_example/src/index.html @@ -0,0 +1,52 @@ + + + + + Erlang Console + + + + +
+ + + + + \ No newline at end of file diff --git a/code/io_chapter/pg_example/src/pg_client.erl b/code/io_chapter/pg_example/src/pg_client.erl new file mode 100644 index 0000000..31e2507 --- /dev/null +++ b/code/io_chapter/pg_example/src/pg_client.erl @@ -0,0 +1,27 @@ +-module(pg_client). +-export([start/0, put_chars/1, get_chars/1, get_line/0]). + +start() -> + {ok, Pid} = pg_io_server:start_link(), + register(pg_io_server, Pid). + +put_chars(Data) -> + pg_io_server ! {io_request, self(), pg_io_server, {put_chars, latin1, Data}}, + receive + {io_reply, pg_io_server, Reply} -> + Reply + end. + +get_chars(N) -> + pg_io_server ! {io_request, self(), pg_io_server, {get_chars, latin1, '', N}}, + receive + {io_reply, pg_io_server, Reply} -> + Reply + end. + +get_line() -> + pg_io_server ! {io_request, self(), pg_io_server, {get_line, latin1, ''}}, + receive + {io_reply, pg_io_server, Reply} -> + Reply + end. diff --git a/code/io_chapter/pg_example/src/pg_example.app.src b/code/io_chapter/pg_example/src/pg_example.app.src new file mode 100644 index 0000000..8a4d1b9 --- /dev/null +++ b/code/io_chapter/pg_example/src/pg_example.app.src @@ -0,0 +1,15 @@ +{application, pg_example, + [{description, "An OTP application"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, {pg_example_app, []}}, + {applications, + [kernel, + stdlib + ]}, + {env,[]}, + {modules, []}, + + {licenses, ["Apache-2.0"]}, + {links, []} + ]}. diff --git a/code/io_chapter/pg_example/src/pg_example_app.erl b/code/io_chapter/pg_example/src/pg_example_app.erl new file mode 100644 index 0000000..104db9b --- /dev/null +++ b/code/io_chapter/pg_example/src/pg_example_app.erl @@ -0,0 +1,18 @@ +%%%------------------------------------------------------------------- +%% @doc pg_example public API +%% @end +%%%------------------------------------------------------------------- + +-module(pg_example_app). + +-behaviour(application). + +-export([start/2, stop/1]). + +start(_StartType, _StartArgs) -> + pg_example_sup:start_link(). + +stop(_State) -> + ok. + +%% internal functions diff --git a/code/io_chapter/pg_example/src/pg_example_sup.erl b/code/io_chapter/pg_example/src/pg_example_sup.erl new file mode 100644 index 0000000..f51dc04 --- /dev/null +++ b/code/io_chapter/pg_example/src/pg_example_sup.erl @@ -0,0 +1,35 @@ +%%%------------------------------------------------------------------- +%% @doc pg_example top level supervisor. +%% @end +%%%------------------------------------------------------------------- + +-module(pg_example_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + +-define(SERVER, ?MODULE). + +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +%% sup_flags() = #{strategy => strategy(), % optional +%% intensity => non_neg_integer(), % optional +%% period => pos_integer()} % optional +%% child_spec() = #{id => child_id(), % mandatory +%% start => mfargs(), % mandatory +%% restart => restart(), % optional +%% shutdown => shutdown(), % optional +%% type => worker(), % optional +%% modules => modules()} % optional +init([]) -> + SupFlags = #{strategy => one_for_all, + intensity => 0, + period => 1}, + ChildSpecs = [], + {ok, {SupFlags, ChildSpecs}}. + +%% internal functions diff --git a/code/io_chapter/pg_example/src/pg_io_server.erl b/code/io_chapter/pg_example/src/pg_io_server.erl new file mode 100644 index 0000000..06738c9 --- /dev/null +++ b/code/io_chapter/pg_example/src/pg_io_server.erl @@ -0,0 +1,58 @@ +-module(pg_io_server). +-export([start_link/0, init/0, loop/1, handle_request/2]). + +-record(state, {conn}). + +start_link() -> + spawn_link(?MODULE, init, []). + +init() -> + case epgsql:connect(#{ + host => "127.0.0.1", + username => "postgres", + password => "", % Default user usually does not need a password for local connections + database => "mydb" + }) of + {ok, Conn} -> + io:format("Successfully connected to PostgreSQL~n"), + ?MODULE:loop(#state{conn = Conn}); + {error, Reason} -> + io:format("Failed to connect to PostgreSQL: ~p~n", [Reason]), + exit(Reason) + end. + +loop(State) -> + receive + {io_request, From, ReplyAs, Request} -> + case handle_request(Request, State) of + {ok, Reply, NewState} -> + From ! {io_reply, ReplyAs, Reply}, + loop(NewState); + {stop, Reply, NewState} -> + From ! {io_reply, ReplyAs, Reply}, + epgsql:close(NewState#state.conn), + exit(normal) + end; + _Unknown -> + loop(State) + end. + +handle_request({put_chars, _Encoding, Chars}, #state{conn = Conn} = State) -> + Query = "INSERT INTO io_data (content) VALUES ($1)", + {ok, _} = epgsql:squery(Conn, Query, [Chars]), + {ok, ok, State}; +handle_request({get_chars, _Encoding, _Prompt, N}, #state{conn = Conn} = State) -> + Query = "SELECT content FROM io_data LIMIT $1", + {ok, Result} = epgsql:squery(Conn, Query, [N]), + {ok, [Row || {_, Row} <- epgsql:decode(Result)], State}; +handle_request({get_line, _Encoding, _Prompt}, #state{conn = Conn} = State) -> + Query = "SELECT content FROM io_data LIMIT 1", + {ok, Result} = epgsql:squery(Conn, Query, []), + {ok, [Row || {_, Row} <- epgsql:decode(Result)], State}; +handle_request(getopts, State) -> + {ok, [], State}; +handle_request({setopts, _Opts}, State) -> + {ok, ok, State}; +handle_request(_Other, State) -> + {error, {error, request}, State}. + diff --git a/code/io_chapter/pg_example/src/pg_router.erl b/code/io_chapter/pg_example/src/pg_router.erl new file mode 100644 index 0000000..f19e0b7 --- /dev/null +++ b/code/io_chapter/pg_example/src/pg_router.erl @@ -0,0 +1,17 @@ +-module(pg_router). +-export([start/0, stop/0, dispatch/0]). + +start() -> + Dispatch = dispatch(), + {ok, _} = cowboy:start_clear(http_listener, + [{port, 8080}], + #{env => #{dispatch => Dispatch}}). + +stop() -> + cowboy:stop_listener(http_listener). + +dispatch() -> + [ + {"/websocket", websocket_handler, []}, + {"/[...]", cowboy_static, {priv_dir, my_app, "static"}} + ]. diff --git a/code/io_chapter/pg_example/src/websocket_handler.erl b/code/io_chapter/pg_example/src/websocket_handler.erl new file mode 100644 index 0000000..95fa0eb --- /dev/null +++ b/code/io_chapter/pg_example/src/websocket_handler.erl @@ -0,0 +1,20 @@ +-module(websocket_handler). +-behaviour(cowboy_websocket). + +-export([init/2, websocket_init/1, websocket_handle/2, websocket_info/2, terminate/3]). + +init(Req, _Opts) -> + {cowboy_websocket, Req, #{}}. + +websocket_init(State) -> + {ok, State}. + +websocket_handle({text, Msg}, State) -> + io:format("Received: ~s~n", [Msg]), + {reply, {text, io_lib:format("Echo: ~s", [Msg])}, State}. + +websocket_info(_Info, State) -> + {ok, State}. + +terminate(_Reason, _Req, _State) -> + ok. diff --git a/code/io_chapter/pg_example/src/websocket_io_server.erl b/code/io_chapter/pg_example/src/websocket_io_server.erl new file mode 100644 index 0000000..efb3647 --- /dev/null +++ b/code/io_chapter/pg_example/src/websocket_io_server.erl @@ -0,0 +1,22 @@ +-module(websocket_io_server). +-export([start_link/1, init/1, handle_info/2, handle_call/3, handle_cast/2]). + +start_link(WebSocketPid) -> + gen_server:start_link(?MODULE, WebSocketPid, []). + +init(WebSocketPid) -> + {ok, WebSocketPid}. + +handle_info({io_request, From, ReplyAs, {put_chars, _Encoding, Chars}}, WebSocketPid) -> + WebSocketPid ! {text, Chars}, + From ! {io_reply, ReplyAs, ok}, + {noreply, WebSocketPid}; +handle_info({io_request, _From, _ReplyAs, {get_chars, _Encoding, _Prompt, _N}}, WebSocketPid) -> + %% Handle reading from WebSocket if necessary + {noreply, WebSocketPid}. + +handle_call(_, _, WebSocketPid) -> + {reply, ok, WebSocketPid}. + +handle_cast(_, WebSocketPid) -> + {noreply, WebSocketPid}.