Skip to content

Commit d70a0f1

Browse files
committed
Introduce 'Connection' struct for managing client/server state
1 parent 1b7b90d commit d70a0f1

8 files changed

Lines changed: 216 additions & 88 deletions

File tree

lib/minecraft/connection.ex

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
defmodule Minecraft.Connection do
2+
@moduledoc """
3+
Maintains the state of a client's connection, and provides utilities for sending and receiving
4+
data. It is designed to be chained in a fashion similar to [`Plug`](https://hexdocs.pm/plug/).
5+
"""
6+
alias Minecraft.Packet
7+
require Logger
8+
9+
@typedoc """
10+
The possible states a client/server can be in.
11+
"""
12+
@type state :: :handshake | :status | :login | :play
13+
14+
@typedoc """
15+
Allowed ranch transport types.
16+
"""
17+
@type transport :: :ranch_tcp
18+
19+
@type t :: %__MODULE__{
20+
current_state: state,
21+
socket: port | nil,
22+
transport: transport | nil,
23+
client_ip: String.t(),
24+
data: binary | nil,
25+
error: any,
26+
protocol_version: integer | nil
27+
}
28+
29+
defstruct current_state: nil,
30+
socket: nil,
31+
transport: nil,
32+
client_ip: nil,
33+
data: nil,
34+
error: nil,
35+
protocol_version: nil
36+
37+
@doc """
38+
Closes the `Connection`.
39+
"""
40+
@spec close(t) :: t
41+
def close(conn) do
42+
:ok = conn.transport.close(conn.socket)
43+
%__MODULE__{conn | socket: nil, transport: nil}
44+
end
45+
46+
@doc """
47+
Continues receiving messages from the client.
48+
49+
To prevent a client from flooding our process mailbox, we only receive one message at a time,
50+
and explicitly `continue` to receive messages once we finish processing the ones we have.
51+
"""
52+
@spec continue(t) :: t
53+
def continue(conn) do
54+
:ok = conn.transport.setopts(conn.socket, active: :once)
55+
conn
56+
end
57+
58+
@doc """
59+
Initializes a `Connection`.
60+
"""
61+
@spec init(port(), transport()) :: t
62+
def init(socket, transport) do
63+
{:ok, {client_ip, _port}} = :inet.peername(socket)
64+
client_ip = IO.iodata_to_binary(:inet.ntoa(client_ip))
65+
:ok = transport.setopts(socket, active: :once)
66+
67+
Logger.info(fn -> "Client #{client_ip} connected." end)
68+
69+
%__MODULE__{
70+
current_state: :handshake,
71+
socket: socket,
72+
transport: transport,
73+
client_ip: client_ip,
74+
data: ""
75+
}
76+
end
77+
78+
@doc """
79+
Stores data received from the client in this `Connection`.
80+
"""
81+
@spec put_data(t, binary) :: t
82+
def put_data(conn, data) do
83+
%__MODULE__{conn | data: conn.data <> data}
84+
end
85+
86+
@doc """
87+
Puts the `Connection` into the given `error` state.
88+
"""
89+
@spec put_error(t, any) :: t
90+
def put_error(conn, error) do
91+
%__MODULE__{conn | error: error}
92+
end
93+
94+
@doc """
95+
Sets the protocol for the `Connection`.
96+
"""
97+
@spec put_protocol(t, integer) :: t
98+
def put_protocol(conn, protocol_version) do
99+
%__MODULE__{conn | protocol_version: protocol_version}
100+
end
101+
102+
@doc """
103+
Replaces the `Connection`'s underlying socket.
104+
"""
105+
@spec put_socket(t, port()) :: t
106+
def put_socket(conn, socket) do
107+
%__MODULE__{conn | socket: socket}
108+
end
109+
110+
@doc """
111+
Updates the `Connection` state.
112+
"""
113+
@spec put_state(t, state) :: t
114+
def put_state(conn, state) do
115+
%__MODULE__{conn | current_state: state}
116+
end
117+
118+
@doc """
119+
Pops a packet from the `Connection`.
120+
"""
121+
@spec read_packet(t) :: {:ok, struct, t} | {:error, t}
122+
def read_packet(conn) do
123+
case Packet.deserialize(conn.data, conn.current_state) do
124+
{packet, rest} when is_binary(rest) ->
125+
Logger.debug(fn -> "REQUEST: #{inspect(packet)}" end)
126+
{:ok, packet, %__MODULE__{conn | data: rest}}
127+
128+
{:error, :invalid_packet} ->
129+
Logger.error(fn ->
130+
"Received an invalid packet from client, closing connection. #{inspect(conn.data)}"
131+
end)
132+
133+
{:error, put_error(conn, :invalid_packet)}
134+
end
135+
end
136+
137+
@doc """
138+
Sends a response to the client.
139+
"""
140+
@spec send_response(t, struct) :: t
141+
def send_response(conn, response) do
142+
Logger.debug(fn -> "RESPONSE: #{inspect(response)}" end)
143+
144+
{:ok, response} = Packet.serialize(response)
145+
146+
:ok = conn.transport.send(conn.socket, response)
147+
conn
148+
end
149+
end

