Linux网络编程学习笔记(一)

1 大小端数据转换函数

1.1 测试系统字节序

#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");
	}
	else
	{
		printf("unknown...\n");
	}
}

1.2 大小端字节序转换函数

#include <arpa/inet.h>

// 系统字节序转网络字节序
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
//网络字节序转系统字节序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

2 socket地址

2.1 通用socket地址

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

sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。

常见的协议族(protocol family,也称domain)和对应的地址族如下表所示:

协议族地址族描述
PF_UNIXAF_UNIXUNIX本地协议族
PF_INETAF_INETTCP/IPv4协议族
PF_INET6AF_INET6TCP/IPv6协议族

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

sa_data成员用于存放socket地址值。但是,不同的协议的地址值具有不同的含义和长度,如下表所示:

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

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

2.2 专用sock地址

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地址,要用网络字节序表示 */
};

// ipv6
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]; /* IPv6地址,要用网络字节序表示 */
}

3 IP地址转换函数

通常,人们习惯用可读性好的字符串来表示IP地址,比如点分十进制表示IPv4,以及用十六进制字符串表示IPv6地址。但编程中我们需要先把它们转化为整数方能使用。

#include <arpa/inet.h>

int inet_pton(int af, const char* src, void* dst);
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);

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

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

#include <netinet/in.h>

#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4 创建socket

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

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

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

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

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

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

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

5 命名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文件描述符,addlen参数指出该socket地址的长度。

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

  • EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口(端口号为0~1023)上时,bind将返回EACCES错误。
  • EADDRINUSE:被绑定的地址正在使用中,比如将socket绑定到一个处于TIME_WAIT状态的socket地址。

6 监听socket

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

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

​ sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHSED)的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(sock >= 0);
    
    /* 创建一个IPv4 socket地址 */
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);
    
    int ret = bind(sock, (struct sockaddr*)address, sizeof(address));
    assert(ret != -1);
    
    /* 循环等待连接,直到有SIGTERM信号将它中断 */
    while(! stop)
    {
        sleep(1);
    }
    
    /* 关闭socket */
    close(sock);
    return 0;
}

该服务器程序(名为testlisten)接收三个参数:IP地址、端口号和backlog值。我们在服务器端运行该程序,并在客户端多次执行telnet命令来连接该服务器程序。同时,每使用telnet命令建立一个连接,就执行一次netstat命令来查看服务器上连接的状态。具体操作如下:

$ ./testlisten 192.168.1.109 1234 5 # 监听12345端口, 给backlog传递典型值5
$ telnet 192.168.1.109 12345	# 多次执行之
$ netstat -nt | grep 12345	# 多次执行之

结果显示,处于ESTABLISHED状态的连接只有6个(backlog值加1),其他连接都处于SYN_RCVD状态。在不同系统上,结果有些偏差,不过监听队列中完整连接的上限通常比backlog值略大。

**注意:**我们把执行过listen调用、处于LISTEN状态的socket称为监听socket,而所有处于ESTABLISHED状态的socket则称为连接socket

7 接收连接

下面的系统调用从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来与被接受连接对应的客户端通信。accep失败时返回-1并设置errno。

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

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.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;
    address.sin_family = PF_INET;
    inet_pton(PF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);
    
    int sock = socket(AF_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", 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;
}

结果显示,accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。

8 发起连接

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

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const 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,连接超时。

9 关闭连接

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

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

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

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

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

10 数据读写

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

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

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

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

//客户端程序,发送带外数据
#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(AF_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/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.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);
    
    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;
}

先在服务端启动服务器程序(名为testoobrecv),然后在客户端执行客户端程序(名为testoobsend)来向服务器发送带外数据。同时用tcpdump抓取这一过程中客户端和服务器交换的TCP报文段。具体操作如下:

$ ./testoobrecv 192.168.1.109 54321 # 在服务器上运行服务器程序,监听54321端口
$ ./testoobsend 192.168.1.109 54321 # 在客户机上执行客户端程序

$ sudo tcpdump -ntx -i eth0 port 54321

服务器程序的输出如下:

got 5 bytes of normal data ‘123ab’

