网络系统编程笔记

捡起来做个毕设

Linux网络编程基础API

  • 创建socket通过socket系统调用,并传入(地址族、服务类型(如SOCK_STREAM)、具体协议(默认为0)),并返回一个socket文件描述符
  • 使用bind()来命名socket,因为socket创建的时候并未给定socket地址,因此需要将socket绑定到socket地址上(这样客户端才能知道怎么连接它)
  • ipv4 socket地址结构(sockaddr_in),包括地址族(6字节)、端口号(2字节)、IPv4地址结构(4字节)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <arpa/inet.h>
    struct sockaddr_in {
    sa_family_t sin_family; // 地址族:AF_INET
    u_int16_t sin_port; // 端口号,要用网络字节序表示(注意是2字节因此搭配htons使用)
    struct in_addr sin_addr; // 在写服务器程序的时候可以赋值为0(或INADDR_ANY)表示本机中所有的ip地址,
    };
    struct in_addr {
    u_int32_t s_addr; // IPv4地址,要用网络字节序表示
    }
  • 对于socket来说,read(默认阻塞)返回0(i.e.EOF)就表示对端主动关闭了连接
  • 之所以区分监听描述符和连接描述符是因为(服务器不单是处理一个客户连接)
  • 建立连接
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <sys/types.h>
    #include <sys/socket.h>
    // 都需要通用socket地址结构(sockaddr)作为参数(因为那时没有void*指针),因此sockaddr_in需要强制转换为sockaddr通用socket地址结构

    // len用sizeof获取,单位为字节。因为strlen返回为字符数,刚好字符也为1字节,因此也可以
    int bind(int listenfd, const struct sockaddr* addr, socklen_t sockaddr_len);
    // backlog参数"提示"内核监听队列的最大长度,如果最大长度超过backlog服务器将不受理新的客户连接
    // backlog只表示处于完全连接(ESTABLISHED)或半连接(SYN_RCVD)的socket上限
    int listen(int listenfd, int backlog);
    // 默认情况下accept是阻塞的,addr为传出参数,传出的是peer socket(也就是客户端)的socket地址结构
    // 成功时返回一个非负的连接文件描述符
    int accept(int listenfd, struct sockaddr* addr, socklen_t* addrlen);

    int connect(int sockfd, const struct sockaddr* addr, socklen_t sockaddr_len);
  • TCP数据读写(send\recv),UDP数据读写(recvfrom、sendto),因为UDP没有连接的概念,因此进行读写的时候需要加上socket地址结构作为参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <sys/types.h>
    #include <sys/socket.h>
    // flags参数通常设置为0即可
    ssize_t recv(int socket, void* buf, size_t len, int flags);
    ssize_t send(int sockfd, const void* buf, size_t len, int flags);
    // src_addr为传出参数,如果不需要接收的socket地址结构信息,可以设置为NULL,*len也设置为NULL
    ssize_t recvfrom(int socket, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
    // dest_addr为对端socket的地址
    ssize_t sendto(int socket, const void* buf, size_t len, int flags, struct sockaddr* dest_addr, socklen_t* addrlen);
  • htons/ntohs,htonl/ntohl完成网络字节序与主机字节序相互之间的转换
    在头文件#include <netinet/in.h>
  • socket地址信息函数
    1
    2
    3
    #include <sys/socket.h>
    int getsockname(int sockfd, struct sockaddr*, socklen_t*); // 将当前连接socket本地端socket地址保存在参数2,3中
    int getpeername(int sockfd, struct sockaddr*, socklen_t*); // 将当前连接socket远端socket地址信息保存在参数2,3中
  • IP地址点分十进制字符串与网络字节序整数的相互转换
    1
    2
    3
    #include <arpa/inet.h>
    int inet_pton(int af, const char* src, void* dst); // 将点分十进制字符串转换为网络字节序整数
    const char* inet_ntop(int af, const void* src, char* dst, socklen_t); // 最后一个参数指定目标存储空间的大小
  • 网络信息API(Ip地址和端口号不便记忆,可以通过主机名来替代IP地址,服务名来代替端口号)
    • gethostbyname/gethostbyaddr(通过主机名或IP地址获取主机信息)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <netdb.h>
      struct hostent* gethostbyname(const char* name); // 会先再本地的/etc/hosts配置文件中查找,查不到再去本地dns服务器中查找
      struct hostent* gethostbyaddr(const void* addr, size_t len, int type) // type为地址族
      struct hostent {
      char* h_name; // 主机名
      char** h_aliases; // 主机别名列表
      int h_addrtype; // 地址族
      int h_length; // 地址长度
      char** h_addr_list; // 按网络字节序列出的主机IP地址列表
      };
    • getservbyname/getservbyport
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <netdb.h>
      struct servent* getservbyname(const char* name, const char* proto);
      struct servent* getservbyport(int port, const char* proto); // proto参数通常是"tcp"(获取流服务)、"udp"(获取数据报服务)、NULL(获取所有类型的服务)
      struct servent {
      char* s_name;
      char** s_aliases;
      int s_port;
      char* s_proto; // 服务类型,通常是tcp或者udp
      };
    • getaddrinfo,根据主机名获得socket地址
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      #include <netdb.h>
      int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result); // result参数指向一个链表,该链表用于存储getaddrinfo反馈的结果
      void freeaddrinfo(struct addrinfo* res); // 对result这块区域的释放
      struct addrinfo {
      int ai_flags;
      int ai_family; // 地址族
      int ai_socktype; // 服务类型SOCK_STREAM
      int ai_protocol; // 与socket调用的第三个参数含义相同
      socklen_t ai_addrlen; // socket地址ai_addr的长度
      char* ai_canonname; // 主机的别名
      struct sockaddr* ai_addr; // 指向socket地址
      struct addrinfo* ai_next; // 指向下一个addrinfo结构
      };
    • getnameinfo(与getaddrinfo是反着来的),根据socket地址获得主机名
      1
      2
      3
      #include <netdb.h>
      // 返回的主机名存储在host参数指向的缓存中,服务名存储在serv指向的缓存中
      int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
  • 操作进程的命令
    • strace:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹
    • ps:列出当前系统中的进程(包括僵尸进程)
    • top:打印出关于当前进程资源的使用信息
  • 全局变量environ指向环境变量列表的首地址
  • 进程相关api
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <string.h>
    char* strstr(const char *str1, const char *str2); // 返回str1中第一次出现str2的位置,若不存在则返回NULL


    #include <stdlib.h>
    // 如果overwrite覆写参数为1,如果环境变量列表中出现该变量,则覆写它; 0 otherwise
    int setenv(const char *name, const char *value, int overwrite);


    #include <unistd.h>
    // 在当前进程的上下文中加载一个程序并运行
    int execve(const char *pathname, char *const argv[],
    char *const envp[]);
    // 子进程复制父进程的虚拟地址空间,共享文件(继承了打开文件表),相同但是独立的地址空间(相同的用户栈、堆、全局本地变量,代码段)
    pid_t fork(void);

    // dup2复制描述符表项oldfd到描述符表项newfd(底层中将描述符表项newfd的内容删除,
    // 如文件打开表项、v-node表项。再将描述符表项newfd指向文件描述符表项所指的打开文件表项)
    int dup2(int oldfd, int newfd);
  • 读取文件元数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #include <unistd.h>
    #include <sys/stat.h>

    // stat函数以一个文件名进行输入,fstat以一个文件描述符输入
    // 两个函数用来获取当前文件的元数据并填写到stat结构体中
    int stat(const char* filename, struct stat* buf);
    int fstat(int fd, struct stat* buf);

    struct stat {
    dev_t st_dev; // 设备
    ino_t st_ino; // inode
    mode_t st_mode; // 文件权限和类型
    nlink_t st_nlink; // 硬链接的个数
    uid_t st_uid; // 用户id
    gid_t st_gid; // 组id
    dev_t st_rdev; // 设备类型
    off_t st_size; // 整体大小(字节)
    unsigned long st_blksize; // 文件I/O的块大小
    unsigned long st_blocks; // 分配块的数量
    time_t st_atime; // 最后访问的时间
    time_t st_mtime; // 最后修改的时间
    time_t st_ctime; // 最后改变的时间
    };

    // 常用宏
    S_ISLNK(st_mode) //是否是一个连接.
    S_ISREG(st_mode) //是否是一个常规文件.
    S_ISDIR(st_mode) //是否是一个目录
    S_ISCHR(st_mode) //是否是一个字符设备.
    S_ISBLK(st_mode) //是否是一个块设备
    S_ISFIFO(st_mode) //是否 是一个FIFO文件.
    S_ISSOCK(st_mode) //是否是一个SOCKET文件

    一些辅助函数

    1
    2
    3
    4
    5
    6
    7
    #include <string.h>
    // 返回指向s的指针
    void* memset(void* s, int c, size_t n);
    // 等同于memset(s, '\0', n)的作用
    void bzero(void* s, size_t n);
    // 获取文件路径名的后缀部分
    char* basename(filename);

