Unix/Linux编程:单线程、并发服务器(TCP)------ECHO

目标:说明一个设计思想------服务器只使用单个控制线程,就能为若干个客户提供表面上的并发性

服务器中的数据驱动处理

如果为一个请求准备响应所需的开销中,IO占了主导地位,则在此种应用中,服务器可以使用异步IO来提供客户间的表面上的并发性。这个想法很简单:让一个服务器执行线程对多个客户打开它们的TCP连接,并使服务器在数据到达时处理该连接。因此,服务器使用数据的到达来触发处理

要理解这种方法为什么是可行的,考虑这里描述的并发ECHO服务器。为了达到并发运行,例子代码中创建了一个单独的从线程来处理每个新的连接。从理论上来讲,服务器依赖操作系统的时间分片机制在多个线程之间共享CPU,并由此在多个连接间共享CPU

但在实际中,ECHO服务器几乎与时间分片无关。如果密切的观察一个并发ECHO服务器的执行,你就会发现通常是数据的到达控制了处理的进行。其原因与穿过互联网的数据流有关。由于下层互联网是用离散的分组交付数据的,因而数据是以突发形式(而不是平稳数据流方式)到达服务器的。如果客户发送数据块的方式是使每一个最终形成的TCP报文段各填入单个IP数据报中,那么客户就加剧了这种突发行为。在服务器上,每个从线程将大部分时间花在read调用中,即它被阻塞以等待下一个突发数据的到达。一旦数据到达,read调用就返回,从线程继续执行。从线程调用write发送数据给客户,然后调用read再次等待后面的数据。CPU要能处理很多客户的请求而不减慢处理速率,就必须运行得足够块,以便在另一个从线程收到数据前就完成了读写

当然,如果符合太重,以致CPU不能在另一个请求到达前处理完另一个请求,分时机制就将起作用。操作系统在所有有数据要处理的从线程之间切换处理器。对于仅需对每个请求进行很少处理的简单服务,执行由数据到达来驱动的机会是很高的。

总结:如果并发服务器处理每个请求仅需很少的时间,通常它就按照顺序方式执行,而执行由数据的到达驱动。只有在工作量太大,以致CPU不能顺序执行时,分时机制取而代之

用单线程仅需数据驱动处理

要理解并发服务器行为的顺序特性,就可以理解单个执行程序如何完成同样的任务。想象一个单服务器线程,它打开了到很多客户的TCP连接。线程将阻塞以等待数据的到达。一旦任何一个连接上有数据到达,线程就被唤醒,并处理请求和发送响应。然后它再次阻塞,等待另一个连接上更多数据的到达。只要CPU足够块的应付服务器上出现的工作负荷,单用单线程就能像使用多线程那样处理各个请求。实际上,与使用多线程和多进程的实现相比,单线程的实现很少需要在线程或进程上下文之间进行切换,因而可能处理更高的负荷。

编写一个单线程并发服务器的关键是通过在操作系统原语select中使用异步IO。一个服务器为它必须管理的每一个连接创建一个套接字,然后调用select等待任一连接上数据的到达。实际上,由于select可以在所有可能的套接字上等待IO,它也能同时等待新的连接

单线程服务器的线程架构

如下给出了单线程、并发服务器的架构,一个线程管理所有的套接字
在这里插入图片描述
实质上,单线程服务器必须完成主线程和从线程双方的职责。它维护一组套接字,组中的一个套接字绑定到主线程将要接受连接的熟知端口上。而组中其他每一个套接字都对应于一个连接,在此连接上一个从线程将处理请求。服务器把这一组套接字描述符作为一个参数传递给select,并等待任何一个套接字上的活动。当select返回时,它返回一个屏蔽位,指明这一组描述符中的哪一个已就绪。服务器按照描述符准确就绪的指示来决定如何继续处理

单线程服务使用描述符来区分主线程和从线程的操作。如果主套接字相应的描述符准备就绪,服务器就进行主线程要做的同样的操作:它在套接字上调用accept以获得新连接。如果对应于一个从套接字的描述符准确就绪,服务器就进行从线程要做的同样操作:它调用read获取一个请求,然后做出回答

