8. readline函数的实现

本文介绍了一个类似于fgets的readline函数的实现,该函数用于读取直到遇到' '为止的数据,解决了粘包问题。此外,还介绍了获取本机地址的常用函数,包括gethostname和gethostbyname等。

一. 实现readline函数   

              如果应用层协议的各字段长度固定,用readn来读是非常方便的。例如设计一种客户端上传文件的

协议,规定前12字节表示文件名,超过12字节的文件名截断,不足12字节的文件名用'\0'补齐,从第13字节开始是文件内容,上传完所有文件内容后关闭连接,服务器可以先调用readn读12个字节,根据文件名创建文件,然后在一个循环中调用read读文件内容并存盘,循环结束的条件是read返回0。


                  字段长度固定的协议往往不够灵活,难以适应新的变化。比如,以前DOS的文件名是8字节主文件名加“.”加3字节扩展名,不超过12字节,但是现代操作系统的文件名可以长得多,12字节就不够用了。那么制定一个新版本的协议规定文件名字段为256字节怎么样?这样又造成很大的浪费,因为大多数文件名都很短,需要用大量的'\0'补齐256字节,而且新版本的协议和老版本的程序无法兼容,如果已经有很多人在用老版本的程序了,会造成遵循新协议的程序与老版本程序的互操作性(Interoperability)问题。如果新版本的协议要添加新的字段,比如规定前12字节是文件名,从13到16字节是文件类型说明,从第17字节开始才是文件内容,同样会造成和老版本的程序无法兼容的问题。


            常见的应用层协议都是带有可变长字段的,字段之间的分隔符用换行的比用'\0'的更常见,例
如本节后面要介绍的HTTP协议。可变长字段的协议用readn来读就很不方便了,为此我们实现一个
类似于fgets的readline函数.


/// recv()只能读取套接字,而不能读取一般文件描述符
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
	while(1)
	{
		int ret = recv(sockfd,buf,len,MSG_PEEK);//MSG_PEEK接收缓冲区的数据,但是并没有清除
		if( ret == -1 && errno == EINTR)
			continue;
		return ret;
	}
}

// 读到'\n' 就返回,加上'\n'一行最多为maxline个字符
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
	int ret;
	int nread;
	char *bufp =(char *) buf;
	int nleft = maxline;
	int count=0;
	
	while(1)
	{	
		// recv_peek读取缓冲区的字符个数,并放入到bufp缓存里面
		ret = recv_peek(sockfd,bufp,nleft);
		if(ret < 0)
			return ret;// 表示失败
		else if(ret == 0)
			return ret; // 表示对方关闭连接了
			
		nread = ret;
		// 判断接收到字符是否有'\n'
		int i;
		for(i=0;i<nread;++i)
		{
			if(bufp[i] == '\n')
			{
			    // readn读取数据,这部分缓冲会被清空的
				ret = readn(sockfd,bufp,i+1);
				if(ret != (i+1))
					exit(EXIT_FAILURE);
				return ret + count;
			}
		}
		if( nread > nleft)
			exit(EXIT_FAILURE);
		nleft -= nread;
		ret = readn(sockfd,bufp,nread);
		if(ret != nread)
			exit(EXIT_FAILURE);
		bufp += nread;// 下一次指针偏移	
		count += nread;
	}
	return -1;
}



                   在readline函数中,我们先用recv_peek查看一下现在缓冲区有多少个字符并读取到bufp,然后查看是否存在换行符'\n'。如果存在,则使用readn连通换行符一起读取(清空缓冲区);如果不存在,也清空一下缓冲区, 且移动bufp的位置,回到while循环开头,再次窥看。注意,当我们调用readn读取数据时,那部分缓冲区是会被清空的,因为readn调用了read函数。还需注意一点是,如果第二次才读取到了'\n',则先用count保存了第一次读取的字符个数,然后返回的ret需加上原先的数据大小。

              使用 readline函数也可以认为是解决粘包问题的一个办法,即以'\n'为结尾当作一条消息。


完整的C/S程序如下:
显示服务器程序:


// echoser.c 

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>

#define ERR_EXIT(m) \
		do{ \
			perror(m); \
			exit(EXIT_FAILURE); \
			}while(0)

		

ssize_t readn(int fd,void *buf,size_t count)
{
	size_t nleft = count ; // 未读取的数据
	ssize_t nread;// 已读取的数据
	char *bufp= (char*)buf;
	while(nleft > 0)
	{
		if( (nread = read(fd,bufp,nleft)) < 0)
		{
			if( errno == EINTR)
				 nread = 0;//  继续读取数据
			else
				return -1;
		}
		else if( nread == 0) // 对方关闭或已经读到eof
			break;
		bufp +=nread;
		nleft -= nread;
	
	}
	return count-nleft;
}

