Linux网络编程

参考资料在前

Linux C++网络编程
关于 TCP 三次握手和四次挥手,满分回答在此
面试官:说说TCP第三次握手丢失会发生什么?
TCP协议规定2MSL等待的原因
一篇文章让你真正搞懂epoll机制

1. 前置知识

  1. 查看linux系统的处理器核心数grep -c processor /proc/cpuinfo
  2. 为了服务器更加高效,避免频繁的进程切换,我们将worker进程数设置为处理器核心数
  3. 每开启一个终端,都会对应一个bash进程
    • 一个终端对应一个bash进程,终端上开启的进程为bash通过fork开启子进程
    • 每个进程还属于一个进程组,每个进程组有一个唯一的进程组编号pgrp
    • session会话:包含一个或多个进程组,session编号SID
    • 终端关闭,系统会给session的主线程发送SIGHUP信号,主线程会把信号发送个session中的所有进程,进程收到信号默认执行退出操作
    zyq@zyq-ThinkStation-P348:/usr/local/nginx$ ps -ef | grep bash
    zyq         2673    2630  0 16:50 pts/0    00:00:00 bash
    zyq         3101    2630  0 16:51 pts/2    00:00:00 bash
    zyq        25978    3101  0 21:16 pts/2    00:00:00 grep --color=auto bash
    
  4. 终端关闭进程不退出的方法
  • nginx进程拦截SIGHUP信号signal(SIGHUP, SIG_IGN),收到该信号不退出(nginx的父进程变为bash的父进程)
  • nginx进程与bash进程不放在同一个session里, setsid();自己成为新session的组长,为session的组长则无效
  • setsid ./nginx启动进程会启用新的session
  • nohup ./nginx启动进程会忽略SIGHUP信号,该命令会默认把输出重定向到当前目录的nohup.out。
  • 后台执行 ./nginx &,仍然可以执行其他命令,但是信息会输出到当前终端,fg切换到前台运行
  1. strace工具跟踪进程
  • 可以跟踪程序执行时进程的系统调用以及所收到的信号
  • 跟踪nginx进程,sudo strace -e trace=signal -p 20268
    zyq@zyq-ThinkStation-P348:~$ ps -eo pid,ppid,sid,tty,pgrp,comm | grep -E 'bash|PID|nginx'
    	PID    PPID     SID TT          PGRP COMMAND
    2673    2630    2673 pts/0       2673 bash
    23838   12633   23838 pts/4      23838 bash
    31380   23838   23838 pts/4      31380 nginx
    
    zyq@zyq-ThinkStation-P348:~/av/cpp2024/epoll_server$ sudo strace -e trace=signal -p 31380
    strace: Process 31380 attached
    --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=23838, si_uid=1000} ---
    +++ killed by SIGHUP +++
    
    
    zyq@zyq-ThinkStation-P348:~/av/cpp2024/epoll_server$ sudo strace -e trace=signal -p 23838
    [sudo] password for zyq: 
    strace: Process 23838 attached
    --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=12633, si_uid=1000} ---
    rt_sigreturn({mask=[CHLD]})             = -1 EINTR (Interrupted system call)
    kill(-31380, SIGHUP)                    = 0 # 发送SIGHUP信号给31380所在的进程组(杀死了nginx进程)
    rt_sigprocmask(SIG_BLOCK, [CHLD TSTP TTIN TTOU], [CHLD], 8) = 0
    rt_sigprocmask(SIG_SETMASK, [CHLD], NULL, 8) = 0
    rt_sigprocmask(SIG_BLOCK, [CHLD], [CHLD], 8) = 0
    rt_sigprocmask(SIG_SETMASK, [CHLD], NULL, 8) = 0
    rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
    --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=31380, si_uid=1000, si_status=SIGHUP, si_utime=0, si_stime=0} ---
    rt_sigreturn({mask=[]})                 = 0
    rt_sigaction(SIGHUP, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7dff0f442520}, {sa_handler=0x631f91d08530, sa_mask=[HUP INT ILL TRAP ABRT BUS FPE USR1 SEGV USR2 PIPE ALRM TERM XCPU XFSZ VTALRM SYS], sa_flags=SA_RESTORER, sa_restorer=0x7dff0f442520}, 8) = 0
    kill(23838, SIGHUP)                     = 0 # 杀死自己
    --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=23838, si_uid=1000} ---
    +++ killed by SIGHUP +++
    
  1. 内存泄漏检查工具valgrind
  • 安装工具sudo apt install valgrind
  • 参数说明
    • 使用内存泄漏检查工具--tool=memcheck
    • 完全检查内存泄漏 --leak-check=full
    • 显示内存泄漏的位置--show-reachable=yes
    • 是否跟入子进程--trace-children=yes
    • 指定信息输出文件,默认是当前终端--log-file=log.txt
  • 使用示例,检查nginx程序执行过程中的内存泄露问题valgrind --tool=memcheck --leak-check=full --show-reachable=yes ./nginx