线程相关

  • Posix线程相关API
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <pthread.h>
    // tid为线程id,attr为线程属性(默认为NULL),f为线程执行的函数,arg为线程执行函数的参数
    // 第三个参数为void*的指针,一定要有个void*参数(第四个)
    int pthread_create(pthread_t* tid, pthread_addr_t* attr, func* f, void* arg);

    // 返回当前线程的pid
    pthread_t pthread_self(void);

    // 下面这个api显示终止当前调用的线程
    // 注意exit函数终止进程及所有进程相关的线程,释放内存和打开文件的资源,并返回相应的状态给父进程
    void pthread_exit(void* thread_return);

    // 回收已终止线程的资源。该函数会阻塞直到线程tid终止,然后回收资源
    // 返回thread_return参数,作为pthread_exit的参数
    int pthread_join(pthread_t tid, void **thread_return); // 等待具体线程终止

    // 线程分为可分离的和可结合的,可结合的线程能够被其他线程回收和终止
    // 在被回收之前它的内存资源(例如栈)是不释放的,调用它能够使线程变为可分离的
    // 使得系统能够自动回收线程资源,避免内存泄漏。
    int pthread_detach(pthread_t tid);
  • 信号量。具有非负整数值的全局计数(用来记录所拥有的资源),用来记录空闲的线程。如果该全局变量为0该线程将会阻塞(没有资源可取),直到计数大于0
    • P操作,挂起该线程。
    • V操作,唤醒该线程。
    • 计数为1的信号量可作为锁。
  • 可重入函数(线程安全函数的真子集),当它被多个线程调用时不会引入共享的全局变量
  • LWP(lighted weight process)
  • ps -Lf pid查看当前进程下的线程-f标识整个format展示
  • 为什么要有线程?
    • 进程间信息难以共享
    • fork创建进程的开销太大,即便有写时复制技术,还需要复制VA,复制页表,打开文件表(在进程结构中)等
  • 临界区:访问某一共享资源的代码片段
  • pthread_t为无符号整型变量,printf时用%ld格式符
  • 条件变量int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);将会释解锁mutex并阻塞在条件变量上,成功返回后mutex被锁定并由调用线程拥有
  • 信号量int sem_init(sem_t* sem, int pshared, unsigned int value); // 在sem地址处初始化一个信号量。如果pshared为0表示信号量在当前进程的线程之间共享(信号量的地址会在全局变量中),如果pthread为非0则会在进程之间共享(信号量的地址会在共享内存中)

