笔记栈

  • 首页

  • 归档

  • 搜索

c++ primer

发表于 2021-02-23 | 更新于 2021-03-10

3章 字符串,向量和数组

  1. 字符串和字面值的拼接

    1
    2
    3
    4
    5
    string s1 = "hello", s2 = "world";
    string s4 = s1 + ","; // 正确
    string s5 = "hello" + ", "; // 错误:不能把字面值直接相加
    string s6 = s1 + ", " + "world"; // 正确
    string s7 = "hello" + ", " + s2; // 错误:不能把字面值直接相加
  2. c语言头文件如name.h,c++则将这些文件命名为cname,去掉h增加c,如cstring。

  3. for (declaration : expression)这种称为范围for语句(range for)。

  4. 既有类模板,也有函数模板,vector是一个类模板,模板不是类或函数,可以将模板看作为编译器生成类或函数编写的一份说明,编译器根据模板创建类或者函数的过程称为实例化,当使用模板时,需要指出编译器应把类或函数实例化成何种类型,在模板名字后面跟一对尖括号,在括号内放上信息。

  5. 某些编译器可能仍需以老式的声明语句来处理元素为vector的vector对象,如 vector<vector<int> >

  6. end()返回的迭代器称为尾后迭代器,该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素。

  7. 标准容器迭代器的运算符

    1
    2
    3
    4
    5
    6
    *iter:返回迭代器iter所指元素的引用
    iter->mem:解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
    ++iter:令iter指示容器中的下一个元素
    --iter:令iter指示容器中的上一个元素
    iter1 == iter2
    iter1 != iter2
  8. 迭代器类型

    1
    2
    3
    4
    vector<int>::iterator it1 = vec.begin();
    string::iterator it2 = s.begin();
    vector<int>::const_iterator it3 = vec.cbegin();
    string::const_iterator it4 = s.cbegin();
  9. 数组

    1
    2
    3
    4
    5
    6
    unsigned cnt = 42;
    constexpr unsigned sz = 42;
    int arr[10];
    int *parr[sz]; // 正确,sz是常量表达式
    string bad[cnt]; // 错误,cnt不是常量表达式
    string strs[get_size()]; // 当get_size是constexpr时正确,否则错误

4章 表达式

  1. 左值右值,简单理解为:左值指向一个具体内存地址,只要不是左值既是右值。具体含义参考https://www.jianshu.com/p/94b0221f64a5
  • 左值引用只能左值去赋值;
  • 常量左值引用除了左值赋值,也可以字面值赋值也就是右值可以赋值。
  1. 位运算符
  • 一般来说,如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型;
  • 左移运算符在右侧插入值为0的二进制位;
  • 右移运算符的行为依赖其左侧运算对象类型,如果运算对象是无符号类型,在左侧插入值为0的二进制位,如果该运算对象是带符号类型,在左侧插入符号位的副本或值为0的二进制位,如何选择要视具体环境而定;
  • 位异或运算符,如果两个运算对象的对应位置有且只有一个为1则运算结果中该位为1,否则为0。
  1. sizeof是运算符不是函数。
  2. 类型转换之显示转换
  • static_cast,dynamic_cast,const_cast,reinterpret_cast

5章 语句

  1. 定义的异常类
  • exception:最常见的问题
  • runtime_error:只有在运行时才能检测出的问题
  • range_error:运行时错误,生成的结果超出了有意义的值域范围
  • overflow_error:运行时错误,计算上溢
  • underflow_error:运行时错误,计算下溢
  • logic_error:程序逻辑错误
  • domain_error:逻辑错误,参数对应的结果值不存在
  • invalid_argument:逻辑错误,无效参数
  • length_error:逻辑错误,试图创建一个超出该类型最大长度的对象
  • out_of_range:逻辑错误,使用一个超出有效范围的值

6章 函数

  1. 可变长参数
  • initializer_list:这是一种标准库类型,跟vector一样是模板类型,跟vector不一样的是元素永远是常量值;
  • 如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内;
  1. 返回数组指针
  • 参考代码如下
    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
    #include <iostream>

    /* 1 */
    int t1[3] = { 1, 2, 3 };
    int(*func1())[3]
    {
    return &t1;
    }

    /* 2 */
    typedef int arrT[3];
    arrT t2 = { 10, 20, 30 };
    arrT* func2()
    {
    return &t2;
    }

    /* 3 尾置返回类型 */
    int t3[3] = { 100, 200, 300 };
    auto func3() -> int (*)[3]
    {
    return &t3;
    }

    /* 4 */
    int t4[3] = { 1000, 2000, 3000 };
    decltype(t4)* func4()
    {
    return &t4;
    }

    /* main */
    int main()
    {
    int (*t1)[3] = func1();
    for (int i = 0; i < sizeof(*t1) / sizeof(int); i++)
    {
    std::cout << (*t1)[i] << std::endl;
    }

    int (*t2)[3] = func2();
    for (int i = 0; i < sizeof(*t2) / sizeof(int); i++)
    {
    std::cout << (*t2)[i] << std::endl;
    }

    int(*t3)[3] = func3();
    for (int i = 0; i < sizeof(*t3) / sizeof(int); i++)
    {
    std::cout << (*t3)[i] << std::endl;
    }

    int(*t4)[3] = func4();
    for (int i = 0; i < sizeof(*t4) / sizeof(int); i++)
    {
    std::cout << (*t4)[i] << std::endl;
    }
    return 0;
    }
  1. 函数特性
  • 一次函数调用其实包含一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行;
  • 一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数,很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联展开;
  • constexpr函数是指用于常量表达式的函数,有几个约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句(函数体内可以包含其他语句,只要这些语句在运行时不执行任何操作就行);
  • 为了能把constexpr函数在编译过程中展开,它被隐式地指定为内联函数;
  • constexpr函数返回的不一定是常量表达式;
  • 和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编译器要想展开函数只有声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或constexpr函数来说,它的多个定义必须保持一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中;
  1. 调试
  • assert(expr)是预处理宏,首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行,如果表达式为真(即非0),assert什么也不做;
  • assert的行为依赖于一个名为NDEBUG的预处理变量的状态,如果定义了NDEBUG,则assert什么也不做,默认状态是没有定义NDEBUG的;
  1. 指向函数的指针
  • 当把一个函数名作为一个值使用时,该函数自动转换成指针;

    1
    2
    pf = lengthCompare;	// pf指向名为lengthCompare的函数
    pf = &lengthCompare; // 等价的赋值语句;取地址符是可选的
  • 我们还能直接使用指向函数的指针调用该函数,无需提前解引用指针;

    1
    2
    3
    bool b1 = pf("hello", "hi");	// 调用lengthCompare函数
    bool b2 = (*pf)("hello", "hi"); // 一个等价的调用
    bool b3 = lengthCompare("hello", "hi"); // 另一个等价的调用
  • 函数指针形参;

    1
    2
    3
    void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));	// 第三个形参是函数类型,它会自动地转换成指向函数的指针
    void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &)); // 等价的声明:显示地将形参定义成指向函数的指针
    useBigger(s1, s2, lengthCompare); // 自动将函数lengthCompare转换成指向该函数的指针
  • 直接使用函数指针类型显得冗长而繁琐;

    1
    2
    3
    4
    5
    6
    // Func和Func2是函数类型
    typedef bool Func(const string &, const string &);
    typedef decltype(lengthCompare) Func2;
    // FuncP和FuncP2是指向函数的指针
    typedef bool(*FuncP)(const string &, const string &);
    typedef decltype(lengthCompare) *FuncP2;
  • 跟数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动将函数返回类型当成对应的指针类型处理;

    1
    2
    int (*f1(int))(int *, int);
    auto f1(int) -> int (*)(int *, int);

