Linux高性能服务器编程第2章-第8章

IP协议:
IP协议提供的是无状态、无连接、不可靠的服务。
IP分片:可能会发生在发送端也有可能发生在中转路由器上
分片和重组提供足够的信息:(数据报标识、标志、片偏移)
ping程序使用的是ICMP回显和应答报文的头部长度8字节
发送端执行的写操作次数和接收端执行的读操作次数之间没有任何的数量的关系,------------------字节流的概念

TCP协议

四个方面:
(1)TCP的头部信息
(2)TCP状态转移过程:TCP的任意一端都是一个状态机
(3)TCP数据流:交互数据流和成块数据流
(4)TCP数据流的控制:超时重传和拥塞控制

TCP头部选项:
可变长的可选部分,这部分最多包含40字节,因为TCP头部最长60字节(其中包含TCP头部20字节的固定部分)。
选项结构:
在这里插入图片描述
kind:选项类型 0(结束选项) 1(空操作选项) 2(最大报文长度选项) 3(窗口扩大因子选项) 4(选择性确认选项) 5(选择性确认的实际工作的选项) 8(时间戳选项)
length:该选项的长度
info:选项的具体信息

特点:面向连接(一对一)、可靠传输、字节流协议
可靠的:(1)TCP协议采用的是发送应答机制,发送端发送的每一个报文段都必须得到接收方的应答,才认为这个TCP报文段传输成功。(2)TCP采用超时重传机制,发送端在发送出一个TCP报文段之后启动定时器,如果定时时间内未收到应答,它将重发该报文段。(3)因为TCP报文段最终是以IP数据报发送的,而IP数据报到达接收端可能会乱序、重复,所以TCP协议还会还会对接收到的TCP报文段重排、整理,再交付应用层。

TCP的状态转移

TIME_WAIT状态
客户端连接再收到服务器的结束报文段之后,并没有直接进入CLOSED状态,而是转移到TIME_WAIT状态,等待的时间最长是2MSL(报文段最大的生存的时间,2个方向上都不可接收到),才能完全的关闭。
TIME_WAIT状态存在的原因:
(1)可靠的终止TCP连接;
(2)保证让迟来的TCP报文段有足够的时间被识别并丢弃。
TIME_WAIT状态的立即结束:sock选项SO_REUSEADDR

connect系统调用
失败返回的原因:
(1)如果connect连接的目标端口不存在(未被任何进程监听),或者该端口仍然处于TIME_WAIT状态的连接占用,则服务器将会给客户端发送一个复位报文段,connect调用失败。
(2)如果目标端口存在,但是connect在超时时间内未收到服务器的确认报文段,则connect调用失败。
connect系统调用失败立即返回到CLOSED状态,否则调用转移至ESTABLISHED状态。

客户端直接从FIN_WAIT_1状态转移到TIME_WAIT状态(不经过FIN_WAIT_2状态),处于FIN_WAIT_1状态的服务器直接接收到带确认信息的结束报文(而不是先收到确认报文,在收到结束报文段)

长时间停留在FIN_WAIT_2状态可能发生的情况:客户端执行半关闭后,未等服务器关闭就可以强行退出。此时的连接由内核来接管,可以成为孤儿连接。为了避免内核会指定内核接管的孤儿连接的数目、指定孤儿连接在内核生存的时间。

复位报文段:
TCP报文段携带RST标志的报文段,复位报文段,以通知对方关闭连接或重新的建立连接
访问端口不存在
异常终止连接 socket选项的SO_LINGER来发送复位报文段
处理半打开连接:

TCP交互数据流
TCP报文段所携带的应用程序的数据按照长度分为两种:交互数据(实时性比较高)和成块数据(效率比较高)。

交互数据流会存在携带交互数据微小的TCP报文段的数据量一般会有很多,这就会导致拥塞发生。解决方法:Nagle算法:
Nagle算法要求一个TCP连接的通信双方在任意的时刻都最多只能发送一个未被确认 的TCP报文段。在该TCP报文段的确认到达之前不能发送其他的TCP报文段。另一方面,发送方在等待确认的同时收集本段需要发送的微量的数据,并在确认到来时以一个TCP报文段将他们全部发出。这就极大的减少了网络上的微小的TCP报文段的数量。


