【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写

参考连接:https://www.nowcoder.com/study/live/504/2/16

【Linux】网络编程一:网络结构模式、MAC/IP/端口、网络模型、协议及网络通信过程简单介绍
【Linux】网络编程二:socket简介、字节序、socket地址及地址转换API
【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写



七, TCP通信

7.1 TCP通信流程

TCP和UDP都是传输层的协议,是传输层比较常用的协议。

UDPTCP
是否创建链接无连接有连接
是否可靠不可靠可靠
连接对象个数支持一对一、一对多、多对一、多对多支持一对一
传输方式面向数据报基于字节流
首部开销一般是8字节最少20字节
适用场景实时性要求比较高的场所,如QQ聊天、电话/视频会议、直播等可靠性要求比较高的,比如文件传输

TCP通信流程:

在这里插入图片描述

  • 服务器端,被动接受连接
      1. 创建一个用于监听的套接字(就是一个文件描述符),监听客户端的连接;socket()
      2. 将监听的文件描述符和本地IP、端口绑定(IP和端口即服务器的地址信息),客户端连接服务器时使用的就是这个IP和端口;bind()
      3. 设置监听,此时监听的文件描述符fd开始工作;listen()
      4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字;accept()
      5. 通信,接受数据read()/recv()、发送数据write()/send();
      6. 通信结束,断开连接;close()
  • 客户端,主动发送请求
      1. 创建用于通信的套接字;socket()
      2. 主动连接服务器,需要指定连接的服务器的 IP 和 端口;connect()
      3. 连接成功,客户端直接和服务器通信,发送数据write()/send()、接受数据read()/recv();
      4. 通信结束,断开连接;close()
7.1.1 套接字相关函数
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int socket(int domain, int type, int protocol); 
int bind(int sockfd, cosnt struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

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

socket()函数介绍:

socket()说明
函数功能创建一个套接字
函数声明int socket(int domain, int type, int protocol);
参数domain协议族
- AF_UNIX/AF_LOCAL:本地套接字通信,进程间通信
- AF_INET:ipv4
- AF_INET6:ipv6
参数type通信过程中使用的协议类型
- SOCK_STREAM:流式协议
- SOCK_DGRAM:报式协议
参数protocol具体的协议,设置为0时
如果type=SOCK_STREAM,默认使用TCP;
如果type=SOCK_DGRAM,默认使用UDP
返回值成功返回socket的文件描述符
失败返回-1

bind()函数介绍:

bind()说明
函数功能绑定,将socket的文件描述符和本地 IP+Port进行绑定;有时也称socket命名
函数声明int bind(int sockfd, cosnt struct sockaddr *addr, socklen_t addrlen);
参数sockfdsocket的文件描述符
参数addr需要绑定的socket地址,封装了IP和port的信息,类型为sockaddr
参数len参数addr结构体栈的内存大小
返回值成功返回0
失败返回-1

listen()函数介绍:

listen()说明
函数功能监听服务器socket是连接
函数声明int listen(int sockfd, int backlog);
参数sockfdsocket的文件描述符
参数backlog未连接的和已经连接的socket的和的最大值,可以通过文件/proc/sys/net/core/somaxconn查看
返回值成功返回0
失败返回-1

accept()函数介绍:

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

connect()函数介绍:

connect()说明
函数功能客户端连接服务器
函数声明int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数sockfd用于通信的socket文件描述符
参数addr客户端要连接的服务器的地址信息
参数addrlen指定参数addr对应的内存大小
返回值成功返回0
失败返回-1
7.1.2 TCP通信实现示例

服务器端server.c

/**
 * @file server.c
 * @author zoya (2314902703@qq.com)
 * @brief 实现TCP服务器端的通信
 * @version 0.1
 * @@date: 2022-10-09
 *
 * @copyright Copyright (c) 2022
 *
 */

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

int main()
{
    int ret = -1;
    // 创建socket,用于监听
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    // 绑定
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
#if 1
    ret = inet_pton(AF_INET, "192.168.109.130", &addr.sin_addr.s_addr); // 字符串转换为整型IP地址
    if (ret != 1)
    {
        perror("inet_pton");
        exit(-1);
    }
#else
    addr.sin_addr.s_addr = INADDR_ANY; // 表示0.0.0.0,表示无线和网卡都绑定
#endif
    addr.sin_port = htons(9999); // 主机字节序转换为网络字节序
    ret = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("bind");
        exit(-1);
    }

    // 监听连接
    if (-1 == listen(sockfd, 5))
    {
        perror("listen");
        exit(-1);
    }

    // 接收客户端连接,阻塞
    struct sockaddr_in clientAddr;
    socklen_t len = sizeof(clientAddr);
    int clientSocket_fd = accept(sockfd, (struct sockaddr *)&clientAddr, &len);
    if (clientSocket_fd == -1)
    {
        perror("accept");
        exit(-1);
    }

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

    // 通信
    // 获取客户端数据,给客户端发送确认信息
    char recvbuf[1024] = {0};
    while (1)
    {
        ssize_t size = read(clientSocket_fd, recvbuf, sizeof(recvbuf));
        if (size == -1)
        {
            perror("read");
            exit(-1);
        }
        else if (size > 0)
        {
            printf("server receive client buf %ld: %s\n", size, recvbuf);
        }
        else if (size == 0)
        {
            // 读到的字节为0表示客户端断开连接
            printf("client closed...");
            break;
        }
        char *str = "hello, i am server!";

        size = write(clientSocket_fd, str, strlen(str));
    }

    // 关闭文件描述符
    close(clientSocket_fd);
    close(sockfd);

    return 0;
}

客户端client.c

/**
 * @file client.c
 * @author zoya (2314902703@qq.com)
 * @brief tcp通信 客户端
 * @version 0.1
 * @@date: 2022-10-09
 *
 * @copyright Copyright (c) 2022
 *
 */

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

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

    // 连接服务器
    struct sockaddr_in servAddr;
    servAddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.109.130", &servAddr.sin_addr.s_addr);
    servAddr.sin_port = htons(9999);
    ret = connect(cfd, (struct sockaddr *)&servAddr, sizeof(servAddr));
    if (ret == -1)
    {
        perror("connect");
        exit(-1);
    }

    // 通信
    //  发送数据
        char recvBuf[1024] = {0};
    while (1)
    {
        char *str = "hello,i'm client!";
        ssize_t size = write(cfd, str, strlen(str));
        

        size = read(cfd, recvBuf, sizeof(recvBuf));
        if (size == -1)
        {
            perror("read");
            exit(-1);
        }
        else if (size > 0)
        {
            printf("client receive server buf : %s\n", recvBuf);
        }
        else if (size == 0)
        {
            printf("与服务器断开连接");
            break;
        }
        
        sleep(1);
    }

    // 关闭连接
    close(cfd);

    return 0;
}

7.2 TCP通信中的三次握手和四次挥手

7.2.1 TCP三次握手

TCP是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的”连接“,其实是客户端和服务器的内存里保存的一份关于对方的信息,如IP地址、端口号等。

TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数,这些参数可以放在TCP头部。

TCP提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接,采用四次挥手来关闭一个连接。

三次握手的目的是保证双方互相之间建立了连接。

三次握手发生在客户端请求连接时,调用connect()是,底层会进行三次握手。

