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

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 
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)之间的转换,通常在网络编程中使用。这些函数在 头文件中声明,用于处理 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 
struct sockaddr 
{
	sa_family_t sa_family; 
    char sa_data[14]; 
}

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

《Linux高性能服务器编程》笔记01_第1张图片

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

《Linux高性能服务器编程》笔记01_第2张图片

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

#include  
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  
struct sockaddr_un
{    
    sa_family_t sin_family; /*地址族:AF_UNIX*/ 
    char sun_path[108]; /*文件路径名*/
};

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

《Linux高性能服务器编程》笔记01_第3张图片

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

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

5.1.4 IP地址转换函数

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

#include 
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 地址的字符串表示和二进制表示之间的转换,它们声明在 头文件中。

  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 
#include 

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 
#include 

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 
#include 

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 
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  
#define INET ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

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

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

  1. inet_pton:
#include 
#include 

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 
#include 

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  
#include 
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)的系统调用,声明在 头文件中。

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 
#include 
#include 

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  
#include 
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 地址和端口号)绑定的系统调用,声明在 头文件中。

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 
#include 
#include 
#include 

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 
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。

这是用于在套接字上监听连接请求的系统调用,声明在 头文件中。

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

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

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

#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

static bool stop = false;

// 信号处理函数,当接收到 SIGTERM 信号时将 stop 设置为 true
static void handle_term(int sig) {
    stop = true;
}

int main(int argc, char* argv[]) {
    // 注册 SIGTERM 信号的处理函数
    signal(SIGTERM, handle_term);

    // 检查命令行参数是否足够
    if (argc <= 3) {
        printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
        return 1;
    }

    // 解析命令行参数
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    int backlog = atoi(argv[3]);

    // 创建一个 TCP 套接字
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    // 初始化地址结构
    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);

    // 将套接字绑定到指定的地址
    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    // 开始监听连接请求,backlog 指定最大未完成连接队列长度
    ret = listen(sock, backlog);
    assert(ret != -1);

    // 循环等待 SIGTERM 信号,直到收到信号时停止循环
    while (!stop) {
        sleep(1);
    }

    // 关闭套接字
    close(sock);
    return 0;
}

代码作用解释

  • 该程序是一个简单的TCP服务器,通过命令行参数指定IP地址、端口号和监听队列长度。
  • 创建一个TCP套接字,将其绑定到指定的IP地址和端口号上,并开始监听连接请求。
  • 通过注册信号处理函数,当收到SIGTERM信号时,将stop标志设置为true,从而退出循环,最终关闭套接字。
  • 通过循环等待SIGTERM信号,该服务器会一直监听连接请求直到接收到SIGTERM信号。
  • 在SIGTERM信号处理函数中设置stop标志,使得程序退出循环,关闭套接字并结束程序。

5.5接受连接

下面的系统调用从listen监听队列中接受一个连接:

#include  
#include 
int accept( int sockfd, struct sockaddr *addr, socklen_t *addrlen );

sockfd参数是执行过 listen系统调用的监听 socket。addr参数用来获取被接受连接的远 端socket地址,该socket地址的长度由addrlen参数指出。accept成功时返回一个新的连接 socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受 连接对应的客户端通信。accept失败时返回-1并设置errno。

这函数用于在已经监听的套接字上接受客户端的连接请求,声明在 头文件中。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 参数

    • sockfd:监听套接字的文件描述符,通过 socketbindlisten 函数创建。
    • addr:指向 struct sockaddr 结构的指针,用于存储客户端的地址信息。
    • addrlen:指向 socklen_t 类型的指针,指定 addr 结构的大小。
  • 返回值

    • 如果成功,返回一个新的套接字文件描述符,该套接字用于与客户端通信。
    • 如果失败,返回 -1,并设置全局变量 errno 表示错误的类型。
  • 作用

    • 当服务器监听套接字收到客户端的连接请求时,调用 accept 函数将创建一个新的套接字,用于与客户端进行通信。这个新的套接字通常是专门为与特定客户端通信而创建的。
    • accept 函数还会返回客户端的地址信息,可以通过传入的 addr 参数获取,addrlen 参数指定了 addr 结构的大小。
  • 示例

#include 
#include 
#include 
#include 
#include 

int main() {
    // 创建一个IPv4 TCP套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listen_sock == -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(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(listen_sock);
        return -1;
    }

    // 开始监听连接请求
    if (listen(listen_sock, 5) == -1) {
        perror("listen failed");
        close(listen_sock);
        return -1;
    }

    // 接受客户端连接请求
    struct sockaddr_in client_addr;
    socklen_t client_addrlen = sizeof(client_addr);
    int client_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &client_addrlen);
    if (client_sock == -1) {
        perror("accept failed");
        close(listen_sock);
        return -1;
    }

    // 使用 client_sock 进行与客户端的通信...

    // 关闭套接字
    close(listen_sock);
    close(client_sock);

    return 0;
}

在这个例子中,accept 函数用于接受客户端的连接请求,创建了一个新的套接字 client_sock,用于与客户端进行通信。客户端的地址信息被存储在 client_addr 结构中。

现在考虑如下情况:如果监听队列中处于ESTABLISHED状态的连接对应的客户端出现网络异常(比如掉线),或者提前退出,那么服务器对这个连接执行的accept调用是否成功?我们编写一个简单的服务器程序来测试之:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[]) {
    // 检查命令行参数是否足够
    if (argc <= 2) {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }

    // 从命令行参数获取 IP 地址和端口号
    const char* ip = argv[1];
    int port = atoi(argv[2]);

    // 初始化服务器地址结构
    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);

    // 创建一个 TCP 套接字
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    // 将套接字绑定到指定的地址
    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    // 开始监听连接请求,backlog 指定最大未完成连接队列长度
    ret = listen(sock, 5);
    assert(ret != -1);

    // 接受客户端连接请求
    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);

    if (connfd < 0) {
        // 处理连接错误
        printf("errno is: %d\n", errno);
    } else {
        // 打印客户端连接信息
        char remote[INET_ADDRSTRLEN];
        printf("connected with ip: %s and port: %d\n",
               inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN),
               ntohs(client.sin_port));

        // 关闭与客户端的连接
        close(connfd);
    }

    // 关闭监听套接字
    close(sock);
    return 0;
}

代码作用解释

  • 该程序是一个简单的TCP服务器,通过命令行参数指定IP地址和端口号。
  • 创建一个TCP套接字,将其绑定到指定的IP地址和端口号上,并开始监听连接请求。
  • 当有客户端连接时,通过accept函数接受连接请求,打印客户端的IP地址和端口号,并立即关闭连接。
  • 如果有错误发生,打印相应的错误码。

我们在Kongming20上运行该服务器程序(名为testaccept),并在ernest-laptop上执行 telnet 命令来连接该服务器程序。具体操作过程如下:

$./testaccept 192.168.1.109 54321#监听54321端ロ

$ telnet 192.168.1.109 54321

启动telnet客户端程序后,立即断开该客户端的网络连接(建立和断开连接的过程要在 服务器启动后20秒内完成)。结果发现accept调用能够正常返回,服务器输出如下:

connected with ip: 192.168.1.108 and port: 38545

接着,在服务器上运行netstat命令以查看accept返回的连接socket的状态:

$ netstat -nt | grep 54321

在这里插入图片描述

netstat命令的输出说明,accept调用对于客户端网络断开毫不知情。下面我们重新执行 上述过程,不过这次不断开客户端网络连接,而是在建立连接后立即退出客户端程序。这次 accept调用同样正常返回,服务器输出如下:

connected with ip: 192.168.1.108 and port: 52070

再次在服务器上运行netstat命令:

$ netstat -nt | grep 54321

在这里插入图片描述

由此可见,accept只是从监听队列中取出连接,而不论连接处于何种状态(如上面的 ESTABLISHED状态和CLOSE_WAIT状态),更不关心任何网络状况的变化。

5.6发起连接

如果说服务器通过listen调用来被动接受连接,那么客户端需要通过如下系统调用来主 动与服务器建立连接:

#include  
#include 
int connect( int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen ); 

sockfd参数由 socket系统调用返回一个socket。serv_addr参数是服务器监听的socket 地址,addrlen参数则指定这个地址的长度。

connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就 可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno。其中两种常见的 errno是ECONNREFUSED和ETIMEDOUT,它们的含义如下:

ECONNREFUSED,目标端口不存在,连接被拒绝。

ETIMEDOUT,连接超时。

这是用于客户端建立与服务器端连接的系统调用,声明在 头文件中。

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
  • 参数

    • sockfd:套接字的文件描述符,通过 socket 调用创建。
    • serv_addr:指向服务器地址信息的 struct sockaddr 结构的指针。
    • addrlen:指定 serv_addr 结构的大小。
  • 返回值

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

    • connect 函数用于在客户端建立与服务器的连接。客户端通过传入服务器的地址信息(IP地址和端口号)来连接服务器。
    • 如果连接成功,返回值为 0,客户端之后可以通过返回的套接字文件描述符进行与服务器的通信。
    • 如果连接失败,返回值为 -1,错误信息可以通过查看全局变量 errno 获取。
  • 示例

#include 
#include 
#include 
#include 
#include 

int main() {
    // 创建一个IPv4 TCP套接字
    int client_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (client_sock == -1) {
        perror("socket creation failed");
        return -1;
    }

    // 准备服务器地址结构
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);  // 服务器端口号
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);  // 服务器IP地址

    // 连接到服务器
    if (connect(client_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connection failed");
        close(client_sock);
        return -1;
    }

    // 连接成功后,可以使用 client_sock 进行与服务器的通信...

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

    return 0;
}

在这个例子中,connect 函数用于连接到服务器。首先创建一个客户端套接字,然后准备服务器的地址结构,最后调用 connect 函数发起连接请求。如果连接成功,客户端之后可以使用返回的套接字文件描述符进行与服务器的通信。

5.7关闭连接

关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述 符的系统调用来完成:

#include 
int close( int fd );

fd参数是待关闭的socket。不过,close系统调用并非总是立即关闭一个连接,而是将fd 的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程 中都对该socket 执行close调用才能将连接关闭。

如果无论如何都要立即终止连接(而不是将socket的引用计数减l),可以使用如下的 shutdown系统调用(相对于close来说,它是专门为网络编程设计的):

#include 
int shutdown( int sockfd, int howto );

sockfd参数是待关闭的socket。howto参数决定了shutdown的行为,它可取表5-3中的某个值。

《Linux高性能服务器编程》笔记01_第4张图片

由此可见,shutdown能够分别关闭socket上的读或写,或者都关闭。而close在关闭连 接时只能将socket 上的读和写同时关闭。shutdown成功时返回0,失败则返回-1并设置errno。

5.8数据读写

5.8.1 TCP数据读写

对文件的读写操作read和write 同样适用于socket。但是socket编程接口提供了几个专 门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读 写的系统调用是:

#include  
#include 
ssize_t recv( int sockfd, void *buf, size_t len, int flags ); 
ssize_t send( int sockfd, const void *buf, size_t len, int flags );

recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags参数 的含义见后文,通常设置为0即可。recv成功时返回实际读取到的数据的长度,它可能小于 我们期望的长度len。因此我们可能要多次调用recv,才能读取到完整的数据。recv可能返回 0,这意味着通信对方已经关闭连接了。recv出错时返回-1并设置errno。

send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。send成功 时返回实际写人的数据的长度,失败则返回-1并设置errno。

fags参数为数据收发提供了额外的控制,它可以取表5-4所示选项中的一个或几个的逻 辑或。

《Linux高性能服务器编程》笔记01_第5张图片

这两个函数分别是用于在套接字上进行数据接收和数据发送的系统调用,声明在 头文件中。

  1. recv 函数:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 参数

    • sockfd:套接字的文件描述符,表示要接收数据的套接字。
    • buf:指向接收数据缓冲区的指针。
    • len:要接收的数据的最大长度。
    • flags:控制接收操作的行为的标志,通常设置为 0。
  • 返回值

    • 如果成功,返回接收到的字节数。
    • 如果连接已关闭(对于 TCP 套接字),返回 0。
    • 如果失败,返回 -1,并设置全局变量 errno 表示错误的类型。
  • 作用

    • 用于从套接字接收数据。可以是阻塞或非阻塞的,具体取决于套接字的属性和 flags 参数。
  1. send 函数:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 参数

    • sockfd:套接字的文件描述符,表示要发送数据的套接字。
    • buf:指向待发送数据的缓冲区的指针。
    • len:待发送数据的长度。
    • flags:控制发送操作的行为的标志,通常设置为 0。
  • 返回值

    • 如果成功,返回实际发送的字节数。
    • 如果连接已关闭(对于 TCP 套接字),返回 -1,并设置全局变量 errnoEPIPE
    • 如果失败,返回 -1,并设置全局变量 errno 表示错误的类型。
  • 作用

    • 用于向套接字发送数据。可以是阻塞或非阻塞的,具体取决于套接字的属性和 flags 参数。

这两个函数是基本的套接字通信函数,用于实现数据的双向传输。在网络编程中,它们通常与 socketbindlistenaccept 等函数一起使用,构建完整的客户端和服务器通信应用。

我们举例来说明如何使用这些选项。MSG_OOB选项给应用程序提供了发送和接收带外 数据的方法,如下所示:

