Linux网络编程

字节序

大端小端

  • byteorder.c
    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
    /*  
    字节序:字节在内存中存储的顺序。
    小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
    大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址
    */

    // 通过代码检测当前主机的字节序
    #include <stdio.h>

    int main() {

    union {
    short value; // 2字节
    char bytes[sizeof(short)]; // char[2]
    } test;

    test.value = 0x0102;
    if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
    printf("大端字节序\n");
    } else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
    printf("小端字节序\n");
    } else {
    printf("未知\n");
    }

    return 0;
    }
    /*
    小端字节序
    */

字节序转换函数

主机字节序(小端/大端)<=> 网络字节序(大端)

  • bytetrans.c
    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
    /*
    #include <arpa/inet.h>

    网络通信时,需要将主机字节序转换成网络字节序(大端),
    另外一段获取到数据以后根据情况将网络字节序转换成主机字节序。

    // 转换端口
    uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
    uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序

    // 转IP
    uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
    uint32_t ntohl(uint32_t netlong); // 网络字节序 - 主机字节序
    */
    #include <stdio.h>
    #include <arpa/inet.h>

    int main() {

    // htons,转换端口
    unsigned short a = 0x0102;
    printf("a : %x\n", a);
    unsigned short b = htons(a);
    printf("b : %x\n", b);

    printf("=======================\n");

    // ntohs,转换端口
    unsigned short a1 = 0x0102;
    printf("a1 : %x\n", a1);
    unsigned short b1 = htons(a1);
    printf("b1 : %x\n", b1);
    a1 = ntohs(b1);
    printf("a : %x\n", a1);

    printf("=======================\n");

    // htonl,转换IP
    char buf[4] = {192, 168, 1, 100};
    int num = *(int *)buf;
    int sum = htonl(num);
    unsigned char *p = (char *)&sum;
    printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));

    printf("=======================\n");

    // ntohl,转换IP
    unsigned char buf1[4] = {1, 1, 168, 192};
    int num1 = *(int *)buf1;
    int sum1 = ntohl(num1);
    unsigned char *p1 = (unsigned char *)&sum1;
    printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));

    return 0;
    }
    /*
    a : 102
    b : 201
    =======================
    a1 : 102
    b1 : 201
    a : 102
    =======================
    100 1 168 192
    =======================
    192 168 1 1
    */

IP地址转换

点分十进制字符串(表示的 IPv4 地址) <=> 网络字节序整数(表示的 IPv4 地址)

  • iptrans.c
    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
    /*
    #include <arpa/inet.h>

    // p:点分十进制的IP字符串,n:表示network,网络字节序的整数
    int inet_pton(int af, const char *src, void *dst);
    af:地址族: AF_INET, AF_INET6
    src:需要转换的点分十进制的IP字符串
    dst:转换后的结果保存在这个里面

    // 将网络字节序的整数,转换成点分十进制的IP地址字符串
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    af:地址族: AF_INET, AF_INET6
    src: 要转换的ip的整数的地址
    dst: 转换成IP地址字符串保存的地方
    size:第三个参数的大小(数组的大小)
    返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
    */
    #include <stdio.h>
    #include <arpa/inet.h>

    int main() {

    // 创建一个ip字符串:点分十进制的IP地址字符串
    char buf[] = "192.168.1.4";
    unsigned int num = 0;

    // 将点分十进制的IP字符串转换成网络字节序的整数
    inet_pton(AF_INET, buf, &num);
    unsigned char * p = (unsigned char *)&num;
    printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));

    // 将网络字节序的IP整数转换成点分十进制的IP字符串
    char ip[16] = "";
    const char * str = inet_ntop(AF_INET, &num, ip, 16);
    printf("str : %s\n", str);
    printf("ip : %s\n", str);
    printf("%d\n", ip == str);

    return 0;
    }
    /*
    192 168 1 4
    str : 192.168.1.4
    ip : 192.168.1.4
    1
    */

socket

专用 socket 地址

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

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
#include <netinet/in.h>

struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)];
};

struct in_addr
{
in_addr_t s_addr;
};

struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};

typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

套接字函数

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
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
/*
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1
*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名
/*
- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
*/

int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
/*
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 5
*/

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符
- -1 : 失败
*/

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1
*/

ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据

TCP通信