TCP成块数据流
ftp协议传输大文件:当传输大量大块数据的时候,发送方在收到连续发送的多个TCP报文段,接收方可以一次性的确认所有的这些报文段。

带外数据(紧急指针和紧急指针标志):TCP利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供一种紧急方式。
TCP连接设置了SO_OOBINLINE,则将带外数据将和普通数据一样被TCP模块TCP的接收缓冲区中。
TCP超时重传(重传定时器)
**拥塞控制:**算法:reno算法 vegas算法 cubic算法等
SWND(发送窗口):限定发送端能够连续的发送的TCP的报文段数量。
发送窗口=min(RWND(接收窗口), CWND(拥塞窗口))
慢启动, 拥塞避免, 快重传、快恢复
慢启动:在TCP模块刚开始发送数据时并不知道网络的实际情况,采用的是一种试探的方式平滑的增加CWND的大小。慢启动的门限,当拥塞窗口超过该值的时候,TCP拥塞控制将进入拥塞避免的阶段。拥塞避免算法使得CWND按照线性方式增大从而减缓扩大。两种实现的方式:
(1)每个RTT时间内重新计算新的CWDN,而不论RTT时间内发送端收到了多少的确认。
(2)每收到一个对新的数据的确认报文,就按照一下的公式更新CWND
CWND += SMSS*SMSS/CWND

第4章:TCP/IP通信:访问Internet上的web服务器

HTTP代理服务器的工作原理
在HTTP通信链上,客户端和目标服务器之间通常存在某些中转代理服务器,他们提供的是对目标资源的中转访问。一个HTTP请求可能被多个代理服务器转发,后面的服务器成为前面服务器的上游服务器。代理服务器按照其作用的方式和作用:正向代理服务器、反向代理服务器和透明代理服务器。
正向代理服务器要求客户端自己设置服务器的地址。客户的每次请求都将直接发送到该代理服务器,并由代理服务器来请求目标资源。例如:处于防火墙内的局域网机器要访问Internet,或者要访问一些被屏蔽掉的国外网站,就需要使用正向代理服务器。
反向代理服务器被设置在服务器端,因而客户端无需再进行任何的设置。反向代理是指的是代理服务器来接收Internet上的连接请求,然后将请求转发给内部网络上的服务器,并将内部服务器上得到得结果返回给客户端。
透明代理只能设置在网关上。用户访问Internet得数据报必然要经过网关,如果在网关上设置代理,则该代理对于用户来说是透明得。透明代理可以看作正向代理得一种特殊情况。

基于Reactor模式的IO框架库:
主要分为句柄、事件多路分发器、事件处理器、具体事件处理器、Reactor.

高级IO函数
pipe()
dup() dup2()
readv() writev()
sendfile()
mmap() munmap()
splice()
tee()
fcntl()

Linux系统日志
内核日志由printk等函数打印至内核的环形缓冲区(ring buffer);环形缓冲区的内容直接映射到/proc/kmsg文件中。syslogd则通过读取该文件获取内核日志。
在这里插入图片描述应用进程使用syslog()_与rsyslogd守护进程进行通信。

进程池和线程池
主进程如何选择子进程来为新任务服务,有两种方式:
1.主进程使用某种算法来主动的选择子进程:随机算法和Round Robin(轮流算法)
2.主进程和所有的子进程通过共享的工作队列来同步,子进程都睡眠在该工作队列上。

服务器调制、调试和测试
调试方法:使用tcpdump抓包
gdb调试器
压力测试工具

Linux对应的程序能打开文件描述符数量有两个层次的限制:用户级别的限制(目标用户运行的所有的进程)和系统级别的限制(所有的用户总共能打开的文件描述符数)
ulimit -n
sysctl -w fs.file-max=max-file-number

内核参数:
文件相关 /proc/sys/fs
/proc/sys/fs/file-max 系统级别的文件描述符(临时修改)
/proc/sys/fs/epoll/max_user_watches 一个用户往epoll内核事件表中注册的事件的总量
网络相关 /proc/sys/net
/proc/sys/net/core/somaxconn 指定listen监听队列里,能够建立完整的连接从而进入ESTABLISHED状态的socket的最大的数目
/proc/sys/net/ipv4/tcp_max_syn_backlog 指定listen监听队列里面,能够转移至ESTABLELISHED或者SYN_RCVD状态的socket的最大的数目
/proc/sys/net/ipv4/tcp_wmem, socket的TCP写缓冲区的最小值、默认值、和最大值
/proc/sys/net/ipv4/tcp_rmem, --------------------读----------------------------------
/proc/sys/net/ipv4/tcp_syncookies TCP同步标签