三次握手保障了客户端和服务器互相了解自己及对方的收、发信息没有问题。

在这里插入图片描述

  • 16位端口号-port number:告知主机报文段来自哪里(源端口号)以及给哪个上层协议或应用程序(目的端口)。进行TCP通信时,客户端通常使用系统自动选择的临时端口号。
  • 32位序号-sequence number:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机A和主机B进行TCP通信,A发送给B的第一个TCP报文段中,序号值被系统初始化位某个随机值ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从A到B),后续的TCP报文段中序号值将被系统设置成ISN 加上 该报文段所携带数据的第一个字节在整个字节流中的偏移。
    • 比如,某个TCP报文段传送的数据是字节流中的第1025~2048字节,那么该报文段的序号值就是ISN+1025。另外一个传输方向(从B到A)的TCP报文段的序号值也具有相同的含义。
  • 32位确认号-acknowledgement number:用作对另一方发送来的TCP报文段的响应,其值是收到的TCP报文段的序号值 + 标志位长度(SYN/FIN) + 数据长度。
    • 假设主机A和主机B之间进行TCP通信,那么A发送出的TCP报文段不仅携带自己的序号,而且包含对B发送来的TCP报文段的确认号。反之,B发送出的TCP报文段也同样携带自己的序号和对A发送来的报文段的确认序号。
  • 4位头部长度-head length:标识TCP头部有多少个32bit(4字节)。
  • 6位标志位包含如下:
    • URG:标识紧急指针(urgent pointer)是否有效;
    • ACK:标识确认好是否生效,称携带ACK标志的TCP报文段为确认报文段。
    • PSH:提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在TCP接收缓冲区)。
    • RST:表示要求对方重新建立连接,称携带RST标志的TCP报文段为复位报文段。
    • SYN:表示请求建立一个连接,称携带SYN标志的TCP报文段为同步报文段。
    • FIN:表示通知对方本端要关闭连接了,称携带FIN标志的TCP报文段为结束报文段。
  • 16位窗口大小-window size:是TCP流量控制的一个手段,这里说的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
  • 16位校验和-TCP checksum:由发送端填充,接收端对TCP报文段执行CRC算法以校验TCP报文段在传输过程中是否损坏。需要注意,这个校验不仅包括TCP头部,也包括数据部分,这也是TCP可靠传输的一个重要保障。
  • 16位紧急指针-urgent pointer:是一个整型的偏移量,和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。确切的说,这个字段是紧急指针相对当前序号的偏移,可以称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。

在这里插入图片描述

第一次握手

  1. 客户端发送请求连接,SYN=1;
  2. 生成一个随机的32位的序号,seq=J;

第二次握手

  1. 服务器端接收客户端的连接;ACK=1;
  2. 服务器回发一个确认序号,ack=客户端序号 + 数据长度 + SYN/FIN(按一个字节);
  3. 服务器端向客户端发起连接请求,SYN=1;
  4. 服务器生成一个随机序号,seq=K;

第三次握手

  1. 客户端应答服务器的连接请求;ACK=1
  2. 客户端回复收到,ack = 服务器端的序号 + 数据长度 + SYN/FIN(按一个字节)

问题1:如何确定发送的数据是完整的?

问题2:如何确定接收数据的顺序和发送数据的顺序是一直的?

通过序号和确认序号,可以确定发送的数据是完整的,也可以确定接收数据的顺序和发送数据的顺序是一致的。

7.2.2 滑动窗口

滑动窗口,Sliding window,是一种流量控制技术,早期的网络通信中,通信双方不会考虑网络的拥挤情况,直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发送不了数据,所以有了滑动窗口机制来解决此问题。

滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包,称为窗口尺寸。

TCP中采用使用滑动窗口进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为0时,发送方一般不能再发送数据报。

滑动窗口是TCP中实现诸如ACK确认、流量控制、拥塞控制的承载结构。

窗口可以简单理解为缓冲区的大小。滑动窗口的大小是随着发送数据和接收数据变化的,每一端通信的双方都有发送缓冲区和接收数据的缓冲区。对服务器来说,有发送缓冲区和接收缓冲区;对客户端来说也有发送缓冲区和接收缓冲区;那么对应的,服务器端和客户端都有发送缓冲区的窗口和接收缓冲区的窗口。

7.2.3 四次挥手

四次挥手发生在断开连接时,程序中调用close()会使用TCP协议进行四次挥手。

客户端和服务端都可以主动发起断开连接,谁先调用close(),谁就是发起。

在TCP连接时,采用三次握手建立的连接是双向的,在断开的时候也需要双向断开连接。

如下:

  1. 客户端向服务器发送断开连接,并发送数据;FIN=1,seq=M;
  2. 服务器收到客户端的断开连接要求,向客户端发送确认;ACK=1,ack=M+1;该操作后客户端不能再向服务器端发送数据,可以接收数据,但是可以发送报文头以回复服务器端的断开连接要求;
  3. 服务器端向客户端发送断开连接,并发送数据;FIN=1,seq=N;
  4. 客户端收到服务器端的断开连接要求,向服务器端发送确认;ACK=1,ack=N+1;该操作后双方断开连接,不能发送和接收数据;

在这里插入图片描述

7.3 实现并发服务器

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

解决思路1:多进程解决

  1. 一父进程;多个子进程;
  2. 父进程等待并接受客户端的连接;
  3. 多个子进程完成通信,接受客户端的连接;

示例代码:

client.c

/**
 * @file 1client.c
 * @author zoya(2314902703@qq.com)
 * @brief TCP通信客户端,循环向服务器发送消息,并接收服务器返回的消息
 * @version 0.1
 * @date 2022-10-10
 *
 * @copyright Copyright (c) 2022
 *
 */

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

int main()
{
    int ret = -1;
    // 创建socket
    int cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd == -1)
    {
        perror("[client] : socket");
        exit(-1);
    }

    // 请求连接
    uint16_t g_port = 9999;
    char *g_ip = "192.168.57.128";
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(g_port);                         // 格式转换,主机字节序port转换为网络字节序port
    ret = inet_pton(AF_INET, g_ip, &addr.sin_addr.s_addr); // 格式转换,主机字节序ip转换为网络字节序ip
    if (ret == 0)
    {
        printf("[client] : string ip is not a valid network address.\n");
        exit(-1);
    }
    else if (ret == -1)
    {
        perror("[client] : inet_pton");
        exit(-1);
    }
    ret = connect(cfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("[client] : connect");
        exit(-1);
    }

    // 发送数据
    char recvBuf[1024] = {0};
    char sendBuf[1024] = {0};
    int num = 0;
    while (1)
    {
        // 每隔1s发送、接收消息
        memset(recvBuf, 0, sizeof(recvBuf));
        memset(sendBuf, 0, sizeof(sendBuf));

        sprintf(sendBuf, "hello,i am client, this is %dth message.", ++num);
        ssize_t size = write(cfd, sendBuf, strlen(sendBuf));

        // 接收数据
        size = read(cfd, recvBuf, sizeof(recvBuf));
        if (size > 0)
        {
            printf("[client] : receive buf - %s\n", recvBuf);
        }
        else if (size == -1)
        {
            perror("[client] : read");
            break;
        }
        else if (size == 0)
        {
            printf("[client] : disconnect!\n");
            break;
        }
        sleep(1);
    }

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

    return 0;
}