TCP通信

  • server.c
    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
    // TCP 通信的服务器端
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>

    int main() {

    // 1.创建socket(用于监听的套接字)
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    if(lfd == -1) {
    perror("socket");
    exit(-1);
    }

    // 2.绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
    saddr.sin_port = htons(9999);
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    if(ret == -1) {
    perror("bind");
    exit(-1);
    }

    // 3.监听
    ret = listen(lfd, 8);
    if(ret == -1) {
    perror("listen");
    exit(-1);
    }

    // 4.接收客户端连接
    struct sockaddr_in clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);

    if(cfd == -1) {
    perror("accept");
    exit(-1);
    }

    // 输出客户端的信息
    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\n", clientIP, clientPort);

    // 5.通信
    char recvBuf[1024] = {0};
    while(1) {
    // 获取客户端的数据
    int num = read(cfd, recvBuf, sizeof(recvBuf));
    if(num == -1) {
    perror("read");
    exit(-1);
    } else if(num > 0) {
    printf("recv client data : %s\n", recvBuf);
    } else if(num == 0) {
    // 表示客户端断开连接
    printf("client closed...");
    break;
    }

    char * data = "hello,i am server";
    // 给客户端发送数据
    write(cfd, data, strlen(data));
    }

    // 关闭文件描述符
    close(cfd);
    close(lfd);

    return 0;
    }
  • client.c
    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
    // TCP通信的客户端

    #include <stdio.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>

    int main() {
    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    perror("socket");
    exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.3.194", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
    perror("connect");
    exit(-1);
    }

    // 3. 通信
    char recvBuf[1024] = {0};
    while(1) {
    char * data = "hello,i am client";
    // 给服务端发送数据
    write(fd, data , strlen(data));

    int len = read(fd, recvBuf, sizeof(recvBuf));
    if(len == -1) {
    perror("read");
    exit(-1);
    } else if(len > 0) {
    printf("recv server data : %s\n", recvBuf);
    } else if(len == 0) {
    // 表示服务器端断开连接
    printf("server closed...");
    break;
    }
    sleep(1);
    }

    // 关闭连接
    close(fd);

    return 0;
    }

TCP通信并发

要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。

进程

(1) 一个父进程,多个子进程。(2)父进程负责等待并接受客户端的连接。(3)子进程:完成通信,接受一个客户端连接,就创建一个子进程用于通信。

  • client.c
    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
    // TCP通信的客户端
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>

    int main() {

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    perror("socket");
    exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.3.194", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
    perror("connect");
    exit(-1);
    }

    // 3. 通信
    char recvBuf[1024];
    int i = 0;
    while(1) {
    sprintf(recvBuf, "data : %d\n", i++);

    // 给服务器端发送数据
    write(fd, recvBuf, strlen(recvBuf)+1);

    int len = read(fd, recvBuf, sizeof(recvBuf));
    if(len == -1) {
    perror("read");
    exit(-1);
    } else if(len > 0) {
    printf("recv server : %s\n", recvBuf);
    } else if(len == 0) {
    // 表示服务器端断开连接
    printf("server closed...");
    break;
    }

    sleep(1);
    }

    // 关闭连接
    close(fd);

    return 0;
    }
  • server_process.c
    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
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <signal.h>
    #include <wait.h>
    #include <errno.h>

    void recyleChild(int arg) {
    while(1) {
    int ret = waitpid(-1, NULL, WNOHANG);
    if(ret == -1) {
    // 所有的子进程都回收了
    break;
    }else if(ret == 0) {
    // 还有子进程活着
    break;
    } else if(ret > 0){
    // 被回收了
    printf("子进程 %d 被回收了\n", ret);
    }
    }
    }

    int main() {

    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recyleChild;
    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);


    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
    perror("socket");
    exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
    perror("bind");
    exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
    perror("listen");
    exit(-1);
    }

    // 不断循环等待客户端连接
    while(1) {
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    // 接受连接
    int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
    if(cfd == -1) {
    if(errno == EINTR) {
    continue;
    }
    perror("accept");
    exit(-1);
    }

    // 每一个连接进来,创建一个子进程跟客户端通信
    pid_t pid = fork();
    if(pid == 0) {
    // 子进程
    // 获取客户端的信息
    char cliIp[16];
    inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(cliaddr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1) {
    int len = read(cfd, &recvBuf, sizeof(recvBuf));

    if(len == -1) {
    perror("read");
    exit(-1);
    }else if(len > 0) {
    printf("recv client : %s\n", recvBuf);
    } else if(len == 0) {
    printf("client closed....\n");
    break;
    }
    write(cfd, recvBuf, strlen(recvBuf) + 1);
    }
    close(cfd);
    exit(0); // 退出当前子进程
    }
    }
    close(lfd);

    return 0;
    }

