0%

安装

安装pytest时,直接使用pip安装就可以了:

1
pip install pytest

pytest辨认测试文件和测试函数

如果运行pytest没有指定文件的话,pytest会将所有形式为”test_*.py”或者”*_test.py”的文件运行。

另外,pytest要求测试函数必须以”test”开头,并且不能通过其他方式在代码中显示指定哪些函数是需要被pytest测试的测试函数。

第一个测试程序

在项目目录下新建一个叫做”test_first.py”的文件,文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
import math

def test_sqrt():
num = 25
assert math.sqrt(num) == 5

def testsqure():
num = 7
assert num * num == 40

def tesequlity():
assert 10 == 11

之后,在命令行中执行命令:

1
pytest

之后pytest就会开始运行单元测试,并产生测试报告。注意到,第三个函数不是以test开头的函数,所以pytest将不会对第三个函数进行测试。

使用”-v”(verbose)参数可以打印更多的信息。

pytest选择执行文件

在上例的基础上,再创建一个名叫”test_second.py”的文件,文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
def test_greater():
num = 100
assert num > 100

def test_greater_equal():
num = 100
assert num >= 100

def test_less():
num = 100
assert num < 200

此时,如果执行执行”pytest”命令,pytest将对test_first.py和test_second.py中的函数都进行测试。

如果只想对”test_scond.py”文件进行测试,可以执行下面的命令:

1
pytest test_second.py

指定测试函数搜索名称

在pytest命令中,可以使用”-k”参数指定关键字过滤,pytest只会测试那些含有关键字的函数。注意,即使指定了关键字,仍然要求函数以test开头,否则即使有关键字也不会对该函数进行测试。例如在文件中有函数的名称为”keyword”,即使使用命令:

1
pytest -k keyword

pytest也不会对”keyword”这个函数进行测试。

将测试函数分组

pytest运行用户对测试函数使用”marker”,”marker”可以给测试函数添加多种特性,mark本质上是一个函数装饰器。pytest自身提供了一些内建的”marker”,另外用户也可以自定义”marker”。marker在代码中的使用方法如下:

1
2
3
4
import pytest
@pytest.mark.<markname>
def test_xxxx():
pass

在测试时,可以对pytest命令添加”-m”参数来指定marker,pytest将只会运行有相同marker标记的测试函数。

1
pytest -m <markname>
注意函数名仍然需要以test起始。

Fixture

Fixture是一个自定义的函数,这个函数在每个测试函数被执行前,都会执行一遍这个fixture函数。fixture函数通常可以用来给测试函数提供数据源,例如从数据库获取数据等。

声明一个Fixture函数

1
2
3
4
import pytest
@pytest.fixture
def input_value():
return 30

使用Fixture函数:在测试函数中使用fixture函数,需要将fixture函数的函数名作为输入参数:

1
2
def test_input(input_value):
assert input_value % 3 == 0

conftest.py

conftest.py在使用了pytest的项目中是一个特殊的文件,我们可以在这个文件中定义fixture函数,而在其他所有测试函数中使用定义的fixture函数,并且不必在代码中显示地import这个文件。

1
2
3
4
5
6
7
8
9
#in conftest.py
import pytest
@pytest.fixture
def input_value():
return 30

#in other test file
def test_input(input_value):
assert input_value % 3 == 0

Parameterizing Tests

Parameterizing Tests可以给测试函数指定多个输入参数的值,直接看例子:

1
2
3
4
5
import pytest

@pytest.mark.parametrize("num", "output", [(1, 11), (2, 22), (3,35), (4, 44)])
def test_multiplication_11(num, output):
assert num * 11 == output

Xfail/Skip Tests

使用xfail “marker”标记的测试函数,会被pytest执行,但是不会进入pytest的统计,即使测试函数失败了也不会被打印出来。

1
2
3
4
import pytest
@pytest.mark.xfail
def test_something():
pass

使用skip “marker”标记的测试函数,不会被pytest执行。

指定N个测试失败后结束测试

可以在pytest命令中使用”—maxfail”参数来指定测试多少个函数失败后就停止测试。

1
pytest --maxfail=3

一般在测试用例较多,测试时间较长时使用。

并行测试

默认情况下,pytest是串行运行测试函数的,当测试函数数量较多时,可能需要并行运行测试函数。要并行运行测试函数,需要下载pytest-xdist插件

1
pip install pytest-xdist

现在可以使用 pytest -n \来指定并行度

1
pytest -n 3

Select系统调用

select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。

select系统调用的原型如下:

1
2
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
  • nfds参数指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件描述符中的最大值加1因为文件描述符是从0开始计数的。
  • readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通过这三个参数传入自己感兴趣的文件描述符。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位bit标记一个文件描述符。
  • timeout参数用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。不过我们不能完全信任select调用返回后的timeout值,比如调用失败时timeout值是不确定的。如果给timeout参数传递NULL,则select将一直阻塞,直到某个文件描述符就绪

select成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。select失败时返回-1并设置errno为EINTR。

如果觉得自己手动使用位操作去处理fd_set,可以使用下面的一系列宏和函数来访问fd_set结构体中的位:

1
2
3
4
5
#include <sys/select.h>
FD_ZERO(fd_set *fdset); /*清零fdset的所有位*/
FD_SET(int fd, fd_set *fdset); /*设置fdset的位fd*/
FD_CLR(int fd, fd_set *fdset); /*清除fdset的位fd*/
int FD_ISSET(int fd, fd_set *fdset); /*测试fdset的位fd是否被设置*/

文件描述符就绪条件

哪些情况下文件描述符可以被认为是可读、可写或者出现异常,对于select的使用非常关键。在网络编程中,下列情况可认为socket可读:

  • socket内核接受缓冲区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时我们可以无阻塞地读该socket,并且读操作返回的字节数大于0。
  • socket通信的对方关闭连接。此时对该socket的读操作将返回0。
  • 监听scoket上有新的连接请求。
  • socket上有未处理的错误,此时我们可以使用getsockopt来读取和清除错误。

下列情况下socket可写:

  • socket内核发送缓存区中的可用字节数小于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0。
  • socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
  • socket使用非阻塞connect连接成功或者失败(超时)之后。
  • socket上有未处理的错误。此时我们可以使用getsocketopt来读取和清除该错误。

网络程序中,select能处理的异常情况只有一种:socket上接受到带外数据。

下面是使用select函数处理socket带外数据的例子:

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
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <unistd.h>

int main() {
sockaddr_in sockAddress{};
sockAddress.sin_family = AF_INET;
inet_pton(AF_INET, "localhost", &sockAddress.sin_addr);
sockAddress.sin_port = htons(9000);

int sockFD = socket(PF_INET, SOCK_STREAM, 0);
if (sockFD < 0) {
std::cerr << "create socket fail" << std::endl;
return sockFD;
}

int ret = bind(sockFD, (sockaddr *)&sockAddress, sizeof(sockAddress));
if (ret < 0) {
std::cerr << "socket bind fail" << std::endl;
return ret;
}

ret = listen(sockFD, 5);
if (ret < 0) {
std::cerr << "socket listen fail" << std::endl;
return ret;
}

sockaddr_in clientSock{};
socklen_t clientSockLen;
int clientSockFD = accept(sockFD, (sockaddr *)&clientSock, &clientSockLen);
if (clientSockFD < 0) {
std::cerr << "socket accept fail" << std::endl;
return clientSockFD;
}

char buffer[1024];
fd_set read_fds;
fd_set exception_fds;
FD_ZERO(&read_fds);
FD_ZERO(&exception_fds);

while (true) {
memset(buffer, 0, sizeof(buffer));
/*每次调用select 前都要重新在read_fds和exception_fds中设置文件描述符clientSockFD,因为事件发生之后,文件描述符集合将被内核修改*/
FD_SET(clientSockFD, &read_fds);
FD_SET(clientSockFD, &exception_fds);
ret = select(clientSockFD + 1, &read_fds, nullptr, &exception_fds, nullptr);
if (ret < 0) {
std::cerr << "select fail " << std::endl;
break;
}
/*对于可读事件,采用普通的recv函数读取数据*/
if (FD_ISSET(clientSockFD, &read_fds)) {
ret = recv(clientSockFD, buffer, sizeof(buffer) - 1, 0);
if (ret <= 0) {
break;
}
std::cout << "get " << ret << " bytes of normal data: " << buffer << std::endl;
} else if (FD_ISSET(clientSockFD, &exception_fds)) {
/*对于异常事件,采用带MSG_OOB标志的recv函数读取带外数据*/
ret = recv(clientSockFD, buffer, sizeof(buffer) - 1, MSG_OOB);
if (ret <= 0) {
break;
}
std::cout << "get " << ret << " bytes of oob data: " << buffer << std::endl;
}
close(clientSockFD);
}

return 0;
}

poll 系统调用

poll系统调用和select类似,也是在指定时间内轮训一定数量的文件描述符,以测试其中是否有就绪者。poll的原型如下

1
2
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。
  • nfds指定被监听事件的fds的数量。它本质是一个整数。
  • timeout参数指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。

poll调用的返回值的含义与select相同。

poll函数的第一个参数使用的pollfd结构体的定义如下:

1
2
3
4
5
6
struct pollfd
{
int fd; /*文件描述符*/
short events; /*注册的事件*/
short revents; /*实际发生的事件,由内核补充*/
}
  • fd指定文件描述符
  • events告诉poll监听fd上的那些事件,他是一系列事件的按位或
  • revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件

poll 支持的事件类型如下表所示:

事件 描述 是否可作为输入 是否可作为输出
POLLIN 数据可读(包括普通数据和优先数据) Y Y
POLLRDNORM 普通数据可读 Y Y
POLLRDBAND 优先级带数据可读(Linux不支持) Y Y
POLLPRI 高优先级数据可读,比如TCP带外数据 Y Y
POLLOUT 数据(包括普通数据和优先数据)可写 Y Y
POLLWRNORM 普通数据可写 Y Y
POLLWRBAND 优先级带数据可写 Y Y
POLLRDHUP TCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入 Y Y
POLLERR 错误 N Y
POLLHUP 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件 N Y
POLLNVAL 文件描述符没有打开 N Y

epoll系统调用

内核事件表

epoll是Linux特有的I/O复用函数。它在实现和使用上与select、poll有很大差异。首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符使用epoll_create函数来创建:
1
2
#include <sys/epoll.h>
int epoll_create(int size);
  • size参数现在并不起作用,只是给内核一个提示。告诉它事件表需要多大。该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指要访问的内核事件表。

下面的函数可以用来操作epoll的内核事件表:

1
2
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd为epoll_create返回的事件表文件描述符
  • fd为要操作的文件描述符
  • op指定操作类型,操作类型有如下3种
    • EPOLL_CTL_ADD 往事件表中注册fd上的事件
    • EPOLL_CTL_MOD 修改fd上的注册事件
    • EPOLL_CTL_DEL 删除fd上对的注册事件
  • event 参数指定事件,它是epoll_event结构体指针类型。

epoll_event结构体的定义如下:

1
2
3
4
5
struct epoll_event 
{
__uint32_t events; /* epoll事件 */
epoll_data_t data; /* 用户数据 */
}
  • event 成员描述事件类型。epoll支持的事件类型和poll基本相同,表示epoll事件类型的宏是在poll对应的宏前加上”E”。但epoll有两个额外的事件类型——EPOLLET和EPOLLONESHOT,它们对于epoll的高效运作非常关键,这两个事件将在后面讨论。

  • data成员用于存储用户数据,其类型epoll_data_t的定义如下:

    1
    2
    3
    4
    5
    6
    7
    typedef union epoll_data
    {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
    }epoll_data_t;

    epoll_data_t是一个union,其四个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员可用来指定与fd相关的用户数据。但由于epoll_data_t是一个union,我们不能同时使用ptr和fd成员,因此如果要将文件描述符和用户数据关联起来,只能使用其他手段,比如放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。

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

epoll_wait函数

epoll的一系列系统调用的主要接口时epoll_wait函数。它在一段超时时间内等待一组文件描述符上的事件,其原型如下:

1
2
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • timeout参数指定超时时间,单位为毫秒
  • maxevents参数指定最多监听多少个事件,它必须大于0
  • events参数,如果epoll_wait函数检测到事件,就将所有就绪的事件从内核事件表中复制到它events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检查到的就绪事件。
  • epfd参数指定内核事件表的文件描述符

LT和ET模式

epoll对文件描述符的操作有两种模式:LT(Level Trigger,电平触发)模式和ET(Edge Trigger,边沿触发)模式。LT模式是默认的工作模式,这种模式下epoll相对于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式时epoll的高效工作模式。

对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理事件。这样当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。

而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续epoll_wait调用将不再向应用程序通知这一事件。可见,ET模式在很大程度上降低了epoll事件被重复触发的次数,因此效率要比LT模式高。

LT和ET模式下对事件的不同处理方式

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//
// Created by Bytedance-Wall Flower on 2021/1/10.
//

#include <fcntl.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <cerrno>
#include <arpa/inet.h>


#define MAX_EVENT_NUM 64
#define BUFFER_SIZE 512


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

void monitorEventForFd(int epoll_fd, int fd, bool enable_et) {
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if (enable_et) {
event.events |= EPOLLET;
}
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);
setNonblocking(fd);
}

void lt_mode(epoll_event *events, int number, int epoll_fd, int listen_fd) {
char buf[512];
for (int i = 0; i < number; ++i) {
int sock_fd = events[i].data.fd;
if (sock_fd == listen_fd) {
sockaddr_in client_socket_addr{};
socklen_t sock_len;
int conn_fd = accept(listen_fd, (sockaddr *)&client_socket_addr, &sock_len);
monitorEventForFd(epoll_fd, conn_fd, false); //使用ET模式监听客户端socket fd

} else if (events[i].events & EPOLLIN) {//客户端socket fd有数据可读
printf("client socket fd %d is readable\n", sock_fd);
memset(buf, 0, BUFFER_SIZE);
int ret = recv(sock_fd, buf, BUFFER_SIZE-1, 0);
if (ret <= 0) {
close(sock_fd);
continue;
}
printf("sock fd %d get %d bytes of content: %s\n", sock_fd, ret, buf);
} else {
printf("other event happen in le mode\n");
}
}
}

void et_mode(epoll_event *events, int number, int epoll_fd, int listen_fd) {
char buf[BUFFER_SIZE];
for (int i = 0; i < number; ++i) {
int sock_fd = events[i].data.fd;
if (sock_fd == listen_fd) {
sockaddr_in client_socket_addr{};
socklen_t sock_len;
int conn_fd = accept(listen_fd, (sockaddr *)&client_socket_addr, &sock_len);
monitorEventForFd(epoll_fd, conn_fd, true); //使用ET模式监听客户端socket fd

} else if (events[i].events & EPOLLIN) {
printf("client socket fd %d is readable\n", sock_fd);
while (true) { //循环读,直到无数据可读
memset(buf, 0, BUFFER_SIZE);
int ret = recv(sock_fd, buf, BUFFER_SIZE-1, 0);
if (ret < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
//对于非阻塞io,下面的条件成立表示数据已经全部读取完毕,
//此后epoll 就能再次触发该sock fd上的EPOLLIN事件,以驱动下一次读操作
printf("read later\n");
break;
}
close(sock_fd);
break;
} else if (ret == 0) {
close(sock_fd);
} else {
printf("sock fd %d get %d bytes of content: %s\n", sock_fd, ret, buf);
}
}
} else {
printf("other event happen in et mode\n");
}
}

}

int main() {
sockaddr_in server_sock_addr{};
server_sock_addr.sin_family = AF_INET;
inet_pton(AF_INET, "localhost", &server_sock_addr.sin_addr);
server_sock_addr.sin_port = htons(9000);
int server_fd = socket(PF_INET, SOCK_STREAM, 0);
bind(server_fd, (sockaddr *)&server_sock_addr, sizeof(server_sock_addr));
listen(server_fd, 5);
epoll_event events[MAX_EVENT_NUM];
int epoll_fd = epoll_create(5);
monitorEventForFd(epoll_fd, server_fd, true);//monitor server socket fd use et mode
while (true) {
int ret = epoll_wait(epoll_fd, events, MAX_EVENT_NUM, -1);
if (ret < 0) {
printf("epoll wait fail\n");
break;
}
lt_mode(events, ret, epoll_fd, server_fd); //LT mode to process client connection
// et_mode(events, ret, epoll_fd, server_fd);//ET mode to process client connection
}
close(server_fd);
return 0;
}
注意在ET模式下,当客户端socket有数据可读的时候,需要使用循环一次性把所有数据读完。

EPOLLONESHOT事件

即使使用ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有数据可读(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个socket的局面。这当然不是我们期望的。我们期望的时一个socket连接在任一时刻都只被一个线程处理。这一点可以使用epoll的EPOLLONESHOT事件实现。

对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。但是,反过来,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其他EPOLLIN事件能被触发。

代码示例

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
105
106
107
108
109
110
111
112
113
114
115
116
#include <fcntl.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <cerrno>
#include <arpa/inet.h>
#include <thread>

#define BUFFER_SIZE 512
#define MAX_EVENT_NUM 64

typedef struct FDPair {
int epoll_fd;
int sock_fd;
} FDPair;

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

void monitorEventForFd(int epoll_fd, int target_fd, bool enable_one_shot) {
epoll_event event{};
event.data.fd = target_fd;
event.events = EPOLLIN | EPOLLET;
if (enable_one_shot) {
event.events |= EPOLLONESHOT;
}
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, target_fd, &event);
setNonBlocking(target_fd);
}


void resetOneShot(int epoll_fd, int target_fd) {
//重置target fd上的事件,这样操作之后,
//尽管target fd上的EPOLLONESHOT事件被注册,但是操作系统仍然会触发target fd上的EPOLLIN事件,且只触发一次
epoll_event event{};
event.data.fd = target_fd;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, target_fd, &event);//注意这里的操作是modify
}

//处理客户端socket的工作线程
void* worker(void *arg) {
auto *pair = (FDPair *)arg;
printf("start new thread to receive data on fd: %d\n", pair->sock_fd);
char buf[BUFFER_SIZE];
memset(buf, 0, BUFFER_SIZE);
while (true) {
int ret = recv(pair->sock_fd, buf, BUFFER_SIZE - 1, 0);
if (ret == 0) {
close(pair->sock_fd);
printf("foreigner closed the connection\n");
break;
} else if (ret < 0) {
//errno是线程安全的,不用担心
if (errno == EAGAIN) {
resetOneShot(pair->epoll_fd, pair->sock_fd);
printf("read later\n");
break;
}
} else {
printf("get content: %s\n", buf);
//休眠3s,模拟数据处理过程
sleep(3);
}
}
printf("end thread receiving data on socket fd: %d\n", pair->sock_fd);
return nullptr;
}

