linux的socket命令,Linux socket函数
并发客户端服务器
到目前为止,我们介绍的这些服务器程序在接受下一个客户机连接之前只处理一个客户机请求。对于即时回复的服务器来说,这既高效又简单。但是,如果处理需要很长时间或者有一段时间不活动,这种设计将阻止其他客户端无限期连接。因为服务器通常在最短的延迟时间内为尽可能多的客户端提供服务,所以在服务器的连接端需要进行基本的设计更改。
在本章中,我们将学习以下内容:
使用fork(2)函数处理多个客户端连接。
wait(2)和waitpid(2)函数
选择(2)处理多个客户端连接的功能
掌握这些内容将使我们能够编写一个可以同时处理大量客户端的专业服务器。
了解多个客户的问题。
图11.1显示了多个客户端连接到同一个服务器的情况。
图11.1中心的服务器必须在多个连接的客户端之前平衡资源。服务器通常被设计成每个客户机都认为它对服务器有独占访问权。然而,实际上服务器是以并发的方式为所有客户端服务的。
这可以通过以下方式实现:
派生服务器进程(多进程方法)
线程服务器进程(多线程方法)
一个进程和一个选择调用。
一个进程和一个轮询(2)调用
使用fork系统调用的第一种方法可能是服务多客户端进程的最简单的方法。但是,他的缺点是信息共享变得更加复杂。这通常需要使用消息队列、共享内存和信号量。另一个缺点是,它要求CPU为每个请求启动和管理一个新的进程。
线程服务器是UNIX的新方法,也是Linux的新选项。线程提供了多进程方法的轻量级优势,而不妨碍中心的通信。但是,线程处理很难,尤其是对于新程序员。为此,我们在此不讨论线程的相关内容。
最后两个方法需要调用select或poll函数。每个函数都提供了一种不同的方式来阻止服务的运行,直到有事情发生。我们将在本章中详细讨论选择功能。感兴趣的读者可以阅读poll手册页来了解poll。
使用fork(2)服务多个客户。
这里,我们将使用fork函数来修改第10章中开发的服务器程序,以处理多个客户端。下面的代码是修改后的rpnsrv.c模块。其他代码与上一章中的代码相同。
/*rpnsrv.c
*
RPN服务器示例:
*/
#包含stdio.h
#包括unistd.h
#包含stdlib.h
#包含字符串. h
#包含错误号h
#包含时间. h
#包含sys/types.h
#包含sys/socket.h
#包含netinet/in.h
#包括arpa/inet.h
#包含netdb.h
#包含sys/wait.h
#包含信号. h
#ifndef SHUT_RDWR
#定义SHUT_RDWR 3
#endif
extern int mkaddr(void *addr,
int *addr_len,
char *输入地址,
char *协议);
外部无效rpn_process(FILE *tx,
char * buf);
/*
*进程终止的子进程:
*/
静态void sigchld_handler(int signo)
{
pid _ t PID
int状态;
做
{
PID=waitpid(-1,状态,wno hang);
}而(PID!=-1);
/*
*恢复状态处理程序
*/
signal(SIGCHLD,SIGCHLD _ hander);
}
/*
*该函数报告错误并
*出口回到外壳:
*/
静态无效保释(const char *on_what)
{
如果(errno!=0)
{
fputs(strerror(errno),stderr);
fputs(:,stderr);
}
fputs(on_what,stderr);
fputc(/n ,stderr);
出口(1);
}
int main(int argc,char **argv)
{
int z;
char * srvr _ addr= 127 . 0 . 0 . 1:9090 ;
struct sockaddr _ in adr _ srvr/* AF_INET */
结构sockaddr _ in adr _ clnt/* AF_INET */
int len _ inet/*长度*/
int s=-1;/*套接字*/
int c=-1;/*客户端套接字*/
FILE * rx=NULL/*读取流*/
FILE * tx=NULL/*写入流*/
char buf[4096];/* I/O缓冲器*/
pid _ t PID/*进程ID */
/*
*为SIGCHLD设置信号处理器:
*/
signal(SIGCHLD,SIGCHLD _ hander);
/*
*从命令中输入服务器地址
*行,否则默认为127.0.0.1:
*/
如果(argc=2)
{
srvr _ addr=argv[1];
}
len _ inet=sizeof adr _ srvr
z=mkaddr( adr_srvr,len_inet,
srvr_addr, TCP );
if(z 0 !adr_srvr.sin_port)
{
fprintf(stderr,无效服务器
地址,或没有端口号
已指定。/n’);
出口(1);
}
/*
*创建要使用的TCP/IP套接字:
*/
s=socket(PF_INET,SOCK_STREAM,0);
如果(s==-1)
提环(插座(2));
/*
*绑定服务器地址:
*/
z=bind(s,(struct sockaddr *) adr_srvr,len _ inet);
如果(z==-1)
保释( bind(2));
/*
*使其成为监听插座:
*/
z=listen(s,10);
如果(z==-1)
保释(听(2));
/*
*启动服务器循环:
*/
for(;)
{
/*
*等待连接:
*/
len _ inet=sizeof adr _ clnt
c=accept(s,(struct sockaddr *) adr_clnt,len _ inet);
如果(c==-1)
保释(接受(2));
/*
*对于新的服务器进程
*要服务此客户端:
*/
if((PID=fork())==-1)
{
/*分叉失败:放弃*/
关闭(c);
继续;
}
else if(PID 0)
{
/*父进程:*/
关闭(c);
继续;
}
/*
*子进程
*创建流:
*/
rx=fdopen(c, r );
如果(!rx)
{
/*失败*/
关闭(c);
继续;
}
tx=fdopen(dup(c), w );
如果(!tx)
{
fclose(rx);
继续;
}
/*
*将两个流都设置为行缓冲模式:
*/
setlinebuf(rx);
setlinebuf(tx);
/*
*处理客户的请求:
*/
while(fgets(buf,sizeof buf,rx))
rpn_process(tx,buf);
/*
*关闭此客户端的连接:
*/
fclose(tx);
shutdown(fileno(rx),SHUT _ RDWR);
fclose(rx);
/*
*子进程必须退出:
*/
退出(0);
}
返回0;
}
使用选择(2)来设计服务器
我们之前提供的服务器程序使用fork程序来处理多个客户端请求,但是还有其他可能更好的方法。在多个客户端之间共享信息的服务器可能会发现它需要在单个进程中包含该服务器。要求单个进程的另一个要求是,一个进程不会消耗多个进程所需的系统资源。出于这些原因,我们需要考虑新的服务器设计。
选择(2)功能介绍
select函数允许我们阻塞服务器的执行,直到服务器需要做一些事情。更具体地说,他允许呼叫者知道以下内容:
什么时候需要读取文件描述符?
什么时候写文件描述符而不阻塞服务器程序的执行?
文件描述符什么时候出现异常?
我们可能记得windows套接字句柄是一个文件描述符。当任何指定的连接windows套接字集上发生事件时,select函数将通知服务器。事实上,这将允许服务器以高效的方式处理多个客户端。
正如我们前面指出的,当任何新的请求数据从客户端windows sockets到达时,服务器会得到通知。正是由于这个原因,服务器需要知道数据何时被指定的客户端windows套接字读取。
当将数据发送回客户端时,服务器必须知道将数据无阻塞地写入windows套接字是非常重要的。例如,如果连接的客户端请求返回大量信息,服务器会将这些信息写入windows套接字。如果客户端软件有缺陷或读取数据缓慢,服务器将阻塞一段时间,并尝试写入剩余的结果数据。这将使连接到此服务器的其他客户端不得不等待。这正是我们不想要的,因为每个客户都必须尽快做出响应。
如果我们的服务器必须同时处理超边缘数据,那么我们可能会对windows套接字中发生的异常感兴趣。
选择功能总结如下:
#包含系统/时间. h
#包含sys/types.h
#包括unistd.h
int select(int n,
fd_set *readfds,
fd_set *writefds,
fd _ set * exceptfds,
struct time val * time out);
该函数需要五个输入参数:
1要测试的文件描述符的最大数量(n)。这个值至少是最大文件描述符值加1,因为文件描述符从0开始。
测试了2组文件描述符(readfds)来读取数据。
3组文件描述符(writefds)经过测试,用于写入数据。
测试了4组文件描述符(exceptfds)的异常情况。
5指向应用于此函数调用的超时条件(超时)的指针。这个指针可以为空,表示没有超时(这个函数可能会永远阻塞)。
select函数的返回结果总结如下:
-1表示函数调用中有错误。错误号存储在errno变量中。
0表示发生超时而没有其他错误。
0大于指示事件发生的文件描述符的数量。
时间结构
最后一个参数timeout指向一个必须初始化的结构,除非指定了空指针。Timeval结构定义如下:
结构时间间隔{
长tv _ sec/*秒*/
long tv _ usec/*微秒*/
};
要建立1.75秒的超时值,我们可以使用以下代码:
结构化电视;
TV . TV _ sec=1;
tv.tv _ usec=750000
正在处理文件描述符集合
select函数中的第二、第三和第四个函数需要fd_set值,这对我们来说可能是新的。这是一种不透明的数据类型,因为它需要使用提供的宏进行操作。我们可用的宏汇总如下:
FD _ ZERO(FD _ set * set);
FD_SET(int fd,FD _ SET * SET);
FD_CLR(int fd,FD _ set * set);
FD_ISSET(int fd,FD _ set * set);
这些C宏允许我们操作文件描述符的集合。下一节将详细描述这些宏。
使用FD_ZERO宏
这个C宏用于初始化一组文件描述符。在注册文件描述符(包括windows套接字)之前,我们必须将它们全部初始化为0。要初始化一组名为read_socks和write_socks的文件描述符,我们可以使用下面的C语句:
fd _ set read _ socks
fd _ set write _ socks
FD _ ZERO(read _ socks);
FD _ ZERO(write _ socks);
前两条语句声明了与文件描述符集相关的存储区域。最后两条语句使用FD_ZERO宏将其初始化为一个空集合。换句话说,在执行FD_ZERO之后,这个集合中没有注册的文件描述符。
使用FD_SET宏
在我们使用FD_ZERO宏初始化一组文件描述符之后,接下来我们需要做的就是在其中注册一些文件描述符。这可以通过FD_SET宏来完成。以下示例显示了如何在名为read_socks的集合中注册windows sockets C:
int c;/*客户端套接字*/
fd _ set read _ socks/*读取集*/
.
FD_SET(c,read _ socks);
调用FD_SET后,对应于文件描述符的位被注册到引用的SET中。
使用FD_CLR宏
这个C宏取消了FD_SET宏的操作。再一次,假设windows sockets C,如果调用者想从这个集合中删除这个描述符,我们可以执行下面的代码:
int c;/*客户端套接字*/
fd _ set read _ socks/*读取集*/
.
FD_CLR(c,read _ socks);
FD_CLR宏具有清除文件描述中相应位的功能。注意,这个宏不同于FD_ZERO,因为它只是清除集合中指定的文件描述符。FD_ZERO宏清除集合中的所有位。
使用FD_ISSET宏测试文件描述。
有时需要测试指定的文件描述符是否在这个集合中(即相应的位是否被设置为1)。要测试windows sockets C是否已设置,我们可以编写以下代码:
int c;/*客户端套接字*/
fd _ set read _ socks/*读取集*/
.
if ( FD_ISSET(c,read_socks) ) {
/*套接字c在集合中*/
.
}否则{
/*套接字c不在集合中*/
.
}
if语句调用FD_ISSET宏来查看文件描述符集read_socks中是否存在windows sockets C。如果测试返回true,则集合中有对应于windows sockets C的位,然后执行第一个C代码块。否则,windows sockets C不是该集合的一部分,那么将执行else语句块。
在服务器上应用选择功能
前面的内容详细描述了选择功能。现在您需要将这个函数应用到一个示例中。以下修改后的RPN计算服务器示例将使用select函数来读取只读事件。这种限制是为了保持程序示例相对简短和易于理解。这个演示的局限性将在后面详细讨论。
RPN服务器需要引擎模块rpneng.c做一些修改,这些修改反应在新的模块rpneng2.c中。在这里我们并没有列出全部的代码,而只是用差速器显示了内容上的小改动:
$ diff -c rpneng.c rpneng2.c
* * * RP能。c 1999年年9月13日星期一22时13分56秒
-1999年9月15日星期三21时55分20秒
***************
*** 18,25 ****
* RPN堆栈;
*/
#定义MAX_STACK 32
!静态mpz _ t * STACK[MAX _ STACK];
!静态int sp=0;
/*
*分配一个新的mpz_t值:
- 18,25 -
* RPN堆栈:
*/
#定义MAX_STACK 32
!mpz _ t * *栈;
!int sp=0;
/*
*分配一个新的mpz_t值:
***************
*** 45,51 ****
/*
*释放已分配的mpz_t值:
*/
!静态空隙
rpn_free(mpz_t **v) {
mpz _ clear(* * v);
免费(* v);
- 45,51 -
/*
*释放已分配的mpz_t值:
*/
!空的
rpn_free(mpz_t **v) {
mpz _ clear(* * v);
免费(* v);
$
rpneng2.c模块的主要改变在于RPN栈数组(变量堆栈)以及栈指针(变量sp)声明为外部变量。静态函数rpn_free同样做为变部函数。这允许程序可以由主程序源码模块中访问变量和函数。
/*rpnsrv2.c
*
RPN服务器示例
*使用选择(2):
*/
#包含标准视频
#包含标准库
#包括unistd.h
#包含字符串。h
#包含错误号h
#包含时间。h
#包含系统/时间。h
#包含sys/types.h
#包含sys/socket.h
#包含netinet/in.h
#包括arpa/inet.h
#包含netdb.h
#包含sys/wait.h
#包含gmp.h
#ifndef SHUT_RDWR
#定义SHUT_RDWR 3
#endif
extern int mkaddr(void *addr,
int *addr_len,
字符*输入地址,
字符*协议);
extern void rpn_process(FILE *tx,char * buf);
extern void rpn _ free(mpz _ t * * v);
#定义MAX_STACK 32
#定义最大客户数量64
/*
*在rpneng2.c中声明
*/
外部mpz _ t * *栈;
sp中的走读生
/*
*客户端上下文信息:
*/
数据类型说明结构
{
mpz _ t * *栈;/*堆栈数组*/
int sp/*堆栈prt */
文件* rx/*接收文件*/
FILE * tx/* Xmit文件*/
} ClientInfo
客户信息表客户端[MAX _ CLIENTS];
/*
*此函数报告错误并
*出口回到外壳:
*/
静态无效保释(const char *on_what)
{
如果(errno!=0)
{
fputs(strerror(errno),stderr);
fputs(:,stderr);
}
fputs(on_what,stderr);
fputc(/n ,stderr);
出口(1);
}
/*
*流程客户端丙:
*/
静态int process_client(int c)
{
char buf[4096];/*输入/输出缓冲器*/
FILE *rx=client[c].rx;
FILE *tx=client[c].tx;
/*
*安装正确的RPN堆栈:
*/
stack=client[c].堆栈;
sp=客户端c .sp;
/*
*如果不是EOR,则留置处理:
*/
如果(!feof(rx) fgets(buf,sizeof buf,rx))
rpn_process(tx,buf);
如果(!feof(rx))
{
/*
*保存特殊卡并退出
*/
客户端c .sp=sp
返回0;
}
/*
*关闭此客户端连接:
*/
fclose(tx);
shutdown(fileno(rx),SHUT _ RDWR);
fclose(rx);
客户端c .rx=客户端c .tx=空
while(sp 0)
rpn _ free(stack[-sp]);
免费(栈);
客户端c .堆栈=空
客户端c .sp=0;
返回文件结束
}
/*
*邮件程序:
*/
int main(int argc,char **argv)
{
int z;
char * srvr _ addr= 127。0 .0 .1:9090 ;
struct sockaddr _ in ADR _ srvr/* AF _ INET */
结构sockaddr _ in adr _ clnt/* AF_INET */
int len _ inet/*长度*/
int s=-1;/*套接字*/
int c=-1;/*客户端套接字*/
int n;/*从挑选返回英国压力单位
int mx/*最大fd 1 */
fd _ set rx _ set/*读取集*/
fd _ set wk _ set/*工作集*/
结构化电视;/*超时值*/
/*
*初始化客户端结构:
*/
for(z=0;z MAX _ CLIENTSz)
{
客户端[z]的缩写.堆栈=空
客户端[z]的缩写.sp=0;
客户端[z]的缩写.rx=空
客户端[z]的缩写.tx=空
}
/*
*使用命令中的服务器地址
*行,否则默认为127.0.0.1:
*/
如果(argc=2)
srvr _ addr=argv[1];
len _ inet=sizeof adr _ srvr
z=mkaddr( adr_srvr,len_inet,srvr_addr, TCP );
if(z 0 !adr_srvr.sin_port)
{
fprintf(stderr,无效服务器
地址,或没有端口号
已指定. n’);
出口(1);
}
/*
*创建要使用的传输控制协议套接字:
*/
s=socket(PF_INET,SOCK_STREAM,0);
如果(s==-1)
bail( socket());
/*
*绑定服务器地址:
*/
z=bind(s,(struct sockaddr *) adr_srvr,len _ inet);
如果(z==-1)
bail( bind());
/*
*使其成为监听插座:
*/
z=listen(s,10);
如果(z==-1)
保释( listen());
/*
*表达对插座的兴趣
* s用于读取事件:
*/
FD _ ZERO(rx _ set);/* init */
FD_SET(s,rx _ SET);/* s */
MX=S1;/*最大fd 1 */
/*
*启动服务器循环
*/
for(;)
{
/*
*将rx_set复制到wk_set:
*/
FD _ ZERO(wk _ set);
for(z=0;z z)
{
if(FD_ISSET(z,rx_set))
FD_SET(z,wk _ SET);
}
/*
* 2.03秒的采样超时:
*/
电视。TV _ sec=2;
tv.tv _ usec=30000
n=select(mx,wk_set,NULL,NULL,TV);
如果(n==-1)
{
fprintf(stderr, %s:select(2)/n ,
strerror(errno));
出口(1);
}
else if(!n)
{
puts(“超时");
继续;
}
/*
*检查是否发生了连接:
*/
if(FD_ISSET(s,wk_set))
{
/*
*等待连接:
*/
len _ inet=sizeof adr _ clnt
c=accept(s,(struct sockaddr *) adr_clnt,len _ inet);
如果(c==-1)
保释(接受(2));
/*
*看看我们是否超过了服务器
*容量。如果是这样,请关闭
*套接字并等待
*下一个事件:
*/
如果(c=最大客户端数)
{
关闭(c)和:
继续;
}
/*
*创建流:
*/
客户端c .rx=fdopen(c, r );
如果(!客户端c .rx)
{
关闭(c)和:
继续;
}
客户端c .tx=fdopen(dup(c), w );
如果(!客户端c .tx)
{
fclose(客户端c。rx);
继续;
}
中频(^ 1 MX)
MX=C1;
/*
*将两个流都设置为线
*缓冲模式:
*/
setlinebuf(客户端).rx);
setlinebuf(客户端).tx);
/*
*分配堆栈:
*/
客户端c .sp=0;
客户端c .STACK=(mpz _ t * *)malloc(sizeof(mpz _ t *)* MAX _ STACK);
FD_SET(c,rx _ SET);
}
/*
*检查客户端活动:
*/
for(c=0;c c)
{
如果(c==s)
继续;
if(FD_ISSET(c,wk_set))
{
如果(进程客户端(c)==EOF)
{
FD_CLR(c,rx _ set);
}
}
}
/*
*如果我们能够做到,减少mx:
*/
for(c=MX-1;
c=0!FD_ISSET(c,rx _ set);
c=mx-1)
MX=c;
}
返回0;
}