4 Linux网络编程

4 Linux网络编程

4.1 网络结构模式

  • C/S结构:服务器/客户机,即 Client - Server(C/S)结构。
  • B/S结构:浏览器/服务器,即Browser/Server(B/S)结构

4.2 MAC地址、IP地址和端口

4.2.1 MAC地址

  • MAC 地址(Media Access Control Address),在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC位址。

  • 长度为 48 位(6个字节),通常表示为 12 个 16 进制数

  • 具有唯一性

  • 也被称作物理地址,但是位于数据链路层

4.2.2 IP地址

  • IP 地址(Internet Protocol Address)

  • 长度为 32 位(4个字节),通常表示点分十进制,如192.168.31.42

  • 广播地址,网络地址,地址分类(A,B,C,D,E),

  • 特殊地址:127开头的用于回路测试

  • 子网掩码:划分子网

4.2.3 端口

  • port:虚拟端口和物理端口,这里指虚拟端口。比如80 端口、21 端口、23 端口

  • 端口类型:

    • 周知端口(Well Known Ports):范围从 0 到 1023
    • 注册端口(Registered Ports):1024 到 49151
    • 动态端口 / 私有端口(Dynamic Ports / Private Ports):49152 到 65535

4.3 网络模型

4.3.1 OSI模型 七层参考模型