int main() {
sockaddr_in server_sock_addr{};
server_sock_addr.sin_family = AF_INET;
inet_pton(AF_INET, "localhost", &server_sock_addr.sin_addr);
server_sock_addr.sin_port = htons(9000);
int server_fd = socket(PF_INET, SOCK_STREAM, 0);
bind(server_fd, (sockaddr *)&server_sock_addr, sizeof(server_sock_addr));
listen(server_fd, 5);
epoll_event events[MAX_EVENT_NUM];
int epoll_fd = epoll_create(5);
//注意,监听server fd是不能注册EPOLLONESHOT事件的,
//否则应用程序只能处理一个客户连接,因为后续的客户连接请求将不再触发server_fd上的EPOLLIN事件
monitorEventForFd(epoll_fd, server_fd, false);

while (true) {
int event_count = epoll_wait(epoll_fd, events, MAX_EVENT_NUM, -1);
if (event_count < 0) {
printf("epoll wait fail\n");
break;
}
for (int i = 0; i < event_count; ++i) {
int sock_fd = events[i].data.fd;
if (sock_fd == server_fd) {
sockaddr_in client_sock_addr{};
socklen_t sock_len;
int client_sock_fd = accept(server_fd, (sockaddr *)&client_sock_addr, &sock_len);
monitorEventForFd(epoll_fd, client_sock_fd, true);
} else if (events[i].events & EPOLLIN){
pthread_t thread_handler;
FDPair pair{};
pair.epoll_fd = epoll_fd;
pair.sock_fd = sock_fd;
pthread_create(&thread_handler, nullptr, worker, &pair);
} else {
printf("other event happen\n");
}
}
}
close(server_fd);
return 0;
}
注意在工作线程中,处理完一次client socket fd上的数据后,需要重置该client socket fd

三个I/O复用函数的比较

  • 每次select和poll调用都返回整个用户注册的事件集合(所有注册的fd,不论是就绪的还是未就绪的),所以应用程序检索就绪文件描述符的时间复杂度为O(n)。而epoll_wait返回的events参数仅用来返回就绪事件,这使得应用程序索引就绪文件描述符的事件复杂度减少到O(1)。
  • poll和epoll_wait分别使用nfds和maxevents参数指定最多监听多少个文件描述符和事件。这两个数值都能到达系统允许打开的最大文件描述符数目。而select允许监听的最大文件描述符数量通常有限制。虽然用户可以修改这个限制,但这可能导致不可预期的后果。
  • 从实现原理上来说,select和poll采用的都是轮询的方式,即每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序,因此它们检测就绪事件的算法时间复杂度为O(n)。而epoll_wait采用的是回调的方式。内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪事件队列。内核最后在适当的时机将就绪事件队列中的内容拷贝到用户空间。因此epoll_wati的算法时间复杂度为O(1)。但是,当活动连接比较多的时候,epoll_wati的效率未必比select和poll高,因为此时回调函数被触发得过于频繁。所以epoll_wait适用于连接数量多,但是活动连接较少的情况

I/O复用的高级应用一:非阻塞connect

对于非阻塞的socket调用connect,而连接又没有立即建立时,这个时候,connect会设置errno值为EINPROGRESS。这个时候对于非阻塞的socket,我们可以调用select、poll等函数来监听这个连接失败的socket上的可写事件。当select、poll函数返回后,再利用getsockopt来读取错误码并清除该socket上的错误。如果错误码为0,表示连接成功建立,否则连接失败。非阻塞connect的应用:可以同时发起多个连接并一起等待。

代码示例:

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
//
// Created by Bytedance-Wall Flower on 2021/1/15.
//


#include <fcntl.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <cerrno>
#include <arpa/inet.h>

#define BUFFER_SIZE 512

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

int nonblock_connect(const char *ip, int port, int timeout) {
sockaddr_in server_sock_addr{};
server_sock_addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &server_sock_addr.sin_addr);
server_sock_addr.sin_port = htons(port);
int sock_fd = socket(PF_INET, SOCK_STREAM, 0);
int old_option = setNonBlocking(sock_fd);
int ret = connect(sock_fd, (sockaddr *)&server_sock_addr, sizeof(server_sock_addr));
if (ret == 0) {
//如果连接成功,则恢复sock fd的属性
printf("connect with server immediately\n");
fcntl(sock_fd, F_SETFL, old_option);
return sock_fd;
} else if (errno == EINPROGRESS) {
//只有当errno是EINPROGRESS时才表示连接还在进行,
fd_set write_fds;
timeval select_timeout{};
FD_ZERO(&write_fds);
FD_SET(sock_fd, &write_fds);
select_timeout.tv_sec = timeout;
ret = select(sock_fd + 1, nullptr, &write_fds, nullptr, &select_timeout);
if (ret < 0) {
printf("select timeout\n");
close(sock_fd);
return -1;
}
if (!FD_ISSET(sock_fd, &write_fds)) {
printf("no write events on sock fd: %d\n", sock_fd);
close(sock_fd);
return -1;
}
int error_in_sock_fd = 0;
socklen_t error_len = sizeof(int);
if (getsockopt(sock_fd, SOL_SOCKET, SO_ERROR, &error_in_sock_fd, &error_len) < 0) {
printf("get socket option failed\n");
close(sock_fd);
return -1;
}
if (error_in_sock_fd != 0) {
//错误不为0,表示连接出错
printf("connection failed after select with the error: %d\n", error_in_sock_fd);
close(sock_fd);
return -1;
} else {
//连接成功
printf("connection ready after select with the socket: %d\n", sock_fd);
fcntl(sock_fd, F_SETFL, old_option);//重置为原来的属性
return sock_fd;
}

} else {
//如果连接没有立即建立,连接又不还在进行,出错返回
printf("non-block connect unsupported\n");
close(sock_fd);
return -1;
}
}

int main() {
int sock_fd = nonblock_connect("localhost", 9000, 10);
if (sock_fd < 0) {
return -1;
}
const char *data = "data from client !!!";
send(sock_fd, data, strlen(data), 0);
close(sock_fd);
return 0;
}
注意,上面的代码有平台移植的问题:
  • 首先,非阻塞的socket可能导致connect始终失败。
  • 其次,select对处于EINPROGRESS状态下的socket可能不起作用
  • 对于出错的socket,getsockopt在有些系统(比如Linux)上返回-1,而在有些系统(比如BSD)上则返回0。

这些问题没有一个统一的解决方法,可能需要针对不同的平台使用宏来分别判断。

I/O复用高级应用二:简单聊天室程序

启动kafka

kafka的运行需要java环境和zookeeper,所以需要先下载好jdk和zookeeper,然后启动zookeeper server。详细操作可以参考这里

启动一个kafka server(broker)的命令:

1
bin/kafka-server-start.sh config/server.properties

该命令需要传递一个kafka server配置文件的路径,使用默认的配置即可。

创建topic

kafka提供了一个名为”kafka-topics.sh”的脚本,用于在kafka server上创建topic,在命令行输入命令:

1
2
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 
--partitions 1 --topic hello-kafka

该命令创建了一个叫做hello-kafka的topic。

创建topic后,也可以使用这个脚本获取服务器中的主题列表:

1
bin/kafka-topics.sh --list --zookeeper localhost:2181

启动生产者以发送消息

kafka提供一个kafka-console-producer.sh的脚步用于启动一个producer

1
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic topic-name

这个脚步需要两个参数,一个是kafka server(broker)的ip和端口,另外一个参数就是主题的名称。

启动producer后,脚本会从stdin获取输入,然后发送到kafka集群中。默认情况下,每个新行都作为新消息发布,可以在config/producer.properties文件中配置producer的一些属性。

启动消费者以接收消息

kafka也提供了一个Kafka-console-consumer.sh脚本来启动一个消费者

1
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic topic-name

单节点多代理配置

之前的基本操作的说明都是在单节点(一台物理机器)-单代理(一个kafka broker)下的场景下的,现在说明如何在一台物理机上启动多个Kafka server(broker)。

首先需要启动zookeeper服务器,然后由于需要启动多个kafka broker,需要为每个broker创建一个配置文件,可以将config/server.properties文件复制多份,并重新命令为config/server-one.properties,config/server-two.properties,注意需要修改配置文件中的port字段,让broker监听不同的端口。broker默认监听的端口是9092。

在多代理下可以对一个partition创建副本,可以使用Kafka-topics.sh脚本的”—describe”参数来检查哪个broker是partition的leader:

1
bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic topic-name

在单节点多代理下创建生产者和消费者的方式和在单节点单代理的场景下相同

修改已有Topic的配置

修改主题使用Kafka-topics.sh脚本来完成

1
2
bin/kafka-topics.sh —zookeeper localhost:2181 --alter --topic topic-name 
--partitions count

这个命令可以修改topic-name的分区数量和备份数量。

删除Topic