第四章
代理服务器提供的是对目标资源的中转访问
1.代理服务器按照其使用方式和作用:正向代理服务器、反向代理服务器和透明代理服务器。
正向代理服务器要求客户端自己设置代理服务器的地址。客户端的每一次的请求都直接的发送到该代理服务器,并由代理服务器来请求目标资源。比如:防火墙内的局域网机器要访问Internet,或要访问一些屏蔽掉的国外的网站。
反向代理则被设置在服务器端,因而客户端无需进行任何的设置。
透明代理只能设置在网关上。
第五章
socket地址:网络字节序(大字端)和主机字节序(小字端)
socket(int domain, int type, int protocol); 成功返回socket文件描述符 失败返回errno -1;
bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen); 成功返回0, 失败返回-1 errno ,其中常见的是EACCES(被绑定的地址是受保护的,仅仅是超级用户才能够访问)和EADDRINUSE。
listen(int sockfd, int backlog); backlog参数表示监听队列的最大的长度。如果超过客户端将会收到ECONNREFUSED错误信息。
处于完全连接状态的socket的上限。
accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen); accept失败时返回的是-1并设置errno
accept只是从监听队列中取出连接,而不论连接处于何种状态,更不会关系任何的网络的状况的变化。
connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen); 成功返回0失败返回-1 errno常见的ECONNREFUSED(目标端口不存在,连接被拒绝) ETIMEDOUT 连接超时。
close(int fd); close系统调用并非总是立即关闭一个连接,而是将fd的引用计时器减1.当引用的计时器为0的时候,才会真正的关闭。
如果要立即的终止shutdown(int sockfd, int howto);
TCP 的读写:
recv(int sockfd, void* buf, size_t len, int flags); recv的返回值为0,则表示对方已经关闭连接,recv出错则返回-1,并设置errno
send(int sockfd, const void* buf, size_t len, int flags);
flags参数LMSG_OBB:发送和接收带外数据的方法。
可以使用sockatmark(int sockfd); 判断recv中的sockfd是否属于带外标记。

getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len); ---------本端的socket地址
getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len); -----------远端的socket地址

getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len);

level: SOL_SOCKET(通用的socket选项,与协议无关),IPPROTO_IP(IPV4选项),IPPROTO_IPV6(ipv6选项), IPPROTO_TCP(TCP选项)。
option_name: SO_DEBUG(打开调试的信息) SO_ERROR(获取并清除socket错误状态)SO_REUSEADDR(重用本地地址)
SO_KEEPALIVE(发送周期性的保活报文以维持连接) SO_LINGER(延迟关闭) SO_RCVBUF SO_SNDBUF
SO_RCVLOWAT SO_SNDLOWAT 分别表示接收和发送缓冲区的低水位标记(用来判断socket是否可读或 可写,默认情况下接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1)。
成功返回0失败返回-1设置errno。

struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);

struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);

getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
可以通过主机名获得IP地址(内部使用的是gethostbyname函数),可以通过服务名获得端口号(内部使用的是getservbyname函数)。
getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
可以通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)

第六章 高级IO函数
创建文件描述符的函数,包括pipe, dup(CGI服务器)、dup2函数
用于读写数据的函数,包括readv/writev、sendfile、mmap/munmap、splice和tee函数
用于控制IO行为和属性的函数,包括fcntl函数

int pipe(int fd[2]); fd[0]用于读出数据 fd[1]用于写入数据,----------------管道内部传输的是字节流
socketpair(int domain, int type, int protocol, int fd[2]);-----------------创建双向管道

int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);
dup函数重新建立了一个新的文件描述符,新的文件描述符和原有的文件描述符file_descriptor指向相同的文件、管道或是网络连接。总是获得的是系统当前可用的最小的整数值。并不继承原文件描述符的属性。

