Linux网络编程(二):Socket 编程

news2024/9/25 17:11:26

参考引用

  • 黑马程序员-Linux 网络编程

1. 套接字概念

  • Socket 本身有 “插座” 的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型

    • 本质为内核借助缓冲区形成的伪文件
  • 既然是文件,那么可以使用文件描述符引用套接字

    • 与管道类似,Linux 系统将其封装成文件是为了统一接口,使得读写套接字和读写文件的操作一致
    • 区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递
  • 在 TCP/IP 协议中,“IP 地址 + TCP 或 UDP 端口号” 唯一标识网络通信中的一个进程

    • “IP 地址 + 端口号” 就对应一个 socket
    • 欲建立连接的两个进程各自有一个 socket 来标识,那么这两个 socket 组成的 socket pair 就唯一标识一个连接,因此可以用 socket 来描述网络连接的一对一关系
  • 套接字通信原理如下图所示

在这里插入图片描述

  • 在网络通信中,套接字一定是成对出现的
    • 一个文件描述符指向一个套接字 (该套接字内部由内核借助两个缓冲区实现)

在这里插入图片描述

2. 预备知识

2.1 网络字节序

  • 内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?

    • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
  • TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节,而发送主机可能是小端字节序,也可能是大端字节序。为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换(PC 机采用小端法,网络采用大端法)

    • 如果主机是小端字节序,这些函数将参数做相应的大小端转换后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
    #include <arpa/inet.h>
    
    // h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数
    uint32_t htonl(uint32_t hostlong);    // 本地转网络(IP)
    uint16_t htons(uint16_t hostshort);   // 本地转网络(port) 
    uint32_t ntohl(uint32_t netlong);     // 网络转本地(IP)
    uint16_t ntohs(uint16_t netshort);    // 网络转本地(port)
    

2.2 IP 地址转换函数

#include <arpa/inet.h>

// 本地字节序 (string 形式的 IP)---> 网络字节序(二进制形式的 IP)
    // af:AF_INET(ipv4)、AF_INET6(ipv6)
    // src:传入参数,本地字节序 IP 地址(点分十进制)
    // dst:传出参数,转换后的网络字节序的 IP 地址
    // 返回值:成功 1;异常 0,说明 src 指向的不是一个有效的 IP 地址;失败 -1
int inet_pton(int af, const char *src, void *dst);

// 网络字节序(二进制形式的 IP)---> 本地字节序 (string 形式的 IP)
    // af:AF_INET(ipv4)、AF_INET6(ipv6)
    // src:传入参数,网络字节序 IP 地址
    // dst:传出参数,转换后的本地字节序(string IP)
    // size:dst 的大小
    // 返回值:成功,返回 dst;失败,返回 NULL
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
// inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6 的 in6_addr

2.3 sockaddr 数据结构

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

在这里插入图片描述

// man 7 ip 命令查看下列定义
// strcut sockaddr 已过时,现在统一使用 struct sockaddr_in
struct sockaddr_in {
    __kernel_sa_family_t sin_family;  /* Address family 地址结构类型 */  	
    __be16 sin_port;                  /* Port number 端口号 */		
    struct in_addr sin_addr;          /* Internet address IP 地址 */	
    
        /* Pad to size of `struct sockaddr'. */
    unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
    sizeof(unsigned short int) - sizeof(struct in_addr)];
};
  • IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括 16 位端口号和 32 位 IP 地址,IPv6 地址用 sockaddr_in6 结构体表示,包括 16 位端口号、128 位 IP 地址
    struct sockaddr_in addr;
    addr.sin_family = AF_INET/AF_INET6/AF_UNIX;  // AF_UNIX 用于本地套接字
    addr.sin_port = htons(9527);
    
    int dst;
    inet_pton(AF_INET, "192.157.22.45", (void*)&dst);
    addr.sin_addr.s_addr = dst;
    
    addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 取出系统中有效的任意 IP 地址,二进制类型
    
    bind(fd, (struct sockaddr *)&addr, size);
    

3. 网络套接字函数

