12、close与shutdown区别

 close终止了数据传送的两个方向。
 shutdown可以有选择的终止某个方向的数据传送或者终止数据传送的两个方向。
 shutdown how=1就可以保证对等方接收到一个EOF字符,而不管其他进程是否已经打开了套接字。而close不能保证,直到套接字引用计数减为0时才发送。也就是说直到所有的进程都关闭了套接字。

思考1
客户端向服务器发送:FIN(close) E D C B A,
问:服务器还能收到数据吗?服务器还可以向客户端回报文吗?
客户端想在关闭之后,仍然能接收到回射服务器应答(shutdown)。
思考2
父进程中close(conn);会不会向客户端发送FIN报文段那?
文件的引用计数-1,当减少为0,才会发送引用计数。
思考3:
客户端//shutdown(sock, SHUT_WR);只关闭了写;
言外之意我可以接收数据

测试close与shutdown的区别

第一种测试场景
在这里插入图片描述

运行结果如下

在这里插入图片描述

在这里插入图片描述

现在调用shutdown
在这里插入图片描述
实验结果
在这里插入图片描述

源代码:

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 0
		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));
		
		if (recvbuf[0] == '2')  //注意2 一共2处。。。。
		{
			//如果父进程没有关闭,则客户端应该关闭2次,才能往对端发送FIN分节
			//close(conn); //11111111111
			//close(conn); //11111111111
			shutdown(conn, SHUT_WR);
		}
				
	}	
}

//正确的使用方法
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()函数使用了引用计数技术
			//close(conn);//父进程不需要连接socket 注意1.。。
			
		} 
	}
	return ;
}

int main()
{
	test();
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章