ssize_t writen(int fd,const 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( errno == EINTR)
 				continue;
 			else
 				return -1;
 		}
 		else if( nwritten == 0)
 			continue;
 		bufp  += nwritten;
 		nleft -= nwritten;
 	}
 	return count;
}


/// recv()只能读取套接字,而不能读取一般文件描述符
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
	while(1)
	{
		int ret = recv(sockfd,buf,len,MSG_PEEK);//MSG_PEEK接收缓冲区的数据,但是并没有清除
		if( ret == -1 && errno == EINTR)
			continue;
		return ret;
	}
}

// 读到'\n' 就返回,加上'\n'一行最多为maxline个字符
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
	int ret;
	int nread;
	char *bufp =(char *) buf;
	int nleft = maxline;
	int count=0;
	
	while(1)
	{	
		// recv_peek读取缓冲区的字符个数,并放入到bufp缓存里面
		ret = recv_peek(sockfd,bufp,nleft);
		if(ret < 0)
			return ret;// 表示失败
		else if(ret == 0)
			return ret; // 表示对方关闭连接了
			
		nread = ret;
		// 判断接收到字符是否有'\n'
		int i;
		for(i=0;i<nread;++i)
		{
			if(bufp[i] == '\n')
			{
			    // readn读取数据,这部分缓冲会被清空的
				ret = readn(sockfd,bufp,i+1);
				if(ret != (i+1))
					exit(EXIT_FAILURE);
				return ret + count;
			}
		}
		if( nread > nleft)
			exit(EXIT_FAILURE);
		nleft -= nread;
		ret = readn(sockfd,bufp,nread);
		if(ret != nread)
			exit(EXIT_FAILURE);
		bufp += nread;// 下一次指针偏移	
		count += nread;
	}
	return -1;
}


void do_service(int conn)
{
	char recvbuf[1024];
	while(1)
	{
		memset(recvbuf,0,sizeof(recvbuf));
		int ret = readline(conn,recvbuf,1024);
		if( ret == -1)
			ERR_EXIT("readline err");
		else if(ret == 0)
		{
			printf("client close \n");
			break;
		}
		fputs(recvbuf,stdout);
		writen(conn,recvbuf,strlen(recvbuf));
	}
}

void handler(int sig)
{
	// wait(NULL); // 只能等待第一个退出的子进程
	while(waitpid(-1,NULL,WNOHANG) > 0)
		;
}


int main()
{
	//signal(SIGCHLD,SIG_IGN);  // 忽略SIGCHLD信号
	signal(SIGCHLD,handler);
	int listenfd; 
	if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP) ) < 0)
			// listenfd = socket(AF_INET,SOCK_STREAM,0)
			ERR_EXIT("socket error");
	
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);// INADDR_ANY,这个宏表示本地的任意IP地址
	//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	//inet_aton("127.0.0.1",&servaddr.sin_addr);
	

	int on = 1;
	if( setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
		ERR_EXIT("setsockopt err");

	if( bind( listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
		ERR_EXIT("bind err");
	if( listen(listenfd,SOMAXCONN) < 0)  //  INADDR_ANY,这个宏表示本地的任意IP地址
			ERR_EXIT("lesten err");

	struct sockaddr_in peeraddr; // 传出参数
	socklen_t peerlen = sizeof(peeraddr);// 传入传出参数,必须有初始值
	int conn; // 已经连接套接字(变为主动套接字,可以主动connect)
	
	pid_t pid;
	while(1)
	{
		
		if( (conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)// 3次握手完成
			ERR_EXIT("accept err");
		//通过peeraddr打印连接上来的客户端ip和端口号
			printf("recv connect ip=%s ,port = %d \n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
		
		pid = fork();
		if( pid == -1)
			ERR_EXIT("fork err");
		if(pid == 0) /// 子进程
		{
			close(listenfd);
			do_service(conn);
			exit(EXIT_SUCCESS);
		}
		
		else /// 父进程
			close(conn);
	}
	
}

客户端程序:

/// echolic.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>

#define ERR_EXIT(m) \
		do{ \
			perror(m); \
			exit(EXIT_FAILURE); \
		}while(0)


ssize_t readn(int fd,void *buf,size_t count)
{
	size_t nleft = count ; // 未读取的数据
	ssize_t nread;// 已读取的数据
	char *bufp= (char*)buf;
	while(nleft > 0)
	{
		if( (nread = read(fd,bufp,nleft)) < 0)
		{
			if( errno == EINTR)
				 nread = 0;//  继续读取数据
			else
				return -1;
		}
		else if( nread == 0) // 对方关闭或已经读到eof
			break;
		bufp +=nread;
		nleft -= nread;
	
	}
	return count-nleft;
}

ssize_t writen(int fd,const 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( errno == EINTR)
 				continue;
 			else
 				return -1;
 		}
 		else if( nwritten == 0)
 			continue;
 		bufp  += nwritten;
 		nleft -= nwritten;
 	}
 	return count;
}


/// recv()只能读取套接字,而不能读取一般文件描述符
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
	while(1)
	{
		int ret = recv(sockfd,buf,len,MSG_PEEK);//MSG_PEEK接收缓冲区的数据,但是并没有清除
		if( ret == -1 && errno == EINTR)
			continue;
		return ret;
	}
}

// 读到'\n' 就返回,加上'\n'一行最多为maxline个字符
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
	int ret;
	int nread;
	char *bufp =(char *) buf;
	int nleft = maxline;
	//int count=0;
	
	while(1)
	{	
		// recv_peek读取缓冲区的字符个数,并放入到bufp缓存里面
		ret = recv_peek(sockfd,bufp,nleft);
		if(ret < 0)
			return ret;// 表示失败
		else if(ret == 0)
			return ret; // 表示对方关闭连接了
			
		nread = ret;
		// 判断接收到字符是否有'\n'
		int i;
		for(i=0; i<nread; ++i)
		{
			if(bufp[i] == '\n')
			{
			    // readn读取数据,这部分缓冲会被清空的
				ret = readn(sockfd,bufp,i+1);
				if(ret != (i+1))
					exit(EXIT_FAILURE);
				return ret; //+ count;
			}
		}
		if( nread > nleft)
			exit(EXIT_FAILURE);
		nleft -= nread;
		ret = readn(sockfd,bufp,nread);
		if(ret != nread)
			exit(EXIT_FAILURE);
		bufp += nread;// 下一次指针偏移	
		//count += nread;
	}
	return -1;
}