2. 进程概述

2.1 fork()函数

  1. 基本概念
  • 进程是程序执行的实例,多个进程可以共享同一个可执行程序
  • fork()函数创建子进程,子进程和父进程从fork()返回,开始执行与父进程相同的代码,根据返回值判断父(1)子(0)进程
  • kill -9 子进程,父进程收到了SIGCHLD信号,子进程变成了僵尸进程
  • fork()出来的子进程与父进程共享内存,直到出现任何进程执行写操作时,才会复制新的内存给子进程,因此fork()很快
  1. 僵尸进程的产生
  • 子进程结束,父进程没有调用wait/waitpid来进行额外的处理,子进程就会变成一个僵尸进程
  • 父进程可能还需要该子进程的一些信息,内核保留子进程的信息
  1. 如何处理僵尸进程
  • 杀死父进程
  • 一个进程被杀死时,会给父进程发送SIGCHLD信号,需要自定义SIGCHLD处理函数,调用waitpid回收子进程
include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/wait.h>

void do_sign(int sign) {
	int status;
	pid_t pid;
	printf("收到了SIGCHLD,当前进程ID: %d!\n", getpid());

	// -1 表示等待所有子进程
	// status 保存子进程的状态信息
	// WNOHANG 表示不要阻塞,让waitpid立即返回
	pid = waitpid(-1, &status, WNOHANG);

	// pid == 0 子进程没结束
	// pid == -1 waitpid调用错误

	return;
}

