服务器开发

一起来写web server 03 -- 多线程版本

2016-10-28  本文已影响20人  Yihulee

一起来写web server 03 -- 多线程版本


错误的代码和正确的代码总是非常相似的!

好吧,我们继续开干,这一次,我们来写一个多线程版本的web服务器.

这次代码的思想十分简单,那就是一旦从客户端来了一个连接,就生成一个线程来处理这个连接.这种想法和之前的多进程版本非常类似.但是请注意进程和线程之间的差异.

进程和线程的区别

进程这种东西,一旦父进程调用fork函数,生成了一个子进程,那么子进程基本上不和父进程共享任何东西了,当然,子进程保留了父进程打开文件的指针,复制了父进程的代码区,数据区,寄存器的值,几乎所有的东西,但是一旦fork,两者之间除了父子关系,它们两者的关系就和普通的两个进程一样了.

线程不一样,父线程创建了子线程之后,父子线程之间共享很多东西,当然,子线程有自己的堆栈(堆栈这个玩意自然不能够共享,如果共享,会造成混乱.).寄存器的值也不会共享,应该还有一些我没有提到的东西不能共享吧,不过这些足够了.除此之外,全部共享,也就是说,如果在子线程里关闭了某个文件描述符,那么这个文件描述符在父线程里面一样被关闭了,事实上:

如果你在父(子)进程中能够得到子(父)线程的堆栈的指针的话,父(子)进程也能够访问子(父)进程的堆栈空间了.

这里有一个微小的错误,你能发现吗?

在推进代码的时候,我曾经写过这样一个主函数:

int main(int argc, char *argv[])
{
    int listenfd = Open_listenfd(8080); /* 8080号端口监听 */
    signal(SIGPIPE, SIG_IGN); /* 忽略SIGPIPE消息 */
    while (true) /* 无限循环 */
    {
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        
        int *fdp = (int *)Malloc(sizeof(int));
        int connfd = Accept(listenfd, (SA*)&clientaddr, &len);
        pthread_t tid;
        Pthread_create(&tid, NULL, handle, (void *)connfd);
        //close(connfd);
    }
    return 0;
}

你能发现哪里出错了吗?对了这是处理函数:

void* handle(void* arg)
{
    Pthread_detach(pthread_self()); // 脱离父线程
    int fd = (*(int *)arg);
    printf("%d: fd = %d\n", pthread_self(), fd);
    doit(fd);
    printf("%d: close fd = %d\n", pthread_self(), fd);
    close(fd);
}

这个错误非常地隐蔽,下面是一次打印的结果:

-142670080: fd = 4
-142670080:GET /cpp/concep
t.html HTTP/1.1
-142670080: close fd = 4


-151062784: fd = 5
-151062784:GET /common/ext
.css HTTP/1.1
-151062784: close fd = 5

-142670080: fd = 4
-142670080:GET /common/sit
e_modules.css HTTP/1.1
-142670080: close fd = 4

-142670080: fd = 4
-142670080:GET /common/ski
n_scripts.js HTTP/1.1

-151062784: fd = 6
-142670080: close fd = 4
-151062784:GET /common/sit
e_scripts.js HTTP/1.1

-151062784: close fd = 6

-159455488: fd = 6
Rio_readlineb error: Bad f
ile descriptor

错误的原因

好了,我这里就不卖关子了,代码错在了这里:

Pthread_create(&tid, NULL, handle, (void *)connfd);

我们可以发现,这里的connfd是栈上的一个变量,当while循环了一遍之后,这个connfd的值我们就不再确定了,他有可能是原来的值,这样的话,线程运行不会出错,有可能是任何值,这个时候就会出现打印的bad file diesriptor啦.

有一点我们是要注意的,共享的资源会导致竞争状态的产生,考虑这样一种极端情况,Pthread_create函数之后,子进程并没有得到cpu的运行时间,而主线程一直在运行,很快,主线程获得了一个新的连接,connfd的值被新的文件描述符的值填充了,这个时候子线程才开始运行,子线程获得的值是新的connfd的值吗?

所以,正确的代码示例是这样的:

/*- 
* 多线程版本的web server.
*/
void* handle(void* arg)
{
    Pthread_detach(pthread_self());
    int fd = (*(int *)arg);
    Free(arg); /* 防止内存泄露,释放 */
    printf("%d: fd = %d\n", pthread_self(), fd);
    doit(fd);
    printf("%d: close fd = %d\n", pthread_self(), fd);
    close(fd);
}

int main(int argc, char *argv[])
{
    int listenfd = Open_listenfd(8080); /* 8080号端口监听 */
    signal(SIGPIPE, SIG_IGN); 
    int connfd;
    while (true) /* 无限循环 */
    {
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        
        int *fdp = (int *)Malloc(sizeof(int)); /* 重新分配一块地址 */
        *fdp = Accept(listenfd, (SA*)&clientaddr, &len);
        pthread_t tid;
        Pthread_create(&tid, NULL, handle, (void *)fdp);
        //close(connfd);
    }
    return 0;
}

存在的小bug

这里的signal(SIGPIPE, SIG_IGN);其实对于线程来说并没有起作用,以后的版本会改进.

当然,这种模式效率依旧不是很高.

上一篇下一篇

猜你喜欢

热点阅读