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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值