3.1 socket 模型创建流程图

  • 1 个客户端和 1 个服务器端进行通信:一共有 3 个套接字(一对用于通信的套接字,服务器单独还有一个用于监听的套接字)
    在这里插入图片描述

3.2 socket 函数

// 创建一个套接字
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
  • domain
    • AF_INET 这是大多数用来产生 socket 的协议,使用 TCP 或 UDP 来传输,用 IPv4 的地址
    • AF_INET6 与上面类似,不过是用 IPv6 的地址
  • type
    • SOCK_STREAM(流式) 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的 socket 类型,这个 socket 使用 TCP 来进行传输
    • SOCK_DGRAM(报式) 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用 UDP 来进行它的连接
  • protocol
    • 传 0 表示使用默认协议
  • 返回值
    • 成功:返回新套接字(socket)对应的文件描述符
    • 失败:返回 -1,设置 errno

socket() 打开一个网络通讯端口,如果成功的话,就像 open() 一样返回一个文件描述符,应用程序可以像读写文件一样用 read/write 在网络上收发数据,如果 socket() 调用出错则返回 -1

3.3 bind 函数

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用 bind 绑定一个固定的网络地址(IP)和端口号(port)到套接字

    #include <sys/types.h>
    #include <sys/socket.h>
    
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
  • sockfd

    • 指定待绑定的 socket(用于监听的套接字) 文件描述符
  • addr

    • 构造出 IP 地址加端口号
    • 类型是 struct sockaddr *
    /*
        struct sockaddr_in {
            sa_family_t    sin_family; // address family: AF_INET
            in_port_t      sin_port;   // port in network byte order
            struct in_addr sin_addr;   // internet address
        };
        // internet address
        struct in_addr {
            uint32_t       s_addr;     // address in network byte order
        };
    
    */
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    // 因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址,这样设置可以在所有的 IP 地址上监听
    // 直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址
    addr.sin_addr.s_addr = htonl(INADDR_ANY);  // INADDR_ANY 宏:表示取出系统中有效的任意 IP 地址,二进制类
    (struct sockaddr *)&addr // addr 类型为 struct sockaddr*,故此处需要强制转换
    
  • addrlen

    • sizeof(addr) 结构体长度
  • 返回值

    • 成功:返回 0
    • 失败:返回 -1, 设置 errno

3.4 listen 函数

  • listen 函数用于设置同时与服务器建立连接的(监听)上限数 (同时进行 3 次握手的客户端数量)

    #include <sys/types.h>
    #include <sys/socket.h>
    
    int listen(int sockfd, int backlog);
    
  • sockfd

    • socket 对应的文件描述符
  • backlog

    • 排队建立 3 次握手队列和刚刚建立 3 次握手队列的链接数和
    • 最大值 128
  • 典型的服务器程序可以同时服务于多个客户端

    • 当有客户端发起连接时,服务器调用的 accept() 返回并接受这个连接
    • 如果有大量的客户端发起连接而服务器来不及处理,尚未 accept 的客户端就处于连接等待状态
    • listen() 声明 sockfd 处于监听状态,并且最多允许有 backlog 个客户端处于连接等待状态,如果接收到更多的连接请求就忽略
    • listen() 成功返回 0,失败返回 -1

3.5 accept 函数

  • accept 函数用于阻塞(监听)等待客户端建立连接
    • 三方握手完成后,服务器调用 accept() 接受连接,如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来
    #include <sys/types.h>
    #include <sys/socket.h>
    
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
  • sockdf
    • socket 文件描述符,socket 函数返回值
  • addr
    • 传出参数,返回成功与服务器建立连接的那个客户端的地址结构(含 IP 地址和端口号)
    • 用于和客户端建立连接的套接字
  • addrlen
    • 传入传出参数
      • 传入:sizeof(addr) 大小
      • 传出:客户端 addr 实际大小
  • 返回值
    • 成功:返回一个新的用于和客户端通信的 socket 对应的文件描述符
    • 失败:返回 -1,设置 errno

