《Linux高性能服务器编程》笔记01

时间:2024-01-22 12:50:32

Linux高性能服务器编程

本文是读书笔记,如有侵权,请联系删除。

参考

Linux高性能服务器编程源码: https://github.com/raichen/LinuxServerCodes

豆瓣: Linux高性能服务器编程

文章目录

  • Linux高性能服务器编程
    • 第05章 Linux网络编程基础API
      • 5.1 socket地址API
      • 5.2 创建socket
      • 5.3命名socket
      • 5.4 监听 socket
      • 5.5接受连接
      • 5.6发起连接
      • 5.7关闭连接
      • 5.8数据读写
        • 5.8.1 TCP数据读写
        • 5.8.2 UDP数据读写
        • 5.8.3通用数据读写函数
      • 5.9带外标记
      • 5.10地址信息函数
      • 5.11 socket选项
      • 5.12网络信息API
    • 后记

第05章 Linux网络编程基础API

□socket地址API。socket最开始的含义是一个IP地址和端口对(ip,port)。它唯一地 表示了使用TCP通信的一端。本书称其为socket地址。

□socket基础API。socket的主要API都定义在sys/socket.h头文件中,包括创建 socket、命名 socket、监听socket、接受连接、发起连接、读写数据、获取地址信息、检测带外标记,以及读取和设置socket选项。

□网络信息API。Linux提供了一套网络信息API,以实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。这些API都定义在netdb.h头文件中,我们将讨论其中几个主要的函数。

5.1 socket地址API

5.1.1 主机字节序和网络字节序

现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
当格式化的数据(比如32bit整型数和16bit短整型数)在两台使用不同字节序的主机之间直接传递时,接收端必然错误地解释之。解决问题的方法是:发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。因此大端字节序也称为网络字节序,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。

需要指出的是,即使是同一台机器上的两个进程(比如一个由C语言编写,另一个由JAVA编写)通信,也要考虑字节序的问题(JAVA虚拟机采用大端字节序)。
Linux提供了如下4个函数来完成主机字节序和网络字节序之间的转换:

#include <netinet/in.h>
unsigned long int htonl( unsigned long int hostlong ); 

unsigned short int htons( unsigned short int hostshort); 

unsigned long int ntohl( unsigned long int netlong ); 

unsigned short int ntohs( unsigned short int netshort );

它们的含义很明确,比如htonl表示“host to network long”,即将长整型(32bit)的主 机字节序数据转化为网络字节序数据。这4个函数中,长整型函数通常用来转换IP地址,短整型函数用来转换端口号(当然不限于此。任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序)。

这四个函数是用于处理网络字节序(Network Byte Order)和主机字节序(Host Byte Order)之间的转换,通常在网络编程中使用。这些函数在 <netinet/in.h> 头文件中声明,用于处理 32 位和 16 位整数值的字节序转换。

  1. unsigned long int htonl(unsigned long int hostlong):

    • 函数名:htonl 表示 “host to network long”.
    • 功能:将主机字节序的 32 位整数 hostlong 转换为网络字节序。
    • 返回值:返回一个无符号的长整数,表示网络字节序的值。
  2. unsigned short int htons(unsigned short int hostshort):

    • 函数名:htons 表示 “host to network short”.
    • 功能:将主机字节序的 16 位整数 hostshort 转换为网络字节序。
    • 返回值:返回一个无符号的短整数,表示网络字节序的值。
  3. unsigned long int ntohl(unsigned long int netlong):

    • 函数名:ntohl 表示 “network to host long”.
    • 功能:将网络字节序的 32 位整数 netlong 转换为主机字节序。
    • 返回值:返回一个无符号的长整数,表示主机字节序的值。
  4. unsigned short int ntohs(unsigned short int netshort):

    • 函数名:ntohs 表示 “network to host short”.
    • 功能:将网络字节序的 16 位整数 netshort 转换为主机字节序。
    • 返回值:返回一个无符号的短整数,表示主机字节序的值。

这些函数的使用非常重要,因为不同的系统和架构可能使用不同的字节序,而网络通信通常要求数据以网络字节序进行传输。通过使用这些函数,程序可以确保在网络和主机之间正确地进行字节序的转换,以避免通信问题。