7章 类

  1. 构造函数
  • 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数;
  • 拷贝,赋值和析构主要参考13章;
  1. class或者struct
  • 类既可以使用class也可以使用struct,唯一区别就是默认访问权限,struct默认是public,class默认是private;
  1. 友元
  • 类可以允许其他类或函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend);
  • 一般来说,最好在类定义开始或结束前的位置集中声明友元;
  1. 类的其他特性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Screen {
    public:
    typedef std::string::size_type pos;
    Screen() = default; // 因为Screen有另一个构造函数,所以本函数是必需的
    Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) {}
    char get() const {
    return contents[cursor]; // 读取光标处的字符,隐式内联
    }
    inline char get(pos ht, pos wd) const; // 显式内联
    Screen &move(pos r, pos c); // 能在之后被设为内联
    private:
    pos cursor = 0; // 类内初始值,必需以符号=或者花括号表示
    pos height = 0, width = 0;
    std::string contents;
    };
  2. 类的声明

  • class Screen;对于类型Screen来说,在它声明之后定义之前是一个不完全类型(incomplete type),也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员;
  • 不完全类型使用场景有限:可以定义指向这种类型的指针或引用,也可以声明(但不能定义)以不完全类型作为参数或者返回类型的函数;
  • 直到类被定义后数据成员才能被声明成这种类型。换句话说,我们必须完成类的定义,然后编译器才能知道存储该数据成员需要多少空间。因为只有当类全部完成后类才算被定义,所以一个类的成员类型不能是该类自己。然而,一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针;

    1
    2
    3
    4
    5
    class Link_screen {
    Screen window;
    Link_screen *next;
    Link_screen *prev;
    };
  • 成员初始化顺序,与他们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推;

    1
    2
    3
    4
    5
    6
    7
    class X {
    int i;
    int j;
    public:
    // 未定义的:i在j之前被初始化
    X(int val): j(val), i(j) {}
    };
  1. explicit
  • explicit只能用于直接初始化(阻止隐式转换);

    1
    2
    Sales_data item1(null_book);	// 正确:直接初始化
    Sales_data item2 = null_book; // 错误:不能将explicit构造函数用于拷贝形式的初始化过程
  • 只对一个实参的构造函数有效;

  • 需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit;
  • 只能在类内声明构造函数时使用explicit,在类外部定义时不应重复;

    8章 IO库

基于TCP的文件传输程序的bug分析

发表于 2019-07-02

背景

不知为何,我们的某些服务器间用ftp无法传输文件,倒是能执行ftp的某些命令,我看防火墙配置也是正确的,主动或被动模式都没法传输文件,一时间有点懵逼?索性就写个基于tcp的文件传输吧:stransfer作为服务端,ctransfer作为客户端,由客户端向服务端传输文件。

transfer代码:https://github.com/skypanda100/transfer

现象

过了两天后,代码写完,本地测试都已通过,拿到服务器上跑,居然还是不能传输数据到目标服务器:客户端能够连接服务端,并且客户端发送口令到服务端,服务端通过后发送认证成功消息给客户端,客户端再发送文件,可是一直阻塞在第一次传输文件的阶段,也不知道是客户端阻塞,还是服务端阻塞,再过一段时间链接居然断开了(connection time out)。

分析

拼命想是哪里出问题了?

  • 如果是接收缓冲区满而又不recv,这就会导致发送缓冲区满,客户端就会阻塞在send,可是我一直在recv啊,所以不是这种情况。
  • 如果是因为限速了呢,我把配置文件中的buffer_size(每次传输的大小)从4k改到1k,发现还是会阻塞,但不是阻塞在第一次传输,而且服务端确实接收到了一些数据。再从1k改到512,发现还是会阻塞,但会阻塞在第N次传输(反正比1k,4k时要多)。
  • 接着上面的如果继续想,我的客户端是连续send文件内容,根据tcp的Nagle算法,他会将多次send的内容填满发送缓冲区后再发送出去(当然该算法不是这么简单,比如send一次,在一定时间后Nagle算法会将其发送出去,我记得这个时间是40ms。连续send,则需要缓冲区满后一次发送出去,这样是为了提高性能,具体算法要百度一下),就在此时,发送的数据大小已经超过限制了,接收端无法接收数据。好,感觉有戏了,我就把发送端的socket设置为TCP_NODELAY(禁用Nagle算法,意思就是说tcp不用等发送缓冲区满后再发送,send一次就发送一次),结果,残念啊,还是不行。

我的程序和ftp明明在大部分环境里都正常,可在这里却不正常,为什么… …

吃过饭,刷完新闻,就在倒头入睡之时,反应过来了:

服务端使用epoll每隔3s检查一次socket,客户端为了提升传输效率,会连续不断send文件内容。从这里可以得知,由于客户端send之迅猛,在3s内,服务端的接收缓冲区会填满,这时,tcp对端不再会发送数据过来,数据会在发送缓冲区中等待(即使客户端将socket设置为TCP_NODELAY),3s过后,服务端开始recv,tcp协议发现接收缓冲区被消费了,告诉发送端可以发消息了,此时,发送端一口气把发送缓冲区的数据全部发送,这里就出问题了,因为超过规定的数据量了(缓冲区大小肯定是大于4k的,很有可能限速是低于4k),所以buffer_size为4k时,总是阻塞在第一次,buffer_size改为1K,阻塞在第N次,buffer_size改为512,阻塞在第N+M次。

怎么改呢?客户端不要连续send,send一次,等待服务端回答一次,然后循环往复;buffer_size改小一点儿(没有限速的环境就将buffer_size提升到最大4k吧)。