3.6 connect 函数

  • connect 函数作用:使用现有的 socket 与服务器建立连接
    • connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址
    #include <sys/types.h>
    #include <sys/socket.h>
    
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
  • sockdf
    • socket 文件描述符
  • addr
    • 传入参数,指定服务器端地址结构(含 IP 地址和端口号)
  • addrlen
    • 传入参数,传入 sizeof(addr) 服务器端地址结构的长度
  • 返回值
    • 成功:返回 0
    • 失败:返回 -1,设置 errno

如果不使用 bind 绑定客户端地址结构,则系统默认采用 “隐式绑定”

4. C/S 模型的 TCP 通信实现

4.1 实现分析

在这里插入图片描述

  • 服务器调用 socket()、bind()、listen() 完成初始化后,调用 accept() 阻塞等待,处于监听端口的状态,客户端调用 socket() 初始化后,调用 connect() 发出 SYN 段并阻塞等待服务器应答,服务器应答一个 SYN-ACK 段,客户端收到后从 connect() 返回,同时应答一个 ACK 段,服务器收到后从 accept() 返回

4.2 server 实现

  • 从客户端读字符,然后将每个字符转换为大写并回送给客户端
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define SERV_PORT 9527

void sys_err(const char *str) {
    perror(str);
    exit(1);
}

int main(int argc, char *argv[]) {
    int lfd = 0, cfd = 0;
    int ret, i;
    char buf[BUFSIZ], client_IP[1024];

    struct sockaddr_in serv_addr, clit_addr;         // 定义 服务器地址结构 和 客户端地址结构
    socklen_t clit_addr_len;              // 客户端地址结构大小

    /*
        struct sockaddr_in {
            sa_family_t    sin_family; // address family: AF_INET
            in_port_t      sin_port;   // port in network byte order
            struct in_addr sin_addr;   // internet address
        };
        // internet address
        struct in_addr {
            uint32_t       s_addr;     // address in network byte order
        };

    */
    serv_addr.sin_family = AF_INET;                  // IPv4
    serv_addr.sin_port = htons(SERV_PORT);           // 转为网络字节序的 端口号
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);   // 获取本机任意有效 IP

    lfd = socket(AF_INET, SOCK_STREAM, 0);           // 创建一个 socket
    if (lfd == -1) {
        sys_err("socket error");
    }

    bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); // 给服务器 socket 绑定地址结构(IP+port)

    listen(lfd, 128);                         // 设置监听上限

    clit_addr_len = sizeof(clit_addr);        // clit_addr_len为传入传出参数,此处获取客户端地址结构大小然后传入 accept
    cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len); // 阻塞等待客户端连接请求
    if (cfd == -1)
        sys_err("accept error");

    printf("client ip:%s port:%d\n", 
            inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)), 
            ntohs(clit_addr.sin_port));       // 根据 accept 传出参数,获取客户端 ip 和 port

    while (1) {
        ret = read(cfd, buf, sizeof(buf));    // 读客户端数据
        write(STDOUT_FILENO, buf, ret);       // 写到屏幕查看

        for (i = 0; i < ret; i++)             // 小写 -- 大写
            buf[i] = toupper(buf[i]);

        write(cfd, buf, ret);                 // 将大写,写回给客户端
    }

    close(lfd);
    close(cfd);

    return 0;
}

4.3 client 实现

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

#define SERV_PORT 9527

void sys_err(const char *str) {
    perror(str);
    exit(1);
}

int main(int argc, char *argv[]) {
    int cfd;
    int conter = 10;
    char buf[BUFSIZ];
    
    struct sockaddr_in serv_addr;          // 服务器地址结构

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);

    cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd == -1)
        sys_err("socket error");

    int ret = connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    if (ret != 0)
        sys_err("connect err");

    while (--conter) {
        write(cfd, "hello\n", 6);
        ret = read(cfd, buf, sizeof(buf));
        write(STDOUT_FILENO, buf, ret);
        sleep(1);
    }

    close(cfd);

    return 0;
}
# 测试例程
$ gcc server.c -o server
$ ./server
client ip : 127.0.0.1 port : 46914
hello
hello
hello
...
# 另开一个终端
$ gcc client.c -o client
$ ./client
HELLO
HELLO
HELLO
...

