多进程、多线程、多路复用实现Web服务器

相关原理与知识

  1. 调用fork函数产生一个子进程,返回值为0;父进程返回值为子进程的pid,子进程完全复制父进程。
  2. 将要监视的文件描述符集中到一起,可以利用select函数可以同时监视多个文件描述符。监视文件描述符可以视为监视套接字。程序在select处等待,直到被监视的文件描述符发生状态改变,文件描述符0,1,2分别表示标准输入,标准输出,标准错误输出。socket描述符从3开始。select执行完之后,未发生状态改变的文件描述符位被置零。fd_set是一个结构体,select默认支持文件描述符数量为1024。
  3. 主线程通过调用pthread_create函数用于创建一个线程,第一个参数是将创建线程的ID,第三个参数是线程要执行的函数的指针,最后的参数是函数需要的参数指针。调用返回时,主线程和调用的对等线程并发运行,且tid为新线程的ID。每个线程例程以通用指针作为参数,并返回一个通用指针。
  4. TCP套接字通信模型
    TCP套接字通信模型
  5. web服务器的客户端为浏览器。应用层采用HTTP协议,传输层和网络层采用TCP/IP协议。HTTP请求方法有GET、HEAD、PUT、POST、等,最常用的是GET和POST。
  6. HTML是一种标记语言,由文字和标记组合而成,学习HTML语言就是了解其标记和语法规则。

实验过程

多进程

  1. 服务器程序关键代码
    多进程
    调用Fork创建子进程,父进程继续监听。在子进程中对客户端进行处理,因为fork完全复制父进程,所以要在子进程中关掉监听套接字listenfd。子进程处理完之后,父子进程都要关掉连接套接字connfd。
  2. 执行服务器程序
    执行
  3. 查看结果
    结果

多路复用

  1. 服务器程序关键代码
    多路复用
    Fd_set rds,tmp定义两个文件描述符集,rds为用于监控的集合,tmp用于临时复制rds。FD_ZERO将rds清零,RD_SET将选定的位置1,fd_max是监听的套接字中最大的套接字描述符加一。
    因为每次执行完select之后,未发生状态变化的套接字位会被置0,所以在循环开始时要对rds进行复制,对副本tmp进行select操作。select会把未就绪的描述符位置0,遍历文件描述符至fd_max,对每个文件描述符进行判断是否在文件描述符集tmp中,若在则进行处理。因为处理的都是就绪的套接字,所以不会发生阻塞。
    如果为监听套接字就绪,新建立连接的描述符要加入rds中,如果为连接套接字,处理完之后要将其从文件描述符集rds中清除

  2. 执行服务器程序
    执行结果

多线程

  1. 服务器程序关键代码
    定义一个tid用于记录创建线程的id,主线程用于监听套接字,有新的连接建立之后创建一个副线程,主线程继续监听。副线程调用执行线程函数,在该函数中完成对新建立连接的处理,处理完之后关闭套接字,副线程结束。
    多线程
  2. 执行服务器程序
    执行结果

实验结果分析

使用进程最简单,因为每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系,每个子进程都有自己的地址空间和相关资源,总体能够达到的性能上限非常大,但是调度开销比较大。
多路复用编程复杂度高,但是由于多路复用是在单一进程的上下文中的,因此每个逻辑流程都能访问该进程的全部地址空间,所以开销比多进程低得多。
多线程每个线程都有自己的线程上下文,所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间。由于线程运行在单一进程中,因此共享这个进程虚拟地址空间的整个内容,包括它的代码、数据、堆、共享库和打开的文件。优点是程序逻辑和控制方式简单,所有线程可以直接共享内存和变量等,耗的总资源比进程方式好。但是由于每个线程与主程序共用地址空间,地址空间受限,一个线程的崩溃也可能影响到整个程序的稳定性,性能提高也有限制。

注意问题

  1. 每次执行完select之后,未发生状态变化的套接字位会被置0,所以在循环开始时要对文件描述符集进行复制备份,对副本进行select操作。
  2. pthread的库不是linux系统的库,所以在进行编译的时候要加上 -lpthread
  3. int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(start_rtn)(void), void *restrict arg); start_rtn是函数指针, 为线程开始执行的函数名,函数参数必须为通用型指针,使用参数时再转换成需要的类型以使用,原因是为了方便扩展,比如要传多个参数, 可以用结构封装,然后传指向该结构体的指针即可。
  4. 调用pthread_detach可以分离线程,这样在其结束后可以由系统自动释放资源。

源代码(仅main函数)

  1. tiny_fork.c
int main(int argc, char **argv) 
{
    int listenfd, connfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;

    if (argc != 2) {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
    }

    listenfd = Open_listenfd(argv[1]);
    while (1) {
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); 
        if (Fork() == 0) {
            Close(listenfd);
            Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, 
                        port, MAXLINE, 0);
            printf("Accepted connection from (%s, %s)\n", hostname, port);
            doit(connfd);
            Close(connfd);
            exit(0);
        }
        Close(connfd);
    }

    return 0;
}
  1. tiny_select.c
int main(int argc, char **argv) 
{
    int listenfd, connfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    fd_set rds, tmp;
    int fd_max,i;

    if (argc != 2) {
	fprintf(stderr, "usage: %s <port>\n", argv[0]);
	exit(1);
    }

    listenfd = Open_listenfd(argv[1]);
    FD_ZERO(&rds);
    FD_SET(listenfd, &rds);
    fd_max = listenfd + 1;

    while (1) {
        tmp = rds;
        select(fd_max, &tmp, NULL, NULL, NULL);
        for (i=0; i<fd_max; i++) {
            if (FD_ISSET(i, &tmp)) {
                if (listenfd == i) {
                    clientlen = sizeof(clientaddr);
                    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
                    FD_SET(connfd, &rds);
                    if (fd_max <= connfd) fd_max = connfd+1;

                    Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, 
                        port, MAXLINE, 0);
                    printf("Accepted connection from (%s, %s)\n", hostname, port);
                }
                else {
                    doit(i);
                    FD_CLR(i, &rds);
                    Close(i);
                }
            }
        }
    }
}
  1. tiny_thread.c
void *thread(void *argv);
int main(int argc, char **argv) 
{
    int listenfd, connfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    pthread_t tid;

    if (argc != 2) {
	fprintf(stderr, "usage: %s <port>\n", argv[0]);
	exit(1);
    }

    listenfd = Open_listenfd(argv[1]);
    while (1) {
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
            Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, 
                        port, MAXLINE, 0);
        printf("Accepted connection from (%s, %s)\n", hostname, port);
        Pthread_create(&tid, NULL, thread, &connfd);
    }

    return 0;
}
void *thread(void *argv)
{
    int connfd = *((int *)argv);
    Pthread_detach(pthread_self());
    doit(connfd);  
    Close(connfd);

    return NULL;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值