为什么socket服务器断开之后客户端还能发送一次数据呢?

文章目录

    • 为什么socket服务器断开之后客户端还能发送一次数据呢?
  • 一、了解背后的原因
    • 1、客户端是如何将数据发送给服务器端的?(服务器端发客户端同理)
    • 2、服务器端程序退出后,客户端继续发送数据会发生什么?
    • 3、问题描述:
    • 4、问题原因:
    • 5、问题复现:
  • 二、解决办法
    • 1、如果只处理客户端程序退出而不对发送失败的数据作要求
    • 2、如果既处理客户端程序退出也要准确判断发送的数据是否失败
      • (1)getsockopt()函数:获取socket状态
      • (2) read()/write()和send()/recv()判断
      • (3)select、poll、epoll判断
  • 三、总结

最近遇到一个大坑:发现服务器端断开连接时,客户端还能write成功一次,然后客户端程序退出了,不过服务器端是没有收到的,而且我的服务器数据库里面也没有保存。最后一次能write成功估计是将数据写入到缓冲区里面了,但是客户端还没确定服务器是否已经断开了连接,所以能write成功。因为项目对数据比较敏感,没有成功发送给服务器端的数据需要保存在客户端本地数据库,等客户端重新连接上服务器后再将该数据发送出去,后来我用了下面函数解决了问题,在write之前调用该函数即可:

//头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <libgen.h>
#include <linux/tcp.h>
#include <netinet/in.h>
#include <netinet/ip.h>//函数
int SocketConnected(int sockfd)
{struct tcp_info info;if (sockfd <= 0)return 0;int len = sizeof(info);getsockopt(sockfd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *) & len);if ((info.tcpi_state == 1)) {printf("socket connected\n");return 1;} else {printf("socket disconnected\n");return 0;}
}

一、了解背后的原因

1、客户端是如何将数据发送给服务器端的?(服务器端发客户端同理)

当调用send()或者write()发送数据时,并不是直接将数据发送到网络中,而是先将待发送的数据放到socket发送缓冲区中,然后由TCP协议执行数据的网络发送。send()/write()函数不保证数据能够通过网络成功发送出去,只要能够将数据写入到socket发送缓冲区,send()/write()就可以成功返回。

注意:1.一般来说,读字符终端、网络的socket描述字,管道文件等,这些文件的缺省read都是阻塞的方式。 2.如果是读磁盘上的文件,一般不会是阻塞方式的。如果客户端发送一个空的数据给服务器,服务器的read()将一直阻塞到客户端发的数据不为空为止。同理如果服务器端发送一个空的数据给客户端,客户端的read()也将阻塞到服务器端发送的数据不为空为止。

某个客户端和服务器端的正常连接状态如下:

2、服务器端程序退出后,客户端继续发送数据会发生什么?

当服务器socket突然关闭时,客户端并不知情,此时客户端调用send()/write()依然可以将数据放置到socket发送缓冲区中,所以send()/write()成功返回。

直到TCP协议执行发送动作的时候才发现不对劲,这时客户端才拿到服务器socket已关闭的信息,由于TCP协议无法继续网络数据的传输,所以系统自动关闭客户端socket发送缓冲区的读端。这时,再次调动send()/write()函数,就会导致程序终止,原因是收到了SIGPIPE信号。

SIGPIPE信号产生的规则:当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送SIGPIPE信号。

了解一下除了SIGPIPE信号之外还有哪些信号:

3、问题描述:

当服务器close一个连接时,若client端进程接着发数据,根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,数据还是能存到缓冲区,之后系统会发出一个SIGPIPE信号给该进程,告诉进程这个连接已经断开了,不要再写了,如果client再发送数据到缓冲区,是不能成功的。又或者当一个进程向某个已经收到RST的socket执行写操作时,内核向该进程发送一个SIGPIPE信号。该信号的缺省处理是终止进程,因此进程必须捕获它以免不情愿的被终止。

我遇到的情况是服务器socket句柄已关闭,然后客户端向一个已关闭的服务端连接句柄中执行写操作,从而产生了SIGPIPE信号。

4、问题原因:

根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出),所以进程会退出。

系统里边定义了三种处理方法:
1)SIG_DFL //默认处理信号
2)SIG_IGN //忽略信号
3)SIG_ERR //出错

根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出),所以进程会退出。若不想客户端退出,需要把 SIGPIPE默认执行动作屏蔽。

5、问题复现:

到了这里,大概可以了解这其中的原因了,下面用一个客户端和服务器端的代码进行测试,复现我遇到的问题,并进行分析。