int main(int argc, char *const *argv) {
	pid_t pid;
	int count = 0;
	printf("当前进程ID: %d!\n", getpid());
	
	// 处理子进程被杀死变成僵尸进程的问题
	if (signal(SIGCHLD, do_sign) == SIG_ERR) {
		printf("设置SIGCHLD信号自定义处理逻辑失败!\n");
		exit(1);
	}
	

	pid = fork();
	if (pid < 0) {
		printf("Fail to fork()!\n");
		exit(1);
	}

	// pid == 1 父进程
	// pid == 0 子进程
	// 父子进程共同执行的代码,不确定哪个进程先执行

	while (1) {
		sleep(2);
		printf("%d 休息了2秒,当前进程ID: %d!\n", count++, getpid());
	} 

	printf("再见\n");
	return 0;

2.2 守护进程

  1. 基本概念
  • 在后台运行,不跟任何的控制终端关联,没有控制终端
  • 一般生存周期长,随着操作系统启动(PPID == 0)
  • cmd列带着[],为内核守护进程
  • init进程,系统守护进程
  • 一般拥有root权限
  1. 守护进程编写规则
  • fork()子进程,父进程退出
  • 子进程调用setsid()建立新会话,脱离终端和父进程(否则crtl + c/终端关闭时父进程退出前会关闭子进程)
  • umask(0) 标识不要限制或屏蔽文件权限
  • 将子进程的标准输入输出重定向到空设备/dev/null,不与终端挂钩,不接收键盘输入,也不输出到屏幕
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *const *argv) {
    pid_t pid;
    int fd;


    // fork()子进程
    pid = fork();
    if (pid < 0) {
        printf("Fail to fork()!\n");
        exit(1);
    }
    if (pid > 0) {
        // 父进程退出
        exit(0);
    }


    // 子进程才会执行 pid == 0
    // 子进程调用setsid()建立新会话
    if (setsid() == -1) {
        return -1;
    }
    // `umask(0)` 标识不要限制或屏蔽文件权限
    umask(0);
    // 打开空设备
    fd = open("/dev/null", O_RDWR);
    if (fd == -1) {
        return -1;
    }

    // 标准输入 重定向到 空设备
    if (dup2(fd, STDIN_FILENO) == -1) {
        return -1;
    }

    // 标准输出 重定向到 空设备
    if (dup2(fd, STDOUT_FILENO) == -1) {
        return -1;
    }

    // fd正常是大于3
    if (fd > STDERR_FILENO) {
        if (close(fd) == -1) {
            return -1;
        }
    }


    while (1)
    {
        sleep(2);
    }
    

    return 0;
}
  1. 文件描述符
  • 打开或创建一个新文件时,操作系统都会返回一个文件描述符,三个特殊的文件描述符号,数字分别为0,1,2

    • 0: 标准输入,对应的符号常量为STDIN_FILENO
    • 1: 标准输出,对应的符号常量为STDOUT_FILENO
    • 2: 标准错误,对应的符号常量为STDERR_FILENO
  • 输出重定向,在命令行中用>, ls -la > out.txt

  • 输入重定向,在命令行中用<, cat < in.txt

  • 联合使用,cat < in.txt > out.txt

  • 空设备 /dev/null是一个空设备,与黑洞类似,丢弃一切输入

  1. 守护进程信号
  • SIGHUP 守护进程不会收到来自内核的SIGHUP信号,只能是其他进程发给它的(nginx -s reload就算给master进程发送SIGHUP)
  • SIGINT(ctrl+c),SIGWINCH(终端大小改变) 守护进程不会收到来自内核的SIGINT,SIGWINCH信号
  1. 守护进程和后台进程的区别
  • 守护进程不与终端挂勾,不能在终端输出东西,而后台进程则相反
  • 守护进程不受终端关闭影响,而后台进程随着终端关闭而退出

3. 浅谈printf()函数与write()函数

3.1 printf()函数缓存问题

printf()末尾不加\n就无法及时的将信息显示到屏幕 ,这是因为存在行缓存(windows上一般没有,类Unix上才有),也就是输出的数据不直接显示到终端,而是首先缓存到某个地方,当遇到行刷新标识或者该缓存已满的情况下,才会把缓存的数据显示到终端设备;
ANSI C中定义\n为行刷新标记,所以,printf()函数没有带\n是不会自动刷新输出流,直至行缓存被填满才显示到屏幕上。所以用printf的时候,注意末尾要添加\n;当然也可以printf()之后调用fflush(stdout)强制刷出数据;或者是使用函数setvbuf(stdout,NULL,_IONBF,0);,这个函数. 直接将printf缓冲区禁止, printf就直接输出了。

3.2 write()函数思考

  1. 如何保证多个进程写日志文件不会出现错乱?
  • open((const char *)plogname,O_WRONLY|O_APPEND|O_CREAT,0644); 打开文件使用O_APPEND标记,能够保证多个进程操作同一个文件时不会相互覆盖;
  • 内核wirte()写入时是原子操作;
  • 父进程fork()子进程是亲缘关系。会共享文件表项,
  1. fwrite()与write()
  • C语言的标准库fwrite()相比与系统调用write()多了一层缓冲区,缓冲区满了之后才会调用系统调用write()
  • fwrite()是标准I/O库一般在stdio.h文件
  • write():系统调用;
  • 所有系统调用都是原子性的

4. 网络编程剖析

4.1 listen()监听套接字