高级I/O函数

    • 标准输入 STDIN_FILENO
    • 标准输出 STDOUT_FILENO
    • 标准错误 STDERR_FILENO
  • pipe(用于IPC)
    • 内部传输的数据是字节流,linux管道容量的大小默认为65536字节
    • 定义
      1
      2
      #include <unistd.h>
      int pipe(int fd[2]); // 两个文件描述符,fd[0]为读端,fd[1]为写端
    • 管道是半双工的,若要实现双向数据传输,应使用两个管道
    • 默认情况下是阻塞的(管道空read就会被阻塞,管道满write就会被阻塞)
    • 非阻塞管道(将fd[0]和fd[1]设置为非阻塞的)
      • 如果fd[0]的引用计数减少为0,即没有任何进程从管道中读数据,则针对fd[1]进行write将失败并引发SIGPIPE信号(默认执行动作时terminate)
      • 如果fd[1]的引用计数减少为0,则对fd[0]进行read将会返回0,即EOF(表示对端关闭了连接)
    • 双向管道
      1
      2
      3
      4
      #include <sys/types.h>
      #include <sys/socket.h>
      // 直接创建双向管道(两个fd既可读又可写),前三个参数与socket系统调用的参数一样
      int socketpair(int domain, int type, int protocol, int fd[2]);
  • dup2
    1
    2
    3
    #include <unistd.h>
    int dup(int file_descriptor); // 返回一个新的fd,该fd与参数fd指向同一个打开文件表中的项
    int dup2(int oldfd, int newfd); // 返回不小于newfd的整数值
  • readv/writev。将多块分散的内存写入/读出fd,通常web服务器中的内容可以放在分散的内存中
    1
    2
    3
    4
    5
    6
    7
    8
    #include <sys/uio.h>
    // vector参数类型是iovec结构数组,count表示数组的大小(即分散内存的个数)
    ssize_t readv(int fd, const struct iovec* vector, int count);
    ssize_t writev(int fd, const struct iovec* vector, int count);
    struct iovec {
    void* iov_base; // 内存起始地址
    size_t iov_len; // 这块内存的长度
    };
    • snprintf将格式输出到缓冲区中,sprintf将格式输出到指针所指向的内存区域,printf将格式输出到标准输出。
  • sendfile。函数在两个文件描述符之间直接传递数据(在内核中),从而避免用户缓冲区和内核缓冲区之间的数据拷贝,实现了零拷贝
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
      #include <sys/senfile.h>
    // in_fd是写入的fd,out_fd是读出的fd,in_fd必须是一个类似mmap函数的文件描述符
    // 必须指向真实的文件,不能是socket和管道
    // offset指出从读入文件流的哪个位置开始读,如果为空,则使用默认起始位置
    ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
    ``到fd`
    - mmap。内存映射文件,可以将申请的这段内存作为进程间通信的共享内存(通过shm_open函数返回共享对象的fd供进程使用),也可以将文件映射到其中
    ``` c
    #include <sys/mmap.h>
    // start为用户虚拟地址,prot用于设置共享内存段的访问权限
    // PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE
    // flags参数控制内存段内容被修改后程序的行为
    // mmap函数成功时返回指向目标内存区域的指针
    void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
    int munmap(void* start, size_t length);
  • splice在两个文件描述符间移动数据(零拷贝操作)
  • tee在两个管道描述符之间复制数据(零拷贝操作)
  • fcntl(file control)。
    • 定义
      1
      2
      3
      #include <fcntl.h>
      // cmd参数指定执行何种类型的操作,后面是可变参数类型
      int fcntl(int fd, int cmd, ...);
    • 常用操作及参数
      • F_GETFD(无第三个参数)。获取fd的标志,成功时返回fd的标志
      • F_SETFD(第三个参数类型为long)。设置fd的标志,成功时返回0
      • F_GETFL(第三个参数类型为void)。获取fd的状态标志
      • F_SETFL(第三个参数类型为long)。设置fd的状态标志,成功时返回0
      • F_GETOWN(无第三个参数)。获得SIGIO和SIGURG信号的素组进程的PID或GID
      • F_SETOWN(第三个参数为long)。成功时返回0
      • F_GETSIG(无第三个参数),获取当前应用程序被通知fd可读可写时是哪个信号通知的,返回信号值(0表示SIGIO)
      • F_SETSIG(第三个参数为long),设置当fd可读/写时,系统应该触法哪个信号来通知程序
    • fcntl函数还可以为目标文件描述符指定宿主进程,宿主进程将捕获信号(将信号和描述符关联起来)