lib/minecraft/packet.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ defmodule Minecraft.Packet do
1515
Given a raw binary packet, deserializes it into a `Packet` struct.
1616
"""
1717
@spec deserialize(binary, state :: atom, type :: :client | :server) ::
18-
{packet :: term, new_state :: atom, rest :: binary} | {:error, :invalid_packet}
18+
{packet :: term, rest :: binary} | {:error, :invalid_packet}
1919
def deserialize(data, state, type \\ :client)
2020

21-
def deserialize(data, :handshaking, type) when is_binary(data) do
21+
def deserialize(data, :handshake, type) when is_binary(data) do
2222
{_packet_size, data} = decode_varint(data)
2323
{packet_id, data} = decode_varint(data)
2424
Handshake.deserialize(packet_id, data, type)

lib/minecraft/packet/handshake.ex

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ defmodule Minecraft.Packet.Handshake do
33
Serialization and deserialization routines for handshake packets.
44
"""
55
alias Minecraft.Packet.Client
6-
alias Minecraft.Protocol
76
import Minecraft.Packet
87

98
@type packet_id :: 0
@@ -12,8 +11,7 @@ defmodule Minecraft.Packet.Handshake do
1211
Deserializes a handshake packet.
1312
"""
1413
@spec deserialize(packet_id, binary, type :: :client | :server) ::
15-
{packet :: term, new_state :: Protocol.state(), rest :: binary}
16-
| {:error, :invalid_packet}
14+
{packet :: term, rest :: binary} | {:error, :invalid_packet}
1715
def deserialize(0 = _packet_id, data, :client = _type) do
1816
{protocol_version, rest} = decode_varint(data)
1917
{server_addr, rest} = decode_string(rest)
@@ -32,7 +30,7 @@ defmodule Minecraft.Packet.Handshake do
3230
next_state: next_state
3331
}
3432

35-
{packet, next_state, rest}
33+
{packet, rest}
3634
end
3735

3836
def deserialize(_, _, _) do

lib/minecraft/packet/status.ex

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ defmodule Minecraft.Packet.Status do
44
"""
55
alias Minecraft.Packet.Client
66
alias Minecraft.Packet.Server
7-
alias Minecraft.Protocol
87
import Minecraft.Packet
98

109
@type packet_id :: 0..1
@@ -13,25 +12,24 @@ defmodule Minecraft.Packet.Status do
1312
Deserializes a status packet.
1413
"""
1514
@spec deserialize(packet_id, binary, type :: :client | :server) ::
16-
{packet :: term, new_state :: Protocol.state(), rest :: binary}
17-
| {:error, :invalid_packet}
15+
{packet :: term, rest :: binary} | {:error, :invalid_packet}
1816
def deserialize(0 = _packet_id, data, :client = _type) do
19-
{%Client.Status.Request{}, :status, data}
17+
{%Client.Status.Request{}, data}
2018
end
2119

2220
def deserialize(1 = _packet_id, data, :client) do
2321
<<payload::64-signed, rest::binary>> = data
24-
{%Client.Status.Ping{payload: payload}, :status, rest}
22+
{%Client.Status.Ping{payload: payload}, rest}
2523
end
2624

2725
def deserialize(0 = _packet_id, data, :server) do
2826
{json, rest} = decode_string(data)
29-
{%Server.Status.Response{json: json}, :status, rest}
27+
{%Server.Status.Response{json: json}, rest}
3028
end
3129

3230
def deserialize(1 = _packet_id, data, :server) do
3331
<<payload::64-signed, rest::binary>> = data
34-
{%Server.Status.Pong{payload: payload}, :status, rest}
32+
{%Server.Status.Pong{payload: payload}, rest}
3533
end
3634

3735
def deserialize(_, _, _) do

lib/minecraft/protocol.ex

Lines changed: 38 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,11 @@ defmodule Minecraft.Protocol do
55
"""
66
use GenServer
77
require Logger
8+
alias Minecraft.Connection
89
alias Minecraft.Protocol.Handler
9-
alias Minecraft.Packet
1010

