long blogs

进一步有进一步惊喜


  • Home
  • Archive
  • Tags
  •  

© 2025 long

Theme Typography by Makito

Proudly published with Hexo

Linux高性能服务器编程-读书笔记

Posted at 2021-01-06 linux 读书笔记 

第一章 TCP/IP协议族

1.1 TCP/IP 体系结构以及主要协议

数据链路层(ARP、Data Link RARP)、网络层(ICMP IP)、传输层(TCP UDP SCTP)、应用层(ping telnet OSPF DNS)四层协议组成TCP/IP协议族。

1.1.1 数据链路层

数据链路层实现了网卡几口的网络驱动程序,不同网卡设备有不同的驱动,数据链路层抽象了数据在物理媒介的传输。提供给上层统一的接口。

ARP协议(Address Resolve Protocal, 地址解析协议)、RARP(Reverse Address Resolve Protocal, 逆地址解析协议) 实现IP地址和物理地址(通常是MAC地址)相互转换。ARP协议负责 IP => MAC映射、RARP协议负责MAC => IP转换。

1.1.2 网络层

网络层实现数据包的选路和转发,即负责数据包从那条链路发送数据包。

IP协议(Internet Protocol, 因特网协议),根据数据包中的IP地址投递到目标主机。

ICMP协议(Internet Control Message Protocol, 因特网控制报文协议),用以检测网络连接,报文格式由8位类型+8位代码+16位校验和+不定长报文内容组成。报文类型:

  • 差错控制报文: 回应网络错误,类型3-网络不可达,类型5-重定向(代码0 - 网络重定向,1-主机重定向)
  • 查询报文:查询网络信息,类型8-目标是否可达

校验和: 对整个报文(头部和内容)进行循环冗余校验(Cyclic Redundancy Check, CRC)。ICMP协议标准文档RFC 792。

1.1.3 传输层

为主机程序提供端到端(end to end)的通讯。传输层为应用程序封装了一条端到端的逻辑通讯链路,负责数据的收发、链路的超时重连。网络层封装网络连接的细节,数据链路层(驱动)封装物理网络的电气细节。

TCP协议(Transmission Control Protocol, 传输控制协议),提供可靠的、面向连接和基于流(stream)的服务。使用超时重传、数据确认保证数据包被正确地发送到目的端,没有传输长度限制。

UDP协议(User Datagram Protocol, 用户数据报协议),不可靠、无连接基于数据报的服务,无法保证数据正确传到目的端,目的端通过校验发现数据错误则丢弃,并通知程序数据发送失败。

SCTP协议(Stream Control Transmission Protocal, 流控制传输协议)用来在Internet传输网络信号设计的。

1.1.4 应用层

应用层负责处理应用程序的逻辑,用户态中实现的。

ping 利用ICMP报文检测网络连接

telnet协议:远程登录协议

OSPF(Open Shortest Path First, 开放最短路径优先)协议是一种动态路由更新协议,路由器之间的通信,告知对方各自的路由信息。

DNS(Domain Name Service, 域名解析服务)协议域名到IP地址的转换。

1.2 封装

本层协议通过将在上层数据的基础上加上自己的头部信息(有时候包含尾部信息),以实现该层的功能,该过程称为封装。从应用层到链路层,层层添加数据。

TCP封装后的数据称为TCP报文,UDP封装后称为UDP数据包,IP数据包,数据链路层封装为帧(frame)。

帧的最大传输单元(Max Transmit Unit, MTU), 即帧最多能携带多少上层协议数据,受网络类型限制,以太网帧的MTU是1500字节。

1.3 分用

当帧数据到达目的主机时,从链路层到应用层,层层解析数据。这个过程称为分用(demultiplexing)。对于上层来说,下层的分用是透明的。不需要考虑下层的处理流程,只要该层发送的数据能够正确的发送到目的端的对应的层就行。

以太网帧类型值0x800说明帧数据为IP数据报。

以太网帧类型值0x806说明帧数据为ARP请求或应答报文。

第2章 IP协议详解

第5章 Linux网络编程基础API

socket地址API: socket包含一个IP地址和端口对(ip,port),唯一表示了TCP通信的一端。

socket基础API: 位于sys/sockect.h头文件中,包括创建socket,命名socket,监听socket,接受连接,发起连接,读写数据,获取地址信息,检测外带标记,读取和设置socket选项。

网络信息API: Linux提供一套网络信息API,主机名和IP地址之间的转换,服务名称和端口号之间的转换。在netdb.h。

5.1 socket地址API

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

大端序(big endian)和小端序(little endian)。大端序整数高位字节(23~31bit)存储在内存的低地址,低位字节放在高位地址。小端序,高位放高位,低位放低位。

整数0xffee在内存块中存储如下为大端序。ff为整数大端,放在了内存的低位。

1
2
3
0+++++++16+++++31
| ff | ee |
++++++++++++++++

判断大小端,只要看内存低位存储的是高位数据和低位数据。检查机器字节序代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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都是采用小端序,也称为主机字节序。

发送端将数据转换成大端字节序发送,接收端根据自身使用的字节序对数据进行处理。大端序也称为网络序。相同机器的两个进程通信也要考虑节序问题。(JAVA虚拟机采用大端序)

linux常用的主机字节序和网络字节序之间的转换。

1
2
3
4
5
6
7
#include<netinet/in.h>
/*host to network long*/
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
/*net to host long*/
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

5.1.2 通用socket地址

1
2
3
4
5
6
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};

sa_family成员是地址族类型(sa_family_t)的变量,地址族类型通常和协议族对应,协议族(protocol family,也称为domain)和地址族对应

协议族 地址族 描述
PF_UNIX AF_UNIX UNIX本地域协议族
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端口号和32bitIPv4地址,共16字节
PF_INET6 16bit端口号,32bit流标识,128bitIPV6地址,32bit范围id,共26字节

14字节的sa_data不能满足需求,所以定义下面的通用结构体。

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

5.1.3 专用socket地址

UNIX本地域协议族使用下面的结构体

1
2
3
4
5
6
#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。

IPv4

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
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地址, 网络字节序表示*/
}

5.1.4 IP地址转换函数