server.c

/**
 * @file 1server.c
 * @author zoya(2314902703@qq.com)
 * @brief 服务器端实现并发处理,进程实现
 * @version 0.1
 * @date 2022-10-13
 *
 * @copyright Copyright (c) 2022
 *
 */
#define _XOPEN_SOURCE 500

#include <stdio.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <errno.h>

void recyChild(int signum)
{
    while (1)
    {
        int ret = waitpid(-1, NULL, WNOHANG); // 设置为非阻塞,-1表示回收所有的子进程
        if (ret == -1)
        {
            // 所有的子进程都回收了
            break;
        }
        else if (ret == 0)
        {
            // 还有子进程 活着
            break;
        }
        else if (ret > 0)
        {
            // 还有子进程没有被回收
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main()
{
    // 捕捉信号 SIGCHLD
    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recyChild;
    sigaction(SIGCHLD, &act, NULL);

    // 创建SOCKET
    int lfd = socket(AF_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 = -1;

    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 caddr;
        int len = sizeof(caddr);
        int cfd = accept(lfd, (struct sockaddr *)&caddr, &len); // 返回客户端的文件描述符
        if (cfd == -1)
        {
            // EINTR : 软中断,在连接到达之前,如果有信号则调用会被信号中断
            if (errno == EINTR)
            {
                // 说明产生了中断
                continue;
            }
            perror("accept");
            exit(-1);
        }

        // 每一个连接,就创建一个子进程,与客户端通信
        pid_t pid = fork();

        if (pid == 0)
        {
            // 子进程  进行通信

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

            printf("child process : %d client ip : %s, port : %d\n", getpid(), cip, cport);

            // 接受客户端发送的数据
            char recvbuf[1024] = {0};
            while (1)
            {
                memset(recvbuf, 0, sizeof(recvbuf));
                ssize_t size = read(cfd, recvbuf, sizeof(recvbuf));
                if (size == -1)
                {
                    perror("read");
                    break;
                }
                else if (size > 0)
                {
                    printf("child process recv : %s\n", recvbuf);
                }
                else if (size == 0)
                {
                    printf("child process, client disconnect...\n");
                    break;
                }

                // 发送数据给客户端
                write(cfd, recvbuf, strlen(recvbuf));
            }

            close(cfd);
            exit(0);
        }
    }

    close(lfd);
    return 0;
}

解决思路2:多线程解决

  1. 子线程处理通信;
  2. 主线程进行连接;

server,c

/**
 * @file 1serve_thread.c
 * @author zoya(2314902703@qq.com)
 * @brief 多线程实现并发服务器
 * @version 0.1
 * @date 2022-10-13
 *
 * @copyright Copyright (c) 2022
 *
 * 没有一个连接就创建一个线程,在线程中接受或发送数据
 * 主线程连接通信
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <arpa/inet.h>

struct sockInfo
{
    int fd;                  // 文件描述符
    pthread_t tid;           // 线程号
    struct sockaddr_in addr; // 客户端的地址信息
};

struct sockInfo g_sockinfos[128]; // 同时支持128个客户端连接

void *callback(void *arg)
{
    // 子线程和客户端通信  需要的信息可能有 : 客户端的文件描述符cfd,客户端的地址信息,线程号
    struct sockInfo *sockinfo = (struct sockInfo *)arg;
    int cfd = sockinfo->fd;
    //获取客户端信息
    char cip[16];
    inet_ntop(AF_INET, &sockinfo->addr.sin_addr.s_addr, cip, sizeof(cip));
    unsigned short cport = ntohs(sockinfo->addr.sin_port);

    printf("client ip : %s, port : %d\n", cip, cport);

    char recvbuf[1024] = {0};
    while (1)
    {
        // 接收数据
        ssize_t size = read(cfd, recvbuf, sizeof(recvbuf));
        if (size > 0)
        {
            printf("recv msg : %s\n", recvbuf);
        }
        else if (size == -1)
        {
            perror("read");
            break;
        }
        else if (size == 0)
        {
            printf("client disconnect...\n");
            break;
        }

        write(cfd, recvbuf, strlen(recvbuf));
    }

    close(cfd);

    return NULL;
}

int main()
{

    // 创建SOCKET
    int lfd = socket(AF_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 = -1;

    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(g_sockinfos) / sizeof(g_sockinfos[0]);
    for (int i = 0; i < max; i++)
    {
        bzero(&g_sockinfos[i], sizeof(g_sockinfos[i]));
        g_sockinfos[i].fd = -1;  //
        g_sockinfos[i].tid = -1; //
    }

    // 不断循环等待连接,有连接,创建子线程
    while (1)
    {
        struct sockaddr_in caddr;
        int len = sizeof(caddr);
        int cfd = accept(lfd, (struct sockaddr *)&caddr, &len); // 返回客户端的文件描述符
        if (cfd == -1)
        {
            perror("accept");
            exit(-1);
        }

        // 每一个连接,就创建一个子线程
        struct sockInfo *sockinfo;
        for (int i = 0; i < max; i++)
        {
            // 从数组中找到可用的元素
            if (g_sockinfos[i].fd == -1)
            {
                sockinfo = &g_sockinfos[i];
                break;
            }
            if (i == max - 1)
            {
                sleep(1);
                i--;
            }
        }

        sockinfo->fd = cfd;
        memcpy(&sockinfo->addr, &caddr, len);
        ret = pthread_create(&sockinfo->tid, NULL, callback, sockinfo);
        if (ret != 0)
        {
            printf("[pthread_create]: %s", strerror(ret));
            exit(0);
        }

        pthread_detach(sockinfo->tid); // 设置线程分离
    }

    close(lfd);

    return 0;
}

7.4 TCP状态转换

TCP状态转换发生在三次握手和四次挥手过程中。

在这里插入图片描述

三次握手

  1. 客户端发送连接请求,客户端处于SYN_SENT状态;
  2. 服务器开始处于监听LISTEN状态,收到客户端的连接请求,变为SYN_RCVD状态;
  3. 服务器向客户端发送确认和连接请求,客户端变为ESTABLISHED状态;
  4. 客户端向服务器发送确认,服务器变为ESTABLISHED状态;

四次挥手

  1. 客户端发送断开连接请求(FIN=1),状态变为FIN_WAIT_1
  2. 服务端接收到FIN请求后,服务端变为CLOSE_WAIT(等待关闭),服务端回复客户端ACK相应;
  3. 客户端收到服务端的响应,状态变为FIN_WAIT_2
  4. 服务端发送断开连接请求(FIN=1),服务端状态变为LAST_ACK
  5. 客户端收到服务端的请求后专改变为TIME_WAIT,并向客户端发送ACK;
  • TIME_WAIT:定时经过2倍报文段时间,2MSL。

主动断开连接的一方,最后进入一个TIME_WAIT状态,这个状态持续的时间是:2MSL(Maximum Segement Lifetime)。官方建议msl是2分钟(ubuntu中实际测试是30s)。

MAL是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。TIME_WAIT状态也称为2MSL状态。当一端主动发起关闭,发出最后一个ACK后,即第三次挥手完成后发送了第四次挥手的ACK后就进入TTIME_WAIT状态,必须在此状态上停留两倍的MSL时间,主要目的是怕最后一个ACK包对方没有收到,那么对方在超时后将重发第三次挥手的FIN包,主动关闭端接到重发的FIN后再重发ACK应答。

TIME_WAIT状态下,两端的端口不能使用,要等到2MSL时间结束才可以继续使用。当连接处于2MSL等待阶段时,任何迟到的报文段都将被丢弃。

参考:什么是2MSL。

当TCP连接主动关闭方接收到被动关闭方发送的FIN和最终的ACK后,主动关闭方必须处于TIME_WAIT状态并持续2MSL时间。这样做的目的是能够让TCP连接的主动关闭方在它发送的ACK丢失的情况下重新发送最终的ACK。

主动关闭方重新发送的最终ACK是因为被动关闭方重传了它的FIN,被动关闭方总是重传FIN直到它收到一个最终的ACK。

  • 半关闭

当TCP连接中A向B发送FIN请求关闭,另一端B回应ACK之后(A进入FIN_WAIT_2状态),并没有立即发送FIN给A,A处于半连接状态(半开关),此时A可以接收B发送的数据,但是A不能向B发送数据。

可以使用相应的API控制实现半连接状态。

#include <aya/socket.h>
int shutdown(int sockfd, int how);
  • sockfd:要关闭的socket描述符
  • how:允许位shutdown操作的方式:
    • SHUT_RD(0):关闭sockfd的读功能,此选项不允许sockfd进行读操作;
      • 表示该套接字不再接收数据,任何当前在套接字接收缓冲区的数据将被丢弃;
    • SHUT_WR(1):关闭sockfd的写功能,表示将不允许sockfd进行写操作;
    • SHUT_RDWR(2):关闭sockfd的读写功能,表示调用shutdown两次,首先以SHUT_RD,然后以SHUT_WR;

使用close终止一个连接,只是减少描述符的引用计数,并不直接关闭连接,只有当文件描述符的引用计数为0时,才会关闭连接。

shutdown不考虑文件描述符的引用计数,直接关闭文件描述符。也可以选择终止一个方向的连接,只终止读或只终止写。

如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,所有进程都调用close,套接字被释放。

在多进程中如果一个进程调用了shurdown(sfd,SHUT_RDWR),其它进程将无法进行通信,但如果一个进程close(sfd)将不会影响其它进程。

7.5 端口复用 setsockopt

端口复用最常用的用途:

  1. 防止服务器重启时之前绑定的端口还未释放;
  2. 程序突然退出而系统没有释放端口;
#include <sys/types.h>
#include <sys/sockt.h>
// 设置端口复用,也可以设置端口状态
int setsockopt(int sockfd, int level, int optname, consr void *optval, socklen_t *optlen);  // 该函数仅用于套接字
  • sockfd:指向一个打开的套接字描述符;
  • level:级别,使用SOL_SOCKET(端口复用的级别);
  • optname:选项名称,端口复用使用以下:
    • SO_RUSEADDR
    • SO_RUSEPORT
  • optval:端口复用的值,整型;
    • 1 表示可以复用;
    • 0 表示不可复用;
  • optlen:optval参数的大小;

端口复用设置的时机在服务器绑定端口之前。

socket()    //  创建socket
setsockopt()  // 设置端口复用
bind()  // 绑定

端口复用示例:

server.c中设置端口复用:

/**
 * @file 2server.c
 * @author zoya(2314902703@qq.com)
 * @brief 接收客户端的消息,并转换消息
 * @version 0.1
 * @date 2022-10-14
 *
 * @copyright Copyright (c) 2022
 *
 */

#define _XOPEN_SOURCE 500

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <ctype.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

void my_handler(int signum)
{
    while (1)
    {
        int ret = waitpid(-1, NULL, WNOHANG); // 设置为非阻塞回收资源
        if (ret > 0)
        {
            printf("[server]: 子进程 %d 被回收了\n", ret);
        }
        else if (ret == -1)
        {
            // 所有的子进程都被回收了
            break;
        }
        else if (ret == 0)
        {
            // 还有子进程没有被回收,说明还有子进程需要执行,暂时不需要回收
            break;
        }
    }
}

int main()
{
    // 注册信号SIGCHLD处理函数,回收子进程资源
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = my_handler;
    sigemptyset(&act.sa_mask);
    sigaction(SIGCHLD, &act, NULL);

    // 创建socket
    int sfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sfd == -1)
    {
        perror("[server] : socket()");
        exit(-1);
    }

    int ret = -1;

    // 设置I/O复用
    int optval = 1;
    ret = setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
    if (ret == -1)
    {
        perror("[server] : setsockopt()");
        exit(-1);
    }

    // 绑定
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;
    ret = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("[server] : bind()");
        exit(-1);
    }

    // 监听
    ret = listen(sfd, 128);
    if (ret == -1)
    {
        perror("[server] : listen()");
        exit(-1);
    }

    while (1)
    {
        // 接收连接
        struct sockaddr_in clieaddr;
        socklen_t len = sizeof(clieaddr);
        int cfd = accept(sfd, (struct sockaddr *)&clieaddr, &len);
        if (ret == -1)
        {
            if (errno == EINTR)
            {
                continue;
            }
            perror("[server] : accept()");
            break;
        }

        // 输出客户端信息
        char clieip[16] = {0};
        inet_ntop(AF_INET, &clieaddr.sin_addr.s_addr, clieip, sizeof(clieip));
        int clieport = ntohs(clieaddr.sin_port);
        printf("[server] : client ip : %s, port : %d\n", clieip, clieport);

        // 创建子进程与客户端通信
        pid_t pid = fork();
        if (pid == 0)
        {
            // 子进程,处理与客户端通信

            char recbuf[1024] = {0};
            char sendbuf[1024] = {0};
            while (1)
            {
                memset(recbuf, 0, sizeof(recbuf));
                memset(sendbuf, 0, sizeof(sendbuf));

                //读取客户端消息
                ssize_t size = read(cfd, recbuf, sizeof(recbuf));
                if (size > 0)
                {
                    printf("[server-%d] : recv msg , %s\n", getpid(), recbuf);
                }
                else if (size == -1)
                {
                    perror("[server] : read()");
                    break;
                }
                else if (size == 0)
                {
                    printf("[server] : client disconnect...\n");
                    break;
                }

                /// 向客户端发送消息
                for (int i = 0; i < strlen(recbuf); i++)
                {
                    sendbuf[i] = toupper(recbuf[i]);
                }
                sendbuf[strlen(recbuf)] = '\0';

                write(cfd, sendbuf, strlen(sendbuf));
            }

            close(cfd);
            exit(-1);
        }
    }

    close(sfd);

    return 0;
}

7.6 I/O多路复用

I/O多路复用有时也称为I/O多路转接。

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

7.6.1 常见的I/O模型
  • 阻塞等待 BIO-Blocking I/O

    不占用CPU宝贵的时间片;但是同一时刻只能处理一个操作,效率低;

    • 解决方案:可以使用多线程或者多进程方式解决;
      • 线程或进程会消耗一定的系统资源;
      • 线程或进程调度会消耗CPU资源;
  • 非阻塞,忙轮询 NIO-Non-Blocking I/O

    提高了程序的执行效率;但是需要占用更多的CPU和系统资源,每循环内有O(n)的系统调用;;

    • 解决方案:使用IP多路转接技术,select/poll/epoll
      • select/poll:仅通知有几个数据到了,需要自己遍历是在哪些读缓冲区中;
      • epoll:通知哪些读缓冲区有数据;
  • IO复用

  • 信号驱动

  • 异步

7.6.2 NIO中的多路复用 select/poll/epoll
  1. select

    1. 构造一个文件描述符列表,将要监听的文件描述符添加到该列表中。
    2. 调用系统函数select()监听该列表中的文件描述符,直到这些文件描述符中的一个或多个进行I/O操作时,该函数才返回。select()是阻塞的,且对文件描述符的检测的操作是由内核完成的。
    3. 在返回时,该函数会告诉进程有多少文件描述符要进行I/O操作。
    #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);
    
    

    select函数参数介绍:

    • nfd:委托内核检测的最大文件描述符的值 +1.

    • readfds:要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读属性;

      • 检测读数据;
      • 对应的是对方发送的数据,检测读缓冲区。
    • wrfdsite:要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写属性;

      • 委托内核检测写缓冲区是不是还可以写数据(即检测写缓冲区是否满了);
      • 一般不检测写缓冲区,设置为NULL;
    • exceptfds:检测发生异常的文件描述符的集合,一般不使用设置为NULL。

    • timeout:设置的超时时间;

      • 如果为NULL表示永久阻塞,直到检测到文件描述符有变化;

      • 如果tv_sectv_usec都为0表示不阻塞;

      • 如果tvsec>0tvusec>0表示阻塞对应的时间;

      • struct timeval{
        	long tv_sec;
        	long tv_usec;
        }
        

    select返回值:

    • -1:表示失败;
    • >0:表示检测的集合中有n个文件描述符发生了变化

    如下函数是对二进制位的一些操作:

    void FD_CLR(int fd,fd_set *set);  // 对fd对应的标志位置为0
    int FD_ISSET(int fd, fd_set *set);  // 判断fd对应的标志位是0还是1,返回值是fd对应的标志位的值
    void FD_SET(int fd, fd_set *set);  // 将fd对应的标志位设置为1;
    void FD_ZERO(fd_set *set);  // 设置文件描述符集set对应的文件描述符的标志位都为0
    

    select使用示例:

   
   // server.c
   /**
    * @file 1server_select.c
    * @author zoya(2314902703@qq.com)
    * @brief TCP通信服务端:select实现
    * @version 0.1
    * @date 2022-10-14
    *
    * @copyright Copyright (c) 2022
    *
    */
   
   #include <stdio.h>
   #include <stdlib.h>
   #include <string.h>
   #include <arpa/inet.h>
   #include <unistd.h>
   #include <sys/time.h>
   #include <sys/types.h>
   #include <sys/select.h>
   
   int main()
   {
       // 创建socket
       int sfd = socket(PF_INET, SOCK_STREAM, 0);
       if (sfd == -1)
       {
           perror("[server] : socket()");
           exit(-1);
       }
   
       int ret = -1;
   
       // 绑定
       struct sockaddr_in saddr;
       saddr.sin_family = AF_INET;
       saddr.sin_port = htons(9999);
       saddr.sin_addr.s_addr = INADDR_ANY;
       ret = bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr));
       if (ret == -1)
       {
           perror("[server] : bind()");
           exit(-1);
       }
   
       // 监听
       ret = listen(sfd, 128);
       if (ret == -1)
       {
           perror("[server] : listen()");
           exit(-1);
       }
   
       // NIO模型
       // 创建文件描述符集合
       fd_set rdset, rdsettmp;
       FD_ZERO(&rdset); // 标志位全部置为0
       FD_SET(sfd, &rdset);
   
       int maxfd = sfd;
   
       while (1)
       {
           rdsettmp = rdset;
           // 调用select,检测哪些文件描述符有数据
           ret = select(maxfd + 1, &rdsettmp, NULL, NULL, NULL); // 一直阻塞直到有文件描述符发生变化
           if (ret > 0)
           {
               // 有文件描述符对应的缓冲区数据发生改变
               // 遍历检查是哪个文件描述符发生了改变
               if (FD_ISSET(sfd, &rdsettmp))
               {
                   // 有新的客户端连接,接收连接
                   // 接收连接
                   struct sockaddr_in clieaddr;
                   socklen_t len = sizeof(clieaddr);
                   int cfd = accept(sfd, (struct sockaddr *)&clieaddr, &len);
                   if (ret == -1)
                   {
                       perror("[server] : accept()");
                       break;
                   }
   
                   // 输出客户端信息
                   char clieip[16] = {0};
                   inet_ntop(AF_INET, &clieaddr.sin_addr.s_addr, clieip, sizeof(clieip));
                   int clieport = ntohs(clieaddr.sin_port);
                   printf("[server] : client ip : %s, port : %d\n", clieip, clieport);
   
                   // 把连接的客户端的文件描述符加入到集合中
                   FD_SET(cfd, &rdset);
                   // 更新最大的文件描述符
                   maxfd = (maxfd > cfd) ? maxfd : cfd;
               }
   
               for (int i = sfd + 1; i < maxfd + 1; i++)
               {
                   if (FD_ISSET(i, &rdsettmp))
                   {
                       // 判断文件描述符i是不是为1,1说明这个文件描述符对应的客户端发来了数据
                       char buf[1024] = {0};
                       int size = read(i, buf, sizeof(buf));
                       if (size == -1)
                       {
                           perror("[server] : read()");
                           exit(-1);
                       }
                       else if (size == 0)
                       {
                           // 对方断开连接
                           printf("[server] : client disconnect...\n");
                           FD_CLR(i, &rdset);
                       }
                       else if (size > 0)
                       {
                           printf("[server] : recv msg : %s\n", buf);
                           write(i, buf, strlen(buf) + 1);
                       }
                   }
               }
           }
           else if (ret == -1)
           {
               perror("[server] : select()");
               exit(-1);
           }
           else if (ret == 0)
           {
               // 0表示超时时间到了,没有任何文件描述符发生改变
               continue;
           }
       }
   }

