本文是读书笔记,如有侵权,请联系删除。
参考
Linux高性能服务器编程源码: https://github.com/raichen/LinuxServerCodes
豆瓣: Linux高性能服务器编程
□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.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 位整数值的字节序转换。
unsigned long int htonl(unsigned long int hostlong)
:
htonl
表示 “host to network long”.hostlong
转换为网络字节序。unsigned short int htons(unsigned short int hostshort)
:
htons
表示 “host to network short”.hostshort
转换为网络字节序。unsigned long int ntohl(unsigned long int netlong)
:
ntohl
表示 “network to host long”.netlong
转换为主机字节序。unsigned short int ntohs(unsigned short int netshort)
:
ntohs
表示 “network to host short”.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所示。
宏PF_*和AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用。sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和 长度,如表5-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:
这两个专用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 地址的字符串表示和二进制表示之间的转换,它们声明在
头文件中。
in_addr_t inet_addr(const char* strptr)
:
inet_addr
。strptr
:指向以字符串形式表示的 IPv4 地址的指针。in_addr_t
类型的值,表示二进制形式的 IPv4 地址。如果转换失败,返回 INADDR_NONE
(通常是 -1
)。int inet_aton(const char* cp, struct in_addr* inp)
:
inet_aton
。struct in_addr
结构中。cp
:指向以字符串形式表示的 IPv4 地址的指针。inp
:指向 struct in_addr
结构的指针,用于存储转换后的二进制形式的 IPv4 地址。char* inet_ntoa(struct in_addr in)
:
inet_ntoa
。in
:struct in_addr
结构,包含要转换的二进制形式的 IPv4 地址。这些函数在网络编程中用于处理 IP 地址的表示,常用于套接字编程中的地址转换操作。当在网络通信中需要将字符串形式的 IP 地址与二进制形式进行转换时,这些函数是很有用的。
以下是这三个函数的简单使用例子:
#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;
}
#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;
}
#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地址。
#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;
}
#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_addr
、inet_aton
和 inet_ntoa
更为通用,可以用于IPv4和IPv6地址的转换。它们支持传递地址族参数,允许处理不同类型的地址。
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_INET
和 AF_INET6
。type
:指定套接字的类型,例如 SOCK_STREAM
表示流套接字(TCP),SOCK_DGRAM
表示数据报套接字(UDP),SOCK_RAW
表示原始套接字等。protocol
:指定具体的协议,通常设置为 0,表示使用默认协议。对于 SOCK_STREAM
或 SOCK_DGRAM
类型的套接字,通常设置为 IPPROTO_TCP
或 IPPROTO_UDP
。返回值:
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
函数关闭套接字。在实际应用中,根据需要选择合适的协议族、套接字类型和协议。
创建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,它们的含义分别是:
EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将 socket绑定到知名服务端口(端口号为O~1023)上时,bind将返回EACCES错误。
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
结构的大小。返回值:
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
函数将套接字与该地址绑定。如果绑定成功,接下来就可以使用这个套接字进行通信。绑定是在服务器端创建套接字时的重要步骤,确保服务器能够监听到来自指定地址和端口的连接请求。
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
函数完成的连接。返回值:
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
函数来接受和处理。在服务器端创建套接字后,通常会依次调用 bind
、listen
和 accept
函数来准备接受客户端的连接。
下面我们编写一个服务器程序,以研究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;
}
代码作用解释:
stop
标志设置为true,从而退出循环,最终关闭套接字。stop
标志,使得程序退出循环,关闭套接字并结束程序。下面的系统调用从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
:监听套接字的文件描述符,通过 socket
、bind
和 listen
函数创建。addr
:指向 struct sockaddr
结构的指针,用于存储客户端的地址信息。addrlen
:指向 socklen_t
类型的指针,指定 addr
结构的大小。返回值:
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;
}
代码作用解释:
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状态),更不关心任何网络状况的变化。
如果说服务器通过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
结构的大小。返回值:
errno
表示错误的类型。作用:
connect
函数用于在客户端建立与服务器的连接。客户端通过传入服务器的地址信息(IP地址和端口号)来连接服务器。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
函数发起连接请求。如果连接成功,客户端之后可以使用返回的套接字文件描述符进行与服务器的通信。
关闭一个连接实际上就是关闭该连接对应的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中的某个值。
由此可见,shutdown能够分别关闭socket上的读或写,或者都关闭。而close在关闭连 接时只能将socket 上的读和写同时关闭。shutdown成功时返回0,失败则返回-1并设置errno。
对文件的读写操作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所示选项中的一个或几个的逻 辑或。
这两个函数分别是用于在套接字上进行数据接收和数据发送的系统调用,声明在
和
头文件中。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd
:套接字的文件描述符,表示要接收数据的套接字。buf
:指向接收数据缓冲区的指针。len
:要接收的数据的最大长度。flags
:控制接收操作的行为的标志,通常设置为 0。返回值:
errno
表示错误的类型。作用:
flags
参数。ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
sockfd
:套接字的文件描述符,表示要发送数据的套接字。buf
:指向待发送数据的缓冲区的指针。len
:待发送数据的长度。flags
:控制发送操作的行为的标志,通常设置为 0。返回值:
errno
为 EPIPE
。errno
表示错误的类型。作用:
flags
参数。这两个函数是基本的套接字通信函数,用于实现数据的双向传输。在网络编程中,它们通常与 socket
、bind
、listen
、accept
等函数一起使用,构建完整的客户端和服务器通信应用。
我们举例来说明如何使用这些选项。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;
}
代码作用解释:
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;
}
代码作用解释:
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的某些属性。
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地址了)。
这两个函数是用于在支持数据报传输的套接字上进行数据的接收和发送的系统调用,声明在
和
头文件中。
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
结构的大小。返回值:
errno
表示错误的类型。作用:
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
结构的大小。返回值:
errno
表示错误的类型。作用:
connect
函数指定默认目标地址,然后使用 send
函数。这两个函数常用于 UDP 套接字,其中 recvfrom
用于接收从特定地址发来的数据,而 sendto
用于发送数据到指定的目标地址。
以下是使用 recvfrom
和 sendto
函数的简单示例代码,实现基于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;
}
这个示例中,服务器和客户端使用了 recvfrom
和 sendto
函数进行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地址和端口号会根据实际运行环境而变化。
socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据, 也能用于UDP数据报:
#include
ssize_t recvmsg( int sockfd, struct msghdr* msg, int flags );
ssize_t sendmsg( int sockfd, struct msghdr* msg, int flags );
这两个函数 recvmsg
和 sendmsg
是用于在套接字上进行消息传输的系统调用,声明在
头文件中。
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
参数:
sockfd
:套接字的文件描述符,表示要接收数据的套接字。msg
:指向 struct msghdr
结构的指针,用于指定接收消息的详细信息和存储接收数据的缓冲区。flags
:控制接收操作的行为的标志,通常设置为 0。返回值:
errno
表示错误的类型。作用:
msg
参数指定接收消息的详细信息,包括消息的控制信息(msg_control
)、消息的数据部分(msg_iov
)等。ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
参数:
sockfd
:套接字的文件描述符,表示要发送数据的套接字。msg
:指向 struct msghdr
结构的指针,用于指定发送消息的详细信息和包含待发送数据的缓冲区。flags
:控制发送操作的行为的标志,通常设置为 0。返回值:
errno
为 EPIPE
。errno
表示错误的类型。作用:
msg
参数指定发送消息的详细信息,包括消息的控制信息(msg_control
)、消息的数据部分(msg_iov
)等。这两个函数的灵活性较高,允许在消息传输中包含控制信息,比如文件描述符等。它们通常用于更复杂的套接字通信场景。使用 struct msghdr
结构可以灵活地处理套接字上的消息。
sockfd参数指定被操作的目标socket。msg参数是msghdr结构体类型的指针,msghdr 结构体的定义如下:
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参数及返回值 相同。
以下是一个简单的例子,演示了如何使用 sendmsg
和 recvmsg
函数进行基于消息的套接字通信。在这个例子中,服务器和客户端通过 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-7演示了TCP带外数据的接收方法。但在实际应用中,我们通常无法预期带外数据何时到来。好在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:1/O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数 据在数据流中的具体位置,才能准确接收带外数据。这一点可通过如下系统调用实现:
#include
int sockatmark( int sockfd );
sockatmark判断 sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。 如果是,sockatmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外 数据。如果不是,则sockatmark返回0。
在某些情况下,我们想知道一个连接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 的参数及返回值相同。
如果说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中“数据类型”一列所示。
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 套接字连接到指定的服务器,并设置发送缓冲区大小。以下是各部分的注释和解释:
检查命令行参数:
argc
和 argv
检查是否提供了 IP 地址、端口号和发送缓冲区大小。初始化服务器地址结构:
struct sockaddr_in
结构初始化服务器地址信息。创建套接字:
socket
函数创建一个 TCP 套接字。设置发送缓冲区大小:
setsockopt
函数设置套接字的发送缓冲区大小。获取实际的发送缓冲区大小:
getsockopt
函数获取实际的发送缓冲区大小,并打印出来。尝试连接到服务器:
connect
函数尝试连接到指定的服务器。准备发送的数据:
BUFFER_SIZE
的缓冲区,并用字符 ‘a’ 填充。发送数据:
send
函数发送数据到服务器。关闭套接字:
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 套接字接受客户端的连接请求,并设置接收缓冲区大小。以下是各部分的注释和解释:
检查命令行参数:
argc
和 argv
检查是否提供了 IP 地址、端口号和接收缓冲区大小。初始化服务器地址结构:
struct sockaddr_in
结构初始化服务器地址信息。创建套接字:
socket
函数创建一个 TCP 套接字。设置接收缓冲区大小:
setsockopt
函数设置套接字的接收缓冲区大小。获取实际的接收缓冲区大小:
getsockopt
函数获取实际的接收缓冲区大小,并打印出来。将套接字绑定到指定地址:
bind
函数将套接字绑定到指定的 IP 地址和端口号。监听连接请求:
listen
函数开始监听客户端的连接请求,设置最大连接数为 5。接受客户端连接:
accept
函数接受客户端连接,并获取客户端的地址信息。准备接收数据的缓冲区:
BUFFER_SIZE
的缓冲区。循环接收数据:
recv
函数循环接收数据,直到接收完毕。关闭客户端连接:
close
函数关闭客户端连接套接字。关闭监听套接字:
close
函数关闭服务器监听套接字。我们在ernest-laptop 上运行代码清单5-11所示的服务器程序(名为set_recv_buffer),然后在Kongming20上运行代码清单5-10所示的客户端程序(名为set_send_buffer)来向服务器发送512字节的数据,然后用tcpdump抓取这一过程中双方交换的TCP报文段。具体操作 过程如下:
从服务器的输出来看,系统允许的TCP接收缓冲区最小为256字节。当我们设置TCP 接收缓冲区的大小为50字节时,系统将忽略我们的设置。从客户端的输出来看,我们设置 的TCP发送缓冲区的大小被系统增加了一倍。这两种情况和我们前面讨论的一致。下面是此次TCP通信的tcpdump输出:
首先注意第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章讨论。
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结构体的定义如下:
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的定义如下:
下面我们通过主机名和服务名来访问目标服务器上的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” 服务,并获取当前时间。以下是各部分的注释和解释:
确保命令行参数为主机名:
assert
检查命令行参数数量,确保提供了主机名。通过主机名获取主机信息:
gethostbyname
函数获取指定主机名的主机信息。通过服务名和协议获取服务信息:
getservbyname
函数获取 “daytime” 服务的信息,指定协议为 “tcp”。初始化服务器地址结构:
struct sockaddr_in
结构初始化服务器地址信息。创建套接字:
socket
函数创建一个 TCP 套接字。连接到服务器:
connect
函数连接到指定主机的 “daytime” 服务。读取服务器发送的数据:
read
函数从套接字中读取服务器发送的数据。打印当前时间:
关闭套接字:
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的定义如下:
该结构体中,ai_protocol 成员是指具体的网络协议,其含义和socket系统调用的第三个 参数相同,它通常被设置为0。ai_fags成员可以取表5-6中的标志的按位或。
当我们使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_ protocol 四个字段,其他字段则必须被设置为NULL。例如,代码清单5-13利用了hints参数获取主机ernest-laptop上的“daytime”流服务信息。
从代码清单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中的选项。
getaddrinfo 和 getnameinfo 函数成功时返回 0,失败则返回错误码,可能的错误码如表 5-8所示。
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高性能服务器编程》第五章的学习。