字符串的ip地址转成数字,数字转成ip地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<arpa/inet.h>
/*点分十进制字符串转化网络字节整数表示,失败返回INADDR_NONE*/
in_addr inet_addr(const char* strptr);

/*点分十进制转换为网络字节整数表示,成功返回1,失败返回0*/
int inet_aton(const char *cp, struct in_addr* inp);

/*网络字节整数转换为点分十进制字符串,不可重人*/
char* inet_ntoa(struct in_addr in);

/*网络IP字符串转换成整数的IP地址,由af确定是IPv4或IPv6,成功返回1,失败返回0,并设置errno*/
int inet_pton(int af, const char* src, void *src);

/*整数地址转换成IP字符串,af确定IPv4还是IPv6,,cnt 目标存储单元的大小。成功时返回地址,失败返回NULL,并设置errno*/
const char* inet_ntop(int af, const void* src, char *dst, socklent_t cnt);

1
2
3
4
5
6
/*af常用宏*/
// AF_INET/AF_INET6
/*cnt 常用宏*/
#include<netinet/in.h>
#define INET_ADDRESTRLEN 16 /*ipv4*/
#define INET6_ADDRSTRLEN 46 /*ipv6*/

inet_ntoa函数不可重入测试代码

1
2
3
4
5
6
7
8
9
10
void inet_ntoa_test() {
struct in_addr addr;
addr.s_addr = 195512;
char *szValue1 = inet_ntoa(addr);
printf("address[%p] 1 : %s\n", szValue1, szValue1);
addr.s_addr = 10043424;
char *szValue2 = inet_ntoa(addr);
printf("address[%p] 1 : %s\n", szValue1, szValue1);
printf("address[%p] 2 : %s\n", szValue2, szValue2);
}

输出:

1
2
3
address[0x7f531625e4d0] 1 : 184.251.2.0
address[0x7f531625e4d0] 1 : 32.64.153.0
address[0x7f531625e4d0] 2 : 32.64.153.0

可以看得出来,返回的字符串指向的地址是一样的,调用之后需要复制生成的值。

5.2 创建socket

socket是可读、可写、可控制、可关闭的文件描述符。

1
2
3
#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)。本地协议族PF_UNIX.
  • type参数指定服务类型。SOCK_STREAM流服务,SOCK_DGRAM数据报服务。TCP/IP协议族而言SOCK_STREAM表示使用TCP协议,SOCK_DGRAM表示使用UDP协议。非阻塞socket类型SOCK_NONBLOCK,fork调用创建子进程中关闭该socket的类型SOCK_CLOEXEC。
  • protocol协议表示在前两个参数构成的协议集合下,再进一步确定具体的协议。一般是设为0.

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

5.3 命名socket

创建了socket指定了地址族,但是并没有指定地址族中的具体socket地址。创建了一个TCP的socket,但是并没有指定这个socket的具体地址。将一个socket与socket地址绑定(bind)称为给socket命名。作为服务端的话需要绑定一个固定的地址,方便其他人连。作为客户端可以不绑定socket,使用自动分配的socket去连接服务器。

1
2
3
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socketlen_t addr_len);

bind函数将my_addr所对应的地址绑定到sockfd描述符中,addrlen该socket地址的长度。

绑定成功返回0,失败返回-1,并设置errno.常见的errno,EACCES(地址是受保护的地址,普通用户绑定到0~1024端口中)和EADDRINUSE(被绑定的地址正在使用中).

5.4 监听socket

socket绑定之后需要开启监听队列才能处理客户连接。

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

sockfd被监听的socket, backlog内核监听队列的最大长度,监听队列长度超过backlog,服务器不在受理新的客户连接,客户端收到ECONNREFUSED错误信息。2.2的内核版本之前的linux,backlog参数表示处于半连接(SYN_REVD)和完全连接状态(ESTABLISHED)的socket的上限。2.2内核版本之后,只表示处于完全连接状态的socket的上限,处于半连接状态的socket上限由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。典型的值为5。完整的连接最多有(backlog + 1)个。

5.5 接受连接

从listen监听队列中接受一个连接

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

sockfd执行listen系统调用的监听的socket。addr参数接收连接端的socket地址,长度由addrlen参数决定。accept成功时返回一个新的连接socket,这个socket唯一的标识了被接受的这个连接。这个socket和监听的socket不一样,可以读写该socket与客户端通信。accept失败返回-1并设置errno。测试的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>
#include <libgen.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <errno.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);

// 创建socket
int sock = socket(PF_INET, SOCK_STREAM, 0);
printf("create sockect %d.\n", sock);
assert(sock >= 0);

// 绑定socket
printf("bind socket %d.\n", sock);
int ret = bind(sock, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);

// 监听socket
printf("listen socket %d.\n", sock);
ret = listen(sock, 5);
assert(ret != -1);

sleep(20);
struct sockaddr_in client;
socklen_t client_addrlenght = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlenght);
printf("accept socket %d\n", connfd);
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;
}

测试结果:

1
2
3
4
5
6
./accept_test 127.0.0.1 12345
create sockect 3.
bind socket 3.
listen socket 3.
accept socket 4
connected with ip: 127.0.0.1 and port: 53064

5.6 发起连接

客户端通过系统调用来主动连接服务器

1
2
3
#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唯一的标识这个链接。建立连接错误返回-1并设置errno。常见的errno是ECONNERFUSED目标端口不存在,连接被拒绝。ETIMEDOUT连接超时。

5.7 连接关闭

关闭连接对应的socket,可以使用普通文件描述符的系统调用完成。

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

fd为待关闭的socket,close是将fd的引用技术减1。当fd的引用计数为0时,才真正的关闭连接。在多进程程序中,一次fork系统调用默认将使父进程中打开的socket+1,我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。无论如何都要立即终止连接,可以使用shutdown系统调用。

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

sockfd为待关闭的socket,howto决定怎么关闭。

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

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

5.8 数据读写

对文件的读写操作read和write同样适用于socket。还提供了专门的函数。

TCP流数据读写系统调用

