0

I have a single TCP connection to a server, but possibly have multiple requests at the same time. Most of the time the response will be so big that I would constantly receive lot of data chunks. It's possible for me to check the data length in order to determine it is the END OF STREAM. But with multiple requests, sometimes packets "mixed" with another requests that causes a lot of failures.

For example,

for a normal request:

  • -> request #1 1/3
  • -> request #1 2/3
  • -> request #1 3/3
  • -> request #1 completed

in real life:

  • -> request #1 1/3
  • -> request #1 2/3
  • -> request #2 1/3
  • -> request #2 2/3
  • -> failure at some point

For now, my only thought is to make it serially by serving the request one after one. However it can't fully solve my problem because sometimes I got subscribed events comes in without control.

How can I solve this TCP problem in general?

I'm showing some of the code below (in case somebody knows erlang and elixir)

# Create TCP connection
{:ok, socket} =
      :gen_tcp.connect(host_charlist, port, [:binary, active: true, keepalive: true])

# Send request
def handle_call({:send, msg}, from, state) do
  :ok = :gen_tcp.send(state.socket, msg)
  new_state = %{state | from: from, msg: ""}

  {:noreply, new_state}
end

# When it receive packet
def handle_info({:tcp, _socket, msg}, state) do
  new_state = %{state | msg: state.msg <> msg}
  current_msg_size = byte_size(new_state.msg)
  defined_msg_size = Response.get_body_size(new_state.msg) # this msg size can read from the first packet's header

  cond do
    current_msg_size == defined_msg_size ->
      GenServer.reply(new_state.from, {:ok, new_state.msg})
      {:noreply, %{new_state | msg: ""}}

    current_msg_size > defined_msg_size ->
      GenServer.reply(new_state.from, {:error, "Message size exceed."})
      {:noreply, %{new_state | msg: ""}}

    true ->
      {:noreply, new_state}
  end
end
CW LOK
  • 3
  • 1
  • I assume that you don't control the server, nor the protocol, you just want to write a client. If so, you need to learn the specification of this protocol. Every protocol which may have several requests on the same TCP connection (DNS, HTTP/2) has a specific way to deal with it. – bortzmeyer May 09 '22 at 16:36
  • It is just TCP in binary. Yes I have no control of the servers, to me it's like a black box. – CW LOK May 10 '22 at 04:17
  • "TCP in binary" does not really mean anything. My point is: if you don't control the server, do you have at least a specification / documentation of the protocol? Otherwise, it means reverse-engineering it, which is often long and painful. (Chen Yu's comment "It depends on your detailed protocols specification" is also a good read). – bortzmeyer May 10 '22 at 07:27

2 Answers2

3

At TCP level, in a connection, request and response do no exist, it's a single tube transferring bytes from one side to the other in order.

In order to handle interleaving over a single connection you have to handle it one level up the stack.

Possible solutions include:

  1. Serializing
  2. Framing: You frame the responses and guarantee somehow that frames are sent completely without interleaving in the server, then your receiver can inspect each frame (maybe spanning multiple receives) and assign it to the corresponding request.
  3. One connection for each request: Let the OS handle the interleaving on the wire at the expense of a socket and the handshake each time
José M
  • 3,294
  • 1
  • 14
  • 16
  • Do you mind explaining more about framing? – CW LOK May 10 '22 at 04:17
  • 1
    In many network protocols, messages are "framed", each message is structured on the wire with fields like length of the message, header of the message with a specific message identifier, etc. The written specification is supposed to describe this framing. Again, if you know nothing about the format of the messages, your task will be hard because you will need to reverse-engineer (find the specification from the bits), which is a complicated task. – bortzmeyer May 10 '22 at 07:30
  • There is no identifier. Header only comes with the first packet, the rest is just the body fragments (without header). – CW LOK May 10 '22 at 09:06
  • OK, let me try another way: how, in this protocol, a client is supposed to demultiplex responses corresponding to different requests? As seen in José M's response, there are several ways but we don't know how it is done in this specific (unspecified?) protocol. – bortzmeyer May 10 '22 at 15:37
-1

You can change the tcp option from active = true to active = false or to active = once

:gen_tcp.connect(host_charlist, port, [:binary, active: true, keepalive: true])

Then you can control the receiving of message by yourself, not by flooded by incoming messages.

1、receive upper's calling, process your client request, set active = false, waiting for server's response.

2、Try not to receive only related message, ignore these in the process's message queue or save these in the buffer.

3、when receive server's response, process it, then set active = once.

4、if timeout, set active = once.

https://www.erlang.org/doc/man/gen_tcp.html#controlling_process-2

controlling_process(Socket, Pid) -> ok | {error, Reason} Types Socket = socket() Pid = pid() Reason = closed | not_owner | badarg | inet:posix() Assigns a new controlling process Pid to Socket. The controlling process is the process that receives messages from the socket. If called by any other process than the current controlling process, {error, not_owner} is returned. If the process identified by Pid is not an existing local pid, {error, badarg} is returned. {error, badarg} may also be returned in some cases when Socket is closed during the execution of this function.

If the socket is set in active mode, this function will transfer any messages in the mailbox of the caller to the new controlling process. If any other process is interacting with the socket while the transfer is happening, the transfer may not work correctly and messages may remain in the caller's mailbox. For instance changing the sockets active mode before the transfer is complete may cause this.

Chen Yu
  • 3,955
  • 1
  • 24
  • 51
  • On active,X mode, messages arrive in the same order to the process as they arrive at the socket, so using passive mode instead does not solve the interleaving issue – José M May 09 '22 at 15:06
  • When in active = false, the tcp message will be blocked until you call gen_tcp:receive to receive the message. So it is easy to control the program. when receive unrelated message(interleaving message), ignore it or let other process to do it, just focus on the current calling. It makes thing easy to handle. It is one way to solve the problem, not just this way. – Chen Yu May 09 '22 at 22:48
  • The issue, as I understand from the snippet, is that the bytes are interleaved in the connection because of concurrent requests. Whether active or passive mode is used does not affect the order of the bytes in the socket. – José M May 10 '22 at 00:58
  • I think one of the problem is hard to identify the upcoming packet belongs to which request, I can't ignore most of the packets especially I'm handling concurrent requests. – CW LOK May 10 '22 at 04:12
  • It is related to the protocol itself, message should have header and body part, when receiving message, parse header and find the clue, use `dict` keep the clue and start a timer for later related message. If timeout or related message completed delete the clue form `dict` and delete related `timer`. – Chen Yu May 10 '22 at 04:18
  • Alright, but how would I know which clue belongs to which header? – CW LOK May 10 '22 at 06:25
  • 1
    It depends on your detailed protocols specification, if the protocol designed for multi-channel communication, it must include such "id", "remark", "session","code" etc, such information can be used to distinguish different requests. If the protocol is designed for sigle-channel communication, it will be trouble, you should modify the protocol to add such information. – Chen Yu May 10 '22 at 06:31