got 1 bytes of oob data ‘c’

got 3 byte of normal data ‘123’

由此可见,客户端发送给服务器的3字节的带外数据"abc"中,仅有最后一个字符"c"被服务器当成真正的带外数据接收。并且,服务器对正常数据的接收将被带外数据截断,即当前一部分正常数据"123ab"和后续的正常数据"123"是不能被一个recv调用全部读出的。

tcpdump的输出内容中,和带外数据相关的是下面所示的TCP报文段:

IP 192.168.1.108.60460 > 192.168.1.109.54321: Flags [P.U], se1 4:7, ack 1, win 92, urg 3, options
 [nop,nop,TS, val 102794322 ecr 154703423], length 3

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

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

10.3 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参数则指定该地址的长度。

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

10.4 通用数据读写函数

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; /* 分散的内存块数组,见后文 */
	size_t msg_iovlen;	/* 内存块数组的大小 */
    void* msg_control;	/* 辅助数据 */
	size_t msg_controllen; /* 辅助数据的大小 */
	int msg_flags;	/* 复制函数中的flags参数,并在调用过程中更新 */
}

​ 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指定这样的jovec结构对象有多少个个。对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度由msg_iov指向的数组指定,这称为分散读(scatter read);对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写(gather write)。

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

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

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

10.4.1 辅助数据

辅助数据的概念:

  • 辅助数据通过sendmsg、recvmsg这两个函数,使用msghdr结构中的msg_control和msg_controllen成员发送和接收
  • 辅助数据的另一个称谓是“控制信息”

struct cmsghdr结构体

#include <sys/socket.h>
struct cmsghdr
{
	socklen_t cmsg_len;	/* 此结构体总的大小,通常使用CMSG_LEN宏进行赋值 */
	int cmsg_level;	/* originating protocol */
	int cmsg_type;	/* protocol-specific type */
	/* char cmsg_data[]; */	/* 数据 */
}

img

辅助数据案例一:

  • 下图展示了在一个控制缓冲区出现2个辅助数据对象的例子
  • msg_control指向第一个辅助数据对象。辅助数据对象的总长度由msg_controllen指定,每个辅助数据对象开头是一个struct cmsghdr结构体,在结构体的cmsg_type成员和实际数据之间可以有填充字节。从数据结尾到下一个辅助数据对象之前也可以有填充字节(不是所有实现都支持在单个控制缓冲区中存放多个辅助数据对象)

UNP编程:45---IO管理(recvmsg、sendmsg函数:struct msghdr)_sendmsg_03

辅助数据案例二:

  • 下图演示了一个通过UNIX域套接字传递描述符所用的cmsghdr结构体的格式(假设只传递了一个描述符,描述符存放在cmsg_data数组中。且在cmsg_type和cmsg_data之间没有填充字节)

image-20230510212624650

辅助数据测试宏:

  • 由于recvmsg返回的辅助数据可含有任意数目的辅助数据对象,为了对应用程序屏蔽可能出现的填充字节。定义了下面5个宏,以简化对辅助数据的处理:
#include <sys/socket.h>
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);

struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);

size_t CMSG_ALIGN(size_t length);

size_t CMSG_SPACE(size_t length);

size_t CMSG_LEN(size_t length);

unsigned char *CMSG_DATA(struct cmsghdr *cmsg);

CMSG_FIRSTHDR:返回辅助数据第一个cmsghdr结构的指针。若无辅助数据(或者msg_control为一个空指针,或者cmsg_len小于一个cmsghdr结构的大小)返回NULL
CMSG_NXTHDR:返回指向下一个cmsghdr结构的指针,若没有下一个辅助数据则返回NULL
CMSG_SPACE:用于设置下一个辅助数据总的大小(参数指定)
CMSG_LEN:参数为cmsghdr的cmsg_data成员的大小,然后将此大小+cmsghdr其他成员的大小作为cmsghdr结构体的总大小作为返回值返回,通常将返回值作为cmsg_len的值
CMSG_DATA:指向与cmsghdr结构关联的数据的第一个字节的指针
CMSG_ALIGN:

CMSG_LEN和CMSG_SPACE的区别在于:前者不计辅助数据对象中的数据部分之后可能的填充字节,因而返回的是用于存放在cmsg_len成员中的值,后者计上结尾处肯能的填充字节,因而返回的是为辅助数据对象动态分配空间的大小值

UNP编程:45—IO管理(recvmsg、sendmsg函数:struct msghdr)_mb6128aabee41d4的技术博客_51CTO博客

11 带外数据

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

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

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

12 地址信息函数

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

#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername(int sockdfd, 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的参数及返回值相同。

13 socket选项

如果说fcntl系统调用是控制文件描述符属性的通用POSIX方法,那么下面两个系统调用则是专门用来读取和socket文件描述符属性的方法:

#include <sys/socket.h>
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参数则指定选项的名字。下表中列举了socket通信中几个比较常用的socket选项。option_value和option_len参数分别是被操作选项的值和长度。不同的选项具有不同类型的值。

leveloption name数据类型说明
SOL_SOCKET(通用socket选项,与协议无关)SO_DEBUGint打开调试信息
同上SO_REUSEADDRint重用本地地址
同上SO_TYPEint获取socket类型
同上SO_ERRORint获取并消除socket错误状态
同上SO_DONTROUTEint不查看路由表,直接将数据发送给本地局域网内的主机。含义和send系统调用的MSG_DONTROUTE标志类似
同上SO_RCVBUFintTCP接收缓冲区大小
同上SO_SNDBUFintTCP发送缓冲区大小
同上SO_KEEPALIVEint发送周期性保活报文以维持连接
同上SO_OOBINLINEint接收到的带外数据将存留在普通数据的输入队列中(在线存留),此时我们不能使用带MSG_OOB标志的读操作来读取带外数据(而应该像读取普通数据那样读取带外数据)
同上SO_LINGERlinger若有数据待发送,则延迟关闭
同上SO_RCVLOWATintTCP接收缓冲区低水位标记
同上SO_SNDLOWATintTCP发送缓存区低水位标记
同上SO_RCVTIMEOint接收数据超时
同上SO_SNDTIMEOint发送数据超时
IPPROTO_IP(IPv4选项)IP_TOSint服务类型
同上IP_TTLint存活时间
IPPROTO_IPV6(IPv6选项)IPV6_NEXTHOPsockaddr_in6下一跳IP地址
同上IPV6_RECVPKTINFOint接收分组信息
同上IPV6_DONTFRAGint禁止分片
同上IPV6_RECVTCLASSint接收通信类型
IPPROTO_TCPTCP_MAXSEGintTCP最大报文段大小
同上TCP_NODELAYint禁止Nagle算法

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

​ 值得指出的是,对服务器而言,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效。这是因为连接socket只能由accept调用返回,而accept从listen监听队列接受的连接至少已经完成了TCP三次握手的前两个步骤(因为listen监听队列中的连接至少已进入SYN_RCVD状态),这说明服务器已经往被接受连接上发送出了TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设置,比如TCP最大报文段选项,该选项只能由同步报文段的选项来指定。

​ 对这种情况,Linux给开发人员提供的解决方案是:对监听socket设置这些socket选项,那么accept返回的连接socket将自动继承这些选项。这些socket选项包括:SO_DEBUG、SO_DONTROUT、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY。而对客户端而言,这些socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已完成。

13.1 SO_REUSEADDR选项

TCP连接主动关闭方将由FIN_WAIT_2状态进入TIME_WAIT状态,在TIME_WAIT状态下,该主机无法使用其占用的端口。可通过设置socket选项SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket地址。具体实现方法如下:

int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0)
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.port = htons(port);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));

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

13.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接收缓冲区和发送缓冲区的大小没有最小值限制。

​ 下面是一对客户端和服务器程序,它们分别修改TCP发送缓冲区和接收缓冲区的大小。

// 修改TCP发送缓冲区的客户端程序
#include <sys/socket.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <string.h>
#include <stdlib.h>

#define BUFFER_SIZE 512