线程

(1) 一个父进程,多个子线程(2)父进程负责等待并接受客户端的连接(3)子线程:完成通信,接受一个客户端连接,就创建一个子线程用于通信。

  • client.c
    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
    // TCP通信的客户端
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>

    int main() {

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    perror("socket");
    exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.3.194", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
    perror("connect");
    exit(-1);
    }

    // 3. 通信
    char recvBuf[1024];
    int i = 0;
    while(1) {
    sprintf(recvBuf, "data : %d\n", i++);

    // 给服务器端发送数据
    write(fd, recvBuf, strlen(recvBuf)+1);

    int len = read(fd, recvBuf, sizeof(recvBuf));
    if(len == -1) {
    perror("read");
    exit(-1);
    } else if(len > 0) {
    printf("recv server : %s\n", recvBuf);
    } else if(len == 0) {
    // 表示服务器端断开连接
    printf("server closed...");
    break;
    }

    sleep(1);
    }

    // 关闭连接
    close(fd);

    return 0;
    }
  • server_thread.c
    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
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <pthread.h>

    struct sockInfo {
    int fd; // 通信的文件描述符
    struct sockaddr_in addr;
    pthread_t tid; // 线程号
    };

    struct sockInfo sockinfos[128];

    void * working(void * arg) {
    // 子线程和客户端通信:cfd, 客户端的信息, 线程号
    // 获取客户端的信息
    struct sockInfo * pinfo = (struct sockInfo *)arg;

    char cliIp[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1) {
    int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

    if(len == -1) {
    perror("read");
    exit(-1);
    }else if(len > 0) {
    printf("recv client : %s\n", recvBuf);
    } else if(len == 0) {
    printf("client closed....\n");
    break;
    }
    write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
    }
    close(pinfo->fd);
    return NULL;
    }

    int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
    perror("socket");
    exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
    perror("bind");
    exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
    perror("listen");
    exit(-1);
    }

    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
    bzero(&sockinfos[i], sizeof(sockinfos[i]));
    sockinfos[i].fd = -1;
    sockinfos[i].tid = -1;
    }

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1) {
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    // 接受连接
    int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

    struct sockInfo * pinfo;
    for(int i = 0; i < max; i++) {
    // 从这个数组中找到一个可以用的sockInfo元素
    if(sockinfos[i].fd == -1) {
    pinfo = &sockinfos[i];
    break;
    }
    if(i == max - 1) {
    sleep(1);
    i--;
    }
    }

    pinfo->fd = cfd;
    memcpy(&pinfo->addr, &cliaddr, len);

    // 创建子线程
    pthread_create(&pinfo->tid, NULL, working, pinfo);

    pthread_detach(pinfo->tid);
    }

    close(lfd);
    return 0;
    }

端口复用

作用

  • 防止服务器重启时之前绑定的端口还未释放
  • 程序突然退出而系统没有释放端口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <sys/types.h>
    #include <sys/socket.h>
    // 设置套接字的属性(不仅仅能设置端口复用)
    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    /*
    参数:
    - sockfd : 要操作的文件描述符
    - level : 级别 - SOL_SOCKET (端口复用的级别)
    - optname : 选项的名称
    - SO_REUSEADDR
    - SO_REUSEPORT
    - optval : 端口复用的值(整形)
    - 1 : 可以复用
    - 0 : 不可以复用
    - optlen : optval参数的大小
    */
    // 端口复用,设置的时机是在服务器绑定端口之前。
    setsockopt();
    // 示例中的用法
    int optval = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
    bind();