1
bin/kafka-topics.sh --zookeeper localhost:2181 --delete --topic topic-name

注意,如果配置delete.topic.enable为false,则delete操作不会生效。

kafka简介

Apache Kafka 是一个分布式发布 - 订阅消息系统和一个强大的消息队列中间件,可以处理大量的数据。 Kafka使用文件存储消息并且会将消息保留在磁盘上,同时在群集内复制以防止数据丢失。 Kafka 构建在 ZooKeeper 同步服务之上。 它与 Apache Storm 和 Spark 非常好地集成,用于实时流式数据分析。

kafka内相关术语:

  • 生产者和消费者:消息的发送者叫producer,消息的使用者和接受者叫consumer,生产者将数据保存到kafka集群中,消费者从中获取消息进行业务的处理。
  • broker:kafka集群中有很多台服务器,其中每一台服务器都可以存储消息,将每台服务器称为一个kafka实例,也叫做broker。
  • 主题(topic):一个topic表示同一类消息,相当于对消息进行分类,每个producer将消息发送到kafka中,都需要指定消息的topic是哪个,也就是指明这个消息属于哪一类。
  • 分区(partition):每个topic都可以分成多个partition,每个partition在kafka中其实就是一个文件,任何发布到此partition的消息都会被直接追加到log文件的尾部。为什么对topic进行分区呢:最根本的原因就是kafka基于文件进行存储,当文件内容大到一定程度时,很容易达到单个磁盘的上线,因此,采用分区的办法,一个分区对应一个文件,这样就可以将数据分别存储到不同的服务器上,另外这样可以做负载均衡,容纳更多的消费者。生产者将消息发送到kafka中时,可以不指定partition,由kafka来决定的分配到那个partition,也可以自己指定partition
  • 偏移量(offset):一个分区对应一个磁盘上的文件,而消息在文件中的位置就称为offset,offset是一个long型数字,它可以唯一标记一条消息。由于kafka并没有提供其他额外的索引机制来存储offset,kafka中文件只能顺序地读写,所以在kafka中几乎不允许对消息进行随机读写

综上,总结一下kafka有几个要点:

  • kafka 是一个基于发布-订阅的分布式消息系统(消息队列)
  • kafak 的消息数据保存在磁盘,每个 partition 对应磁盘上的一个文件,消息写入就是简单的文件追加,文件可以在集群内复制备份以防丢失
  • 即使消息被消费,kafka 也不会立即删除该消息,可以通过配置使得过一段时间后自动删除以释放磁盘空间
  • kafka依赖分布式协调服务Zookeeper,适合离线/在线信息的消费,与storm和saprk等实时流式数据分析常常结合使用

kafka基本原理

分布式和分区

kafka的分布式和分区总结来说就是:一个topic对应的多个partition分散地存储在集群中的多个broker上,存储的方式是一个partition对应一个文件,每个broker负责存储在自己机器上的partition中的消息读写。

副本

kafka可以配置partition需要备份的个数(replicas),每个partition会被备份到多台机器上,以提高可用性,备份的数量可以通过配置文件指定。

kakfa对同一partition的多个备份的管理和调度策略是:在每个partition的所有备份中选举一个最为“leader”,由leader负责处理消息的读写,其他partition作为follower只需要简单地与leader进行同步数据即可。如果原来的leader失效,会重新选举其他的folloer来成为新的leader。

至于如果选取leader,这正是Zookeeper所擅长的,kafka使用ZK在broker中选出一个Controller,用于partition分配和Leader选举。

另外,作为leader的服务器承担了该分区所有的读写请求,因此其压力是比较大的,而且,有多少个partition就意味着会有多少个leader,,kafka会将leader分散到不同的broker上,确保整体的负载均衡。

ISR

ISR的全称是in-sync replica,翻译过来就是与leader保持同步的replica集合。虽然kafka可以为一个partition配置N个replica,但是这不意味着该partition可以容忍N-1个replica失效而不丢失数据。

Kafka为partition动态维护一个replica集合。该集合中的所有replica保存的消息日志都与leader replica保持同步状态。只有这个集合中的replica才能被选举为leader,也只有该集合中所有replica都接收到了同一条消息,kafka才会将该消息置于“已提交”状态,即认为这条消息发送成功。

正常情况下,partition的所有replica(含leader replica)都应该与leader replica保持同步,即所有replica都在ISR中。因为各种各样的原因,一小部分replica开始落后于leader replica的进度。当滞后到一定程度时,Kafka会将这些replica“踢”出ISR。相反地,当这些replica重新“追上”了leader的进度时,那么Kafka会将它们加回到ISR中。这一切都是自动维护的,不需要用户进行人工干预,因而在保证了消息交付语义的同时还简化了用户的操作成本。

数据生产流程

对于生产者要写入一条记录,可以指定四个参数,分别是topic,partition, key和value,其中topic和value是必须指定的,而key和partition是可选的。

对于一条记录,先对其进行序列化,然后按照topic和partition,放进对应的发送队列中,如果partition没有指定,那么会根据以下情况来决定发送到哪个partition:

  • key有指定,按照key进行hash,相同的key去同一个partition。
  • key没有指定,Round-Robin来选partition。

producer将会和topic下所有partition leader保持socket连接,消息由producer直接通过socket发送到broker。其中partition leader的位置(ip : port)注册在zookeeper中,producer作为zookeeper client,以及注册了watch用来监听partition leader的变更事件,因此,可以准确的知道谁是当前的leader

另外,producer端采用异步发送:将多条消息暂且在客户端中buffer起来,并将它们批量的发送到broker,小数据IO太多,会拖慢整体的网络延迟,批量延迟发送提升了网络效率。

数据消费过程

对于消费者,不是以单独的形式存在的,每一个消费者都属于一个consumer group,可为每个Consumer指定group name,若不指定group name则属于默认的group,一个consumer group包含多个consumer。特别需要注意的是:订阅Topic是以一个消费组来订阅的,发送到topic的消息,只会被订阅了此topic的每个group中的一个consumer消费。一个topic可以被多个组订阅。

具体来说,是根据partition来分的,一个partition,只能被消费组里的一个消费者消费,但是可以同时被多个消费组消费,消费组里的每个消费者是关联到一个partition的,因此有这样的说法,对于同一个topic,同一个group中不能有多于partition个数的consumer,否则将会存在一些consumer无法得到消息。

在kafka,consumer采用pull方式获取消息,即consumer在和broker建立连接后,主动去pull消息,这样consumer可以根据自己的消费能去适当的获取消息并处理,且可以控制消费消息的进度。

另外partition中不存在消息状态的控制,也没有消息确认机制。当消息被consumer接收之后,需要保存Offset记录消费到哪,以前保存在ZK中,由于ZK的写性能不好,在0.10版本后,kafka把这个offset的保存从ZK中剥离,保存在一个名叫”consumeroffsets topic”的topic中。

服务器模型

C/S模型

P2P模型

服务器编程框架

I/O模型

I/O模型分为两种,阻塞和非阻塞,阻塞I/O执行的系统调用可能因为无法立即完成而被挂起,直到等待的事件发生为止。而非阻塞I/O执行的系统调用则总是立即返回,而不管事件是否已经发生。

两种高效的事件处理模式

Reactor模式

Reactor模型下,主线程只负责监听是否有事件发生,有的话就立即将该事件通知工作线程,除此之外,主线程不做任何其他实质性的工作。读写数据,以及处理客户请求均在工作线程中完成。

Proactor模式

与Reactor模式不同,Proactor模式将所有I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。

日志

Linux系统日志

在linux中提供一个守护进程来处理系统日志——syslogd,不过现在的linux系统上使用的都是它的升级版——rsyslogd。

rsyslogd守护进程既能接受用户进程输出的日志,又能接收内核日志。用户进程是通过调用syslog函数生成系统日志的。该函数将日志输出到一个UNIX本地域socket类型(AF_UNIX)的文件/dev/log中,rsyslogd进程则监听该文件以获取用户进程的输出。

pipe函数

pipe函数可用于创建一个管道,以实现进程间通信(例如父子进程间通信)。pipe函数的定义如下:

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

pipe函数的参数是一个包含两个int型整数的数字指针,是作结果输出的参数。该函数成功时返回0,并将一对打开的文件描述符值填入其参数指向的数组。如果失败,则返回-1,并设置errno。

通过pipe函数创建的这两个文件描述符fd[0],fd[1]分别构成管道的两端,往fd[1]写入的数据可以从fd[0]读出,并且,fd[0]只能用于从管道读出数据,fd[1]则只能用于往管道写入数据,而不能反过来使用,如果要实现双向的数据传输,就应该使用两个管道。

另外,默认情况下,这一对文件描述符都是阻塞的。如果我们用read系统调用来读取一个空的管道,则read将被阻塞,直到管道内有数据可读,如果我们用write系统调用来读取一个空的管道,则read将被阻塞,同理如果用write系统调用来往一个满的管道中写入数据,则write也将被阻塞,直到管道有足够多的空闲空间可用。

如果管道的写端文件描述符fd[1]的引用计数减少至0,即没有任何进程需要往管道中写入数据,则针对管道的读端文件描述符fd[0]的read操作将返回0,即读取到了文件结束标记(End of File, EOF);反之,如果管道的读端文件描述符fd[0]的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对管道的写端文件描述符fd[1]的write操作将失败,并引发SIGPIPE信号。