int main(int argc, char* argv[])
{
    if(argc <= 2)
    {
        printf("usage: %s ip_address port_number send_bufer_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);
    /* 先设置TCP发送缓冲区的大小,然后立即读取之 */
    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缓冲区的服务器程序
#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>

#define BUFFER_SIZE 1024

int main(int argc, char* argv[])
{
	if(argc <= 2)
	{
		printf("usage: %s ip_address port_number recv_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(AF_INET, SOCK_STREAM, 0);
	assert(sock >= 0);
	int recvbuf = atoi(argv[3]);
	int len = sizeof(recvbuf);
	/* 先设置TCP接收缓冲区的大小,然后立即读取之 */
	setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof(recvbuf));
    getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, (socklen_t*)&len);
    printf("the tcp receive buffer size after setting is %d\n", recvbuf);
    
    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 = accetp(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;
}

13.3 SO_RCVLOWAT和SO_SNDLOWAT选项

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

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

13.4 SO_LINGER选项

​ SO_LINGE R选项用于控制close系统调用在关闭TCP连接时的行为。默认情况下,当我们使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区中残留的数据发送给对方。

​ 设置(或获取)SO_LINGER选项的值时,我们需要给setsockopt(或getsockopt)系统调用传递一个linger类型的结构体,其定义如下:

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

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

  1. l_onoff等于0。此时SO_LINGER选项不起作用,close用默认行为来关闭socket。
  2. l_onoff不为0,l_linger等于0。此时close系统调用立即返回,TCP模块将丢弃被关闭的socket对应的TCP发送缓冲区中残留的数据,同时给对方发送一个复位报文段。因此,这种情况给服务器提供了异常终止一个连接的方法
  3. l_onoff不为0,l_linger大于0.此时close的行为取决于两个条件:一是被关闭的socket对应的TCP发送缓冲区中是否还有残留的数据;而是该socket是阻塞的,还是非阻塞的。对于阻塞的socket,close将等待一段长为l_linger的时间,直到TCP模块发送完残留数据并得到对方的确认。如果这段时间内TCP模块没有发送完残留数据并得到对方的确认,那么close系统调用将返回-1并设置errno为EWOULDBLOCK。如果socket是非阻塞的,close将立即返回,此时我们需要根据其返回值和errno来判断残留数 据是否已经发送完毕。

14 网络信息API

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

telnet 127.0.0.1 80
telnet localhost www

​ 上面的例子中,telnet客户端程序是通过调用某些网络信息API来实现主机名到IP地址的转换,以及服务名称到端口号的转换的。

14.1 gethostbyname和gethostbyaddr

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

#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地址列表 */
}
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>

int main()
{
	char *ip = "10.0.0.204";
	struct in_addr addr;
	bzero(&addr, sizeof(addr));
	inet_pton(AF_INET, ip, &addr);
	struct hostent *host;
	bzero(&host, sizeof(host));
	host = gethostbyaddr(&addr, sizeof(addr), AF_INET);
	printf("h_name = %s\n", host->h_name);

	const char* name = "www.baidu.com";
	host = gethostbyname(name);
	printf("h_name = %s\n", host->h_name);
	int i = 0;
	for(i = 0; i < 2; i++)
	{
		char ip[INET_ADDRSTRLEN];
		inet_ntop(AF_INET, host->h_addr_list[i], ip, INET_ADDRSTRLEN);
		printf("%s\n", ip);
	}

	return 0;
}

h_name = localhost.localdomain
h_name = www.a.shifen.com
183.232.231.172
183.232.231.174

14.2 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 <sys/socket.h>
#include <netinet/in.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本身使用网络字节序,所以无需转换字节序 */
    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 != -1);
    buffer[result] = '\0';
    printf("the day time is: %s", buffer);
    close(sockfd);
    
	return 0;
}

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

14.3 getaddrinfo

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

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

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。例如,利用hints参数获取主机“ernest-laptop"上的"daytime"流服务信息。

struct addrinfo hints;
struct addrinfo* res;

bzero(&hints, sizeof(hints));
getaddrinfo("ernest-laptop", "daytime", &hints, &res);

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

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

14.4 getnameinfo

​ getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的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);

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