1
2
3
4
#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参数分别指定读缓冲区的位置和大小,flag通常设置为0,recv成功时返回实际读取到的数据的长度,可能小于期望的长度,需要多次调用recv才能读取到完整数据。recv返回0说明对方关闭连接,出错返回-1并设置errno。

选项 含义 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

使用MSG_OOB发送和接收数据

接收数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <libgen.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <time.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);

time_t t = time(NULL);
printf("create socket %ld\n", t);
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

t = time(NULL);
printf("bin socket %ld\n", t);
int ret = bind(sock, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);


t = time(NULL);
printf("listen socket %ld\n", t);
ret = listen(sock, 5);
assert(ret != -1);

struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
t = time(NULL);
printf("accept sockect %ld\n", t);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
t = time(NULL);
printf("accept connect fd %d, %ld\n", connfd, t);

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;
}

发送数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <libgen.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <time.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";
time_t t = time(NULL);
printf("start send normal data %ld\n", t);
send(sockfd, normal_data, strlen(normal_data), 0);
t = time(NULL);
printf("finish send normal data %ld\n", t);
printf("start send oob data %ld\n", t);
send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
t = time(NULL);
printf("finish send oob data %ld\n", t);

send(sockfd, normal_data, strlen(normal_data), 0);
}

close(sockfd);
return 0;
}

5.8.2 UDP数据读写

函数调用

1
2
3
4
#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);

flags的值含义和TCP的flags的值一致。返回值含义和TCP的一致。recvfrom/sendto也可以使用Stream的socket数据读写,需要把最后两个参数设为NULL就行。

5.8.3 通用数据读写

可以用于TCP和UDP数据的读写

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

msghdr定义

1
2
3
4
5
6
7
8
9
10
struct msghdr
{
void* msg_name; /*socket地址*/
socketlen_t msg_namelen; /*socket地址的长度*/
struct iovec* msg_iov; /*分散的内存块*/
int msg_iovlen; /*分散内存块的数量*/
void* msg_control; /*指向辅助数据的大小*/
socklen_t msg_controllent; /*辅助数据的大小*/
int msg_flags; /*复制函数中的flag参数,调用过程中更新*/
};

msg_name成员指向一个socket地址结构变量,指定通讯对方的地址,TCP连接没有意义,设置为NULL。

msg_iov结构体指针

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

msg_iovlen指定这样的msg_iov对象有多少个。对于recvmsg,读取到的数据放在msg_iovlen块分散的内存中,内存的位置和长度则由msg_iov指向的数组指定,称为分散读(scatter read)。对于sendmsg,msg_iovlen块分散的内存将被一并发送,称为集中写(gather write);

msg_control和msg_controllen用于辅助数据的传送。

msg_flags不用指定,会自己复制recvmsg/sendmsg的flag参数的内容。recvmsg和sendmsg的返回值和send/recv的flags参数相同。

5.9 外带标记

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

sockatmark判断sockfd是否处于带外标记,即下一个读取到的数据是否是带外数据。是返回1,使用MSG_OOB标志接收recv带外数据。不是返回0;

5.10 地址信息函数

1
2
3
#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);

getsockname获得sockfd对应的本端的socket地址,socket的长度存储于address_len中。socket地址大于address的大小,socket地址被截断。成功返回0,失败返回-1,并设置errno;

getpeername获得sockfd对应的远端socket地址,含义和getsockname一致。

5.11 socket选项

设置和读取socket文件描述符属性的方法

1
2
3
#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指定选项名称,option_value选项的值,option_len选项长度。

level: SOL_SOCKET(通用socket选项,与协议无关)

option_name 数据类型 说明
SO_DEBUG int 打开调试信息
SO_REUSEADDR int 重用本地址
SO_TYPE int 获取socket类型
SO_ERROR int 获取并清楚socket错误状态
SO_DONTROUTE int 不查看路由器表,直接将数据发送给本地局域网内的主机。含义和send系统调用的MSG_DONTROUTE标志类似。
SO_RCVBUF int TCP接收缓冲区大小
SO_SNDBUF int TCP发送缓冲区大小
SO_KEEPALIVE int 发送周期性报文以维持连接
SO_OOBINLINE int 接收到的带外数据将存留在普通数据的输入队列中(在线存留),此时我们不能使用带MSG_OOB标志来读取带外数据(需要像读取普通数据那样读取带外数据)
SO_LINGER linger 若有数据待发送,则延迟关闭
SO_RCVLOWAT int TCP接收缓存区低水位标记
SO_SNDLOWAT int TCP发送缓冲区低水位标记
SO_RCVTIMEO timeval 接收数据超时
SO_SNDTIMEO timeval 发送数据超时

level:IPPROTO_IP(IPv4选项)

option_name 数据类型 说明
IP_TOS int 服务类型
IP_TTL int 存活时间

level:IPPROTO_IPV6(IPv6选项)

option_name 数据类型 说明
IPV6_NEXTHOP sockaddr_in6 下一跳的地址
IPV6_RECVPKTINFO int 接收分组信息
IPV6_DONTFRAG int 禁止分片
IPV6_RECVTCLASS int 接收通讯类型

level:IPPROTO_TCP(TCP选项)

option_name 数据类型 说明
TCP_MAXSEG int TCP最大报文段大小
TCP_NODELAY int 禁止Nagle算法

部分的socket选项在调用listen系统调用前设置才有效,有些socket选项却应该在TCP同步报文段设置。解决方案:对监听socket设置这些socket选项,那么accept返回的连接socket将自动继承这些选项(SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SND_BUF、SO_SNDLOWAT、TCP_MAXSEG、TCP_NODELAY),客户端,这些socket选项应该在调用connect函数之前设置。

5.11.1  SO_REUSEADDR选项

通过设置该选项,socket可以强制使用被处于TIME_WAIT状态的连接占用的socket地址。也可以通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket,使得TCP根本不进入TIME_WAIT状态,允许应用程序立即重用本地的socket地址。

5.11.2 SO_RCVBUF和SO_SNDBUF选项

 TCP接收缓冲区和发送缓冲区的大小。设置大小时,将值加倍,并且不得小于某个值。TCP接收缓冲区最小值是256字节,发送区最小值是2048字节。这样做的目的就是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞。也可以修改内核参数/proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem强制TCP接收和发送缓冲区没有大小限制。