在这里插入图片描述

  1. 创建套接字int socket(int domain, int type, int protocol);
    • domain:使用的地址协议族,如 AF_INET、AF_INET6分别表示IPv4、IPv6格式。
    • type:套接字类型,如 SOCK_STREAM(流式传输协议)表示TCP套接字,SOCK_DGRAM(报式传输协议)表示UDP套接字。
    • protocol:通常为0,表示自动选择协议。
  2. IP和端口号绑定int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    • sockfd:套接字的文件描述符,通过socket调用得到的返回值。
    • addr:包含要绑定的IP地址和端口号的结构体。
    • addrlen:addr 结构体的大小,sizeof(addr)。
  3. 开始监听int listen(int sockfd, int backlog);
    • sockfd:套接字的文件描述符,通过socket调用得到的返回值。
    • backlog:在队列中等待接受的最大连接数(已完成连接队列 + 未完成连接队列)
  4. 从已完成连接队列中获取连接int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    • sockfd:套接字的文件描述符。
    • addr:用于存储客户端地址信息的结构体。
    • addrlen:addr 结构体的大小。
  5. 客户端连接到服务端的监听套接字int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    • sockfd:套接字的文件描述符,通过socket调用得到的返回值。
    • addr:用于存储服务端地址信息的结构体,这个IP和端口也需要转换为大端然后再赋值。
    • addrlen:addr 结构体的大小。
    • 返回时机:客户端调用connect只要收到服务端发来的SYN报文就返回了,也就是第二次握手包时就返回了
  6. 发送数据size_t send(int sockfd, const void *buf, size_t len, int flags);
    • sockfd:套接字的文件描述符。
    • buf:包含要发送数据的缓冲区。
    • len:要发送的数据的长度。
    • flags:发送标志,通常为0。
  7. 接收数据size_t recv(int sockfd, void *buf, size_t len, int flags);
    • sockfd:套接字的文件描述符。
    • buf:包含要发送数据的缓冲区。
    • len:要发送的数据的长度。
    • flags:发送标志,通常为0。
  8. 关闭套接字int close(int sockfd);

TCP会为每个监听套接字维护两个队列,通过accept获取的是已经完成三次握手的连接。
往返时间RTT是指未完成连接队列中连接的留存时间。对于客户端,这个RTT时间是第一次和第二次握手加起来的时间;对于服务器,这个RTT时间实际上是第二次和第三次握手加起来的时间;如果一个恶意客户,迟迟不发送三次握手的第三个包。那么这个连接就建立不起来,那么连接一直处于SYN_RCVD队列【服务器端的未完成队列中】,这个停留时间大概是75秒,如果超过这个时间,这一项会被操作系统干掉。

  • 如果两个队列之和【已完成连接队列,和未完成连接队列】达到了listen()所指定的第二参数,也就是说队列满了。此时,再有一个客户发送syn请求,服务器怎么反应?

    • 实际上服务器会忽略这个syn,不给回应; 客户端这边,发现syn没回应,过一会会重发syn包;
    • 从连接被扔到已经完成队列中去,到accept()从已完成队列中把这个连接取出这个之间是有个时间差的,如果还没等accept()从已完成队列中把这个连接取走的时候,客户端如果发送来数据,这个数据就会被保存再已经连接的套接字的接收缓冲区里,这个缓冲区有多大,最大就能接收多少数据量;
  • syn攻击【syn flood】:典型的利用TCP/IP协议涉及弱点进行坑爹的一种行为

    • 拒绝服务攻击(DOS/DDOS);
    • backlog:进一步明确和规定了:指定给定套接字上内核为之排队的最大已完成连接数【已完成连接队列中最大条目数】;
    • 大家在写代码时尽快用accept()把已完成队列里边的连接取走,尽快 留出空闲为止给后续的已完成三路握手的条目用,那么这个已完成队列一般不会满;
    • 一般这个backlog值给300左右;

参考资料:套接字-Socket网络编程4(TCP通信流程)

