TCP的连接建立和关闭-27
TCP 面向连接-字节流-可靠传输
TCP连接是全双工的,允许两个方向的数据传输被独立关闭。
如何保持可靠
1.建立连接
2.超时重传
3.确认机制保证数据的成功传输。每个TCP端都有一个序列号和数据完整性校验和。每个段的接收者接收到完好的段时,都会想发送者发会送小的确认分组。延迟确认算法会在一个特定的窗口时间内将输出确认存在缓冲区中,以寻找能够捎带它的输出数据分组。
TCP状态转移-40
TCP状态转移总图
TIME_WAIT状态
客户端收到结束报文之后,进入TIME_WAIT状态,在这个状态,客户端连接要等到2MSL(报文段最大生存时间)的时间,才能完全关闭。
TIME_WAIT状态存在的原因:
1.可靠地终止TCP连接。如果报文段6丢失,客户端需要停留在某个状态以处理重复收到的结束报文段。否则,客户端将以复位报文段来回应服务器,服务器则认为这是一个错误。
2.保证让迟来的TCP报文段有足够的时间被识别并丢弃。如果不存在TIME_WAIT状态,则应用程序能够立即建立一个和刚关闭的连接有同样地址和端口号的连接,新的化身可能还会受到属于原来的连接的,携带应用程序数据的TCP报文段。
TIME_WAIT端口耗尽问题
客户端每次连接到服务器上去时,都会忽的一个新的源端口,以实现连接的唯一性。但由于可用源端口的数量有限,而且在2MSL秒内连接是无法重用的,连接率就被限制在了60000/120=500次/秒。如果再不断进行优化,并且服务器连接率不高于500次/秒,就可确保不遇到TIME_WAIT端口耗尽的问题。
修正方法:增加客户端负载生成机器的数量,或者确保客户端和服务器在循环使用几个虚拟IP地址以增加更多的连接组合。
复位报文段
某些情况下,TCP链家的一端会向另一端发送携带RST标志的报文段,以通知对方关闭连接或重新建立连接。
产生复位报文段的3种情况:
1.访问不存在的端口
2.异常终止连接,TCP提供了异常终止一个连接的方法,即给对方发送一个复位报文段。一旦发送了复位报文段,发送端所有排队等待发送的数据将都被丢弃。
3.处理半打开连接
半打开状态:服务器关闭或异常终止了连接,而对方没有收到结束报文段,此时客户端还维持这原来的连接,及时服务器重启,也没有该连接的任何信息了。这种情况为半连接状态。
TCP超时重传
TCP模块为每个TCP报文段都维护了一个重传定时器,该定时器在TCP报文段第一次被发送是启动。虽然超时会导致TCP报文重传,但TCP报文段的重传可以发生在超时之前,即快速重传。
拥塞控制
拥塞控制的4个部分:
慢启动、拥塞避免、快速重传、快速恢复
拥塞控制概述
拥塞控制的最终受控变量是发送端向网络一次连续写入的数据量(SWND)。发送端要合理地选择SWND大小,如果SWND太小,会引起明显的网络延迟,如果SWND太大,容易网络拥塞。
SMSS:发送者最大端大小,其值一般等于MSS
CWND:拥塞窗口
RWND:接受通告窗口
ssthresh:慢启动门限
慢启动和拥塞避免
TCP连接建立好后,拥塞窗口将被设置成初始值,其大小为2-4个SMSS.
慢启动的理由:TCP模块刚开始发送数据时并不知道网络的实际情况,需要用一种试探的方式平滑地增加拥塞窗口的大小。
慢启动门限:当拥塞窗口的大小超过该值时,TCP拥塞控制将进入拥塞避免阶段。
发送端发生拥塞的依据:
1.传输超时,或者说TCP重传定时器溢出,这种情况使用慢启动和拥塞避免
2.接收到重复的确认报文段。这种情况使用快速重传和快速恢复
快速重传和快速恢复
发送端如果连续收到3个重复的确认报文段,就认为是拥塞发生了,然后启动快重传和快恢复。
快重传和快恢复的过程:
1.当收到第3个重复的确认报文段时,计算慢启动门限 ssthresh = max(FlightSize/2, 2*SMSS) CWMD<=SMSS,其中FlightSize是已经发送但是未收到确认的字节数。然后立即重传丢失的报文段,并设置拥塞窗口
2.每次收到一个重复的确认时,设置拥塞窗口 = 拥塞窗口+MSS,此时发送端可以发送新的TCP报文段(如果新的CWND允许的话)。
3.当收到新数据的确认时,设置CWND=ssthresh
快速重传和快速回复完成之后,拥塞控制将恢复到拥塞避免阶段。
Reactor模式
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上写入服务器处理客户请求的结果。
select系统调用
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, strct timeval* timeout);
nfds参数指定被监听的文件描述符的总数。它通常被置为select监听的所有文件描述符的最大值+1
readfds、writefds、exceptfds分别指向可读、可写和异常事件对于的文件描述符集合。
fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位标记一个文件描述符。
timeout参数用来设置select函数的超时时间。
poll系统调用
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
//fds是pollfd结构类型的数组,定义所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件
struct pollfd{
int fd;//文件描述符
short events;//注册的事件
short revents//实际发生的事件,由内核填充
};
fd成员指定文件描述符;events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或;revents成员则由内核修改
epoll系统调用
内核事件表
epoll把用户关心的文件描述符上的事件放在内核的一张事件表里。epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。
#include <sys/epoll.h>
int epoll_create(int size)
该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
event 参数指定事件,它是epoll_event结构指针类型。
struct epoll_event{
__uint32_t events;//epoll事件
epoll_data_t data;//用户数据
};
epoll_wait函数
epoll系统调用的主要接口是epoll_wait函数。他在一段超时时间内等待一组文件描述符上的事件
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
maxevents表示最多监听多少个事件。epoll_wait函数如果检测到事件,就将所有就绪的时间从内核事件表中复制到它的第二个参数events指向的数组中。只输出epoll_wait检测出的就绪事件,极大地提高了应用程序索引就绪文件描述符的效率。
LT和ET模式
LT模式:当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,这样,当应用程序下一次调用epoll_wait时,epoll_wait还会向应用程序通告此事件,直到事件被处理。
ET模式:当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序立即处理改事件,ET模式很大程度上降低了同一个epoll事件被触发的次数。
在并发的模式下,当一个线程读取完socket上的数据开始处理,在这个过程中socket又有新数据到达,另一个线程被唤醒处理新到达的数据,导致同一个socket被两个线程处理。可以使用EPOLLONESHOT事件避免这种现象。
三种I/O复用函数的比较
select
没有将文件描述符和事件绑定,select只支持3中事件,用户每次调用select都要重置这三个fd_set集合
poll
它把文件描述符和事件绑定,任何事件都被统一处理,用户通过pollfds.events传入用户感兴趣的时间,内核通过修改pollfds.revents反馈其中的就绪事件。由于select和poll调用都返回整个用户注册的时间集合(包含就绪的未就绪的),所有应用程序索引就绪文件描述符的时间复杂度是O(n)。
多进程编程
fork系统调用
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
该函数的每次调用都返回2次,在父进程中返回的是子进程的PID,在子进程中则返回0.
fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和源进程相同,比如堆指针、栈指针和标志寄存器的值。
子进程的代码和父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。复制是采用写时复制,即只有在任一进程对数据执行写操作时,复制才会发生。
处理僵尸进程
子进程结束运行之后,父进程读取器退出状态之前,称子进程处于僵尸态。另一种使子进程进入僵尸态的情况是:父进程结束或异常终止,而子进程继续运行。此时子进程的PPID被操作系统设置为1,即init进程。init进程接管了该子进程,并等待它结束。
在父进程中调用,以等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生,或者使子进程的僵尸态立即结束
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* stat_loc);
pid_t waitpid(pid_t pid, int* stat_loc, int options);
wait阻塞进程,直到该进程的某个子进程结束运行。它返回结束运行的子进程的PID,并将该子进程的退出组昂头信息存储于stat_loc参数指向的内存中。
waitpid只等待pid参数指定的子进程。如果pid取值为-1,那么它就和wait函数相同。
管道
管道是父进程和子进程通信的常用手段。管道能在父、子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[0]和fd[1])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须有一个关闭fd[0],另一个关闭fd[1]。
信号量
共享内存
共享内存是最高效的IPC机制,它不涉及进程间的任何数据传输。然鹅,我们必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。
shmget系统调用创建一段新的共享内存,获取一端已经存在的共享内存
#include<sys/shm.h>
int shnget(key_t key, size_t size, int shmflg);
共享内存被创建/获取后,我们不能立即访问他,而是需要先将它关联到进程的地址空间中,使用完共享内存后,也要将它从进程地址空间分离。
#include<sys/shm.h>
void* shmat(int shm_id, const void* shm_addr, int shmflg);
int shmdt(const void* shm_addr);
shm_id是由shmget调用返回的共享内存标识符。shm_addr参数指定将共享内存关联到进程的哪块地址空间,最红的效果还是受到可选标志位SHM_RND的影响
#include<sys/shm.h>
int shmctl(int shm_id, int command, struct shmid_ds* buf);
shmctl控制共享内存的某些属性。
消息队列
消息队列是在两个进程之间传递二进制块数据的一种简单有效的方式。每个数据库都有一个特定的类型,接收方可以根据类型来有选择地接受数据
#include <sys/msg.h>
int msgget(key_t key, int msgflag);
key参数是一个键值,用来标识一个全局唯一的消息队列。
#include <sys/msg.h>
int msgsnd(int msqid, const void* msg_ptr, size_t msg_sz, int msgflg);
msgid参数是msgget返回的消息队列标识符
msg_ptr参数指向一个准备发送的消息,消息必须被定义为如下类型:
struct msgbuf{
long mtype;//消息类型
char mtext{512};//消息数据
};
mtype成员指定消息的类型,必须是一个正整数。
#include<sys/msg.h>
int msgcrv(int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
msg_ptr用于存储接受的消息,msg_sz是消息数据部分的长度。
#include <sys/msg.h>
int msgctl(int msqid, int command,struct msqid_ds* buf);
command参数指定要执行的命令