参考

https://www.cnblogs.com/JohnABC/p/7238417.html

https://www.cnblogs.com/wajika/p/6573014.html

Nmap

发表于 2019-06-25 | 更新于 2019-06-26

Nmap

Nmap 是一款网络扫描和主机侦测的非常有用的工具。

  • 扫描端口,侦测哪些端口是否开放

    open:端口开放;closed:防火墙开放了端口,但是未使用;filtered:防火墙禁止的端口。

    1
    nmap -sS -p 12300-12315 -v IP地址
  • 扫描端口,侦测某个端口是否开放

    1
    nmap -p 12300 IP地址
  • 主机发现,Ping扫描

    1
    nmap -sP 192.168.1.0/24
  • 主机发现,无Ping,慢得很

    1
    nmap -P0 192.168.1.0/24

ip后的24的含义是子网掩码前24位为1,也就是255.255.255.0

更多内容可以参考nmap的manual

参考

https://baike.baidu.com/item/nmap/1400075?fr=aladdin

再谈IO多路复用之epoll

发表于 2019-05-12 | 更新于 2019-05-13

API

epoll_create

1
2
3
#include <sys/epoll.h>

int epoll_create(int size);

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

1
2
3
4
5
lrwx------. 1 root root 64 5月  12 11:45 0 -> /dev/pts/0
lrwx------. 1 root root 64 5月 12 11:45 1 -> /dev/pts/0
lrwx------. 1 root root 64 5月 12 11:45 2 -> /dev/pts/2
lrwx------. 1 root root 64 5月 12 11:45 3 -> socket:[251991]
lrwx------. 1 root root 64 5月 12 11:45 4 -> anon_inode:[eventpoll]
  • size:告诉内核这个监听的数目一共有多大。

epoll_ctl

1
2
3
#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

  • epfd:epoll_create()的返回值。
  • op:动作,用三个宏来表示:
    1. EPOLL_CTL_ADD:注册新的fd到epfd中。
    2. EPOLL_CTL_MOD:修改已经注册的fd的监听事件。
    3. EPOLL_CTL_DEL:从epfd中删除一个fd。
  • fd:需要监听的fd。
  • event:告诉内核需要监听什么事,struct epoll_event结构如下:

    1
    2
    3
    4
    struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
    };

    events可以是以下几个宏的集合:

    1. EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。
    2. EPOLLOUT:表示对应的文件描述符可以写。
    3. EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。
    4. EPOLLERR:表示对应的文件描述符发生错误。
    5. EPOLLHUP:表示对应的文件描述符被挂断。
    6. EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
      • LT模式:默认为此模式,当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
      • ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

epoll_wait

1
2
3
#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待事件的产生,类似于select()调用。

  • epfd:epoll_create()的返回值。
  • events:从内核得到事件的集合。
  • maxevents:告诉内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。
  • timeout:超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
  • 返回值:需要处理的事件数目,如返回0表示已超时,-1表示有错误发生。

特点

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

Example