选项含义
NI_NAMEREQD如果通过socket地址不能获得主机名,则返回一个错误
NI_DGRAM返回数据报服务。大部分同时支持流和数据报的服务使用相同的端口号来提供两种服务。但端口512-514是例外。比如TCP的514端口提供的是shell登录服务,而UDP的514端口提供的是syslog服务(参见/etc/services文件)
NI_NUMERICHOST返回字符串表示的IP地址,而不是主机名
NI_NUMERICSERV返回字符串表示的十进制端口号,而不是服务名
NI_NOFQDN仅返回主机与名的第一部分。比如对主机名nebula.testing.com,getnameinfo只将nebula写入host缓存中

​ getaddrinfo和getnameinfo函数成功时返回0,失败则返回错误码。下面的函数可以将其返回的错误码转换成其字符串形式:

#include <netdb.h>
const char* gai_strerror(int error);

15 其它API

15.1 socketpair

在linux下,使用sockpair函数能够创建一对套接字进行进程间通信(IPC)。

函数原型如下:

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

int socketpair(int domain, int type, int protocol, int sv[2]);

domain:表示协议族,在Linux下只能为AF_LOCAL或者AF_UNIX。(自从Linux2.6.27后也支持SOCK_NONBLOCK和SOCK_CLOEXEC)

type:表示协议,可以是SOCK_STREAM或者SOCK_DGRAM。SOCK_STREAM是基于TCP的,而SOCK_DGRAM是基于UDP的

protocol:表示类型,只能为0

sv[2]:套接字柄对,该两个句柄作用相同,均能进行读写双向操作

返回值:0表示创建成功,-1表示创建失败,并且设置对应错误的errno,具体错误号如下所述:

EAFNOSUPPORT:本机上不支持指定的address。
EFAULT:地址sv无法指向有效的地址空间内。
EMFILE:已经达到了系统限制文件描述符,或者该进程使用过量的描述符
EOPNOTSUPP:指定的协议不支持创建的套接字对
EPROTONOSUPPORT:本机不支持指定的协议

注意:

  • 该函数只能用于UNIX域(Linux)下
  • 只能用于有亲缘关系的进程(或线程)间通信。
  • 所创建的套接字对的作用是一样的,均可读可写(而管道PIPE只能进行单向读或写)
  • 在读的时候,管道内必须有内容,否则将会阻塞;简而言之,该函数是阻塞的。

15.1.2 AF_INET域和AF_UNIX域socket通信原理对比

15.1.2.1 AF_INET域socket通信过程

image-20230306185532987

典型的TCP/IP四层模型的通信过程。

发送方、接收方依赖IP:Port来标识,即将本地的socket绑定到对应的IP端口上,发送数据时,指定对方的IP端口,经过Internet,可以根据此IP端口最终找到接收方;接收数据时,可以从数据包中获取到发送方的IP端口。

发送方通过系统调用send()将原始数据发送到操作系统内核缓冲区中。内核缓冲区从上到下依次经过TCP层、IP层、链路层的编码,分别添加对应的头部信息,经过网卡将一个数据包发送到网络中。经过网络路由到接收方的网卡。网卡通过系统中断将数据包通知到接收方的操作系统,再沿着发送方编码的反方向进行解码,即依次经过链路层、IP层、TCP层去除头部、检查校验等,最终将原始数据上报到接收方进程。

15.1.2.2 AF_UNIX域socket通信过程

典型的本地IPC,类似于管道,依赖路径名标识发送方和接收方。即发送数据时,指定接收方绑定的路径名,操作系统根据该路径名可以直接找到对应的接收方,并将原始数据直接拷贝到接收方的内核缓冲区中,并上报给接收方进程进行处理。同样的接收方可以从收到的数据包中获取到发送方的路径名,并通过此路径名向其发送数据。

image-20230306185956898

相同点

操作系统提供的接口socket(),bind(),connect(),accept(),send(),recv(),以及用来对其进行多路复用事件检测的select(),poll(),epoll()都是完全相同的。收发数据的过程中,上层应用感知不到底层的差别。

不同点