5. 出错处理封装函数

  • 系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。为使错误处理的代码不影响主程序的可读性,把与 socket 相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块 wrap.c

在这里插入图片描述

  • wrap.c
#include "wrap.h"

void perr_exit(const char *s) {
    perror(s);
    exit(-1);
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) {
    int n;

again:
    if ((n = accept(fd, sa, salenptr)) < 0) {
        if ((errno == ECONNABORTED) || (errno == EINTR))
            goto again;
        else
            perr_exit("accept error");
    }
    return n;
}

int Bind(int fd, const struct sockaddr *sa, socklen_t salen) {
    int n;
    
    if ((n = bind(fd, sa, salen)) < 0)
        perr_exit("bind error");
    
    return n;
}

int Connect(int fd, const struct sockaddr *sa, socklen_t salen) {
    int n;
    n = connect(fd, sa, salen);
    if (n < 0) {
        perr_exit("connect error");
    }
    
    return n;
}

int Listen(int fd, int backlog) {
    int n;
    
    if ((n = listen(fd, backlog)) < 0)
        perr_exit("listen error");
    
    return n;
}

int Socket(int family, int type, int protocol) {
    int n;
    
    if ((n = socket(family, type, protocol)) < 0)
        perr_exit("socket error");
    
    return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes) {
    ssize_t n;

again:
    if ( (n = read(fd, ptr, nbytes)) == -1) {
        if (errno == EINTR)
            goto again;
        else
            return -1;
    }
    
    return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes) {
    ssize_t n;

again:
    if ((n = write(fd, ptr, nbytes)) == -1) {
        if (errno == EINTR)
            goto again;
        else
            return -1;
    }
    return n;
}

int Close(int fd) {
    int n;
    if ((n = close(fd)) == -1)
        perr_exit("close error");
    
    return n;
}

/*参三: 应该读取的字节数*/                          //socket 4096  readn(cfd, buf, 4096)   nleft = 4096-1500
ssize_t Readn(int fd, void *vptr, size_t n) {
    size_t  nleft;              //usigned int 剩余未读取的字节数
    ssize_t nread;              //int 实际读到的字节数
    char   *ptr;
    
    ptr = vptr;
    nleft = n;                  //n 未读取字节数
    
    while (nleft > 0) {
        if ((nread = read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)
                nread = 0;
            else
                return -1;
        } else if (nread == 0)
            break;
    
        nleft -= nread;   //nleft = nleft - nread 
        ptr += nread;
    }
    return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n) {
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
    
    ptr = vptr;
    nleft = n;
    while (nleft > 0) {
        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
            if (nwritten < 0 && errno == EINTR)
                nwritten = 0;
            else
                return -1;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}

static ssize_t my_read(int fd, char *ptr) {
    static int read_cnt;
    static char *read_ptr;
    static char read_buf[100];
    
    if (read_cnt <= 0) {
again:
        if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {   //"hello\n"
            if (errno == EINTR)
                goto again;
            return -1;
        } else if (read_cnt == 0)
            return 0;
    
        read_ptr = read_buf;
    }
    read_cnt--;
    *ptr = *read_ptr++;
    
    return 1;
}

/*readline --- fgets*/    
//传出参数 vptr
ssize_t Readline(int fd, void *vptr, size_t maxlen) {
    ssize_t n, rc;
    char c, *ptr;
    ptr = vptr;
    
    for (n = 1; n < maxlen; n++) {
        if ((rc = my_read(fd, &c)) == 1) {   //ptr[] = hello\n
            *ptr++ = c;
            if (c == '\n')
                break;
        } else if (rc == 0) {
            *ptr = 0;
            return n-1;
        } else
            return -1;
    }
    *ptr = 0;
    
    return n;
}
  • wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_

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

void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);

#endif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1324136.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CGAL中流线的二维放置