img

  1. 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
  2. 数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能。定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。将比特组合成字节进而组合成帧,用MAC地
    址访问介质。
  3. 网络层:进行逻辑地址寻址,在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
  4. 传输层:定义了一些传输数据的协议和端口号( WWW 端口 80 等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP 特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如 QQ 聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。
  5. 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求。
  6. 表示层:数据的表示、安全、压缩。主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等(也就是把计算机能够识别的东西转换成人能够能识别的东西(如图片、声音等)。
  7. 应用层:网络服务与最终用户的一个接口。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。

4.3.2 TCP/IP 模型

OSPF:开放最短路径选择协议,用于路由交换,工作在网络层
img

  1. 应用层:应用层是 TCP/IP 协议的第一层,是直接为应用进程提供服务的。
    1. 对不同种类的应用程序它们会根据自己的需要来使用应用层的不同协议,邮件传输应用使用了 SMTP 协议、万维网应用使用了 HTTP 协议、远程登录服务应用使用了有 TELNET 协议。
    2. 应用层还能加密、解密、格式化数据。
    3. 应用层可以建立或解除与其他节点的联系,这样可以充分节省网络资源。
  2. 传输层:作为 TCP/IP 协议的第二层,运输层在整个 TCP/IP 协议中起到了中流砥柱的作用。且在运输层中, TCP 和 UDP 也同样起到了中流砥柱的作用。
  3. 网络层:网络层在 TCP/IP 协议中的位于第三层。在 TCP/IP 协议中网络层可以进行网络连接的建立和终止以及 IP 地址的寻找等功能。
  4. 网络接口层:在 TCP/IP 协议中,网络接口层位于第四层。由于网络接口层兼并了物理层和数据链路层所以,网络接口层既是传输数据的物理媒介,也可以为网络层提供一条准确无误的线路。

4.4 协议

协议(protocol),网络协议的简称

4.4.1 常见协议

  • 应用层常见的协议有:FTP协议(File Transfer Protocol 文件传输协议)、HTTP协议(Hyper Text Transfer Protocol 超文本传输协议)、NFS(Network File System 网络文件系统)。
  • 传输层常见协议有:TCP协议(Transmission Control Protocol 传输控制协议)、UDP协议(UserDatagram Protocol 用户数据报协议)。
  • 网络层常见协议有:IP 协议(Internet Protocol 因特网互联协议)、ICMP 协议(Internet ControlMessage Protocol 因特网控制报文协议)、IGMP 协议(Internet Group Management Protocol 因特网组管理协议)。
  • 网络接口层常见协议有:ARP协议(Address Resolution Protocol 地址解析协议)、RARP协议(Reverse Address Resolution Protocol 反向地址解析协议)。

4.5 网络通信的过程

img

当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次处理帧中本层负责的头部数据,以获取所需的信息,并最终将处理后的帧交给目标应用程序。这个过程称为分用(demultiplexing)。分用是依靠头部信息中的类型字段实现的

img

一次传输过程:

img

img

ARP协议:通过IP地址查找MAC地址

img

4.6 socket介绍

所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。

4.6.1 字节序

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)

字节序:

  • 字节序:字节在内存中存储的顺序。
  • 小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
  • 大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址

4.6.2 字节序转换函数

  • 发送端总是把要发送的数据转换成大端字节序数据后再发送

  • 接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。

  • 网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而
    可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。

  • BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。转换函数名称的含义如下:

    • h - host 主机,主机字节序
    • to - 转换成什么
    • n - network 网络字节序
    • s - short unsigned short
    • l - long unsigned int
#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 主机字节序 - 网络字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 主机字节序 - 网络字节序

4.6.3 socket 地址

  • 通用 socket 地址

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下

ipv4

#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
typedef unsigned short int sa_family_t;

ipv6

#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;

  • 专用 socket 地址

很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型

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

4.6.4 IP地址转换(字符串ip-整数 ,主机、网络字节序的转换)

旧的函数,只能针对ipv4并且是静态

#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);

更新后的函数:ipv4和ipv6都可用并且是动态更新

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

4.6.5 TCP通信流程

  • UDP:用户数据报协议,面向无连接,可以单播,多播,广播, 面向数据报,不可靠
  • TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输

TCP和UDP的区别

区别 UDP TCP
是否创建连接 无连接 面向连接
是否可靠 不可靠 可靠的
连接的对象个数 一对一、一对多、多对一、多对多 支持一对一
传输的方式 面向数据报 面向字节流
首部开销 8个字节 最少20个字节
适用场景 实时应用(视频会议,直播) 可靠性高的应用(文件传输)
  • TCP通信的流程
  1. 服务器端(被动接受连接的角色)

    1. 创建一个用于监听的套接字
      • 监听:监听有客户端的连接
      • 套接字:这个套接字其实就是一个文件描述符
    2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
      • 客户端连接服务器的时候使用的就是这个IP和端口
    3. 设置监听,监听的fd开始工作
    4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
    5. 通信
      • 接收数据
      • 发送数据
    6. 通信结束,断开连接
  2. 客户端

    1. 创建一个用于通信的套接字(fd)
    2. 连接服务器,需要指定连接的服务器的 IP 和 端口
    3. 连接成功了,客户端可以直接和服务器通信
      • 接收数据
      • 发送数据
    4. 通信结束,断开连接

4.6.6 套接字函数

  • int socket(int domain, int type, int protocol);
    • 功能:创建一个套接字
    • 参数:
      • domain: 协议族
        • AF_INET : ipv4
        • AF_INET6 : ipv6
        • AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
      • type: 通信过程中使用的协议类型
        SOCK_STREAM : 流式协议
        SOCK_DGRAM : 报式协议
      • protocol : 具体的一个协议。一般写0
        • SOCK_STREAM : 流式协议默认使用 TCP
        • SOCK_DGRAM : 报式协议默认使用 UDP
      • 返回值:
        • 成功:返回文件描述符,操作的就是内核缓冲区。
        • 失败:-1
  • int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名
    • 功能:绑定,将fd 和本地的IP + 端口进行绑定
    • 参数:
      • sockfd : 通过socket函数得到的文件描述符
      • addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
      • addrlen : 第二个参数结构体占的内存大小
  • int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
    • 功能:监听这个socket上的连接
    • 参数:
      • sockfd : 通过socket()函数得到的文件描述符
      • backlog : 未连接的和已经连接的和的最大值, 5
  • int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    • 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
    • 参数:
      • sockfd : 用于监听的文件描述符
      • addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
      • addrlen : 指定第二个参数的对应的内存大小
    • 返回值:
      • 成功 :用于通信的文件描述符
      • -1 : 失败
  • int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    • 功能: 客户端连接服务器
    • 参数:
      • sockfd : 用于通信的文件描述符
      • addr : 客户端要连接的服务器的地址信息
      • addrlen : 第二个参数的内存大小
    • 返回值:成功 0, 失败 -1
  • ssize_t write(int fd, const void *buf, size_t count); // 写数据
  • ssize_t read(int fd, void *buf, size_t count); // 读数据

示例代码:
server.c

// TCP 通信的服务器端

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

int main() {

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

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

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

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

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

    // 4.接收客户端连接
    struct sockaddr_in clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
    
    if(cfd == -1) {
        perror("accept");
        exit(-1);
    }

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

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

        char * data = "hello,i am server";
        // 给客户端发送数据
        write(cfd, data, strlen(data));
    }
   
    // 关闭文件描述符
    close(cfd);
    close(lfd);

    return 0;
}

