前言:服务器是现代软件不可或缺的一部分,服务器的技术也是一个非常复杂和有趣的方向。随着操作系统的不断发展,服务器的底层架构也在不断变化。本文描述了一个使用C++和多线程实现的简单HTTP服务器。首先,让我们看一下如何创建服务器。intmain(){intserver_fd;结构sockaddr_in服务器地址;server_fd=socket(AF_INET,SOCK_STREAM,0);上=1;setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));if(server_fd<0){perror("创建套接字错误");转到退出;}memset(&server_addr,0,sizeof(server_addr));server_addr.sin_family=AF_INET;server_addr.sin_port=htons(8888);server_addr.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(server_fd,(structsockaddr*)&server_addr,sizeof(server_addr))<0){perror("绑定地址错误");转到退出;}if(listen(server_fd,511)<0){perror("监听端口错误");转到退出;}while(1){intconnfd=accept(server_fd,nullptr,nullptr);if(connfd<0){perror("接受错误");继续;}//处理}close(server_fd);关于转0;退出:退出(1);我们看到,根据操作系统提供的API,创建一个TCP服务器是非常简单的。它只需要调用几个函数。最后,该进程将阻塞在accept中并等待连接到来。我们处于一个死循环中的每个请求的死循环中。显然,这样的效率肯定很低,因为如果我们使用传统的读/写功能,会导致进程阻塞,从而导致多个请求排队等待处理。在此基础上,我们使用多线程来提高效率。std::threadthreads[MAX_THREAD];std::condition_variablecondition_variable;std::deque请求;std::mutexmutex;for(inti=0;i锁(mutex);requests.push_back(connfd);condition_variable.notify_one();我们看到当主线程收到请求时,并没有自己处理,而是加入到请求队列中让子线程去处理,因为子线程会自动阻塞,所以需要唤醒主线程建立一个线程来处理新的请求。接下来我们看一下子线程的逻辑。voidworker(std::mutex*mutex,std::condition_variable*condition_variable,std::deque*requests){intconnfd;while(true){{std::unique_locklock(*mutex);//没有任务则等待,否则取出任务处理while((*requests).size()==0){(*condition_variable).wait(lock);}connfd=(*requests).front();(*请求).pop_front();}字符缓冲区[4096];诠释;while(1){memset(buf,0,sizeof(buf));intbytes=read(connfd,buf,sizeof(buf));如果(字节<=0){关闭(connfd);}else{write(connfd,buf,bytes);}}}}子线程不断从任务队列中取任务,具体是连接字符对应的文件描述,然后不断读取里面的数据,最后返回给客户端。但是这样的功能显然意义不大,所以我们在这个基础上实现了一个HTTP服务,让它可以处理HTTP请求。当然,我们手写一个优秀的HTTP解析器并不容易,所以直接使用开源的。这里我们选择llhttp,它是Node.js使用的HTTP解析器。这里就不一一列举了,只是简单介绍下llhttp的用法。typedefvoid(*p_on_headers_complete)(on_headers_complete_info,parser_callback);typedefvoid(*p_on_body_complete)(on_body_complete_info,parser_callback);typedefvoid(*p_on_body)(on_body_info,parser_callback);structparser_callback{void*data;p_on_headers_completeon_headers_complete;p_on_bodyon_body;p_on_body_completeon_body_complete;};classHTTP_Parser{public:HTTP_Parser(llhttp_typetype,parser_callbackcallbacks={});inton_message_begin(llhttp_t*解析器);inton_status(llhttp_t*parser,constchar*at,size_tlength);inton_url(llhttp_t*parser,constchar*at,size_tlength);inton_header_field(llhttp_t*parser,constchar*at,size_tlength);inton_header_value(llhttp_t*parser,constchar*at,size_tlength);inton_headers_complete(llhttp_t*解析器);inton_body(llhttp_t*parser,constchar*at,size_tlength);inton_message_complete(llhttp_t*解析器);intparse(constchar*data,intlen);诠释完成();voidprint();};HTTP_Parser是我自己实现的HTTPParserWrapper,主要是对llhttp的封装,我们看到HTTP_Parser里面有很多回调钩子,对应着llhttp提供的。另外HTTP_Parser支持调用者传入hooks,由parser_callback定义。当llhttp回调HTTP_Parser时,HTTP_Parser会在合适的时候调用parser_callback中的回调,比如当解析完HTTPHeader时,或者解析完整个消息时。具体的解析过程就是在调用方接收到数据的时候执行parse函数,然后llhttp会不断的调用我们传入的hook。知道了HTTP解析器的一般用法,下面我们来看看如何在项目中使用。parser_callbackcallback={&connfd,[](on_body_complete_infoinfo,parser_callbackcallback){int*connfd=(int*)callback.data;constchar*data="HTTP/1.1200OK\r\nServer:multi-thread-server\r\ncontent-length:11\r\n\r\nhello:world\r\n\r\n";写(*connfd,数据,strlen(数据));关闭(*connfd);},};HTTP_Parser解析器(HTTP_REQUEST,回调);字符缓冲区[4096];诠释;while(1){memset(buf,0,sizeof(buf));整数错误=0;ret=read(connfd,buf,sizeof(buf));parser.parse(buf,ret);}这里只列出关键代码。当我们接收到数据后,通过parser.parse(buf,ret)调用llhttp进行解析,llhttp会不断回调hook函数,当一条消息解析完成后,会执行on_body_complete回调,在这里我们可以响应HTTP请求,比如这里返回200响应报文,然后关闭连接。因为我们可以通过llhttp获取到具体的请求url,所以我们可以对其进行进一步的扩展,根据url进行不同的处理。至此,一个HTTP服务器已经实现。早期的服务器端也是采用这种多进程/多线程的处理方式。现在有了多路复用等技术,很多服务器都是基于事件驱动实现的。但是,从主线程接收请求并分发到子线程处理的思想在一些服务器中仍然存在,比如Node.js,但是在Node.js中是在进程之间传递。本文大致介绍到这里。服务器技术是一个非常复杂和有趣的方向。上层架构也随着操作系统的能力不断变化。本文只是简单的探索和兴趣。具体代码在https://github。com/theanarkh/多线程服务器。下面是架构图。