int main()
{
	int sock;
	if( (sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
			ERR_EXIT("sock err");
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));

	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); 
	
	if( connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
			ERR_EXIT("connect error");
			
	
	char sendbuf[1024] = {0};
	char recvbuf[1024] = {0};
	while( fgets(sendbuf,sizeof(sendbuf),stdin) != NULL)  
	{
		writen(sock,sendbuf,strlen(sendbuf));// 需要的注意的的时sizeof(sendbuf)和strlen(recvbuf)不一样的,容易出现混淆
		int ret = readline(sock,recvbuf,1024);///最后一个参数为缓冲区的最大值
		if( ret == -1 )
			ERR_EXIT("readline");
		else if(ret == 0)
		{
			printf("client close \n");
			break;
		}
		fputs(recvbuf,stdout);
		memset(recvbuf,0,sizeof(recvbuf));
		memset(sendbuf,0,sizeof(sendbuf));
	}
    
    close(sock);   
	return 0;
}

二. 获取本机地址的一些常用函数

         getsockname , getpeername,  gethostname, gethostbyname, gethostbyaddr

      #include <unistd.h>

       int gethostname(char *name, size_t len);
       int sethostname(const char *name, size_t len);

--------------------------------------------------------------------------------------

       #include <netdb.h>
       extern int h_errno;

       struct hostent *gethostbyname(const char *name);

   struct hostent {
               char  *h_name;            /* official name of host */
               char **h_aliases;         /* alias list */
               int    h_addrtype;        /* host address type */
               int    h_length;          /* length of address */
               char **h_addr_list;       /* list of addresses */
           }
           #define h_addr h_addr_list[0] /* for backward compatibility */


                                gethostname 可以得到主机名,而gethostbyname 可以通过主机名得到一个结构体指针,可以通过此结构体得到与主机相关的ip地址信息等。

测试代码:

#include <stdio.h>
#include <unistd.h>
#include <netdb.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>


#define ERR_EXIT(m) \
	do \
	{ \
		perror(m); \
		exit(EXIT_FAILURE); \
	}while(0) 
	
int getlocalip(char *ip)
{
	char host[100] = {0};
	if( gethostname(host,sizeof(host)) < 0) 
		return -1;
		
	struct hostent* hp;
	 if( (hp = gethostbyname(host)) < 0)
	 	return -1;
	 strcpy(ip,inet_ntoa(*(struct in_addr*)hp->h_addr_list[0]));
	 return 0;
}	

int main()
{
	char host[100] = {0};
	if( gethostname(host,sizeof(host)) < 0) 
		ERR_EXIT("gethostname err");
	
	 struct hostent *hp;
	 if( (hp = gethostbyname(host)) ==NULL)
	 	ERR_EXIT("gethostbyname err");
	 int i=0;
	 while(hp->h_addr_list[i] != NULL)
	 {
	 	printf("%s\n",inet_ntoa(*(struct in_addr*)hp->h_addr_list[i]));
	 	++i;
	 }
	 char ip[16] = {0};
	 getlocalip(ip);
	 printf("localip = %s\n",ip);
	 
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值