//客户端程序:
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <string.h>int main(int argc, char *argv[])
{int     sockfd;int     connect_rv ;char    write_buf[128] = {0};char    read_buf[128] = {0};int     read_rv;sockfd = socket(AF_INET, SOCK_STREAM,0);if(sockfd == -1){printf("create socket fail\n");return -1;}struct sockaddr_in server;memset(&server, 0, sizeof(struct sockaddr_in));server.sin_family = AF_INET;server.sin_port = htons(6666);//无论是端口还是地址,网络字节序都是大端字节序,所以要进行相应的转换。server.sin_addr.s_addr = inet_addr("127.0.0.1");connect_rv = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if(connect_rv == -1){perror("connect fail");return -2;}//实时判断标准输入是否有数据输入,不过需要注意的是fgets()会将回车符\n也读到write_buf中while(fgets(write_buf,sizeof(write_buf), stdin) != NULL){write(sockfd, write_buf, sizeof(write_buf));//将标准输入的数据发送给服务器端//printf("write_rv:%d  errno:%s\n", write_rv, strerror(errno));memset(read_buf, 0, sizeof(read_buf));read_rv = read(sockfd, read_buf, read_rv);//接收服务器发送过来的数据//printf("read_rv:%d  errno:%s\n", read_rv, strerror(errno));if(read_rv<0){printf("read fail\n");}puts(read_buf);//将读到的内容存到数组里面}close(sockfd);return 0;
}
//服务器端
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>int main()
{
int     sockfd = -1;
int     bind_rv = -1;
int     listen_rv = -1;
int     connect_fd = -1;
int     read_rv = -1;
char    buf[128];sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd == -1){perror("create socket fail");return -1;}struct sockaddr_in server;//创建结构体,记得使用memset清空内存memset(&server, 0, sizeof(struct sockaddr_in));server.sin_family = AF_INET;server.sin_port = htons(6666);server.sin_addr.s_addr = htonl(INADDR_ANY);bind_rv= bind(sockfd, (const struct sockaddr *)&server, sizeof(server));if(sockfd == -1){perror("bind the IP and port fail");return -2;}listen_rv = listen(sockfd, SOMAXCONN);struct sockaddr_in client;socklen_t len = sizeof(client);//accept()第三个参数是存放长度变量的地址connect_fd = accept(sockfd, (struct sockaddr *)&client, &len);if(connect_fd == -1){perror("accept fail");return -3;}while(1){memset(buf,0,sizeof(buf));//使用memset,确保每次读取数据之前,buf是空的read_rv=read(connect_fd, buf, sizeof(buf));//接收客户端发送过来的数据,并存到数组buf中printf("read_rv:%d\n", read_rv);if(read_rv < 0){printf("read  fail\n");}fputs(buf, stdout);//将buf的数据打印到标准输出上write(connect_fd, buf, read_rv);//将buf发给客户端printf("write_rv:%d\n\n\n", write_rv);}close(sockfd);return 0;
}

运行服务器端和客户端程序:


如果没有在命令行上写入任何字符,就回车发送,客户端会将“\n”发送给服务器端。

二、解决办法

1、如果只处理客户端程序退出而不对发送失败的数据作要求

可用signa()和sigaction()函数进行信号注册,对SIGPIPE进行响应处理。将SIGPIPE的默认处理方法屏蔽,可用signal(SIGCHLD,SIG_IGN)或者重载其处理方法。两者区别在于signal设置的信号句柄只能起一次作用,信号被捕获一次后,信号句柄就会被还原成默认值了;sigaction设置的信号句柄,可以一直有效,值到你再次改变它的设置。具体代码如下:

struct sigaction action;action.sa_handler = handle_pipe;sigemptyset(&action.sa_mask);action.sa_flags = 0;sigaction(SIGPIPE, &action, NULL);void handle_pipe(int sig){//不做任何处理即可}在源文件中要添加signal.h头文件:#include <signal.h>
//头文件
#include <signal.h>
signal(SIGPIPE, handle_pipe);void handle_pipe(int sig)
{if(sig == SIGPIPE){}//不做任何处理即可
}

很奇怪的就是,我在使用这个方法的时候,我给客户端安装SIGPIPE信号,并对该信号进行忽略处理,就read不到服务端发送过来的数据了,read读到的数据为0,但是服务器端显示是正常发送的。

2、如果既处理客户端程序退出也要准确判断发送的数据是否失败

(1)getsockopt()函数:获取socket状态

头文件:
#include <sys/types.h>
#include <sys/socket.h>定义函数:int getsockopt(int s, int level, int optname, void* optval, socklen_t* optlen);

函数说明:getsockopt()会将参数s 所指定的socket 状态返回. 参数optname 代表欲取得何种选项状态, 而参数optval 则指向欲保存结果的内存地址, 参数optlen 则为该空间的大小. 参数level、optname 请参考setsockopt().

返回值:成功则返回0, 若有错误则返回-1, 错误原因存于errno

错误代码:
1、EBADF 参数s 并非合法的socket 处理代码
2、ENOTSOCK 参数s 为一文件描述词, 非socket
3、ENOPROTOOPT 参数optname 指定的选项不正确
4、EFAULT 参数optval 指针指向无法存取的内存空间

范例:

//头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <libgen.h>
#include <linux/tcp.h>
#include <netinet/in.h>
#include <netinet/ip.h>//这定义了一个函数socketconnected(),里面调用了getsocket()函数进行判断客户端和服务器端的连接状态,
//如果正常连接则返回1,否则返回0;因此就可以解决服务器断开,而客户端还能write()/send()一次的问题。
int socketconnected(int sockfd)
{struct tcp_info info;//其实我们就是使用到tcp_info结构体的tcpi_state成员变量来存取socket的连接状态,int len = sizeof(info);//如果此返回值为1,则说明socket连接正常,如果返回值为0,则说明连接异常。//所以我们也可以直接用一个整形变量来存这个值,然后进行判断即可。if (sockfd <= 0)return 0;getsockopt(sockfd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *) & len);if((info.tcpi_state == 1)) {printf("socket connected\n");return 1;} else {printf("socket disconnected\n");return 0;}
}

(2) read()/write()和send()/recv()判断

用 read()/write()和send()/recv()取判断socket连接状态时,会面临一个问题就是,这几个函数是阻塞的。

当read()/recv()返回值小于等于0时,socket连接断开。但是还需要判断 errno是否等于 EINTR,如果errno == EINTR 则说明read()/recv()函数是由于程序接收到信号后返回的,socket连接还是正常的,不应close掉socket连接。

ssize_t write(int fd, const void*buf,size_t nbytes)

write函数将buf中的nbytes字节内容写入文件描述符fd,成功时返回写的字节数,失败时返回-1,并设置errno变量,在网络程序中,当我们向套接字文件描述符写时有两可能.

1)write的返回值大于0,表示写了部分或者是全部的数据. 这样我们用一个while循环来不停的写入,但是循环过程中的buf参数和nbyte参数得由我们来更新。也就是说,网络写函数是不负责将全部数据写完之后在返回的。

2)返回的值小于0,此时出现了错误.我们要根据错误类型来处理,如果错误为EINTR表示在写的时候出现了中断错误,如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接),为了处理以上的情况,我们自己编写一个写函数来处理这几种情况.

int my_write(int fd,void *buffer,int length)
{
int bytes_left;
int written_bytes;
char *ptr;ptr=buffer;
bytes_left=length;
while(bytes_left>0)
{written_bytes=write(fd,ptr,bytes_left);if(written_bytes<=0){       if(errno==EINTR)written_bytes=0;else             return(-1);}bytes_left-=written_bytes;ptr+=written_bytes;
}
return(0);
}

读函数read

ssize_t read(int fd,void *buf,size_t nbyte)

read函数是负责从fd中读取内容.当读成功 时,read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误.如果错误为EINTR说明读是由中断引起 的, 如果是ECONNREST表示网络连接出了问题. 和上面一样,我们也写一个自己的读函数.

int my_read(int fd,void *buffer,int length)
{
int bytes_left;
int bytes_read;
char *ptr;
ptr = buffer;bytes_left=length;
while(bytes_left>0)
{bytes_read=read(fd,ptr,bytes_left);if(bytes_read<0){if(errno==EINTR)bytes_read=0;elsereturn(-1);}else if(bytes_read==0)break;bytes_left-=bytes_read;ptr+=bytes_read;
}
return(length-bytes_left);
}

(3)select、poll、epoll判断

这种方法用在服务端应该是比较合理的,因为服务器需要连接多个客户端的时候,使用多路复用就可以连接多个客户端,但是将这个判断方法用在客户端比较麻烦。
这里有关于select判断socket是否断开的博客,可参考:

https://blog.csdn.net/zzhongcy/article/details/21992123

三、总结

总的来说使用getsockopt()函数进行判断是最方便的,直接在write或read之前调用getsockopt()判断就可以了。

此博客为本人学习笔记,如侵,删。

参考链接:
http://c.biancheng.net/cpp/html/358.html
https://www.cnblogs.com/embedded-linux/p/7468442.html
https://blog.csdn.net/petershina/article/details/7946615