select的缺点:

  • 每次调用,需要把fd集合从用户态拷贝到内核态,如果fd很多,开销很大;
  • 每次调用,都需要在内核遍历传递进来的fd集合,开销在fd很多时也很大;
  • select支持的文件描述符数量太小,默认是1024;
  • fds集合不能重用,每次都需要重置;
  1. poll

    poll是对select的改进。

    #include <poll.h>
    struct pollfd{
    	int fd;  // 委托内核检测的文件描述符
    	short events;  // 委托内核检测文件描述符的什么事件
    	short revents;  // 文件描述符实际发生的事情
    }
    int poll(struct pollfd *fds, nfds_t nfds,int timeout);
    

    参数说明:

    • fdsstruct pollfd结构体数组,是一个需要检测的文件描述符的集合;没有个数1024的限制;
    • nfds:第一个参数中最后一个有效元素的下标 + 1
    • timeout:阻塞时长,0表示不阻塞;-1表示阻塞,当检测到需要检测的文件描述符有变化,解除阻塞;>0的值表示阻塞的时长,单位:毫秒;

    返回值:

    • -1表示失败;
    • >0表示检测到集合中有文件描述符发生变化
    事件常值作为events的值作为revents的值说明
    读事件POLLN普通或优先带数据可读
    读事件POLLRDNORM普通数据可读
    读事件POLLRDBAND优先级带数据可读
    读事件POLLPRI高优先级数据可读
    写事件POLLOUT普通或优先带数据可写
    写事件POLLWRNORM普通数据可写
    写事件POLLWRBAND优先级带数据可写
    错误事件POLLERR发生错误
    错误事件POLLHUP发生挂起
    错误事件POLLNVAL描述不是打开的文件

    poll的缺点:

    • 每次需要把文件描述符数组从用户态拷贝到内核态,开销比较大。
    • 主动遍历,每次在内核中都会主动遍历哪些文件描述符发生改变

    poll使用示例:

  // server.c
  /**
   * @file 1poll_server.c
   * @author zoya (2314902703@qq.com)
   * @brief TCP通信服务端:poll实现IO多路复用
   * @version 0.1
   * @date 2022-10-17
   *
   * @copyright Copyright (c) 2022
   *
   */
  
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <poll.h>
  #include <unistd.h>
  #include <arpa/inet.h>
  
  int main()
  {
      // 创建socket
      int sfd = socket(PF_INET, SOCK_STREAM, 0);
      if (sfd == -1)
      {
          perror("[server] : socket()");
          exit(-1);
      }
  
      int ret = -1;
  
      // 绑定
      struct sockaddr_in saddr;
      saddr.sin_family = AF_INET;
      saddr.sin_port = htons(9999);
      saddr.sin_addr.s_addr = INADDR_ANY;
      ret = bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr));
      if (ret == -1)
      {
          perror("[server] : bind()");
          exit(-1);
      }
  
      // 监听
      ret = listen(sfd, 128);
      if (ret == -1)
      {
          perror("[server] : listen()");
          exit(-1);
      }
  
      // 创建pollfd结构体数组
      struct pollfd fds[1024];
      // 初始化pollfd结构体数组
      for (int i = 0; i < sizeof(fds) / sizeof(fds[0]); i++)
      {
          fds[i].fd = -1;
          fds[i].events = POLLIN;
      }
      fds[0].fd = sfd;
      int maxfd = 0;
  
      while (1)
      {
          // 调用select,检测哪些文件描述符有数据
          ret = poll(fds, maxfd + 1, -1); // -1表示阻塞直到有文件描述符发生变化
          if (ret > 0)
          {
              // 有文件描述符发生变化,表示有连接
              if (fds[0].revents & POLLIN)
              {
                  // 有新的客户端连接
                  struct sockaddr_in clieaddr;
                  socklen_t len = sizeof(clieaddr);
                  int cfd = accept(sfd, (struct sockaddr *)&clieaddr, &len);
                  if (ret == -1)
                  {
                      perror("[server] : accept()");
                      break;
                  }
  
                  // 输出客户端信息
                  char clieip[16] = {0};
                  inet_ntop(AF_INET, &clieaddr.sin_addr.s_addr, clieip, sizeof(clieip));
                  int clieport = ntohs(clieaddr.sin_port);
                  printf("[server] : client ip : %s, port : %d\n", clieip, clieport);
  
                  // 把新的客户端连接加入到fds数组中
                  for (int i = 1; i < 1024; i++)
                  {
                      if (fds[i].fd == -1)
                      {
                          fds[i].fd = cfd;
                          fds[i].events = POLLIN;
                          maxfd = maxfd > i ? maxfd : i;
                          break;
                      }
                  }
              }
              for (int i = 1; i < maxfd + 1; i++)
              {
                  if (fds[i].revents & POLLIN)
                  {
                      // 接收、发送数据
                      char buf[1024] = {0};
                      int size = read(fds[i].fd, buf, sizeof(buf));
                      if (size == -1)
                      {
                          perror("[server] : read()");
                          exit(-1);
                      }
                      else if (size == 0)
                      {
                          // 对方断开连接
                          printf("[server] : client disconnect...\n");
                          fds[i].fd = -1;
                          fds[i].events = POLLIN;
                      }
                      else if (size > 0)
                      {
                          printf("[server] : recv msg : %s\n", buf);
                          write(fds[i].fd, buf, strlen(buf));
                      }
                  }
              }
          }
          else if (ret == -1)
          {
              perror("[server] : select()");
              exit(-1);
          }
          else if (ret == 0)
          {
              // 0表示超时时间到了,没有任何文件描述符发生改变
              continue;
          }
      }
  
      return 0;
  }
  1. epoll

    epoll的原理

  • int epfd = epoll_create()在内核中创建epoll实例,类型为struct eventpoll;返回文件描述符,用于操作内核中的文件描述符。
  • epoll_ctl(epfd,EPOLL_CTL_ADD,sfd,&ev)委托内核检测文件描述符对应的缓冲区是否发生变化;
  • epoll_wait(epfd,...)告知内核从rbr中检测是否有文件描述符的信息发生了改变,如果有变化,就把所有信息复制到rdlist中。