5-6oobsend.cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[]) {
    // 检查命令行参数是否足够
    if (argc <= 2) {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }

    // 从命令行参数获取服务器的IP地址和端口号
    const char* ip = argv[1];
    int port = atoi(argv[2]);

    // 初始化服务器地址结构
    struct sockaddr_in server_address;
    bzero(&server_address, sizeof(server_address));
    server_address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &server_address.sin_addr);
    server_address.sin_port = htons(port);

    // 创建一个 TCP 套接字
    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(sockfd >= 0);

    // 连接到服务器
    if (connect(sockfd, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
        printf("connection failed\n");
    } else {
        // 连接成功后发送带外数据
        printf("send oob data out\n");
        const char* oob_data = "abc";
        const char* normal_data = "123";

        // 发送普通数据
        send(sockfd, normal_data, strlen(normal_data), 0);

        // 发送带外数据,使用 MSG_OOB 标志
        send(sockfd, oob_data, strlen(oob_data), MSG_OOB);

        // 再次发送普通数据
        send(sockfd, normal_data, strlen(normal_data), 0);
    }

    // 关闭套接字
    close(sockfd);
    return 0;
}

代码作用解释

  • 该程序是一个简单的客户端程序,用于连接到指定的服务器,并发送包含普通数据和带外数据的消息。
  • 通过命令行参数获取服务器的IP地址和端口号。
  • 创建一个 TCP 套接字,并连接到指定的服务器。
  • 如果连接成功,先发送普通数据,然后通过使用 MSG_OOB 标志发送带外数据,最后再发送一条普通数据。
  • 带外数据是一种具有高优先级的数据,可以用于传递紧急信息。在这个例子中,通过带外数据的发送顺序演示了其具有高优先级的特性。

5-7oobrecv.cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024

int main(int argc, char* argv[]) {
    // 检查命令行参数是否足够
    if (argc <= 2) {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }

    // 从命令行参数获取服务器的IP地址和端口号
    const char* ip = argv[1];
    int port = atoi(argv[2]);

    // 初始化服务器地址结构
    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);

    // 创建一个 TCP 套接字
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    // 将套接字绑定到指定的地址
    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    // 开始监听连接请求
    ret = listen(sock, 5);
    assert(ret != -1);

    // 接受客户端连接请求
    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
    if (connfd < 0) {
        // 处理连接错误
        printf("errno is: %d\n", errno);
    } else {
        char buffer[BUF_SIZE];

        // 接收普通数据
        memset(buffer, '\0', BUF_SIZE);
        ret = recv(connfd, buffer, BUF_SIZE-1, 0);
        printf("got %d bytes of normal data '%s'\n", ret, buffer);

        // 接收带外数据
        memset(buffer, '\0', BUF_SIZE);
        ret = recv(connfd, buffer, BUF_SIZE-1, MSG_OOB);
        printf("got %d bytes of oob data '%s'\n", ret, buffer);

        // 再次接收普通数据
        memset(buffer, '\0', BUF_SIZE);
        ret = recv(connfd, buffer, BUF_SIZE-1, 0);
        printf("got %d bytes of normal data '%s'\n", ret, buffer);

        // 关闭与客户端的连接
        close(connfd);
    }

    // 关闭监听套接字
    close(sock);
    return 0;
}

代码作用解释

  • 该程序是一个简单的服务器程序,用于接受客户端连接,接收客户端发送的数据,并演示了接收普通数据和带外数据的过程。
  • 通过命令行参数获取服务器的IP地址和端口号。
  • 创建一个 TCP 套接字,并将其绑定到指定的地址。
  • 开始监听连接请求,接受客户端连接,并接收客户端发送的数据。
  • 演示了通过 recv 函数接收普通数据和带外数据,并打印接收到的信息。
  • 关闭与客户端的连接和监听套接字。

我们先在Kongming20上启动代码清单5-7所示的服务器程序(名为testoobrecv),然 后从ernest-laptop 上执行代码清单5-6所示的客户端程序(名为testoobsend)来向服务器发 送带外数据。同时用tcpdump抓取这一过程中客户端和服务器交换的TCP报文段。具体操 作如下:

在这里插入图片描述

服务器程序输出如下:

在这里插入图片描述

由此可见,客户端发送给服务器的3字节的带外数据“abc”中,仅有最后一个字符 “c”被服务器当成真正的带外数据接收(正如3.8节讨论的那样)。并且,服务器对正常数据 的接收将被带外数据截断,即前一部分正常数据“123ab”和后续的正常数据“123”是不能 被一个recv调用全部读出的。tcpdump的输出内容中,和带外数据相关的是代码清单5-8所示的TCP报文段。

在这里插入图片描述

这里我们第一次看到tcpdump输出标志U,这表示该TCP报文段的头部被设置了紧急 标志。“urg 3”是紧急偏移值,它指出带外数据在字节流中的位置的下一字节位置是7(3+4, 其中4是该TCP报文段的序号值相对初始序号值的偏移)。因此,带外数据是字节流中的第 6字节,即字符“c”。

值得一提的是,flags参数只对send和recv的当前调用生效,而后面我们将看到如何通 过 setsockopt系统调用永久性地修改socket的某些属性。

5.8.2 UDP数据读写

socket编程接口中用于UDP数据报读写的系统调用是:

#include  
#include 
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 structsockaddr* dest_addr, socklen_t addrlen );

recvfrom读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小。因为 UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数 src _addr所指的内容,addrlen参数则指定该地址的长度。

sendto往sockfd 上写入数据,buf和len参数分别指定写缓冲区的位置和大小。dest_addr 参数指定接收端的socket 地址,addrlen参数则指定该地址的长度。这两个系统调用的flags参数以及返回值的含义均与send/recv系统调用的flags参数及返 回值相同。

值得一提的是,recvfrom/sendto系统调用也可以用于面向连接(STREAM)的socket的 数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因 为我们已经和对方建立了连接,所以已经知道其socket地址了)。

这两个函数是用于在支持数据报传输的套接字上进行数据的接收和发送的系统调用,声明在 头文件中。

  1. recvfrom 函数
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
  • 参数

    • sockfd:套接字的文件描述符,表示要接收数据的套接字。
    • buf:指向接收数据缓冲区的指针。
    • len:要接收的数据的最大长度。
    • flags:控制接收操作的行为的标志,通常设置为 0。
    • src_addr:指向用于存储发送端地址信息的 struct sockaddr 结构的指针。
    • addrlen:指向 socklen_t 类型的指针,指定 src_addr 结构的大小。
  • 返回值

    • 如果成功,返回接收到的字节数。
    • 如果失败,返回 -1,并设置全局变量 errno 表示错误的类型。
  • 作用

    • 用于从支持数据报传输的套接字接收数据。接收到的数据和发送端的地址信息将存储在指定的参数中。
  1. sendto 函数
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
  • 参数

    • sockfd:套接字的文件描述符,表示要发送数据的套接字。
    • buf:指向待发送数据的缓冲区的指针。
    • len:待发送数据的长度。
    • flags:控制发送操作的行为的标志,通常设置为 0。
    • dest_addr:指向目标地址信息的 struct sockaddr 结构的指针。
    • addrlen:指定 dest_addr 结构的大小。
  • 返回值

    • 如果成功,返回实际发送的字节数。
    • 如果失败,返回 -1,并设置全局变量 errno 表示错误的类型。
  • 作用

    • 用于向支持数据报传输的套接字发送数据。可以指定目标地址信息,也可以通过先调用 connect 函数指定默认目标地址,然后使用 send 函数。

