1.Linux网络编程基础API:
字节序分为大端字节序和小端字节序.小端字节序是指低位地址存放在内存的低位地址处,高位字节存储在内存的高地址处.我们的x86主机一般采用小端存储.所以小端字节序也被成为主机字节序.
实现主机字节序与网络字节序的转换:
//long型常用来转换IP地址,短整形常用来转port
#include <netinet/in.h>
unsigned long int htonl (unsigned long int hostlong);
unsigned long int htons (unsigned short int hostshort);
unsigned long int ntohl (unsigned long int netlong);
unsigned long int ntohs (unsigned short int netshort);
IP地址转换函数(将点分十进制表示的IP地址转换为网络字节序表示的IP):
#include <arpa/inet.h>
int inet_pton(int af,const char* src,void* dst); //由字符串->网络字节序
//如:inet_pton(AF_INET,ip,&serv_addr.sin_addr.s_addr);
const char* inet_ntop(int af,const void* src,char* dst,socklen_t cnt);//则与上面的函数相反
创建socket:
#include <sys/types.h>
#include <sys/socket.h>
int socket( int domain, int type, int protocol);//成功返回socket文件描述符
//如:lfd = lfd = socket(AF_INET,SOCK_STREAM,0);
绑定命名socket:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);//将addr所指的socket地址分配给未命名的sockfd文件描述符;
//如: ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//成功返回0;
监听socket:
#include <sys/socket.h>
int listen(int sockfd, int backlog);//成功时返回0,失败返回-1
//如: ret = listen(lfd, 128);
接受连接:
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr*addr, socklen_t*addrlen)//服务器调用成功时返回一个新的socket,该socket唯一的标识这个连接.
//如: connfd =accept (lfd, (struct sockaddr*)&clie_addr, &clie_addr_len)
注意:accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心网络情况的变化.
发起连接:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr*addr,socklen_t addrlen);//客户端调用,成功时返回0;
//如connect(cfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
数据的读写:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
//flag一般设置为0,需要发送带外数据时设置为MSG_OOB.
总结:
服务器端编写步骤:创建套接字->绑定IP与端口(bind)->监听(listen)->等待并接收连接请求(accept)->通信(send/write)->关闭(close);
客户端编写步骤:创建套接字->连接服务端(connect)->通信(recv/read)->关闭(close);
2.端口复用:
端口复用的用途:
(1) 防止服务器重启之前被绑定的端口没有及时的释放;
(2) 程序突然退出而系统没有释放端口,如按了ctrl+c,必须要等待一段时间才可以继续使用,,这一段时间就叫主动等待时长,即为下图的2MSL,且主动关闭连接的一方处于TIME_WAIT状态.
(3) 半关闭状态:
- A给B发送是FIN(A调用了close函数), 但是B没有给A发送FIN(B没有调用close);
- A断开了与B的连接, B没有断开与A的连接;
- FIN_WAIT_2:说明主动关闭的一方处于半关闭状态,该状态下只能接收数据,不能发送.
- FIN_WAIT:主动发起关闭的一方处于这状态,位于半关闭之后的一个状态.主要是为了确保了另一端接收到本方发送的ACK数据,因为有可能在这期间网络发生变化,导致ACK丢失.2MSL这段时间必须等满时间才退出这种状态(MSL为最大报文生成时间),之后调用close;因为对方关闭之后是不会发送数据告诉本方已接受到ACK了.
端口复用设置方法:
int opt = 1;
setsockopt(sockfd, SOL_SOCKET,SO_REUSEADDR,(const void *)&opt, sizeof(opt));//注意:必须在bind之前进行设置.
3.高级IO函数:
管道(实现进程间通信):
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
int pipe( int fd[2]);//创建两个文件描述符fd[0],fd[1]分别构成读管道与写管道.
int socketpair(int domain, int type, int protocol, int fd[2]);
该创建的管道为单向管道,想要实现双向则需使用两个管道或者直接调用socketpair函数.
dup函数和dup2函数:
作用:如果想把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接(CGI).可以借助与dup与dup2来实现复制文件描述符.
#include <unistd.h>
int dup(int file_descriptor); //总是返回系统中最小的可用文件描述符
int dup2(int file_descriptor_one,int file_descriptor_two);
mmap函数和munmap函数:
作用:mmap用于申请一段内存空间,我们可以将这段内存作为进程间通信的共享内存;munmap则释放由mmap创建的内存空间.
#include <sys/mman.h>
void* mmap(void* start, size_t length, int port, int flags, int fd, off_t offset);
//start可用于指定特定的地址作为这段内存的起始位置,设置为NULL则自动分配,length指定内存长度,port用来设置访问权限,flags设置程序段的行为标志
//如:share_mem = (char*)mmap( NULL, USER_LIMIT * BUFFER_SIZE, PROT_READ | PROT_WRITE,MAP_SHARED, shmfd, 0 );
int munmap(void* start,size_t length);
4.高性能服务器框架:
服务器基本框架:
I/O处理单元(接入服务,负载均衡) -> 请求队列(通信方式) -> 逻辑单元(进程线程) -> 请求队列(通信方式) -> 网络存储单元(数据库).
I/O模型
首先需要明确同步与异步,阻塞与非阻塞:
同步和异步是针对应用程序和内核的交互而言的;
同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,
异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知.
阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式.
阻塞方式下读取或者写入函数将一直等待.
非阻塞方式下,读取或者写入函数会立即返回一个状态值.
总共有五中I/O模型:
- 阻塞I/O:无法立即完成操作而被操作系统挂起,直到等待的事件发生为止;阻塞等待好处是不占用cpu宝贵的时间片,但同一个时刻只能处理一个操作,效率低.
- 非阻塞I/O:总是立即返回,不管事件是否已经发生,提高了程序的执行效率,但需要占用更多的cpu和系统资源.
- I/O复用:应用程序通过I/O复用函数向内核注册一组时间,内核通过I/O复用函数把其中就绪的事件通知给应用程序,常用的I/O复用函数有:select,poll和epoll_wait.I/O复用本身是阻塞的,其可以提高效率的原因在于它们具有同时监听多个I/O事件的能力.
- 信号驱动:当目标文件描述符上有事件发生时,SIGIO信号的信号处理函数将被触发.
- 异步I/O:直接由内核执行读写操作并触发读写完成事件,程序没有阻塞阶段.
前四种方法都属于同步I/O模型,其要求用户代码自行执行I/O操作(将数据从内核缓冲区读入用户缓冲区,或将数据从用户缓冲区写入内核缓冲区),而异步I/O则由内核来执行I/O(数据在内核缓冲区和用户缓冲区之间的移动则由"后台"来完成).
两种高效的事件处理模式
同步I/O模型常用于实现Reactor模式,异步I/O模型则用于实现Proactor模型.
参考"[https://blog.csdn.net/caiwenfeng_for_23/article/details/8458299]"
首先来看看Reactor模式,Reactor模式应用于同步I/O的场景,其是一种基于事件驱动的设计模式,即通过回调机制,我们将事件的接口注册到Reactor上,当事件发生之后,就会回调注册的接口.我们以读操作为例来看看Reactor中的具体步骤:
读取操作:
应用程序注册读就绪事件和相关联的事件处理器
事件分离器等待事件的发生
当发生读就绪事件的时候,事件分离器调用第一步注册的事件处理器
事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
因此,Reactor有几个必要的组价:
- Handle(句柄或者文件描述符):该本质上就是一个文件描述符,用于表示一个个事件,是事件产生的发源地.
- 同步事件分离器:本身是一个系统调用,用于等待事件的发生;调用方在调用它时会被阻塞,一直阻塞到同步事件分离器上有事件为止.对于Linux来说,其代表的就是IO多路复用机制,比如poll,select,epoll等.
- 事件处理器:管理已注册事件和调度,分成不同类型的事件(读,写等),当事件发生时,调用相应的回调函数来处理,供我们在特定事件产生时实现相应的回调方法进行业务逻辑的处理.
- Reactor反应器(initiation Dispatcher初始分发器):实际上是Reactor的核心,用于控制事件的调度方式,同时又提供了应用事件处理器的注册,删除等操作.一旦有激活事件时,它会首先分离出事件,然后调用相应的回调函数进行处理,否则则继续通过事件分离器来等待事件的发生.
Reactor模式的优缺点
优点:
1)响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
2)编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
3)可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;
4)可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性.
缺点:
Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用改进版的Reactor模式如Proactor模式.
下面我们来看看Proactor模式中读取操作和写入操作的过程:
读取操作:
应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
事件分离器等待读取操作完成事件
在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作.
主要介绍Reactor多线程模式:主线程只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知给工作线程;除此之外,主线程不做任何其他实质性的工作,只负责监听当前套接字是否可读可写,至于什么事情要处理,交给工作线程来做.
使用同步I/O模型epoll_wait来实现Reactor模式:
(1)主线程往epoll内核事件表中注册socket上的读就绪事件;
(2)主线程调用epoll_wait()等待socket上有数据可读;
(3)当socket上有数据可读,epoll_wait 通知主线程。主线程从socket可读事件放入请求队列;
(4)睡眠在请求队列上的某个可读工作线程被唤醒,从socket上读取数据,处理客户的请求,然后向 epoll内核事件表里注册写的就绪事件;
(5)主线程调用epoll_wait等待socket可写;
(6)当socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列;
(7)睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果.
5.两种高效的并发模式
1.半同步/半异步模式
在并发模式中的同步与异步与I/O模型中的同步异步不同,这里的"同步"指的是程序完全按照代码的序列顺序执行,"异步"指的是程序的执行需要由系统时间来驱动,常见的事件有中断,信号等.对于服务器这种既要求较好的实时性有要求能够同时处理多个客户请求的应用程序,就应该使用半同步/半异步模式来实现.
同步用于处理服务器框架中的客户逻辑单元(进程线程),异步I/O实现I/O处理单元(接入部分).
2.领导者/追随者模式
该种模式是多个工作线程轮流获得事件源的结合,轮流监听,分发并处理事件的一种模式,在任一时间点,程序都仅有一个领导者线程,他负责监听I/O事件,而其他线程则属于追随者,休眠在线程池中等待成为新的领导者.