手把手教你用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功能了。