下面的程序是基于socket的tcp应答程序。

  • 服务端
    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
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/epoll.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <string.h>
    #include <unistd.h>
    #define BACKLOG 5 //完成三次握手但没有accept的队列的长度
    #define CONCURRENT_MAX 8 //应用层同时可以处理的连接
    #define SERVER_PORT 11332
    #define BUFFER_SIZE 1024
    #define QUIT_CMD ".quit\n"

    int main(int argc, const char * argv[])
    {
    char input_msg[BUFFER_SIZE];
    char recv_msg[BUFFER_SIZE];
    //本地地址
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero), 8);
    //创建socket
    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(server_sock_fd == -1)
    {
    perror("socket error");
    return 1;
    }
    //绑定socket
    int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if(bind_result == -1)
    {
    perror("bind error");
    return 1;
    }
    //listen
    if(listen(server_sock_fd, BACKLOG) == -1)
    {
    perror("listen error");
    return 1;
    }

    //create epollfd
    int events_len = CONCURRENT_MAX + 2;
    int epollfd = epoll_create(events_len);
    if(epollfd == -1)
    {
    fprintf(stderr, "create epollfd failed!\n");
    exit(-1);
    }

    //clientfd
    int clientfds[CONCURRENT_MAX];
    for(int i = 0;i < CONCURRENT_MAX;i++)
    {
    clientfds[i] = -1;
    }

    //add event to kernel
    struct epoll_event stdin_event;
    stdin_event.events = EPOLLIN;
    stdin_event.data.fd = STDIN_FILENO;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, STDIN_FILENO, &stdin_event);

    struct epoll_event server_event;
    server_event.events = EPOLLIN;
    server_event.data.fd = server_sock_fd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, server_sock_fd, &server_event);

    //get events from kernel
    int timeout = 20 * 1000;
    struct epoll_event events[events_len];

    //do epoll
    while(1)
    {
    int ret = epoll_wait(epollfd, events, events_len, timeout);
    if(ret < 0)
    {
    perror("epoll 出错\n");
    continue;
    }
    else if(ret == 0)
    {
    printf("epoll 超时\n");
    continue;
    }
    else
    {
    for(int i = 0;i < ret;i++)
    {
    int fd = events->data.fd;
    if(fd == server_sock_fd && (events->events & server_event.events))
    {
    //有新的连接请求
    struct sockaddr_in client_address;
    socklen_t address_len;
    int client_sock_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);
    printf("new connection client_sock_fd = %d\n", client_sock_fd);
    if(client_sock_fd > 0)
    {
    int index = -1;
    for(int client_i = 0;client_i < CONCURRENT_MAX;client_i++)
    {
    if(clientfds[client_i] == -1)
    {
    index = client_i;
    clientfds[client_i] = client_sock_fd;

    // add event to kernel
    struct epoll_event client_event;
    client_event.events = EPOLLIN;
    client_event.data.fd = client_sock_fd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, client_sock_fd, &client_event);

    break;
    }
    }
    if(index >= 0)
    {
    printf("新客户端(%d)加入成功 %s:%d\n", index, inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
    }
    else
    {
    bzero(input_msg, BUFFER_SIZE);
    strcpy(input_msg, "服务器加入的客户端数达到最大值,无法加入!\n");
    send(client_sock_fd, input_msg, BUFFER_SIZE, 0);
    printf("客户端连接数达到最大值,新客户端加入失败 %s:%d\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
    }
    }
    }
    else if(fd == STDIN_FILENO && (events->events & stdin_event.events))
    {
    bzero(input_msg, BUFFER_SIZE);
    fgets(input_msg, BUFFER_SIZE, stdin);
    //输入“.quit"则退出服务器
    if(strcmp(input_msg, QUIT_CMD) == 0)
    {
    exit(0);
    }
    for(int client_i = 0;client_i < CONCURRENT_MAX;client_i++)
    {
    if(clientfds[client_i] > 0)
    {
    printf("向客户端(%d)发送消息\n", client_i);
    send(clientfds[client_i], input_msg, BUFFER_SIZE, 0);
    }
    }
    }
    else
    {
    //处理某个客户端过来的消息
    bzero(recv_msg, BUFFER_SIZE);
    long byte_num = recv(events[i].data.fd, recv_msg, BUFFER_SIZE, 0);
    if(byte_num > 0)
    {
    if(byte_num > BUFFER_SIZE)
    {
    byte_num = BUFFER_SIZE;
    }
    recv_msg[byte_num] = '\0';
    for(int client_i = 0;client_i < CONCURRENT_MAX;client_i++)
    {
    if(clientfds[client_i] == events[i].data.fd)
    {
    printf("客户端(%d):%s\n", client_i, recv_msg);
    break;
    }
    }
    }
    else if(byte_num < 0)
    {
    for(int client_i = 0;client_i < CONCURRENT_MAX;client_i++)
    {
    if(clientfds[client_i] == events[i].data.fd)
    {
    printf("从客户端(%d)接受消息出错.\n", client_i);
    break;
    }
    }
    }
    else
    {
    // delete event in kernel
    struct epoll_event client_event;
    client_event.events = EPOLLIN;
    client_event.data.fd = events[i].data.fd;
    epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, &client_event);

    for(int client_i = 0;client_i < CONCURRENT_MAX;client_i++)
    {
    if(clientfds[client_i] == events[i].data.fd)
    {
    clientfds[client_i] = -1;
    printf("客户端(%d)退出了.\n", client_i);
    break;
    }
    }
    }
    }
    }
    }
    }
    close(epollfd);

    return 0;
    }
  • 客户端(仍然采用select)
    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
    #include<stdio.h>
    #include<stdlib.h>
    #include<netinet/in.h>
    #include<sys/socket.h>
    #include<arpa/inet.h>
    #include<string.h>
    #include<unistd.h>
    #define BUFFER_SIZE 1024

    int main(int argc, const char * argv[])
    {
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(11332);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero), 8);

    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(server_sock_fd == -1)
    {
    perror("socket error");
    return 1;
    }
    char recv_msg[BUFFER_SIZE];
    char input_msg[BUFFER_SIZE];

    if(connect(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == 0)
    {
    fd_set client_fd_set;
    struct timeval tv;

    while(1)
    {
    tv.tv_sec = 20;
    tv.tv_usec = 0;
    FD_ZERO(&client_fd_set);
    FD_SET(STDIN_FILENO, &client_fd_set);
    FD_SET(server_sock_fd, &client_fd_set);

    select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv);
    if(FD_ISSET(STDIN_FILENO, &client_fd_set))
    {
    bzero(input_msg, BUFFER_SIZE);
    fgets(input_msg, BUFFER_SIZE, stdin);
    if(send(server_sock_fd, input_msg, BUFFER_SIZE, 0) == -1)
    {
    perror("发送消息出错!\n");
    }
    }
    if(FD_ISSET(server_sock_fd, &client_fd_set))
    {
    bzero(recv_msg, BUFFER_SIZE);
    long byte_num = recv(server_sock_fd, recv_msg, BUFFER_SIZE, 0);
    if(byte_num > 0)
    {
    if(byte_num > BUFFER_SIZE)
    {
    byte_num = BUFFER_SIZE;
    }
    recv_msg[byte_num] = '\0';
    printf("服务器:%s\n", recv_msg);
    }
    else if(byte_num < 0)
    {
    printf("接受消息出错!\n");
    }
    else
    {
    printf("服务器端退出!\n");
    exit(0);
    }
    }
    }
    //}
    }
    return 0;
    }

参考

http://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html

再谈IO多路复用之poll

发表于 2019-05-09 | 更新于 2019-05-13

API

1
2
3
#include <poll.h>

int poll( struct pollfd *fds, unsigned int nfds, int timeout);
  • fds:待测试的描述以及待测试的事件等,可以是多个。
    pollfd结构体如下:

    1
    2
    3
    4
    5
    struct pollfd {
    int fd; /* 文件描述符 */
    short events; /* 等待的事件 */
    short revents; /* 实际发生了的事件 */
    };

    每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。合法的事件如下:

    • POLLIN:有数据可读
    • POLLRDNORM:有普通数据可读
    • POLLRDBAND:有优先数据可读
    • POLLPRI:有紧迫数据可读
    • POLLOUT:写数据不会导致阻塞
    • POLLWRNORM:写普通数据不会导致阻塞
    • POLLWRBAND:写优先数据不会导致阻塞
    • POLLMSGSIGPOLL:消息可用

    此外,revents域中还可能返回下列事件:

    • POLLER:指定的文件描述符发生错误
    • POLLHUP:指定的文件描述符挂起事件
  • POLLNVAL:指定的文件描述符非法

    POLLIN | POLLPRI等价于select()的读事件,POLLOUT | POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM | POLLRDBAND,而POLLOUT则等价于POLLWRNORM。例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN | POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。

  • nfds:fds的个数。
  • timeout:参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
  • 返回值:成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
    • EBADF:一个或多个结构体中指定的文件描述符无效
    • EFAULTfds:指针指向的地址超出进程的地址空间
    • EINTR:请求的事件之前产生一个信号,调用可以重新发起
    • EINVALnfds:参数超出PLIMIT_NOFILE值
    • ENOMEM:可用内存不足,无法完成请求

缺点

poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

Example