示例

  • tcp_client.c
    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
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>

    int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    perror("socket");
    return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
    perror("connect");
    return -1;
    }

    while(1) {
    char sendBuf[1024] = {0};
    fgets(sendBuf, sizeof(sendBuf), stdin);

    write(fd, sendBuf, strlen(sendBuf) + 1);

    // 接收
    int len = read(fd, sendBuf, sizeof(sendBuf));
    if(len == -1) {
    perror("read");
    return -1;
    }else if(len > 0) {
    printf("read buf = %s\n", sendBuf);
    } else {
    printf("服务器已经断开连接...\n");
    break;
    }
    }

    close(fd);

    return 0;
    }
  • tcp_server.c
    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
    #include <stdio.h>
    #include <ctype.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>

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

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);

    if(lfd == -1) {
    perror("socket");
    return -1;
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);

    int optval = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

    // 绑定
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
    perror("bind");
    return -1;
    }

    // 监听
    ret = listen(lfd, 8);
    if(ret == -1) {
    perror("listen");
    return -1;
    }

    // 接收客户端连接
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    if(cfd == -1) {
    perror("accpet");
    return -1;
    }

    // 获取客户端信息
    char cliIp[16];
    inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(cliaddr.sin_port);

    // 输出客户端的信息
    printf("client's ip is %s, and port is %d\n", cliIp, cliPort );

    // 接收客户端发来的数据
    char recvBuf[1024] = {0};
    while(1) {
    int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);
    if(len == -1) {
    perror("recv");
    return -1;
    } else if(len == 0) {
    printf("客户端已经断开连接...\n");
    break;
    } else if(len > 0) {
    printf("read buf = %s\n", recvBuf);
    }

    // 小写转大写
    for(int i = 0; i < len; ++i) {
    recvBuf[i] = toupper(recvBuf[i]);
    }

    printf("after buf = %s\n", recvBuf);

    // 大写字符串发给客户端
    ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);
    if(ret == -1) {
    perror("send");
    return -1;
    }
    }

    close(cfd);
    close(lfd);

    return 0;
    }

常看网络相关信息的命令

1
2
3
4
5
6
7
netstat
参数:
-a 所有的socket
-p 显示正在使用socket的程序的名称
-n 直接使用IP地址,而不通过域名服务器

netstat -anp | greap 9999

IO多路复用的背景

阻塞等待

优点:不占用CPU宝贵的时间片。
缺点:同一时刻只能处理一个操作,效率低。

非阻塞,忙轮询

优点:提高了程序的执行效率。
缺点:需要占用更多的cpu和系统资源。

多线程或多进程,实现并发的缺点

当进程或线程的数量非常多时(1W+),系统调用多了,耗费了过多的系统资源。

BIO

BIO: Blocked IO

根本问题:blocking
(1)系统调用多了,线程或者进程也会消耗资源
(2)CPU调度资源浪费

NIO

NIO: Non-blocking IO

存在问题:
1W Client,每循环内 O(n) 系统调用,消耗的系统资源很多。

IO多路复用技术

I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的系统调用主要有 select、poll 和 epoll。

select

介绍

1
2
3
4
5
6
思想:
1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
2. 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回。
a.这个函数是阻塞
b.函数对文件描述符的检测的操作是由内核完成的
3. 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
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
// sizeof(fd_set) = 128 1024
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/*
参数:
- nfds: 委托内核检测的最大文件描述符的值 + 1
- readfds: 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
- 一般检测读操作
- 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
- 是一个传入传出参数
- writefds: 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
- 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
- exceptfds: 检测发生异常的文件描述符的集合
- timeout: 设置的超时时间
struct timeval {
long tv_sec; // seconds
long tv_usec; // microseconds
};
- NULL: 永久阻塞,直到检测到了文件描述符有变化
- tv_sec = 0 tv_usec = 0, 不阻塞
- tv_sec > 0 tv_usec > 0, 阻塞对应的时间
返回值:
-1: 异常
=0: 没有文件描述符发生变化
>0: 成功,表示发生变化的文件描述符的个数 n
*/
// 从set集合中清除fd
void FD_CLR(int fd, fd_set *set);
// 判断fd是否在set集合中
int FD_ISSET(int fd, fd_set *set);
// 将fd添加到set集合中
void FD_SET(int fd, fd_set *set);
// 清空文件描述符集合
void FD_ZERO(fd_set *set);