实现:ECHO

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>

#include <unistd.h>
#include <string.h>
#include <stdio.h>

#define	QLEN		  32	/* maximum connection queue length	*/
#define	BUFSIZE		4096

extern int	errno;
int		errexit(const char *format, ...);
int		passiveTCP(const char *service, int qlen);
int		echo(int fd);

/*------------------------------------------------------------------------
 * main - Concurrent TCP server for ECHO service
 *------------------------------------------------------------------------
 */
int
main(int argc, char *argv[])
{
    char	*service = "echo";	/* service name or port number	*/
    struct sockaddr_in fsin;	/* the from address of a client	*/
    int	msock;			/* master server socket		*/
    fd_set	rfds;			/* read file descriptor set	*/
    fd_set	afds;			/* active file descriptor set	*/
    unsigned int	alen;		/* from-address length		*/
    int	fd, nfds;

    switch (argc) {
        case	1:
            break;
        case	2:
            service = argv[1];
            break;
        default:
            errexit("usage: TCPmechod [port]\n");
    }

    msock = passiveTCP(service, QLEN);

    nfds = getdtablesize();
    FD_ZERO(&afds);
    FD_SET(msock, &afds);

    while (1) {
        memcpy(&rfds, &afds, sizeof(rfds));

        if (select(nfds, &rfds, (fd_set *)0, (fd_set *)0,
                   (struct timeval *)0) < 0)
            errexit("select: %s\n", strerror(errno));
        if (FD_ISSET(msock, &rfds)) {
            int	ssock;

            alen = sizeof(fsin);
            ssock = accept(msock, (struct sockaddr *)&fsin,
                           &alen);
            if (ssock < 0)
                errexit("accept: %s\n",
                        strerror(errno));
            FD_SET(ssock, &afds);
        }
        for (fd=0; fd<nfds; ++fd)
            if (fd != msock && FD_ISSET(fd, &rfds))
                if (echo(fd) == 0) {
                    (void) close(fd);
                    FD_CLR(fd, &afds);
                }
    }
}

/*------------------------------------------------------------------------
 * echo - echo one buffer of data, returning byte count
 *------------------------------------------------------------------------
 */
int echo(int fd)
{
    char	buf[BUFSIZ];
    int	cc;

    cc = read(fd, buf, sizeof buf);
    if (cc < 0)
        errexit("echo read: %s\n", strerror(errno));
    if (cc && write(fd, buf, cc) < 0)
        errexit("echo write: %s\n", strerror(errno));
    return cc;
}

在这里插入图片描述

服务器使用系统函数getdtablesize来获取描述符的最大个数,然后使用FD_ZERO和FD_SET创建一个比特向量,对应于希望测试的套接字描述符。然后服务器进入一个无限循环,在循环中调用select,等待一个或者多个描述符就绪(因为select在返回时,清除了所有没有能进行读或写操作的描述符,所以程序将用到的活动描述符复制到rfds中,以供每次循环使用)

  • 如果主描述符准备就绪,服务器就调用accept获取一个新连接,它将新连接用的描述符加入到它所管理的那些描述符中,并继续等待更多的描述符动作;
  • 如果一个从描述符准备就绪,服务器就调用过程echo,该过程再调用read从这一连接获取数据,并调用write将数据发回给客户。如果其中某个从描述符报告了文件结束(end-of-file)的条件,服务器就关闭该描述符,并使用FD_CLEAR从select使用的那组描述符中删除它。

总结

并发服务器中的执行通常是由数据到达驱动的,而不是从下层操作系统中时间分片机制驱动的。在服务器仅需很少处理的情况下,单线程的实现可使用异步IO管理到多个客户的连接,这种实现与使用多个线程的实现一样高效。

在单线程实现中,一个指向线程完成了主线程和从线程的职责。当主线程准备就绪时,服务器接受一个新的连接。当其他任何一个套机字就绪时,服务器读取一个请求,并发送响应。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值