4.2 阻塞/非阻塞IO

  1. 阻塞IO
    调用阻塞式函数,这个函数就卡在这里,整个程序流程不往下走了【休眠sleep,不会占用CPU】,该函数卡在这里等待一个事情发生,只有这个事情发生了,这个函数才会往下走;这种函数,就认为是阻塞函数;accept(); 这种阻塞,并不好,效率很低;一般我们不会用阻塞方式来写服务器程序,效率低;
  2. 非阻塞IO
    不会卡住,充分利用时间片,执行效率更高;非阻塞模式的两个鲜明特点:
    • 不断的调用accept(),recvfrom()函数来检查有没有数据到来,如果没有,函数会返回一个特殊的错误标记来告诉你,这种标记可能是EWULDBLOCK,也可能是EAGAIN;
    • 如果数据没到来,那么这里有机会执行其他函数,但是也得不停的再次调用accept(),recvfrom()来检查数据是否到来,非常累;
    • 如果数据到来,那么就得卡在这里把数据从内核缓冲区复制到用户缓冲区,所以复制这个阶段是卡着完成的;

4.3 同步/异步IO

  1. 异步IO

    • 调用一个异步I/O函数时,我门要给这个函数指定一个接收缓冲区,我还要给定一个回调函数;
    • 调用完一个异步I/O函数后,该函数会立即返回。
    • 其余判断交给操作系统,操作系统会判断数据是否到来,如果数据到来了,操作系统会把数据拷贝到你所提供的缓冲区里,然后调用你所指定的这个回调函数来处理数据。
  2. 同步I/O

    • 调用select()判断有没有数据,有数据,走下来,没数据卡在那里;
    • select()返回之后,用recvfrom()去取数据;当然取数据的时候也会卡那么一下;同步I/O感觉更麻烦,要调用两个函数才能把数据拿到手;但是同步I/O和阻塞式I/O比,就是所谓的 I/O复用【用两个函数来收数据的优势】 能力;
  3. I/O复用

    • 所谓I/O复用,就是我多个socket【多个TCP连接】可以弄成一捆【一堆】,我可以用select这种同步I/O函数在这等数据;
    • select()的能力是等多条TCP连接上的任意一条有数据来;然后哪条TCP有数据来,我再用具体的比如recvfrom()去收。
    • 这种调用一个函数能够判断一堆TCP连接是否来数据的这种能力,叫I/O复用,英文I/O multiplexing【I/O多路复用】
  4. 非阻塞和异步I/O的差别

    • 非阻塞I/O要不停的调用I/O函数来检查数据是否来,如果数据来了,就得卡在I/O函数这里把数据从内核缓冲区复制到用户缓冲区,然后这个函数才能返回;
    • 异步I/O根本不需要不停的调用I/O函数来检查数据是否到来,只需要调用一次,然后就可以干别的事情去了;内核判断数据到来,拷贝数据到你提供的缓冲区,调用你的回调函数来通知你,你并没有被卡在那里的情况;

4.4 TCP/IP设计

4.4.1 三次握手

在这里插入图片描述

  1. 为什么是三次握手?
    三次握手的目的是建立可靠的通信信道,也就是要确保双方的发送和接收能力都是正常的

    • 第一次握手,客户端向服务端发送SYN报文,在服务端的视角看,可以确认客户端发送能力正常,服务端的接收能力正常
    • 第二次握手,服务端向客户端发送ACK+SYN报文,在客户端的视角看,可以确认客户端的发送和接收能力是正常的,服务端的发送和接收能力正常
    • 第三次握手,客户端向服务端发送ACK包报文,在服务端的视角看,可以确认客户端发送和接收能力正常,服务端的发送和接收能力正常
      如果只有两次握手,在客户端的视角看,可以确认双反的收发能力正常;但在服务端的视角看,只能确认客户端的发送能力和服务的接收能力正常。
  2. 初始序列号为什么是随机的?
    当一端为建立连接而发送它的 SYN 时,它会为连接选择一个初始序号。ISN 随时间而变化,因此每个连接都将具有不同的 ISN。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

  3. 为什么只有第三次握手可以携带数据?

    • 假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,然后疯狂重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
    • 对于第三次的话,完成第二次握手后,客户端已经处于 ESTABLISHED 状态,已经知道服务器的接收、发送能力是正常的了,所以能携带数据。
  4. 如果第三次握手丢失了,服务端会如何处理
    服务器发送完 SYN-ACK 包,如果未收到客户端响应的确认包,也即第三次握手丢失。需要分两种情况处理。

    • 第一种是客户端没有发送其他数据包,那么服务器就会重传SYN-ACK包,若等待一段时间仍未收到客户确认包,就进行第二次重传。如果重传次数超过系统规定的最大重传次数,则系统将该连接信息从半连接队列中删除。重传等待一般是指数增长1s,2s,4s,8s…
    • 第二种是客户端有发送其他数据包,那么这个数据包会被当做是携带了数据包的第三次握手。TCP规定,除了前两次握手报文之外,其他所有报文都将ACK标志位设置为1,所有服务端会把数据包当做是ack确认包从而完成了三次握手。