(read/write、select、getsockopt、signal)实时判断socket连接状态/是否断开相关推荐

  1. java socket 判断Socket连接失效

    要判断socket连接链路是否可用时,不能通过socket.isClosed() 和 socket.isConnected() 方法判断,要通过心跳包 socket.sendUrgentData(0x ...

  2. JAVA 判断Socket 远程端是否断开连接

    JAVA 判断Socket 远程端是否断开连接 最近在做项目的时候,遇到这样一个问题,如何判断 Socket 远程端连接是否关闭,如果关闭的话,就要重建连接Socket的类提供了一些已经封装好的方法, ...

  3. 客户端怎么判断Socket连接已与服务器断开

    原文地址:http://blog.csdn.net/god2469/article/details/8801356 下面来罗列一下判断远端已经断开的方法: 法一: 当recv()返回值小于等于0时,s ...

  4. 如何判断Socket连接失效

    在开发工程中对Socket进行管理时对于这个连接的超时和是否失效进行研究. 对于连接超时和失效肯定会想到设置超时时间和判断连接是否可用.但是设置超时时间后起作用是在调用read方法的时候,如果只是设置 ...

  5. python判断sqlite连接状态_python3 自动识别usb连接状态,即对usb重连的判断方法

    在做自动化测试时,遇到两种情况需要判断usb是否已连接上(注,本文仅针对用adb命令来control手机) 一种是在开测时(前提是同时要测试多台), 希望等待所有设备usb全部识别后同时进行测试.对于 ...

  6. Android中监听判断网络连接状态的方法

    这个python代码是用来从DHT网络(一种分布式的"磁力链接"的共享网络,这个叫法是我个人对这种分布式网络的称呼)中,检测收集"磁力链接".每一个磁力链接就对 ...

  7. Android Socket连接(模拟心跳包,断线重连,发送数据等)

    首页 博客 学院 下载 GitChat TinyMind 论坛 问答 商城 VIP 活动 招聘 ITeye CSTO 写博客 发Chat 喻志强的博客 耐心 细心 用心 传播正能量 RSS订阅 原 A ...

  8. wifi连接状态android,判断android设备wifi连接状态

    添加访问权限(AndroidManifest.xml文件里) Java代码(MainActivity.java文件) package com.example.androidtest; import a ...

  9. android进入wifi权限,判断android设备wifi连接状态,判断android设备wifi,添加访问权限(Andro...

    判断android设备wifi连接状态,判断android设备wifi,添加访问权限(Andro 添加访问权限(AndroidManifest.xml文件里) ```Java代码(MainActivi ...

最新文章

  1. 经典面试题:Redis 内存满了怎么办?
  2. LTP--linux稳定性测试 linux性能测试 ltp压力测试
  3. Django与CSRF 、AJAX
  4. html 中的frameset标签
  5. 绘制等压面图_等压面练习
  6. zillow房价预测比赛_Zillow Prize: 百万美刀奖金的房价预测比赛
  7. HTMl5 的新特性
  8. 韩顺平 java 坦克大战_韩顺平坦克大战源代码 韩顺平老师视频里的坦克大战 - 下载 - 搜珍网...
  9. Web容器、Servlet容器、Spring容器、SpringMVC容器之间的关系
  10. 中国机器人界“诺贝尔奖”!深眸科技实力斩获恰佩克年度创新品牌奖
  11. 一种快速求解最大团问题的算法
  12. [转帖]保罗·艾伦的故事
  13. 如何编写一个自己的网站
  14. 2022年版中国人工智能芯片行业专项调研及投资前景方向预测报告
  15. linux创建删除文件命令行,彻底消失,Linux下用命令行彻底删除文件
  16. 大学生就业管理系统源码
  17. 前500超级计算机,比传统超算前500名总和还快!百万人一起创造了这台“世界最快超级计算机”...
  18. linux软件安装和项目部署
  19. 台球和斯诺克设备行业调研报告 - 市场现状分析与发展前景预测
  20. 收集K8S容器的标准输出日志实践

热门文章

  1. 从零开始之驱动发开、linux驱动(六十六、内核调试篇--printk原理)
  2. 企业微信怎么扫描名片加好友?企业微信名片如何统计好友添加情况?
  3. ROS Kinetic Kame机器人操作系统学习笔记
  4. 基于 Milvus 的以图搜图系统 2.0
  5. iOS造轮子-数字金额每隔3位用逗号隔开(Swift实现)
  6. Mac查看端口占用情况
  7. 程序语言Python Tutorial(一):激发你的欲望 程序语言
  8. Linux下端口映射工具
  9. 岭回归原理简单分析与理解
  10. 漫长的岁月,沧海历经的人生