1. 程式人生 > >手把手教你用nginx開發自己的伺服器------利用nginx開發一個helloWorld程式(一)

手把手教你用nginx開發自己的伺服器------利用nginx開發一個helloWorld程式(一)

能開始學習nginx的你,肯定也擼了不少程式碼了,相信你學習程式碼都是從helloWorld開始的,那麼,今天我們就用nginx開發一個helloWorld,我們將要實現的功能就是當瀏覽器來訪問你的伺服器時,你的終端列印一個helloWorld。

先別急著開始擼程式碼,先聊一聊自己為什麼想寫這個專欄,其實本人也是個伺服器開發菜鳥,感覺年紀稍微大了一點,反應和記憶力就下降的很厲害了,必須得寫點什麼幫助自己記憶一下。這個專欄將會幫助廣大的nginx菜鳥們一點點深入理解nginx,我的每篇文章都不會很長,但都會有一個重點,如果你細心跟著我一直學習下去,你至少將學會如何用nginx搭建自己的web伺服器,用nginx搭建自己的反向代理伺服器,然後如果你足夠認真,對於nginx的各種高階特性,諸如記憶體對齊帶來的高效率,slab演算法,各種池(記憶體池,連線池)也會有一定的瞭解。在講解nginx的同時,我也會同時結合nginx中的很多關於網路的特性,講解說明一些現有的前沿的知識。如http2.0,tls1.3等,如果你發現我的文章有任何紕漏之處,歡迎大家指正。

在正式開始上手nginx之前,我們先回憶下一個echo伺服器是怎麼做的:

#include <netdb.h>
#include <sys/socket.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>

#define EHCO_PORT    8080
#define MAX_CLIENT_NUM        10

int main()
{
    int socketfd;
    socketfd = socket(AF_INET, SOCK_STREAM, 0);
       
    if(socketfd == -1)
    {
        printf("errno=%d ", errno);
        exit(1);
    }
    else
    {
        printf("socket create successfully ");
    }

    struct sockaddr_in sa;
    bzero(&sa, sizeof(sa));
    sa.sin_family = AF_INET;
    sa.sin_port = htons(EHCO_PORT);
    sa.sin_addr.s_addr = htons(INADDR_ANY);
    bzero(&(sa.sin_zero), 8);

    if(bind(socketfd, (struct sockaddr *)&sa, sizeof(sa))!= 0)
    {
        printf("bind failed ");
        printf("errno=%d ", errno);
        exit(1);
    }
    else
    {
        printf("bind successfully ");
    }

    //listen
    if(listen(socketfd ,MAX_CLIENT_NUM) != 0)
    {
        printf("listen error ");
        exit(1);
    }
    else
    {
        printf("listen successfully ");
    }

    int clientfd;
    struct sockaddr_in clientAdd;
    char buff[101];
    socklen_t len = sizeof(clientAdd);
    int closing =0;
    while( closing == 0  && (clientfd = accept(socketfd, (struct sockaddr *)&clientAdd, &len)) >0 )
    {
        int n;
        while((n = recv(clientfd,buff, 100,0 )) > 0)
        {
            printf("number of receive bytes = %d ", n);
            write(STDOUT_FILENO, buff, n);
            send(clientfd, buff, n, 0);
            buff[n] = '';
            if(strcmp(buff, "quit ") == 0)
            {
                break;
            }
            else if(strcmp(buff, "close ") == 0)
            {
                //server closing
                closing = 1;
                printf("server is closing ");
                break;
            }
        }

        close(clientfd);
    }

    close(socketfd);

    return 0;
}

任何伺服器,都逃不過那幾個基本的函式,socket().bind(),listen(),connect(),accept(),read(),write(),send(),recv()無論多麼高階的伺服器,本質上都是利用作業系統給的IO複用和各種設計模式對這幾個介面做了封裝。在學習伺服器開發的時候個人覺得一定要把握這個本質(畢竟再複雜的邏輯也就是收到一段message,然後處理message,再發送相應的response回去嘛)。

回憶好一個echo伺服器是怎麼做的之後,再複習一下一個不得不說的基本知識:事件驅動機制。什麼是事件驅動機制呢?字面理解就是,一個事件到來,才會觸發下一個事件,一個事件觸發一個事件,層層推進。文字說起來很抽象,其實本質上就是select,poll,epoll這幾個IO多路複用函式結合回撥函式實現的架構。來看下一個簡單的epoll伺服器的邏輯是怎麼樣的:

//這裡摘取了部分重要的程式碼來說明一個簡單的epoll伺服器的框架,如果想看完整的epoll伺服器實現可以去我的github上看https://github.com/zk3326312/EpollServer
if((listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
    {
        perror("socket error");
        return -1;
    }

    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(default_port);
    serveraddr.sin_family = AF_INET;


    if( bind(listenfd,(sockaddr*)&serveraddr,sizeof(sockaddr)) == -1)
    {
        cout<<"bind error"<<endl;
        return -1;
    }


    if(listen(listenfd,listen_que_num) == -1)
    {
        cout<<"listen error"<<endl;
        return -1;
    }

    sp_Epoll->add(listenfd,EPOLLIN);


    while(true)
    {
        int nfds = sp_Epoll->wait(maxfdNUM,-1);
        for(int i = 0;i < nfds;i++)
        {
            if(sp_Epoll->_events[i].data.fd == listenfd)
            {
                if((confd = accept(listenfd,(sockaddr*)&clientaddr,&clilen)) == -1)
                {
                    cout<<"accept error"<<endl;
                    return -1;
                }
                char* str = inet_ntoa(clientaddr.sin_addr);
                cout<<"accept a connection of"<<str<<endl;
                sp_Epoll->add(confd,EPOLLIN);
            }
            else if((sp_Epoll->_events[i].events & EPOLLIN) == EPOLLIN)
            {
                int tempfd = sp_Epoll->_events[i].data.fd;
                if(tempfd < 0)
                {
                    continue;
                }
                char buffer[buffer_len];
                int  n = 0;
                if((n = recv(tempfd,buffer,buffer_len,0)) > 0)
                {
                    cout<< "receive message is"<< buffer<<endl;
                    buffer[n] = '\0';
                }
                else
                {
                    if(errno == ECONNRESET) //TCP connect over time,RST
                    {
                        close(tempfd);
                        sp_Epoll->_events[i].data.fd = -1;
			continue;
                    }
                    else if(errno == EINTR)
                    {
                        cout << "connect problem"<<endl;
                        continue;
                    }
                    else
                    {
                        close(tempfd);
                        sp_Epoll->_events[i].data.fd = -1;
			cout << "the connection is terminated by client"<<endl;
			continue;
                    }
                }

                if(strcmp(buffer,"quit") == 0)
                {
                    close(tempfd);
                    continue;
                }
                sp_Epoll->mod(confd,EPOLLOUT);
            }
            else if((sp_Epoll->_events[i].events & EPOLLOUT) == EPOLLOUT)
            {
                int tempfd = sp_Epoll->_events[i].data.fd;
                send(tempfd,"success recv your message",25,0);
                //sp_Epoll->del(tempfd);
                sp_Epoll->mod(tempfd,EPOLLIN);
            }
        }
    }


    return 0;
}

初看nginx的程式碼,又多又雜,到處都是回撥和巨集,很容易讓人無從下手(看不懂真的不怪你),但是當你牢牢記住事件驅動這個本質時,你就會清晰很多了,整個過程無非就是連線到了,註冊讀寫事件到epoll中,然後等fd就緒了再處理嘛,好好消化一下,下一篇文章我們就要正式開始編寫nginx的helloWorld功能了。