5.1.2 通用 socket 地址
socket 网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:

#include <bits/socket.h>
struct sockaddr 
{
	sa_family_t sa_family; 
    char sa_data[14]; 
}

sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称domain,见后文)和对应的地址族如表5-1所示。

在这里插入图片描述

宏PF_*和AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用。sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和 长度,如表5-2所示。

在这里插入图片描述

由表5-2可见,14字节的sa_data根本无法完全容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用socket 地址结构体:

#include <bits/socket.h> 
struct sockaddr_storage
{
    sa_family_t sa_family; 
    unsigned long int __ss_align;
    char __ss_padding[128-sizeof (__ss_align )];
}

这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)。

5.1.3专用socket 地址

上面这两个通用socket地址结构体显然很不好用,比如设置与获取IP地址和端口号就需要执行烦琐的位操作。所以Linux为各个协议族提供了专门的socket 地址结构体。UNIX本地域协议族使用如下专用socket地址结构体:

#include <sys/un.h> 
struct sockaddr_un
{    
    sa_family_t sin_family; /*地址族:AF_UNIX*/ 
    char sun_path[108]; /*文件路径名*/
};

TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6:

在这里插入图片描述

这两个专用socket地址结构体各字段的含义都很明确,我们只在右边稍加注释。

所有专用socket 地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为 通用socket 地址类型sockaddr(强制转换即可),因为所有 socket编程接口使用的地址参数 的类型都是sockaddr。

5.1.4 IP地址转换函数

通常,人们习惯用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPv4地址,以及用十六进制字符串表示IPv6地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的IP地址转化为可读的字符串。下面3个函数可用于用点分十进制字符串表示的IPv4地址和用网络字节序整数表示的 IPv4地址之间的转换:

#include <arpa/inet.h>
in_addr_t inet_addr( const char* strptr );
int inet_aton( const char* cp, struct in_addr* inp ); 
char* inet_ntoa( struct in_addr in );

inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的 IPv4地址。它失败时返回INADDR_NONE。

inet _aton 函数完成和inet_addr 同样的功能,但是将转化结果存储于参数inp指向的地址 结构中。它成功时返回1,失败则返回0。

inet_ntoa 函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的 IPv4地址。但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向 该静态内存,因此inet _ntoa是不可重入的。

这三个函数是用于 IPv4 地址的字符串表示和二进制表示之间的转换,它们声明在 <arpa/inet.h> 头文件中。

  1. in_addr_t inet_addr(const char* strptr):

    • 函数名:inet_addr
    • 功能:将点分十进制表示的 IPv4 地址转换为二进制表示的 32 位整数。
    • 参数:
      • strptr:指向以字符串形式表示的 IPv4 地址的指针。
    • 返回值:返回一个 in_addr_t 类型的值,表示二进制形式的 IPv4 地址。如果转换失败,返回 INADDR_NONE(通常是 -1)。
  2. int inet_aton(const char* cp, struct in_addr* inp):

    • 函数名:inet_aton
    • 功能:将点分十进制表示的 IPv4 地址转换为二进制表示的 32 位整数,并存储在 struct in_addr 结构中。
    • 参数:
      • cp:指向以字符串形式表示的 IPv4 地址的指针。
      • inp:指向 struct in_addr 结构的指针,用于存储转换后的二进制形式的 IPv4 地址。
    • 返回值:如果转换成功,返回非零值;如果失败,返回零。
  3. char* inet_ntoa(struct in_addr in):

    • 函数名:inet_ntoa
    • 功能:将二进制表示的 32 位整数形式的 IPv4 地址转换为点分十进制表示的字符串形式。
    • 参数:
      • instruct in_addr 结构,包含要转换的二进制形式的 IPv4 地址。
    • 返回值:返回一个指向以字符串形式表示的 IPv4 地址的指针。请注意,返回的指针指向静态内存,因此在多次调用时要小心使用。

这些函数在网络编程中用于处理 IP 地址的表示,常用于套接字编程中的地址转换操作。当在网络通信中需要将字符串形式的 IP 地址与二进制形式进行转换时,这些函数是很有用的。