#include <sys/epoll.h>
struct eventpoll{
	...
	struct rb_root rbr;  // 采用红黑树的数据结构,查找效率比较高
	struct list_head rdlist;  // 记录需要检测的文件描述符,双链接的形式
	...
};
struct union epoll_data{
    void *ptr;
    int fd;  // 常用的是fd
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;
struct epoll_event{
    int events; // 检测哪些事件
    epoll_data_t data;  // 用户数据信息
};

int epoll_create(int size);  
// 创建一个新的epoll实例,在内核中创建了一个数据,这个数据中比较重要的有两个rbr和rdlist,
// rbr表示需要检测的文件描述符的信息(红黑树);
// rdlist存放检测到数据发送改变的文件描述符信息(双链表);  
// 参数size:Linux2.6.8之后被忽略,但必须大于0; 
// 返回值: 失败返回-1;成功返回文件描述符,通过该返回值可以操作epoll实例。


// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op,int fd, struct epoll_event *event);
// 参数
// - epfd:epoll实例对应的文件描述符
// - op:要进行的操作
//     - EPOLL_CTL_ADD:添加
//     - EPOLL_CTL_MOD:修改
//     - RPOLL_CTL_DEL:删除
// - fd:要检测的文件描述符
// - event:检测文件描述符的操作
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// 参数:
// - epfd:epoll实例对应的文件描述符;
// - events:传出参数,保存了发生变化的文件描述符信息;
// - maxevents:参数events数组的大小;
// - timeout:阻塞时间,0表示不阻塞;-1表示阻塞直到检测到文件描述符发生变化;>0表示阻塞的时间,单位是ms;
// 返回值:
//     - 成功,返回发生变化的文件描述符的个数 >0;
//     - 失败,返回-1;

常见的epoll检测事件:

  • EPOLLIN:读缓冲区变化;
  • EPOLLOUT:写缓冲区变化
  • EPOLLERR:错误;
  • EPOLLET:设置边沿触发模式;

epoll的工作模式有LT模式和ET模式,即水平触发和边沿触发。

  • LT模式:

LT,level-triggered,水平触发,是缺省的工作方式,同时支持blockno-block socket。在这种做法中,内核告诉一个文件描述符是否就绪了,就可以对这个就绪的fd进行IO操作,如果不做任何操作,内核继续通知。

假设委托内核检测读事件,即检测fd的读缓冲区

  • 读缓冲区有数据,即epoll检测到了给用户通知

    • 用户不读数据,数据一直在缓冲区,epoll一直通知
    • 用户读一部分数据,epoll仍然通知
    • 缓冲区中的数据读完,epoll不通知
  • ET模式:

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

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

ET模式下,需要配合循环读数据和非阻塞的方式读取数据

假设委托内核检测读事件,->检测fd的读缓冲区

  • 读缓冲区有数据,即epoll检测到了给用户通知
    • 用户不读数据,数据一直在缓冲区,epoll下一次检测不通知
    • 用户读一部分数据,epoll下一次不会通知
    • 缓冲区中的数据读完,epoll不通知

7.7 本地套接字

本地套接字用来实现本地进程间通信(有关系和没有关系的进程间通信)。本地套接字和网络套接字类似,一般采用TCP通信流程。

本地套接字通信流程:

在这里插入图片描述