Linux服务器程序规范

  • 服务器程序一般以后台进程(i.e. 守护进程)的方式运行;有一套日志系统(至少能输出日志到文件中);服务器程序一般以一个专门的非root身份运行;Linux服务器通常是可配置的(用配置文件来管理);会有一个PID文件记录后台进程的PID;需要考虑到系统资源的限制。

服务器框架

  • 模块
    • I/O处理单元:处理客户连接,读写网络数据
    • 逻辑单元:业务进程或线程
    • 网络存储单元:数据库、文件或缓存
    • 请求队列(通常被实现为池的一部分):各单元之间的通信方式
  • 在处理I/O的时候,阻塞(进程放弃处理机处于暂停的状态)和非阻塞都是同步I/O,只有调用特殊的API才是异步I/O
  • 同步I/O模型
    • 阻塞I/O,可能被阻塞的系统调用包括accept、send、recv和connect
    • 非阻塞I/O,执行的系统调用无论事件是否发生总是立即返回。可以根据errno来区分情况:对accept、read、recv,事件未发生errno通常被设置为EAGIN或EWOULDBLOCK;对connect而言errno会被设置为EINPROGRESS(意为”在处理中”)
    • I/O复用,应用程序通过I/O复用函数向内核注册事件,I/O复用函数本身是阻塞的(因此需要通过并发编程手段来提高服务器的性能),可同时监听多个I/O事件
    • 信号驱动I/O,比如
      1. 通过SIGIO/SIGURG信号(必须与文件描述符关联使用)报告I/O事件。可以为一个目标文件描述符指定一个宿主进程,然后当描述符fd可读/写时,系统将触发相应的信号,目标文件描述符的宿主进程将捕获到信号,执行相应的信号处理函数(这一过程是异步的(注意不是异步I/O))
      2. 完成非阻塞I/O的操作(同步的过程)
  • 异步I/O模型
    • 异步I/O,由操作系统告诉内核用户读写缓冲区的位置,以及I/O操作完成后内核通知应用程序的方式,包括文件的偏移量。异步I/O的读写操作总是立即返回,真正的I/O操作已经被内核接管(内核来执行I/O操作)。等最后完成写入后内核在通知用户写入完成
  • 同步I/O vs. 异步I/O
    • 同步I/O向应用程序通知的是I/O就绪事件,而异步I/O向应用程序通知的是I/O完成事件注意这里和接下来事件处理模式的同步和异步指的是何种I/O事件