以下是这三个函数的简单使用例子:

  1. inet_addr:
#include <arpa/inet.h>
#include <stdio.h>

int main() {
    const char* ip_str = "192.168.1.1";
    
    // 将点分十进制表示的 IPv4 地址转换为二进制表示的整数
    in_addr_t ip_binary = inet_addr(ip_str);

    if (ip_binary == INADDR_NONE) {
        printf("Invalid IP address\n");
    } else {
        printf("Binary IP: %u\n", ip_binary);
    }

    return 0;
}
  1. inet_aton:
#include <arpa/inet.h>
#include <stdio.h>

int main() {
    const char* ip_str = "192.168.1.1";
    struct in_addr ip_binary;

    // 将点分十进制表示的 IPv4 地址转换为二进制表示的整数,并存储在结构体中
    if (inet_aton(ip_str, &ip_binary)) {
        printf("Binary IP: %u\n", ip_binary.s_addr);
    } else {
        printf("Invalid IP address\n");
    }

    return 0;
}
  1. inet_ntoa:
#include <arpa/inet.h>
#include <stdio.h>

int main() {
    struct in_addr ip_binary;
    ip_binary.s_addr = htonl(0xC0A80101); // 192.168.1.1 的二进制表示

    // 将二进制表示的 IPv4 地址转换为点分十进制表示的字符串
    char* ip_str = inet_ntoa(ip_binary);

    if (ip_str != NULL) {
        printf("IP Address: %s\n", ip_str);
    } else {
        printf("Conversion failed\n");
    }

    return 0;
}

这些例子演示了如何使用这三个函数进行 IPv4 地址的字符串表示和二进制表示之间的转换。请注意,在实际应用中,应该检查转换的有效性,避免出现错误。

下面这对更新的函数也能完成和前面3个函数同样的功能,并且它们同时适用于IPv4 地址和IPv6地址:

#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 cnt );

