WebServer服务器项目可能会被问到的问题(一)

相信WebServer服务器是一部分CPP技术栈同学会放到简历上的项目。楼主稍微总结了一下WebService服务器项目可能会被提问的一些问题,希望能对各位同学有所帮助。
第一篇的链接:WebServer服务器项目可能会被问到的问题(一)
第二篇的连接:WebServer服务器项目可能会被问到的问题(二)
第三篇的连接:WebServer服务器项目可能会被问到的问题(三)
希望对大家有所帮助呀~
加油加油!
第一篇包含的内容包括:

  • WebServer知识点概览
  • WebServer的大概工作流程
  • 性能相关的一些问题
  • 大端序与小端序
  • SQL连接池
  • Nginx是怎样实现异步非阻塞的

由于都是个人整理的答案,难免会有缺漏,提前感谢各位大佬评论区指正啊

webserver概览

  • epoll ET模式(边缘触发)
  • mysql(业务部分的内容)
  • 连接池(数据库)
  • 线程池
  • 日志
  • 定时器
  • Reactor模式
  • http
  • 大端序小端序互转 hton
  • 读写缓冲区

大概流程

主线程监听连接

主线程让epoll监听活跃的文件描述符

处理完之后开工作线程

工作线程任务(读写是分开的,不一定是同一个线程搞操作)

读写缓冲区是数据httpconn的,httpconn是一个类,一个用户一个实例。所以直接独立了。

  • read客户端数据(用非阻塞IO)
    • 有个读缓冲,每个连接独享一个读 和写缓冲,有个65K缓冲区 保证能一次读完,读不完就扩容
  • 以下业务逻辑
    • 先去看读缓冲有没有数据
    • 解析HTTP
    • 生成响应数据
    • 封装响应传回去

Reactor没有分离读和业务逻辑,proactor才可以实现封装业务逻辑

性能相关

半同步半反应堆

个人答案:

  • 一种高效的并发模式(半同步/半异步模式)的一种实现方式,另一种并发模式是领导者/追随者模式。
  • 并发模式指的是I/O处理单元和多个逻辑单元之间协调完成任务的方法。
  • 这里的同步和异步与I/O模型中的同步与异步不同:
    • I/O模型中的同步异步:内核向应用程序通知的是就绪事件还是完成事件
      • 并发模型中的同步异步:程序执行顺序是否按照代码顺序执行。中断、信号。
  • 同步线程:按照同步方式运行的线程
    • 优点:逻辑简单
      • 缺点:效率相对较低、实时性较差
  • 异步线程:按照异步方式运行的线程
    • 优点:执行效率高、实时性强
      • 缺点:程序复杂、难以调试、不适合大量并发。
  • 半同步/半反应堆的内容:
    • 异步线程只有一个,由主线程充当。负责监听所有socket伤的事件。如果有新连接请求,主线程接受得到新的连接socket,往epoll内核事件表中竹醋socket上的读写事件。如果连接上有读写事件,主线程将该连接socket插入请求队列。所有工作线程(同步线程)睡眠在请求队列上,当有任务到来,通过竞争的方式获得任务管理权。
      • 上面的方式采用的事件处理模式为Reactor模式。要求工作线程自己从socket傻姑娘读取客户请求和往socket上写入服务器应答。
      • 也可以采用模拟的Proactor事件处理模式。要求主线程完成数据的读写,主线程将应用程序数据、任务类型等信息封装成一个任务对象,然后插入请求队列;工作线程从请求队列中取得任务对象后,直接处理即可,不需要读写操作。
  • 半同步/半反应堆的缺点:
    • 主线程和工作线程共享请求队列。主线程添加任务、工作线程取出任务,都需要对请求队列进行加锁保护,从而耗费cpu时间。
      • 每个工作线程在同一时间只能处理一个客户请求。

线程池如何实现

个人答案:

  • 主线程轮流选取子线程
  • 通过共享队列+互斥量来同步
  • 信号量来通信
  • 同步问题

线程的数目

个人答案:

  • CPU密集型,线程数和CPU数目相同即可
  • I/O密集型,线程数目可以大一点。
    • (线程等待时间/线程CPU时间 + 1)* CPU数目