5.11.3 SO_RCVLOWAT和SO_SNDLOWAT选项

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

5.11.4 SO_LINGER选项

SO_LINGER选项用于控制close系统调用关闭TCP连接时的行为。默认情况下,使用close关闭socket时,close将立即返回,将该socket发送缓冲区残留数据发送给对方。linger类型

1
2
3
4
5
6
#include<sys/socket.h>
struct linger
{
int l_onoff; /* 开启(非0)还是关闭(0)该选项*/
int l_linger; /* 滞留时间*/
}
  • l_onoff等于0。此时SO_LINGER不起作用,close用默认行为关闭socket。
  • l_onoff不为0。l_linger等于0。此时close系统调用立即返回,TCP模块将丢弃被关闭的socket发送缓冲区残留的数据,同时发送一个复位报文段。可以用来处理异常终止一个连接的方法。
  • l_onoff不为0,l_linger大于0。阻塞的socket,close等待l_linger的时间,等待发送所有的残留数据并得到对方的确认。超时返回-1,并设置errno为EWOULDBLOCK。非阻塞socket,close立即返回,根据errno判断残留数据是否已经发送完毕。

5.12 网络信息API

5.12.1 gethostbyname和gethostbyaddr

1
2
3
4
5
6
7
8
9
10
11
#include<netdb.h>
struct hostent
{
char* h_name; /* 主机名*/
char** h_aliases; /* 主机别名列表,可能有多个*/
int h_addrtype; /* 地址类型(地址族)*/
int h_length; /* 地址长度*/
char** h_addr_list; /* 按网络字节序列出的主机IP地址列表*/
}
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);

gethostbyname根据域名获得主机完整信息,gethostbyaddr函数根据IP获得主机完整信息。type参数包括AF_INET和AF_NET6

5.12.2 getservbyname和getservbyport

1
2
3
4
5
6
7
8
9
10
#include<netdb.h>
struct servent
{
char* s_name; /* 服务名称*/
char** s_aliases; /* 服务的别名列表,可能有多个*/
int s_port; /* 端口号*/
char* s_proto; /* 服务类型,通常是tcp或者udp*/
}
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);

根据名称或根据端口获得服务,主要是读取/ect/services文件夹获得服务信息。

5.12.3 getaddrinfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<netdb.h>
struct addrinfo
{
int ai_flags; /**/
int ai_family; /*地址族*/
int ai_socktype; /*服务类型SOCK_STREAM或SOCK_DGRAM*/
int ai_protocol; /**/
socklent_t ai_addrlen; /*socket地址长度*/
char* ai_canonname; /*主机别名*/
struct sockaddr* ai_addr; /*socket地址*/
struct addrinfo* ai_next; /*下一个sockinfo对象*/
}
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
void freeaddrinfo(struct addrinfo* res);

getaddrinfo即能通过主机名获得IP地址,也可以通过服务名获得端口号。是否可重入取决于内部调用的gethostbyname和getservbyname函数是否是可重入版本。

ai_protocol指具体协议,与socket系统调用第三个参数一致,通常设置为0。

ai_flags如下表按位或

选项 含义
AI_PASSIVE 在hints参数中设置,表示调用者是否会取得的socket地址用于被动打开。服务器通常需要设置它,表示接受任何本地socket地址上的服务请求,客户端不能设置它。
AI_CANONNAME 在hints参数中设置,告诉getaddrinfo函数返回主机的别名。
AI_NUMBERICHOST 在hints参数中设置,表示hostname必须是用字符串表示的IP地址从而避免了DNS查询。
AI_NUMBERICSERV 在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。

5.12.4 getnameinfo

getnameinfo函数能通过socket地址同时获得以字符串表示的主机名和服务名,是否可重入取决于内部使用的gethostbyaddr和getservbyport函数是否可重入版本。

1
2
#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行为。

flags参数

参数 含义
NI_NAMERQD 如果通过socket地址不能获得主机名,则返回一个错误。
NI_DGRAM 返回数据报服务。大部分同时支持流和数据报服务使用相同端口号来提供。端口512-514例外,TCP的514提供的是shell登录服务。
NI_NUMBERICHOST 返回字符串表示的IP地址而不是主机名
NI_NUMBERICSERV 返回字符串表示的十进制端口号而不是服务名
NI_NOFQDN 仅返回主机域名的第一部分,nebula.testing.com只存储nebula写入缓存中。

getaddrinfo和getnameinfo函数成功时返回0,失败返回错误码。

选项 含义
EAI_AGAIN 调用临时失败,提示应用程序过后再试
EAI_BADFLAGS 非法ai_flags值
EAI_FAIL 名称解析失败
EAI_FAMILY 不支持ai_family参数
EAI_MEMORY 内存分配失败
EAI_NONAME 非法的主机名或服务名
EAI_OVERFLOW 用户提供的缓冲区溢出,仅发生在getnameinfo调用中
EAI_SERVICE 没有支持的服务。使用数据报来寻找ssh服务,ssh只能使用流服务。
EAI_SOCKTYPE 不支持的服务类型。hints的ai_socktype和ai_protocol不一致,第一个指定SOCK_DGRAM,后者使用IPROTO_TCP会导致该错误。
EAI_SYSTEM 系统错误,错误值在errno中

错误码转成字符串形式

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

第6章 高级I/O函数

  • 用于创建文件描述符的函数,包括pipe、dup/dup2函数。
  • 用于读写数据的函数,包括readv/writev、sendfile、mmap/munmap、splice和tee函数。
  • 用于控制I/O行为和属性的函数,包括fcntl函数。

6.1 pipe函数

1
2
#include<unistd.h>
int pipe(int fd[2]);

fd[0]和fd[1]分别构成管道两端,fd[0]只能用于从管道读出数据,fd[1]只能用于往管道写入数据。数据流fd[1]-->fd[0],管道都是阻塞的,读空阻塞,写满阻塞。想要实现双向通讯需要两个管道。写端关闭(引用计数为0),读端read反0(EOF)。读端关闭,写端write失败,引发SIGPIP信号。传输字节流,Linux 2.6.11之后默认65536字节,fcntl可修改容量。