管道内传输的数据是字节流,管道本身有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写入多少个字节的数据。自Linux2.6.11内核起,管道容量的大小默认是65536字节。我们可以使用fcntl函数来修改管道容量。

其实shell命令中的管道操作的实现也是通过pipe函数来实现的。

pipe的实现原理,其实是在操作系统内核中开辟了一个缓冲区(位于内存),然后让返回的两个文件描述符都指向这个内核缓存区,然后设置一个文件描述符只能读,一个文件描述符只能写。写pipe时需要将数据从用户空间拷贝到内核缓冲区,读数据时需要将数据从内核缓冲区拷贝到用户空间。

pipe只能用在两个有亲缘关系的进程上,例如父子进程;如果要在两个没有关系的进程上用管道通信,需要使用fifo命名管道,FiFo命名管道利用了磁盘文件。

dup函数和dup2函数

有时候我们希望把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接,这可以通过下面的用于复制文件描述符的dup或者dup2函数来实现。

1
2
3
#include <unistd.h>
int dup(inf fd);
int dup2(int fd_one, int fd_two);

dup函数创建一个新的文件描述符,该文件描述符和原有文件描述符fd指向相同的文件、管道或者网络连接。并且dup返回的文件描述符总是取系统当前可用的最小整数值。dup2和dup类型,不过它将返回第一个不小于(大于等于)fd_two的整数值。dup和dup2系统调用失败时返回-1并设置errno。

利用dup函数实现将标准输出重定向到一个网络连接中:

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 <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
sockaddr_in sockAddress{};
sockAddress.sin_family = AF_INET;
inet_pton(AF_INET, "localhost", &sockAddress.sin_addr);
sockAddress.sin_port = htons(5000);

int sockFD = socket(PF_INET, SOCK_STREAM, 0);
if (sockFD < 0) {
std::cerr << "create socket fail" << std::endl;
return sockFD;
}

int ret = bind(sockFD, (sockaddr *)&sockAddress, sizeof(sockAddress));
if (ret < 0) {
std::cerr << "socket bind fail" << std::endl;
return ret;
}

ret = listen(sockFD, 5);
if (ret < 0) {
std::cerr << "socket listen fail" << std::endl;
return ret;
}

sockaddr_in clientSock{};
socklen_t clientSockLen;
int clientSockFD = accept(sockFD, (sockaddr *)&clientSock, &clientSockLen);
if (clientSockFD < 0) {
std::cerr << "socket accept fail" << std::endl;
return clientSockFD;
} else {
close(STDOUT_FILENO);
int newFd = dup(clientSockFD);
std::cout << "new fd is equal to std out fd: " << (newFd == STDOUT_FILENO) << std::endl;
std::cout << "this is direct from std out" << std::endl;
close(clientSockFD);
close(newFd);
}
close(sockFD);
return 0;
}

readv函数和writev函数

readv函数将数据从磁盘读到分散的内存块中,即分散读;wirtev函数则将多块分散的内存数据一并写入文件描述符中,即集中写。它们的定义如下:

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 iovect* vector, int count);

struct iovect用来描述一块内存区,它的定义如下:

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

sendfile函数

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参数指定在文件描述符in_fd和out_fd之间传输的字节数

sendfile成功时返回传输的字节数。失败则返回-1并设置errno。

sendfile函数有一个限制:in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket管道;而out_fd则必须是一个socket。由此可见,sendfile几乎是专门为在网络上传输文件而设计的。 # mmap、munmap、msync函数 mmap函数用于申请一段内存空间。`我们可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中`。munmap函数则释放由mmap创建的这段内存空间。它们定义如下:
1
2
3
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void* start, size_t length);
* start参数允许用户使用某个特定的地址作为这段内存的起始地址,如果它被设置为NULL,则系统自动分配一个地址。 * length参数指定内存段的长度。 * prot参数用来设置内存段的访问权限。它可以取以下几个值的按位或 * PROT_READ,内存段可读 * PROT_WRITE,内存段可写 * PROT_EXEC,内存段可执行 * PROT_NONE,内存段不能被访问 * flags参数控制内存段内容被修改后程序的行为。它的`常用`取值可以是如下这些值的按位或 * MAP_SHARED,在进程间共享这段内存,对该内存段的修改将反映到被映射的文件中 * MAP_PRIVATE,内存段为调用进程所私有,对该段内存的修改不会反映到被映射的文件中 * MAP_ANONYMOUS,这段内存不是从文件映射来的。其内容被初始化为全0。这种情况下,mmap函数的最后两个参数将被忽略 * MAP_FIXED,内存段必须位于start参数指定的地址处,start必须是内存页面大小(4096字节)的整数倍,考虑到可移植性,addr 通常设为 NULL ,不指定 MAP_FIXED * MAP_HUGETLB,按照“大内存页面”来分配内存空间,“大内存页面”的大小可通过/proc/meminfo文件来查看 * fd参数是被映射文件对应的文件描述符。它一般通过open系统调用来获得 * offset参数设置从文件的何处开始映射 mmap函数成功时返回指向目标内存区域的指针,失败则返回MAP_FAILED,并设置errno。munmap函数成功时返回0,失败时返回-1并设置errno。`当 mmap 成功返回时,fd 就可以关闭,这并不影响创建的映射区。` `进程退出的时候,映射区会自动删除`。不过当不再需要映射区时,可以调用 munmap 显式删除。当映射区删除后,后续对映射区的引用会生成 SIGSEGV 信号。 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回的地址空间的操作只在内存中有意义。并且只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件。 ## linux内存映射mmap原理 在调用mmap函数的时候,实际上只是创建并初始化了相关的结构体,这个结构体中记录了从`逻辑地址`到磁盘空间的映射,`此时并没有发生任何磁盘数据的传输`。 当使用mmap返回的逻辑地址对数据进行访问时,操作系统将逻辑地址转换为物理地址,如果发现页表项中没有对应的物理页时,此时就会根据mmap建立的映射关系,从磁盘中间对应数据加载到物理页中,`这个物理页是属于用户空间的物理页` `为什么使用mmap会比使用普通的read/write快?`先看看使用普通的read/write的过程,当使用read/write函数从某个fd上读取数据时,操作系统首先从磁盘将数据加载到属于内核的`物理地址`中,然后需要将这些数据从属于内核的`物理地址`拷贝到属于用户空间的`物理地址`中,此时才会在用户空间中获取到这些数据。这个过程经过了两次数据拷贝的过程。而使用mmap,会直接把数据从磁盘中拷贝到属于用户的物理空间中,只经过了一次数据拷贝的操作。 # splice函数 splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作。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表示从输入数据流的何处开始读取数据,如果fd_in是一个管道文件描述符,那么off_in参数必须被设置为NULL,表示从输入数据流的当前偏移位置开始读取数据。 * fd_out表示需要写数据的文件描述符。 * off_out表示从何处开始写数据,如果fd_out是一个管道文件描述符,那么off_out参数必须设置为NULL,表示从当前偏移位置开始写数据。 * len参数表示需要拷贝数据的长度 * flags参数控制数据如何移动,它可以被设置为下列这些值的按位或 * SPLICE_F_MOVE:如果合适的话,按整页内存移动数据。这只是给内核一个提示。不过,因为它的实现存在BUG,自内核2.6.21后,它实际上没有任何效果。 * SPLICE_F_NONBLOCK:非阻塞的splice操作,但实际效果还会受文件描述符本身的阻塞状态的影响 * SPLICE_F_MORE:给内核一个提示:后续的splice调用将读取更多的数据 * SPLICE_F_GIFT:对splice没有效果 使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符,但因为管道文件有大小限制,所以splice函数一次移动太多数据可能会导致长时间阻塞

tee函数

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

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

tee函数的参数的含义和splice函数相同,只不过fd_in和fd_out必须都是管道文件描述符。

fcntl函数

fcntl函数,正如其名字(file control)描述的那样,提供了对文件描述符的各种控制操作。另外一个常见的控制文件描述符属性和行为的系统调用是ioctl,而且ioctl比fcntl能够执行更多的操作。但是,对于控制文件描述符常用的属性和行为,fcntl函数是有POSIX规范指定的首选方法。fcntl函数的定义如下:

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

fd参数是被操作的文件描述符,cmd参数指定执行何种类型的操纵。根据操作类型的不同,该函数可能还需要第三个可选参数arg。fcnt函数支持的常用操作及其参数如下表所示:

操作分类 操纵 含义 第三个参数类型 成功时的返回值
复制文件描述 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 设置文件的状体标志,但部分标志是不能被修改的(比如访问模式标志) 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指定的管道的容量 管道容量

在网络编程中,fcntl函数通常用来将一个socket文件描述符设置为非阻塞的:

1
2
3
4
5
6
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;/*返回文件描述符旧的状态标志,方便之后恢复该状态标志*/
}
此外,SIGIO和SIGURG这两个信号与其他Linux信号不同,他们必须与某个文件描述符相关联才能使用,当被关联的文件描述符可读或可写时,系统将触发SIGIIO信号,当被关联的文件描述符(而且必须是一个socket)上有带外数据可读时,系统将触发SIGURG信号,将信号和文件描述符关联的方法,就是使用fcntl函数为目标文件描述符指定宿主进程或进程组,那么被指定的宿主进程或进程组将捕获这两个信号。