多进程模型

  • 为每个客户端分配一个进程来处理请求。

  • 服务器的主进程负责监听客户的连接,一旦与客户端连接完成,accept() 函数就会返回一个「已连接 Socket」,这时就通过 fork() 函数创建一个子进程,实际上就把父进程所有相关的东西都复制一份,包括文件描述符、内存地址空间、程序计数器、执行的代码等。

    • 根据返回值来区分是父进程还是子进程,如果返回值是 0,则是子进程;如果返回值是其他的整数,就是父进程。
      • 因为子进程会复制父进程的文件描述符,于是就可以直接使用「已连接 Socket 」和客户端通信了。
      • 子进程不需要关心「监听 Socket」,只需要关心「已连接 Socket」;父进程则相反,将客户服务交给子进程来处理,因此父进程不需要关心「已连接 Socket」,只需要关心「监听 Socket」。
  • 可能出现的问题:

    • 当「子进程」退出时,实际上内核里还会保留该进程的一些信息,也是会占用内存的,如果不做好“回收”工作,就会变成僵尸进程,随着僵尸进程越多,会慢慢耗尽我们的系统资源。
      • 父进程要“善后”好自己的孩子,怎么善后呢?那么有两种方式可以在子进程退出后回收资源,分别是调用 wait()waitpid() 函数。
      • 进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

多线程模型

  • 线程是运行在进程中的一个“逻辑流”,单进程中可以运行多个线程,同进程里的线程可以共享进程的部分资源的,比如文件描述符列表、进程空间、代码、全局数据、堆、共享库等,这些共享资源在上下文切换时是不需要切换,而只需要切换线程的私有数据、寄存器等不共享的数据,因此同一个进程下的线程上下文切换的开销要比进程小得多。
  • 当服务器与客户端 TCP 完成连接后,通过 pthread_create() 函数创建线程,然后将「已连接 Socket」的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。
  • 注意事项
    • 父进程accept后会把socket放入一个队列。然后线程从这个队列中取socket做操作
    • 这个队列是全局的,每个线程都会操作,为了避免多线程竞争,线程在操作这个队列前要加锁。

大端序小端序

网络序是大端字节序

大端字节序是看着一样的,数据低位存在内存大位(高位)

小端字节序看着是反过来的,数据低位存在内存小位(低位)

字节序的单位应该是字节,所以string(char为组织结构)是没有大小端之分的。

但是看int和short是能看出来是大端字节序还是小端字节序,借助union。

有现成的转换函数。BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数: htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。

h   - host 主机,主机字节序
to  - 转换成什么
n   - network  网络字节序
s   - short  unsigned short 
l   - long  unsigned int

sql连接池

实际生产中,数据库连接是一种关键的、有限的、昂贵的资源。怎样清空不活跃的用户是需要解决的问题。数据库连接池初始化后需要创建一定量数据库连接放到连接池中。数据库连接池有最大连接数与最小连接数。

不论数据库连接是否使用,连接池都将一直保持最小连接数的连接。连接池的达到最大数据库连接数量时,再有新的连接则会被加入到等待队列中。

设计思路

容器

对连续内存没有要求,要求头尾插入删除时间复杂度低即可。list容器即符合要求。所以通过list容器存放空闲的连接。

线程锁

对容器进行读取放回操作时,需要加锁保证安全性。

单例模式

由于所有连接需要被统一管理,所以维护一个连接池对象,这个对象采用单例模式实现。

获取释放连接

获取连接

  • 容器有空闲连接,直接拿
  • 容器无空闲连接
    • 未达上限,自己创建
      • 达上限,报错打回等待(这一步是有改进空间的,可以阻塞掉等待。不过也不一定好)

释放连接

  • 放回容器
  • 目前暂无较好的销毁连接策略

销毁对象池

  1. 关闭销毁池中连接
  2. 释放连接池对象(clear操作)
  3. 完成释放

nginx是怎样实现异步非阻塞的

首先Nginx使用的是epoll,边沿触发模式(ET高效的那个),epoll确实是同步的。select/poll/epoll永远都是阻塞的(除非timeout=0),跟socket是否阻塞无关。

要理解nginx的异步,首先要知道nginx的事件驱动框架是如何运作的。

nginx主要分为网络事件和定时器事件,而网络事件又以TCP为主。网络事件主要包含read/write事件,而定时器事件就一个超时事件这个没什么好说的。

nginx的网络事件是不需要创建的,nginx在初始化的时候会预分配出所有的网络read/write事件,每当有新连接到来时,就会把read/write事件跟对应的socket关联起来,然后放入epoll事件队列中。timer事件由模块开发者自行创建然后通过相关的API投入到epoll事件队列中。