事件处理模式

  • 服务器通常处理三类事件:I/O事件、信号及定时事件
  • Reactor模式(使用同步I/O模型),即I/O多路复用+非阻塞I/O
    • 利用I/O复用,分为主线程,请求队列,工作线程三部分。主线程往内核事件表中注册socket读就绪事件,epoll_wait等待socket上有数据可读,若有事件发生,通知主线程将事件放入请求队列中,睡眠在请求队列上的工作线程被唤醒,从socket上读取客户请求的数据,再注册socket可写事件,若socket可写,epoll_wait通知主线程,主线程将事件放入请求队列,工作线程将往socket上写入服务器处理客户请求的结果。
  • Proactor模式(使用异步I/O模型),可以勇reactor来模拟,使用主线程完成数据的读写
  • 并发模式。是指I/O处理单元和多个逻辑单元之间协调完成任务的方式,这里的”异步”指的是程序的执行需要由系统事件来驱动(比如中断、信号),半同步半异步模式允许一个线程同时处理多个客户连接

I/O复用

  • 使用到I/O多路复用的场景
    • 客户端要同时处理多个socket
    • 客户端要同时处理用户输入和网络连接
    • 服务器要同时监听socket和连接socket
    • 服务器要同时处理TCP请求和UDP请求
    • 服务器要同时监听多个端口
  • select系统调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <sys/select.h>
    // nfds为被监听描述符的总数(通常为最大可分配的描述符值加1,因为从0开始)
    // 后面三个分别指向事件对应的描述符集合,每一次select内核都会修改这些集合,因此每次select之后都要用宏初始化它
    // timeout为0,select立即返回;为NULL则select一直阻塞直到某个fd就绪
    int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

    struct timeval {
    long tv_sec; // 秒级
    long tv_usec; // 微秒级
    };

    // fd_set结构体仅包含一个整型数组,每一位标记一个fd,能容纳的最大fd为1024
    FD_ZERO(fd_set* fdset); // 清除fdset的所有位
    FD_SET(int fd, fd_set* fdset); // 设置fdset的位fd
    FD_CLR(int fd, fd_set* fdset); // 清除fdset的位fd
    int FD_ISSET(int fd, fd_set* fdset); // 测试fdset的位fd是否被设置
  • poll系统调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <poll.h>
    // nfds为监听事件集合的大小,timeout为0立即返回,-1将阻塞直到有事件发生
    int poll(struct pollfd* fds, nfds_t nfds, int timeout);

    struct pollfd {
    int fd; // 事件fd(事件发生用于I/O的fd)
    short events; // 注册的事件
    short revents; // 实际发生的事件,由内核填充
    };
    • 事件类型
      • POLLIN,数据可读
      • POLLOUT,数据可写
      • POLLRDHUP,TCP连接被对方关闭或对方关闭了写操作
      • POLLHUP,挂起
      • POLLNVAL,文件描述符没有打开
  • epoll系统调用
    • 内核事件表,epoll将关心的fd放入内核事件表中,epoll需要额外的文件描述符来唯一标识内核事件表,即epoll_create返回的epollfd。通过函数回调的方式来通知服务器有事件发生(即便有了非阻塞I/O,只有在事件发生的情况下,调用非阻塞I/O才能提高程序的效率)。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      #include <sys/epoll.h>
      // size参数只是给内核一个提示告诉它内核事件表需要多大,返回的epollfd(非负fd)将用作其他所有epoll系统调用的第一个参数
      // 手册中明确写了,当返回的epfd不用时,要调用close关闭它
      int epoll_create(int size); // size参数在linux 2.6.8以后就被忽略了,但是必须要大于0

      // 操作内核事件表
      // op参数对fd上的事件进行操作:EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL
      // 并将改event参数表(添加/删除/改变)更新到epfd连接的内核事件表中
      int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

      struct epoll_event {
      __unint32_t events; // epoll事件
      epoll_data_t data; // 用户数据
      };

      typedef union epoll_data {
      void* ptr;
      int fd; // 用的最多
      uint32_t u32;
      uint64_t u64;
      } epoll_data_t;

      // 成功时返回就绪fd的个数,只需要遍历+events数组即可处理事件,时间复杂度O(1)
      // epoll_wait将就绪的事件(发生变化的fd)从内核事件表复制到第二个参数events中,maxevents限定events结构体的大小
      int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
  • epoll的ET和LT模式
    • LT模式即水平触发,上一次就绪未处理的fd,后续epoll_wait还是会像应用程序通知该事件(就绪fd从内核事件表中复制到第二个参数events数组中),直到事件被处理
    • ET模式即边沿触发,有事件发生应当立即处理,如果不处理后续epoll_wait将不会向应用程序通知该事件(很大程序上降低了一个epoll事件被重复触发的次数),因此需要一次性读完或者写完。这也决定了使用ET模式的fd必须是非阻塞的,如果fd是阻塞的,读写操作将因为没有后续事件的发生而一直处于阻塞状态
  • EPOLLONESHOT事件
    • 解决了在ET模式下,当一个fd数据未处理完,此时切换到另一个线程继续处理,这时出现了两个线程同时操纵一个fd的局面(我们期望的是一个socket连接在任一时刻都之被一个线程处理)。对于注册了EPOLLONESHOT事件的fd,os最多触发其上注册的一个读或写或异常事件,且只触发一次(需要再次注册该事件到fd上),这时候就不会出现一个fd被两个线程同时读的情况了。
  • C语言char data[10] = {0};实际上就是赋值10个0字符