1111
@behaviour :ranch_protocol
1212

13-
@typedoc """
14-
The possible states a client/server can be in.
15-
"""
16-
@type state :: :handshaking | :status | :login | :play
17-
18-
defmodule State do
19-
@moduledoc false
20-
defstruct [:current, :socket, :transport, :client_ip]
21-
end
22-
2313
@impl true
2414
def start_link(ref, socket, transport, protocol_opts) do
2515
pid = :proc_lib.spawn_link(__MODULE__, :init, [{ref, socket, transport, protocol_opts}])
@@ -33,66 +23,57 @@ defmodule Minecraft.Protocol do
3323
@impl true
3424
def init({ref, socket, transport, _protocol_opts}) do
3525
:ok = :ranch.accept_ack(ref)
36-
{:ok, {client_ip, _port}} = :inet.peername(socket)
37-
client_ip = :inet.ntoa(client_ip)
38-
39-
state = %State{
40-
current: :handshaking,
41-
socket: socket,
42-
transport: transport,
43-
client_ip: client_ip
44-
}
45-
46-
:ok = transport.setopts(socket, active: :once)
47-
Logger.info(fn -> "Client #{client_ip} connected." end)
48-
:gen_server.enter_loop(__MODULE__, [], state)
26+
conn = Connection.init(socket, transport)
27+
:gen_server.enter_loop(__MODULE__, [], conn)
4928
end
5029

5130
@impl true
52-
def handle_info({:tcp, socket, packet}, state) do
53-
case Packet.deserialize(packet, state.current) do
54-
{packet, current, rest} when is_binary(rest) ->
55-
Logger.debug(fn -> "REQUEST: #{inspect(packet)}" end)
56-
57-
if byte_size(rest) > 0 do
58-
send(self(), {:tcp, socket, rest})
59-
end
60-
61-
handle_packet(packet, socket, current, state)
62-
63-
{:error, :invalid_packet} ->
64-
Logger.error(fn -> "Received an invalid packet from client, closing connection." end)
65-
{:stop, :normal, state}
66-
end
31+
def handle_info({:tcp, socket, data}, conn) do
32+
conn
33+
|> Connection.put_socket(socket)
34+
|> Connection.put_data(data)
35+
|> handle_conn()
6736
end
6837

69-
def handle_info({:tcp_closed, socket}, state) do
70-
Logger.info(fn -> "Client #{state.client_ip} disconnected." end)
71-
:ok = state.transport.close(socket)
72-
{:stop, :normal, state}
38+
def handle_info({:tcp_closed, socket}, conn) do
39+
Logger.info(fn -> "Client #{conn.client_ip} disconnected." end)
40+
:ok = conn.transport.close(socket)
41+
{:stop, :normal, conn}
7342
end
7443

7544
#
7645
# Helpers
7746
#
47+
defp handle_conn(%Connection{data: ""} = conn) do
48+
conn = Connection.continue(conn)
49+
{:noreply, conn}
50+
end
51+
52+
defp handle_conn(%Connection{} = conn) do
53+
case Connection.read_packet(conn) do
54+
{:ok, packet, conn} ->
55+
handle_packet(packet, conn)
56+
57+
{:error, conn} ->
58+
conn = Connection.close(conn)
59+
{:stop, :normal, conn}
60+
end
61+
end
7862

79-
defp handle_packet(packet, socket, current, state) do
80-
case Handler.handle(packet) do
81-
{:ok, :noreply} ->
82-
:ok = state.transport.setopts(socket, active: :once)
83-
{:noreply, %State{state | current: current}}
63+
defp handle_packet(packet, conn) do
64+
case Handler.handle(packet, conn) do
65+
{:ok, :noreply, conn} ->
66+
handle_conn(conn)
8467

85-
{:ok, response_packet} ->
86-
Logger.debug(fn -> "RESPONSE: #{inspect(response_packet)}" end)
87-
{:ok, response} = Packet.serialize(response_packet)
88-
:ok = state.transport.setopts(socket, active: :once)
89-
:ok = state.transport.send(socket, response)
90-
{:noreply, %State{state | current: current}}
68+
{:ok, response, conn} ->
69+
conn
70+
|> Connection.send_response(response)
71+
|> handle_conn()
9172

92-
err ->
73+
{:error, _, conn} = err ->
9374
Logger.error(fn -> "#{__MODULE__} error: #{inspect(err)}" end)
94-
:ok = state.transport.close(socket)
95-
{:stop, :normal, state}
75+
conn = Connection.close(conn)
76+
{:stop, :normal, conn}
9677
end
9778
end
9879
end

0 commit comments

Comments
 (0)