Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记

文章目录

  • Linux高性能服务器编程(第二篇 深入解析高性能服务器编程)
    • 第5章 Linux网络编程基础API
      • 1. socket地址API
      • 2. 创建socket
      • 3. 命名socket
      • 4. 监听socket
      • 5. 接受连接
      • 6. 发起连接
      • 7. 关闭连接
      • 8. 数据读写
      • 9. 带外标记
      • 10. 地址信息函数
      • 11. socket选项
      • 12. 网络信息API
    • 第6章 高级I/O函数
      • 1. pipe函数
      • 2. dup函数和dup2函数
      • 3. readv和writev函数
      • 4. sendfile函数
      • 5. mmap和munmap函数
      • 6. splice函数
      • 7. tee函数
      • 8. fcntl函数
    • 第7章 Linux服务器程序规范
      • 1. 日志
      • 2. 用户信息
      • 3. 进程间关系
      • 4. 系统资源限制
      • 5. 改变工作目录和根目录
      • 6. 服务器程序后台化
    • 第八章 高性能服务器程序框架
      • 1. 服务器模型
      • 2. 服务器编程框架
      • 3. I/O模型
      • 4. 两种高效的事件处理模式
      • 5. 两种高效的并发模式
      • 6. 有限状态机
      • 7. 提高服务器性能的其它建议

Linux高性能服务器编程(第二篇 深入解析高性能服务器编程)


第5章 Linux网络编程基础API

1. socket地址API

  • 主机字节序和网络字节序:32位机一次性装载4字节,字节排列顺序影响装载后整数的值。现代PC大多采用小端字节序,因此小端字节序也称主机字节序。发送端统一发送大端字节序,因此大端字节序也称网络字节序
/**
 * g++ chapter5/5.1_byteorder.cpp -o out.app && ./out.app
 * 
 * 代码5-1 判断机器字节序
 */

#include 
using namespace std;

// 字节序转换函数:
#include 
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostlong);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

int main()
{

    union {
        short value;
        char union_bytes[sizeof(short)];
    } test;

    test.value = 0x0102;

    if ((test.union_bytes[0] == 1) && (test.union_bytes[1] == 2))
    {
        cout << "Big Endian" << endl;
    }
    else if ((test.union_bytes[0] == 2) && (test.union_bytes[1] == 1))
    {
        cout << "Little Endian" << endl; //实际输出
    }
    else
    {
        cout << "Unknown..." << endl;
    }

    return 0;
}

2. 创建socket

  • Linux中一切皆文件,socket也不例外
#include 
#include 
// domain: IPv4设置为PF_INET,IPv6设置为PF_INET6
// type: TCP为SOCK_STREAM,UDP为SOCK_DGRAM
// protocol: 给定前两个参数时选择一个具体协议,通常是唯一的(前两个参数已经完全决定),默认设为0
// 调用成功时返回socket文件描述符,失败返回-1并设置errno
int socket(int domain, int type, int protocol);

3. 命名socket

  • 创建socket时指定了协议族,但未指定具体地址,将一个socket与socket地址绑定称为给socket命名;服务器需要命名socket,但客户端通常匿名,由操作系统自动分配socket地址。
#include 
#include 
// 成功返回0,失败返回-1并设置errno。最常见的两种errno:
// EACCESS:被绑定的是受保护地址(普通用户绑定端口号0~1023)
// EADDRINUSE: 被绑定的地址正在使用中。
int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addrlen);

4. 监听socket

  • 服务器要创建监听队列存放待处理的客户连接
  • 完整连接最多有 b a c k l o g + 1 backlog+1 backlog+1
#include 
#include 
// backlog: 监听队列的最大长度,完全连接状态的socket的上限
int listen(int sockfd,int backlog);
/**
 * g++ chapter5/5.4_testlisten.cpp -o testlisten.app && ./testlisten.app 127.0.0.1 12345 3
 * 
 * telnet 127.0.0.1 12345 # 多次执行
 * 
 * netstat -nt | grep 12345
 * 
 * 代码 5-3 backlog参数
 */

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

static bool stop = false;

static void handle_term(int sig)
{
    stop = true;
}

int main(int argc, char *argv[])
{
    signal(SIGTERM, handle_term);
    if (argc <= 3)
    {
        cout << "Usage: " << argv[0] << " IP port backlog" << endl;
        return 1;
    }
    const char *ip = argv[1];
    int port = atoi(argv[2]);
    int backlog = atoi(argv[3]);

    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock > 0);
    // 创建一个IPv4 socket地址
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    // 命名socket
    int ret = bind(sock, (struct sockaddr *)&address, sizeof(address));
    assert(ret != -1);

    // 开始监听
    cout << "backlog : " << backlog << endl;
    ret = listen(sock, backlog);
    assert(ret != -1);

    while (!stop)
    {
        sleep(1);
    }
    //关闭socket
    close(sock);

    return 0;
}
  • 实验结果:
    Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第1张图片