这两个函数常用于 UDP 套接字,其中 recvfrom 用于接收从特定地址发来的数据,而 sendto 用于发送数据到指定的目标地址。

以下是使用 recvfromsendto 函数的简单示例代码,实现基于UDP的简单通信:

服务器端代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024
#define SERVER_PORT 8080

int main() {
    // 创建UDP套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 初始化服务器地址结构
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERVER_PORT);

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

    while (1) {
        char buffer[BUF_SIZE];
        struct sockaddr_in client_addr;
        socklen_t client_addrlen = sizeof(client_addr);

        // 接收数据报文
        ssize_t recv_bytes = recvfrom(sockfd, buffer, BUF_SIZE - 1, 0, (struct sockaddr*)&client_addr, &client_addrlen);
        if (recv_bytes == -1) {
            perror("recvfrom failed");
            break;
        }

        // 在接收到的数据后添加字符串结束符
        buffer[recv_bytes] = '\0';

        // 打印客户端信息和接收到的数据
        printf("Received data from %s:%d - %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);

        // 发送回应数据报文
        const char* response = "Hello, client!";
        ssize_t send_bytes = sendto(sockfd, response, strlen(response), 0, (struct sockaddr*)&client_addr, client_addrlen);
        if (send_bytes == -1) {
            perror("sendto failed");
            break;
        }
    }

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

    return 0;
}

客户端代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024
#define SERVER_PORT 8080

int main() {
    // 创建UDP套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 初始化服务器地址结构
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 服务器IP地址
    server_addr.sin_port = htons(SERVER_PORT);

    // 发送数据报文
    const char* message = "Hello, server!";
    ssize_t send_bytes = sendto(sockfd, message, strlen(message), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (send_bytes == -1) {
        perror("sendto failed");
        exit(EXIT_FAILURE);
    }

    char buffer[BUF_SIZE];

    // 接收服务器的回应数据报文
    ssize_t recv_bytes = recvfrom(sockfd, buffer, BUF_SIZE - 1, 0, NULL, NULL);
    if (recv_bytes == -1) {
        perror("recvfrom failed");
        exit(EXIT_FAILURE);
    }

    // 在接收到的数据后添加字符串结束符
    buffer[recv_bytes] = '\0';

    // 打印服务器的回应数据
    printf("Received response from server: %s\n", buffer);

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

    return 0;
}

这个示例中,服务器和客户端使用了 recvfromsendto 函数进行UDP通信。服务器监听指定端口,接收来自客户端的消息,并发送回应消息。客户端发送一条消息给服务器,然后接收服务器的回应。

下面是一个可能的输出结果的示例:

服务器端输出:

Received data from 127.0.0.1:54321 - Hello, server!
Received data from 192.168.1.2:12345 - How are you?

客户端输出:

Received response from server: Hello, client!
Received response from server: Hi there!

在这个示例中,服务器端接收到两个不同客户端的消息,并发送回应消息。客户端发送消息给服务器,并接收服务器的回应消息。每个消息都包含了发送方的IP地址和端口号,以及消息内容。请注意,具体的IP地址和端口号会根据实际运行环境而变化。

5.8.3通用数据读写函数

socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据, 也能用于UDP数据报:

#include 
ssize_t recvmsg( int sockfd, struct msghdr* msg, int flags ); 
ssize_t sendmsg( int sockfd, struct msghdr* msg, int flags );

这两个函数 recvmsgsendmsg 是用于在套接字上进行消息传输的系统调用,声明在 头文件中。

  1. recvmsg 函数
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
  • 参数

    • sockfd:套接字的文件描述符,表示要接收数据的套接字。
    • msg:指向 struct msghdr 结构的指针,用于指定接收消息的详细信息和存储接收数据的缓冲区。
    • flags:控制接收操作的行为的标志,通常设置为 0。
  • 返回值

    • 如果成功,返回接收到的字节数。
    • 如果连接已关闭(对于 TCP 套接字),返回 0。
    • 如果失败,返回 -1,并设置全局变量 errno 表示错误的类型。
  • 作用

    • 用于从套接字接收消息。通过 msg 参数指定接收消息的详细信息,包括消息的控制信息(msg_control)、消息的数据部分(msg_iov)等。
  1. sendmsg 函数
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
  • 参数

    • sockfd:套接字的文件描述符,表示要发送数据的套接字。
    • msg:指向 struct msghdr 结构的指针,用于指定发送消息的详细信息和包含待发送数据的缓冲区。
    • flags:控制发送操作的行为的标志,通常设置为 0。
  • 返回值

    • 如果成功,返回实际发送的字节数。
    • 如果连接已关闭(对于 TCP 套接字),返回 -1,并设置全局变量 errnoEPIPE
    • 如果失败,返回 -1,并设置全局变量 errno 表示错误的类型。
  • 作用

    • 用于向套接字发送消息。通过 msg 参数指定发送消息的详细信息,包括消息的控制信息(msg_control)、消息的数据部分(msg_iov)等。

这两个函数的灵活性较高,允许在消息传输中包含控制信息,比如文件描述符等。它们通常用于更复杂的套接字通信场景。使用 struct msghdr 结构可以灵活地处理套接字上的消息。

sockfd参数指定被操作的目标socket。msg参数是msghdr结构体类型的指针,msghdr 结构体的定义如下:

《Linux高性能服务器编程》笔记01_第6张图片

msg_name成员指向一个socket地址结构变量。它指定通信对方的socket地址。对 于面向连接的TCP协议,该成员没有意义,必须被设置为NULL。这是因为对数据流 socket而言,对方的地址已经知道。msg_namelen成员则指定了msg_name所指socket 地址的长度。msg_iov 成员是iovec结构体类型的指针,iovec结构体的定义如下:

struct iovec
{
    void *iov_base; /*内存起始地址*/ 
    size_t iov_len; /*这块内存的长度*/
}

由上可见,iovec结构体封装了一块内存的起始位置和长度。msg_iovlen指定这样的 iovec结构对象有多少个。对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散 的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读(scatterread):对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写(gather write)。

msg_control 和msg_controllen 成员用于辅助数据的传送。我们不详细讨论它们,仅在第 13章介绍如何使用它们来实现在进程间传递文件描述符。

msg_flags成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读 写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg flags中。

recvmsg/sendmsg 的flags参数以及返回值的含义均与send/recv的flags参数及返回值 相同。

以下是一个简单的例子,演示了如何使用 sendmsgrecvmsg 函数进行基于消息的套接字通信。在这个例子中,服务器和客户端通过 Unix 域套接字进行通信,传递的消息包含字符串和文件描述符。

服务器端代码:server.c

#include 
#include 
#include 
#include 
#include 
#include 

#define SOCK_PATH "/tmp/socket_example"

void handle_error(const char* msg) {
    perror(msg);
    exit(EXIT_FAILURE);
}

int main() {
    // 创建Unix域套接字
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd == -1) {
        handle_error("socket");
    }

    struct sockaddr_un server_addr;
    server_addr.sun_family = AF_UNIX;
    strcpy(server_addr.sun_path, SOCK_PATH);

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

    // 监听连接请求
    if (listen(sockfd, 5) == -1) {
        handle_error("listen");
    }

    printf("Server is waiting for connections...\n");

    while (1) {
        int client_fd = accept(sockfd, NULL, NULL);
        if (client_fd == -1) {
            handle_error("accept");
        }

        // 准备消息
        char message[] = "Hello, client!";
        struct iovec iov;
        iov.iov_base = message;
        iov.iov_len = sizeof(message);

        // 准备消息头
        struct msghdr msg;
        msg.msg_name = NULL;
        msg.msg_namelen = 0;
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;

        // 发送消息
        if (sendmsg(client_fd, &msg, 0) == -1) {
            handle_error("sendmsg");
        }

        // 关闭客户端连接
        close(client_fd);
    }

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

    return 0;
}