示例

  • client.c
    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
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>

    int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);

    if(fd == -1) {
    perror("socket");
    return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
    perror("connect");
    return -1;
    }

    int num = 0;
    while(1) {
    char sendBuf[1024] = {0};
    sprintf(sendBuf, "send data %d", num++);
    write(fd, sendBuf, strlen(sendBuf) + 1);

    // 接收
    int len = read(fd, sendBuf, sizeof(sendBuf));

    if(len == -1) {
    perror("read");
    return -1;
    } else if(len > 0) {
    printf("read buf = %s\n", sendBuf);
    } else {
    printf("服务器已经断开连接...\n");
    break;
    }
    // sleep(1);
    usleep(1000);
    }

    close(fd);

    return 0;
    }
  • select.c
    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
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/select.h>

    int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 创建一个fd_set的集合,存放的是需要检测的文件描述符
    fd_set rdset, tmp;
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;

    while(1) {
    tmp = rdset;

    // 调用select系统函数,让内核检测哪些文件描述符有数据
    int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);

    if(ret == -1) {
    perror("select");
    exit(-1);
    } else if(ret == 0) {
    continue;
    } else if(ret > 0) {
    // 检测到了监听的文件描述符的对应的缓冲区的数据发生了改变
    if(FD_ISSET(lfd, &tmp)) {
    // 表示有新的客户端连接
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    // 与客户端通信的文件描述符
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

    // 将新的文件描述符加入到集合中
    FD_SET(cfd, &rdset);

    // 更新最大的文件描述符
    maxfd = maxfd > cfd ? maxfd : cfd;
    }

    // 与客户端通信的文件描述符
    for(int i = lfd + 1; i <= maxfd; i++) {
    if(FD_ISSET(i, &tmp)) {
    // 说明这个文件描述符对应的客户端发来了数据
    char buf[1024] = {0};
    int len = read(i, buf, sizeof(buf));
    if(len == -1) {
    perror("read");
    exit(-1);
    } else if(len == 0) {
    printf("client closed...\n");
    close(i);
    FD_CLR(i, &rdset);
    } else if(len > 0) {
    printf("read buf = %s\n", buf);
    write(i, buf, strlen(buf) + 1);
    }
    }
    }
    }
    }
    close(lfd);

    return 0;
    }

缺点

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

poll

介绍

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
#include <poll.h>

struct pollfd {
int fd; // 委托内核监控的文件描述符
short events; // 输入参数:告诉内核监控的事件(读事件、写事件、异常事件)
short revents; // 输出参数:内核告诉应用程序哪些文件描述符发生事件
};

struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/*
- 参数:
- fds: 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
- nfds: 第一个参数数组中最后一个有效元素的下标 + 1
- timeout: 阻塞时长
-1: 一直阻塞,直到有事件发生
=0: 不阻塞,立即返回
>0: 阻塞时长
若在时长范围内有事件发生,则立即返回;
若超过时长,也立即返回
- 返回值:
-1: 异常
=0: 没有文件描述符发生变化
>0: 成功,表示发生变化的文件描述符的个数 n
*/

示例

  • client.c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>

    int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);

    if(fd == -1) {
    perror("socket");
    return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1) {
    perror("connect");
    return -1;
    }

    int num = 0;
    while(1) {
    char sendBuf[1024] = {0};
    sprintf(sendBuf, "send data %d", num++);
    write(fd, sendBuf, strlen(sendBuf) + 1);

    // 接收
    int len = read(fd, sendBuf, sizeof(sendBuf));

    if(len == -1) {
    perror("read");
    return -1;
    } else if(len > 0) {
    printf("read buf = %s\n", sendBuf);
    } else {
    printf("服务器已经断开连接...\n");
    break;
    }
    // sleep(1);
    usleep(1000);
    }
    close(fd);

    return 0;
    }
  • poll.c
    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
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <poll.h>

    int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; i++) {
    fds[i].fd = -1;
    fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;
    int nfds = 0;

    while(1) {
    // 调用poll系统函数,让内核帮检测哪些文件描述符有数据
    int ret = poll(fds, nfds + 1, -1);

    if(ret == -1) {
    perror("poll");
    exit(-1);
    } else if(ret == 0) {
    continue;
    } else if(ret > 0) {
    // 检测到了监听的文件描述符的对应的缓冲区的数据发生了改变
    if(fds[0].revents & POLLIN) {
    // 表示有新的客户端连接
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

    // 将新的文件描述符(连接的文件描述符fd)加入到集合中
    for(int i = 1; i < 1024; i++) {
    if(fds[i].fd == -1) {
    fds[i].fd = cfd;
    fds[i].events = POLLIN;
    break;
    }
    }

    // 更新最大的文件描述符的索引
    nfds = nfds > cfd ? nfds : cfd;
    }

    // 与客户端通信的文件描述符
    for(int i = 1; i <= nfds; i++) {
    if(fds[i].revents & POLLIN) {
    // 说明这个文件描述符对应的客户端发来了数据
    char buf[1024] = {0};
    int len = read(fds[i].fd, buf, sizeof(buf));
    if(len == -1) {
    perror("read");
    exit(-1);
    } else if(len == 0) {
    printf("client closed...\n");
    close(fds[i].fd);
    fds[i].fd = -1;
    } else if(len > 0) {
    printf("read buf = %s\n", buf);
    write(fds[i].fd, buf, strlen(buf) + 1);
    }
    }
    }
    }
    }
    close(lfd);

    return 0;
    }