5. 接受连接

  • accept只从监听队列中取出连接,不论连接处于何种状态,不关心网络状况变化。
/**
 * g++ chapter5/5.5_testaccept.cpp -o testaccept.app && ./testaccept.app 127.0.0.1 12345
 * 
 * telnet 127.0.0.1 12345
 * 
 * netstat -nt | grep 12345
 * 
 * 代码 5-5 接受一个异常连接
 */

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main(int argc, char *argv[])
{
    if (argc < 3)
    {
        cout << "Usage: " << argv[0] << " IP port" << endl;
        return 1;
    }
    const char *ip = argv[1];
    int port = atoi(argv[2]);

    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock > 0);
    // 创建一个IPv4 socket地址
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    // 命名socket
    int ret = bind(sock, (struct sockaddr *)&address, sizeof(address));
    assert(ret != -1);

    // 开始监听
    ret = listen(sock, 3);
    assert(ret != -1);

    // 暂停,等待客户端操作结束
    sleep(20);

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int conn_fd = accept(sock, (struct sockaddr *)&client, &client_addrlength);
    if (conn_fd < 0)
    {
        cout << "Error:" << errno << endl;
    }
    else
    { //连接成功,打印客户端端口号和IP
        char remote[INET_ADDRSTRLEN];
        printf("Connected IP: %s ,Port: %d\n", inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port));
        close(conn_fd);
    }

    //关闭socket
    close(sock);

    return 0;
}
  • 实验结果
    实验结果
    实验结果

6. 发起连接

#include 
#include 
// 成功返回0,失败返回-1并设置errno
int connect(int sockfd,const struct sockaddr* serv_addr,socklen_t addrlen);

7. 关闭连接

#include 
// close是将fd的引用计数减一,当fd的引用计数为0时才会真正关闭;在多进程程序中,一次fork默认会将父进程打开的socket的引用计数加一,因此父子进程都要关闭该socket。
int close(int fd);

// 如果要立即终止连接,而不是引用计数减一,使用shutdown。
// howto: SHUT_RD,只关闭读,接收缓冲区数据全部丢弃;SHUT_WR,关闭写,程序不能继续写,缓冲区数据会发送完,此时处于半连接状态;SHUT_RDWR,同时关闭读写。
#include
int shutdown(int sockfd,int howto);

8. 数据读写

#include 
#include 

// TCP读写
// recv返回实际长度,可能小于期望长度;返回0:对方已关闭;返回-1:出错
ssize_t recv(int sockfd,void* buf,size_t len,int flags);
//返回实际发送数据
ssize_t send(int sockfd,const void* buff,size_t len,int flags);

// UDP读写
// UDP是无连接的,读写都需要指定对方地址
ssize_t recvfrom(int sockfd,void* buf,size_t len,int flags,struct sockaddr* src_addr,socklen_t* addrlen);
ssize_t sendto(int sockfd,const void* buf,size_t len,int flags,const struct sockaddr* dest_addr,socklen_t* addrlen);

// 通用数据读写
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);

Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第2张图片

9. 带外标记

#include
//检测下一个被读到的数据是否是带外数据
int sockatmark(int cockfd);

10. 地址信息函数

#include
// 获取本地socket地址和对方socket地址
int getsockname(int sockfd,struct sockaddr* address, socklen_t* address_len);
int getpeername(int sockfd,struct sockaddr* address, socklen_t* address_len);

11. socket选项

#include
int getsockopt(int sockfd,int level, int option_name, void* option_value,socklen_t* restrict option_len);
int setsockopt(int sockfd,int level, int option_name, const void* option_value,socklen_t option_len);

Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第3张图片

12. 网络信息API

  • gethostbyname
  • gethostbyaddr
  • getservnyname
  • getservbyport
  • getaddrinfo
  • getnameinfo

第6章 高级I/O函数

1. pipe函数

#include
// 创建一个单向管道,实现进程间通信
// fd[1]只能写,fd[0]只能读
int pipe(int fd[2]);
#include 
#include 
// 创建一个双向管道,domain只能是AF_UNIX
int socketpair(int domain,int type,int protocol, int fd[2]);

2. dup函数和dup2函数