下面的程序是基于socket的tcp应答程序。

  • 服务端
    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
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    #include <stdio.h>
    #include <stdlib.h>
    #include <poll.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <string.h>
    #include <unistd.h>
    #define BACKLOG 5 //完成三次握手但没有accept的队列的长度
    #define CONCURRENT_MAX 8 //应用层同时可以处理的连接
    #define SERVER_PORT 11332
    #define BUFFER_SIZE 1024
    #define QUIT_CMD ".quit\n"

    int main(int argc, const char * argv[])
    {
    char input_msg[BUFFER_SIZE];
    char recv_msg[BUFFER_SIZE];
    //本地地址
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero), 8);
    //创建socket
    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(server_sock_fd == -1)
    {
    perror("socket error");
    return 1;
    }
    //绑定socket
    int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if(bind_result == -1)
    {
    perror("bind error");
    return 1;
    }
    //listen
    if(listen(server_sock_fd, BACKLOG) == -1)
    {
    perror("listen error");
    return 1;
    }

    //pollfd
    int timeout = 20 * 1000;
    int fds_len = 2 + CONCURRENT_MAX;
    struct pollfd fds[fds_len];
    for(int i = 0;i < fds_len;i++)
    {
    fds[i].fd = -1;
    fds[i].events = POLLIN;
    fds[i].revents = 0;
    }
    fds[0].fd = STDIN_FILENO;
    fds[1].fd = server_sock_fd;

    //do poll
    while(1)
    {
    int ret = poll(fds, fds_len, timeout);
    if(ret < 0)
    {
    perror("poll 出错\n");
    continue;
    }
    else if(ret == 0)
    {
    printf("poll 超时\n");
    continue;
    }
    else
    {
    for(int i = 0;i < fds_len;i++)
    {
    if(fds[i].revents & fds[i].events)
    {
    fds[i].revents = 0;

    if(i == 0)
    {
    printf("发送消息:\n");
    bzero(input_msg, BUFFER_SIZE);
    fgets(input_msg, BUFFER_SIZE, stdin);
    //输入“.quit"则退出服务器
    if(strcmp(input_msg, QUIT_CMD) == 0)
    {
    exit(0);
    }
    for(int client_i = 0;client_i < CONCURRENT_MAX;client_i++)
    {
    if(fds[client_i + 2].fd > 0)
    {
    printf("向客户端(%d)发送消息\n", client_i);
    send(fds[client_i + 2].fd, input_msg, BUFFER_SIZE, 0);
    }
    }
    }
    else if(i == 1)
    {
    //有新的连接请求
    struct sockaddr_in client_address;
    socklen_t address_len;
    int client_sock_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);
    printf("new connection client_sock_fd = %d\n", client_sock_fd);
    if(client_sock_fd > 0)
    {
    int index = -1;
    for(int client_i = 0;client_i < CONCURRENT_MAX;client_i++)
    {
    if(fds[client_i + 2].fd == -1)
    {
    index = client_i;
    fds[client_i + 2].fd = client_sock_fd;
    break;
    }
    }
    if(index >= 0)
    {
    printf("新客户端(%d)加入成功 %s:%d\n", index, inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
    }
    else
    {
    bzero(input_msg, BUFFER_SIZE);
    strcpy(input_msg, "服务器加入的客户端数达到最大值,无法加入!\n");
    send(client_sock_fd, input_msg, BUFFER_SIZE, 0);
    printf("客户端连接数达到最大值,新客户端加入失败 %s:%d\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
    }
    }
    }
    else
    {
    //处理某个客户端过来的消息
    bzero(recv_msg, BUFFER_SIZE);
    long byte_num = recv(fds[i].fd, recv_msg, BUFFER_SIZE, 0);
    if(byte_num > 0)
    {
    if(byte_num > BUFFER_SIZE)
    {
    byte_num = BUFFER_SIZE;
    }
    recv_msg[byte_num] = '\0';
    printf("客户端(%d):%s\n", i - 2, recv_msg);
    }
    else if(byte_num < 0)
    {
    printf("从客户端(%d)接受消息出错.\n", i - 2);
    }
    else
    {
    fds[i].fd = -1;
    fds[i].revents = 0;
    printf("客户端(%d)退出了\n", i - 2);
    }
    }
    }
    }
    }
    }
    return 0;
    }
  • 客户端(仍然采用select)
    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
    #include<stdio.h>
    #include<stdlib.h>
    #include<netinet/in.h>
    #include<sys/socket.h>
    #include<arpa/inet.h>
    #include<string.h>
    #include<unistd.h>
    #define BUFFER_SIZE 1024

    int main(int argc, const char * argv[])
    {
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(11332);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero), 8);

    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(server_sock_fd == -1)
    {
    perror("socket error");
    return 1;
    }
    char recv_msg[BUFFER_SIZE];
    char input_msg[BUFFER_SIZE];

    if(connect(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == 0)
    {
    fd_set client_fd_set;
    struct timeval tv;

    while(1)
    {
    tv.tv_sec = 20;
    tv.tv_usec = 0;
    FD_ZERO(&client_fd_set);
    FD_SET(STDIN_FILENO, &client_fd_set);
    FD_SET(server_sock_fd, &client_fd_set);

    select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv);
    if(FD_ISSET(STDIN_FILENO, &client_fd_set))
    {
    bzero(input_msg, BUFFER_SIZE);
    fgets(input_msg, BUFFER_SIZE, stdin);
    if(send(server_sock_fd, input_msg, BUFFER_SIZE, 0) == -1)
    {
    perror("发送消息出错!\n");
    }
    }
    if(FD_ISSET(server_sock_fd, &client_fd_set))
    {
    bzero(recv_msg, BUFFER_SIZE);
    long byte_num = recv(server_sock_fd, recv_msg, BUFFER_SIZE, 0);
    if(byte_num > 0)
    {
    if(byte_num > BUFFER_SIZE)
    {
    byte_num = BUFFER_SIZE;
    }
    recv_msg[byte_num] = '\0';
    printf("服务器:%s\n", recv_msg);
    }
    else if(byte_num < 0)
    {
    printf("接受消息出错!\n");
    }
    else
    {
    printf("服务器端退出!\n");
    exit(0);
    }
    }
    }
    //}
    }
    return 0;
    }

参考

http://www.cnblogs.com/Anker/archive/2013/08/15/3261006.html

再谈IO多路复用之select

发表于 2019-05-06 | 更新于 2019-05-13

API

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

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
  • maxfdp1:待测试的描述符个数,最大描述符加1。因为描述符是从0开始的。

  • readset:让内核测试读的描述字,如果不敢兴趣则置为空指针。

  • writeset:让内核测试写的描述字,如果不敢兴趣则置为空指针。

  • exceptset:让内核测试异常的描述字,如果不敢兴趣则置为空指针。

  • timeout:告知内核等待所指定描述字中的任何一个就绪可花多少时间。这个参数有以下几种可能性:

    1. 永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
    2. 等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
    3. 根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

    timeval结构体如下:

    1
    2
    3
    4
    struct timeval {
    long tv_sec; //seconds
    long tv_usec; //microseconds
    };

缺点

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024

Example

