Linux网络编程II

1.TCP通信并发。使用多线程或多进程。
1. 一个父进程,多个子进程
2. 父进程负责等待并接受客户端的连接
3. 子进程:完成通信,接收一个客户端连接就创建一个子进程用于通信
ps: 回收子进程使用SIGCHILD信号,使用sigaction()信号捕捉函数,使用waitpid()非阻塞回收子进程资源
 
2.TCP状态转换。
 
3.半关闭
  • 当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
    - sockfd: 需要关闭的socket的描述符
    - how: 允许shutdown操作选择以下几种方式:
            - SHUT_RD(0): 关闭sockfd上的读功能,该套接字不再接收数据,任何在套接字接收缓冲区的数据都将被丢弃;
            - SHUT_WR(1): 关闭sockfd的写功能,进程不能对此套接字发出写操作;
            - SHUT_RDWR(2): 关闭读写功能,相当于调用shut down两次,首先是SHUT_RD,然后是SHUT_WR.
 
1. 使用close(),如果多个进程共享一个套接字,close每调用一次,计数减1,直到计数为0,也就是所有进程都调用了close,套接字被释放。
2. 使用shutdown(),在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其他进程将无法通信。但如果一个进程close(sfd)将不会影响其他进程。

 

4.端口复用。
  • 场景:服务器先关闭,会进入FIN_WAIT_2状态,客户端关闭,服务器会进入TIME_WAIT状态,此时服务器的端口还未被释放,不能被重新使用。
  • 程序突然退出而系统没有释放端口
// 设置套接字的属性,包括端口复用
#include <sts/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    - sockfd: 要操作的文件描述符
    - level: 级别 - SOL_SOCKET(端口复用)
    - optname: 选项的名称
            - SO_REUSEADDR
            - SO_REUSEPORT
    - optval: 端口复用的值(整型)
            - 1: 可以复用
            - 0: 不可以复用
    - optlen: optval参数的大小,sizeof(optval)

 

5.IO多路复用(多路转接)
  • IO:对缓冲区的操作
  • IO多路复用使程序能同时监听多个文件描述符,能够提高程序的性能,Linux下实现多路复用的系统调用主要有select、poll、epoll。
 
6.IO模型
  • BIO模型(Blocking)
  • NIO模型(Nonblocking)
  • IO多路转接技术
    • select、poll
    • epoll
 
7.IO多路转接技术:select
  • 首先构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
  • 调用一个系统函数(select),监听该列表中的文件描述符,直到这些描述符中一个或多个进行了IO操作时,该函数才返回。
    • 该函数是阻塞的
    • 函数对文件描述符的检测操作是由内核完成的
  • 在返回时,该函数会告诉进程有多少(哪些)描述符要进行IO操作。
  • 缺点:
    • 每次调用select,都需要把fd集合从用户区拷贝到内核区,这个开销在fd比较多的时候会很大
    • 每次调用select都需要在内核遍历传递进来的所有fd(O(n)时间复杂度),这个开销也很大
    • select支持的文件描述符数量少,默认只有1024(fd_set底层是二进制位形式表示,fd集合用128个字节表示,所有默认表示的是1024位代表1024个文件描述符)
    • fds集合不能重用,每次需要重置(需要有一个fds集合专门记录需要检测的文件描述符,另外每次需要一个临时的fds记录从内核返回来的结果)
#include <sys/times.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);    // sizeof(fd_set) = 128 byte = 1024 bit
    - nfds: 委托内核检测的最大文件描述符+1
    - readfds:  要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读属性,
                - 对应的是对方发送过来的数据,
                - 为读是被动接收数据,检测的就是读缓冲区
                - 是一个传入传出参数
    - writefds: 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写属性
                - 委托内核检测写缓冲区是否还可以写数据
    - exceptefds: 检测发生异常的文件描述符的集合
    - timeout: 设置的超时时间, NULL 永久阻塞,直到检测到了文件描述符有变化;tv_sec = 0 tv_usec = 0 不阻塞;tv_sec > 0 tv_usec > 0 阻塞对应时间
    - 返回值:失败返回-1;成功返回>0(n)的数,表示检测的集合中有n个文件描述符发生了变化
struct timeval {
    long tv_sec;
    long tv_usec;
}
 
void FD_CLR(int fd, fd_set *set);
    - 作用:将参数文件描述符fd对应的标志位设置为0
 
void FD_ISSET(int fd, fd_set *set);
    - 作用:判断fd对应的标志位是0还是1,
    - 返回值:fd对应的标志位的值
 
void FD_SET(int fd, fd_set *set);
    - 作用:将参数文件描述符fd对应的标志位设置为1
 
void FD_ZERO(fd_set *set);
    - 作用:fd_set一共有1024位,初始化所有位为0
 
 
8.IO多路转接技术:poll
  • 改进了select的第3和第4点缺点,用结构体pollfd取代了select的fd_set数据结构,pollfd可以保存需要检测事件以及内核返回的结果。
  • 缺点:没有改进select的第1和第2点缺点
    • 每次调用也需要把fd集合从用户区拷贝到内核区,这个开销在fd比较多的时候会很大
    • 每次调用需要在内核遍历传递进来的所有fd(O(n)时间复杂度),这个开销在fd比较多的时候也很大
    • poll调用的返回的内核结果只告知了有几个文件描述符发生了改变,并没有告知是具体哪几个,依然需要程序遍历找出(O(n))
