Server的生命周期
1. 创建
1 | int socket(int domain, int type, int protocol); |
2. 绑定
1 | int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
3. 监听
1 | int listen(int sockfd, int backlog); |
4. 接收
1 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
5. 结束
1 |
|
Client的生命周期
1. 创建
2. 绑定 (一般不会使用)
3. 连接
1 | int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
4. 结束
套接字读操作
读取长度
1 | ssize_t read(int fd, void *buf, size_t count); |
一直读取发送给服务器,如果不限制长度,会导致服务器block在read
1 | tail -f /var/log/system.log | nc localhost 4481 |
EOF事件
服务器读取固定长度,但是如果长度不够的话,只要有EOF,就会停止读取,客户端最简单的方式发EOF就是关闭套接字。
部分读取
只要有数据,就会读取,不等是否达到固定长度或者是否有EOF过来。
套接字写操作
1 | ssize_t write(int fd, const void *buf, size_t count); |
套接字选项字节
TIME_WAIT状态
当你关闭(close)了某个缓冲区,但其中仍有未处理数据的套接字之时就会出现TIME_WAIT状态。前面曾说过,调用write只是保证数据已经进入了缓冲层。当你关闭一个套接字时,它未处理的数据并不会被丢弃。
在幕后,内核使连接保持足够长的打开时间,以便将未处理的数据发送完毕。这就意味着它必须发送数据,然后等待接收方的确认,以免数据需要重传。
如果关闭一个尚有数据未处理的服务器并立刻将同一个地址绑定到另一个套接字上(比如重启服务器),则会引发一个
Errno::EADDRINUSE,除非未处理的数据被丢弃掉。设置
SO_REUSE_ADDR可以绕过这个问题,使你可以绑定到一个处于
TIME_WAIT状态的套接字所使用的地址上。
非阻塞式IO
连接复用
https://www.cnblogs.com/aspirant/p/9166944.html
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。
IO多路复用之select总结
概念
该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下
1 |
|
(1)第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2…maxfdp1-1均将被测试。
因为文件描述符是从0开始的。
(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写
(3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
这个参数有三种可能:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
基本原理
测试代码
客户端向服务器发送信息,服务器接收并原样发送给客户端,客户端显示出接收到的信息。
服务端:
1 |
|
客户端:
1 |
|
IO多路复用之poll总结
概念
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制.
1 |
|
pollfd结构体定义如下:
1 | struct pollfd { |
1 | /* 等待的事件 */ |
- POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。
- POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。
例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。
timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;
timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
返回值和错误代码
成功时,poll()返回结构体中revents域不为0的文件描述符个数;
如果在超时前没有任何事件发生,poll()返回0;
失败时,poll()返回-1,并设置err下列值之一:
1 | EBADF //一个或多个结构体中指定的文件描述符无效。 |
测试代码
客户端向服务器发送信息,服务器接收并原样发送给客户端,客户端显示出接收到的信息。
服务器端程序:
1 |
|
客户端代码:
1 |
|
IO多路复用之epoll总结
概念
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll操作过程需要三个接口,分别如下:
1 |
|
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
1 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event \*event); |
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
1 | EPOLL_CTL_ADD // 注册新的fd到epfd中; |
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
1 | struct epoll_event { |
events可以是以下几个宏的集合:
1 | EPOLLIN // 表示对应的文件描述符可以读(包括对端SOCKET正常关闭); |
1 | int epoll_wait(int epfd, struct epoll_event \* events, int maxevents, int timeout); |
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
工作模式
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
测试程序
服务端:
1 |
|
客户端:
1 |
|
Nagle 算法
默认应用于所有TCP连接的优化,避免发送大量的微型TCP分组问题。可以通过选项字节TCP_NODELAY 告诉服务器不带延迟,即时发送。
消息划分
EOF只有在关闭的时候发送,所以为每条消息都关闭连接,开销大,所以需要消息划分。
使用新行 (HTTP协议)
如果是同一系统,可以使用该方法但是不同系统,会出现不兼容。
使用内容长度
SSL套接字
一个套接字可以升级到SSL,但是一个套接字不能同时运行两个服务,如果需要两个服务,需要开两套接口。
紧急数据
支持将数据推送到对列前端。
send不带参数,同write一样,但是第二个参数MSG_OOB代表外带数据。
发送紧急数据,需要发送方和接受放都配合,接收方,失意难过recv,如果不存在紧急数据,调用recv第二个参数是OOB会失败。
局限
每次只能发送一个字节
实践框架
串行化
优点
简单,没有锁,没有共享状态
缺点
不能并发,阻塞等待
单连接进程
实现原理
fork一个子进程,获得客户端连接的副本。
流程
连接抵达服务器
服务器接受连接
fork一个子进程
服务器返回步骤1,由子进程并行处理连接
优点
简单,并行,不用留心边界,没有锁。
缺点
对子进程个数没有限制
单连接线程
实现原理
accept返回的客户端套接字,传给每个线程,每个线程有自己的实例
优点
由于每个客户端,单独一个线程处理,所以不用担心同步锁问题
缺点
线程数量没有限制
Preforking
连接到达之前先fork进程
处理流程
- 主服务进程创建监听套接字
- fork一大批子进程
- 每个子进程在共享套接字上接受连接accept,然后单独处理
- 主服务器进程随时关注子进程
优点
无需担心负载均衡或者子进程连接同步,内核替我们完成改功能。
并且内核确保,只有一个套接字副本可以接受某个特点的连接。并且限制了进程的数量。
缺点
fork进程较多,消耗内存较多。
线程池
类似于preforking
事件驱动
处理流程
服务器监听套接字,等待套接字接入连接。
将接入的新连接加入到套接字列表进行监视。
服务器监视活动的连接以及监听套接字。
当某个活动连接可读时,服务器从该连接读取一块数据并分派相关回调函数。
当某个活动连接仍然可读时,服务器读取另外一块数据,再次分派给回调函数。
服务器收到另一个新连接时,将其加入到套接字列表进行监听。
服务器注意到第一个连接已经可写了,将响应信息写入该连接。
所以这一切,都在单线程中。
优点
没有线程限制,所以并能力非常高
缺点
所有操作不能阻塞,一个缓慢的客户端,会影响该模式。
混合模式
nginx
preforing+事件驱动