下面的程序是基于socket的tcp应答程序。

  • 服务端

    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
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    #include<stdio.h>
    #include<stdlib.h>
    #include<netinet/in.h>
    #include<sys/socket.h>
    #include<arpa/inet.h>
    #include<string.h>
    #include<unistd.h>
    #define BACKLOG 5 //完成三次握手但没有accept的队列的长度
    #define CONCURRENT_MAX 8 //应用层同时可以处理的连接
    #define SERVER_PORT 11332
    #define BUFFER_SIZE 1024
    #define QUIT_CMD ".quit\n"
    int client_fds[CONCURRENT_MAX];
    int main(int argc, const char * argv[])
    {
    char input_msg[BUFFER_SIZE];
    char recv_msg[BUFFER_SIZE];
    //本地地址
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero), 8);
    //创建socket
    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(server_sock_fd == -1)
    {
    perror("socket error");
    return 1;
    }
    //绑定socket
    int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if(bind_result == -1)
    {
    perror("bind error");
    return 1;
    }
    //listen
    if(listen(server_sock_fd, BACKLOG) == -1)
    {
    perror("listen error");
    return 1;
    }
    //fd_set
    fd_set server_fd_set;
    int max_fd = -1;
    struct timeval tv; //超时时间设置
    while(1)
    {
    tv.tv_sec = 20;
    tv.tv_usec = 0;
    FD_ZERO(&server_fd_set);
    FD_SET(STDIN_FILENO, &server_fd_set);
    if(max_fd <STDIN_FILENO)
    {
    max_fd = STDIN_FILENO;
    }
    //printf("STDIN_FILENO=%d\n", STDIN_FILENO);
    //服务器端socket
    FD_SET(server_sock_fd, &server_fd_set);
    // printf("server_sock_fd=%d\n", server_sock_fd);
    if(max_fd < server_sock_fd)
    {
    max_fd = server_sock_fd;
    }
    //客户端连接
    for(int i =0; i < CONCURRENT_MAX; i++)
    {
    //printf("client_fds[%d]=%d\n", i, client_fds[i]);
    if(client_fds[i] != 0)
    {
    FD_SET(client_fds[i], &server_fd_set);
    if(max_fd < client_fds[i])
    {
    max_fd = client_fds[i];
    }
    }
    }
    int ret = select(max_fd + 1, &server_fd_set, NULL, NULL, &tv);
    if(ret < 0)
    {
    perror("select 出错\n");
    continue;
    }
    else if(ret == 0)
    {
    printf("select 超时\n");
    continue;
    }
    else
    {
    //ret 为未状态发生变化的文件描述符的个数
    if(FD_ISSET(STDIN_FILENO, &server_fd_set))
    {
    printf("发送消息:\n");
    bzero(input_msg, BUFFER_SIZE);
    fgets(input_msg, BUFFER_SIZE, stdin);
    //输入“.quit"则退出服务器
    if(strcmp(input_msg, QUIT_CMD) == 0)
    {
    exit(0);
    }
    for(int i = 0; i < CONCURRENT_MAX; i++)
    {
    if(client_fds[i] != 0)
    {
    printf("client_fds[%d]=%d\n", i, client_fds[i]);
    send(client_fds[i], input_msg, BUFFER_SIZE, 0);
    }
    }
    }
    if(FD_ISSET(server_sock_fd, &server_fd_set))
    {
    //有新的连接请求
    struct sockaddr_in client_address;
    socklen_t address_len;
    int client_sock_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);
    printf("new connection client_sock_fd = %d\n", client_sock_fd);
    if(client_sock_fd > 0)
    {
    int index = -1;
    for(int i = 0; i < CONCURRENT_MAX; i++)
    {
    if(client_fds[i] == 0)
    {
    index = i;
    client_fds[i] = client_sock_fd;
    break;
    }
    }
    if(index >= 0)
    {
    printf("新客户端(%d)加入成功 %s:%d\n", index, inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
    }
    else
    {
    bzero(input_msg, BUFFER_SIZE);
    strcpy(input_msg, "服务器加入的客户端数达到最大值,无法加入!\n");
    send(client_sock_fd, input_msg, BUFFER_SIZE, 0);
    printf("客户端连接数达到最大值,新客户端加入失败 %s:%d\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
    }
    }
    }
    for(int i =0; i < CONCURRENT_MAX; i++)
    {
    if(client_fds[i] !=0)
    {
    if(FD_ISSET(client_fds[i], &server_fd_set))
    {
    //处理某个客户端过来的消息
    bzero(recv_msg, BUFFER_SIZE);
    long byte_num = recv(client_fds[i], recv_msg, BUFFER_SIZE, 0);
    if (byte_num > 0)
    {
    if(byte_num > BUFFER_SIZE)
    {
    byte_num = BUFFER_SIZE;
    }
    recv_msg[byte_num] = '\0';
    printf("客户端(%d):%s\n", i, recv_msg);
    }
    else if(byte_num < 0)
    {
    printf("从客户端(%d)接受消息出错.\n", i);
    }
    else
    {
    FD_CLR(client_fds[i], &server_fd_set);
    client_fds[i] = 0;
    printf("客户端(%d)退出了\n", i);
    }
    }
    }
    }
    }
    }
    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
    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
    #include<stdio.h>
    #include<stdlib.h>
    #include<netinet/in.h>
    #include<sys/socket.h>
    #include<arpa/inet.h>
    #include<string.h>
    #include<unistd.h>
    #define BUFFER_SIZE 1024

    int main(int argc, const char * argv[])
    {
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(11332);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero), 8);

    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(server_sock_fd == -1)
    {
    perror("socket error");
    return 1;
    }
    char recv_msg[BUFFER_SIZE];
    char input_msg[BUFFER_SIZE];

    if(connect(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == 0)
    {
    fd_set client_fd_set;
    struct timeval tv;

    while(1)
    {
    tv.tv_sec = 20;
    tv.tv_usec = 0;
    FD_ZERO(&client_fd_set);
    FD_SET(STDIN_FILENO, &client_fd_set);
    FD_SET(server_sock_fd, &client_fd_set);

    select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv);
    if(FD_ISSET(STDIN_FILENO, &client_fd_set))
    {
    bzero(input_msg, BUFFER_SIZE);
    fgets(input_msg, BUFFER_SIZE, stdin);
    if(send(server_sock_fd, input_msg, BUFFER_SIZE, 0) == -1)
    {
    perror("发送消息出错!\n");
    }
    }
    if(FD_ISSET(server_sock_fd, &client_fd_set))
    {
    bzero(recv_msg, BUFFER_SIZE);
    long byte_num = recv(server_sock_fd, recv_msg, BUFFER_SIZE, 0);
    if(byte_num > 0)
    {
    if(byte_num > BUFFER_SIZE)
    {
    byte_num = BUFFER_SIZE;
    }
    recv_msg[byte_num] = '\0';
    printf("服务器:%s\n", recv_msg);
    }
    else if(byte_num < 0)
    {
    printf("接受消息出错!\n");
    }
    else
    {
    printf("服务器端退出!\n");
    exit(0);
    }
    }
    }
    //}
    }
    return 0;
    }