4.4.2 四次挥手

在这里插入图片描述
对于每个TCP连接,操作系统需要分别开辟一个收发缓冲区来处理数据的收发。当关闭一个TCP连接时,如果发送缓冲区有数据,操作系统会把发送缓冲区中残留的数据发完再发FIN包表示连接关闭。

  1. 为什么是四次挥手?
    四次挥手的目的是确保双方的数据都已发送完毕,确保连接安全关闭。

    • 第一次挥手,客户端向服务端发送FIN报文,此时代表客户端数据已经发送完毕
    • 第二次挥手,服务端向客户端发送ACK报文,此时客户端到服务端的连接关闭
    • 第三次挥手,服务端向客户端发送FIN报文,此时代表服务端数据已经发送完毕
    • 第四次挥手,客户端向服务端发送ACK报文,此时双方连接关闭
  2. 为什么主动关闭(客户端)的一方需要TIME-WAIT等待2MSL?

    • 保证TCP协议的全双工连接能够可靠关闭。如果第四次挥手ACK包丢失,服务端重发FIN包, 由于客户端已经关闭
      ,客户端就找不到与重发的FIN对应的连接,就会向服务端连接复位RST包,导致TCP协议不符合可靠连接的要求。
    • 保证这次连接的重复数据段从网络中消失。如果客户端直接CLOSED,然后又再向Server发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的,特别是如果开启了快速重用端口选项SO_REUSEPORT。也就是说有可能新连接和老连接的端口号是相同的。假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达Server,由于新连接和老连接的端口号是一样的,新旧连接的数据包发生混淆了。等待2MSL,这样可以保证本次连接的所有数据都从网络中消失。
  3. 浅谈SO_REUSEPORT选项
    SO_REUSEADDR:主要解决TIME_WAIT状态导致bind()失败的问题

    //setsockopt(SO_REUSEADDR)用在服务器端,socket()创建之后,bind()之前
    //setsockopt():设置一些套接字参数选项;
    //参数2:是表示级别,和参数3配套使用,也就是说,参数3如果确定了,参数2就确定了;
    //参数3:允许重用本地地址
    int  reuseaddr=1; //开启
    if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR, (const void *) &reuseaddr,sizeof(reuseaddr)) == -1)
    {
            char *perrorinfo = strerror(errno); 
        printf("setsockopt(SO_REUSEADDR)返回值为%d,错误码为:%d,错误信息为:%s;\n",-1,errno,perrorinfo);
    }
    
    • SO_REUSEADDR允许启动一个监听服务器并捆绑其端口,即使以前建立的将端口用作他们的本地端口的连接仍旧存在;【即便TIME_WAIT状态存在,服务器bind()也能成功】
    • 允许同一个端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可;
    • SO_REUSEADDR允许单个进程捆绑同一个端口到多个套接字,只要每次捆绑指定不同的本地IP地址即可;
    • SO_REUSEADDR允许完全重复的绑定:当一个IP地址和端口已经绑定到某个套接字上时,如果传输协议支持,同样的IP地址和端口还可以绑定到另一个套接字上;一般来说本特性仅支持UDP套接字[TCP不行];
    • 所有TCP服务器都应该指定本套接字选项,以防止当套接字处于TIME_WAIT时bind()失败的情形出现

5. epoll技术

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