1 建立socket传递的地址域,及bind()的地址结构稍有区别:

socket() 分别传递不同的域AF_INET和AF_UNIX

bind()的地址结构分别为sockaddr_in(制定IP端口)和sockaddr_un(指定路径名)

2 AF_INET需经过多个协议层的编解码,消耗系统cpu,并且数据传输需要经过网卡,受到网卡带宽的限制。AF_UNIX数据到达内核缓冲区后,由内核根据指定路径名找到接收方socket对应的内核缓冲区,直接将数据拷贝过去,不经过协议层编解码,节省系统cpu,并且不经过网卡,因此不受网卡带宽的限制。

3 AF_UNIX的传输速率远远大于AF_INET

4 AF_INET不仅可以用作本机的跨进程通信,同样的可以用于不同机器之间的通信,其就是为了在不同机器之间进行网络互联传递数据而生。而AF_UNIX则只能用于本机内进程之间的通信。

15.2 getopt处理短选项

getopt()函数是用来分析命令行参数的,参数argc和argv分别代表参数个数和内容,与main()函数的命令行参数是一样的。分析命令行参数的时候就是根据这个字符串来的,将在下面进行介绍。

#inlcude <unistd.h>
int getopt(int argc, char* const argv[], const char *optstring);

getopt()会对argv中的条目进行顺序处理,其中第一个条码被忽略(对应的命令本身的字符串),之后对于argv数组中的每一个条目,getopt将其中使用“-”开头的字符串视为参数项(option element),参数项除开始的-字符外的所有字符被视为参数字符。

getopt函数运用一次只会查询一次会有中间变量来存储当前的位置,所以循环调用直到返回值为-1代表结束,参数能被正确的按照要求顺序读出。

getopt正常调用时,会返回对应的合法参数字符对应的ASCI码值。当遇到不合法的参数字符时(不包含在optstring中),会返回?。当所有的命令行条目被解析完成后,getopt返回-1.

#include <iostream>
#include <unistd.h>

using namespace std;

int main(int argc, char *argv[])
{
 int ret;
 ret = getopt(argc, argv, "iabc");
 while(ret != -1){
     cout << char(ret) << endl;
     ret = getopt(argc, argv, "iabc");
 }
 return 0;
}

image-20230527150150619

optstring参数

  • 单个字符,表示选项。如"abc"。
  • 单个字符后接一个冒号":",表示该选项后必须跟一个参数。参数紧跟在选项后或者以空格隔开。该参数的指针赋给optarg。(这是头文件定义的,属于全局变量)
  • 单个字符后跟两个冒号,表示该选项后必须跟一个参数。参数必须紧跟在选项后不能以空格隔开。该参数的指针赋给optarg。
  • optstring字符串使用字符"+“开始时,getopt会在遇到第一个非合法参数项时结束。而当optstring字符串使用”-"开始时,所有的非参数项的argv数组中的条目均被视为字符值为1的参数项的参数,也就是被视为-1 hello形式的调用,getopt返回值为1,而optarg指向字符串hello
  • 使用getopt()时,会犯的错误无外乎有两个:无法识别的选项(Invalid option) 和丢失选项参数(Missing option argument)
    通常情况下,getopt()在发现这两个错误时,会打印相应的错误信息,并且返回字符"?" 。例如,遇见无法识别的选项时会打印"invalid option",发现丢失参数时打印"option requires an argument"。但是当设置opterr为0时,则不会打印这些信息,因此为了便于发现错误,默认情况下,opterr都是非零值。
  • 如果你想亲自处理这两种错误的话,应该怎么做呢? 首先你要知道什么时候发生的错误是无法识别的选项,什么时候发生的错误是丢失选项参数。如果像上面描述的那样,都是返回字符"?“的话,肯定是无法分辨出的。有什么办法吗? 有! getopt()允许我们设置optstring的首字符为冒号”:“,在这种情况下,当发生无法识别的选项错误时getopt()返回字符”?“,当发生丢失选项参数错误时返回字符”:"。这样我们就可以很轻松地分辨出错误类型了,不过代价是getopt()不会再打印错误信息了,一切事物都由我们自己来处理了。

