linux高性能服务器编程
从高到底的协议有:
应用层:ping(使用ICMP)、telnet(使用tcp)、OSPF(使用IP)、DNS(使用UDP)
传输层:TCP、UDP
网络层:ICMP、IP
数据链路层:ARP、RARP
tcpip链接建立断开的状态图:
tcp的头部:
16位的端口号:唯一标志一台主机上的进程
32位的序号:一次tcp通信过程中某一个传输方向上的字节流的每个字节的编号
32位的确认号:用作对另一方发送来的tcp报文段的响应。其值是收到的tcp报文段的序号值+1
4位头部长度:最大表示15,单位是4字节,头部最长是15*4=60字节
6位标志位:
URG标志:表示紧急指针是否有效
ACK标志:表示确认好是否有效。称携带ACK标志的tcp报文段为确认报文
PSH标志:提示接收端应用进程应该立即从tcp接受缓冲区中读走数据
RST标志:表示要求对方重新建立链接。我们称携带RST标志的tcp报文为复位报文段
SYN标志:表示请求建立一个链接。称携带SYN标志的报文段为同步报文段
FIN标志:表示通知对方本端要关闭了(即不再发送数据了,但是可以接受数据),称携带FIN标志的报文为结束报文段
16位窗口大小:是tcp流量控制的一个手段。这里说的窗口,指的是接收通告窗口(RWND)。它告诉对方本端的tcp接受缓冲区还能容纳多少字节数据,这样对方就可以控制发送数据的速度
16位校验和:校验tcp头部和tcp数据
16位紧急指针:是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。即,这个字段是紧急指针相对于当前序号的偏移。
tcp建立链接,三次握手:
客户端请求发起链接:SYN
服务器确认:SYN,ACK
客户端确认:ACK
tcp关闭链接,4次握手:
客户端请求关闭(不再发送数据,但是仍然可以接受数据):FIN
服务器确认(可以继续向客户端发送数据):ACK
进入半关闭状态(shutdown函数提供了半关闭状态的支持)
服务器端关闭(不再发送数据):FIN
客户端确认:ACK
对于链接超时,一般的情况下客户端会自动发起重连,如果重连若干次(一般是5次)仍然失败,那么通知应用程序超时
tcp状态转移总结:
服务器端:
1、服务器通过listen系统调用,进入LISTEN状态
2、服务器受到客户链接请求(同步报文段),旧把该链接放入内核等待队列,并向客户发送携带SYN标志的确认报文,此时该链接进入SYN_RCVD状态。如果此时服务器接收到客户端发来的确认报文,那么该链接就进入ESTABLISHED状态。ESTABLISHED状态是链接双方能够进行双向数据传输的状态。
3、当客户主动关闭链接时(通过close、shutdown发送结束报文),服务器返回确认报文,链接进入CLOSE_WAIT状态。这个状态的含义就是等待服务器进程关闭链接,等服务器发送数据完毕,服务器向客户端发送结束报文(FIN),链接进入LAST_ACK状态,等待客户端对结束报文的最后一次确认。接收到确认之后链接进入CLOSED状态。
客户端:
1、客户端通过connect调用向服务器发起链接请求,发送一个SYN报文,链接进入SYN_SENT状态
2、如果connect链接的目标端口不存在,或者未被监听、或者端口仍然处于TIME_WAIT状态的链接占用,则服务器给客户端发送一个复位报文,connect失败;端口存在,但是超时,connect失败。connect失败,链接进入CLOSED状态。
3、如果connect链接成功,链接进入ESTABLISHED状态(双方开始发送数据)
4、如果客户端汉族动关闭链接,向服务器发送一个FIN报文,链接进入FIN_WAIT_1状态,如果客户端接受到服务器发送过来的FIN确认报文,那么链接进入FIN_WAIT_2状态,当客户端处于FIN_WAIT_2状态时,服务器处于CLOSE_WAIT状态,这个状态就是可能发生半关闭的状态。
5、服务器向客户端发送FIN报文,客户端接受之后向服务器发送确认,链接进入TIME_WAIT状态。此时服务器的状态是LAST_ACK,直到接收到确认。客户端等待2MSL的时间之后进入CLOSED状态。
重点讨论的TIME_WAIT状态:
客户端接受到服务器的FIN报文之后并不直接进入CLOSED状态。而是进入TIME_WAIT状态,等待2MSL的时间,才进入CLOSED状态。MSL的大小是2min。
TIME_WAIT状态存在的原因是:
1、可靠的终止TCP链接:如果客户端确认服务器结束报文(FIN)的报文段丢失,那么服务器就会重发FIN报文,此时客户端要停留在某个状态以处理重复受到的结束报文段(这种处理就是向服务器发送确认报文段)。如果不进入TIME_WAIT而是直接进入CLOSED状态,那么客户端受到重复的FIN报文时,会向服务器发送复位报文段,服务器会认为这是一个错误,这不是它期望的。所以要有TIME_WAIT状态。
2、让迟到的tcp报文段有足够的时间被识别并被丢弃。linux系统上,一个端口不能被打开多次,当一个链接处于TIME_WAIT状态时,我们无法立即使用该端口来建立一个新的链接,这个新的、和原来相似的链接称为原来的链接的化身。新链接可能收到属于旧链接的报文段(例如迟到的数据报文或者重复的FIN报文),这显然不是新链接的的数据,是不应该发生的。
3、另外,因为TCP报文段的最大生存时间是MSL,所以坚持3MSL的时间的TIME_WAIT状态能够确保网络上两个传输方向上尚未被接收到、迟到的报文都已经消失。所以,一个新的链接可以在2MSL之后安全的建立,而绝不会收到属于原来链接的数据。
可以通过socket选项SO_REUSEADDR来强制进程立即使用处于TIME_WAIT状态的端口。
Nagle算法:
它要求tcp链接的通信双方在任意时刻都最多只能发送一个未被确认的tcp报文段,在该报文段的确认未到达之前不能发送其他的tcp报文;另一方面,发送方在等待确认的同时收集本端需要发送的微量数据,并在确认到来时以一个tcp报文将它们全部发出去,这样就能够大大减少网络上微小的tcp报文段的数量。
该算法的另一个优点是其适应性:确认到达越快,数据就发送的越快
带外数据(OOB):
OOB比普通数据有更高的优先级别。一般tcp中使用紧急指针实现紧急数据传送,OOB的大小只有一个字节,如果你写入了多个字节,那么只有最后一个字节被当作带外数据,其他的数据被当作普通数据。
接受到带外数据的一端必须迅速将带外数据取出,否则会被后来的数据覆盖。接收到带外数据的一段会把带外数据取出存放到一个特殊的缓冲区,如果设置了SO_OOBINLINE选项,那么带外数据将会和普通数据一起存放,此时需要通过紧急指针来识别带外数据
拥塞控制:
包含了四个部分:慢启动、拥塞避免、快速重传、快速恢复
一些概念:SWND发送窗口、RWND接收通告窗口、CWND拥塞窗口,实际的SWND是CWND和RWND中较小的一个
慢启动通常和拥塞避免一起使用:
先成倍的增加窗口大小,到达阈值之后,开始慢慢增长(慢启动)
快速重传和快速恢复通常一起使用:
如果发送端连续收到三个重复的确认报文,就认为拥塞发生了,然后启动快速重传(即立即重传丢失的报文段),然后重新设置阈值大小,从阈值开始慢慢增长窗口(即快速恢复)
网络字节顺序:大端字节顺序
主机字节顺序:目前pc很多都是小端字节顺序
主机字节和网络字节的转换函数:
htol——主机转网络long
htos——主机转网络short
ntol——网络转主机long
ntos——网络转主机short
通用地址结构
soakaddr
协议族和地址族中两者的值完全相同,所以两者常混用
还有一个通用的地址结构:sockaddr_storage,不过用的不是很多
三种地址结构(包括地址和端口):
unix本地地址:sockaddr_un
ipv4地址:sockaddr_in
ipv6地址:sockaddr_in6
ipv4地址结构中还包含了ipv4的地址:in_addr
ipv6地址结构还包含了ip6的地址:in6_addr
ip字符串和地址之间的转换:
1、字符串转地址:
inet_addr
inet_aton
inet_pton(建议使用这个函数)
2、地址转字符串:
inet_ntoa(不可重入)
inet_ntop(建议使用这个)
地址信息函数:
getsockname(获取本端socket地址)
getpeername(获取远端socket地址)
socket函数中:domain是PF_INET、PF_INET6、PF_UNIX
type是:SOCK_STREAM、SOCK_DGRAM
protocol是:0
该函数失败返回-1,并设置errno
bind函数:
成功返回0,失败返回-1并设置erron,常见的erron是:EACCES表示没有权限访问该端口;EADDRINUSE表示地址正在被使用
listen函数:
成功返回0,失败返回-1并设置erron值。这个才是真正被动接受客户链接的函数
accpet函数:
失败返回-1,并设置erron值。注意accpet函数只是从监听队列中取出链接,而不论链接处于何种状态,也不关心网络的变化
connect函数:
成功返回0,失败返回-1并设置erron。常见的erron有:ECONNREFUSED,链接被拒绝,一般是端口不存在;ETIMEDOUT,链接超时
shutdown函数:
有3中howto的可选值:SHUT_RD、SHUT_WR、SHUT_RDWR
send和recv函数的flags选项:
flags只对当前send、recv调用有效,如果想一直有效,可以设置套接字选项
recvmsg和sendmsg适用于tcp和udp
recv和send适用于tcp
recvfrom和sendto适用于udp
sockatmark函数判断套接字是否处于带外标记,如果处于表示有带外数据到来
套接选项:
网络信息api:
gethostbyname根据主机名返回主机的完整信息(返回hostent主机信息结构)
gethostbyaddr根据ip返回主机的完整信息(返回hostent主机信息结构体)
getservbyname根据名称获取某个服务的完整信息(返回servent主机信息结构)
getservbyport根据端口号获取某个服务的完整信息(返回servent主机信息结构)
getaddrinfo既能通过主机名获取ip地址又可以通过服务名获取端口号(返回addrinfo结构)
调用之后需要调用freeaddrinfo函数来释放内存
getnameinfo根据socket地址同时获取字符串表示的主机名和服务名
linux的错误码转换成字符串:strerror和gai_strerror
高级io函数
包括:
1、用于创建文件描述符的函数:pipe/socketpair、dup/dup2函数
2、用于读写的函数:redv/writev,sendfile、mmap/munmap、splice和tee函数
3、用于控制io行为和属性的函数:fcntl
pipe函数,用于创建一个管道,函数成功返回0,并得到一对文件描述符;失败返回-1.
父进程从fd0中读取数据,将数据写到fd1中。
sockaetpair函数能够方便的创建双向管道
dup函数用于复制文件描述符,复制得到的文件描述符和原来的文件描述符指向相同的文件,但是文件描述符的值不同
dup2和dup类似,不过它指定将原文件描述符复制为指定的(未被使用)的文件描述符
readv从文件描述符中将数据读取到分散的内存块中,即分散读
writev将多块分散的内存数据一并写到文件描述符中,即集中写
sendfile函数在两个文件描述符之间直接传输数据(完全在内核中操作),效率很高
mmap/mnumap函数用于创建共享内存,将文件映射到内存中,就如同内存数据一样操作文件
splice函数用于两个文件描述符之间移动数据,也是0拷贝操作,效率很高
tee函数在两个管道之间复制数据,0拷贝操作,效率高
fctnl函数对文件描述符提供了各种操作
linux服务器程序规范
一些规范:
1、服务器程序一般以后台进程(也叫守护进程)运行。
2、服务器程序一般有一套日志系统
3、服务器程序一般以某个专门的非root身份运行
4、服务器程序一般是可以配置的
5、服务器程序在启动的时候一般会生成一个pid文件存入/var/run目录中
6、服务器程序一般要考虑系统资源和限制
日志:
主要用到syslog函数或者openlog函数
用户信息:
getuid/setuid获取用户的真实id(运行进程的真实id,例如mbs以root权限运行一个进程,那么mbs是真实用户id、而root是有效用户id)
geteuid/seteuid获取有效用户id
getgid/setgid获取真实足id
getegid/setegid获取有效组id
进程间关系:
进程组:
getpgid获取进程所属的进程组的id
setpgid设置进程所属的进程组的id
一个进程只能设置自己或者其子进程的PGID
会话:
一些有关联的进程组将形成一个会话,可以用下面函数创建会话:
setsid/getsid——该函数不能由进程组的首领进程调用,否则会产生错误。对于非组首领的进程,调用该函数不仅创建新会话,而且有如下效果:
1、该进程称为会话的首领,此时它是会话里的唯一成员
2、新建一个进程组,其PGID就是调用进程的PID,该进程成为该组的首领
3、调用进程将与终端分离
系统资源限制:
setrlimit/getrlimit
改变工作和根目录:
getcwd/chdir函数用于改变工作目录
chroot用于改变进程的根目录
服务器程序后台化:
daemon函数
高性能服务器程序框架
服务器包含下面三个主要模块:
1、io处理单元。4中io模型和两种高效事件处理模式
2、逻辑单元。
3、存储单元。
四种io模型:
1、阻塞io。
2、io复用。select、poll、epoll等都是
3、SIGIO信号。信号触发读写就绪事件。
4、异步io。linux中不存在异步io,window下的完成端口是异步io。
两种高效的事件处理模式:
Reactor(通常是同步io模型)和Proactor(异步io模型,但是可以用同步io模拟该模型)
Reactor框架中用户定义的操作是在实际操作之前调用的。比如你定义了操作是要向一个SOCKET写数据,那么当该SOCKET可以接收数据的时候,你的操作就会被调用;而Proactor框架中用户定义的操作是在实际操作之后调用的。比如你定义了一个操作要显示从SOCKET中读入的数据,那么当读操作完成以后,你的操作才会被调用。
两种高效的并发模式:
如果程序是计算密集型,并发编程没有优势,反而因为任务的切换导致效率降低。如果程序是io密集型,由于io的读写速度比cpu的速度慢几百倍,所以经常会造成程序阻塞,因此并发模型对io密集型程序有很好的效率提高作用
io中的同步异步区分的是内核向应用程序通知是何种io事件、以及由谁来完成io读写
并发中的同步异步是指代码序列的执行顺序
服务器的两种主要的并发编程模式:
1、半同步/半异步模式。——同步用于处理客户逻辑,异步用于处理io事件。其中有一种比较经典的实现——半同步/半反应堆模式,简介如下:
异步线程只有一个,由主线程来充当。它负责监听所有socket上的事件。如果监听的socket上有可读事件发生,即有新的链接请求到来,主线程就接受链接得到新的链接的socket,然后往epoll内核事件表中注册该socket上的读写事件。如果socket上有读写事件发生,主线程就将该socket插入到请求队列中。所有的工作线程都睡眠在请求队列上,当有任务到来时,它们将通过竞争获取任务的接管权,然后进行处理。
2、领导者/追随者模式。——多个工作线程轮流获得事件源集合、轮流监听、分发并处理事件的一种模式。在任意时间点,程序都只有一个领导者线程,它负责监听io事件、其他线程都是追随者。领导者如果检测到io事件,那么从线程池中选出新的领导者线程,然后自己处理io事件,新的领导者则等待新的io事件,原来的领导者处理io事件,二者实现并发。
介绍一种逻辑单元内部的一种高效的编程方法:有限状态机
有的应用层协议头包含数据包类型字段,每种类型可以映射为逻辑单元的一种执行状态,服务器可以根据它来便携相应的处理逻辑。
影响服务器性能的首要因素是系统的硬件资源。除了硬件之外,可以用下面几个方面来提高服务器的性能:
1、池。以空间换时间的做法。内存池、线程池等等
2、避免数据复制。使用sendfile等函数
3、尽量避免上下文切换和锁。
io复用
有下面的集中io复用技术:
1、客户端同时处理多个socket(非阻塞connect)
2、客户端同时处理用户输入和网络链接
3、服务器同时处理监听socket和链接socket(应用得最多的地方)
4、服务器同时处理tcp和udp请求
5、服务器同时监听多个端口。
需要之处的是,io复用虽然能监听多个文件描述符,但它本省是阻塞的,并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能安顺序依次处理其中的每个文件描述符,要实现并发只能通过多进程和多线程技术
linux下实现io复用的系统调用主要是select、poll、epoll
socket上普通数据和带外数据都将使select返回,但是socket处于不同的就绪状态:前者处于可读状态,后者处于异常状态。
poll比select稍微高效
epoll是增强版的poll:
1、epoll是linux特有的io复用函数。它使用一族函数来完成任务。
2、epoll将用户关心的文件描述符上的事件放在内核的一个事件表中,从而无须像select、poll那样每次调用都要重复传入文件描述符集或事件集。epoll需要用一个而外的文件描述符来唯一的标志内核表中的这个事件表。这个文件描述符使用epoll_create来创建
3、用epoll_ctl函数来操作epoll的内核事件表:
int epoll_ctl(intepfd,int op,struct epoll_event* event)
epfd就是epoll文件描述符
op有下面几种操作:
EPOLL_CTL_ADD:往事件表中注册fd上的事件
EPOLL_CTL_MOD:修改fd上的注册事件
EPOLL_CTL_DEL:删除fd上的注册事件
struct epoll_event
{
__uint32_tevents; //epoll事件
epoll_data_tdata; //用户数据(是一个联合体,里面通常存放文件描述符)
};
4、epoll_wait相当于select函数
5、epoll有两个特别的事件类型——EPOLLET和EPOLLONESHOT这对于epoll的高效运作非常关键。
6、epoll有两种模式:LT电平触发模式和ET边沿触发模式。LT是默认的模式,而ET是高效的模式。
对于LT模式,如果程序没有及时处理某一事件,当下次调用epoll_wait函数的时候,该事件依然存在,直到被处理。ET模式则只通知一次。
7、使用ET模式的每一个文件描述符都应该是非阻塞的。
8、即使使用ET模式,一个socket上的某些事件还是有可能被触发多次,这在并发进程中就会存在一个问题。我们期望任一时刻socket都只被一个线程处理,这一点可以使用epoll的EPOLLONSHOT事件来实现。
linux信号
kill函数用于一个进程给另一个进程发送信号
信号处理函数的原型:typedefvoid (*__sighandler_t)(int);
预定义的两种信号处理方式:
SIG_IGN:忽略
SIG_DFL:按照默认方式处理
与网络编程相关的几个信号:
SIGHUP:控制终端挂起
SIGPIPE:往读端被关闭的管道或者socket中写数据
SIGURG:socket紧急数据
SIGALRM:计时器(定时器)
SIGCHLD:子进程状态发生变化
设置信号及其对应的处理函数通过这个函数:sigaction,还有一个结构体:sigaction
设置了进程信号掩码之后,被屏蔽的信号将不能被进程接受。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果取消对被挂起信号的屏蔽,则它能立即被进程接收到。
即使进程多次接收到同一个被挂起的信号,最终的反应结果也只有一次
信号是一种异步事件,信号处理函数和程序主循环是两条不同的执行路线。一种比较好的统一事件源的方式是:把信号的主要处理逻辑放在程序的主循环中,但信号函数被触发的时,它只是简单的通知主循环受到信号,并且把信号值传给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道来将信号“传递”给主循环,主循环里面使用io复用函数来监听管道的读端文件描述符有可读事件。这样信号事件就可以和其他的io事件一起被统一处理了
SIGHUP信号:
当挂起进程的控制终端时,SIGHUP信号被触发。对于没有控制终端的网络后台进程而言,它们通常利用SIGHUP信号来强制服务器重新读取配置文件
SIGPIPE信号:
默认情况下受到SIGPIPE信号如果不进行处理,那么进程会被终止,所以我们应该捕获SIGPIPE信号。往一个读端关闭的管道或者socket中写数据会触发SIGPIPE信号
定时器
linux提供了三种定时方法:
1、socket选项:SO_RECVTOMEO和SO_SNDTIMEO
2、SIGALRM信号(用alarm周期性的触发该信号)
3、io复用系统调用的超时参数
一个高性能的定时器:定时轮、时间堆(按照时间构成小根堆)
libevent框架
1、libevent基于Reactor实现
2、libevent使用流程:
2.1、调用event_init函数创建event_base对象。一个event_base相当于一个Reactor实例
2.2、创建具体的事件处理器,并设置它们从属的Reactor实例。可以使用通用的event_new来创建也可以使用专门的事件处理器函数来创建。event_new返回事件处理器event。
2.3、调用event_add函数,将事件处理器添加到注册事件队列中,并将该事件处理器对应的事件添加到事件多路分发器中
2.4、调用event_base_dispatch函数来执行事件循环
2.5、事件循环结束之后,使用*_free系列函数来释放系统资源。
3、源代码组织结构:
头文件目录:include/event2,其中event.h提供核心函数
通用数据结构目录:compat/sys
sample目录:提供一些实例程序
test目录:提供一些测试代码
WIN32-Code目录:windows平台上的一些专用代码
event.c:实现libevent整体架构
*-internal.h:内部使用的辅助性头文件
minheap-internal.h:实现一个时间堆
signal.c:提供对信号的支持
evmap.c:维护句柄(文件描述符)与事件处理器的映射关系
event_tagging.c:提供了往缓冲区中添加标记数据,以及从缓冲区中读取标记数据的函数
event_iocp.c:提供了对windows的完成端口的实现
buffer*.c:提供了对网络io缓冲的控制,使用SSL协议对应用数据进行保护,以及0拷贝文件传输
evthread*.c:提供多线程支持
listener.c:封装了监听socket的操作,包括监听连接和接受连接
logs.c:日志系统
evutil.c,evutil_rand.c,strlcpy.c,arc4random.c:提供了一些工具函数
epoll_sub.c:未见使用
这四个文件最为重要:
event_internal.h、include/event2/event_struct.h、event.c、evmap.c最重要
Reactor模式实现的libevent(IO框架库)一些概念:
1、句柄。
句柄就是IO框架库要处理的对象,即IO事件、信号和定时事件,统一称为事件源。一个事件源通常和一个句柄绑定在一起。句柄的作用是,当内核检测到就绪事件时,它将通过句柄来通知应用程序这一事件。linux下,IO事件对应的句柄就是文件描述符,信号事件对应的句柄就是信号量。
2、事件多路分发器。
事件的到来是随机的、异步的。程序需要循环的等待并处理事件,这就是一个事件循环。在事件循环中,等待事件一般使用io复用技术实现。IO框架库一般将系统支持的各种IO复用系统调用封装成统一的接口,称为事件多路分发器。事件多路分发器的demultiplex方法是等待事件的核心函数,其内部调用的是select、poll、epoll_wait等函数。此外事件多路分发器还需要实现register_event和remove_event方法,以供调用者往事件多路分发器中添加事件和从事件多路分发器中删除事件。
3、事件处理器和具体事件处理器。
事件处理执行事件对应的业务逻辑。它通常包含一个或多个handle_evnet回调函数,这些回调函数在事件循环中被执行。IO框架库提供的事件处理器通常是一个接口,用户需要自己实现自己的事件处理器,即具体事件处理器。此外,事件处理器一般还提供一个get_handle方法,它返回与该事件处理器相关的句柄。当事件多路分发器检测到有事件发生时,它通过句柄来通知应用程序,因此必须将事件处理和句柄绑定,才能在事件发生时获取正确的事件处理器。
4、Reactor。Reactor是IO框架库的核心,它提供几个主要的方法是:
4.1、handle_events。该方法执行事件循环。它重复如下过程:等待事件,然后依次处理所有就绪事件对应的事件处理器。
4.2、register_handler。该方法调用事件多路分发器的register_event方法来往事件多路分发器中注册一个事件。
4.3、remove_handler。该方法调用事件多路分发器的remove_event方法来删除事件多路分发器中的一个事件。