inet_pton函数将用字符串表示的IP地址src(用点分十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结 果存储于dst指向的内存中。其中,af参数指定地址族,可以是AF_INET或者AF_INET6。inet _pton 成功时返回1,失败则返回0并设置 errno。

inet _ntop 函数进行相反的转换,前三个参数的含义与 inet_pton 的参数相同,最后一个 参数cnt指定目标存储单元的大小。下面的两个宏能帮助我们指定这个大小(分别用于IPv4 和IPv6):

#include <netinet/in.h> 
#define INET ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

inet_ntop 成功时返回目标存储单元的地址,失败则返回NULL并设置 errno。

这两个函数也是用于进行IPv4和IPv6地址之间的转换,它们声明在 <arpa/inet.h> 头文件中。这些函数更灵活且适用于IPv4和IPv6地址。

  1. inet_pton:
#include <arpa/inet.h>
#include <stdio.h>

int main() {
    const char* ip_str = "192.168.1.1";
    struct in_addr ipv4_addr;
    struct in6_addr ipv6_addr;

    // 将点分十进制表示的IPv4或IPv6地址转换为二进制表示
    if (inet_pton(AF_INET, ip_str, &ipv4_addr) > 0) {
        // 转换成功,ipv4_addr 包含二进制表示的IPv4地址
        printf("Binary IPv4 Address: %u\n", ipv4_addr.s_addr);
    } else if (inet_pton(AF_INET6, ip_str, &ipv6_addr) > 0) {
        // 转换成功,ipv6_addr 包含二进制表示的IPv6地址
        printf("Binary IPv6 Address: ... (output not shown)\n");
    } else {
        // 转换失败
        printf("Invalid IP address\n");
    }

    return 0;
}
  1. inet_ntop:
#include <arpa/inet.h>
#include <stdio.h>

int main() {
    struct in_addr ipv4_addr;
    struct in6_addr ipv6_addr;

    // 假设ipv4_addr 和 ipv6_addr 包含有效的二进制表示的IPv4和IPv6地址

    char ipv4_str[INET_ADDRSTRLEN];
    char ipv6_str[INET6_ADDRSTRLEN];

    // 将二进制表示的IPv4或IPv6地址转换为点分十进制表示的字符串
    const char* ipv4_str_ptr = inet_ntop(AF_INET, &ipv4_addr, ipv4_str, INET_ADDRSTRLEN);
    const char* ipv6_str_ptr = inet_ntop(AF_INET6, &ipv6_addr, ipv6_str, INET6_ADDRSTRLEN);

    if (ipv4_str_ptr != NULL) {
        // 转换成功,ipv4_str 包含点分十进制表示的IPv4地址
        printf("IPv4 Address: %s\n", ipv4_str);
    } else {
        // 转换失败
        printf("Conversion to IPv4 failed\n");
    }

    if (ipv6_str_ptr != NULL) {
        // 转换成功,ipv6_str 包含点分十进制表示的IPv6地址
        printf("IPv6 Address: %s\n", ipv6_str);
    } else {
        // 转换失败
        printf("Conversion to IPv6 failed\n");
    }

    return 0;
}

这两个函数相对于之前提到的 inet_addrinet_atoninet_ntoa 更为通用,可以用于IPv4和IPv6地址的转换。它们支持传递地址族参数,允许处理不同类型的地址。

5.2 创建socket

UNIX/Linux的一个哲学是:所有东西都是文件。socket也不例外,它就是可读、可写、 可控制、可关闭的文件描述符。下面的socket 系统调用可创建一个socket:

#include <sys/types.h> 
#include <sys/socket.h>
int socket( int domain, int type, int protocol );

domain参数告诉系统使用哪个底层协议族。对TCP/IP协议族而言,该参数应该设置为 PF_INET(Protocol Family of Internet,用于IPv4)或PF_INET6(用于IPv6);对于UNIX 本地域协议族而言,该参数应该设置为PF_UNIX。

type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_ UGRAM(数据报)服务。对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用 TCP协议,取SOCK_DGRAM表示传输层使用UDP协议。

值得指出的是,自Linux内核版本2.6.17起,type参数可以接受上述服务类型与下面两个重要的标志相与的值:SOCK_NONBLOCK和SOCK_CLOEXEC。它们分别表示将新创 建的socket设为非阻塞的,以及用fork调用创建子进程时在子进程中关闭该socket。在内 核版本2.6.17之前的Linux中,文件描述符的这两个属性都需要使用额外的系统调用(比如 fcntl)来设置。

protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值 通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,我们都应该把 它设置为0,表示使用默认协议。

socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。

这函数是用于创建套接字(socket)的系统调用,声明在 <sys/types.h><sys/socket.h> 头文件中。

int socket(int domain, int type, int protocol);
  • 参数

    • domain:指定协议族(address family),例如 AF_INET 表示IPv4,AF_INET6 表示IPv6,AF_UNIX 表示本地通信等。常见的值有 AF_INETAF_INET6
    • type:指定套接字的类型,例如 SOCK_STREAM 表示流套接字(TCP),SOCK_DGRAM 表示数据报套接字(UDP),SOCK_RAW 表示原始套接字等。
    • protocol:指定具体的协议,通常设置为 0,表示使用默认协议。对于 SOCK_STREAMSOCK_DGRAM 类型的套接字,通常设置为 IPPROTO_TCPIPPROTO_UDP
  • 返回值

    • 如果成功,返回一个非负整数,表示新创建的套接字的文件描述符。
    • 如果失败,返回 -1,并设置全局变量 errno 表示错误的类型。
  • 示例

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>

int main() {
    // 创建一个IPv4 TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    if (sockfd == -1) {
        perror("socket creation failed");
        return -1;
    }

    // 使用套接字...

    // 关闭套接字
    close(sockfd);

    return 0;
}

这个例子创建了一个基于IPv4的TCP套接字。程序首先调用 socket 函数创建套接字,然后可以使用返回的文件描述符进行通信,最后通过 close 函数关闭套接字。在实际应用中,根据需要选择合适的协议族、套接字类型和协议。

5.3命名socket

创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体 socket 地址。将一个socket与socket 地址绑定称为给socket命名。在服务器程序中,我们通 常要命名socket,因为只有命名后客户端才能知道该如何连接它。客户端则通常不需要命名 socket,而是采用匿名方式,即使用操作系统自动分配的socket 地址。命名socket的系统调 用是bind,其定义如下:

