11SIGPIPE信号
如果对方socket已关闭,对等方再发写数据,则会产生SIGPIPE信号
SIGPIPE信号会让进程终止(man 7 signal,阅读SIGPIPE默认ACT)
往一个已经接收FIN的套接中写是允许的,接收到FIN仅仅代表对方不再发送数据。
在收到RST段之后,如果再调用write就会产生SIGPIPE信号,对于这个信号的处理我们通常忽略即可。
signal(SIGPIPE, SIG_IGN);
Standard signals
Linux supports the standard signals listed below. Several
signal numbers are architecture-dependent, as indicated in
the "Value" column. (Where three values are given, the
first one is usually valid for alpha and sparc, the middle
one for x86, arm, and most other architectures, and the
last one for mips. (Values for parisc are not shown; see
the Linux kernel source for signal numbering on that archi‐
tecture.) A - denotes that a signal is absent on the cor‐
responding architecture.)
First the signals described in the original POSIX.1-1990
standard.
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no
readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background process
结论:
对SIGPIPE处理方法:
1)忽略该信号即可signal(SIGPIPE, SIG_IGN);
2)捕捉。改变默认行为。
TCP/IP 的RST段重置
1)服务器端启动、客户端启动
2)服务器端先kill与客户端通讯的子进程,服务器端会给客户端发送FIN分节
此时:只代表服务器端不发送数据了,不能代表客户端不能往套接字中写数据。
3)如果子进程此时写数据给服务器端(解除屏幕阻塞,输入字符aaaa),
将要导致TCP/IP协议重置,产生RST段;产生SIGPIPE信号。。
4)所以,一般情况下,需要我们处理SIGPIPE信号,忽略即可。
演示:
其他代码请看(https://blog.csdn.net/WUZHU2017/article/details/82786507)
//修改这个函数
void echo_cli(int sockfd)
{
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//写数据(本身带有\n), 所以不需要再单独加\n,因为从stdin输入完需要按下enter键
writen(sockfd, sendbuf, strlen(sendbuf));
/////////////////////////////////////////////////////////////////////////////
#if 1
sleep(3);
//测试对端关闭的情况下再次写数据,造成客户端产生SIGPIPE信号,该信号的默认动作是终止
writen(sockfd, "再次写数据aaa....\n", 100);
#endif
///////////////////////////////////////////////////////////////////////////////
//按照行读数据
int ret = readline(sockfd, recvbuf, sizeof(recvbuf));
if (ret == -1)
ERR_EXIT("readline()");
else if (ret == 0)
{
printf("server close\n");
break;
}
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sockfd);
}
首先启动客户端,在启动服务端,然后杀死服务端的子进程,即模拟服务端close(),此时客户端向服务端写数据,客户端会异常退出
修改代码
再次观察现象
服务器加上对僵尸进程的处理
完成的代码如下
9client_readline.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
/*
包尾加上\n编程实践
*/
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
/*
使用说明:
//1一次全部读走 //2次读完数据 //出错分析 //对方已关闭
思想:
tcpip是流协议,不能保证1次读操作,能全部把报文读走,所以要循环
读指定长度的数据。
按照count大小读数据,若读取的长度ssize_t<count 说明读到了一个结束符,
对方已关闭
函数功能:
从一个文件描述符中读取count个字符到buf中
参数:
@buf:接受数据内存首地址
@count:接受数据长度
返回值:
@ssize_t:返回读的长度 若ssize_t<count 读失败失败
*/
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; //剩下需要读取的数据个数
ssize_t nread; //成功读取的字节数
char * bufp = (char*)buf;//将参数接过来
while (nleft > 0)
{
//如果errno被设置为EINTR为被信号中断,如果是被信号中断继续,
//不是信号中断则退出。
if ((nread = read(fd, bufp, nleft)) < 0)
{
//异常情况处理
if (errno == EINTR) //读数据过程中被信号中断了
continue; //再次启动read
//nread = 0; //等价于continue
return -1;
}else if (nread == 0) //到达文件末尾EOF,数据读完(读文件、读管道、socket末尾、对端关闭)
break;
bufp += nread; //将字符串指针向后移动已经成功读取个数的大小。
nleft -=nread; //需要读取的个数=需要读取的个数-已经成功读取的个数
}
return (count - nleft);//返回已经读取的数据个数
}
/*
思想:tcpip是流协议,不能1次把指定长度数据,全部写完
按照count大小写数据
若读取的长度ssize_t<count 说明读到了一个结束符,对方已关闭。
函数功能:
向文件描述符中写入count个字符
函数参数:
@buf:待写数据首地址
@count:待写长度
返回值:
@ssize_t:返回写的长度 -1失败
*/
ssize_t writen(int fd, void *buf, size_t count)
{
size_t nleft = count; //需要写入的个数
ssize_t nwritten; //成功写入的字节数
char * bufp = (char*)buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) <= 0)
{
//异常情况处理 信号打断,则继续
if ((nwritten < 0) && (errno == EINTR)) //读数据过程中被信号中断了
continue; //再次启动write
//nwritten = 0; //等价continue
else
return -1;
}
bufp += nwritten; //移动缓冲区指针
nleft -=nwritten; //记录剩下未读取的数据
}
return count;//返回已经读取的数据个数
}
//读数据,但不把数据缓冲区清空
//@ssize_t返回值:返回缓冲区数据的长度 -1失败
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
//MSG_PEEK 读取队列中指定大小的数据,但不取出
int ret = recv(sockfd, buf, len, MSG_PEEK);
//如果被信号中断,则继续
if (ret == -1 && errno == EINTR)
continue;
return ret;
}
}
/*
maxline 一行最大数
先提前peek一下缓冲区,如果有数据从缓冲区读数据,
1、缓冲区数据中带\n
2 、缓存区中不带\n
读取数据包直到\n
功能:按行读取文件,只要遇到\n就,读走数据,返回,
@buf 接收数据内存首地址
@maxline 接收数据内存最大值
*/
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread; //成功预读取的数据个数
char *bufp = buf; //读取数据存放的数组,在外分配内存
int nleft = maxline;//封包最大值
while (1)
{
//看一看缓冲区有没有数据,并不移除内核缓冲区数据
//读数据,但不把数据缓冲区清空,成功:ret是报文的长度
ret = recv_peek(sockfd, bufp, nleft);
if (ret < 0) //失败
return ret;
else if (ret == 0)//对方已关闭
return ret;
nread = ret;
int i;
//读数据,但不把数据缓冲区清空,避免了一个字节一个字节的读数据
//先利用recv的MSG_PEEK功能,预读数据,然后查找\n
//根据\n的位置,根据指定长度,再真正的读数据
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n') //若缓冲区有\n
{
ret = readn(sockfd, bufp, i+1);//将数据从缓存区读走
if (ret != i + 1)
exit(EXIT_FAILURE);
return ret;//有\n就返回,并返回读走的数据
}
}
//若数据长度 nread > 缓冲区最大长度maxline 退出
if (nread > nleft)
exit(EXIT_FAILURE);
//若没有\n,说明消息还没有结束,不是完整的一条消息,就把这些数据也读到buf缓冲区中。
//依此循环,直到遇到\n,把整个一行数据,全部读完,放入buf中
//bufp记录了每次需追加的位置
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
exit(EXIT_FAILURE);
bufp += nread; //bufp每次跳到追加的末尾
}
return -1;
}
void echo_cli(int sockfd)
{
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//写数据(本身带有\n), 所以不需要再单独加\n,因为从stdin输入完需要按下enter键
writen(sockfd, sendbuf, strlen(sendbuf));
#if 1
sleep(3);
//测试对端关闭的情况下再次写数据,造成客户端产生SIGPIPE信号,该信号的默认动作是终止
writen(sockfd, "再次写数据aaa....\n", 100);
#endif
//按照行读数据
int ret = readline(sockfd, recvbuf, sizeof(recvbuf));
if (ret == -1)
ERR_EXIT("readline()");
else if (ret == 0)
{
printf("server close\n");
break;
}
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sockfd);
}
void test()
{
int sockfd = 0;
const char *serverip = "192.168.66.128";
//若程序收到SIGPIPE,则忽略
signal(SIGPIPE, SIG_IGN);
//创建socket
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket()");
//定义socket结构体 man 7 ip
struct sockaddr_in srvsddr;
srvsddr.sin_family = AF_INET;
srvsddr.sin_port = htons(8001);//转化为网络字节序
//第一种
#if 0
srvsddr.sin_addr.s_addr = inet_addr(serverip);
#endif
//第二种
#if 0
//srvsddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 就是0.0.0.0 不存在网络字节序
//srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
#endif
//第三种
//建议使用这种
#if 1
int ret;
ret = inet_pton(AF_INET, serverip, &srvsddr.sin_addr);
if (ret == 0)
{
ERR_EXIT("inet_pton()");
}
#endif
//进程-》内核
if (connect(sockfd, (struct sockaddr*)&srvsddr, sizeof(srvsddr)) < 0)
ERR_EXIT("connect");
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
//内核-》进程
//获取本地的地址 注意是已连接以后的套接字
if ((getsockname(sockfd, (struct sockaddr *)&localaddr, &addrlen)) < 0)
ERR_EXIT("getsockname()");
printf("本机的ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
echo_cli(sockfd);
return ;
}
int main()
{
test();
return 0;
}
10server_readline.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
/*
包尾加上\n编程实践
*/
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
/*
使用说明:
//1一次全部读走 //2次读完数据 //出错分析 //对方已关闭
思想:
tcpip是流协议,不能保证1次读操作,能全部把报文读走,所以要循环
读指定长度的数据。
按照count大小读数据,若读取的长度ssize_t<count 说明读到了一个结束符,
对方已关闭
函数功能:
从一个文件描述符中读取count个字符到buf中
参数:
@buf:接受数据内存首地址
@count:接受数据长度
返回值:
@ssize_t:返回读的长度 若ssize_t<count 读失败失败
*/
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; //剩下需要读取的数据个数
ssize_t nread; //成功读取的字节数
char * bufp = (char*)buf;//将参数接过来
while (nleft > 0)
{
//如果errno被设置为EINTR为被信号中断,如果是被信号中断继续,
//不是信号中断则退出。
if ((nread = read(fd, bufp, nleft)) < 0)
{
//异常情况处理
if (errno == EINTR) //读数据过程中被信号中断了
continue; //再次启动read
//nread = 0;//等价于continue
return -1;
}else if (nread == 0) //到达文件末尾EOF,数据读完(读文件、读管道、socket末尾、对端关闭)
break;
bufp += nread; //将字符串指针向后移动已经成功读取个数的大小。
nleft -=nread; //需要读取的个数=需要读取的个数-已经成功读取的个数
}
return (count - nleft);//返回已经读取的数据个数
}
/*
思想:tcpip是流协议,不能1次把指定长度数据,全部写完
按照count大小写数据
若读取的长度ssize_t<count 说明读到了一个结束符,对方已关闭。
函数功能:
向文件描述符中写入count个字符
函数参数:
@buf:待写数据首地址
@count:待写长度
返回值:
@ssize_t:返回写的长度 -1失败
*/
ssize_t writen(int fd, void *buf, size_t count)
{
size_t nleft = count; //需要写入的个数
ssize_t nwritten; //成功写入的字节数
char * bufp = (char*)buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) <= 0)
{
//异常情况处理 信号打断,则继续
if ((nwritten < 0) && (errno == EINTR)) //读数据过程中被信号中断了
continue; //再次启动write
//nwritten = 0; //等价continue
else
return -1;
}
bufp += nwritten; //移动缓冲区指针
nleft -=nwritten; //记录剩下未读取的数据
}
return count;//返回已经读取的数据个数
}
//从指定的socket中读取指定大小的数据但不取出,封装后不被信号中断
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
//MSG_PEEK 读取队列中指定大小的数据,但不取出
int ret = recv(sockfd, buf, len, MSG_PEEK);
//如果被信号中断,则继续
if (ret == -1 && errno == EINTR)
continue;
return ret;
}
}
/*
maxline 一行最大数
先提前peek一下缓冲区,如果有数据从缓冲区读数据,
1、缓冲区数据中带\n
2 、缓存区中不带\n
读取数据包直到\n
*/
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread; //成功预读取的数据个数
char *bufp = buf; //读取数据存放的数组,在外分配内存
int nleft = maxline;//封包最大值
while (1)
{
//看一看缓冲区有没有数据,并不移除内核缓冲区数据
ret = recv_peek(sockfd, bufp, nleft);
if (ret < 0) //失败
return ret;
else if (ret == 0)//对方已关闭
return ret;
nread = ret;
int i;
//逐字符读取
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n') //若缓冲区有\n
{
ret = readn(sockfd, bufp, i+1);//读走数据
if (ret != i + 1)
exit(EXIT_FAILURE);
return ret;//有\n就返回,并返回读走的数据
}
}
//如果读到的数大于 一行最大数 异常处理
if (nread > nleft)
exit(EXIT_FAILURE);
nleft -= nread;//若缓冲区没有\n, 把剩余的数据读走
ret = readn(sockfd, bufp, nread);
if (ret != nread)
exit(EXIT_FAILURE);
bufp += nread;//bufp指针后移后,再接着偷看缓冲区数据recv_peek,直到遇到\n
}
return -1;
}
void do_service(int conn)
{
char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = readline(conn, recvbuf, sizeof(recvbuf));
if (ret == -1)
ERR_EXIT("readline()");
if (ret == 0)
{
printf("client close\n");
break;
}
//将数据打印输出
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
}
}
//正确的使用方法
void handle_sigchld2(int signo)
{
int mypid;
while (( mypid = waitpid(-1, NULL, WNOHANG)) > 0)
{
printf("孩子退出,父进程要收尸:%d\n", mypid);
}
}
void test()
{
int sockfd = 0;
int conn = 0;
const char *serverip = "192.168.66.128";
//安装信号处理函数 使用waitpid
#if 1
signal(SIGCHLD, handle_sigchld2);
#endif
//创建socket
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket()");
//定义socket结构体 man 7 ip
struct sockaddr_in srvsddr;
srvsddr.sin_family = AF_INET;
srvsddr.sin_port = htons(8001);//转化为网络字节序
//第一种
#if 0
srvsddr.sin_addr.s_addr = inet_addr(serverip);
#endif
//第二种
#if 0
//srvsddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 就是0.0.0.0 不存在网络字节序
//srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
#endif
//第三种
//建议使用这种
#if 1
int ret;
ret = inet_pton(AF_INET, serverip, &srvsddr.sin_addr);
if (ret == 0)
{
ERR_EXIT("inet_pton()");
}
#endif
//设置端口复用
//使用SO_REUSEADDR选项可以使得不必等待TIME_WAIT状态消失就可以重启服务器
int optval = 1;
if( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
ERR_EXIT("setsockopt()");
if(bind(sockfd, (struct sockaddr *)&srvsddr,sizeof(srvsddr)) <0 )
ERR_EXIT("bind()");
if(listen(sockfd, SOMAXCONN) < 0)
ERR_EXIT("listen()");
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);//值-结果参数
pid_t pid;
while (1)
{
if ((conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
ERR_EXIT("accept()");
printf("客户端1 ip:%s port:%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
#if 0
//方法2 只要能拿到连接socket就行
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
//注意是已连接以后的套接字
if (getpeername(conn, (struct sockaddr*)&clientaddr, &clientlen) < 0)
ERR_EXIT("getpeername");
printf("客户端2 ip=%s port=%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
#endif
pid = fork();
if (pid == -1)
{
ERR_EXIT("fork()");
}
if (pid == 0)
{
//子进程不需要监听socket
close(sockfd);
do_service(conn);
exit(EXIT_SUCCESS);
}else
{
close(conn);//父进程不需要连接socket
}
}
return ;
}
int main()
{
test();
return 0;
}