epoll可以理解为event poll,它是一种事件驱动的I/O模型,可以用来替代传统的select和poll模型。epoll的优势在于它可以同时处理大量的文件描述符,而且不会随着文件描述符数量的增加而降低效率。

epoll的实现机制是通过内核与用户空间共享一个事件表,这个事件表中存放着所有需要监控的文件描述符以及它们的状态,当文件描述符的状态发生变化时,内核会将这个事件通知给用户空间,用户空间再根据事件类型进行相应的处理。

epoll的接口和工作模式相对于select和poll更加简单易用,因此在高并发场景下被广泛使用。

epoll底层维护两个数据结构,一个是红黑数,保存所有监听的socket,一个是双向链表,保存当前有事件发生的socket,当epoll监听的socket有事件发生时,内核调用epoll_event_callback()向双向链表增加节点。

5.1 epoll_create()

  • 函数声明int epoll_create(int size)
  • 参数 size 大于0就行
  • 功能 创建一个epoll对象,返回该对象的描述符【文件描述符】,这个描述符就代表这个epoll对象
  • 函数实现
    • 分配内存创建eventpoll结构体
    • eventpoll->rbr指向红黑数的根节点
    • eventpoll->rdlist双向链表的头节点

5.2 epoll_ctl()

  • 函数声明int epoll_ctl(int efpd,int op,int sockid,struct epoll_event *event);
  • 参数
    • efpd:epoll_create()返回的epoll对象描述符;
    • op:动作,添加/删除/修改 ,对应数字是1,2,3, EPOLL_CTL_ADD, EPOLL_CTL_DEL ,EPOLL_CTL_MOD
      • EPOLL_CTL_ADD添加事件:等于你往红黑树上添加一个节点,每个客户端连入对应一个socket,这个socket就是红黑树中的key,把这个节点添加到红黑树上去;
      • EPOLL_CTL_MOD:修改事件;你 用了EPOLL_CTL_ADD把节点添加到红黑树上之后,才存在修改;
      • EPOLL_CTL_DEL:是从红黑树上把这个节点干掉;这会导致这个socket【这个tcp链接】上无法收到任何系统通知事件;
    • sockid:表示客户端连接,就是你从accept();这个是红黑树里边的key;
    • event:事件信息,这里包括的是 一些事件信息;EPOLL_CTL_ADD和EPOLL_CTL_MOD都要用到这个event参数里边的事件信息;
  • 功能
    把一个socket以及这个socket相关的事件添加到这个epoll对象描述符中去,目的就是通过这个epoll对象来监视这个socket【客户端的TCP连接】上数据的来往情况;当有数据来往时,系统会通知我们;
  • 函数实现
    • 【EPOLL_CTL_ADD】增加节点到红黑树中,添加新的监听socket
    • 【EPOLL_CTL_DEL】从红黑树中把节点干掉,移除监听的socket
    • 【EPOLL_CTL_MOD】找到红黑树节点,修改这个节点中的内容,修改socket的监听事件(读/写/关闭)

5.3 epoll_wait()

  • 函数声明 int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
  • 参数
    • epfd:是epoll_create()返回的epoll对象描述符;
    • events:是内存,也是数组,长度 是maxevents,表示此次调用可以收到maxevents个已经准备好的读写事件;说白了,就是返回的是 实际 发生事件的tcp连接数目;
    • timeout:阻塞等待的时长;
  • 功能
    • 阻塞一小段时间并等待事件发生,返回事件集合,也就是获取内核的事件通知;
    • 说白了就是遍历这个双向链表,把这个双向链表里边的节点数据拷贝出去,拷贝完毕的就从双向链表里移除;双向链表里记录的是所有有数据/有事件的socket【TCP连接】;
    • epitem结构设计的高明之处:既能够作为红黑树中的节点(rbr),又能够作为双向链表中的节点(rdlink);

5.4 epoll工作模式

  • LT 水平触发,低速模式,效率差(缺省),一个事件不处理,它会一直触发
  • ET 边缘触发,高速模式,速度快,只对非阻塞socket生效,内核只会通知一次(不管是否处理),编码难度加大
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

it00zyq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值