#include <sys/types.h> 
#include <sys/socket.h>
int bind( int sockfd, const struct sockaddr* my_addr, socklen_t addrlen );

bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指 出该socket地址的长度。

bind成功时返回0,失败则返回-1并设置errno。其中两种常见的errno是EACCES和 EADDRINUSE,它们的含义分别是:

  1. EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将 socket绑定到知名服务端口(端口号为O~1023)上时,bind将返回EACCES错误。

  2. EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_ WAIT状态的socket 地址。

这是用于将套接字(socket)与特定的地址(通常是 IP 地址和端口号)绑定的系统调用,声明在 <sys/types.h><sys/socket.h> 头文件中。

int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
  • 参数

    • sockfd:套接字的文件描述符,由 socket 调用返回。
    • my_addr:指向 struct sockaddr 结构的指针,包含要绑定的地址信息。
    • addrlen:指定 my_addr 结构的大小。
  • 返回值

    • 如果成功,返回 0。
    • 如果失败,返回 -1,并设置全局变量 errno 表示错误的类型。
  • 示例

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>

int main() {
    // 创建一个IPv4 TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    if (sockfd == -1) {
        perror("socket creation failed");
        return -1;
    }

    // 准备地址结构
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);  // 设置端口号
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 绑定到所有可用的网络接口

    // 将套接字绑定到地址
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(sockfd);
        return -1;
    }

    // 使用绑定的套接字进行通信...

    // 关闭套接字
    close(sockfd);

    return 0;
}

这个例子首先创建了一个基于IPv4的TCP套接字,然后准备了一个 struct sockaddr_in 结构表示要绑定的地址。接着使用 bind 函数将套接字与该地址绑定。如果绑定成功,接下来就可以使用这个套接字进行通信。绑定是在服务器端创建套接字时的重要步骤,确保服务器能够监听到来自指定地址和端口的连接请求。

5.4 监听 socket

socket被命名之后,还不能马上接受客户连接,我们需要使用如下系统调用来创建一个 监听队列以存放待处理的客户连接:

#include <sys/socket.h>
int listen( int sockfd, int backlog );

sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。监 听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到 ECONNREFUSED错误信息。在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(SYN RCVD)和完全连接状态(ESTABLISHED)的socket的上限。但自内核版 本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限 则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。backlog参数的典型值是5。

listen 成功时返回0,失败则返回-1并设置errno。

这是用于在套接字上监听连接请求的系统调用,声明在 <sys/socket.h> 头文件中。

int listen(int sockfd, int backlog);
  • 参数

    • sockfd:套接字的文件描述符,由 socket 调用返回,并通过 bind 函数绑定到一个地址。
    • backlog:指定在等待连接队列中允许的未完成连接的最大数量。未完成连接是指已经收到客户端连接请求,但还没有通过 accept 函数完成的连接。
  • 返回值

    • 如果成功,返回 0。
    • 如果失败,返回 -1,并设置全局变量 errno 表示错误的类型。
  • 示例

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>

int main() {
    // 创建一个IPv4 TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    if (sockfd == -1) {
        perror("socket creation failed");
        return -1;
    }

    // 准备地址结构
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);  // 设置端口号
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 绑定到所有可用的网络接口

    // 将套接字绑定到地址
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(sockfd);
        return -1;
    }

    // 开始监听连接请求,backlog 指定最大未完成连接队列长度
    if (listen(sockfd, 5) == -1) {
        perror("listen failed");
        close(sockfd);
        return -1;
    }

    // 等待接受连接请求...

    // 关闭套接字
    close(sockfd);

    return 0;
}

在这个例子中,listen 函数被用于开始监听连接请求。它告诉操作系统,套接字 sockfd 现在可以接受连接请求,并指定了最大未完成连接队列的长度为 5。未完成连接队列中的连接请求会在之后通过 accept 函数来接受和处理。在服务器端创建套接字后,通常会依次调用 bindlistenaccept 函数来准备接受客户端的连接。

下面我们编写一个服务器程序,以研究backlog参数对listen系统 调用的实际影响。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>