主机字节序和网络字节序

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

当两台字节序不同的主机之间进行通信时,由于字节序的不同就会导致对数据的错误解释。但是接受端也不知道发送端发送过来的数据的字节序到底是大端还是小端,解决办法是:发送端总是把要发送的数据转化为大端字节序数据后再发送,而接受端可以根据自身采用的字节序决定是否对接收到的数据进行转换。因此大端字节序也称为网络字节,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。

另外需要指出的是,即使是同一台机器上的两个进程(比如一个由C语言编写,另一个由Java编写)通信,也要考虑字节序的问题(Java虚拟机采用大端字节序)。

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

1
2
3
4
5
#include <netinet/in.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);

它们的含义很明确,比如htonl表示”host to network long”,即将32位无符号数从主机字节序转换为网络字节序数据。

socket

通用socket地址

Linux C Socket网络编程接口表示socket地址的结构体sockaddr,其定义如下:

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

sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocal 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端口号和32bit IPv4地址,共6字节
PF_INET6 16bit端口号,32bit流标识,128bit IPv6地址,32bit范围ID,共26字节

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

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

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

专用socket地址

上面的两个通用socket地址结构体显然都不好用,所以Linux为各个协议族提供了专门的socket地址结构体。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct sockadddr_in{
sa_family_t sun_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]; /* IPv6地址,要用网络字节序表示 */
}

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

IP地址转换函数

通常,人们习惯用可读性好的字符串来表示IP地址,比如用点分10进展字符串表示IPv4地址,以及用16进制字符串表示IPv6地址。但编程中我们需要先把它们转化为整数才能使用。而记录日志时则相反,我们要把整数表示的IP地址转为可读的字符串。Linux提供了3个用于IP地址形式转化的函数:

1
2
3
4
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr); /*in_addr 是 __uint32_t 的typdef*/
int inet_aton(const char* p, 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是不可重入的,下面的代码揭示了其不可重入性:

1
2
3
4
5
6
7
char *value1 = inet_ntoa(inet_addr("1.2.3.4"));
char *value2 = inet_ntoa(inet_addr("10.194.71.60"));
printf("address 1: %s\n", value1);
printf("address 2: %s\n", value2);
//最后打印的结果为:
address1: 10.194.71.60
address2: 10.194.71.60

下面这对更新的函数也能完成和前面3个函数相同的功能,并且它们同时适用于IPv4地址和IPv6地址。

1
2
3
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
int char* inet_ntop(int af, const void *scr, char *dst, socklen_t cnt);
  • inet_pton函数用于将字符串表示的IP地址src(用点十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)转化成用网络字节序整数表示的IP地址,并把转换结果存储于dst指向的内存中。其中af参数指定地址族,可以是AF_INET或者AF_INET6。inet_pton成功时返回1,失败时返回0。

  • inet_ntop函数将网络字节序整数表示的IP地址,转化为IP字符串。前三个参数的含义与inet_pton的参数相同,最后一个参数cnt指定字符串dst目标存储单元的大小。有两个宏能帮助我们快速指定这个大小(分别用于IPv4和IPv6):

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

创建socket

UNIX/Linux的一个思想就是:所有的东西都是文件。socket也不例外,他就是可读、可写、可控制、可关闭的文件描述符。

下面的socket系统调用可创建一个socket:

1
2
#include <sys/socket.h>
int socket(int domin, int type, int protocol);
  • domin 参数告诉系统使用哪个底层协议族。对于TCP/IP协议族而言,该参数应该设置为PF_INET(Protocal Family of Internet,用于IPv4)或PF_INET6(用于IPv6),对于UNIX本地协议族而言,该参数应设置为PF_UNIX
  • type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_DGRAM(数据报)服务。对于TCP/IP协议族而言,SOCK_STEAM表示传输层使用TCP协议,SOCK_DGRAM表示传输层使用UDP协议。
  • protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一,几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议。

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

绑定socket

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

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

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

监听socket

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

1
2
#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • sockfd参数指定被监听的sock文件描述符
  • backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接。客户端也将受到ECONREFUSED错误。在内核版本2.2之前的linux中,backlog参数是值所有处于半连接状态和完全连接状态的socket上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket上限,处于半连接状态的socket上限由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义

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

接收连接

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

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

当当前监听队列中没有连接时,accept会被阻塞。

accept只是从监听队列中取出连接,而不管取出连接后,连接处于何种状态(连接或者断开)

发起连接

在客户端需要通过一下系统调用来主动与服务器建立连接:

1
2
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklent_t addrlen);
  • sockfd是由socket系统调用返回的socket文件描述符
  • serv_addr是服务器监听的socket地址
  • addrlen参数则serv_addr的长度

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

关闭连接

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

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

不过,close系统调用并非总是立即关闭一个连接,而是将fd的引用记数减一,只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将父进程中打开的socket文件描述符的引用计数加1。如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用shutdown系统调用(相对于close来说,它是专门为网络编程设计的)。

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

howto参数决定了shutdown的行为:

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

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

socket服务端,客户端示例

服务端

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
#include <sys/socket.h>
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
int sock_fd = socket(PF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
std::cout << "create socket fd fail" << std::endl;
return sock_fd;
}
struct sockaddr_in sock_addr;
sock_addr.sin_family = AF_INET;//ipv4地址协议族
sock_addr.sin_port = htons(22996); //转换为网络字节序
inet_pton(AF_INET, "localhost", &sock_addr.sin_addr);//将字符串类型的地址转换为网络类型地址

//绑定
int ret = bind(sock_fd, reinterpret_cast<struct sockaddr *>(&sock_addr), sizeof(sock_addr));
if (ret < 0) {
std::cout << "bind fail" << std::endl;
return ret;
}

//监听
ret = listen(sock_fd, 5);
if (ret < 0) {
std::cout << "listen fail" << std::endl;
return ret;
}

while (ret >= 0) {
//接收
struct sockaddr client_addr;
socklen_t client_socklen;
ret = accept(sock_fd, &client_addr, &client_socklen);
if (client_socklen == sizeof(struct sockaddr_in)) {//如果是ipv4连接
struct sockaddr_in *client_ipv4_sock = reinterpret_cast<struct sockaddr_in *>(&client_addr);
char client_ipv4_str[INET_ADDRSTRLEN];
std::cout << "connection from "
<< inet_ntop(client_ipv4_sock->sin_family, &client_ipv4_sock->sin_addr, client_ipv4_str, INET_ADDRSTRLEN) //网络ip地址转换为字符串表示
<< ":"
<< ntohs(client_ipv4_sock->sin_port) //网络字节序端口号转主机字节序
<< std::endl;

}
}
return 0;
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
int main() {
int sock_fd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_sock_addr{};
server_sock_addr.sin_family = AF_INET;
server_sock_addr.sin_port = htons(22996);
inet_pton(AF_INET, "localhost", &server_sock_addr.sin_addr);
int ret = connect(sock_fd, reinterpret_cast<struct sockaddr*>(&server_sock_addr), sizeof(server_sock_addr));
if (ret < 0) {
std::cout << "connect fail" << std::endl;
}
close(sock_fd);
return 0;
}

数据读写

TCP数据读写

对文件的读写操作read和write同样适用于socket,但是socket编程接口提供了几个专门用于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参数分别指定读缓冲区的位置和大小。recv成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次recv,才能读取到完整的数据。recv可能返回0,这意味着通信对方已经关闭连接了。recv出错时返回-1并设置errno。
  • send往sockfd上写入数据,buf和len参数分别指定缓冲区的位置和大小。send成功时返回实际写入的数据长度,失败则返回-1并设置errno。

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

选项名 含义 send recv
MSG_CONFIRM 指示数据链路层协议持续监听对方的回应,直到得到答复,它仅能用于SOCK_DGRAM和SOCK_RAW类型的socket Y N
MSG_DONTROUTE 不查看路由表,直接将数据发送给本地局域网络内的主机。这表示发送者确切地知道目标主机就在本地网络上 Y N
MSG_DONTWAIT 对socket的此次操作时非阻塞的。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

UDP数据读写

socket编程接口中用于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, sockelen_t *addrlen);
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklent_t addrlen);

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

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

这两个系统调用的flags参数以及返回值的含义均于send/recv系统调用的flags参数及返回值相同。

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

带外标记检查

当Linux内核检查到TCP紧急标志时,将通知应用程序有带外数据需要接受。内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有外带数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据,可以通过以下函数来实现:

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

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

socket选项

Linux中有下面两个系统调用是专门用来读取和设置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参数则指定选项的名字。

下表列举了socket通信中几个比较常用的socket选项。option_value和option_len参数分别是被操作选项的值和长度。不同的选项具有不同类型的值。

level option name 数据类型 说明
SOL_SOCKET(通用socket选项,与协议无关) 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 发送数据超时
IPPROTO_IP IP_TOS int 服务类型
IP_TTL int 存活时间
IPPROTO_IPV6 IPV6_NEXTHOP sockaddr_in6 下一跳IP地址
IPV6_RECVPKTINFO int 接收分组信息
IPV6_DONTFRAG int 禁止分片
IPV6_RECVTCLASS int 接收通信类型
IPPROTO_TCP TCP_MAXSEG int TCP最大报文段大小
TCP_NODELAY int 禁止Nagle算法

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