头文件的相关内容

#include <unistd.h>

extern char *optarg;	//选项的参数指针
extern int optind;	// 下一次调用getopt时,从optind存储的位置处重新检查选项。
extern int opterr;	//当dpterr=0时,getopt不向stderr输出错误信息。
extern int optopt;	//当命令行字符不包括在optstring中或者选项缺少必要的参数时,该选项存储在optopt中,getopt返回"?"
int getopt(int argc, char* const argv[], const char *optstring);

15.3 getopt_long

img

​ 其中操作对象Operands又称nonoption argument。

getopt_long()函数的工作方式与getopt()类似,只是它也接受长选项,以两个破折号开头。如果缩写是唯一的或与某些定义的选项完全匹配,则可以缩写长选项名称。长选项可以采用--arg=param--arg param形式参数。

int getopt_long(int argc, char *const argv[], const char *optstring, const struct option *longopts, int *longindex);
  • 注意与getopt()相比,使用getopt_long()需要加头文件<getopt.h>
  • getopt_long()除了会接受长选项其它概念和getopt是一样的;
  • 如果使用getopt_long()只想接受短选项,设置longoptsNULL即可;如果只想接受长选项,相应的设置optstringNULL即可;
  • 长选项名是可以使用缩写方式,比如:选项有--file \ --create,那么输入--c / --cr / --cre等都会被正确识别为create选项;
  • longopts是指向struct option数组的第一个元素的指针,struct option定义在<getopt.h>中;
  • longindex如果非NULL,则是返回是被到struct option数组中元素的位置指针;
struct option{
	const char *name;
	int has_arg;
	int *flag;
	int val;
};
/*
name:	长选项名
has_arg:	是否带参数或可选参数,这个值在getopt.h中有宏定义,如下:
	#define no_argument			0
	#define required_argument	1
	#define optional_argument	2
flag:	确定函数返回值的情况,如果flag==NULL,则识别选项后返回val(常用的如:
		设置val为长命令的短命令字符);否则,识别后getopt返回0,flag指向一个设置到val的变量;
val:设置为返回值,或者是falg指向的变量;这里注意不要写-1到val,否则其作用是getopt_long返回-1,然后停止解析选项;
*/

c, char* const argv[], const char *optstring);

15.3 getopt_long

[外链图片转存中…(img-RQtYuJzc-1708135683532)]

​ 其中操作对象Operands又称nonoption argument。

getopt_long()函数的工作方式与getopt()类似,只是它也接受长选项,以两个破折号开头。如果缩写是唯一的或与某些定义的选项完全匹配,则可以缩写长选项名称。长选项可以采用--arg=param--arg param形式参数。

int getopt_long(int argc, char *const argv[], const char *optstring, const struct option *longopts, int *longindex);
  • 注意与getopt()相比,使用getopt_long()需要加头文件<getopt.h>
  • getopt_long()除了会接受长选项其它概念和getopt是一样的;
  • 如果使用getopt_long()只想接受短选项,设置longoptsNULL即可;如果只想接受长选项,相应的设置optstringNULL即可;
  • 长选项名是可以使用缩写方式,比如:选项有--file \ --create,那么输入--c / --cr / --cre等都会被正确识别为create选项;
  • longopts是指向struct option数组的第一个元素的指针,struct option定义在<getopt.h>中;
  • longindex如果非NULL,则是返回是被到struct option数组中元素的位置指针;
struct option{
	const char *name;
	int has_arg;
	int *flag;
	int val;
};
/*
name:	长选项名
has_arg:	是否带参数或可选参数,这个值在getopt.h中有宏定义,如下:
	#define no_argument			0
	#define required_argument	1
	#define optional_argument	2
flag:	确定函数返回值的情况,如果flag==NULL,则识别选项后返回val(常用的如:
		设置val为长命令的短命令字符);否则,识别后getopt返回0,flag指向一个设置到val的变量;
val:设置为返回值,或者是falg指向的变量;这里注意不要写-1到val,否则其作用是getopt_long返回-1,然后停止解析选项;
*/
  • 25
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值