当前位置:   article > 正文

(read/write、select、getsockopt、signal)实时判断socket连接状态/是否断开_getsockopt函数判断sock链接状态

getsockopt函数判断sock链接状态

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

最近遇到一个大坑:发现服务器端断开连接时,客户端还能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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

一、了解背后的原因

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;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
//服务器端
#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

运行服务器端和客户端程序:
在这里插入图片描述
在这里插入图片描述
如果没有在命令行上写入任何字符,就回车发送,客户端会将“\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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
//头文件
#include <signal.h>
signal(SIGPIPE, handle_pipe);

void handle_pipe(int sig)
{
	if(sig == SIGPIPE)
	{

	}
		//不做任何处理即可
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

很奇怪的就是,我在使用这个方法的时候,我给客户端安装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);
  • 1
  • 2
  • 3
  • 4
  • 5

函数说明: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;
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

(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)
  • 1

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

读函数read

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

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;
      else
         return(-1);
    }
    else if(bytes_read==0)
        break;
     bytes_left-=bytes_read;
     ptr+=bytes_read;
}
return(length-bytes_left);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

(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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/158615
推荐阅读
相关标签
  

闽ICP备14008679号