-
Notifications
You must be signed in to change notification settings - Fork 13.3k
Server closes connection after client shuts down the sending side of its socket #2569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Here comes a small C client that connects to the server running on the ESP (change the IP address accordingly), shuts down the sending side of its socket (unless #include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <sys/errno.h>
#include <unistd.h>
#define SHUTDOWN_WRITE 1
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
fprintf(stderr, "Failed to create socket: %s\n", strerror(errno));
return 1;
}
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("192.168.178.42");
serv_addr.sin_port = htons(1400);
if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
fprintf(stderr, "Failed to connect to host: %s\n", strerror(errno));
return 1;
}
#if SHUTDOWN_WRITE
if (shutdown(sockfd, SHUT_WR) < 0) {
fprintf(stderr, "Failed to shut down writing side of socket: %s\n", strerror(errno));
return 1;
}
#endif
char response[256];
while (1) {
memset(response, 0, sizeof(response));
int n = read(sockfd, response, sizeof(response) - 1);
if (!n) {
// EOF (server closed connection)
break;
}
if (n < 0) {
fprintf(stderr, "Failed to read from socket: %s\n", strerror(errno));
return 1;
}
printf("%s", response);
}
close(sockfd);
return 0;
} If the |
Looks like the issue is caused by the receive callback |
This function returns ERR_ABRT because it first calls |
it's there because that is the only indication we have that the connection is closed from the remote end. Else we will have pcb objects around and fill the maximum of 5 quite easily. |
In the "active close" (we're the closing side) case, the socket will be go to the FIN_WAIT1 state. CLOSE_WAIT is used only for the "passive close" (they're the closing side) case. After a "passive full close" (both directions closed), the remote side will respond with RST packets to anything we're sending. After "passive half close" (them->us closed by them, us->them open), the remote side will respond with the normal ACKs. I don't think there is a way to distinguish the two without sending something to the remote side and handling the RST in case of a "passive full close". |
Thanks for clarifying :) |
I'm currently building both client and server, so I can just keep both directions of the socket open until the response has been received. The browsers and tools I tested also seemed to behave that way. Strictly speaking, the server must close the socket explicitly once it is done with the client. If a client dies (power button, network cable), it might never send a FIN, so the server will probably run out of resources if it doesn't close the socket itself. Most of the time, sockets are closed automatically in the destructor of some higher-level class. If they're not, I'd rather consider it a bug in that higher-level class (or in the application) than in the socket implementation. I think linux just keeps the socket half-open indefinitely. Running out of resources is not much of an issue there. |
we can have a total of 4 or 5 client connections at any one time on ESP8266. Keepnig unnecessary connections open is really not an option here. In AsyncTCP I have implemented timeouts and so on that can coop with sudden loss of connection, but imagine this: You have 4 connections already open, one drops and then tries to reconnect. It will be stopped in SYN because the ESP has all connections open (I have been there many times) |
@chschu @me-no-dev @igrr |
This is where it happens: https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/src/include/ClientContext.h#L463 A setting could be introduced to not close the connection, but it would be user's responsibility to do so. That is the only solution I can come up with. |
Closing the pcb will still be done automatically once WiFiClient goes out
of scope, i.e. ~WiFiClient calls ClientContext::unref which calls
ClientContext::close which calls tcp_close/tcp_abort.
So perhaps application doesn't need to do anything?
The only consideration against this change is that which @me-no-dev has
given a few comments above: in case the remote end closes connection and
immediately tries to re-connect, we will have two live pcbs for some time
instead of one, which will affect memory use.
Let's keep this open and revisit after the upcoming release.
…On Thu, Oct 12, 2017 at 1:50 PM, Me No Dev ***@***.***> wrote:
This is where it happens: https://github.com/esp8266/
Arduino/blob/master/libraries/ESP8266WiFi/src/include/ClientContext.h#L463
A setting could be introduced to not close the connection, but it would be
user's responsibility to do so. That is the only solution I can come up
with.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#2569 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AEJceuQxVbgJSCVe_mAbFHJQAW5Y2Qcgks5sraimgaJpZM4KMAjO>
.
|
@igrr @me-no-dev code has advanced, including changes in ClientContext. Is there anything further to look into for this one? |
By not sending response to client, client.status() remains 4 but client data doesn't get printed after that. Is it mandatory to close connection after every query by a client? |
@d-a-v do you know anything about this? |
Hi all! This has been a very useful thread, thanks for bringing this up @chschu! |
I didn't try to follow your reasoning, I have first a simple question for you @kirillandy, |
Hi! Yes, I was able to recreate it and it was exactly as @chschu described it. I monitored the whole thing in WireShark just like he did. Although this was a couple months ago when I asked the question. It was a little bit of an inconvenient problem but I managed to change my project concept to bypass the RST packet, but it would be helpful to get a clear explanation of what the issue was about. I believe that the core at the time of my post here was already using LWIP 2 (not sure which version exactly), based on the verbose output of the compiler and linker. EDIT: also, looking at my installed Arduino platforms in the Arduino IDE, I've got ESP8266 2.5.2, whereas the lastest one is 2.6.3. Should I update that as well? |
You can update, but lwIP is I believe the exact same version stable-2.1.2. |
So here is a simple check. It is somewhat similar to what@chschu did in the original test. IDE Settings Sketch for ESP8266 Server
There are 2 versions of the sketch. 1) where the client is the first to disconnect and 2) where the server is the first to disconnect. To change them, simply uncomment the serverClient.stop() line in the loop() function (commented = sketch 1, uncommented = sketch 2) Closing the connection on the client side Closing the connection on the server side To clarify, my Linux command for this experiment is different because if I use the |
could you try to read all the data sent by the client? |
You mean to try a serverClient.read() on my ESP after the connection has been terminated (so after the "while serverClient.status() != CLOSED" loop)? |
no. before it. |
Oh, oops, I actually did everything before seeing your reply @JAndrassy, but there are some things to note, nonetheless. I don't see where I can put a read before the connection closes. That's just a normal read in the ESTABLISHED state...
And here is a screenshot showing a couple of tests. I sent 2 separate messages (so I connected to the server twice). WireShark at the top only shows what happened for the 2nd message (I forgot to open it :)). I read everything in the reception buffer after the connection is aborted. I also added a long delay(5000) to be extra sure. |
you can read the available data even the peer closed the connection. they are buffered in esp8266. but after stop() you can't read data. for some time now in ESP8266WiFi library connected() for not secure connection returns false even if data are available. #6701 |
This didn't use to be the case. I strongly remember the server losing any unread data after the client closes the connection with a FIN which is why I had to change my algorithm so that the server always returned 4 predefined bytes to indicate that it had gotten and read everything from the client. I guess this was changed by version 2.6.3 and I was using 2.5.2. (Sidenote: I couldn't close the connection on the server side (active close) because then it would take a few seconds for me to be able to connect to the server again. I think this is because the server stays in the TIME_WAIT state for 2xMSL. This was inconvenient because I needed the server to be available asap after a session closes, so I could only close connections on the client side). |
So, to summarize, I carried out the same experiment with 2 ESPs and got the same result. If the client calls stop(), the server will reset but any unread data will stay in the server reception buffer. Which is fine by me. |
This is linked to #6701.
@JAndrassy this is not forgotten by maintainers and is likely to be changed back for core-v3.0.0. @kirillandy Since you have a fine view of the initialization and de-initialization of a TCP link, you might find a way to improve the logic under the hood, or propose new |
@d-a-v I can't say I'll be able to offer anything worth considering. On the one hand, this fact does make writing error-checking code using different socket libraries a bit awkward (where a "reset" error is actually not a real error and should be ignored because that's just how the ESP8266 is programmed to work). On the other hand it's not that big of a problem for me and I do have some breathing space. |
If I may, I really don't want to flood this issue with posts, but I have just encountered another side-effect of this problem, which I think is the original reason why I posted here in the first place. Even though I have established that the receive buffer is not freed by the server after the client disconnects, I thought that this was just too simple, it was not exactly the problem which I had 2-3 months ago. Apologies for my erratic change in opinion.
So this is what I was frustrated about back in November. And it doesn't seem to fit in with what we have discovered these past 2 days: if a client disconnecting does NOT free the server-side buffer AFTER the server.available() was done, why would you free the buffer BEFORE I even execute available? Code
|
and what is recorded in Wireshark? |
You had me worried there,@JAndrassy, I always forget an obvious part of every experiment I do and things turn out to be simpler than I think they are. Unfortunately, this wasn't the case 🤔 Same exact situation. The connection is set up, the data is transmitted, then a RST happens. We've seen it all before. But just because I didn't call Server.available() all information on the server-side is lost and the server has no idea that anything happened. So we've got 2 completely different behaviors for the exact same scenario: if .available() WAS called, then the rec buffer is saved, whereas if .available() WASN'T called, the rec buffer vanishes. But in either case the connection is allowed to go through and data transmission occurs. EDIT: (Personally, I suppose the first is much more logical and preferable than the second :)) |
I am afraid that in the scenario where you call available(), data larger then some internal buffer (or one tcp packet?) would not be preserved. So it is the same scenario. With RST esp8266 indicates that it is 'not happy' with the situation? (connection closed by the client too early) |
I do understand the worries about overflow and discarding the buffer if we have more data than some segment size. But we're not talking about data that is larger than an internal buffer... I'm sending a measly 5-8 bytes in all my experiments :) Here's a better explanation of what bothers me.
Finally, there are 2 possible solutions I can think of to avoid this ambiguity:
|
We have |
Sorry for the slight delay. I've got SetDefaultSync(true) in the setup() function on both sides which is the same, basically, isn't it? According to the docs: https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/client-class.html I also remember using flush() but it used to immediately return and didn't help. Either way, what will it prove? If it only assures that the data left the send buffer (as you claim), then that isn't much help at all. If it guarantees that the ACK for all the sent data was received, then we still have the ambiguity problem which I described in my previous post. |
Some thoughts (@d-a-v please correct me if I'm wrong):
|
TBH I don't really understand the Arduino's About the ambiguity you raised, I agree with 1) but 2), while being wrong today, is doable even though I don't like it because 1) is possible and more async, more fluent.
(I have still not made the test myself) |
Thanks for looking into it David! I see. So, if I'm reading this right, are we talking about a lower-level problem involving the specifics of the lwIP code, and it's more of a question for the devs of that library? |
I see it as a problem of mapping the LWIP API to Arduino API. Arduino API is not assync so the sketch must poll server and clients as all examples show it. |
@JAndrassy The underlying TCP management is async. @kirillandy Assuming you are working with latest git, can you try to change With that I always get "note2" and I can't explain right now why you didn't get it (before the backlog limit PR). edit:
That's because when client is trying to make connections but no server is listening, lwIP seems to be recording the attemps, then when server is started the attemps are registering then immediately closed (closed: no pcb: segfault). That's odd, still under investigation. update: that's not odd since the server is update2: This fixes a bug in tcp backlog use. |
In other words, @JAndrassy, you mean I need to write my code to maximize the probability of my server getting the Client object from .available() during the brief period when my client is connected? The most obvious idea would be to implement a "server .stop()s first" pattern where the server disconnects first, triggering a normal "active close" in the TCP diagram. I can't really use this because it takes time for my clients to be able to connect to the server again, because it goes into the TIME_WAIT state. Not saying it's terrible, it might be ideal for other projects, to each their own. Otherwise, if I stick to my "client .stop()s first" preference, then I must write my server code in a way for it to almost always get Clients from .available() during the period when the client executes its connect->write->stop code. I agree this might work well with the examples you mentioned (you're referring to the examples in the docs of this repository, right? https://github.com/esp8266/Arduino/tree/0a58172c6af6c96b8caa1fe5aa4aaa44963d1bc3/doc/esp8266wifi) |
I'm on it @d-a-v |
that is what I attempted to say: async api behind 'poll' api problem |
@d-a-v |
Of course. And TCP is here to guarantee to the client that the message has been acknowledged by server. In that case server should get it. If for any reason that cannot be honored, then client should not be able to connect in the first place. |
@kirillandy |
Thanks. No, Git is fine. It's about time I got it all set up. Haven't had to do this type of fine-tuning work yet, so it's all a beneficial learning process for me. |
Basic Infos
Hardware
Hardware: ESP-12F on NodeMCU clone (Geekcreit Doit)
Core Version: 2.3.0
Description
A WiFiClient instance (or the underlying TCP/IP lib) obtained from WiFiServer::available() seems to close the TCP connection when it receives a FIN packet. According to the TCP spec, it should go to the CLOSE_WAIT state, which would still allow data to be sent in the opposite direction.
This happens if a client connects to a server running on the ESP, sends some data and shuts down the writing side of the socket before receiving a response. The client socket's reading side is still open in this case, and the server should be able to send a response to the client.
This issue also occurs with ESP8266WebServer, because it uses WiFiServer/Client.
Settings in IDE
Module: NodeMCU 1.0 (ESP-12E Module)
Flash Size: 4MB (3M SPIFFS)
CPU Frequency: 80Mhz
Upload Using: SERIAL
Sketch
Debug Messages
Client that shuts down the sending side immediately
Shell command:
(sending side is shut down immediately, because input pipe of netcat is closed after
sleep 0
)Client output:
Serial output:
(4 is ESTABLISHED, 0 is CLOSED)
Wireshark capture:

Client that waits for the server to close the connection (works correctly)
Shell command:
(this gives the server enough time to respond and close the connection)
Client output:
Serial output:
(4 is ESTABLISHED, 0 is CLOSED)
Wireshark capture:

The text was updated successfully, but these errors were encountered: