从0到服务器开发——TinyWebServer(上)

简介: 从0到服务器开发——TinyWebServer

前言:

修改、完整注释、添加功能的项目代码:

https://github.com/white0dew/WebServer

它是个什么项目?——Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器。

  • 使用 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型
  • 使用状态机解析HTTP请求报文,支持解析GET和POST请求
  • 访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件
  • 实现同步/异步日志系统,记录服务器运行状态
  • 经Webbench压力测试可以实现上万的并发连接数据交换

项目原代码:https://github.com/qinguoyi/TinyWebServer

强无敌!这篇文章是我在学习这个项目时所写的笔记。

一、基础知识

要开始这个项目,需要对linux编程、网络编程有一定的了解,这方面书籍推荐《Unix网络编程》和《Linux高性能服务器编程》。

什么是web sever?

Web服务器一般指网站服务器,是指驻留于因特网上某种类型计算机的程序,可以处理浏览器等Web客户端的请求并返回相应响应——可以放置网站文件,让全世界浏览;可以放置数据文件,让全世界下载。目前最主流的三个Web服务器是Apache、 Nginx 、IIS。服务器与客户端的关系如下:


  e721f208653de11cd3d0ceace230b0e1.png

在本项目中,web请求主要是指HTTP协议,有关HTTP协议知识可以参考介绍,HTTP基于TCP/IP,进一步了解请百度。

什么是socket?

客户端与主机之间是如何通信的?——Socket

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),我们以下客户端获取服务端的时间的例子,来理解socket的使用过程:

服务器端代码

// 《unix网络编程》的公共头文件
#include  "unp.h"
#include  <time.h>
int main(int argc, char **argv)
{
  int         listenfd, connfd;
  struct sockaddr_in  servaddr;
  char        buff[MAXLINE];
  time_t        ticks;
    // 创建socket套接字文件描述符
  listenfd = Socket(AF_INET, SOCK_STREAM, 0); 
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family      = AF_INET;
    // 将套接字绑定到所有可用的接口
    // 注htol是主机序转网络字节序,请百度了解
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port        = htons(13); 
    // 绑定该socket和地址
  Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
    // 服务器开始监听这个端口上(创建监听队列)
  Listen(listenfd, LISTENQ);
    // 服务器处理代码
  for ( ; ; ) {
        // 从监听队列中,取出一个客户端连接
    connfd = Accept(listenfd, (SA *) NULL, NULL);
        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        Write(connfd, buff, strlen(buff));
    Close(connfd);
  }
}

客户端程序

// 《unix网络编程》的公共头文件
#include "unp.h"
int main(int argc, char **argv)
{
  int         sockfd, n;
  char        recvline[MAXLINE + 1];
  struct sockaddr_in  servaddr;
  if (argc != 2)
    err_quit("usage: a.out <IPaddress>");
    // 创建客户端socket
  if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    err_sys("socket error");

  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port   = htons(13);  /* daytime server */
  if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    err_quit("inet_pton error for %s", argv[1]);
    //尝试连接对应地址的服务器端口
  if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
    err_sys("connect error");
    // 读取socket中的内容
  while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
    recvline[n] = 0;  /* null terminate */
    if (fputs(recvline, stdout) == EOF)
      err_sys("fputs error");
  }
  if (n < 0)
    err_sys("read error");
  exit(0);
}

TCP服务器与TCP客户端的工作流程见下:

进一步了解socket可以参考

试想,如果有多个客户端都想connect服务器,那么服务器如何对这些客户端进行处理?这就需要介绍一下IO复用。

IO复用是什么?

IO复用指的是在单个进程中通过记录跟踪每一个Socket(I/O流)的状态来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力,参考链接

如上文所说,当多个客户端与服务器连接时,这就涉及如何“同时”给每个客户端提供服务的问题。服务器的基本框架如下:

图中的逻辑单元,就是上例中“写入服务器时间”这一功能。要解决多客户端连接的问题,首先得有一个队列来对这个连接请求进行排序存放,而后需要通过并发多线程的手段对已连接的客户进行应答处理

本项目是利用epollIO复用技术实现对**监听socket(listenfd)连接socket(客户请求连接之后的socket)的同时监听。注意I/O复用虽然可以同时监听多个文件描述符,但是它本身是阻塞的,所以为提高效率,这部分通过线程池来实现并发,为每个就绪的文件描述符分配一个逻辑单元(线程)**来处理。

Unix有五种基本的IO模型

  • 阻塞式IO(守株待兔)
  • 非阻塞式IO(没有就返回,直到有,其实是一种轮询(polling)操作)
  • IO复用(select、poll等,使系统阻塞在select或poll调用上,而不是真正的IO系统调用(如recvfrom),等待select返回可读才调用IO系统,其优势就在于可以等待多个描述符就位)
  • 信号驱动式IO(sigio,即利用信号处理函数来通知数据已完备且不阻塞主进程)
  • 异步IO(posix的aio_系列函数,与信号驱动的区别在于,信号驱动是内核告诉我们何时可以进行IO,而后者是内核通知何时IO操作已完成)

对于到来的IO事件(或是其他的信号/定时事件),又有两种事件处理模式

  • Reactor模式:要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生(可读、可写),若有,则立即通知工作线程,将socket可读可写事件放入请求队列,读写数据、接受新连接及处理客户请求均在工作线程中完成。(需要区别读和写事件)
  • Proactor模式:主线程和内核负责处理读写数据、接受新连接等I/O操作工作线程仅负责业务逻辑(给予相应的返回url),如处理客户请求

通常使用同步I/O模型(如epoll_wait)实现Reactor,使用异步I/O(如aio_read和aio_write)实现Proactor,但是异步IO并不成熟,本项目中使用同步IO模拟proactor模式。有关这一部分的进一步介绍请参考第四章、线程池。


PS:什么是同步I/O,什么是异步I/O呢?

  • 同步(阻塞)I/O:等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。
  • 异步(非阻塞)I/O:当代码执行IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时(内核已经完成数据拷贝),再通知CPU进行处理。(异步操作的潜台词就是你先做,我去忙其他的,你好了再叫我

IO复用需要借助select/poll/epoll,本项目之所以采用epoll,参考问题(Why is epoll faster than select?

对于select和poll来说,所有文件描述符都是在用户态被加入其文件描述符集合的,每次调用都需要将整个集合拷贝到内核态;epoll则将整个文件描述符集合维护在内核态,每次添加文件描述符的时候都需要执行一个系统调用。系统调用的开销是很大的,

  • 而且在有很多短期活跃连接的情况下,epoll可能会慢于select和poll由于这些大量的系统调用开销。
  • select使用线性表描述文件描述符集合,文件描述符有上限;poll使用链表来描述epoll底层通过红黑树来描述,并且维护一个ready list,将事件表中已经就绪的事件添加到这里,在使用epoll_wait调用时,仅观察这个list中有没有数据即可。

select和poll的最大开销来自内核判断是否有文件描述符就绪这一过程:每次执行select或poll调用时,它们会采用遍历的方式,遍历整个文件描述符集合去判断各个文件描述符是否有活动;epoll则不需要去以这种方式检查,当有活动产生时会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的文件描述符放到之前提到的ready list中等待epoll_wait调用后被处理

  • select和poll都只能工作在相对低效的LT模式下,而epoll同时支持LT和ET模式。
  • 综上,当监测的fd数量较小,且各个fd都很活跃的情况下,建议使用select和poll;当监听的fd数量较多,且单位时间仅部分fd活跃的情况下,使用epoll会明显提升性能。

其中提到的LT与ET是什么意思?

  • LT是指电平触发(level trigger),当IO事件就绪时,内核会一直通知,直到该IO事件被处理;
  • ET是指边沿触发(Edge trigger),当IO事件就绪时,内核只会通知一次,如果在这次没有及时处理,该IO事件就丢失了。

什么是多线程?

上文提到了并发多线程,在计算机中程序是作为一个进程存在的,线程是对进程的进一步划分,即在一个进程中可以有多个不同的代码执行路径。相对于进程而言,线程不需要操作系统为其分配资源,因为它的资源就在进程中,并且线程的创建和销毁相比于进程小得多,所以多线程程序效率较高。


但是在服务器项目中,如果频繁地创建/销毁线程也是不可取的,这就引入了线程池技术,即提前创建一批线程,当有任务需要执行时,就从线程池中选一个线程来进行任务的执行,任务执行完毕之后,再将该线程丢进线程池中,以等待后续的任务。

关于这部分的详细介绍可以参考:多线程与并发

二、项目学习

完成了基础知识的了解之后,现在就来进行项目代码的学习,这就有一个问题了,究竟,怎样才算是看懂了一个开源项目?把所有代码都复现一遍?

如果真是复现一遍,性价比太小了。如果这个开源项目是工作需要,或者说就是在它的基础上进行修改,那么对其代码整体进行浏览是必不可少的。但若是只是为了学习这个项目的架构和思想,那么从整体入手,细究某一个功能,再瞄准感兴趣的代码块就可以了。


对于本文的服务器项目,笔者主要是为了学习web服务器的相关知识,不需要全部了解,但是大部分代码都得理清脉络,于是我就采用了这种方式来学习:


代码架构,每一个目录负责什么模块(这个部分可以结合开源项目的文档,可以加快对项目的理解速度)

编译运行,看看有什么功能;

挑某一个功能,细究其代码实现,我就先挑“用户登录注册”功能来进行研究,再考虑其他的功能;

添加功能,如何在现有的框架下增加一个功能?比如上传文件、上传博客等等?添加留言板?

未完…

ok,学习路线规划好了,下面就开始代码学习之旅!

代码架构

用VsCode打开项目,该项目的代码架构如下:

参考文档,该项目的代码框架如下:

编译运行

安装Mysql、创建数据库、修改代码,编译,运行:

 sh ./build.sh 
 ./server
 // 打开浏览器
 localhost:9006

浏览器显示如下:

点击新用户,注册一个账号之后再登录,有一下三个功能:

分别是网页上展示一个图片/视频/微信公众号。

通过阅读代码框架和运行逻辑,先给出一个服务器运行时工作流程图如下:


所有功能我最感兴趣的还是登录注册功能,去看看如何实现的。

从0到服务器开发——TinyWebServer(中):https://developer.aliyun.com/article/1508285

相关文章
|
2月前
|
移动开发 网络协议 NoSQL
不为人知的网络编程(十七):冰山之下,一次网络请求背后的技术秘密
本文将抛弃千篇一律的计网知识理论,从现实的互联网技术实践角度,一步步为你分享一次网络请求背后的技术秘密。
59 0
|
3月前
|
安全 Linux Windows
小试跨平台局域网文件传输工具NitroShare,几点感想
小试跨平台局域网文件传输工具NitroShare,几点感想
|
6月前
|
传感器 运维 监控
局域网监控软件如何选丨这篇文章教会你
监控局域网活动,试试WorkWin(员工行为监控,USB及带宽管理,远程控制,权限分配)和NetFlow Analyzer(流量分析,带宽监控,如Wireshark和PRTG)。还有ManageEngine OpManager(实时监控,报警通知,自动化任务),确保网络安全和效率。来源不详。
87 2
|
7月前
|
Java 数据库连接 API
从0到服务器开发——TinyWebServer(下)
从0到服务器开发——TinyWebServer
173 2
|
7月前
|
缓存 移动开发 网络协议
从0到服务器开发——TinyWebServer(中)
从0到服务器开发——TinyWebServer
266 0
|
编解码 安全 数据安全/隐私保护
大隐隐于市,分享5个不为人知的小众软件
电脑上的各类软件有很多,除了那些常见的大众化软件,还有很多不为人知的小众软件,专注于实用功能,简洁干净、功能强悍。今天分享5个实用的软件,简单实用,效果拉满,堪称工作生活必备!
133 2
|
7月前
|
数据可视化 安全 数据挖掘
搬运5款不为人知的小众软件,简单实用
电脑上的各类软件有很多,除了那些常见的大众化软件,还有很多不为人知的小众软件,专注于实用功能,简洁干净、功能强悍。
82 0
|
存储 C# C语言
居民身份证阅读器产品开发学习心得(再谈标准-软件-协议)
居民身份证阅读器产品开发学习心得(再谈标准-软件-协议)
90 1
|
Oracle Java 关系型数据库
从0开始搭建一台服务器开发环境(上)
这套文章将教您如何一步一步搭建一台服务器,通过这套文章你将搭建的服务,以及配置如下:
880 1
从0开始搭建一台服务器开发环境(上)
|
NoSQL 网络协议 Java
从0开始搭建一台服务器开发环境(中)
4,安装maven 5,安装tomcat 6,安装redis 8,安装mysql 9,安装svn
201 0
从0开始搭建一台服务器开发环境(中)