赞
踩
最近遇到一个大坑:发现服务器端断开连接时,客户端还能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; } }
当调用send()或者write()发送数据时,并不是直接将数据发送到网络中,而是先将待发送的数据放到socket发送缓冲区中,然后由TCP协议执行数据的网络发送。send()/write()函数不保证数据能够通过网络成功发送出去,只要能够将数据写入到socket发送缓冲区,send()/write()就可以成功返回。
注意:1.一般来说,读字符终端、网络的socket描述字,管道文件等,这些文件的缺省read都是阻塞的方式。 2.如果是读磁盘上的文件,一般不会是阻塞方式的。如果客户端发送一个空的数据给服务器,服务器的read()将一直阻塞到客户端发的数据不为空为止。同理如果服务器端发送一个空的数据给客户端,客户端的read()也将阻塞到服务器端发送的数据不为空为止。
某个客户端和服务器端的正常连接状态如下:
当服务器socket突然关闭时,客户端并不知情,此时客户端调用send()/write()依然可以将数据放置到socket发送缓冲区中,所以send()/write()成功返回。
直到TCP协议执行发送动作的时候才发现不对劲,这时客户端才拿到服务器socket已关闭的信息,由于TCP协议无法继续网络数据的传输,所以系统自动关闭客户端socket发送缓冲区的读端。这时,再次调动send()/write()函数,就会导致程序终止,原因是收到了SIGPIPE信号。
SIGPIPE信号产生的规则:当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送SIGPIPE信号。
了解一下除了SIGPIPE信号之外还有哪些信号:
当服务器close一个连接时,若client端进程接着发数据,根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,数据还是能存到缓冲区,之后系统会发出一个SIGPIPE信号给该进程,告诉进程这个连接已经断开了,不要再写了,如果client再发送数据到缓冲区,是不能成功的。又或者当一个进程向某个已经收到RST的socket执行写操作时,内核向该进程发送一个SIGPIPE信号。该信号的缺省处理是终止进程,因此进程必须捕获它以免不情愿的被终止。
我遇到的情况是服务器socket句柄已关闭,然后客户端向一个已关闭的服务端连接句柄中执行写操作,从而产生了SIGPIPE信号。
根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出),所以进程会退出。
系统里边定义了三种处理方法:
1)SIG_DFL //默认处理信号
2)SIG_IGN //忽略信号
3)SIG_ERR //出错
根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出),所以进程会退出。若不想客户端退出,需要把 SIGPIPE默认执行动作屏蔽。
到了这里,大概可以了解这其中的原因了,下面用一个客户端和服务器端的代码进行测试,复现我遇到的问题,并进行分析。
//客户端程序: #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”发送给服务器端。
可用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,但是服务器端显示是正常发送的。
头文件:
#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; } }
用 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; else return(-1); } else if(bytes_read==0) break; bytes_left-=bytes_read; ptr+=bytes_read; } return(length-bytes_left); }
这种方法用在服务端应该是比较合理的,因为服务器需要连接多个客户端的时候,使用多路复用就可以连接多个客户端,但是将这个判断方法用在客户端比较麻烦。
这里有关于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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。