ssize_t readv(int fd, const struct iovec* vector, int count); //分散读
ssize_t writev(int fd, const struct iovec* vector, int count); //集中写

ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
两个文件描述符之间直接的传递数据(完全在内核操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,称为零拷贝。
in_fd:待写入内容的文件描述符
out_fd:待读出的文件描述符
offet从读入文件流的那个位置开始读。
count 读出的写入之间传输的字节数。
in_fd:必须是一个支持类似mmap函数的文件描述符,即必须是真实的文件,不能是socket和管道。
out_fd:必须是一个socket。

共享内存:
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void* start, size_t length);

splice函数:
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);两个文件描述符之间移动数据,也是零拷贝操作。
fd_in 和 fd_out必须至少有一个是管道文件描述符

tee函数:两个管道文件描述符之间复制数据,零拷贝。
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

fcntl函数:对文件描述符的各种控制操作。
对文件描述符提供控制属性和行为的系统属性是:fcntl ioctl
int fcntl(int fd , int cmd, …)

第7章
(1)Linux服务器程序一般,脱离于控制终端,以后台的进程的形式运行,因而也不会意外的接收到用户的输入。后台进程又称为守护进程。守护进程的父进程是init进程(pid为1的进程)
(2)日志系统
(3)Linux服务器程序一般以某个专门的非root身份运行。比如mysqld,httpd,syslogd等分别拥有自己的运行账户。
Linux提供守护进程来处理系统的日志–syslogd(rsyslogd) (用户进程输出的日志和接收内核日志)。
void syslog(int priority, const char* message, …);
void openlog(const char* ident, int logopt, int facility); //改变syslog的默认输出的方式,进一步结构化数组
int setlogmask(int maskpri); //设置syslog的日志的掩码;
void closelog() //关闭日志功能

进程组:进程的集合,进程组的leader的进程号与进程组的ID相同
pid_t getpgid(pid_t pid);
int setpgid(pid_t pid, pid_t pgid);
会话:一些有关联的进程组形成一个会话。该函数不可以由进程组的首领调用。
由非首领的进程,调用该函数不仅会产生一个会话,而且还会:
(1)调用进程称为会话的首领,此时该进程就是新会话的唯一成员
(2)新建一个进程组,其PGID就是调用进程的PID,调用进程成为该组的新的首领
(3)调用进程将甩开终端。
pid_t setsid(void);
(调用进程成为会话的首领,此时该进程是新会话的唯一成员;新建一个进程组,其PGID就是调用进程的PID,调用进程成为该组的首领;调用进程将会甩开终端)
系统资源限制
物理设备的限制、系统策略限制、以及具体实现的限制
getrlimit(int resource, struct rlimit rlim);
setrlimit(int resource, const struct rlimit
rlim);
ulimit命令可以修改当前shell环境下的资源限制(软限制和硬限制) 也可以通过对配置文件永久改变系统的软硬限制。

改变工作目录和根目录
#include <unistd.h>
char* getcwd(char* buf, size_t size); 内部使用malloc动态分配内存,并将当前的工作目录存储在其中。
int chdir(const char* path);
int chroot(const char* path);

服务器程序后台化:如何创建一个守护进程