创建双向管道的函数

1
2
3
4
5
6
#include<sys/types.h>
#include<sys/socket.h>
/*
*return 0 success; -1 failed,设置errno
*/
int socketpair(int domain, int type, int protocol, int fd[2]);

前3个入参socket函数含义一致。domain使用AF_UNIX,只能本地使用双向管道。fd[2]参数可读可写。

6.2 dup函数和dup2函数

把标准输入重定向到一个文件或标准输出重定向到一个网络连接。使用dup或dup2函数完成。

1
2
3
#include<unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <libgen.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 = 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{
close(STDOUT_FILENO);
dup(connfd);
printf("abcd\n");
close(connfd);
}
close(sock);
return 0;
}

readv函数和writev函数

readv函数是将数据从文件描述符读到分散的内存块中,分散读。writev函数则将多块分散内存数据一并写入文件描述符中,集中写。

1
2
3
#include<sys/uio.h>
ssize_t readv(int fd, const struct iovec* vector, int count);
ssize_t writev(int fd, const struct iovec* vector, int count);

例子:

demo6_3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <stdbool.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>

#define BUFFER_SIZE 1024
static const char* status_line[2] = {"200 OK", "500 Internal server error"};
int main(int argc, char* argv[]) {
if (argc <= 3){
printf("usage: %s ip_address port_number filename\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
const char* file_name = argv[3];

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{
/*HTTP应答的状态行、头部字段和一个空行的缓存区*/
char header_buf[BUFFER_SIZE];
memset(header_buf, '\0', BUFFER_SIZE);
char* file_buf;
struct stat file_stat;
bool valid = true;
int len = 0;
if (stat(file_name, &file_stat) < 0) { /*目标文件不存在*/
valid = false;
}else{
if (S_ISDIR(file_stat.st_mode)) {
/*目标文件是一个目录*/
valid = false;
} else if (file_stat.st_mode & S_IROTH){
/*当前用户有读取目标文件的权限*/
int fd = open(file_name, O_RDONLY);
file_buf = calloc(1, file_stat.st_size + 1);
if (read(fd, file_buf, file_stat.st_size) < 0){
valid = false;
}
}else{
valid = false;
}
}
if (valid) {
ret = snprintf(header_buf, BUFFER_SIZE -1, "%s %s\r\n",
"HTTP/1.1", status_line[0]);
len += ret;
ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len,
"Content-Length: %d\r\n", file_stat.st_size);
len += ret;
ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s", "\r\n");
/*使用writev将header_buf和file_buf的内容一起写出*/
struct iovec iv[2];
iv[0].iov_base = header_buf;
iv[0].iov_len = strlen(header_buf);

iv[1].iov_base = file_buf;
iv[1].iov_len = file_stat.st_size;
ret = writev(connfd, iv, 2);
}else{
/*目标文件无效,内部错误*/
ret = snprintf(header_buf, BUFFER_SIZE -1, "%s %s\r\n",
"HTTP/1.1", status_line[1]);
len += ret;
ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s",
"\r\n");
send(connfd, header_buf, strlen(header_buf), 0);
}
close(connfd);
free(file_buf);
}

close(sock);
return 0;
}

6.4 sendfile函数

sendfile函数处于内核中两个文件描述符之间直接传递数据,没有内核缓冲区和用户缓冲区之间数据拷贝,零拷贝,效率高。

1
2
#include<sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);

in_fd待读出内容的文件描述符,out_fd写入内容的文件描述符。offset参数指定从读入文件流的那个位置开始读,为空默认起始位置。count为传输字节数。sendfile成功返回字节数,失败-1设置errno。要求:in_fd必须支持mmap函数,必须指向真实文件,不能是socket。out_fd必须是一个socket。

demo6.4 文件传输

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<stdio.h>
#include<stdlib.h>
#include <libgen.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <strings.h>
#include <errno.h>
#include <unistd.h>
#include <sys/sendfile.h>

