经过一个国庆长假,又有一段时间没有写博文了,今天继续对linux网络编程进行学习,如今的北京又全面进入雾霾天气了,让我突然想到了一句名句:“真爱生活,珍惜生命”,好了,言归正传。
回顾一下我们之间实现在TCP回射客户/服务器程序,首先回顾一下第一个版本:
TCP客户端从stdin获取(fgets)一行数据,然后将这行数据发送(write)到TCP服务器端,这时TCP服务器调用read方法来接收然后再将数据回射(write)回来,客户端收到(read)这一行,然后再将其输出fputs标准输出stdout,但是这个程序并没有处理粘包问题,因为TCP是流协议,消息与消息之间是没有边界的,关于粘包问题,可以参考博文:http://www.cnblogs.com/webor2006/p/3946541.html,为了解决这个问题,于是第二个改进版程序诞生了:
一行一行的发送数据,每一行都有一个\n字符,所以我们在服务器端实现了按行读取的readline方法,另外发送也并不能保证一次调用write方法就将tcp应用层的所有缓冲区拷贝到了套接口缓冲区,所以我们封装了一个writen方法进行一个更可靠消息的发送,所以就很好的解决了粘包问题。
回顾一下程序:
echosrv.c:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> #include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h> #define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while() ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char*)buf; while (nleft > )
{
if ((nread = read(fd, bufp, nleft)) < )
{
if (errno == EINTR)
continue;
return -;
}
else if (nread == )
return count - nleft; bufp += nread;
nleft -= nread;
} return count;
} ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char*)buf; while (nleft > )
{
if ((nwritten = write(fd, bufp, nleft)) < )
{
if (errno == EINTR)
continue;
return -;
}
else if (nwritten == )
continue; bufp += nwritten;
nleft -= nwritten;
} return count;
} ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while ()
{
int ret = recv(sockfd, buf, len, MSG_PEEK);
if (ret == - && errno == EINTR)
continue;
return ret;
}
} ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = buf;
int nleft = maxline;
while ()
{
ret = recv_peek(sockfd, bufp, nleft);
if (ret < )
return ret;
else if (ret == )
return ret; nread = ret;
int i;
for (i=; i<nread; i++)
{
if (bufp[i] == '\n')
{
ret = readn(sockfd, bufp, i+);
if (ret != i+)
exit(EXIT_FAILURE); return ret;
}
} if (nread > nleft)
exit(EXIT_FAILURE); nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
exit(EXIT_FAILURE); bufp += nread;
} return -;
} void echo_srv(int conn)//由于这个函数的意义就是回显消息给客户端,所以改一个函数名
{
char recvbuf[];
while ()
{
memset(recvbuf, , sizeof(recvbuf));
int ret = readline(conn, recvbuf, );
if (ret == -)
ERR_EXIT("readline");
if (ret == )
{
printf("client close\n");
break;
} fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
}
} int main(void)
{
int listenfd;
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < )
/* if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
ERR_EXIT("socket"); struct sockaddr_in servaddr;
memset(&servaddr, , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons();
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
/*inet_aton("127.0.0.1", &servaddr.sin_addr);*/ int on = ;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < )
ERR_EXIT("setsockopt"); if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < )
ERR_EXIT("bind");
if (listen(listenfd, SOMAXCONN) < )
ERR_EXIT("listen"); struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn; pid_t pid;
while ()
{
if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < )
ERR_EXIT("accept"); printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); pid = fork();
if (pid == -)
ERR_EXIT("fork");
if (pid == )
{
close(listenfd);
echo_srv(conn);
exit(EXIT_SUCCESS);
}
else
close(conn);
} return ;
}
echocli.c:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> #include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h> #define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while() ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char*)buf; while (nleft > )
{
if ((nread = read(fd, bufp, nleft)) < )
{
if (errno == EINTR)
continue;
return -;
}
else if (nread == )
return count - nleft; bufp += nread;
nleft -= nread;
} return count;
} ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char*)buf; while (nleft > )
{
if ((nwritten = write(fd, bufp, nleft)) < )
{
if (errno == EINTR)
continue;
return -;
}
else if (nwritten == )
continue; bufp += nwritten;
nleft -= nwritten;
} return count;
} ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while ()
{
int ret = recv(sockfd, buf, len, MSG_PEEK);
if (ret == - && errno == EINTR)
continue;
return ret;
}
} ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = buf;
int nleft = maxline;
while ()
{
ret = recv_peek(sockfd, bufp, nleft);
if (ret < )
return ret;
else if (ret == )
return ret; nread = ret;
int i;
for (i=; i<nread; i++)
{
if (bufp[i] == '\n')
{
ret = readn(sockfd, bufp, i+);
if (ret != i+)
exit(EXIT_FAILURE); return ret;
}
} if (nread > nleft)
exit(EXIT_FAILURE); nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
exit(EXIT_FAILURE); bufp += nread;
} return -;
} void echo_cli(int sock)//将之前这一段代码封装成一个函数,回显客户端,让其代码更加整洁。
{
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
writen(sock, sendbuf, strlen(sendbuf)); int ret = readline(sock, recvbuf, sizeof(recvbuf));
if (ret == -1)
ERR_EXIT("readline");
else if (ret == 0)
{
printf("client close\n");
break;
} fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
} close(sock);
} int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < )
ERR_EXIT("socket"); struct sockaddr_in servaddr;
memset(&servaddr, , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons();
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < )
ERR_EXIT("connect"); struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if (getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < )
ERR_EXIT("getsockname"); printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); echo_cli(sock); return ;
}
运行效果如下:
对于上面运行程序,当客户端退出之后,服务端会产生僵进程:
【说明】:关于什么是僵尸进程,可以参考博文:http://www.cnblogs.com/webor2006/p/3512781.html
对于僵进程的避勉,有如下方法,之前都有介绍过:
忽略SIGCHLD信号,所以,可以在服务端加入:
编译运行,看是否解决了僵进程的问题:
第二种方式,则可以捕捉SIGCHLD信号来进行忽略,具体代码如下:
此时再编译运行:
但是,对于上面两种解决方式还是会存在一些问题,如果有很多个子进程同时退出,wait函数并不能等待所有子进程的退出,因为wait仅仅只等待第一个子进程退出就返回了,这时就需要用到waitpid了,在这之前,需要模拟一下由五个客户端并发连接至服务器,并同时退出的情况,用简单示例图来描述如下:
客户端创建五个套接字来连接服务器,一旦连接了服务器就会创建一个子进程出来为客户端进行处理,从图中可以看,服务端创建了5个子进程出来。
对于服务端程序是不需要进行修改的,只需修改客户端创建五个套接字既可,修改客户端程序如下:
这时,编译运行:
关于这个比较容易理解,是由于wait函数只等待一个子进程退出就返回了,所以有四个进程处于僵尸的状态。
再运行一次:
那如果再运行一次呢?
从以上结果来看,僵尸进程的数目不一定。
由于wait函数只能返回一个子进程,这时候我们应该用什么方法解决呢?可以用waitpid函数来解决,具体程序修改如下:
再来运行看是否还存在僵尸进程呢?
这种情况有一点不太好解释,但是还会遇到这种情况:
对于这种情况,就可以用理论来解释了,原因是由于有五个子进程都要退出,这就意味着有五个信号要发送给父进程,
在关闭连接时,会向服务器父进程发送SIGCHLD信号,具体如下图:
此时父进程处于一个handle_sigchld()的过程,而在这个处理过程中,如果其它信号到了,其它信号会丢失,之前也提到过,这些信号是不可靠信号,不可靠信号只会排队一个,如果只排队一个的话,那么最终能够处理两个子进程,所以,最后存在三个僵尸进程。
那为什么有时会有2个僵进程,有时又会有四个呢,这里来解释一下:
原因可能跟FIN(客户端在终止时,会向服务器发送FIN)到达的时机有关,服务器收到FIN的时候,返回等于0就退出了子进程,这时就会向服务器发送SIGCHLD信号,如果这些信号都是同时到达的话,那么就有可能只处理一个,这时就会出现了4个僵尸进程;如果不是同时到达,handle_sigchld()函数就会执行多次,如果被执行了两次,捕捉到了两个信号的话,那就此时就会出现3个僵进程,以此类推,但不管结果怎样,都是属于需要解决的情况。
那怎么解决此问题呢,可以用一个循环来做:
这时再编译运行:
好了,今天就学到这,下节见~~