Linux服务器 - 5.网络编程基础API

本文主要从以下3个方面讨论Linux网络API:socket地址API、socket基础API和网络信息API。

1. socket地址API

主机字节序和网络字节序

字节序分为大端字节序(big endian)和小端字节序(little endian)。大端字节序是指一个整数的高位字节(23~21bit)存储在内存的低地址处,低位字节(0~7bit)存储在内存的高地址处。小端字节序则正好相反。以下代码可用于检查机器的字节序:

#include <stdio.h>
void byteorder()
{
    union
    {
        short value;
        char union_bytes[sizeof(short)];
    } test;
    test.value = 0x0102;
    if ((test.union_bytes[0] == 1) && (test.union_bytes[1] == 2))
    {
        printf("big endian\n");
    }
    else if ((test.union_bytes[0] == 2) && (test.union_bytes[1] == 1))
    {
        printf("little endian\n", );
    }
    else
    {
        printf("unknown...\n", );
    }
}

现在PC大多采用小端字节序,因此又称为主机字节序。

当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端将错误地解释之。解决方法时:发送端总是把要发送地数据转化为大端字节序后在发送,接收端接收到的数据必然是大端字节序的,再根据自身采用的字节序对接收到的数据进行转换。因此大端字节序也成为网络字节序。

Linux提供了如下4个函数来完成主机字节序和网络字节序之间的转换:

#include <netinet/in.h>
unsigned long int htonl( unsigned long int hostlong );
unsigned long int htons( unsigned short int hostshort );
unsigned long int ntohl( unsigned long int netlong );
unsigned long int ntohs( unsigned long int netshort );

通用socket地址

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

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

sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下所示:

协议族 地址族 描述
PF_UNIT AF_UNIX UNIT地址域协议族
PF_INET AF_INET TCP/IPv4协议族
PF_INET6 AF_INET6 TCP/IPv6协议族

宏PF_*和AF_*都定义在bits/socket.h头文件中,且后者和前者有完全相同的值,所以二者通常混用。

sa_data成员用于存放socket地址值。不同的协议族的地址值有不同的含义:

协议族 地址值含义和长度
PF_UNIX 文件的路径名,长度可达108字节
PF_INET 16bit端口号和32bit IPv4地址,共6字节
PF_INET6 16bit端口号,32bit流标识,128bit IPv6地址,32bit范围ID,共26字节

由上可见,14字节的sa_data无法完全容纳多协议族的地址值。因此,linux定义了新的通用socket地址结构体:

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

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

专用socket地址

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

UNIX本地域协议族使用如下专用socket地址结构体:

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

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

struct sockaddr_in
{
    sa_family_t sin_family;     /* 地址族:AF_INET */
    u_int16_t sin_port;         /* 端口号,要用网络字节序表示 */
    struct in_addr sin_addr;    /* IPv4地址结构体 */
};
struct in_addr
{
    u_int32_t s_addr;           /* IPv4地址,要用网络字节序表示 */
};
struct sockaddr_in6
{
    sa_family_t sin6_family;    /* 地址族:AF_INET6 */
    u_int16_t sin6_port;        /* 端口号,要用网络字节序表示 */
    u_int32_t sin6_flowinfo;    /* 流信息,应设置为0 */
    struct in6_addr sin6_addr;  /* IPv6地址结构体 */
    u_int32_t sin6_scope_id;    /* scope ID,尚处于实验阶段 */
};
struct in6_addr
{
    unsigned char sa_addr[16];  /* IPv4地址,要用网络字节序表示 */
};

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

IP地址转换函数

通常,人们习惯用可读性好的字符串来表示IP地址,但编程中我们需要先把他们转化为整数才能使用,以下三个函数可用于IPv4地址字符串表示方法和网络字节序整数表示方法之间的转换:

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

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

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

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

char* szValue1 = inet_ntoa("1.2.3.4");
char* szValue2 = inet_ntoa("10.20.30.40");
printf("address 1: %s\n", szValue1);
printf("address 2: %s\n", szValue2);

// OUTPUT:
// address 1: 10.20.30.40
// address 2: 10.20.30.40

下面这对更新的函数也能完成和前面三个函数同样的功能,同时适用与IPv4和IPv6:

#include <arpa/inet.h>
int inet_pton( int af, const char* src, void* dst );
const char* inet_ntop( int af, const void* src, char* dst, socklen_t cnt );

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

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

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

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

2. 创建socket

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

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

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

type参数指定服务类型。服务类型主要有SOCK_STREAM(流服务,传输层使用TCP协议)和SOCK_UGRAM(数据报,传输层使用TCP协议)。从Linux内核版本2.6.17其,type参数还接受上述服务类型与下面两个重要的标志想与的值:SOCK_NONBLOCK和SOCK_CLOEXEC。它们分别表示将新创建的socket设为非阻塞的,以及用fork调用创建子进程时在子进程中关闭该socket。在2.6.17之前,这两个属性需要使用额外系统调用(比如fcntl)来设置。

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

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

3. 命名socket

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

命名socket的系统调用是bind,定义如下:

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

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