  • 服务端
    • 创建监听套接字,int lfd = socket(AF_UNIX,SOCK_STREAM,0);
    • 监听的套接字绑定本地的套接字文件,本地地址 struct sockaddr_un addrbind(lfd,addr,len);
      • 绑定成功后,指定的sun_path中的套接字文件会自动生成
    • 监听是否有客户端连接,listen(lfd,128);
    • 等待并接受客户端连接请求,使用本地地址,int cfd = accept(lfd,caddr,len);
    • 通信,接收(read/recv)或发送(write/send)数据;
    • 关闭连接
  • 客户端
    • 创建通信的套接字,int fd = socket(AF_UNIX,SOCK_STREAM,0);
    • 监听的套接字绑定本地的IP端口
    • 本地地址 struct sockaddr_un addrbind(lfd,addr,len);
      • 绑定成功后,指定的sun_path中的套接字文件会自动生成
    • 请求连接服务器,connet(fd,saddr,len);
    • 通信,发送(write/send)或者接收(read/recv)数据;
    • 关闭连接

本地套接字通信示例:

server.c

/**
 * @file 2server_ipc.c
 * @author zoya (2314902703@qq.com)
 * @brief 本地套接字服务端
 * @version 0.1
 * @@date: 2022-10-18
 *
 * @copyright Copyright (c) 2022
 *
 */

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

#define PATH_UNIX 100

int main()
{
    // 删除服务端sock
    unlink("server.sock");
    // 创建监听套接字
    int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (lfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    int ret = -1;
    // 绑定本地套接字文件
    struct sockaddr_un addr;
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, "server.sock"); // 服务端套接字生成的文件
    ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("bind");
        exit(-1);
    }

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

    // 等待客户端连接
    struct sockaddr_un caddr;
    socklen_t len = sizeof(caddr);
    int cfd = accept(lfd, (struct sockaddr *)&caddr, &len);
    if (cfd == -1)
    {
        perror("accept");
        exit(-1);
    }

    printf("client socket filename : %s\n", caddr.sun_path);

    // 通信
    char buf[128] = {0};
    while (1)
    {
        memset(buf, 0, sizeof(buf));
        int size = recv(cfd, buf, sizeof(buf), 0);
        if (size == -1)
        {
            perror("recv");
            exit(-1);
        }
        else if (size == 0)
        {
            printf("client disconnect...\n");
            break;
        }
        else if (size > 0)
        {
            printf("client say : %s\n", buf);
            // 发送数据
            send(cfd, buf, size, 0);
        }
    }

    close(cfd);
    close(lfd);

    return 0;
}

client.c

/**
 * @file 2client_ipc.c
 * @author zoya (2314902703@qq.com)
 * @brief 本地套接字通信客户端
 * @version 0.1
 * @@date: 2022-10-18
 *
 * @copyright Copyright (c) 2022
 *
 */

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

int main()
{
    //  删除客户端sock
    unlink("client.sock");

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

    int ret = -1;
    // 绑定本地套接字文件
    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "client.sock"); // 客户端套接字生成的文件
    ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("bind");
        exit(-1);
    }

    struct sockaddr_un saddr;
    saddr.sun_family = AF_UNIX;
    strcpy(saddr.sun_path, "server.sock"); // 连接服务端套接字文件
    socklen_t len = sizeof(saddr);
    // 主动连接服务器
    ret = connect(cfd, (struct sockaddr *)&saddr, len);
    if (ret == -1)
    {
        perror("connect");
        exit(-1);
    }

    // 通信
    char buf[128] = {0};
    int num = 0;
    while (1)
    {
        memset(buf, 0, sizeof(buf));

        sprintf(buf, "i am client, this is %dth msg.\n", ++num);
        int size = send(cfd, buf, strlen(buf), 0);
        printf("client say: %s\n", buf);
        // 接收数据
        size = recv(cfd, buf, sizeof(buf), 0);
        if (size == -1)
        {
            perror("recv");
            exit(-1);
        }
        else if (size == 0)
        {
            printf("client disconnect...\n");
            break;
        }
        else if (size > 0)
        {
            printf("server say : %s\n", buf);
        }

        sleep(1);
    }

    close(cfd);

    return 0;
}

八, UDP通信

8.1 UDP通信流程及相关API介绍

UDP通信流程如下:

在这里插入图片描述

UDP通信时使用到的API有:

#include <sys/types.h>
#include <sys/socket.g>
ssize_t sendto(int sockfd,const void *buf,size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd,void *buf,size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen )

参数说明:

  • sockfd:通信的socket fd
  • buf:要发送或接收的数据;
  • len:发送数据或接收数据的长度;
  • flags:一般设置为0;
  • dest_addr:通信的另外一端的地址消息;
  • src_addr:保存另外一端的地址信息;也可以指定为NULL,表示不需要
  • addrlendest_addrsrc_addr 地址的内存大小;

返回值:

sento():成功返回发送的字节数,失败返回-1;

recvfrom():成功返回收到的字节数,失败返回-1;

UDP通信示例:

server.c

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

int main()
{
    // 创建socket
    int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
    if (sfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    //绑定
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("bind");
        exit(-1);
    }

    // 通信
    char buf[1024] = {0};
    while (1)
    {
        memset(buf, 0, sizeof(buf));
        // 接收消息
        struct sockaddr_in caddr;
        socklen_t len = sizeof(caddr);
        ssize_t size = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr *)&caddr, &len);
        if (size == -1)
        {
            perror("recvfrom");
            break;
        }

        // 输出客户端信息
        char cip[16] = {0};
        printf("client ip : %s, port : %d\n",
               inet_ntop(AF_INET, &caddr.sin_addr.s_addr, cip, sizeof(cip)), ntohs(caddr.sin_port));

        printf("recv msg : %s\n", buf);

        // 发送数据
        size = sendto(sfd, buf,strlen(buf)+1,0,(struct sockaddr*)&caddr,sizeof(caddr));
        if(size == -1)
        {
            perror("sendto");
            break;
        }
    }

    close(sfd);

    return 0;
}

client.c

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