client.c

// TCP通信的客户端

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

int main() {

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

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

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

    
    // 3. 通信
    char recvBuf[1024] = {0};
    while(1) {

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

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

    }

    // 关闭连接
    close(fd);

    return 0;
}

4.6.7 TCP 三次握手

  • 目的是为了保证双方互相之间建立了连接

  • 具体流程:

    1. 第一次握手:
      1. 客户端将SYM标志位置为1
      2. 生成一个随机的32位的序号seq=J,这个序号后边是可以携带数据(数据的大小)
    2. 第二次握手:
      1. 服务器端接收客户端的连接:ACK=1
      2. 服务器会回发-一个确认序号:ack=客户端的序号+数据长度+ SYN/FIN(按一个字节算)
      3. 服务器端会向客户端发起连接请求: SYN=1
      4. 服务器会生成一个随机序号: seq = K
    3. 第三次握手:
      1. 客户单应答服务器的连接请求: ACK=1
      2. 客户端回复收到了服务器端的数据: ack=服务端的序号+数据长度+ SYN/FIN(按一个字节算)

4.6.8 TCP 滑动窗口

4.6.9 TCP 四次挥手

4.6.10 TCP 通信并发

4.6.11 TCP 状态转换

img

img

4.6.12 端口复用

端口复用最常用的用途是:

  • 防止服务器重启时之前绑定的端口还未释放

  • 程序突然退出而系统没有释放端口

  • 设置套接字的属性(不仅仅能设置端口复用):
    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_toptlen)

  • 参数:

    • sockfd :要操作的文件描述符
    • leve1 :级别一SOL_ SOCKET (端口复用的级别)
    • optname :选项的名称
      • SO_ REUSEADDR
      • SO_ REUSEPORT
    • optval :端口复用的值(整形)
      • 1:可以复用
      • 0:不可以复用
    • optlen : optva1参数的大小
  • 端口复用,设置的时机是在服务器绑定端口之前

setsockopt();
bind();

4.7 I/O多路复用(I/O多路转接)

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

4.7.1 select

主旨思想:

  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
  2. 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回。
    1. 这个函数是阻塞
    2. 函数对文件描述符的检测的操作是由内核完成的
  3. 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

  • 参数:
    • nfds : 委托内核检测的最大文件描述符的值 + 1
    • readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
      • 一般检测读操作
      • 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
      • 是一个传入传出参数
    • writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
      • 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
    • exceptfds : 检测发生异常的文件描述符的集合
    • timeout : 设置的超时时间
      struct timeval {
          long tv_sec; /* seconds */
          long tv_usec; /* microseconds */
      };
      
      • NULL : 永久阻塞,直到检测到了文件描述符有变化
      • tv_sec = 0 tv_usec = 0, 不阻塞
      • tv_sec > 0 tv_usec > 0, 阻塞对应的时间
  • 返回值 :
    • -1 : 失败
    • >0比如(n) : 检测的集合中有n个文件描述符发生了变化
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);

select的缺点:

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

4.7.2 poll

struct pollfd {
    int fd; /* 委托内核检测的文件描述符 */
    short events; /* 委托内核检测文件描述符的什么事件 */
    short revents; /* 文件描述符实际发生的事件 */
};
//举例
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
  • int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    • 参数:
      • fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
      • nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
      • timeout : 阻塞时长
        0 : 不阻塞
        -1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
        >0 : 阻塞的时长
    • 返回值:
      -1 : 失败
      >0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化

img

4.7.3 epoll

  • #include <sys/epoll.h>

创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。

  • int epoll_create(int size);
    • 参数:
      size : 目前没有意义了。随便写一个数,必须大于0
    • 返回值:
      -1 : 失败

    0 : 文件描述符,操作epoll实例的

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
struct epoll_event {
    uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

常见的Epoll检测事件:

  • EPOLLIN

  • EPOLLOUT

  • EPOLLERR

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

    • 参数:
      • epfd : epoll实例对应的文件描述符
      • op : 要进行什么操作
        EPOLL_CTL_ADD: 添加
        EPOLL_CTL_MOD: 修改
        EPOLL_CTL_DEL: 删除
      • fd : 要检测的文件描述符
      • event : 检测文件描述符什么事情
  • int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