bind成功时返回0,失败则返回-1并设置errno。常见的两个errno是EACCES和EADDRINUSE,含义如下:

4. 监听socket

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

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

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

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

下面是一个服务器程序,以研究backlog参数对listen系统调用的实际影响:

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

static bool stop = false;
/* SIGTERM信号的处理函数,触发时结束主程序循环 */
static void handle_term(int sig)
{
    stop = true;
}

int main(int argc, char* argv[])
{
    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]);
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(socket >= 0);
    
    /* 创建一个IPv4 socket地址 */
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);
    
    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);
    ret = listen(sock, backlog);
    assert(ret != -1);
    
    /* 循环等待连接,直到有SIGTERM信号将它中断 */
    while(!stop)
    {
        sleep(1);
    }
    
    /* 关闭socket */
    close(sock);
    return 0;
}

5. 接收连接

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

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

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

下面展示接受一个异常的连接:

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

int main(int argc, char* argv[])
{
    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]);
    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 = scoket(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);
    
    /* 暂停20秒以等待客户端连接和相关操作(掉线或退出)完成 */
    sleep(20);
    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
    {
        /* 接受连接成功则打印出客户端的IP地址和端口号 */
        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;
}

启动服务器程序后,在客户端通过telnet连接服务器,并在连接成功后立即断开该客户端的网络连接。结果发现accept调用能够正常返回,服务器输出如下:

connected with ip: 192.168.1.102 and port: 38383

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

$ netstat -nt | grep 4321
tcp     0   0   192.168.1.101:4321  192.168.1.102:38383    ESTABLISHED

由以上输出可以看出,accept调用对于客户端网络断开毫不知情。

下面重新执行上述过程,但是不断开客户端网络连接,而是在建立连接后立即退出客户端程序。这次accept调用同样正常返回,输出如下:

connected with ip: 192.168.1.102 and port: 52525

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

$ netstat -nt | grep 4321
tcp     1   0   192.168.1.101:4321  192.168.1.102:52525    CLOSE_WAIT

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

6. 发起连接

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

#include <sys/types.h>
#include <sys/socket.h>
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,含义如下:

7. 关闭连接

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

#include <unistd.h>
int close( int fd );

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

如果无论如何都要立即终止连接(而不是将socket的引用计数器减1),可以使用shutdown系统调用:

#include <sys/socket.h>
int shutdown( int sockfd, int howto );

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

可选值 含义
SHUT_RD 关闭sockfd上读的这一半。应用程序不能再针对socket文件描述符执行读操作,并且该socket接收缓冲区中的数据都被丢弃
SHUT_WR 关闭sockfd上写的这一半。sockfd的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该socket文件描述符执行写操作。这种情况下,连接处于半关闭状态
SHUT_RDWR 同时关闭sockfd上的读和写

由此可见,shutdown能够分别关闭socket上的读或写,或者都关闭。而close在关闭连接时只能将socket上的读和写同时关闭。

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

8. 数据读写

TCP数据读写

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

#include <sys/types.h>
#include <sys/socket.h>
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往socket上写数据,buf和len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度,失败则返回-1并设置errno。

flags参数为数据收发提供了额外的控制,可取下表中的一个或几个的逻辑或:

选项名 含义 send recv
MSG_CONFIRM 指示数据链路层协议持续监听对方的回应,直到得到回复。它仅能用于SOCK_DGRAM和SOCK_RAW类型的socket Y N
MSG_DONTROUTE 不查看路由表,直接将数据发送给本地局域网络内的主机。这表示发送者确切地知道目标主机就在本地网络上 Y N
MSG_DONTWAIT 对socket的此次操作将是非阻塞的 Y Y
MSG_MORE 告诉内核应用程序还有更多数据要发送,内核将超时等待新数据写入TCP发送缓冲区后一并发送。这样可防止TCP发送过多小的报文段,从而提高传输效率 Y N
MSG_WAITALL 读操作仅在读取到指定数量的字节后才返回 N Y
MSG_PEEK 窥探读缓存中的数据,此次读操作不会导致这些数据被清除 N Y
MSG_OOB 发送或接收紧急数据 Y Y
MSG_NOSIGNAL 往读端关闭的管道或者socket连接中写数据时不引发SIGPIPE信号 Y N

发送带外数据

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

int main(int argc, char* argv[])
{
    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]);
    
    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 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
    {
        const char* oob_data = "abc";
        const char* normal_data = "123";
        send(sockfd, normal_data, strlen(normal_data), 0);
        send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
        send(sockfd, normal_data, strlen(normal_data), 0);
    }
    close(sockfd);
    return 0;
}

接收带外数据

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

#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;
    }
    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 ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert (ret != -1);
    ret = listen(sock, 5);
    assert (ret 1= -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;
}

UDP数据读写

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

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, 
                struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags,
                const struct sockaddr* dest_addr, socklen_t addrlen);

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

sendto往sockfd上写数据,buf和len参数分别指定写缓冲区的位置和大小。dest_addr参数指定接收端的socket地址,addrlen参数则指定该地址的长度。

这两个系统调用的flags参数和返回值与send/recv系统调用相同。

另外,recvfrom/sendto系统调用也可以用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL即可。