改进

没有解决select中(1)、(2)

  • (1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
  • (2)同时,每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。

解决了select中(3)、(4)

  • (3)select支持的文件描述符数量太小了,默认是1024。
  • (4)fds集合不能重用,每次都需要重置。

epoll

介绍

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
#include <sys/epoll.h>
struct eventpoll {
// ....
struct rb_root rbr;
struct list_head rdlist;
// ....
};

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
/*
常见的Epoll检测事件:
- EPOLLIN
- EPOLLOUT
- EPOLLERR
- EPOLLET
*/

/*
创建一个新的epoll实例,在内核中创建了一个数据。
一个是需要检测的文件描述符的信息(红黑树),另一个是就绪列表,
存放检测到数据发送改变的文件描述符信息(双向链表)
*/
int epoll_create(int size);
/*
- 参数:
size: Linux 2.6.8 之后,该参数被忽略,要求一个大于 0 的数
- 返回值:
-1: 异常
>0: 一个文件描述符,表示 epoll 树的树根,用于操作 epoll 实例
*/

// 对epoll实例进行管理:添加/修改/删除文件描述符信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
- 参数:
- epfd: epoll实例对应的文件描述符
- op: 操作类型
EPOLL_CTL_ADD: 添加
EPOLL_CTL_MOD: 修改
EPOLL_CTL_DEL: 删除
- fd: 要操作的文件描述符
- event: 委托内核监控文件描述符的事件
EPOLLIN:可读事件
EPOLLOUT:可写事件
EPOLLERR:异常事件
*/

// 委托内核监控事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/*
- 参数:
- epfd: epoll实例对应的文件描述符
- events: 传出参数,保存了变化的文件描述符的信息
- maxevents: 第二个参数结构体数组的大小
- timeout: 阻塞时间
-1: 阻塞,直到检测到fd数据发生变化,解除阻塞
=0: 不阻塞
>0: 阻塞的时长(毫秒)
- 返回值:
-1: 异常
>0: 成功,返回变化的文件描述符的个数 n
*/

工作模式

Epoll 的工作模式

  • LT 模式 (水平触发)
    1
    2
    3
    4
    5
    6
    7
    假设委托内核检测读事件 -> 检测fd的读缓冲区
    读缓冲区有数据 - > epoll检测到了会给用户通知
    a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
    b.用户只读了一部分数据,epoll会通知
    c.缓冲区的数据读完了,不通知

    LT(level-triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知。
  • ET 模式(边沿触发)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    假设委托内核检测读事件 -> 检测fd的读缓冲区
    读缓冲区有数据 - > epoll检测到了会给用户通知
    a.用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
    b.用户只读了一部分数据,epoll不通知
    c.缓冲区的数据读完了,不通知

    ET(edge-triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。

    ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

改进

解决了select和poll的全部缺点

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

LT模式示例

  • client.c
    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
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>

    int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);

    if(fd == -1) {
    perror("socket");
    return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
    perror("connect");
    return -1;
    }

    int num = 0;
    while(1) {
    char sendBuf[1024] = {0};
    sprintf(sendBuf, "send data %d", num++);
    write(fd, sendBuf, strlen(sendBuf) + 1);

    // 接收
    int len = read(fd, sendBuf, sizeof(sendBuf));
    if(len == -1) {
    perror("read");
    return -1;
    } else if(len > 0) {
    printf("read buf = %s\n", sendBuf);
    } else {
    printf("服务器已经断开连接...\n");
    break;
    }
    // sleep(1);
    usleep(1000);
    }
    close(fd);

    return 0;
    }
  • epoll_lt.c
    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
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/epoll.h>

    int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create(): 创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    // 对epoll实例进行管理:添加文件描述符信息
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
    int ret = epoll_wait(epfd, epevs, 1024, -1);

    if(ret == -1) {
    perror("epoll_wait");
    exit(-1);
    }

    printf("ret = %d\n", ret);

    for(int i = 0; i < ret; i++) {
    int curfd = epevs[i].data.fd;

    if(curfd == lfd) {
    // 有客户端连接请求到达
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    // 将cfd对应的读事件添加到epoll实例中
    epev.events = EPOLLIN;
    epev.data.fd = cfd;
    // 对epoll实例进行管理:添加文件描述符信息
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
    } else {
    if(epevs[i].events & EPOLLOUT) {
    continue;
    }
    // 有客户端发送数据过来
    char buf[1024] = {0};
    int len = read(curfd, buf, sizeof(buf));
    if(len == -1) {
    perror("read");
    exit(-1);
    } else if(len == 0) {
    printf("client closed...\n");
    // 对epoll实例进行管理:删除文件描述符信息
    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
    close(curfd);
    } else if(len > 0) {
    printf("read buf = %s\n", buf);
    write(curfd, buf, strlen(buf) + 1);
    }
    }
    }
    }
    close(lfd);
    close(epfd);

    return 0;
    }