int main(int argc, char* argv[]){
if (argc <= 3){
printf("usage: %s ip_address port_number filename\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
const char* file_name = argv[3];

int filefd = open(file_name, O_RDONLY);
assert(filefd > 0);
struct stat stat_buf;
fstat(filefd, &stat_buf);

struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
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_addrlenght = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlenght);
if (connfd < 0){
printf("errno is: %d\n", errno);
}else{
sendfile(connfd, filefd, NULL, stat_buf.st_size);
close(connfd);
}
close(sock);
return 0;
}

和demo6_3相比,不需要自己申请空间,实现了文件的传送,效率更高。

6.5 mmap函数和munmap函数

mmap函数申请一段内存空间,可作为进程间通讯的共享内存,也可以将文件直接映射到里面。munmap函数释放mmap申请的内存空间。

1
2
3
#include<sys/mman.h>
void* mmap(void *start, size_t lenght, int prot, int flags, int fd, off_t offset);
int munmap(void *start,size_t length);

start参数允许用户使用某个特定地址段作为这段内存的起始地址。设置为NULL由系统自动分配。length设定内存段长度。prot设置内存段的访问权限,以下可按位或。

序号 值 说明
1 PROT_READ 内存段可读
2 PROT_WRITE 内存段可写
3 PROT_EXEC 内存段可执行
4 PROT_NONE 内存段不能被访问

flags控制内存段内容被修改后程序行为,常用的值如下。可按位或。MAP_SHARED和MAP_PRIVATE互斥。

序号 值 说明
1 MAP_SHARED 在进程间共享这段内存,对该段内存的修改将被反映到文件中。提供进程间共享内存的POSIX方法
2 MAP_PRIVATE 内存段为调用进程私有,对该段内存段的修改不会反映到被映射的文件中。
3 MAP_ANONYMOUS 这段内存不是从文件映射而来的,内容初始化为0,mmap后两个参数被忽略。
4 MAP_FIXED 内存段必须位于start参数指定的地址处,start必须是内存页面大小(4096)的整数倍
5 MAP_HUGETLB 按照”大内存页面”来分配内存空间。“大内存页面”的大小可通过/proc/meminfo文件查看

fd被映射文件对应的描述符,一般open获得。offset参数设置从文件的何处开始映射。

mmap函数成功返回目标内存区域的指针,失败返回MAP_FAILED((void*)-1,并设置errno。munmap函数成功时返回0,失败返回-1设置errno。

6.6 splice 函数

用于两个文件描述符之间移动数据,零拷贝。

1
2
#include<fcntl.h>
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);

fd_in参数是待输入数据的文件描述符。如果是管道文件描述符,off_in参数必须设置为NULL。fd_in不是管道文件描述符(socket)等,off_in表示从输入数据流的何处开始读取数据。off_in为NULL,表示从输入数据流的当前偏移位置读入。off_in不为NULL,具体偏移位置。fd_out/off_out与上述输入一致。len控制数据移动长度。flags参数控制数据如何移动,可以按位或。

序号 值 含义
1 SPLICE_F_MOVE 如果合适的话,按整页内存移动数据。由于有bug。从内核2.6.21后,该flags没有任何效果
2 SPLICE_F_NONBLOCK 非阻塞的splice操作,但实际效果还是受文件描述符本身的阻塞状态影响。
3 SPLICE_F_MORE 给内核一个提示:后续的splice调用将读取更多的数据。
4 SPLICE_F_GIFT 对splice没有效果

splice函数,fd_in和fd_out至少有一个是管道文件描述符。splice函数调用成功时返回移动字节的数量。可能返回0,没有数据需要移动,这发生从管道中读取数据(fd_in是管道文件描述符)而管道没有被写入任何数据时。splice函数返回-1并设置errno。常见errno表。

序号 值 说明
1 EDADF 参数所指的文件描述符有错
2 EINVAL 目标文件系统不支持splice,或目标文件以追加方式打开,或两个文件描述符都不是管道文件描述符,或者某个offset参数被用于不支持随机访问的设备
3 ENOMEM 内存不够
4 ESPIPE 参数fd_in或fd_out是管道文件描述符,而off_in或off_out不为NULL。

零拷贝回射服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#define _GNU_SOURCE
#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>
#include <fcntl.h>
#include <libgen.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;
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_addrlenght = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlenght);
if (connfd < 0){
printf("errno is: %d\n", errno);
}else{
int pipefd[2];
assert(ret != -1);
ret = pipe(pipefd);

/*将connfd上流入客户端的数据定向到管道中*/
ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);

/*将管道的输出重定向到connfd客户连接文件描述符*/
ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);

close(connfd);
}
close(sock);
return 0;
}

6.7 tee函数

tee函数在管道文件描述符之间复制数据,零拷贝操作。不消耗数据,因此源文件描述符上的数据仍然可以可以后续读操作,tee函数的原型。

1
2
#include<fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

参数与splice, fd_in和fd_out都是管道描述符。成功复制时返回两个文件描述符之间复制的数据量(字节数)。返回0表示没有复制任何数据,失败返回-1设置errno。

实现tee程序

实现tee程序,同时将数据输出到控制台和文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#define _GNU_SOURCE
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char* argv[]){

if (argc != 2){
printf("usage: %s <file>\n", argv[0]);
return 1;
}
int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);
assert(filefd > 0);

int pipefd_stdout[2];
int ret = pipe(pipefd_stdout);
assert(ret != -1);

int pipefd_file[2];
ret = pipe(pipefd_file);
assert(ret != -1);

ret = splice(STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);

ret = tee(pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK);
assert(ret != -1);

ret = splice(pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);

ret = splice(pipefd_stdout[0], NULL, STDOUT_FILENO, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);

assert(ret != -1);

close(filefd);
close(pipefd_stdout[0]);
close(pipefd_stdout[1]);
close(pipefd_file[0]);
close(pipefd_file[1]);
return 0;
}

fcntl函数

file control, 提供对文件描述符各种控制操作。还有ioctl比fcntl能够执行更多的控制。fcntl为POSIX规范指定的方法。

1
2
#include<fcntl.h>
int fcntl(int fd, int cmd, ...);

fd被操作的文件描述符,cmd参数指定执行何种类型的操作。

  • 复制文件描述符
操作 含义 第三个参数类型 成功时返回值
F_DUPFD 创建一个新的文件描述符,其值大于或等于arg long 新创建的文件描述符的值
F_DUPFD_CLOEXEC 与F_DUPFD类似,不过创建文件描述符的同时,设置其close-on-exec标识 long 新创建的文件描述符的值
  • 获取和设置文件描述符的标识
操作 含义 第三个参数的类型 成功时返回值
F_GETFD 获得fd标志,比如close-on-exec标志 无 fd的标志
F_SETFD 设置fd的标志。 long 0
  • 获取和设置文件描述符的状态
操作 含义 第三个参数类型 成功返回值
F_GETFL 获取fd的状态标志,可以由open系统调用设置的标志(O_APPEND、O_CREAT),访问模式(O_RDONLY、O_WRONLY和O_RDWR) void fd的状态标志
F_SETFL 设置fd的状态标志,但部分标志是不可能被修改 long 0
  • 管理信号
操作 含义 第三个参数类型 成功返回值
F_GETOWN 获得SIGIO和SIGURG信号的宿主进程的PID或进程组的组ID 无 信号的宿主进程的PID或进程组的组ID
F_SETOWN 设置SIGIO和SIGURG信号的宿主进程的PID或进程组的组ID long 0
F_GETSIG 获取当应用程序被通知fd可读或可写时,是哪个信号通知该事件 无 信号值,0表示SIGIO
F_SETSIG 设置当fd可读或可写时,系统应该触发哪个信号来通知应用程序 long 0
  • 操作管道容量
操作 含义 第三个参数的类型 成功时的返回值
F_SETPIPE_SZ 设置由fd指定的管道的容量。/proc/sys/fs/pipe-size-max内核参数指定了fcntl能设置的管道容量的上限 long 0
F_GETPIPE_SZ 获取由fd指定的管道的容量 无 管道容量

将文件描述符设置为非阻塞。

1
2
3
4
5
6
7
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}