本章介绍CGAL 2D流线放置包。定义一节给出了基本定义和概念。基本概念一节对整合过程进行了描述。最远点播种策略一节简要介绍了该算法。“实现”一节介绍了包的实现&#xff0c;“示例”一节详细介绍了两个示例放置。 该算法的核心思想是对域中最大空腔中心的流线进行积分&am…

HuggingFace下载模型

目录 方式一&#xff1a;网页下载 方式二&#xff1a;Git下载 方式一&#xff1a;网页下载 方式二&#xff1a;Git下载 有些模型的使用方法页面会写git clone的地址&#xff0c;有些没写&#xff0c;直接复制网页地址即可 网页地址&#xff1a; ​https://huggingface.co/…

12.19_黑马数据结构与算法笔记Java

目录 203 排序算法 选择排序 204 排序算法 堆排序 205 排序算法 插入排序 206 排序算法 希尔排序 207 排序算法 归并排序 自顶至下 208 排序算法 归并排序 自下至上 209 排序算法 归并加插入 210 排序算法 单边快排 211 排序算法 双边快排 212 排序算法 快排 随机基准…

技术博客:市面上加密混淆软件的比较和推荐

引言 市面上有许多加密混淆软件可供开发者使用&#xff0c;但哪些软件是最好用的&#xff1f;哪些软件受到开发者的喜爱&#xff1f;本文将根据一次在CSDN上的投票结果&#xff0c;为大家介绍几款在程序员中普及度较高的加密软件。以下是投票结果&#xff0c;希望能对大家的选…

【jvm从入门到实战】(十) 实战篇-内存调优

内存溢出和内存泄漏&#xff1a;在Java中如果不再使用一个对象&#xff0c;但是该对象依然在GC ROOT的引用链上&#xff0c;这个对象就不会被垃圾回收器回收&#xff0c;这种情况就称之为内存泄漏。内存泄漏绝大多数情况都是由堆内存泄漏引起的。少量的内存泄漏可以容忍&#x…

MySQL5.x与8.0

大致区别 1. 性能&#xff1a;MySQL 8.0 的速度要比 MySQL 5.7 快 2 倍 MySQL 8.0 在以下方面带来了更好的性能&#xff1a;读/写工作负载、IO 密集型工作负载、以及高竞争&#xff08;"hot spot"热点竞争问题&#xff09;工作负载2. NoSQL&#xff1a;MySQL 从 5.7 …

CPU算力分配 - 华为OD统一考试

OD统一考试 题解: Java / Python / C++ 题目描述 现有两组服务器A和B,每组有多个算力不同的CPU,其中 A 是A组第个CPU的运算能力,是 B组 第个CPU的运算能力。一组服务器的总算力是各CPU的算力之和。 为了让两组服务器的算力相等,允许从每组各选出一个CPU进行一次交换。 求…

基于PHP的蛋糕购物商城系统

有需要请加文章底部Q哦 可远程调试 基于PHP的蛋糕购物商城系统 一 介绍 此蛋糕购物商城基于原生PHP开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统角色分为用户和管理员。 技术栈&#xff1a;phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册/登录/注销…

做一个wiki页面是体验HTML语义的好方法

HTML语义&#xff1a;如何运用语义类标签来呈现Wiki网页 在上一篇文章中&#xff0c;我花了大量的篇幅和你解释了正确使用语义类标签的好处和一些场景。那么&#xff0c;哪些场景适合用到语义类标签呢&#xff0c;又如何运用语义类标签呢&#xff1f; 不知道你还记不记得在大…

爱芯派pro通过无线网卡rtl8188eu连接热点

爱芯派pro通过无线网卡rtl8188eu连接热点 爱芯派pro目前的底板的pcie的复位有问题&#xff0c;所以pcie接口无法挂载上去&#xff0c;所以自己购买的rtl8822网卡也用不了&#xff0c;然后想起来自己还有正点原子的rtl8188eu网卡&#xff0c;但是没有和工作人员进行摸索后才知道…