客户端代码:client.c

#include 
#include 
#include 
#include 
#include 
#include 

#define SOCK_PATH "/tmp/socket_example"

void handle_error(const char* msg) {
    perror(msg);
    exit(EXIT_FAILURE);
}

int main() {
    // 创建Unix域套接字
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd == -1) {
        handle_error("socket");
    }

    struct sockaddr_un server_addr;
    server_addr.sun_family = AF_UNIX;
    strcpy(server_addr.sun_path, SOCK_PATH);

    // 连接到服务器
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        handle_error("connect");
    }

    // 准备接收消息的缓冲区
    char buffer[256];
    memset(buffer, 0, sizeof(buffer));

    // 准备消息头
    struct msghdr msg;
    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    // 接收消息
    if (recvmsg(sockfd, &msg, 0) == -1) {
        handle_error("recvmsg");
    }

    // 打印接收到的消息
    printf("Received message: %s\n", buffer);

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

    return 0;
}

这个例子中,服务器通过 sendmsg 发送消息,客户端通过 recvmsg 接收消息。消息包含了一个字符串作为消息数据。在实际应用中,消息头 msghdr 还可以包含控制信息(例如,文件描述符)。

5.9带外标记

代码清单5-7演示了TCP带外数据的接收方法。但在实际应用中,我们通常无法预期带外数据何时到来。好在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:1/O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数 据在数据流中的具体位置,才能准确接收带外数据。这一点可通过如下系统调用实现:

#include  
int sockatmark( int sockfd );

sockatmark判断 sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。 如果是,sockatmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外 数据。如果不是,则sockatmark返回0。

5.10地址信息函数

在某些情况下,我们想知道一个连接socket的本端socket地址,以及远端的socket地 址。下面这两个函数正是用于解决这个问题:

#include 
int getsockname( int sockfd, struct sockaddr* address, socklen_t* address_len ); 
int getpeername( int sockfd, struct sockaddr* address, socklen_t* address_len );

getsockname获取 sockfd 对应的本端socket 地址,并将其存储于address参数指定的内 存中,该socket地址的长度则存储于address_len参数指向的变量中。如果实际socket地址 的长度大于address所指内存区的大小,那么该socket地址将被截断。getsockname 成功时返 回0,失败返回-1并设置errno。

getpeername获取sockfd对应的远端socket 地址,其参数及返回值的含义与getsockname 的参数及返回值相同。

5.11 socket选项

如果说fcntl系统调用是控制文件描述符属性的通用POSIX方法,那么下面两个系统调 用则是专门用来读取和设置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 );

sockfd参数指定被操作的目标socket。level参数指定要操作哪个协议的选项(即属性), 比如IPv4、IPv6、TCP等。option_name参数则指定选项的名字。我们在表5-5中列举了socket 通信中几个比较常用的socket 选项。option_value和 option_len参数分别是被操作选项 的值和长度。不同的选项具有不同类型的值,如表5-5中“数据类型”一列所示。

《Linux高性能服务器编程》笔记01_第7张图片

getsockopt和setsockopt这两个函数成功时返回0,失败时返回-1并设置errno。

值得指出的是,对服务器而言,有部分socket选项只能在调用listen系统调用前针对监 听socket设置才有效。这是因为连接socket只能由accept 调用返回,而accept从listen监 听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤(因为listen监听队列中的连接至少已进入SYN_RCVD状态,参见图3-8和代码清单5-4),这说明服务器已经往被接受连接上发送出了TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设置,比如TCP最大报文段选项(回忆3.2.2小节,该选项只能由同步报文段来发送)。对这种情况,Linux给开发人员提供的解决方案是:对监听 socket设置这些socket选项,那么 accept返回的连接socket将自动继承这些选项。这些socket选项包括:SO_DEBUG、SO_ DONTROUTE、 SO_KEEPALIVE、 SO_LINGER、 SO_OOBINLINE、 SO_RCVBUF、 SO RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY。而对 客户端而言,这些socket选项则应该在调用connect函数之前设置,因为connect调用成功返 回之后,TCP三次握手已完成。

下面我们详细讨论部分重要的socket 选项。

5.11.1 SO_REUSEADDR选项

我们在3.4.2小节讨论过TCP连接的TIME_WAIT状态,并提到服务器程序可以通过设 置socket选项SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket 地址。具体实现方法如代码清单5-9所示。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[]) {
    // 检查命令行参数,确保提供了 IP 地址和端口号
    if (argc <= 2) {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }
    
    const char* ip = argv[1];
    int port = atoi(argv[2]);

    // 创建套接字
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    // 设置 SO_REUSEADDR 选项,允许重用地址
    int reuse = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    // 初始化服务器地址结构
    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);

    // 将套接字绑定到指定地址
    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    // 监听连接请求,设置最大连接数为 5
    ret = listen(sock, 5);
    assert(ret != -1);

    // 接受客户端连接
    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
    if (connfd < 0) {
        printf("errno is: %d\n", errno);
    } else {
        // 打印客户端连接信息
        char remote[INET_ADDRSTRLEN];
        printf("connected with ip: %s and port: %d\n",
               inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN),
               ntohs(client.sin_port));
        // 关闭连接
        close(connfd);
    }

    // 关闭监听套接字
    close(sock);
    return 0;
}