第7章 linux服务器程序规范

  • 一般为后台进程(守护进程daemon),父进程通常为init进程
  • 含有日志系统,至少能够输出到日志文件
  • 非root身份运行,有自己的运行账户
  • 可配置的,多命令行或配置文件启动管理
  • 生成pid文件到/var/run目录中,用以记录后台进程的PID
  • 考虑系统资源和限制,能够承受负荷(文件描述符、内存总量)

7.1 日志

7.1.1 Linux系统日志

syslogd为旧版的系统使用守护进程处理系统日志,rsyslogd为升级版的。rsyslogd可以接收用户进程输出日志和内核输出日志。用户可以调用syslog函数生成系统日志。日志输出到/dev/log中。rsyslogd监听该文件用来监听用户进程输出。内核日志之前使用rklogd来管理,现在rsyslogd额外模块实现了相同的功能。内核使用printk等函数打印到内核环状缓存(ring buffer)中,环状缓存直接映射到/proc/kmsg文件中。rsyslogd读取该文件获得内核日志。rsyslogd得到日志之后,输出到日志文件。调试信息/var/log/debug文件,普通信息/var/log/messages文件,内核消息/var/log/kern.log。如何分配可在rsyslogd配置文件中设置。/etc/rsyslog.conf为rsyslogd配置文件。

配置文件可以设置项:

  • 内核日志输入路径
  • 是否接收UDP日志及其监听端口(默认514,/etc/service文件)
  • 是否接收TCP日志及其监听端口
  • 日志文件权限,包含哪些子配置文件/etc/rsyslog.d/*.conf
  • rsyslogd的子配置文件指定各类日志的目标存储文件

7.1.2 syslog函数

应用程序使用syslog函数与rsyslogd守护进程通讯。

1
2
#include<syslog.h>
void syslog(int priority, const char* message, ...);

变参,第二个参数message和第三个参数形成结构化输出,priority设施值与日志级别按位或。设施值默认值是LOG_USER。

日志级别

1
2
3
4
5
6
7
8
9
#include<syslog.h>
#define LOG_EMERG 0 /*系统不可用*/
#define LOG_ALERT 1 /*报警,需要立即采取动作*/
#define LOG_CRIT 2 /*非常严重的情况*/
#define LOG_ERR 3 /*错误*/
#define LOG_WARNING 4 /*警告*/
#define LOG_NOTICE 5 /*通知*/
#define LOG_INFO 6 /*信息*/
#define LOG_DEBUG 7 /*调试*/

openlog函数可以改变syslog的输出方式,进一步结构化日志内容

1
2
#include<syslog.h>
void openlog(const char* ident, int logopt, int facility);

ident参数指定的字符串被添加到日志消息的时间和日期之后,常常设置为程序的名字。logopt参数影响后续的syslog的调用行为,参数值可按位或。

1
2
3
4
#define LOG_PID    0x01  /*在日志消息中包含程序PID*/
#define LOG_CONS 0x02 /*如果消息不能记录到日志文件,则打印至终端*/
#define LOG_ODELAY 0x04 /*延迟打开日志功能直到第一次调用syslog*/
#define LOG_NDELAY 0x08 /*不延迟打开日志功能*/

facility可以使用syslog函数中设施默认值。

设置日志掩码可以对特定日志级别的数据进行过滤,日志级别大于日志掩码的都被忽略掉。返回设置之前的日志掩码。

1
2
#include<syslog.h>
int setlogmask(int maskpri);

使用closelog函数可以关闭日志功能。

1
2
#include<syslog.h>
int closelog();

7.2 用户信息

7.2.1 UID、EUID、GID和EGID

大部分的服务器程序必须以root身份启动,但不能以root身份运行。下面函数可以获得和设置当前进程的真实用户ID(UID)、有效用户ID(EUID)、真实组ID(GID)和有效组ID(EGID):

1
2
3
4
5
6
7
8
9
10
11
#include<sys/types.h>
#include<unistd.h>
uid_t getuid();
uid_t geteuid();
gid_t getgid();
gid_t getegid();

int setuid(uid_t uid);
int seteuid(uid_t uid);
int setgid(gid_t gid);
int setegid(gid_t gid);

一个进程拥有两个用户ID:UID和EUID。EUID存在的目的是方便资源的访问:允许运行程序的用户拥有该程序有效用户的权限。su程序的UID是所有人,有效用户的root。所有人可以用su来修改账户信息,修改账户信息需要访问/etc/passwd,这个文件只有root用户才有权限。那么普通用户就需要依靠一个中间人来访问该文件。普通用户可以依靠中间人的身份(EUID)访问中间人的资源。同理,真实组和有效组也是一样的道理。运行该程序的用户id对应uid,该程序的所有者对应euid.

  • demo
1
2
3
4
5
6
7
8
#include <unistd.h>
#include <stdio.h>
int main(int argc, char* argv[]){
uid_t uid = getuid();
uid_t euid = geteuid();
printf("userid is : %d, effective userid is : %d\n",uid, euid);
return 0;
}

编译之后执行以下命令

1
2
sudo chown root:root test_uid # 修改目标文件所有者为root
sudo chmod +s test_uid # 设置目标文件的set-user-id标志

使用普通用户运行之后结果为

1
userid is : 1000, effective userid is : 0

7.2.2 切换用户

以root身份启动的进程切换为以一个普通用户身份运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static bool switch_to_user(uid_t user_id, gid_t gp_id) {
/*目标用户不是root*/
if ((user_id == 0) && (gp_id == 0)) {
return false;
}
/*确保当前用户是合法用户:root或目标用户*/
gid_t gid = getgid();
uid_t uid = getuid();
if (((gid != 0) || (uid != 0)) && ((gid != gp_id) || (uid != user_id))) {
return false;
}
/*已经是目标用户了,不用切换了*/
if (uid != 0) {
return true;
}
/*切换到目标用户*/
if ((setgid(gp_id) < 0) || (setuid(user_id) < 0)){
return false;
}
return true;
}

7.3 进程间关系

7.3.1 进程组

linux每个进程都有自己的PID,并且每个进程都有自己的进程组的ID(PGID)。

1
2
#include<unistd.h>
pid_t getpgid(pid_t pid);

获得指定进程组的ID,失败返回-1设置errno。