参考

http://www.cnblogs.com/Anker/p/3265058.html

同步IO、异步IO、阻塞IO、非阻塞IO

发表于 2019-05-05 | 更新于 2019-05-06

概念剖析

  • 同步
    所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件事一件事地做,等前一件做完了才能做下一件事。
    例如B/S模式(同步):提交请求->等待服务器处理->处理完毕返回,这个期间客户端浏览器不能干任何事。
  • 异步
    异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
    例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕。
  • 阻塞
    阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
    有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回,它还会抢占cpu去执行其他逻辑,也会主动检测io是否准备好。
  • 非阻塞
    非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

IO模型

  • 阻塞I/O(blocking I/O)
    使用recv的默认参数一直等数据直到拷贝到用户空间,这段时间内进程始终阻塞。
    阻塞I/O
  • 非阻塞I/O(nonblocking I/O)
    改变flags,让recv不管有没有获取到数据都返回,如果没有数据那么一段时间后再调用recv看看,如此循环。但是它只有检查有无数据的时候是非阻塞的,在数据到达的时候依然要等待复制数据到用户空间(等着水将水杯装满),因此它还是同步IO。
    非阻塞I/O
  • IO复用(select,poll和epoll)
    这里在调用recv前先调用select或者poll,这2个系统调用都可以在内核准备好数据(网络数据到达内核)时告知用户进程,这个时候再调用recv一定是有数据的。因此这一过程中它是阻塞于select或poll,而没有阻塞于recv,有人将非阻塞IO定义成在读写操作时没有阻塞于系统调用的IO操作(不包括数据从内核复制到用户空间时的阻塞,因为这相对于网络IO来说确实很短暂),如果按这样理解,这种IO模型也能称之为非阻塞IO模型,但是按POSIX来看,它也是同步IO。
    IO复用
  • 信号驱动IO
    通过调用sigaction注册信号函数,等内核数据准备好的时候系统中断当前程序,执行信号函数(在这里面调用recv)。
    信号驱动IO
  • 异步IO
    调用aio_read,让内核等数据准备好,并且复制到用户进程空间后执行事先指定好的函数。
    异步IO

总结

  1. 数据准备阶段
  2. 内核空间复制回用户空间
    阻塞IO模型、非阻塞IO模型、IO复用模型(select/poll/epoll)、信号驱动IO模型都属于同步IO,因为阶段2是阻塞的(尽管时间很短)。只有异步IO模型是真真正正的异步IO,因为不管在阶段1还是阶段2都可以干别的事。

参考

https://www.cnblogs.com/chaser24/p/6112071.html
https://www.cnblogs.com/euphie/p/6376508.html

linux的用户态和内核态

发表于 2019-05-05 | 更新于 2019-05-06

Linux体系架构

linux架构
从宏观上来看,Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核空间)。内核从本质上看是一种软件(控制计算机的硬件资源,并提供上层应用程序运行的环境)。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

系统调用是操作系统的最小功能单位,这些系统调用根据不同的应用场景可以进行扩展和裁剪,现在各种版本的Unix实现都提供了不同数量的系统调用,如Linux的不同版本提供了240-260个系统调用,FreeBSD大约提供了320个(reference:UNIX环境高级编程)。我们可以把系统调用看成是一种不能再化简的操作(类似于原子操作,但是不同概念),有人把它比作一个汉字的一个“笔画”,而一个“汉字”就代表一个上层应用,我觉得这个比喻非常贴切。因此,有时候如果要实现一个完整的汉字(给某个变量分配内存空间),就必须调用很多的系统调用。如果从实现者(程序员)的角度来看,这势必会加重程序员的负担,良好的程序设计方法是:重视上层的业务逻辑操作,而尽可能避免底层复杂的实现细节。库函数正是为了将程序员从复杂的细节中解脱出来而提出的一种有效方法。它实现对系统调用的封装,将简单的业务逻辑接口呈现给用户,方便用户调用,从这个角度上看,库函数就像是组成汉字的“偏旁”。这样的一种组成方式极大增强了程序设计的灵活性,对于简单的操作,我们可以直接调用系统调用来访问资源,如“人”,对于复杂操作,我们借助于库函数来实现,如“仁”。显然,这样的库函数依据不同的标准也可以有不同的实现版本,如ISO C 标准库,POSIX标准库等。

Shell是一个特殊的应用程序,俗称命令行,本质上是一个命令解释器,它下通系统调用,上通各种应用,通常充当着一种“胶水”的角色,来连接各个小功能程序,让不同程序能够以一个清晰的接口协同工作,从而增强各个程序的功能。同时,Shell是可编程的,它可以执行符合Shell语法的文本,这样的文本称为Shell脚本,通常短短的几行Shell脚本就可以实现一个非常大的功能,原因就是这些Shell语句通常都对系统调用做了一层封装。为了方便用户和系统交互,一般,一个Shell对应一个终端,终端是一个硬件设备,呈现给用户的是一个图形化窗口。我们可以通过这个窗口输入或者输出文本。这个文本直接传递给shell进行分析解释,然后执行。

下图是对上图的一个细分结构,从这个图上可以更进一步对内核所做的事有一个“全景式”的印象。主要表现为:向下控制硬件资源,向内管理操作系统资源:包括进程的调度和管理、内存的管理、文件系统的管理、设备驱动程序的管理以及网络资源的管理,向上则向应用程序提供系统调用的接口。从整体上来看,整个操作系统分为两层:用户态和内核态,这种分层的架构极大地提高了资源管理的可扩展性和灵活性,而且方便用户对资源的调用和集中式的管理,带来一定的安全性。

linux架构

用户态和内核态的切换

因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。所以,为了减少有限资源的访问和使用冲突,Unix/Linux的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念。简单说就是有多大能力做多大的事,与系统相关的一些特别关键的操作必须由最高特权的程序来完成。Intel的X86架构的CPU提供了0到3四个特权级,数字越小,特权越高,Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态和用户态。运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如C函数库中的内存分配函数malloc(),它具体是使用sbrk()系统调用来分配内存,当malloc调用sbrk()的时候就涉及一次从用户态到内核态的切换,类似的函数还有printf(),调用的是wirte()系统调用来输出字符串,等等。

linux程序运行过程

用户态的应用程序可以通过以下三种方式来访问内核态的资源:

  • 系统调用
  • 库函数
  • Shell脚本

到底在什么情况下会发生从用户态到内核态的切换,一般存在以下三种情况:

  • 系统调用,原因如上分析

  • 异常事件: 当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。

  • 外围设备的中断:当外围设备完成用户的请求操作后,会像CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。