信号(软件中断)

  • sigaction
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <signal.h>
    // signum为具体的信号(除了SIGKILL、SIGSTOP)
    // 如果oldact不为NULL,那么之前的action将会被保存到oldact中(传出参数)
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

    struct sigaction {
    // 信号处理函数
    void (*sa_handler)(int);
    // 不常用
    void (*sa_sigaction)(int, siginfo_t *, void *);
    // 临时阻塞的信号集,在调用处理程序handler时临时阻塞某些信号
    // 不允许这些信号中断处理程序handler的执行
    sigset_t sa_mask;
    // sa_flag指定是用第一个sa_handler还是sa_sigaction进行处理,使用0是sa_handler
    int sa_flags;
    // 被废弃掉了
    void (*sa_restorer)(void);
    };
  • 信号集,指定一组将由进程阻塞的信号集合
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 标志位为1表示阻塞这个信号
    #include <signal.h>
    // 清空信号集中的数据(即接收所有信号),所有标志位置为0,参数为传出参数set
    int sigemptyset(sigset_t *set);
    // 将信号集中的标志位都置为1(即阻塞所有信号),传出参数为set
    int sigfillset(sigset_t *set);
    // 往信号集中添加具体的信号,即阻塞某个具体的信号
    int sigaddset(sigset_t *set, int signum);

    int sigdelset(sigset_t *set, int signum);