每隔进程组都有一个首领进程,PID和PGID相同。进程组一直存在,直到所有进程都退出或加入其他进程组。

1
2
#include<unistd.h>
int setpgid(pid_t pid, pid_t pgid);

将pid对应的进程的进程组设置为pgid对应的进程组。pid和pgid相同,该进程设置为进程组首领。pid为0,设置当前进程的PGID为pgid。pgid为0,使用pid作为目标的PGID。设置成功返回0,失败返回-1并设置errno。

一个进程只能设置自己或子进程的PGID。并且,当子进程调用exec系列函数之后,不能再在父进程中对它设置PGID。子进程开启新的进程,新的进程所属的进程组ID为创建该进程的子进程。如果这时候父进程更改子进程的PGID会影响孙子进程相关ID。

7.3.2 会话

一些关联的进程组将形成一个会话(session)

1
2
#include<unistd.h>
pid_t setsid(void);

该函数不能由进程组首领进程调用,会产生错误。非首领进程调用

  • 创建新的会话
  • 调用进程称为会话的首领(调用该函数的进程),该进程是该会话的唯一成员
  • 新建一个进程组,PGID为调用进程的PID,并且该进程成为该组的首领进程
  • 调用进程甩开终端

成功返回PGID,失败返回-1并设置errno。

1
2
#include<unistd.h>
pid_t getsid(pid_t pid);

获得该进程pid和sid。

7.3.3 用ps命令查看进程关系

ps -o pid,ppid,pgid,sid,comm | less命令可以查看进程之间的关系。

7.4 系统资源限制

读写资源限制

1
2
3
4
#include<sys/resource.h>

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
1
2
3
4
struct rlimit{
rlim_t rlim_cur;
rlim_t rlim_max;
};

rlim_t为整数类型,描述资源级别。rlim_cur指定资源的软限制,rlim_max指定资源的硬限制。软限制是一个建议性的、最好不要超越的限制,超越的话,系统可能向进程发送信号以终止其运行。CPU时间超过,产生SIGXCPU信号,文件尺寸超过限制,产生SIGXFSZ信号。只有root用户才可以改变资源的硬限制。也可以通过配置文件来永久改变系统软硬件限制。

常见的资源限制类型

资源限制类型 含义
RLIMIT_AS 进程虚拟内存总量限制(单位字节),操过该限制将使得某些函数(mmap)产生ENOMEM错误
RLIMIT_CORE 进程核心转储文件(core dump)的大小限制(单位是字节),其值为0表示不产生核心转储文件
RLIMIT_CPU 进程CPU时间限制(单位是秒)
RLIMIT_DATA 进程数据段(初始化数据data段,未初始化数据bss段和堆)限制(单位为字节)
RLIMIT_FSIZE 文件大小限制(单位是字节),超过该限制将使得某些函数(write)产生EFBIG错误
RLIMIT_NOFILE 文件描述符数量限制,超过该限制将使得某些函数(比如pipe)产生EMFILE错误
RLIMIT_NPROC 用户能够创建的进程数限制,超过该限制将使得某些函数(fork)产生EAGAIN错误
RLIMIT_SIGPENDING 用户能够挂起的信号数量限制
RLIMIT_STACK 进程栈内存限制(单位是字节),超过该限制将引起SIGSEGV信号

setrlimit和getrlimit成功返回0,失败返回-1并设置errno。

7.5 改变工作目录和根目录

获得当前工作目录和改变进程工作目录的函数分别是

1
2
3
#include<unistd.h>
char* getcwd(char* buf, size_t size);
int chdir(const char* path);

buf为存储当前工作目录,size指定路径长度。如果目录绝对路径(加上‘\0’)超过size,gecwd返回NULL并设置errno为ERANGE。如果buf为NULL,size有效。getcwd自动申请内存存储,并返回指针。失败返回NULL并设置errno。

chdir函数的path参数指定要切换到的目标目录。成功时返回0,失败时返回-1并设置errno。

改变进程根目录的函数是chroot

1
2
#include<unistd.h>
int chroot(const char* path);

chroot并不改变进程的当前工作目录,调用chroot之后还要使用chdir("/")来将工作目录切换至新的根目录。改变根目录之后可能无法访问/dev等目录了。默认根目录是”/“,更改根目录为”/tmp”之后。使用”/dev”路径访问的路径其实是”/temp/dev”路径。所以,只有特权进程才能改变根目录。

7.6 服务器程序后台优化

将服务器程序以守护进程方式运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <fcntl.h>

bool daemonize() {
pid_t pid = fork();
if (pid < 0) {
return false;
}else if (pid > 0){
exit(0);
}
/*设置文件权限掩码,当进程创建新文件(使用open(const char *pathname, int flags, mode_t mode)系统调用)时,文件的权限将是mode & 0777*/
umask(0);

/*创建新的会话,设置本进程为进程组的首领*/
pid_t sid = setsid();
if (sid < 0) {
return false;
}

/*切换工作目录*/
if (chdir("/") < 0) {
return false;
}

/*关闭标准输入设备、标准输出设备和标准错误输出设备*/
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

/*关闭其他打开的文件描述符*/
/*将标准输入、标准输出和标准错误输出都定向到/dev/null文件*/
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
return true;
}

可以使用下面的函数完成功能

1
2
#include<unistd.h>
int daemon(int nochdir, int noclose);

nochdir参数用于指定是否改变工作目录,如果给它传递0,则工作目录将被设置为”/“,否则使用当前工作目录。noclose参数为0时,标准输入、标准输出、标准错误输出都被重定向到/dev/null文件。否则使用原来的设备。成功反0,失败-1设置errno。

第8章 高性能服务器程序框架

服务器解构为如下三个主要模块:

  • I/O处理单元。四种I/O模型和两种高效事件处理模式
  • 逻辑单元。两种高效并发模式,高效的逻辑处理方式–有限状态机
  • 存储单元。不讲

8.1 服务器模型

8.1.1 C/S模型

8.1.2 P2P模型

8.2 服务器编程框架

8.3 I/O模型

8.4

Share 

 Previous post: C-Protoc Next post: 软件随想录-读书笔记 

© 2025 long

Theme Typography by Makito

Proudly published with Hexo