然后,nginx会一直(阻塞)等待epoll返回事件通知或者epoll_wait超时,一旦有事件触发,nginx就会调用关联的(read/write)handler处理事件。这里重点来了,开发者必须保证每一个事件handler都不得包含任何阻塞调用。!!这里,万一handler处理不完被阻塞了(例如缓冲区满了需要等待读写,会把这个事件重新加回epoll中等待通知,这里体现了异步)否则,nginx worker的主线程将会因为一个事件阻塞,导致队列里面可能还有一大堆事件不能及时处理,这会严重影响nginx的效率。这也就回答了题主为什么socket不能设置为阻塞的原因,如果socket是阻塞的,那么一个socket的IO事件就会阻塞后续所有的事件处理,CPU就会空转,等在那里没事干了。而在socket非阻塞调用期间,nginx可以继续处理其他的事件。

这也是nginx把一个请求划分为多个阶段处理的原因之一。

以上就是nginx异步非阻塞的实现,它形容的是nginx事件处理流程,而不是对epoll的调用。它有一个很明显的缺点就是:nginx的异步非阻塞完全依赖开发者来保证。倘若有一个很水的开发者开发了一个第三方模块,而这个模块里调用了阻塞的API,或者是高CPU运算操作,那么就会拖累整个nginx进程。

以下是nginx worker的事件循环代码:

作者:CodeTank
链接:https://www.zhihu.com/question/63193746/answer/206682206
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

for ( ;; ) {

    if (ngx_exiting) {
        if (ngx_event_no_timers_left() == NGX_OK) {
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
            ngx_worker_process_exit(cycle);
        }
    }

    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "worker cycle");

    ngx_process_events_and_timers(cycle);

    if (ngx_terminate) {
        ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
        ngx_worker_process_exit(cycle);
    }

    if (ngx_quit) {
        ngx_quit = 0;
        ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
                        "gracefully shutting down");
        ngx_setproctitle("worker process is shutting down");

        if (!ngx_exiting) {
            ngx_exiting = 1;
            ngx_set_shutdown_timer(cycle);
            ngx_close_listening_sockets(cycle);
            ngx_close_idle_connections(cycle);
        }
    }

    if (ngx_reopen) {
        ngx_reopen = 0;
        ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
        ngx_reopen_files(cycle, -1);
    }
}

非常简单,nginx worker初始化之后的整一个生命周期都在这个循环里。ngx_process_events_and_timers 就是事件处理函数。每循环一次都会处理一波事件,或者是epoll_wait超时。

通常一个请求无法在一次 epoll 调度下就完成,比如往下游吐响应的时候,写缓冲区不足,此时就需要将该请求的写事件加回到 epoll,等下次可写时触发事件回调。因此往往一个请求需要多次调度才能完成。

在一个请求的事件加回到 epoll 后,nginx 转而去处理其他的请求。我认为 nginx 的异步体现在这里。

nginx 的所有网络 IO 操作,都使用了非阻塞的套接字,所以 nginx 的非阻塞应该体现在这里

当然,nginx 也有 AIO,这里也体现了异步的过程。

#春招##面经##秋招##项目##阿里巴巴##腾讯##字节跳动#
全部评论
立个flag,本周五前把第二弹给更新了。虽然周五好像就是明天。加油加油!😂
1 回复 分享
发布于 2022-04-21 22:20
1 回复 分享
发布于 2022-04-19 15:46
1 回复 分享
发布于 2022-04-18 02:57
点赞
1 回复 分享
发布于 2022-04-18 00:16
有没有Webserver这个项目的视频啊,求一个
4 回复 分享
发布于 2022-04-19 17:16
面试官:讲的很好,收藏了,下次面试的时候就绕开这些东西问
12 回复 分享
发布于 2022-04-29 14:27
linux 高性能服务器编程的内容吗
5 回复 分享
发布于 2022-04-18 15:25
最近正在写这项目,收藏了
3 回复 分享
发布于 2022-04-19 15:46
牛逼 顶!
2 回复 分享
发布于 2022-04-18 19:53
m
点赞 回复 分享
发布于 2023-08-19 17:16 湖南
m
点赞 回复 分享
发布于 2023-08-06 20:21 江苏
m
点赞 回复 分享
发布于 2023-06-28 15:21 安徽
1
点赞 回复 分享
发布于 2023-05-08 21:30 江西
非常感谢楼主
点赞 回复 分享
发布于 2023-04-27 11:52 台湾
点赞 回复 分享
发布于 2023-03-10 12:55 江苏
一键三连了
点赞 回复 分享
发布于 2022-08-12 17:38
楼主,我之前写过这个项目,不过现在再投java,项目经验里能写这个吗?
点赞 回复 分享
发布于 2022-07-23 00:04
请问这个项目哪里可以学习
点赞 回复 分享
发布于 2022-07-16 19:06
太感谢了楼主了
点赞 回复 分享
发布于 2022-06-25 15:58
牛逼
点赞 回复 分享
发布于 2022-04-23 17:47