    • 参数:
      • epfd : epoll实例对应的文件描述符
      • events : 传出参数,保存了发送了变化的文件描述符的信息
      • maxevents : 第二个参数结构体数组的大小
      • timeout : 阻塞时间
        • 0 : 不阻塞
        • -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
        • > 0 : 阻塞的时长(毫秒)
    • 返回值:
      • 成功,返回发送变化的文件描述符的个数 > 0
      • 失败 -1

4.7.4 Epoll 的工作模式:

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

LT(level - triggered)是缺省的工作方式,并且同时支持 block 和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。

  • ET 模式(边沿触发)
    • 假设委托内核检测读事件 -> 检测fd的读缓冲区
    • 读缓冲区有数据 - > epoll检测到了会给用户通知
      1. 用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
      2. 用户只读了一部分数据,epoll不通知
      3. 缓冲区的数据读完了,不通知

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

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

struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

常见的Epoll检测事件:

  • EPOLLIN
  • EPOLLOUT
  • EPOLLERR
  • EPOLLET

4.8 UDP通信和本地套接字

4.8.1 UDP通信

img

需要用到的头文件

#include <sys/types.h>
#include <sys/socket.h>
  • ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
    • 参数:
      • sockfd : 通信的fd
      • buf : 要发送的数据
      • len : 发送数据的长度
      • flags : 0
      • dest_addr : 通信的另外一端的地址信息
      • addrlen : 地址的内存大小
  • ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
    • 参数:
      • sockfd : 通信的fd
      • buf : 接收数据的数组
      • len : 数组的大小
      • flags : 0
      • src_addr : 用来保存另外一端的地址信息,不需要可以指定为NULL
      • addrlen : 地址的内存大小

4.8.2 广播

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

    1. 只能在局域网中使用。
    2. 客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。
  • int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);

    • sockfd : 文件描述符
    • level : SOL_SOCKET
    • optname : SO_BROADCAST
    • optval : int类型的值,为1表示允许广播
    • optlen : optval的大小

4.8.3 组播(多播)

单播地址标识单个 IP 接口,广播地址标识某个子网的所有 IP 接口,多播地址标识一组 IP 接口。单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折中方案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。另外,广播一般局限于局域网内使用,而多播则既可以用于局域网,也可以跨广域网使用。

  1. 组播既可以用于局域网,也可以用于广域网
  2. 客户端需要加入多播组,才能接收到多播的数据

组播地址
IP 多播通信必须依赖于 IP 多播地址,在 IPv4 中它的范围从 224.0.0.0 到 239.255.255.255 ,并被划分为局部链接多播地址、预留多播地址和管理权限多播地址三类

设置组播

  • int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);\
    • 服务器设置多播的信息,外出接口
      • level : IPPROTO_IP
      • optname : IP_MULTICAST_IF
      • optval : struct in_addr
    • 客户端加入到多播组:
      • level : IPPROTO_IP
      • optname : IP_ADD_MEMBERSHIP
      • optval : struct ip_mreq
struct ip_mreq
{
/* IP multicast address of group. */
struct in_addr imr_multiaddr; // 组播的IP地址
/* Local IP address of interface. */
struct in_addr imr_interface; // 本地的IP地址
};
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};

4.8.4 本地套接字

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

img

// 本地套接字通信的流程 - tcp
// 服务器端
1. 创建监听的套接字
int lfd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0);
2. 监听的套接字绑定本地的套接字文件 -> server端
struct sockaddr_un addr;
// 绑定成功之后,指定的sun_path中的套接字文件会自动生成。
bind(lfd, addr, len);
3. 监听
listen(lfd, 100);
4. 等待并接受连接请求
struct sockaddr_un cliaddr;
int cfd = accept(lfd, &cliaddr, len);
5. 通信
接收数据:read/recv
发送数据:write/send
6. 关闭连接
close();
// 客户端的流程
1. 创建通信的套接字
int fd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0);
2. 监听的套接字绑定本地的IP 端口
struct sockaddr_un addr;
// 绑定成功之后,指定的sun_path中的套接字文件会自动生成。
bind(lfd, addr, len);
3. 连接服务器
struct sockaddr_un serveraddr;
connect(fd, &serveraddr, sizeof(serveraddr));
4. 通信
接收数据:read/recv
发送数据:write/send
5. 关闭连接
close();
// 头文件: sys/un.h
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; // 地址族协议 af_local
char sun_path[UNIX_PATH_MAX]; // 套接字文件的路径, 这是一个伪文件, 大小永远=0
};
posted @   mobbu  阅读(69)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示