这个程序实现了一个简单的单线程服务器,能够接受一个客户端连接并打印连接信息。

经过setsockopt的设置之后,即使sock处于TIME_WAIT状态,与之绑定的socket地址 也可以立即被重用。此外,我们也可以通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来 快速回收被关闭的socket,从而使得TCP连接根本就不进入TIME_WAIT状态,进而允许应 用程序立即重用本地的socket地址。

5.11.2 SO_RCVBUF和SO_SNDBUF选项

SO RCVBUF和SO SNDBUF选项分别表示TCP接收缓冲区和发送缓冲区的大小。不过,当我们用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。TCP接收缓冲区的最小值是256字节,而发送缓冲区的最小值是2048字节(不过,不同的系统可能有不同的默认最小值)。系统这样做的目的,主要 是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞(比如快速重传算法就期望TCP接收缓冲区能至少容纳4个大小为SMSS的TCP报文段)。此外,我们可以直接修改内核参数 /proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem来强制TCP接收缓冲区和发送缓 冲区的大小没有最小值限制。我们将在第16章讨论这两个内核参数。

下面我们编写一对客户端和服务器程序,如代码清单5-10和代码清单5-11所示,它们 分别修改TCP发送缓冲区和接收缓冲区的大小。

5-10set_send_buffer.cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFFER_SIZE 512

int main(int argc, char* argv[]) {
    // 检查命令行参数,确保提供了 IP 地址、端口号和发送缓冲区大小
    if (argc <= 3) {
        printf("usage: %s ip_address port_number send_buffer_size\n", basename(argv[0]));
        return 1;
    }
    
    const char* ip = argv[1];
    int port = atoi(argv[2]);

    // 初始化服务器地址结构
    struct sockaddr_in server_address;
    bzero(&server_address, sizeof(server_address));
    server_address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &server_address.sin_addr);
    server_address.sin_port = htons(port);

    // 创建套接字
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    // 设置发送缓冲区大小
    int sendbuf = atoi(argv[3]);
    int len = sizeof(sendbuf);
    setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof(sendbuf));

    // 获取实际的发送缓冲区大小
    getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, (socklen_t*)&len);
    printf("The TCP send buffer size after setting is %d\n", sendbuf);

    // 尝试连接到服务器
    if (connect(sock, (struct sockaddr*)&server_address, sizeof(server_address)) != -1) {
        // 准备发送的数据
        char buffer[BUFFER_SIZE];
        memset(buffer, 'a', BUFFER_SIZE);

        // 发送数据
        send(sock, buffer, BUFFER_SIZE, 0);
    }

    // 关闭套接字
    close(sock);
    return 0;
}

这个程序是一个简单的客户端,通过 TCP 套接字连接到指定的服务器,并设置发送缓冲区大小。以下是各部分的注释和解释:

  1. 检查命令行参数

    • 通过 argcargv 检查是否提供了 IP 地址、端口号和发送缓冲区大小。
  2. 初始化服务器地址结构

    • 使用 struct sockaddr_in 结构初始化服务器地址信息。
  3. 创建套接字

    • 使用 socket 函数创建一个 TCP 套接字。
  4. 设置发送缓冲区大小

    • 通过 setsockopt 函数设置套接字的发送缓冲区大小。
  5. 获取实际的发送缓冲区大小

    • 使用 getsockopt 函数获取实际的发送缓冲区大小,并打印出来。
  6. 尝试连接到服务器

    • 使用 connect 函数尝试连接到指定的服务器。
  7. 准备发送的数据

    • 创建一个大小为 BUFFER_SIZE 的缓冲区,并用字符 ‘a’ 填充。
  8. 发送数据

    • 使用 send 函数发送数据到服务器。
  9. 关闭套接字

    • 使用 close 函数关闭套接字。

这个程序主要用于演示如何设置套接字的发送缓冲区大小,并连接到服务器发送数据。

5-11set_recv_buffer.cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFFER_SIZE 1024

int main(int argc, char* argv[]) {
    // 检查命令行参数,确保提供了 IP 地址、端口号和接收缓冲区大小
    if (argc <= 3) {
        printf("usage: %s ip_address port_number receive_buffer_size\n", basename(argv[0]));
        return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    // 初始化服务器地址结构
    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);

    // 创建套接字
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    // 设置接收缓冲区大小
    int recvbuf = atoi(argv[3]);
    int len = sizeof(recvbuf);
    setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof(recvbuf));

    // 获取实际的接收缓冲区大小
    getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, (socklen_t*)&len);
    printf("The receive buffer size after setting is %d\n", recvbuf);

    // 将套接字绑定到指定地址
    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    // 监听连接请求,设置最大连接数为 5
    ret = listen(sock, 5);
    assert(ret != -1);

    // 接受客户端连接
    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
    if (connfd < 0) {
        printf("errno is: %d\n", errno);
    } else {
        // 准备接收数据的缓冲区
        char buffer[BUFFER_SIZE];
        memset(buffer, '\0', BUFFER_SIZE);

        // 循环接收数据,直到接收完毕
        while (recv(connfd, buffer, BUFFER_SIZE - 1, 0) > 0) {}
        
        // 关闭客户端连接
        close(connfd);
    }

    // 关闭监听套接字
    close(sock);
    return 0;
}

这个程序是一个简单的服务器,通过 TCP 套接字接受客户端的连接请求,并设置接收缓冲区大小。以下是各部分的注释和解释:

  1. 检查命令行参数

    • 通过 argcargv 检查是否提供了 IP 地址、端口号和接收缓冲区大小。
  2. 初始化服务器地址结构

    • 使用 struct sockaddr_in 结构初始化服务器地址信息。
  3. 创建套接字

    • 使用 socket 函数创建一个 TCP 套接字。
  4. 设置接收缓冲区大小

    • 通过 setsockopt 函数设置套接字的接收缓冲区大小。
  5. 获取实际的接收缓冲区大小

    • 使用 getsockopt 函数获取实际的接收缓冲区大小,并打印出来。
  6. 将套接字绑定到指定地址

    • 使用 bind 函数将套接字绑定到指定的 IP 地址和端口号。
  7. 监听连接请求

    • 使用 listen 函数开始监听客户端的连接请求,设置最大连接数为 5。
  8. 接受客户端连接

    • 使用 accept 函数接受客户端连接,并获取客户端的地址信息。
  9. 准备接收数据的缓冲区

    • 创建一个大小为 BUFFER_SIZE 的缓冲区。
  10. 循环接收数据

    • 使用 recv 函数循环接收数据,直到接收完毕。
  11. 关闭客户端连接

    • 使用 close 函数关闭客户端连接套接字。
  12. 关闭监听套接字

    • 使用 close 函数关闭服务器监听套接字。