值得指出的是,对服务器而言,有部分socket选项只能在调用listen系统调用前对socket设置才有效。这是因为连接socket只能有accept调用返回,而accept从监听队列中接受的连接至少已经完成TCP三次握手的前两个步骤,这说明服务器已经向客户端发出了TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设置,比如TCP最大报文段选项。这种情况Linux给开发人员提供的解决方案是:在调用listen前,对socket设置的这些socket选项,那么accept返回的连接socket将自动继承这些选项。这些选项选项包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SDNLOWAT、TCP_MAXSEG、TCP_NODELAY而对客户端而言,这些socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已完成。

SO_REUSEADDR选项

对于处于TIME_WAIT状态的TCP连接,服务器程序可以通过设置socket选项SO_REUSEADDR来强制使用处于TIME_WAIT状态的连接占用的socket地址。

1
2
3
4
5
6
7
8
9
10
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sock > 0);
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

struct sockaddr_in sock_addr;
sock_addr.sin_family = AF_INET;
inet_pton(AF_INET, ip_string, &sock_addr.sin_addr);
sock_addr.sin_port = htons(port);
int ret = bind(sock, (sturct sockaddr *)&sockaddr, sizeof(sockaddr));

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

SO_RCVBUF和SO_SNDBUF选项

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

SO_RCVLOWAT和SO_SNDLOWAT选项

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

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

SO_LINGER选项

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

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

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

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

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

声音的产生

物体的振动产生声波,通过声音传播介质,传入人的鼓膜,再到听小骨,最后到听觉神经和大脑。

声音的三要素

  • 音调:由声音的频率决定,频率越高,声音越高,在乐音中,规定国际标准音高为440HZ,对应为音名为A4。
  • 响度:又称为音量,音强,由振幅和人离声源距离决定。
  • 音色:由发生物体材料和结构决定

模拟信号和数字信号

模拟音频信号

模拟音频信号是指时间轴连续,振幅轴连续的音频信号。自然界中存在的声音都算是模拟信号。

数字音频信号

时间和幅度都用离散的数字表示的信号。计算机只能存储和处理数字音频信号。

A/D,D/A转换

  • A/D转换:模/数转换,模拟信号转为数字信号
  • D/转换:数/模转换,数字信号转为模拟信号

一般计算机处理音频信号的过程:对模拟信号进行采样、量化、编码、压缩转换成数字信号(设备一般是麦克风🎤和声卡),然后对数字音频进行处理(变声、降噪、存储),要播放音频的时候,将音频信号再还原成模拟信号播放(设备一般是声卡和扬声器🔉)。

模拟信号到数字音频的转换(A/D转换)

采样

在模拟信号的时间轴上每隔一定时间抽取一个信号的幅度样本(时间轴数字化)。

  • 采样周期(T):每隔T秒进行一次采样
  • 采样频率(F):一秒采样多少次
  • T = 1/F

奈奎斯特采样定理:如果想要通过数字信号重建原始模拟信号,那么采样频率必须大于模拟信号最高频率的两倍。(解释

量化

由于在采样的时候,从模拟信号中获取到的幅度值其实本质上是个模拟量,这个模拟量可能是在计算机中是无法表示的,因为即使是使用浮点数,可以表示的值也都是离散的小数值,所以如果想要把采样过的值存储到计算机中进行处理,就需要将这些值转换为计算机可以处理的值。这个过程就是量化

在将计算机不能存储和处理的值转换为计算机可以存储和处理的值的时候,最后计算机存储的结果可能和和实际的结果存在偏差。这就是量化误差。例如:某个计算只能存储int类型的值,但是某次采样到的数据的值为3.1,那么量化后的值应该为3,那么就产生了量化误差。

在音频信号处理中,一般采用8bit、16bit、24bit去存储经过量化过后的值(既可以使用整型数据,也可以使用浮点型数据,使用整型数据属于均匀量化,使用浮点数据属于非均匀量化),使用的bit位数越多,可以表示的数据量就越多,可以表示的精度就越高。所以一般高清音质,高保真音质使用的24bit来存储量化后的值。

PCM文件

WAV文件

音频编解码

音频编解码用于对量化后的音频数据进行压缩,方便对音频数据的存储和传输。

一些基础概念

码率

指音频(或视频)文件在单位时间内使用的数据量,单位一般是Kb/s或者Mb/s(注意是bit)。固定码率是指音频(或视频)文件在每一个单位时间内使用的数据量都相等。可变码率指不同单位时间内使用的数据量可以不同。

压缩比

原始数据和压缩后的数据总体的大小占比。一般来说,对于同一个原始数据,压缩比越小,码率越高,最后还原出来的数据越真实。

三大主要音频编解码标准

ITU

主要指定有线语言压缩标准,一般用于语音通话

3GPP

主要指定无线语音压缩标准

MPEG

主要指定音乐压缩标准等,例如MP3、AAC编解码标准

shader简介

shader程序是OpenGL渲染管线中的某个步骤,他运行在GPU上。shader程序之间是隔离的,他们之间进行通信的唯一方式就是接受上游shader的输出作为自己的输入并输出新的数据作为下游的输入。

GLSL

shader程序是有GLSL这种编程语言编写的,shader总是以一个版本声明开始,接下来就是一系列的输入输出变量和uniform,然后就是main函数,每个shader程序都是以main函数作为程序入口,在main函数中处理输入变量并为输出变量赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

void main()
{
// process input(s) and do some weird graphics stuff
...
// output processed stuff to output variable
out_variable_name = weird_stuff_we_processed;
}

每个shader的输入变量也叫做vertex attribute,由于硬件的限制,我们在shader中声明的vertex attribute的数量是有上限的,OpenGL保证至少可以声明16个4维变量,最大数量可以通过查询GL_MAX_VERTEX_ATTRIBS来获取。

1
2
3
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

虽然大多数情况下最多值就等于最小值16。

Types

GLSL 有和C语言一样的基础类型: int, float, double, uint and bool。另外GLS也有两种container类型: vectorsmatrices

Vectors

GLSL中的vector类型是可以容纳1、2、3 或者 4 个基本类型元素的container。它们有以下几种声明形式form (n 代表元素的个数):

  • vecn: the default vector of n floats.
  • bvecn: a vector of n booleans.
  • ivecn: a vector of n integers.
  • uvecn: a vector of n unsigned integers.
  • dvecn: a vector of n double components.

我们可以使用 .x, .y, .z and .w 来分别获取到第一到第四个元素。大多数情况下,我们使用float类型的vecn就足够了。

vertor类型有一些很方便的赋值和构造方式:

1
2
3
4
5
6
7
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

Matrix

//TODO: 看完后序章节后补全矩阵这部分。

Ins And Outs

每个shader程序使用inout关键字来标识输入输出,如果下一个shader的输入变量和上一个shader的输出变量名一致,OpenGL就会将变量的值传递过去(这个逻辑是在链接shader program的时候完成的)。

但是vertex shader和fragment shader有一些特殊的地方。因为vertex是渲染管线的第一个阶段,它的输入由用户来定义。为了说明vertex shader的输入是如何组织的,我们在vertex shader中定义输入的时候还需要使用location来说明:

1
layout (location=0) in vec3 pos;

然后用户在CPU中定义输入的来源数据。

fragment shader要求必须有一个vec4类型的颜色输出变量,因为fragment shader的作用就是生成最后像素的颜色值。如果你没有定义这个颜色输出,OpenGL最后输出的可能就是纯黑色或者纯白色。

So if we want to send data from one shader to the other we’d have to declare an output in the sending shader and a similar input in the receiving shader. When the types and the names are equal on both sides OpenGL will link those variables together and then it is possible to send data between shaders (this is done when linking a program object).

Uniforms

uniforms是另外一种从CPU传递数据到shader程序的方式,uniform和vertex attribute不同,它是全局性质的,也就是说,uniform变量在shader program之间是隔离的,但是在shader program中的每个shader之间是共享的。另外一旦uniform变量被赋值,它的值就会一直保持这个,直到下一次被重置或者重新赋值。定义uniform变量,我们只需在定义变量时使用uniform关键字即可:

1
uniform vec4 outColor;

如果你你定义了uniform变量但是没有在任何地方使用,GLSL编译器或最终会将这个变量移除。

uniform变量的赋值可以在glsl中赋值,也可以在CPU中赋值,在CPU中赋值时,需要先获取到uniform变量的location,然后才能赋值:

1
2
3
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, 1.0, 0.0f, 1.0f);
注意,在CPU端获取uniform变量时不需要先调用glUseProgram,但是为uniform变量赋值的之前则必须先调用glUseProgram。

fragment interpolation

OpenGL在渲染一个primitive时,在rasterization阶段,一个primitive最终会对应到屏幕中的若干个像素,也即是若干个fragmen,rasterization阶段会决定这些fragment的位置,基于这个位置,fragment shader中的所有输入变量的值将会是对应primitive中各顶点输出值的线性插值结果。

OpenGL简介(What is OpenGL)

