黑马程序员,man 7 ip
,高并发服务器、UDP
高并发服务器
多进程并发服务器
使用多进程并发服务器时要考虑以下几点:
父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
系统内创建进程个数(与内存大小相关)
进程创建过多是否降低整体服务性能(进程调度)
server.c
,和每个客户端的数据交互放在子进程中。子进程结束后,捕捉SIGCHLD
信号回收进程资源
1 |
|
多线程并发服务器
在使用线程模型开发服务器时需考虑以下问题:
调整进程内最大文件描述符上限
线程如有共享数据,考虑线程同步
服务于客户端线程退出时,退出处理。(退出值,分离态)
系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
server.c
1 |
|
多路I/O转接服务器
多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
主要使用的方法有三种
select
函数实现poll
函数实现epoll
函数实现
select实现
select能监听的文件描述符个数受限于FD_SETSIZE
,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力
select函数
1 |
|
存在的问题:
- 文件描述符有上限 1024
- 不知道具体对应哪个文件描述符有反馈,需要for(1 —> 1023)或自定义数据结构(数组)
- 监听集合作为传入传出参数,会发生改变。需要保存原集合
使用流程
select()
监听 服务器端的监听套接字listenfd
的 读(有读数据说明有客户端进行连接)- 使用
accept()
建立连接返回交互套接字connfd
,后select()
监听connfd
的读写 select()
返回 需要处理的文件描述符的个数,使用FD_ISSET()
进行判断是哪些文件描述符
server.c
还是小写转大写
1 |
|
pselect函数
pselect原型如下,此模型应用较少。
1 |
|
poll实现
优点:
- 能监听的文件描述符可以突破1024(方法参见epoll)
- 监听、返回集合 分离开
- 搜索范围小
poll函数
1 |
|
如果不再监控某个文件描述符时,可以把pollfd
中,fd设置为-1,poll
不再监控此pollfd,下次返回时,把revents
设置为0
使用示例
server.c
小写转大写
1 |
|
ppoll函数
GNU定义了ppoll(非POSIX标准),可以支持设置信号屏蔽字。
1 |
|
epoll实现
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
目前epell是linux大规模并发网络程序中的热门首选模型。
epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
可以使用cat命令查看一个进程可以打开的socket描述符上限:cat /proc/sys/fs/file-max
如有需要,可以通过修改配置文件的方式修改该上限值。
1 | sudo vi /etc/security/limits.conf |
基础API
epoll_create
函数epoll_ctl
函数epoll_wait
函数
1)创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。
1 |
|
2)控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
1 |
|
3)等待所监控文件描述符上有事件的产生,类似于select()
调用
1 |
|
使用示例
server.c
小写转大写
1 |
|
epoll进阶
事件模型
EPOLL
事件有两种模型:
Edge Triggered (ET)
边缘触发只有数据到来才触发,不管缓存区中是否还有数据。Level Triggered (LT)
水平触发只要有数据都会触发。
思考如下步骤:
假定我们已经把一个用来从管道中读取数据的文件描述符(RFD)添加到epoll描述符。
管道的另一端写入了2KB的数据
调用epoll_wait,并且它会返回RFD,说明它已经准备好读取操作
读取1KB的数据
调用epoll_wait……
在这个过程中,有两种工作模式:ET模式 和 LT模式
ET模式
ET模式即Edge Triggered
工作模式。
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET
标志,那么在第5步调用epoll_wait之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
1) 基于非阻塞文件句柄
2) 只有当read或者write返回EAGAIN
(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
LT模式
LT模式即Level Triggered
工作模式。
与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。
LT(level triggered):LT是缺省的工作方式,并且同时支持block
和no-block socket
。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll
都是这种模型的代表。
ET(edge-triggered):ET是高速工作方式,只支持no-block socket
。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
管道示例
- 创建一个管道,子进程写数据,父进程读数据
- 子进程每次写10B大小的数据,再睡眠10s
- 父进程使用epoll监听管道的读端,每次读5B大小的数据
1 |
|
ET边沿触发模式:子进程写一次aaaa\nbbbb\n
, 父进程读一次aaaa\n
;……
LT水平触发模式: 子进程写一次aaaa\nbbbb\n
, 父进程读一次aaaa\n
,父进程再读一次bbbb\n
;……
C/S示例
和管道示例类似,子进程换成了客户端,父进程换成了服务器
server.c
1 |
|
client.c
1 |
|
ET边沿触发模式:
LT水平触发模式:
非阻塞读示例
基于网络C/S非阻塞模型的epoll ET触发模式。服务器对套接字非阻塞读,但要加上while循环
1 |
|
反应堆模型
libevent
核心思想实现
1 | /* |
线程池
预先创建阻塞于accept多线程,使用互斥锁上锁保护accept
预先创建多线程,由主线程调用accept
threadpool.h
1 |
|
threadpool.c
1 |
|
UDP服务器
传输层主要应用的协议模型有两种,一种是TCP协议,另外一种则是UDP协议。TCP协议在网络通信中占主导地位,绝大多数的网络通信借助TCP协议完成数据传输。但UDP也是网络通信中不可或缺的重要通信手段。
相较于TCP而言,UDP通信的形式更像是发短信。不需要在数据传输之前建立、维护连接。只专心获取数据就好。省去了三次握手的过程,通信速度可以大大提高,但与之伴随的通信的稳定性和正确率便得不到保证。因此,我们称UDP为“无连接的不可靠报文传递”。
那么与我们熟知的TCP相比,UDP有哪些优点和不足呢?由于无需创建连接,所以UDP开销较小,数据传输速度快,实时性较强。多用于对实时性要求较高的通信场合,如视频会议、电话会议等。但随之也伴随着数据传输不可靠,传输数据的正确率、传输顺序和流量都得不到控制和保证。所以,通常情况下,使用UDP协议进行数据传输,为保证数据的正确性,我们需要在应用层添加辅助校验协议来弥补UDP的不足,以达到数据可靠传输的目的。
与TCP类似的,UDP也有可能出现缓冲区被填满后,再接收数据时丢包的现象。由于它没有TCP滑动窗口的机制,通常采用如下两种方法解决:
1) 服务器应用层设计流量控制,控制发送数据速度。
2) 借助setsockopt函数改变接收缓冲区大小。如:
1 |
|
C/S模型
由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,保证通讯可靠性的机制需要在应用层实现。
编译运行server,在两个终端里各开一个client与server交互,看看server是否具有并发服务的能力。用Ctrl+C关闭server,然后再运行server,看此时client还能否和server联系上。和前面TCP程序的运行结果相比较,体会无连接的含义。
server
1 |
|
client
1 |
|
广播
要设置广播权限
1 | int flag = 1; |
使用ifconfig
可以查看广播地址
server
1 |
|
client
1 |
|
多播/组播
组播组可以是永久的也可以是临时的。组播组地址中,有一部分由官方分配的,称为永久组播组。永久组播组保持不变的是它的ip地址,组中的成员构成可以发生变化。永久组播组中成员的数量都可以是任意的,甚至可以为零。那些没有保留下来供永久组播组使用的ip组播地址,可以被临时组播组利用。
224.0.0.0~224.0.0.255
为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;
224.0.1.0~224.0.1.255
是公用组播地址,可以用于Internet;欲使用需申请。
224.0.2.0~238.255.255.255
为用户可用的组播地址(临时组地址),全网范围内有效;
239.0.0.0~239.255.255.255
为本地管理组播地址,仅在特定的本地范围内有效。
可使用ip address
或ip ad
命令查看网卡编号,如:
if_nametoindex
函数可以根据网卡名,获取网卡序号。
获取组播权限
1 |
|
加入多播组
1 | setsockopt(connfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)); //加入多播组 |
server
1 |
|
client
1 |
|
本地套接字
socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。
UNIX Domain Socket是全双工的,API接口语义丰富,相比其它IPC机制有明显的优越性,目前已成为使用最广泛的IPC机制,比如X Window服务器和GUI程序之间就是通过UNIX Domain Socket通讯的。
使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un
表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
对比网络套接字地址结构和本地套接字地址结构:
1 | struct sockaddr_in { |
以下程序将UNIX Domain socket绑定到一个地址。
1 | size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path); |
server
1 |
|
client
1 |
|