netstat

  • 使用netstat -na | grep TIME_WAIT查看time wait状态下的连接

gcc

  • 参数
    • -c编译而不进行链接
    • -S进行汇编
    • -E进行预处理
    • -I指定头文件所在的目录的相对位置
    • -D参数表示程序编译的时候指定一个宏,比如在文件中有#ifdef DEBUG语句,如果编译时不携带宏,那么DEBUG宏的值就默认是0。这个参数方便进行调试,发布release版本时,配合#ifdef屏蔽掉log信息。
    • -w参数屏蔽所有的警告,有时候警告会导致程序崩溃
    • -Wall生成所有的警告(如声明但未使用的变量就会提示警告)
    • -l程序编译时指定使用的库名称
    • -L编译时指定搜索库的路径
    • -fpic生成位置无关代码
    • -shared生成共享目标文件
    • -std参数指定C方言,gcc默认方言为GNU C

静态库

  • 命名。linux下静态库的命名位libxxx.a
  • 制作。使用ar(archive)工具,将.o文件制作成一个静态库,ar用到的参数
    • -r。将所选文件添加到一个归档(archive)中
    • -c。(create)创建归档
    • -s。为这个文件插入一个索引
  • 在分发静态库时要将库文件和头文件一起分发

Makefile

  • Makefile其他规则一般都是为第一条规则服务的,如果规则用不到则不必执行。先检查依赖是否存在,再去执行相应的shell命令
  • 变量。注意自动变量只能在当前规则中使用
    • 自定义变量。变量名=变量值
    • 常用预定义变量。
      • AR:归档维护程序的名称,默认值为ar(制作静态库的工具)
      • CC:C编译器的名称,默认值为cc
      • CXX:C++编译器的名称,默认值为g++
      • $@: 获取目标的完整名称
      • $<: 获取第一个依赖文件的名称
      • $^: 获取所有的依赖文件
    • 获取变量的值
      • $(变量名)
      • %为通配符
    • .PHONY:clean表示clean是个伪目标,不会因为依赖而不执行clean

HTTP协议

  • 请求和响应的头以ASCII的形式给出;而消息内容具有一个类MIME的格式
  • 请求报文:请求行、请求头部、空行、请求数据
  • web服务器解析请求,定位请求资源
  • 响应报文:状态行、响应头部、空行、响应数据