系统调用的本质其实也是中断,相对于外围设备的硬中断,这种中断称为软中断,这是操作系统为用户特别开放的一种中断,如Linux int 80h中断。所以,从触发方式和效果上来看,这三种切换方式是完全一样的,都相当于是执行了一个中断响应的过程。但是从触发的对象来看,系统调用是进程主动请求切换的,而异常和硬中断则是被动的。

总结

本文仅是从宏观的角度去理解Linux用户态和内核态的设计,并没有去深究它们的具体实现方式。从实现上来看,必须要考虑到的一点我想就是性能问题,因为用户态和内核态之间的切换也会消耗大量资源。

参考

https://www.cnblogs.com/bakari/p/5520860.html

更换centos的yum源

发表于 2019-05-01 | 更新于 2019-05-06

网上给出的将centos的yum源替换为阿里云源,目前不能用了,所以贴出下面源配置

centos5

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
# CentOS-Base.repo
#
# The mirror system uses the connecting IP address of the client and the
# update status of each mirror to pick mirrors that are updated to and
# geographically close to the client. You should use this for CentOS updates
# unless you are manually picking other mirrors.
#
# If the mirrorlist= does not work for you, as a fall back you can try the
# remarked out baseurl= line instead.
#
#

[base]
name=CentOS-$releasever - Base - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/os/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os
gpgcheck=1
gpgkey=https://mirrors.ustc.edu.cn/centos/RPM-GPG-KEY-CentOS-5

#released updates
[updates]
name=CentOS-$releasever - Updates - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/updates/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates
gpgcheck=
gpgkey=https://mirrors.ustc.edu.cn/centos/RPM-GPG-KEY-CentOS-5

#additional packages that may be useful
[extras]
name=CentOS-$releasever - Extras - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/extras/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras
gpgcheck=1
gpgkey=https://mirrors.ustc.edu.cn/centos/RPM-GPG-KEY-CentOS-5

#packages used/produced in the build but not released
[addons]
name=CentOS-$releasever - Addons - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/addons/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=addons
gpgcheck=1
gpgkey=https://mirror.centos.org/centos/RPM-GPG-KEY-CentOS-5

#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-$releasever - Plus - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/centosplus/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=centosplus
gpgcheck=1
enabled=0
gpgkey=https://mirrors.ustc.edu.cn/centos/RPM-GPG-KEY-CentOS-5

#contrib - packages by Centos Users
[contrib]
name=CentOS-$releasever - Contrib - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/contrib/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=contrib
gpgcheck=1
enabled=0
gpgkey=https://mirrors.ustc.edu.cn/centos/RPM-GPG-KEY-CentOS-5

centos6

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
# CentOS-Base.repo
#
# The mirror system uses the connecting IP address of the client and the
# update status of each mirror to pick mirrors that are updated to and
# geographically close to the client. You should use this for CentOS updates
# unless you are manually picking other mirrors.
#
# If the mirrorlist= does not work for you, as a fall back you can try the
# remarked out baseurl= line instead.
#
#

[base]
name=CentOS-$releasever - Base - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/os/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os
gpgcheck=1
gpgkey=https://mirrors.ustc.edu.cn/centos/RPM-GPG-KEY-CentOS-6

#released updates
[updates]
name=CentOS-$releasever - Updates - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/updates/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates
gpgcheck=1
gpgkey=https://mirrors.ustc.edu.cn/centos/RPM-GPG-KEY-CentOS-6

#additional packages that may be useful
[extras]
name=CentOS-$releasever - Extras - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/extras/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras
gpgcheck=1
gpgkey=https://mirrors.ustc.edu.cn/centos/RPM-GPG-KEY-CentOS-6

#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-$releasever - Plus - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/centosplus/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=centosplus
gpgcheck=1
enabled=0
gpgkey=https://mirrors.ustc.edu.cn/centos/RPM-GPG-KEY-CentOS-6

#contrib - packages by Centos Users
[contrib]
name=CentOS-$releasever - Contrib - mirrors.ustc.edu.cn
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/contrib/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=contrib
gpgcheck=1
enabled=0
gpgkey=https://mirrors.ustc.edu.cn/centos/RPM-GPG-KEY-CentOS-6

centos7

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
# CentOS-Base.repo
#
# The mirror system uses the connecting IP address of the client and the
# update status of each mirror to pick mirrors that are updated to and
# geographically close to the client. You should use this for CentOS updates
# unless you are manually picking other mirrors.
#
# If the mirrorlist= does not work for you, as a fall back you can try the
# remarked out baseurl= line instead.
#
#

[base]
name=CentOS-$releasever - Base
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#released updates
[updates]
name=CentOS-$releasever - Updates
# mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/updates/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#additional packages that may be useful
[extras]
name=CentOS-$releasever - Extras
# mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/extras/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-$releasever - Plus
# mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=centosplus
baseurl=https://mirrors.ustc.edu.cn/centos/$releasever/centosplus/$basearch/
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

修改系统时间

发表于 2018-10-24 | 更新于 2021-03-18
  1. 本地安装

    1
    $ yum localinstall vsftpd-3.0.2-28.el7.x86_64.rpm
  2. 关闭SELINUX

    1
    $ setenforce 0
1
2
#修改为SELINUX=disabled
$ vi /etc/sysconfig/selinux
  1. 修改vsftpd配置文件

    1
    2
    3
    4
    #添加pasv_min_port=4000,被动模式最小端口
    #添加pasv_max_port=4100,被动模式最大端口
    #将chroot_local_user=YES的注释去掉,将本地用户限制到自己的主目录
    $ vi /etc/vsftpd/vsftpd.conf
  2. 添加用户

    1
    2
    3
    $ useradd ftp_cmacast
    #密码设置为cmacast112
    $ passwd ftp_cmacast
  3. 将该用户设置为不可登录

    1
    $ usermod -s /sbin/nologin ftp_cmacast
  4. 创建目录

    1
    2
    $ mkdir -p /mnt/data
    $ chmod 777 -R /mnt
  5. 设置用户的主目录

    1
    $ usermod -d /mnt/data ftp_cmacast
  6. 设置防火墙

    1
    2
    3
    4
    $ systemctl restart firewalld
    $ firewall-cmd --permanent --add-port=20-21/tcp
    $ firewall-cmd --permanent --add-port=4000-4100/tcp
    $ firewall-cmd --reload
  7. 启动ftp

    1
    $ systemctl restart vsftpd
12…5
呐喊

呐喊

真的勇士敢于直面惨淡的人生
45 日志
© 2021 呐喊
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Gemini v7.1.1
|