相关推荐

刘湘_passion:出国旅游?那就小心你的腰子咯
点赞 评论 收藏
分享
面试官人很好,态度和蔼可亲,没答出来时也会引导你去思考。由于是晚上面的,导致我白天一天都有点紧张,面的时候状态也不是很好,正常可能面试官提问完应该思考几秒再答,而我就像抢答一样一口气把所有会的都说出来,这样就导致逻辑比较混乱,东一句西一句的。首先是自我介绍,先把会的技术大致讲一下,由于我八股背的多所以着重讲了一下,Java,go,jvm,MySQL,Redis,计网,操作系统这些,然后一小部分闲聊,然后先问了一下项目,面试官问我这个项目是否落实之类的,直接坦言说是写的练手的,包括之前也写过IM通讯,外卖之类的。然后面试官就把提问的重点放在了八股上。先问了Java:类加载器(答:3种+自定义类加载器、tomcat、原因+双亲委派+好处)JVM参数(答:xmx,xms,newsize这些,问我是如何设定的,我回答是把内存分一半给堆,再把堆分一半给新生代,这方面确实不太了解)然后问了一下并发相关的:线程池(答:线程池的7个参数(忘了线程工厂和阻塞时间了),3个重要参数,还有线程如何启用,为什么要设计最大线程数之类的,提到Java栈默认分配1MB运行时不可以更改)AQS(答:先讲clh是自旋锁+list,然后是AQS在这个基础上做的两个优化,然后举了一下reentrantlock根据state如何获取资源)CAS(答:使用三个字段,aba问题,然后将通常搭配自旋锁实现,面试官问通常会自旋多少次,这个不太了解,答的100,然后问100次大概多少秒,回答微秒级,然后面试官讲了一下怎么做资源可能没用完,意识到可能还需要进行阻塞操作)然后考虑一下Linux命令(top,ps,如何使用管道符过滤线程和使用Linux启动线程没答出来)然后问Redis:持久化机制(答:三种aof,rdb,混合,aof的三个参数刷盘策略,rdb以快照保存,使用bgsave会使用子线程来保存不会阻塞,而aof虽然会阻塞但是只在写完数据后追加一条命令,不会太影响,然后是他俩的优缺点,还有混合是怎么保存数据的)集群模式(答:三种,主从复制到缺点再到哨兵机制,正常使用三个哨兵互相监督,主节点挂了投票选主哨兵然后选主节点,然后额外讲一下脑裂的问题,主节点进行数据更新然后把命令写入aof来同步从节点,最后cluster集群,如何实现,使用16383个哈希槽(艹答成16384了),先根据哈希码取余,再根据节点数取余决定放在哪个节点上,然后问了一下我会怎么选集群模式,首先是cluster的问题,会让管道操作之类的失效,然后哨兵会导致整个集群结构变得复杂,使用小项目可能会考虑哨兵,大的考虑cluster,然后考了一下cluster如果一个节点挂了怎么办,根据节点数重新取余然后数据转移,面试官说这么转移比较慢,有没有别的办法,我隐约记得使用一个类似环形数组的方式,想不起来了)然后考了一下MySQL的b+树(这方面的知识点太多了,导致我什么都想讲逻辑就比较乱,讲了一下聚簇索引,树的叶子节点对应着一张页16KB,MySQL有一个区的概念,把这些页放在同一个区中,这样叶子节点的双向链表遍历时速度更快,然后b+树的扇出比较大(非常二,说成扇度之类的,面试官以为说的是扇区)这样层数就比较小,一行1kb数据的话3层可以放心2000w数据)其他的暂时想不起来了算法是lru,面试官问要不要提示,我说写个,然后写了10分钟左右,说大概写好了,但是面试官指出了2个小错误,第一个马上就改回来了,第二个一直没看出来(大脑这时候已经停止工作了)反问:问学习建议,说根据实际的项目进行深入,考虑应该怎么做,还问了一下组里面是做Java的吗?面试官说他是做go的,组里什么语言都有,语言影响不大,连忙补充了一句我对go的底层有深入源码的学习)结束。总体感觉答得不太好,没有太体现出深度,细节也不够全面。
下一个更好呗:佬,我投完云智一直没消息,多久约的一面啊
查看14道真题和解析
点赞 评论 收藏
分享
评论
243
1885
分享

创作者周榜

更多
牛客网
牛客企业服务