C语言网络编程

linux异步网络编程

基于epoll的网络编程实践

从整体看,就是将epoll与socket进行了整合,途中红框为epoll相关的内容,黑框为socket。

  • socket先经过socket()创建套接字,这时候要将套接字的fd设置为非阻塞的形式
  • 接着调用bind(),将套接字绑定ip地址与端口。从别的代码看,bind与设置非阻塞顺序可以颠倒
  • 然后进行listen()
  • 以往会在accept处阻塞,这里既然设置了非阻塞,就不能直接调用accept,而是通过epoll的方式去监听。创建epoll的fd,然后通过epoll_ctl将要监听的socketfd加入
  • 接着通过epoll_wait()来多路服用监听
  • 如果有连接发生,就调用accept建立连接,并将建立的连接也放到epoll的fd中监听
  • 如果是数据来到,则调用read()、write()等函数,进行处理。

对于服务端,代码可以这样来:

参考1参考2

这里有个疑问:

虽然是多路服用,但在执行handle时,还是在主线程中,如果handle比较慢,那并发量依旧上不来,这该怎么处理?

  • 阻塞与非阻塞

    阻塞的系统调用是指,当进行系统调用时,除非出错或被信号打断,进程将会一直陷入内核态直到调用完成。

    非阻塞的系统调用是指,无论I/O操作是否成功,调用都会立即返回。

    在linux环境下,所有的I/O系统调用默认都是阻塞的。

  • 同步与异步

    同步与异步指的是I/O数据的复制工作是否同步完成。

    同步既可以是阻塞的,也可以是非阻塞的。

    以系统调用read为例。阻塞的read会一直陷入内核态,直到read返回;

    非阻塞的read在数据未准备好情况下,会直接返回错误,而当有数据时,非阻塞的read同样会一直陷入内核态,直到read完成。这个read就是同步的操作。

Mosquito的方式

主循环

image-20220722160824021

connect与publish

image-20220722161043978

有这个过程看,这里的方式就是通过epoll的多路复用来监听各个tcp连接,然后分别来进行处理的过程。

nginx的实现

nginx代码更宏大一些,既有master与worker进程之别,又有core、event、http等Module方式的组织差异,最后在数据的管理上也堪称典范,通过ngx_cycle_t、ngx_module_t、ngx_connect_t等结构来管理数据。

我们这里不对nginx全局进行分析,只对nginx网络的epoll方式来进行分析。

除了网络部分,数据的组织也需要

image-20220728112800738

整个建立的过程与上边相同,但它的内容更丰富,封装了ngx_connection_t、ngx_listening_t、ngx_event_t等结构体。

请求连接由ngx_event_accept()进行处理,通过accept到后,调用ngx_add_conn()再次放入监听,最后调用connection对应的listening的handle,这个handle指向的是ngx_http_init_connection(),在这里,将新连接的读事件回调handle赋值给了ngx_http_wait_request_handler(),这里也就正式进入了http模块的处理了。

这里来看,ngx在各个worker进程内部,使用的都是多路复用IO的方式处理连接请求。

nginx的做法也回答了最开始的疑问,通过多进程的方式来增加并发。

当然这就涉及到各个进程之间抢新连接的问题了,也就是通过ngx_accept_mutex来加锁解决。

epoll与并发

epoll API

前边用到了epoll的3个函数,这里先对这3个函数进行介绍。

  • epoll_create

    int epoll_create(int size);    
    

    描述:创建一个epoll的句柄(fd)

    size: 告诉内核要监听的fd最大数量,在用完epoll后,需要调用close()关闭,否则会造成fd耗尽。

    返参:一个epoll的fd

  • epoll_ctl

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *events);    
    

    描述:epoll句柄上事件的管理函数,可以注册事件、修改事件、删除事件。

    epfd:由epoll_create生成的epoll句柄。

    op: 要进行的操作,包括:EPOLL_CTL_ADD注册事件、EPOLL_CTL_MOD修改事件、EPOLL_CTL_DEL删除事件

    fd: 需要操作的fd

    events:告诉内核需要监听什么事。这个epoll_event是epoll最有趣地方之一。

    返参:成功返回0, 失败晚会-1.

    来看看epoll_event(忽略顺序):

    struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
    };
    
    typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
    } epoll_data_t;
    

    第一个变量events指明事件类型,可以是以下宏:

    1. EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭
    2. EPOLLOUT:表示对应的文件描述符可以写
    3. EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。这两个后边再说。
    4. ...

    第二个变量data是一个共用体,用fd或ptr。

  • epoll_wait

    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);    
    

    描述:等待事件的产生

    epfd: 明在哪个epoll句柄上等待

    events:从内核等待到的事件集合(数组),也是epoll_event结构,其中data与epoll_ctl中设置的data相同。epoll_event中events用表达类型,data = fd || ptr。fd代表哪个fd产生了事件;ptr指向一个内存地址,其中必须包含fd,还可以包含在该fd上一系列数据,如nginx中ngx_connection_t。

    maxevents:最大的events个数,不等大于epoll_create设置的size

    timeout:超时时间,0立即返回,-1 阻塞。

    返回:等到到events个数。

ET与LT模式

参考

epoll 对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger)。LT 模式是默认模式,LT 模式与 ET 模式的区别如下:

  • LT 模式:只要某个监听中的文件描述符处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该描述符;
  • ET 模式:只有某个监听中的文件描述符从 unreadable 变为 readable 或从 unwritable 变为 writable 时,epoll_wait 才会返回该描述符。

用图来表示如下:

这里需要研究的问题是,既然epoll对Event(这里参照nginx的命名)是单线程的,那为什么不开个线程池来处理?

epoll模式下,需要各种操作都是异步的,不陷入内核,不等待,这样就不会阻塞,这样操作就会足够快。

如果有读写数据库操作,这时候其实都需要是异步的方式才能发挥其作用,同样交给epoll来做,这样才算可以。

参考

NAT

局域网中私有地址到公有地址的转换

私网地址包括:192.168.x.x、10.x.x.x、172.16.x.x - 172.31.x.x

最常见的就是路由器。

公网的路由不会存私网的地址

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×