一. 实现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;
}