int main()
{
    // 创建socket
    int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
    if (sfd == -1)
    {
        perror("socket");
        exit(-1);
    }
    // 通信
    // 接收消息
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);

    socklen_t len = sizeof(saddr);

    char buf[1024] = {0};
    int num = 0;
    while (1)
    {
        memset(buf, 0, sizeof(buf));
        sprintf(buf, "UDP : i am client, this is %dth msg.\n", ++num);

        // 发送数据
        ssize_t size = sendto(sfd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr));
        if (size == -1)
        {
            perror("sendto");
            break;
        }

        size = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr *)&saddr, &len);
        if (size == -1)
        {
            perror("recvfrom");
            break;
        }

        printf("recv msg : %s\n", buf);

        sleep(1);
    }

    close(sfd);

    return 0;
}

8.2 广播

广播:向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1;

  • 广播只能在局域网中使用;

  • 客户端需要绑定服务器广播使用的端口才可以接收到广播的消息;

    使用setsockopt()函数可以设置广播属性,

    把该函数的参数level设置为SOL_SOCKET

    参数optname设置为SO_BROADCAST

    参数optval设置为1表示允许发送广播,值为0表示不允许发送广播;

    广播流程:

在这里插入图片描述

广播使用示例:

server.c

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

int main()
{
    // 创建socket
    int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
    if (sfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    // 设置广播属性
    int optval = 1;
    setsockopt(sfd, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval)); // 设置允许广播

    // 创建一个广播的地址
    struct sockaddr_in broadcast_addr;
    broadcast_addr.sin_family = AF_INET;
    broadcast_addr.sin_port = htons(9999);
    inet_pton(AF_INET, "192.168.57.255", &broadcast_addr.sin_addr.s_addr);  // 192.168.57.255这个IP地址中的主机ID部分全部为1,即255

    // 通信
    char buf[1024] = {0};
    int num = 0;
    while (1)
    {
        memset(buf, 0, sizeof(buf));
        sprintf(buf, "i am server, this is %dth msg.\n", ++num);

        // 发送数据
        ssize_t size = sendto(sfd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr));
        if (size == -1)
        {
            perror("sendto");
            break;
        }

        printf("广播的数据 : %s\n", buf);
        sleep(1);
    }

    close(sfd);

    return 0;
}

client.c

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

int main()
{
    // 创建socket
    int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
    if (sfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    // 客户端绑定本地的IP和端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("bind");
        exit(-1);
    }
    socklen_t len = sizeof(addr);

    char buf[1024] = {0};
    int num = 0;
    while (1)
    {
        memset(buf, 0, sizeof(buf));

        // 接收数据
        ssize_t size = recvfrom(sfd, buf, sizeof(buf), 0, NULL, NULL);
        if (size == -1)
        {
            perror("recvfrom");
            break;
        }

        printf("recv msg : %s\n", buf);
    }

    close(sfd);

    return 0;
}

8.3 组播/多播

单播地址标识单个IP端口,广播地址标识某个子网的所有IP接口,多播/组播标识一组IP接口。

单播和广播的寻址方案是两个极端,多播则在两者之间提供一种折中方案。

多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。

另外,广播一般局限于局域网内使用,多播既可以用于局域网,也可以跨广域网使用。

注意:客户端需要加入多播组,才能接收到多播的数据;

组播地址:IP多播通信必须依赖于IP多播地址,在 IPv4中范围从224.0.0.0239.255.255.255,并被划分为局部连接多播地址、预留地址和管理权限多播地址三类。

IP地址说明
234.0.0.0
~
224.0.0.255
局部链接多播地址,是为路由协议和其它用途保留的地址,路由器并不转发属于此范围的IP包
224.0.1.0
~
224.0.1.255
预留多播地址,公用组播地址,可用于internet,使用前需要申请
224.0.2.0
~
238.255.255.255
预留多播地址,用户可用组播地址,临时组地址,全网范围内有效
239.0.0.0
~
239.255.255.255
本地管理组播地址,可供组织内部使用,类似于私有IP地址,不能用于internet,可限制多播范围

设置组播使用setsockopt函数

服务器端设置多播信息时,函数参数设置:

  • 参数level设置为IPPROTO_IP
  • 参数optnam设置为IP_MULTICAST_IF;设置组播外出接口
  • 参数optval是结构体struct in_addr

客户端加入到多播组,函数参数设置:

  • 参数level设置为IPPROTO_IP
  • 参数optnam设置为IP_ADD_MEMBERSHIP,加入到多播组;
  • 参数optval是结构体struct ip_mreqn
struct ip_mreq
{
	struct in_addr imr_multiaddr; //组播的IP地址
	struct in_addr imr_interface; //加入的客服端主机IP地址,本地的IP地址
};

组播流程:

在这里插入图片描述

组播示例:

server.c

/**
* @file 1server_multi.c
* @author zoya (2314902703@qq.com)
* @brief UDP通信组播-服务端
* @version 0.1
* @@date: 2022-10-18
* 
* @copyright Copyright (c) 2022
* 
*/

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

int main()
{
    // 创建socket
    int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
    if (sfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    // 设置多播属性,设置外出接口
    struct in_addr optval;
    // 初始化多播地址
    inet_pton(AF_INET,"239.0.0.10",&optval.s_addr);
    setsockopt(sfd, IPPROTO_IP, IP_MULTICAST_IF, &optval, sizeof(optval)); // 设置组播外出接口

    // 初始化客户端地址信息
    struct sockaddr_in caddr;
    caddr.sin_family = AF_INET;
    caddr.sin_port = htons(9999);
    inet_pton(AF_INET, "239.0.0.10", &caddr.sin_addr.s_addr);

    // 通信
    char buf[1024] = {0};
    int num = 0;
    while (1)
    {
        memset(buf, 0, sizeof(buf));
        sprintf(buf, "i am server, this is %dth msg.\n", ++num);

        // 发送数据
        ssize_t size = sendto(sfd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&caddr, sizeof(caddr));
        if (size == -1)
        {
            perror("sendto");
            break;
        }

        printf("组播的数据 : %s\n", buf);
        sleep(1);
    }

    close(sfd);

    return 0;
}

client.c

/**
* @file 1client_multi.c
* @author zoya (2314902703@qq.com)
* @brief UDP通信广播-客户端
* @version 0.1
* @@date: 2022-10-18
* 
* @copyright Copyright (c) 2022
* 
*/

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

struct ip_mreq
{
struct in_addr imr_multiaddr; //多播组的IP地址
struct in_addr imr_interface; //加入的客服端主机IP地址
};

int main()
{
    // 创建socket
    int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
    if (sfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    // 客户端绑定本地的IP和端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("bind");
        exit(-1);
    }
    // 加入到多播组
    struct ip_mreq op;
    inet_pton(AF_INET,"239.0.0.10",&op.imr_multiaddr.s_addr);
    op.imr_interface.s_addr = INADDR_ANY;
    setsockopt(sfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&op,sizeof(op));

    socklen_t len = sizeof(addr);
    char buf[1024] = {0};
    int num = 0;
    while (1)
    {
        memset(buf, 0, sizeof(buf));

        // 接收数据
        ssize_t size = recvfrom(sfd, buf, sizeof(buf), 0, NULL, NULL);
        if (size == -1)
        {
            perror("recvfrom");
            break;
        }

        printf("recv msg : %s\n", buf);
    }

    close(sfd);

    return 0;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值