bool daemonize()
{
	//(1)创建子进程,关闭父进程,这样就可以使用程序在后台运行
	pid_t pid = fork();
	if(pid < 0)
	{
		return false;
	}else if(pid > 0)
	{
		exit(0);
	}
	//(2)设置文件权限掩码,当进程创建新文件(使用open(const char* pathname, int flag, mode_t mode)系统调用)时,文件的权限是mode & 0777
	umask(0);
	//(3)创建新的会话,设置本进程为进程组的首领
	pid_t sid = setsid();
	if(sid < 0) return false;
	//(4)切换工作目录
	if((chdir("/")) < 0)
	{
		return false;
	}
	//(5)关闭标准输入设备、标准输出设备和标准错误输出设备
	close(STDIN_FILENO);
	close(STDOUT_FILENO);
	close(STDERR_FILENO);
	//(6)关闭其他已经打开的文件描述符
	//(7)将标准输入、标准输出和标准错误输出都定向到/dev/null文件
	open("/dev/null", O_RDONLY);
	open("/dev/null", O_RDWR);
	open("/dev/null", O_RDWR);
	return true;

同样功能的库文件:
int daemon(int nochdir, int noclose);

第8章
服务器架构主要分为三个主要的模块:
(1)IO处理模块(处理客户连接,读写网络数据):4种IO模型和2种高效事件处理模式
(2)逻辑单元(业务进程或线程):2种高效的并发模型,高效的逻辑处理方式(有限状态机)
(3)存储单元(数据库、文件或缓存)。
C/S模型的逻辑:资源相对比较集中,服务器为通信的中心,访问量过大的时候,可以所有客户都将得到很慢的响应。
服务器启动之后,首先创建一个(或多个)监听socket,并调用bind函数经其绑定在服务器感兴趣的端口上,然后调用listen函数等待客户连接,服务器稳定运行之后,客户端就可以调用connect函数向服务器发起来连接。
P2P模型
P2P模型使得每一台机器在消耗服务的同时也可以给别人提供服务,这样资源就能够充分、自由的共享。

四种IO模型
**阻塞IO:**阻塞的文件描述符----系统调用可能因为无法立即完成而被操作系统挂起,直到等待的事件发生为止。
客户端通过connect向服务器发起连接,connect将首先发送同步报文段给服务器,然后等待服务器返回给确认报文段,如果服务器的确认报文段没有立即到达客户端,则connect调用将会被挂起,直到客户端收到确认报文并被唤醒connect调用。
被阻塞的系统调用:accept、send、recv、connect
非阻塞IO:非阻塞的文件描述符-----系统调用总是会返回,而不管事件是否发生。如果没有立即发生,这些系统调用就返回-1,和出错的情况一样的errno,对于accept、send和recv而言,事件未发生时errno通常被设置为EAGAIN(“再来一次”)或EWOULDBLOCK(“期望阻塞”),对于connect而言,errno则被设置为EINPROGRESS(“正在处理中”)

IO通知机制:(1)IO复用select poll epoll (本身是阻塞的) (2)SIGIO信号可以用来报告IO事件

同步IO:(阻塞IO,IO复用、信号驱动IO):IO的读写操作,都是在IO事件发生之后,由应用程序(用户代码)来完成的。
异步IO用户可以直接对IO执行读写操作,这些操作告诉内核用户读写缓冲区的为止。以及IO操作完成之后内核通知应用程序的方式。异步IO的读写操作总是立即返回,无论IO是否阻塞,因为真正的读写操作是内核完成的。

2种事件处理模式–Reactor(同步IO实现)和Proactor(异步实现—模拟同步IO方式)
Reactor:要求主线程(IO处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即通知工作线程,除此之外,主线程不做任何其他实质性的工作。读写数据、接受连接以及处理客户请求均在工作线程中完成。
使用同步IO模型实现的Reactor模式:
(1)主线程往epoll内核事件表中注册socket上的读就绪事件
(2)主线程调用epoll_wait等待socket上有数据可读
(3)当socket上有数据可读时,epoll_wait通知主线程,主线程将socket可读事件放入请求队列
(4)睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件注册表中注册该socket上写就绪事件。
(5)主线程调用epoll_wait等待socket可写
(6)当socket可写的时候,epoll_wait通知主线程,主线程将socket可写事件放入到请求队列,睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。
Proactor:将所有的IO操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。
(1)主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序。
(2)主线程继续处理其他的逻辑
(3)当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可以可用。
(4)应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完成之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成之后如何通知用用程序。
(5)主线程继续处理其他的逻辑
(6)当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序已经发送完毕。
(7)应用程序预先定义好的信号处理函数选择一个工作线程来处理善后处理。
模拟Proactor模式
同步IO模拟Proactor模式的原理:主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一”完成事件“。那从工作线程的角度来看,他们就直接获得了数据读写的结果,接下来就只对读写的结果进行逻辑处理。
(1)主线程往epoll内核事件表中注册socket上的读就绪事件
(2)主线程调用epoll_wait等待socket上的读就绪事件。
(3)当socket上有数据可以读时,epoll_wait通知主线程。主线程从socket循环读取数据,知道没有数据可以读,然后将读到的数据封装为一个请求对象插入到请求队列
(4)睡眠在请求队列上的某个工作线程被唤醒,她将获得请求对象并处理客户请求,然后将epoll内核事件表中注册socket的写就绪事件
(5)当主线程调用epoll_wait等待socket可写
(6)当socket可写时,epoll_wait通知主线程,主线程往socket上写入服务器处理客户请求的结果。

两种高效的并发模式

半同步/半异步模式
这里的同步和异步模式中”同步“和"异步"指的是内核向应用程序通知的何种IO事件(就绪事件还是完成事件),以及由谁来完成IO的读写(是应用程序还是内核)。
选择工作线程的方式:Round Robin(轮流选取工作线程)的算法; 条件变量;信号量来随机选择工作线程。
变体:半同步/半反应堆模式
不同的是:异步线程只有一个,由主线程来充当。它负责监听所有的socket上的事件。
领导者和追随者模式
多个线程轮流的获得事件源集合,轮流监听、分发并处理事件的一种模式。任何时间点,程序仅仅有一个领导者,它负责监听IO事件。而其他则都是追随者,他们休眠在线程池中等待成为新的领导者。当前的领导者线程如果检测到IO事件,首先要从线程池中推选出新的领导者,然后处理IO事件。此时,新的领导者等待新的IO事件,而原来的领导者则处理IO事件,二者实现并发。
缺点:仅仅支持一个事件源

有限状态机
逻辑单元内部的高效的编程方法:有限状态机

//状态独立的有限状态机
STATE_MACHINE(Package _pack)
{
	PackageType _type = _pack.GetType();
	switch(_type)
	{
		case type_A:
			process_package_A(_pack);
			break;
		case type_B:
			process_package_B(_pack);
			break;
	}
}
//带状态转移的有限状态机
STATE_MACHINE()
{
	State cur_State = type_A;
	while(cur_State != type_c)
	{
		Package _pack = getNewPackage();
		switch(cur_State)
		{
			case type_A:
				process_package_state_A(_pack);
				cur_State = type_B;
				break;
			case type_B:
				process_package_state_B(_pack);
				cur_State = type_C;
				break;
		}
	}
}

提高服务器性能的其他建议:
软环境:一方面是指系统的软件资源,比如操作系统允许用户打开的最大的文件描述符的数量;另一方面指的是服务器程序本身。
池、数据复制、上下文的切换和锁:
**池:**这组资源在服务器启动之初就已经被完全的创建好并初始化,被称为静态资源分配。当服务器进入正式运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无需动态的分配。
缺点:池中的资源是预先静态分配的,我们就无法预期应该分配多少资源。
内存池:用于socket的接收缓存和发送缓存。
线程池:实现并发
连接池:服务器或服务器机群的内部永久连接。每个逻辑单元可能都需要频繁的访问本地的某个数据库。简单的做法就是:逻辑单元每次访问数据库的时候,就向数据库发起连接,而访问完之后释放连接。效率比较低,改进:连接池是服务器预先和数据库程序建立的一组连接的集合。当某个逻辑单元需要访问数据库的时候,他可以直接从连接池中取得一个连接的实体并使用。待完成数据库访问之后,逻辑单元再将该连接直接返回给连接池。

**数据复制:**避免发生不必要的数据复制,尤其当数据复制放生在用户代码和内核之间的时候。
上下文切换和锁:。。。

----=================================================================================

阻塞IO (阻塞等待的是【内核数据准备好】和【数据从内核态拷贝到用户态】)
非阻塞IO【最后一次read调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区的这个过程】
同步IO
异步IO:当发起aio_read之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝的过程同样是异步的,内核自动完成,和前面的同步操作不一样,应用程序不需要主动发起拷贝动作。

两种高效的事件处理模式:Reactor(同步IO模型)和Proactor(异步IO模型)
同步IO模型以(epoll_wait为例)实现的Reactor模式的工作的流程:
(1)主线程往epoll内核事件表中注册socket上的读就绪事件;
(2)主线程调用epoll_wait等待socket上的读就绪事件;
(3)当socket有数据可读时,epoll_wait通知主线程,主线程则将socket可读事件放入请求队列
(4)睡眠在请求队列上的工作线程被唤醒,他从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件。
(5)主线程调用epoll_wait等待socket可写
(6)当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。
(7)睡眠在请求队列上的某个工作线程被唤醒,它往socket上写如服务器处理客户请求的结果。

单Reactor单进程:

在这里插入图片描述进程里面有Reactor、Acceptor、Handler这三个对象:
Reactor对象的作用是监听和分发事件;
Acceptor对象的作用是获取连接;
Handler对象的作用是处理业务;
对象里面的是select、accept、read、send是系统调用函数,dispath和业务处理时需要完成的操作,其中dispath是分发事件操作。
具体的方案流程:
Reactor对象通过select(IO多路复用接口)监听事件,收到事件后通过dispathch进行分发,具体分发给Acceptor对象还是Handler对象,还要看接受到的事件的类型。
如果是建立连接的事件,则就交给Acceptor对象进行处理,Acceptor对象进行处理,Acceptor对象会通过accept方法获取连接,并创建一个Handler对象来进行响应;Handler对象通过read->业务处理->send的流程来完成完整的业务流程;
单个Reactor单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程的竞争。
但是存在2个缺点:
(1)因为只有一个进程,无法充分利用多核CPU的性能;
(2)Handler对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理要耗时比较长,那么就要造成响应的时延。
不适用计算机密集型的场景,只适用于业务处理非常快速的场景

单个Reactor多线程、多进程

在这里插入图片描述
1.单个Reactor多线程的方案:redis
与单Reactor单进程方案不一样的是:
Handler对象不再负责业务处理只负责数据的接收和发送,Handler对象通过read读取到数据后,会将数据发给子线程里的Processor对象进行业务逻辑的处理;
子线程里面的Processor对象就进行业务逻辑处理,处理完成之后,将结果发给主线程中的Handler对象,接着由Handler通过send方法将响应的结果发送给client。

2单个Reactor多进程的方案
单Reactor多进程相比于单Reactor多线程实现起来比较麻烦,主要因为要考虑子进程-父进程的双向通信,并且父进程还得知道子进程要将数据发送给那个客户端。
而多线程间可以共享数据,虽然要额外的考虑并发的问题,但是这远比进程间通信的复杂度低的多,因此实际应用中再也看不到单Reactor多进程的模式。

问题:单个Reactor的模式还有个问题,因为一个Reactor对象承担所有事件的监听个响应,而且只要主线程运行,在面对瞬间高并发的场景的时候,容易成为性能的瓶颈的地方。

多Reactor多进程/线程 Netty Memcache

在这里插入图片描述说明如下:
主线程中的MainReactor对象通过select监控连接建立事件,收到事件后通过Acceptor对象中accept获取连接,将新的连接分配给某个子线程。
子线程中的SubReactor对象将MainReactor对象分配的连接加入select继续进行监听,并创建一个Handler用于处理连接的响应事件。
如果有新的事件发生时,SubReactor对象会调用当前连接对应的Handler对象来进行响应。
Handler对象通过read->业务逻辑处理->send的流程来完成完整的业务流程。

综上:Reactor和Proactor的区别:
Reactor是非阻塞同步网络模型,感知的是就绪可读写事件。在每次感知到有事件发生后,就需要应用进程主动调用read方法来完成数据的读取,也就是要应用进程主动将socket接收缓冲中的数据读到应用进程内存中,这个过程是同步的,读取完数据之后应用进程才能处理数据。
Proactor是异步网络模式,感知的是已经完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的等信息,这样系统内核才可以自动的帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不想Reactorr那样还需要应用进程主动的发起read、write来读写数据,操作系统完成读写工作之后,就会通知应用进程直接的处理数据。
因此,Reactor可以理解为【来了事件操作系统通知应用进程,让应用进程来处理】,而Proactor可以理解为【来了事件操作系统来处理,处理完成之后在通知应用进程】。这里的事件指的是新连接、有数据可以读、有数据可以写的这些IO事件里处理包含从驱动读取到内核以及从内核读取到用户空间。
Reactor模式是基于待完成的IO事件,而Proactor模式则是基于已经完成的IO事件

异步IO模型(sio_read和aio_write)实现的Proactor模式的工作流程:
在这里插入图片描述Proactor模式的工作的流程:
Proactor Initiator 负责创建 Proactor 和 Handler对象,并将Proactor和Handler都通过
Asynchronous Operation Processor 注册到内核;Asynchronous Operation Processor负责处理注册请求,并处理IO操作。
Asynchronous Operation Process完成IO操作后通知Proactor; Proactor根据不同的事件类型回调不同的Handler进行业务处理;Handler完成业务逻辑处理;

(1)主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序。
(2)主程序继续处理其他的逻辑
(3)当socket上的数据被读入用户缓冲区后,内核将会向应用程序发送一个信号,以通知应用程序已经可用。
(4)应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲的位置,以及写操作完成时如何通知应用程序。
(5)主线程继续处理其他的逻辑
(6)当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,已通知应用程序数据已经发送完毕。
(7)应用程序预先定义好的信号处理函数选择一个工作线程来做善后的处理。
同步IO模型模拟Proactor模式:
(1)主线程往epoll内核事件表中注册socket上的读就绪事件
(2)主线程调用epoll_wait等待socket上有数据可读
(3)当socket上有数据可读的时候,epoll_wait通知主线程。主线程从socket循环读取数据,直到没有更多的数据可读,然后将读到的数据封装成一个请求对象并插入请求队列。
(4)睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件。
(5)主线程调用epoll_wait等待socket可写。
(6)当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。

select poll epoll_wait是阻塞的可以提高程序的效率的原因在于他们可以同时监听多个IO事件
逻辑单元: 两种有效的并发模式, 高效的逻辑处理单元
并发模式指的是IO处理单元和多个逻辑单元之间协调完成任务的方法。
服务器主要的两种并发编程的模式:半同步/半异步模式 --------领导者/追随者模式

半同步/半异步模式
半同步半反应堆模式采用的事件处理模式是Reactor模式
缺点:主线程和工作线程共享请求队列。主线程往请求队列中添加任务,或者工作线程从请求队列中取出任务,都需要对请求队列,都需要对请求队列加锁保护,从而白白耗费CPU的时间。
每个工作线程在同一时间只能处理一个客户端的请求。如果客户数量较多,而工作线程较少,则请求队列会堆积很多的任务对象,客户端的响应速度将会越来越慢。如果通过增加工作线程来解决这一问题,则工作线程的切换也将耗费大量的CPU的时间。

领导者/追随者模式
缺点:仅支持一个事件源集合。

有限状态机:实现简单的HTTP请求的读取和分析。
主从状态机之间的关系:主状态机在内部调用从状态机。

从状态机

在这里插入图片描述这个状态机的初始的状态是LINE_OK,原始驱动力来自于buffer中新到达的客户数据。在main函数中,我们循环调用recv函数往buffer中读入客户数据。每次成功读取数据之后,我们就调用parse_contenth函数来分析新读入的数据。parse_content函数首先要做的是调用parse_line函数获取一个行。

提高服务器性能的其他的建议:
池、数据复制、上下文切换和锁
:以空间换时间;根据不同的资源类型,池可以分为分多种,内存池、进程池、线程池和连接池。
内存池通常用于socket的接收缓存和发送缓存。
进程池和线程池:并发编程的手段
连接池:服务器或服务器机群的内部永久连接。每一个单元都有可能需要频繁的访问本地的数据库。最简单的做法就是逻辑单元需要访问数据库的时候,就向数据库程序发起连接,而访问完毕之后释放连接。效率比较低。连接池是服务器预先和数据库程序建立的一组连接的集合。当某个逻辑单元需要访问数据库的时候,他可以直接从连接池中取的一个连接的实体并使用之。

数据复制:发生在用户代码和内核之间。内核直接处理,应用程序不关心这些数据。另拷贝函数sendfile来直接的将其发送给客户端。用户代码内部的数据复制也是应该避免的。这时候需要想到共享内存而不是管道或消息队列

上下文切换和锁:
锁可以导致服务器效率低下,如何避免使用锁

一些额外的函数:
char* strpbrk(const char* str1, const char* str2); ----------检索字符串str1中第一个匹配字符串str2中字符的字符,不包含结束字符。
int strcasecmp(const char* s1, const char* s2); -----------判断字符串是否相等(忽略大小写)
size_t strspn(const char* str1, const char* str2); -------------------检索字符串str1中第一个不再字符串str2中出现的字符下标

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值