我们在ernest-laptop 上运行代码清单5-11所示的服务器程序(名为set_recv_buffer),然后在Kongming20上运行代码清单5-10所示的客户端程序(名为set_send_buffer)来向服务器发送512字节的数据,然后用tcpdump抓取这一过程中双方交换的TCP报文段。具体操作 过程如下:

在这里插入图片描述

从服务器的输出来看,系统允许的TCP接收缓冲区最小为256字节。当我们设置TCP 接收缓冲区的大小为50字节时,系统将忽略我们的设置。从客户端的输出来看,我们设置 的TCP发送缓冲区的大小被系统增加了一倍。这两种情况和我们前面讨论的一致。下面是此次TCP通信的tcpdump输出:

《Linux高性能服务器编程》笔记01_第8张图片

首先注意第2个TCP报文段,它指出服务器的接收通告窗口大小为192字节。该值小于256字节,显然是在情理之中。同时,该同步报文段还指出服务器采用的窗口扩大因子是 6。所以服务器后续发送的大部分TCP报文段(6、8、10和12)的实际接收通告窗口大小都 是3×2°字节,即192字节。因此客户端每次最多给服务器发送192字节的数据。客户端一 共给服务器发送了512字节的数据,这些数据必须至少被分为3个TCP报文段(4、7和9)来发送。

有意思的是TCP报文段5和6。当服务器收到客户端发送过来的第一批数据(TCP报文段4)时,它立即用TCP报文段5给予了确认,但该确认报文段的接收通告窗口的大小为0。这说明TCP模块发送该确认报文段时,应用程序还没来得及将数据从TCP接收缓冲中读出。所以此时客户端是不能发送数据给服务器的,直到服务器发送一个重复的确认报文段(TCP 报文段6)来扩大其接收通告窗口。

5.11.3 SO_RCVLOWAT和SO_SNDLOWAT选项

SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低 水位标记。它们一般被l/O复用系统调用(见第9章)用来判断socket是否可读或可写。当TCP接收缓冲区中可读数据的总数大于其低水位标记时,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据;当TCP发送缓冲区中的空闲空间(可以写入数据的空间)大于其低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socke上写入数据。

默认情况下,TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1 字节。

5.11.4 SO_LINGER选项

SOLINGER选项用于控制close系统调用在关闭TCP连接时的行为。默认情况下,当 我们使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责把该socket 对应的TCP发送缓冲区中残留的数据发送给对方。如表5-5所示,设置(获取)SO_LINGER选项的值时,我们需要给setsockopt(getsockopt)系统调用传递一个linger类型的结构体,其定义如下:

#include 
struct linger
{     
    int l_onoff; /*开启(非0)还是关闭(0)该选项*/
    int l_linger;/*滞留时间*/
};

根据linger结构体中两个成员变量的不同值,close系统调用可能产生如下3种行为之一:

I_onoff等于0。此时SO_LINGER选项不起作用,close用默认行为来关闭socket。

l_onoff不为0,1_linger等于0。此时close系统调用立即返回,TCP模块将丢弃被关 闭的socket对应的TCP发送缓冲区中残留的数据,同时给对方发送一个复位报文段(见3.5.2小节)。因此,这种情况给服务器提供了异常终止一个连接的方法。

l_onoff不为0, l_linger大于0。此时close的行为取决于两个条件:一是被关闭 的socket对应的TCP发送缓冲区中是否还有残留的数据;二是该socket是阻塞 的,还是非阻塞的。对于阻塞的socket,close将等待一段长为l_linger的时间,直 到TCP模块发送完所有残留数据并得到对方的确认。如果这段时间内TCP模块没 有发送完残留数据并得到对方的确认,那么close系统调用将返回-1并设置errno为 EWOULDBLOCK。如果socket是非阻塞的,close将立即返回,此时我们需要根据其 返回值和errno来判断残留数据是否已经发送完毕。关于阻塞和非阻塞,我们将在第 8章讨论。

5.12网络信息API

socket地址的两个要素,即IP地址和端口号,都是用数值表示的。这不便于记忆,也不便于扩展(比如从IPv4转移到IPv6)。因此在前面的章节中,我们用主机名来访问一台机器,而避免直接使用其IP地址。同样,我们用服务名称来代替端口号。比如,下面两条 telnet 命令具有完全相同的作用:

telnet 127.0.0.1 80 
telnet localhost www

上面的例子中,telnet客户端程序是通过调用某些网络信息API来实现主机名到IP地址的转换,以及服务名称到端口号的转换的。下面我们将讨论网络信息API中比较重要的 几个。

5.12.1 gethostbyname和gethostbyaddr

gethostbyname 函数根据主机名称获取主机的完整信息,gethostbyaddr 函数根据IP 地址获取主机的完整信息。gethostbyname函数通常先在本地的/etc/hosts配置文件中查 找主机,如果没有找到,再去访问DNS服务器。这些在前面章节中都讨论过。这两个函 数的定义如下:

#include 
struct hostent* gethostbyname( const char* name );
struct hostent* gethostbyaddr( const void* addr, size_t len, int type );

name参数指定目标主机的主机名,addr参数指定目标主机的IP地址,len参数指定 addr 所指IP地址的长度,type参数指定addr所指IP地址的类型,其合法取值包括AF_INET(用于IPv4地址)和AF_INET6(用于IPv6地址)。这两个函数返回的都是hostent结构体类型的指针,hostent结构体的定义如下:

《Linux高性能服务器编程》笔记01_第9张图片

5.12.2 getservbyname和getservbyport

getservbyname函数根据名称获取某个服务的完整信息,getservbyport 函数根据端口号 获取某个服务的完整信息。它们实际上都是通过读取/etc/services文件来获取服务的信息的。 这两个函数的定义如下:

#include 
struct servent* getservbyname( const char* name, const char* proto ); 
struct servent* getservbyport( int port, const char* proto );

name参数指定目标服务的名字,port参数指定目标服务对应的端口号。proto参数指定 服务类型,给它传递“tcp”表示获取流服务,给它传递“udp”表示获取数据报服务,给它 传递NULL则表示获取所有类型的服务。这两个函数返回的都是servent结构体类型的指针,结构体servent的定义如下:

《Linux高性能服务器编程》笔记01_第10张图片

下面我们通过主机名和服务名来访问目标服务器上的daytime服务,以获取该机器的系 统时间,如代码清单5-12所示。

5-12access_daytime.cpp