通用数据读写函数

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

#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);

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

struct msghdr
{
    void* msg_name;             /* socket地址 */
    socklen_t msg_namelen;      /* socket地址的长度 */
    struct iovec* msg_iov;      /* 分散的内存块,见后文 */
    int msg_iovlen;             /* 分散内存块的数量 */
    void* msg_control;          /* 指向辅助数据的起始位置 */
    socklen_t msg_controllen;   /* 辅助数据的大小 */
    int msg_flags;              /* 复制函数中的flags参数,并子啊调用过程中更新 */
};

msg_name成员指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被设置为NULL。msg_namelen成员则指定了msg_name所指socket地址的长度。

msg_iov成员是iovec结构体类型的指针,定义如下:

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

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

9. 带外标记

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

#include <sys/socket.h>
int sockatmark(int sockfd);

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

10. 地址信息函数

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

#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);

11. socket选项

SO_REUSEADDR选项

SO_RCVBUF和SO_SNDBUF选项

SO_RCVLOWAT和SO_SNDLOWAT选项

SO_LINGER选项

12. 网络信息API

gethostbyname和gethostbyaddr

gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。定义如下:

#include <netdb.h>
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结构体定义如下:

#include <netdb.h>
struct hostent
{
    char* h_name;       /* 主机名 */
    char** h_aliases;   /* 主机别名列表,可能有多个 */
    int h_addrtype;     /* 地址类型(地址族) */
    int h_length;       /* 地址长度 */
    char** h_addr_list; /* 按网络字节序列出的主机IP地址列表 */
}

getservbyname和getservbyport

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

#include <netdb.h>
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定义如下:

#include <netdb.h>
struct servent
{
    char* s_name;       /* 服务名称 */
    char** s_aliases;   /* 服务的别名列表,可能有多个 */
    int s_port;         /* 端口号 */
    char* s_proto;      /* 服务类型,通常是tcp或者udp */
};

下面代码展示通过主机名和服务名来访问目标服务器上的daytime服务,以获取该机器的系统时间:

#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>

int main(int argc, char *argv[])
{
    assert(argc == 2);
    char* host = argv[1];
    /* 获取目标主机地址信息 */
    struct hostent* hostinfo = gethostbyname(host);
    assert(hostinfo);
    /* 获取daytime服务信息 */
    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;
    /* 注意下面的代码,因为h_addr_list本身是使用网络字节序的地址列表,
       所以使用其中的IP地址时,无须对目标IP地址转换字节序 */
    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 day time is: %s", buffer);
    close(sockfd);
    return 0;
}

上面的四个函数都是不可重入的,即非线程安全的。不过netdb.h头文件给出了它们的可重入版本,即在函数名尾部加上_r。

getaddrinfo

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

#inclue <netdb.h>
int getaddrinfo(const char* hostname, const char* service,
        const struct addrinfo* hints, struct addrinfo** result);

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

struct addrinfo
{
    int ai_flags;               /* 见下文 */
    int ai_family;              /* 地址族 */
    int ai_socktype;            /* 服务类型,SOCK_STREAM或SOCK_DGRAM */
    int ai_protocol;            /* 见下文 */
    socklen_t ai_addrlen;       /* socket地址ai_addr的长度 */
    char* ai_canonname;         /* 主机的别名 */
    struct sockaddr* ai_addr;   /* 指向socket地址 */
    struct addrinfo* ai_next;   /* 指向下一个sockinfo结构的对象 */
}

其中ai_protocol成员是指具体的网络协议,其含义和socket系统调用的第三个参数相同,通常被设置为0。

ai_flags成员可以取下表的标志及其按位或:

选项 含义
AI_PASSIVE 在hints参数中设置,表示调用者是否会将取得的socket地址用于被动打开。服务器通常需要设置它,表示接受任何本地socket地址上的服务请求。客户端程序不能设置它
AI_CANONNAME 在hints参数中设置,告诉getaddrinfo函数返回主机的别名
AI_NUMERICHOST 在hints参数中设置,表示hostname必须是用字符串表示的IP地址,从而避免了DNS查询
AI_NUMERICSERV 在hints参数中设置,强制service参数使用十进制端口号的字符串形式,而不能是服务名
AI_V4MAPPED 在hints参数中设置,如果ai_family被设置为AF_INET6,那么当没有满足条件的IPv6地址被找到时,将IPv4地址映射为IPv6地址
AI_ALL 必须和AI_V4MAPPED同时使用,否则将被忽略。表示同时返回符合条件的IPv6地址以及由IPv4地址映射得到的IPv6地址
AI_ADDRCONFIG 仅当至少配置有一个IPv4地址时,才返回IPv4地址信息;同样,仅当至少配置有一个IPv6是,才返回IPv6地址信息。它同AI_V4MAPPED时互斥的

当我们使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段则必须被设置为NULL。

getaddrinfo调用结束后,我们必须使用如下配对函数来释放内存:

#include <netdb.h>
void freeaddrinfo(struct addrinfo* res);

getnameinfo

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

#include <netdb.h>
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen,
        char* host, socklen_t hostlen,
        char* serv, socklen_t servlen,
        int flags)