Swagger升级指南:Swagger2与Swagger3注解差异揭秘

在API开发的世界里&#xff0c;Swagger已经成为了一个不可或缺的工具&#xff0c;它让API的文档化和前后端的协作变得前所未有地简单。随着Swagger的进化&#xff0c;我们迎来了Swagger3&#xff0c;也被称为OpenAPI Specification 3.0。本篇博客将带大家深入了解Swagger2和Swa…

【Python 基础】-- 在 mac OS 中安装 多个 python 版本

目录 1、需求 2、实现 2.1 安装 pyenv 2.2 安装 pyenv-virtualenv 2.3 配置环境变量 2.4 创建 python 3.9.9 的环境 2.5 激活环境&#xff0c;在当前项目目录中使用&#xff0c;即执行 python 1、需求 由于项目所依赖的 python 版本有多个&#xff0c;需要在不同的 pyth…

在线客服系统中的全渠道服务:多渠道整合与无缝沟通体验

很多采购人员在了解在线客服系统的时候都会遇到一个名词——全渠道。很多人第一次接触可能并不理解它是什么意思&#xff0c;也不知道自己的企业是否需要这个”全渠道“。今天这篇文章就为大家解答一二。 一、全渠道是什么&#xff1f; 全渠道 (Omni-Channel)&#xff0c;就是…

DeepLabV3+实现sar影像海面溢油区识别

今天我们分享DeepLabV3的sai影像水体提取。 数据集 本次使用的数据集是Deep-SAR Oil Spill (SOS) dataset。该数据集由中国地质大学的朱祺琪团队制作并共享。该数据集包含墨西哥湾溢油区域和波斯湾溢油区域&#xff0c;分别获取于ALOS 和Sentinel-1A卫星。由ECHO研究组搜集制作…

说说 Spring Boot 实现接口幂等性有哪几种方案?

一、什么是幂等性 幂等是一个数学与计算机学概念&#xff0c;在数学中某一元运算为幂等时&#xff0c;其作用在任一元素两次后会和其作用一次的结果相同。 在计算机中编程中&#xff0c;一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数或幂等…

【go语言】error错误机制及自定义错误返回类型

简介 Go 语言通过内置的 error 接口来处理错误&#xff0c;该接口定义如下&#xff1a; type error interface {Error() string }这意味着任何实现了 Error() 方法的类型都可以作为错误类型。在 Go 中&#xff0c;通常使用 errors 包的 New 函数来创建简单的错误&#xff1a;…

三维尺寸中,您需要了解的设备及其特点

三维尺寸测量需要用到一些精密仪器&#xff0c;它们都有各自的特点。那么三维尺寸测量中常用的设备有哪些&#xff1f; 1、三坐标测量机 三坐标测量机即三坐标测量计算机数控系统&#xff0c;是一种高精度的测量设备。除了传统的点、线、面和圆柱体等基本轮廓外&#xff0c;还可…

车辆违规实线变道检测系统:融合Gold-YOLO改进YOLOv8

1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 研究背景与意义 随着城市化进程的加快和交通工具的普及&#xff0c;道路交通安全问题日益凸显。其中&#xff0c;车辆违规实线变道是导致交通事故的重要原因之一。在道路上&…

游戏引擎?

游戏引擎是指一些已编写好的可编辑电脑游戏系统或者一些交互式实时图像应用程序的核心组件。这些系统为游戏设计者提供各种编写游戏所需的各种工具&#xff0c;其目的在于让游戏设计者能容易和快速地做出游戏程式而不用由零开始。大部分都支持多种操作平台&#xff0c;如Linux、…

【STM32工具篇】使用CLion开发STM32

本文主要记录使用CLion开发STM32&#xff0c;并调试相关功能 使用的CLion版本&#xff1a;2023.3.1 CLion嵌入式配置教程&#xff1a;STM32CubeMX项目 |CLion 文档 (jetbrains.com) OpenOCD官网下载&#xff1a;Download OpenOCD for Windows (gnutoolchains.com) GNU ARM工…