/**
 * g++ chapter6/6.2_cgi.cpp -o cgi.app && ./cgi.app 127.0.0.1 12345
 * 
 * telnet 127.0.0.1 12345
 * 
 * 代码 6-1 CGI服务器原理
 */

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main(int argc, char *argv[])
{
    if (argc < 3)
    {
        cout << "Usage: " << argv[0] << " IP port" << endl;
        return 1;
    }
    const char *ip = argv[1];
    int port = atoi(argv[2]);

    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock > 0);
    // 创建一个IPv4 socket地址
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    // 命名socket
    int ret = bind(sock, (struct sockaddr *)&address, sizeof(address));
    assert(ret != -1);

    // 开始监听
    ret = listen(sock, 3);
    assert(ret != -1);

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int conn_fd = accept(sock, (struct sockaddr *)&client, &client_addrlength);
    if (conn_fd < 0)
    {
        cout << "Error:" << errno << endl;
    }
    else
    { //连接成功
        close(STDOUT_FILENO);
        // dup创建新的文件描述符,它与原有文件描述符指向相同的文件
        // 总是返回系统中最小的可用文件描述符,所以返回值实际是1,即STDOUT_FILENO
        // dup2函数返回第一个不小于file_descriptor_two的整数值
        int new_fd = dup(conn_fd);
        cout << "fd=" << new_fd << endl;
        cout << "hello!" << endl;

        close(conn_fd);
    }

    //关闭socket
    close(sock);

    return 0;
}
  • 实验结果
    Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第4张图片

3. readv和writev函数

#include
// 分散读写,一次读写多个内存块
ssize_t readv(int fd,const struct iovec* vector,int count);
ssize_t writev(int fd,const struct iovec* vector,int count);

4. sendfile函数

#include
// 直接在两个文件描述符之间传递数据(完全在内核中操作),
// 避免了内核与用户缓冲区之间的数据拷贝,效率很高,称为零拷贝。
// in_fd必须指向真实文件,out_fd必须指向socket,
// offset指定从哪个位置开始拷贝,count指定传输数据量
ssize_t sendfile(int out_fd, int in_fd,off_t* offset,size_t count);

5. mmap和munmap函数

  • mmap用于申请一段内存空间,我们可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中。
  • munmap释放这段空间。

6. splice函数

在两个文件描述符之间移动数据,也是零拷贝操作。

7. tee函数

在两个管道文件描述符之间复制数据,也是零拷贝操作,但不消耗数据。

8. fcntl函数

提供了对文件描述符的各种控制操作。在网络编程中,通常用来讲一个文件描述符设置为非阻塞的


第7章 Linux服务器程序规范

1. 日志

Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第5张图片

#include
void openlog(const char* ident,int logopt,int facility);
int setlogmask(int maskpri);//设置日志级别,用于日志过滤
void syslog(int priority,const char* message,...);
void closelog();

2. 用户信息

  • UID:真实用户ID
  • EUID:有效用户ID,实际访问资源时使用的用户
  • GID
  • EGID
/**
 * g++ chapter7/7.1_test_uid.cpp -o test_uid.app
 * 
 * sudo chown root:root test_uid.app
 * #设置目标文件的set-user-id标志,使得运行程序的用户拥有该程序有效用户的权限
 * sudo chmod +s test_uid.app 
 * 
 * ./test_uid.app
 * 
 * 代码 7-1 测试进程的UID和EUID
 */

#include 
#include 
using namespace std;

int main()
{
    uid_t uid = getuid();
    uid_t euid = geteuid();
    cout << "UID: " << uid << " EUID: " << euid << endl;
    return 0;
}

Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第6张图片

3. 进程间关系

  • 进程组:每个程序都隶属于一个进程组,因此它们除了PID信息外还有进程组PGID;首领进程的PGID与其PID相同
    Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第7张图片

4. 系统资源限制

Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第8张图片

5. 改变工作目录和根目录

#include 
// 如果buf空间不够,则返回NULL;如果buf为NULL,则返回内部malloc的内存,要自己释放
char* getcwd(char* buf,size_t size);
int chdir(const char* path);
// 切换根目录,只有特权进程能切换根目录
int chroot(const char* path);

6. 服务器程序后台化


第八章 高性能服务器程序框架

1. 服务器模型

  • C/S模型
  • P2P模型
    Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第9张图片

2. 服务器编程框架

Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第10张图片

3. I/O模型

Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第11张图片

4. 两种高效的事件处理模式

  • 三类事件:I/O事件,信号事件,定时事件
    Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第12张图片
    Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第13张图片

5. 两种高效的并发模式

  • 半同步/半异步模式
    Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第14张图片
    Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第15张图片
  • 领导者/追随者模式
    Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记_第16张图片

6. 有限状态机

7. 提高服务器性能的其它建议

  • 池:空间换时间;静态分配,动态分配;内存池,进程池,线程池,连接池
  • 数据复制:零拷贝;共享内存;避免内存拷贝
  • 上下文切换和锁:进程线程切换的系统开销大;避免锁,减小锁粒度

你可能感兴趣的:(Linux高性能服务器编程(第二篇 第5-8章)——阅读笔记)