#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[]) {
    // 确保命令行参数为主机名
    assert(argc == 2);
    char* host = argv[1];

    // 通过主机名获取主机信息
    struct hostent* hostinfo = gethostbyname(host);
    assert(hostinfo);

    // 通过服务名和协议获取服务信息
    struct servent* servinfo = getservbyname("daytime", "tcp");
    assert(servinfo);
    printf("Daytime port is %d\n", ntohs(servinfo->s_port));

    // 初始化服务器地址结构
    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_port = servinfo->s_port;
    address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;

    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    int result = connect(sockfd, (struct sockaddr*)&address, sizeof(address));
    assert(result != -1);

    // 读取服务器发送的数据
    char buffer[128];
    result = read(sockfd, buffer, sizeof(buffer));
    assert(result > 0);
    buffer[result] = '\0';
    printf("The daytime is: %s", buffer);

    // 关闭套接字
    close(sockfd);
    return 0;
}

这个程序是一个简单的客户端,通过 TCP 套接字连接到指定主机的 “daytime” 服务,并获取当前时间。以下是各部分的注释和解释:

  1. 确保命令行参数为主机名

    • 使用 assert 检查命令行参数数量,确保提供了主机名。
  2. 通过主机名获取主机信息

    • 使用 gethostbyname 函数获取指定主机名的主机信息。
  3. 通过服务名和协议获取服务信息

    • 使用 getservbyname 函数获取 “daytime” 服务的信息,指定协议为 “tcp”。
  4. 初始化服务器地址结构

    • 使用 struct sockaddr_in 结构初始化服务器地址信息。
  5. 创建套接字

    • 使用 socket 函数创建一个 TCP 套接字。
  6. 连接到服务器

    • 使用 connect 函数连接到指定主机的 “daytime” 服务。
  7. 读取服务器发送的数据

    • 使用 read 函数从套接字中读取服务器发送的数据。
  8. 打印当前时间

    • 将读取到的数据打印出来,表示当前时间。
  9. 关闭套接字

    • 使用 close 函数关闭套接字。

需要指出的是,上面讨论的4个函数都是不可重人的,即非线程安全的。不过netdb.h 头文件给出了它们的可重入版本。正如Linux下所有其他函数的可重人版本的命名规则那样, 这些函数的函数名是在原函数名尾部加上 _r(re-entrant)。

5.12.3 getaddrinfo

getaddrinfo函数既能通过主机名获得IP地址(内部使用的是gethostbyname 函数), 也能通过服务名获得端口号(内部使用的是getservbyname函数)。它是否可重入取决于其 内部调用的gethostbyname和getservbyname函数是否是它们的可重人版本。该函数的定义 如下:

#include 
int getaddrinfo( const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result ) ;

hostname参数可以接收主机名,也可以接收字符串表示的IP地址(IPv4采用点分十进制字符串,IPv6则采用十六进制字符串)。同样,service参数可以接收服务名,也可以 接收字符串表示的十进制端口号。hints参数是应用程序给getaddrinfo的一个提示,以对 getaddrinfo的输出进行更精确的控制。hints参数可以被设置为NULL,表示允许getaddrinfo 反馈任何可用的结果。result参数指向一个链表,该链表用于存储getaddrinfo 反馈的结果。

getaddrinfo 反馈的每一条结果都是addrinfo结构体类型的对象,结构体addrinfo的定义如下:

《Linux高性能服务器编程》笔记01_第11张图片

该结构体中,ai_protocol 成员是指具体的网络协议,其含义和socket系统调用的第三个 参数相同,它通常被设置为0。ai_fags成员可以取表5-6中的标志的按位或。

《Linux高性能服务器编程》笔记01_第12张图片

当我们使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_ protocol 四个字段,其他字段则必须被设置为NULL。例如,代码清单5-13利用了hints参数获取主机ernest-laptop上的“daytime”流服务信息。

《Linux高性能服务器编程》笔记01_第13张图片

从代码清单5-13中我们能分析出,getaddrinfo将隐式地分配堆内存(可以通过valgrind 等工具查看),因为res 指针原本是没有指向一块合法内存的,所以,getaddrinfo调用结束后, 我们必须使用如下配对函数来释放这块内存:

#include 
void freeaddrinfo ( struct addrinfo* res );

5.12.4 getnameinfo

getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是 gethostbyaddr函数)和服务名(内部使用的是getservbyport 函数)。它是否可重入取决于 其内部调用的gethostbyaddr和getservbyport函数是否是它们的可重人版本。该函数的定义 如下:

#include 
int getnameinfo( const struct sockaddr* sockaddr, socklen_t addrlen, char* host,
                socklen_t hostlen, char* serv, 
                socklen_t servlen, int flags );

getnameinfo将返回的主机名存储在host参数指向的缓存中,将服务名存储在serv 参数指向的缓存中,hostlen和servlen参数分别指定这两块缓存的长度。fags参数控制 getnameinfo的行为,它可以接收表5-7中的选项。

《Linux高性能服务器编程》笔记01_第14张图片

getaddrinfo 和 getnameinfo 函数成功时返回 0,失败则返回错误码,可能的错误码如表 5-8所示。

《Linux高性能服务器编程》笔记01_第15张图片

Linux 下 strerror 函数能将数值错误码errno转换成易读的字符串形式。同样,下面的函 数可将表5-8中的错误码转换成其字符串形式:

#include 
const char* gai_strerror( int error );

gai_strerror 是一个用于将地址信息相关错误码转换成人类可读字符串的函数。该函数的声明如下:

#include 

const char* gai_strerror(int error);
  • 参数

    • error: 要转换的地址信息错误码。
  • 返回值

    • 返回一个指向表示错误消息的字符串的指针。

该函数通常与其他函数一起使用,例如 getaddrinfo,用于解释地址信息相关的错误。调用时,可以将错误码传递给 gai_strerror 函数,以获取对应的错误消息字符串。

示例用法:

#include 
#include 
#include 

int main() {
    struct addrinfo hints, *result;
    int status;

    // 设置 hints 结构体等...

    // 调用 getaddrinfo 函数
    status = getaddrinfo("example.com", "http", &hints, &result);

    if (status != 0) {
        // 输出错误信息
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
        exit(EXIT_FAILURE);
    }

    // 处理结果等...

    // 释放资源
    freeaddrinfo(result);

    return 0;
}

在上面的示例中,如果 getaddrinfo 返回非零的错误码,就会使用 gai_strerror 打印相应的错误消息。

gai_strerror 的输出结果是一个指向表示错误消息的字符串的指针。该字符串包含对应于传入的错误码的人类可读的错误描述。

在示例代码中,如果 getaddrinfo 函数返回非零错误码,将调用 gai_strerror(status) 获取错误消息,并将其打印到标准错误流中。例如,可能的输出结果为:

getaddrinfo error: Name or service not known

上述错误消息指示主机名或服务未知,这有助于更好地理解发生了什么问题。实际的错误消息内容取决于传递给 gai_strerror 的错误码。

后记

截至2024年1月19日16点40分,完成《Linux高性能服务器编程》第五章的学习。

你可能感兴趣的:(Linux,Server,服务器,linux)