【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通信流程
        • 7.1.1 套接字相关函数
        • 7.1.2 TCP通信实现示例
      • 7.2 TCP通信中的三次握手和四次挥手
        • 7.2.1 TCP三次握手
        • 7.2.2 滑动窗口
        • 7.2.3 四次挥手
      • 7.3 实现并发服务器
      • 7.4 TCP状态转换
      • 7.5 端口复用 setsockopt
      • 7.6 I/O多路复用
        • 7.6.1 常见的I/O模型
        • 7.6.2 NIO中的多路复用 select/poll/epoll
      • 7.7 本地套接字
    • 八, UDP通信
      • 8.1 UDP通信流程及相关API介绍
      • 8.2 广播
      • 8.3 组播/多播


七, TCP通信

7.1 TCP通信流程

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

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

TCP通信流程:

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写_第1张图片

  • 服务器端,被动接受连接
      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 
#include 
#include 

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);
参数sockfd socket的文件描述符
参数addr 需要绑定的socket地址,封装了IP和port的信息,类型为sockaddr
参数len 参数addr结构体栈的内存大小
返回值 成功返回0
失败返回-1

listen()函数介绍:

listen() 说明
函数功能 监听服务器socket是连接
函数声明 int listen(int sockfd, int backlog);
参数sockfd socket的文件描述符
参数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 ([email protected])
 * @brief 实现TCP服务器端的通信
 * @version 0.1
 * @@date: 2022-10-09
 *
 * @copyright Copyright (c) 2022
 *
 */

#include 
#include 
#include 
#include 
#include 
#include 

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 ([email protected])
 * @brief tcp通信 客户端
 * @version 0.1
 * @@date: 2022-10-09
 *
 * @copyright Copyright (c) 2022
 *
 */

#include 
#include 
#include 
#include 
#include 

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()是,底层会进行三次握手。

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

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写_第2张图片

  • 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的紧急指针是发送端向接收端发送紧急数据的方法。

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写_第3张图片

第一次握手

  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;该操作后双方断开连接,不能发送和接收数据;

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写_第4张图片

7.3 实现并发服务器

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

解决思路1:多进程解决

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

示例代码:

client.c

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

#include 
#include 
#include 
#include 
#include 

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([email protected])
 * @brief 服务器端实现并发处理,进程实现
 * @version 0.1
 * @date 2022-10-13
 *
 * @copyright Copyright (c) 2022
 *
 */
#define _XOPEN_SOURCE 500

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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([email protected])
 * @brief 多线程实现并发服务器
 * @version 0.1
 * @date 2022-10-13
 *
 * @copyright Copyright (c) 2022
 *
 * 没有一个连接就创建一个线程,在线程中接受或发送数据
 * 主线程连接通信
 *
 */
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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状态转换发生在三次握手和四次挥手过程中。

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写_第5张图片

三次握手

  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 
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 
#include 
// 设置端口复用,也可以设置端口状态
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([email protected])
 * @brief 接收客户端的消息,并转换消息
 * @version 0.1
 * @date 2022-10-14
 *
 * @copyright Copyright (c) 2022
 *
 */

#define _XOPEN_SOURCE 500

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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 
    #include 
    #include 
    #include 
    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([email protected])
    * @brief TCP通信服务端:select实现
    * @version 0.1
    * @date 2022-10-14
    *
    * @copyright Copyright (c) 2022
    *
    */
   
   #include 
   #include 
   #include 
   #include 
   #include 
   #include 
   #include 
   #include 
   
   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 
    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 ([email protected])
   * @brief TCP通信服务端:poll实现IO多路复用
   * @version 0.1
   * @date 2022-10-17
   *
   * @copyright Copyright (c) 2022
   *
   */
  
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  
  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 
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通信流程。

本地套接字通信流程:

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写_第6张图片

  • 服务端
    • 创建监听套接字,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 ([email protected])
 * @brief 本地套接字服务端
 * @version 0.1
 * @@date: 2022-10-18
 *
 * @copyright Copyright (c) 2022
 *
 */

#include 
#include 
#include 
#include 
#include 
#include 

#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 ([email protected])
 * @brief 本地套接字通信客户端
 * @version 0.1
 * @@date: 2022-10-18
 *
 * @copyright Copyright (c) 2022
 *
 */

#include 
#include 
#include 
#include 
#include 
#include 

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通信流程如下:

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写_第7张图片

UDP通信时使用到的API有:

#include 
#include 
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 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 

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表示不允许发送广播;

    广播流程:

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写_第8张图片

广播使用示例:

server.c

#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 

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

组播流程:

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写_第9张图片

组播示例:

server.c

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

#include 
#include 
#include 
#include 
#include 

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 ([email protected])
* @brief UDP通信广播-客户端
* @version 0.1
* @@date: 2022-10-18
* 
* @copyright Copyright (c) 2022
* 
*/

#define _XOPEN_SOURCE 500
#include 
#include 
#include 
#include 
#include 

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

你可能感兴趣的:(Linux,网络,tcp/ip,udp)