ET模式示例

  • client.c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>

    int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);

    if(fd == -1) {
    perror("socket");
    return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
    perror("connect");
    return -1;
    }

    int num = 0;
    while(1) {
    char sendBuf[1024] = {0};
    // sprintf(sendBuf, "send data %d", num++);
    fgets(sendBuf, sizeof(sendBuf), stdin);
    write(fd, sendBuf, strlen(sendBuf) + 1);

    // 接收
    int len = read(fd, sendBuf, sizeof(sendBuf));
    if(len == -1) {
    perror("read");
    return -1;
    } else if(len > 0) {
    printf("read buf = %s\n", sendBuf);
    } else {
    printf("服务器已经断开连接...\n");
    break;
    }
    }
    close(fd);

    return 0;
    }
  • epoll_et.c
    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
    #include <stdio.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/epoll.h>
    #include <fcntl.h>
    #include <errno.h>

    int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create(): 创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    // 对epoll实例进行管理:添加文件描述符信息
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
    int ret = epoll_wait(epfd, epevs, 1024, -1);

    if(ret == -1) {
    perror("epoll_wait");
    exit(-1);
    }

    printf("ret = %d\n", ret);

    for(int i = 0; i < ret; i++) {
    int curfd = epevs[i].data.fd;

    if(curfd == lfd) {
    // 监听的文件描述符有数据达到,有客户端连接
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

    // 设置cfd属性非阻塞
    int flag = fcntl(cfd, F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(cfd, F_SETFL, flag);

    epev.events = EPOLLIN | EPOLLET; // 设置边缘触发模式
    epev.data.fd = cfd;
    // 对epoll实例进行管理:添加文件描述符信息
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
    } else {
    if(epevs[i].events & EPOLLOUT) {
    continue;
    }

    // 循环读取出所有数据
    char buf[5];
    int len = 0;
    while((len = read(curfd, buf, sizeof(buf))) > 0) {
    // 打印数据
    // printf("recv data : %s\n", buf);
    write(STDOUT_FILENO, buf, len);
    write(curfd, buf, len);
    }
    if(len == 0) {
    printf("client closed....");
    } else if(len == -1) {
    if(errno == EAGAIN) {
    printf("data over.....");
    } else {
    perror("read");
    exit(-1);
    }
    }
    }
    }
    }
    close(lfd);
    close(epfd);

    return 0;
    }

Linux网络编程
https://lcf163.github.io/2021/04/10/Linux网络编程/
作者
乘风的小站
发布于
2021年4月10日
许可协议