#include <poll.h>
struct pollfd {
    int fd;                // 委托内核检测的文件描述符
    short events;          // 委托内核检测文件描述符的什么事件 POLLIN 读  POLLOUT 写
    short revents;         // 内核返回的文件描述符发生的事件
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    - fds: 需要检测的文件描述符的集合
    - nfds: 委托内核检测的最大文件描述符+1
    - timeout: 阻塞时长, NULL 永久阻塞,-1 阻塞,当检测到需要检测的文件描述符发生变化时解除阻塞;0 不阻塞;>0 阻塞时长
    - 返回值:失败返回-1;成功返回>0(n)的数,表示检测的集合中有n个文件描述符发生了变化
 

  

9.IO多路转接技术:epoll
  • 监控多个文件描述符,检测其中是否有可以进行IO操作的( monitoring multiple file descriptors to see if I/O is possible on any of them.)
  • 底层实现是红黑树和双链表,红黑树记录需要检测的文件描述符,遍历复杂度O(logn);双链表记录改变了的文件描述符,返回给程序后避免了再次遍历查找具体改变的文件描述符
#include <sys/epoll.h>
int epoll_create(int size);
    - 作用:创建一个新的epoll实例。在内核中创建一个数据,包括需要检测的文件描述符(RBT),和就绪列表存放检测到数据发生改变的文件描述符信息(双链表)
    - size: >0, 无意义;以前底层hashmap实现时需要
    - 返回值:成功返回文件描述符,操作epoll实例;失败返回-1并设置errno
 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    - epfd: epoll实例对应的文件描述符
    - op: 要进行什么操作,EPOLL_CTL_ADD 添加;EPOLL_CTL_MOD 修改;EPOLL_CTL_DEL 删除
    - fd: 要检测的文件描述符
    - event: 检测文件描述符什么事件,常见的epoll检测事件:EPOLLIN、EPOLLOUT、EPOLLERR
 
int epoll_wait(nt epfd, struct epoll_event *events, int maxevents, int timeout);
    - epfd: epoll实例对应的文件描述符
    - events: 传出参数,保存了发生了变化的文件描述符的信息
    - maxevents: 第二参数结构体数组的大小
    - timeout: 阻塞时长, NULL 永久阻塞,-1 阻塞,当检测到需要检测的文件描述符发生变化时解除阻塞;0 不阻塞;>0 阻塞时长
    - 返回值:失败返回-1;成功返回>0(n)的数,表示检测的集合中有n个文件描述符发生了变化

  

10.epoll的工作模式
  • LT模式(水平触发)
    • 缺省的工作模式,同时支持阻塞和非阻塞socket。
    • 内核告诉你一个文件描述符是否就绪,然后程序可以对该就绪的fd进行IO操作。如果不进行任何操作,内核会继续通知程序。
    • 过程:
      • 假设委托内核检测读事件 -> 检测fd的读缓冲区
      • 读缓冲区有数据 -> epoll检测到了会给用户通知
        • a. 用户不读数据 -> 数据一直在缓冲区,epoll会一直通知
        • b. 用户只读了一部分数据 -> epoll会继续通知
        • c. 缓冲区的数据读完了 -> 不通知
  • ET模式(边沿触发)
    • 高速的工作模式,支持非阻塞socket。
    • 当描述符从从未就绪变为就绪时,内核会通知程序,然后内核会假设程序已经知道文件描述符就绪了,将不再为那个文件描述发送更多的就需通知。直到缓冲区有新数据有新数据到达或由空变为非空的时候,将会再次触发内核通知程序。
    • ET模式从很大程度上减少了epoll时间被重复处罚的次数,效率比LT模式高。
    • ET模式下epoll工作必须使用非阻塞套接字接口,并且需要结合循环读取数据的方式,以避免由于一个文件描述符的阻塞读/写操作把处理多个文件描述符的任务饿死。
    • 过程:
      • 假设委托内核检测读事件 -> 检测fd的读缓冲区
      • 读缓冲区有数据 -> epoll检测到了会给用户通知
        • a. 用户不读数据 -> 数据一直在缓冲区,epoll下次检测的时候不通知
        • b. 用户只读了一部分数据 -> epoll不通知
        • c. 缓冲区的数据读完了 -> 不通知,直到缓冲区有新的数据写入才通知
 
11.LT和ET模式下对读写操作是否就绪的判断
  • 水平触发
    • 读操作:只要缓冲内容不为空,LT模式返回读就绪。
    • 写操作:只要缓冲区还不满,LT模式返回写就绪。
  • 边沿触发
    • 读操作:1)缓冲区由空变为非空;2)缓冲区有新数据到达时;3)缓冲区有数据可读,且文件描述符进行EPOLL_CTL_MOD修改EPOLLIN事件时。
    • 写操作:1)缓冲区由非空变为空;2)缓冲区有数据被发走时,即缓冲区内容减少时;3)缓冲区有空间可写,且文件描述符进行EPOLL_CTL_MOD修改EPOLLOUT事件时。
 
12. 使用epoll模型,水平(LT)触发模式,当socket可写时,会不停的触发socket可写的事件,如何处理?
  • 开始不把socket加入epoll,需要向socket写数据的时候,直接调用write或者send发送数据。如果返回EAGAIN,把socket加入epoll,在epoll的驱动下写数据,全部数据发送完毕后,再移出epoll。
 
 
 
 
 
 
posted @ 2021-08-09 21:14  萌新的学习之路  阅读(40)  评论(0编辑  收藏  举报