OpenGL是一个提供操作图形和图像的API标准(注意是标准,而不是实际的某种编程语言的接口),这些接口标准由Khronos Group管理。OpenGL接口标准定义每个函数的输入输出是什么,每个函数应该有怎样的表现等等。这些接口最终由开发者来实际实现(一般是显卡厂商)。虽然不同开发者实现的Open GL的代码细节不同,但是由于标准的存在,对于用户来说使用方式和最终结果是没有区别的(如果有,那就是显卡制造商写的OpenGL实现有问题)。

Core Profile模式和Immdiate 模式

在以前,使用OpenGL开发用的是immdiate模式,immediate模式是指固定的渲染管线,用户只需要开发少量的代码,就能获得最终的渲染图形,但是同时,用户对渲染过程的控制度很低,在处理渲染上灵活性很差。从OpenGL 3.2开始,OpenGL开始使用core-profile模式,从3.2版本开始,这是OpenGL的一个分支,这个版本删除了所有旧的不推荐使用的功能。当我们在core-profile模式下,OpenGL会强迫我们使用现代的渲染方法,当我们尝试使用已经Deprecated的方法时,OpenGL会抛出异常并停止渲染。

OpenGL扩展(Extensions)

OpenGL一个很好的特性就是扩展,当显卡公司对于渲染有了新的优化或者有新的渲染技术,这些就可以通过扩展的形式发布出去给用户使用,而不用等OpenGL把这些功能新加入到OpenGL标准中,对于用户而言,在编写OpenGL程序的时候只需要判断是否有相应扩展,如果有就可以使用相应的接口。

1
2
3
4
5
6
7
8
if(GL_ARB_extension_name)
{
// Do cool new and modern stuff supported by hardware
}
else
{
// Extension not supported: do it the old way
}

状态机

OpenGL标准本身可以看作是一个状态机的定义:定义当前OpenGL应该如果运行的变量集合。OpenGL的状态同城称为OpenGL context,在使用OpenGL时,我们经常通过设置一些选项、操作一些缓冲区然后对当前上下文进行渲染来更改其状态。

GLAD

OpenGL 实际上只是一个标准/规范,由驱动程序制造商将规范实现到特定显卡支持的驱动程序。由于OpenGL驱动有很多不同的版本,其大部分函数的位置在编译时是未知的,需要在运行时查询。然后开发人员的任务是检索需要的函数的位置并将它们存储在函数指针中供以后使用,这个步骤可以通过GLAD这个库来帮助实现。

渲染管线

在OpenGL中,所有的都是3维空间,但是屏幕石油二维的像素组成的,所以OpenGL大多数的工作其实就是将3维空间的坐标转换到二维空间的像素,这个过程就是有渲染管线来完成的。渲染管线可以分为两大部分:第一部分,将3D坐标转换为2D坐标;第二部分将2D坐标转换为具体的颜色像素。

渲染管线的具体步骤可以分为多步,每一步都需要将前一步的输出作为自己的输入,并且每一步都是高度可定制化的,而且可以很方便的并行处理。由于并行化的特性,现代图形卡可以快速处理渲染管线中的步骤。

Because of their parallel nature, graphics cards of today have thousands of small processing cores to quickly process your data within the graphics pipeline. The processing cores run small programs on the GPU for each step of the pipeline. These small programs are called shaders.

vertex shader

vertex shader将单个vertex作为输入,单个vertex作为输出。vertex shader的主要作用是将3D坐标转换为另外一个3D坐标,并对vertex的属性做一些额外的处理。

primitive assembly

primitive assembly步骤将vertex shader的所有输出作为输入,以此来形成primitive shape,primitive shape是组成最终物体的基础形状。

In order for OpenGL to know what to make of your collection of coordinates and color values OpenGL requires you to hint what kind of render types you want to form with the data. Do we want the data rendered as a collection of points, a collection of triangles or perhaps just one long line? Those hints are called primitives and are given to OpenGL while calling any of the drawing commands. Some of these hints are GL_POINTS, GL_TRIANGLES and GL_LINE_STRIP.

geometry shader

primitive assembly步骤的输出作为geometry shader的输入。geometry shader将一组形成primitive的vertex作为输入,geometry shader在此基础上通过生成新的点来生成新的primitive(可以和primitve assembly阶段生成的primitive不同)以此来生成其他新的形状。

rasterization

resterization步骤将geometry shader的输出作为输入,rasterization阶段将最终的primitive转换为最终显示在屏幕的像素点位置,并且将超出屏幕部分的去掉以提高性能。

fragment shader

fragment shader的主要目的是计算像素点的最终颜色,这个阶段一般是所有OpenGL高级特效生成的阶段。通常fragment shader包含可以用来计算最终像素颜色的数据,例如光照,光照颜色,阴影等信息。

A fragment in OpenGL is all the data required for OpenGL to render a single pixel.

alpha test and blender

在所有颜色值都已经被决定后,会进入到alpha test和blender阶段。这个阶段会检查frament的深度信息和stencil信息,判断这个fragment是在其他object的前面还是后面,一次判断是否需要最终渲染这个fragment。另外这一步也会检查alpha通道,以此来混合(blend)颜色信息。因此,即使在fragment shader中计算出了像素的输出颜色,最终像素的实际颜色也可能会和计算出来的不同。

可以看到,一个渲染管线包含了很多步骤和很多可配置的渲染属性。但是,在大多数情况下,我们只需要和处理vertex shader和fragment shader就可以了。geometry shader是可选的并且通常用默认的geometry shader实现就可以了。vertex shader和fragment shader是现代OpenGL要求用户至少提供的两个shader,这两个shader没有默认的实现。

coin-or 简介

single: compile single lib by using configure

./configure \
—host=arm-apple-darwin \
—enable-static \
—disable-shared \
—prefix=$PWD/build/arm64 \
CC=gcc \
CFLAGS=”-DNDEBUG -miphoneos-version-min=8.0 -arch arm64 -g -O0 -fembed-bitcode \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk” \
CXX=g++ \
CXXFLAGS=”-DNDEBUG -miphoneos-version-min=8.0 -arch arm64 -g -O0 -fembed-bitcode \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk \
-std=c++11 -stdlib=libc++” \
LDFLAGS=”” \
LIBS=”-lc++ -lc++abi”

导出 PKG_CONFIG_PATH

export pkgconfig path
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/Users/bytedance/Desktop/coin-or/Osi/build/arm64/lib/pkgconfig
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/Users/bytedance/Desktop/coin-or/CoinUtils/build/arm64/lib/pkgconfig

pkgconfig folder can be read by pkg-config

遇到报错ld: -bind_at_load and -bitcode_bundle (Xcode setting ENABLE_BITCODE=YES) cannot be used together

export MACOSX_DEPLOYMENT_TARGET=”10.15.0”

ios

./coinbrew build Clp \
—host=arm-apple-darwin \
—enable-static \
—disable-shared \
—prefix=$PWD/ios \
CC=gcc \
CFLAGS=”-DNDEBUG -miphoneos-version-min=8.0 -arch arm64 -g -O0 -fembed-bitcode \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk” \
CXX=g++ \
CXXFLAGS=”-DNDEBUG -miphoneos-version-min=8.0 -arch arm64 -g -O0 -fembed-bitcode \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk \
-std=c++11 -stdlib=libc++” \
LDFLAGS=”” \
LIBS=”-lc++ -lc++abi” \
—tests none \
—no-third-party \
—verbosity 4

osx to_be_fixed

./coinbrew build Clp —enable-static —disable-shared CC=clang CXX=clang++ —tests none —prefix=$PWD/osx —no-third-party

android

android armv8-a

export NDK=/Users/bytedance/Downloads/android-ndk-r21b
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
export TARGET=aarch64-linux-android
export API=21
export AR=$TOOLCHAIN/bin/$TARGET-ar
export AS=$TOOLCHAIN/bin/$TARGET-as
export CC=$TOOLCHAIN/bin/$TARGET$API-clang
export CXX=$TOOLCHAIN/bin/$TARGET$API-clang++
export LD=$TOOLCHAIN/bin/$TARGET-ld
export RANLIB=$TOOLCHAIN/bin/$TARGET-ranlib
export STRIP=$TOOLCHAIN/bin/$TARGET-strip

./coinbrew build Clp \
—host=$TARGET \
—enable-static \
—disable-shared \
—prefix=$PWD/android/armv8-a \
—tests none \
—no-third-party

android armv7-a

export NDK=/Users/bytedance/Downloads/android-ndk-r21b
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
export TARGET=armv7a-linux-androideabi
export API=21
export AR=$TOOLCHAIN/bin/arm-linux-androideabi-ar
export AS=$TOOLCHAIN/bin/arm-linux-androideabi-as
export CC=$TOOLCHAIN/bin/$TARGET$API-clang
export CXX=$TOOLCHAIN/bin/$TARGET$API-clang++
export LD=$TOOLCHAIN/bin/arm-linux-androideabi-ld
export RANLIB=$TOOLCHAIN/bin/arm-linux-androideabi-ranlib
export STRIP=$TOOLCHAIN/bin/$arm-linux-androideabi-strip

./coinbrew build Clp \
—host=$TARGET \
—enable-static \
—disable-shared \
—prefix=$PWD/android/armv7-a \
—tests none \
—no-third-party

local

static

./coinbrew build Clp —enable-static —disable-shared —tests none —prefix=$PWD/static —no-third-party

dynamic

./coinbrew build Clp —prefix=$PWD/dynamic —no-third-party —verbosity 4