本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie
这是一个简单的回射服务器程序。它将客户发送的数据读入缓冲区并回射其中内容
下面我会介绍同一个使用 TCP 协议的回射服务器程序的几个不同版本,分别是 fork 版本、select 版本、poll 版本、多线程版本
fork 版本:为每一个客户连接派生(fork) 一个子进程用来处理客户请求
/**
* TCP/IPv4 协议相关
* **/
#include "unp.h"
int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
//1.创建套接字,捆绑服务器的众所周知端口
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
//2.等待完成客户连接
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
//3.并发服务器
if ( (childpid = Fork()) == 0) { /* 子进程 */
Close(listenfd); /* 关闭监听套接字*/
str_echo(connfd); /* 处理客户请求*/
exit(0);
}
Close(connfd); /* 父进程关闭连接套接字 */
}
}
#include "unp.h"
void
str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
// 读入缓冲区并回射其中内容
while ( (n = read(sockfd, buf, MAXLINE)) > 0)
Writen(sockfd, buf, n);
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
err_sys("str_echo: read error");
}
问题:僵死子进程
改善:通过捕获 SIGCHLD 信号并在信号的调用 waitpid 函数来回收子进程的资源。
之所以用 waitpid 而不用 wait 是因为 Unix 信号是不排队的,而 waitpid 因为可以设置不阻塞,所以可以在循环中
被调用,这样可以处理同时到达的多个 SIGCHLD 信号。
/**
* TCP/IPv4 协议相关 收拾终止了的子进程
**/
#include "unp.h"
int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);
//1.创建套接字,绑定众所周知的端口
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
//2.当fork 子进程时,必须捕获 SIGCHLD 信号, sig_chld 为信号处理函数
Signal(SIGCHLD, sig_chld); /* must call waitpid() */
//3.等待客户连接
for ( ; ; ) {
clilen = sizeof(cliaddr);
//4.当捕获信号时,必须处理被中断的系统调用
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
//5.并发服务器
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
#include "unp.h"
void
sig_chld(int signo)
{
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
select 版本:使用 select 来处理任意个客户的单进程程序,而不是为每个客户派生一个子进程
/**
* TCP 使用 select
**/
#include "unp.h"
int
main(int argc, char **argv)
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
//1.创建套接字,绑定众所周知的端口
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
//2.初始化
maxfd = listenfd; /* initialize */
maxi = -1; /* index into client[] array */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* -1 indicates available entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
/* end fig01 */
/* include fig02 */
for ( ; ; ) {
//3.阻塞于 select
//select 等待某个事件发生:或是新客户连接的建立,或是数据、FIN或 RST的到达
rset = allset; /* structure assignment */
nready = Select(maxfd+1, &rset, NULL, NULL, NULL);
//4.accept 新的连接
if (FD_ISSET(listenfd, &rset)) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef NOTDEF
printf("new client: %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
ntohs(cliaddr.sin_port));
#endif
//用 client 数组中第一个未用项记录这个已连接描述符
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd; /* save descriptor */
break;
}
if (i == FD_SETSIZE)
err_quit("too many clients");
//在 allset 描述符中打开已连接套接字 connfd 的对应位
FD_SET(connfd, &allset); /* add new descriptor to set */
if (connfd > maxfd)
maxfd = connfd; /* for select */
if (i > maxi)
maxi = i; /* max index in client[] array */
//nready 是 select 函数的返回值,表示跨所有描述符集的已就绪的总位数
//就绪描述符数目减 1,若其值变为 0,就可以避免进入下一个 for 循环
if (--nready <= 0)
continue; /* no more readable descriptors */
}
//5.检查现有连接
//对于每个现在的客户连接,测试其描述符是否在 select 返回的描述符集中 --> ? select 有返回描述符集吗 ?
//如果是,就从客户读入一行文本并回射给它。
//如果关闭了连接,那么 read 将返回 0,要相应地更新数据结构
for (i = 0; i <= maxi; i++) { /* check all clients for data */
if ( (sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
/*4connection closed by client */
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
Writen(sockfd, buf, n);
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
/* end fig02 */
poll 版本:在 select 版本中,必须分配一个 client 数组以及一个名为 rset 的描述符集。改用 poll 后,我们只需分配 一个 pollfd 结构的数组来
维护客户信息,而不必分配另一个数组。
/**
* TCP/IPv4 协议相关,使用poll,单个进程处理所有客户
* **/
/* include fig01 */
#include "unp.h"
#include <limits.h> /* for OPEN_MAX */
int
main(int argc, char **argv)
{
int i, maxi, listenfd, connfd, sockfd;
int nready;
ssize_t n;
char buf[MAXLINE];
socklen_t clilen;
//1.分配 pollfd 结构数组
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
//2.初始化
//把 client 数组的第一项用于监听套接字,并设置 events为 POLLRDNORM
//把其余各项的成员转为 -1
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* -1 indicates available entry */
maxi = 0; /* max index into client[] array */
/* end fig01 */
/* include fig02 */
for ( ; ; ) {
//3.调用 poll,检查新的连接
nready = Poll(client, maxi+1, INFTIM);
//检查 client[0],即监听套接字上是否有新客户连接
if (client[0].revents & POLLRDNORM) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef NOTDEF
printf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));
#endif
//保存已连接套接字
for (i = 1; i < OPEN_MAX; i++)
if (client[i].fd < 0) {
client[i].fd = connfd; /* save descriptor */
break;
}
if (i == OPEN_MAX)
err_quit("too many clients");
client[i].events = POLLRDNORM;
if (i > maxi)
maxi = i; /* max index in client[] array */
if (--nready <= 0)
continue; /* no more readable descriptors */
}
//4.检查某个现有连接上的数据
// i 从 1,开始,因为 client[0]是监听套接字
for (i = 1; i <= maxi; i++) { /* check all clients for data */
if ( (sockfd = client[i].fd) < 0)
continue;
if (client[i].revents & (POLLRDNORM | POLLERR)) {
//读取数据并回射
if ( (n = read(sockfd, buf, MAXLINE)) < 0) {
if (errno == ECONNRESET) {
/*4connection reset by client */
#ifdef NOTDEF
printf("client[%d] aborted connection\n", i);
#endif
Close(sockfd);
client[i].fd = -1;
} else
err_sys("read error");
//终止连接
} else if (n == 0) {
/*4connection closed by client */
#ifdef NOTDEF
printf("client[%d] closed connection\n", i);
#endif
Close(sockfd);
client[i].fd = -1;
} else
Writen(sockfd, buf, n);
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
/* end fig02 */
多线程版本:这个版本的代码为每个客户使用一个线程,而不是子进程,同时它使用了 tcp_listen 函数来使得该程序与协议无关。
/**
* TCP 每个用户一个线程
* **/
#include "unpthread.h"
static void *doit(void *); /* each thread executes this function */
int
main(int argc, char **argv)
{
int listenfd, connfd;
pthread_t tid;
socklen_t addrlen, len;
struct sockaddr *cliaddr;
//1.创建监听套接字
if (argc == 2)
listenfd = Tcp_listen(NULL, argv[1], &addrlen);
else if (argc == 3)
listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
else
err_quit("usage: tcpserv01 [ <host> ] <service or port>");
cliaddr = Malloc(addrlen);
//2.创建线程
for ( ; ; ) {
len = addrlen;
connfd = Accept(listenfd, cliaddr, &len);
//accept 返回之后,改为调用 pthread_create 取代调用 fork
Pthread_create(&tid, NULL, &doit, (void *) connfd);
}
}
//3.线程函数
static void *
doit(void *arg)
{
//pthread_detach 使自身脱离主线程,这样主线程不用等待它
Pthread_detach(pthread_self());
str_echo((int) arg); /* same function as before */
//关闭已连接套接字
Close((int) arg); /* done with connected socket */
return(NULL);
}
问题:上面的代码把整数变量 connfd 类型强制转换成 void 指针,这并不具备兼容性。
改善:可以把一个整数指针类型强制转换为 void *,然后把这个 void * 指针类型强制转换回原来的整数指针。
但如果主线程中只有一个整数变量 connfd 的话,每次调用 accept 该变量都会被覆写为另一个新值。
因此还应该专门为每一个线程 malloc 一个整数变量的空间,用来存放该线程的处理的已连接套接字描述符,
同时还要在线程中 free 掉该空间。
/**
* TCP 每个客户一个线程,可移植的参数传递
* **/
#include "unpthread.h"
static void *doit(void *); /* each thread executes this function */
int
main(int argc, char **argv)
{
int listenfd, *iptr;
thread_t tid;
socklen_t addrlen, len;
struct sockaddr *cliaddr;
//1.创建监听套接字
if (argc == 2)
listenfd = Tcp_listen(NULL, argv[1], &addrlen);
else if (argc == 3)
listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
else
err_quit("usage: tcpserv01 [ <host> ] <service or port>");
cliaddr = Malloc(addrlen);
//2.创建线程
for ( ; ; ) {
len = addrlen;
iptr = Malloc(sizeof(int));
*iptr = Accept(listenfd, cliaddr, &len);
Pthread_create(&tid, NULL, &doit, iptr);
}
}
//3.线程函数
static void *
doit(void *arg)
{
int connfd;
connfd = *((int *) arg);
free(arg);
Pthread_detach(pthread_self());
str_echo(connfd); /* same function as before */
Close(connfd); /* done with connected socket */
return(NULL);
}