Android系统原理性问题分析 - 单路情况下的C/S模型

news2024/11/29 22:53:16

声明

  • 在Android系统中经常会遇到一些系统原理性的问题,在此专栏中集中来讨论下。
  • Android系统中很多地方都采用了I/O多路复用的机制,为了引出I/O多路复用机制,先来分析多路并发情况下的C/S模型。
  • 此篇参考一些博客和书籍,代码基于Android 7.1.1,不方便逐一列出,仅供学习、知识分享。
  • 此篇代码下载:单路情况下的CS模型 tcp-socket udp-socket ud-socket

1 概述

在一个典型的客户端/服务器场景中,应用程序使用 socket 进行通信的方式如下:

  • 各个应用程序创建一个 socket。socket 是一个允许通信的“设备”,两个应用程序都需要用到它。
  • 服务器将自己的 socket 绑定到一个众所周知的地址(名称)上使得客户端能够定位到它的位置。

使用 socket()系统调用能够创建一个 socket,它返回一个用来在后续系统调用中引用该socket 的文件描述符。

#include <sys/socket.h>
int socket(int domain, int type, int protocol);		//returns file descriptor on success, or -1 on error

不管参数protocol,此篇中protocol参数都设置为0,如果对这个参数感兴趣,可以自行搜索学习。主要分析下前两个参数: domain 和 type。

1.1 通信 domain

socket 存在于一个通信 domain 中,它确定:

  • 识别出一个 socket 的方法(即 socket“地址”的格式);
  • 通信范围(位于同一主机上的应用程序之间还是在位于使用一个网络连接起来的不同主机上的应用程序之间),也就是IPC或RPC的概念。

现代操作系统至少支持下列 domain:

  • UNIX(AF_UNIX) domain :允许在同一主机上的应用程序之间进行通信。
  • IPv4(AF INET) domain :允许在使用因特网协议 IP4 网络连接起来的主机上的应用程序之间进行通信。
  • IPV6 (AF INET6) domain :允许在使用因特网协议 IPV6 网络连接起来的主机上的应用程序之间进行通信。
Domaindomain执行的通信应用程序间的通信地址格式地址结构
Unix DomainAF_UNIX内核中同一主机路径名sockaddr un
Internet DomainAF_INET通过IPv4通过 IPv4 网络连接起来的主机32位IPV4 地址+16 位端口号sockaddrin
Internet DomainAF_INET6通过IPv6通过 IPv6 网络连按起来的主机128 位 IPv6 地址+16 位端口号sockaddr in6

1.2 socket 类型

  每个 socket 实现都至少提供了两种 socket:流和数据报。这两种 socket 类型在 Unix domain 和 Internet domain 中都得到了支持。下表对这两种 socket 类型的属性进行了总结:

属性socket 类型
流 (SOCK_STREAM)数据报 (SOCK_DGRAM)
可靠传输?YesNo
消息边界保留?NoYes
面向链接?YesNo

  流 socket (SOCK_STREAM) 提供了一个可靠的双向的字节流通信信道。在这段描述中的术语的含义如下:

  • 可靠的:表示可以保证发送者传输的数据会完整无缺地到达接收应用程序 (假设网络链接和接收者都不会崩溃) 或收到一个传输失败的通知。
  • 双向的:表示数据可以在两个 socket 之间的任意方向上传输。
  • 字节流:表示与管道一样不存在消息边界的概念 。

  一个流 socket 类似于使用一对允许在两个应用程序之间进行双向通信的管道,它们之间的差别在于 (Internet domain) socket 允许在网络上进行通信。
  流 socket 的正常工作需要一对相互连接的 socket,因此流 socket 通常被称为面向连接的。术语“对等 socket”是指连接另一端的 socket,“对等地址”表示该 socket 的地址,“对等应用程序”表示利用这个对等 socket 的应用程序。有些时候,术语“远程”(或外部) 是作为对等的同义词使用。类似地,有些时候术语“本地”被用来指连接的这一端上的应用程序、socket或地址。一个流 socket 只能与一个对等 socket 进行连接。

  数据报 socket (SOCK_DGRAM)允许数据以被称为数据报的消息的形式进行交换。在数据报 socket 中,消息边界得到了保留,但数据传输是不可靠的。消息的到达可能是无序的、重复的或者根本就无法到达。
  数据报 socket 是更一般的无连接 socket 概念的一个示例。与流 socket 不同,一个数据报socket 在使用时无需与另一个 socket 连接。
在 Internet domain 中,数据报 socket 使用了用户数据报协议 (UDP),而流 socket 则 (通常)使用了传输控制协议 (TCP)。一般来讲,在称呼这两种 socket 时不会使用术语“Internet domain数据报 socket” 和 “Internet domain 流 socket”,而是分别使用术语“UDP socket”和“TCP socket”。

1.3 Socket 相关的系统调用

关键的 socket 系统调用包括以下几种:

  • socket() 系统调用创建一个新 socket。
  • bind() 系统调用将一个 socket 绑定到一个地址上。通常,服务器需要使用这个调用来将其 socket 绑定到一个众所周知的地址上使得客户端能够定位到该 socket 上。
  • listen() 系统调用允许一个流 socket 接受来自其他 socket 的接入连接。
  • accept() 系统调用在一个监听流 socket 上接受来自一个对等应用程序的连接,并可选地返回对等 socket 的地址。
  • connect() 系统调用建立与另一个 socket 之间的连接。

2 Internet DOMAIN的C/S模型

2.1 TCP socket 的C/S模型(SOCK_STREAM)

在这里插入图片描述
1. 建立连接过程
  服务器调用socket()、bind()、listen() 完成初始化后,调用 accept() 阻塞等待,处于监听端口的状态。客户端调用 socket() 初始化后,调用 connect() 发出 SYN 段并阻塞等待服务器应答,服务器应答一个 SYN-ACK 段,客户端收到后从 connect() 返回,同时应答一个 ACK 段,服务器收到后从 accept() 返回。

2. 数据传输的过程
  建立连接后,TCP 协议提供全双工的通信服务,但是一般的 C/S 程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从 accept() 返回后立刻调用 read(),读 socket 就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用 write() 发送请求给服务器,服务器收到后从 read() 返回,对客户端的请求进行处理,在此期间客户端调用 read() 阻塞等待服务器的应答,服务器调用 write() 将处理结果发回给客户端,再次调用 read() 阻塞等待下一条请求,客户端收到后从 read() 返回,发送下一条请求,如此循环下去。

3. 断开连接过程
  若客户端没有更多的请求了,就调用 close() 关闭连接,就像写端关闭的管道一样,服务器的 read() 返回 0,这样服务器就知道客户端关闭了连接,也调用 close() 关闭连接。

注意:任何一方调用close() 后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用 shutdown() 则连接处于半关闭状态,仍可接收对方发来的数据。

2.1.1 Server端代码

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>

#include "wrap.h"

#define SERV_PORT 6666

int main(void)
{
    int sfd, cfd;
    int len, i;
    char buf[BUFSIZ], clie_IP[BUFSIZ];

    struct sockaddr_in serv_addr, clie_addr;
    socklen_t clie_addr_len;

    sfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&serv_addr, sizeof(serv_addr));           
    serv_addr.sin_family = AF_INET;                 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
    serv_addr.sin_port = htons(SERV_PORT);          

    Bind(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    Listen(sfd, 2);                                

    printf("wait for client connect ...\n");

    clie_addr_len = sizeof(clie_addr_len);
    cfd = Accept(sfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
    printf("cfd = ----%d\n", cfd);

    printf("client IP: %s  port:%d\n", 
            inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), 
            ntohs(clie_addr.sin_port));

    while (1) {
        len = Read(cfd, buf, sizeof(buf));
        Write(STDOUT_FILENO, buf, len);

        for (i = 0; i < len; i++)
            buf[i] = toupper(buf[i]);
        Write(cfd, buf, len); 
    }

    Close(sfd);
    Close(cfd);

    return 0;
}

2.1.2 Client端代码

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

#include "wrap.h"

#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666

int main(void)
{
    int sfd, len;
    struct sockaddr_in serv_addr;
    char buf[BUFSIZ]; 

    sfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&serv_addr, sizeof(serv_addr));                       
    serv_addr.sin_family = AF_INET;                             
    inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);    
    serv_addr.sin_port = htons(SERV_PORT);                      

    Connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    while (1) {
        fgets(buf, sizeof(buf), stdin);
        int r = Write(sfd, buf, strlen(buf));       
        printf("Write r ======== %d\n", r);
        len = Read(sfd, buf, sizeof(buf));
        printf("Read len ========= %d\n", len);
        Write(STDOUT_FILENO, buf, len);
    }

    Close(sfd);

    return 0;
}

  由于客户端不需要固定的端口号,因此不必调用 bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用 bind(),只是没有必要调用 bind() 固定一个端口号,服务器也不是必须调用 bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就很麻烦

2.2 UDP socket 的C/S模型(SOCK_DGRAM)

在这里插入图片描述
  由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,保证通讯可靠性的机制需要在应用层实现

2.2.1 Server端代码

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

#define SERV_PORT 8000

int main(void)
{
    struct sockaddr_in serv_addr, clie_addr;
    socklen_t clie_addr_len;
    int sockfd;
    char buf[BUFSIZ];
    char str[INET_ADDRSTRLEN];
    int i, n;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(SERV_PORT);

    bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    printf("Accepting connections ...\n");
    while (1) {
        clie_addr_len = sizeof(clie_addr);
        n = recvfrom(sockfd, buf, BUFSIZ,0, (struct sockaddr *)&clie_addr, &clie_addr_len);
        if (n == -1)
            perror("recvfrom error");

        printf("received from %s at PORT %d\n",
                inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
                ntohs(clie_addr.sin_port));

        for (i = 0; i < n; i++)
            buf[i] = toupper(buf[i]);

        n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&clie_addr, sizeof(clie_addr));
        if (n == -1)
            perror("sendto error");
    }
    close(sockfd);

    return 0;
}

2.2.2 Client端代码

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

#define SERV_PORT 8000

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    int sockfd, n;
    char buf[BUFSIZ];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    while (fgets(buf, BUFSIZ, stdin) != NULL) {
        n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
        if (n == -1)
            perror("sendto error");

        n = recvfrom(sockfd, buf, BUFSIZ, 0, NULL, 0);         //NULL:不关心对端信息
        if (n == -1)
            perror("recvfrom error");

        write(STDOUT_FILENO, buf, n);
    }

    close(sockfd);

    return 0;
}

2.3 TCP socket 与 UDP socket 的区别

  Internet DOMAIN中的传输层主要应用的协议模型:TCP协议和UDP协议。TCP协议在网络通信中占主导地位,绝大多数的网络通信借助TCP协议完成数据传输。但UDP也是网络通信中不可或缺的重要通信手段。
  相较于TCP而言,UDP通信的形式更像是发短信。不需要在数据传输之前建立、维护连接。只专心获取数据就好。省去了三次握手的过程,通信速度可以大大提高,但与之伴随的通信的稳定性和正确率便得不到保证。因此,称UDP为“无连接的不可靠报文传递”。
  与TCP相比,UDP有哪些优点和不足呢?由于无需创建连接,所以UDP开销较小,数据传输速度快,实时性较强。多用于对实时性要求较高的通信场合,如视频会议、电话会议等。但随之也伴随着数据传输不可靠,传输数据的正确率、传输顺序和流量都得不到控制和保证。所以,通常情况下,使用UDP协议进行数据传输,为保证数据的正确性,需要在应用层添加辅助校验协议来弥补UDP的不足,以达到数据可靠传输的目的
  与TCP类似的,UDP也有可能出现缓冲区被填满后,再接收数据时丢包的现象。由于它没有TCP滑动窗口的机制,通常采用如下两种方法解决:

  1. 服务器应用层设计流量控制,控制发送数据速度。
  2. 借助 setsockopt 函数改变接收缓冲区大小。如:
    #include <sys/socket.h>
    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    
    int n = 220x1024
    setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));
    

3 Unix DOMAIN的C/S模型

  socket API 原本是为网络通讯设计的,后来在 socket 的框架上发展出一种 IPC 机制,也就是Unix Domain Socket。虽然网络 socket 也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是Unix Domain Socket 用于 IPC 更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。Unix Domain Socket 也提供面向流和面向数据报两种 API 接口,类似于 TCP 和 UDP,但是面向消息的 Unix Domain Socket 也是可靠的,消息既不会丢失也不会顺序错乱
在这里插入图片描述
  UNIX Domain Socket是全双工的,API接口语义丰富,相比其它 IPC 机制有明显的优越性,目前已成为使用最广泛的IPC机制。使用Unix Domain Socket的过程和网络 socket 相似,也要先调用 socket() 创建一个 socket 文件描述符,address_family 指定为 AF_UNIX,type 可以选择 SOCK_DGRAM/SOCK_STREAM,protocol 参数仍然指定为 0。
  Unix Domain Socket 与网络 socket 编程最明显的不同在于地址格式不同,用结构体 sockaddr_un 表示,网络编程的 socket 地址是IP地址+端口号,而 Unix Domain Socket 的地址是一个 socket 类型的文件在文件系统中的路径此 socket 文件由 bind() 调用创建,如果调用 bind() 时该文件已存在,则 bind() 错误返回

对比网络套接字地址结构和本地套接字地址结构:

struct sockaddr_in {
__kernel_sa_family_t sin_family; 			/* Address family */  	地址结构类型
__be16 sin_port;					 	/* Port number */		端口号
struct in_addr sin_addr;					/* Internet address */	IP地址
};

struct sockaddr_un {
__kernel_sa_family_t sun_family; 		/* AF_UNIX */			地址结构类型
char sun_path[UNIX_PATH_MAX]; 		/* pathname */		socket文件名(含路径)
};

3.1 C/S数据模型(SOCK_STREAM)

3.1.1 Server端代码

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>

#include "wrap.h"

#define SERV_ADDR  "serv.socket"

int main(void)
{
    int lfd, cfd, len, size, i;
    struct sockaddr_un servaddr, cliaddr;
    char buf[4096];

    lfd = Socket(AF_UNIX, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_UNIX;
    strcpy(servaddr.sun_path,SERV_ADDR);

    len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path);     /* servaddr total len */

    unlink(SERV_ADDR);                              /* 确保bind之前serv.sock文件不存在,bind会创建该文件 */
    Bind(lfd, (struct sockaddr *)&servaddr, len);           /* 参3不能是sizeof(servaddr) */

    Listen(lfd, 20);

    printf("Accept ...\n");
    while (1) {
        len = sizeof(cliaddr);
        cfd = Accept(lfd, (struct sockaddr *)&cliaddr, (socklen_t *)&len);

        len -= offsetof(struct sockaddr_un, sun_path);      /* 得到文件名的长度 */
        cliaddr.sun_path[len] = '\0';                       /* 确保打印时,没有乱码出现 */

        printf("client bind filename %s\n", cliaddr.sun_path);

        while ((size = read(cfd, buf, sizeof(buf))) > 0) {
            for (i = 0; i < size; i++)
                buf[i] = toupper(buf[i]);
            write(cfd, buf, size);
        }
        close(cfd);
    }
    close(lfd);

    return 0;
}

3.1.2 Client端代码

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>         
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>

#include "wrap.h"

#define SERV_ADDR "serv.socket"
#define CLIE_ADDR "clie.socket"

int main(void)
{
    int  cfd, len;
    struct sockaddr_un servaddr, cliaddr;
    char buf[4096];

    cfd = Socket(AF_UNIX, SOCK_STREAM, 0);

    bzero(&cliaddr, sizeof(cliaddr));
    cliaddr.sun_family = AF_UNIX;
    strcpy(cliaddr.sun_path,CLIE_ADDR);

    len = offsetof(struct sockaddr_un, sun_path) + strlen(cliaddr.sun_path);     /* 计算客户端地址结构有效长度 */

    unlink(CLIE_ADDR);
    Bind(cfd, (struct sockaddr *)&cliaddr, len);                                 /* 客户端也需要bind, 不能依赖自动绑定*/

    
    bzero(&servaddr, sizeof(servaddr));                                          /* 构造server 地址 */
    servaddr.sun_family = AF_UNIX;
    strcpy(servaddr.sun_path,SERV_ADDR);

    len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path);   /* 计算服务器端地址结构有效长度 */

    Connect(cfd, (struct sockaddr *)&servaddr, len);

    while (fgets(buf, sizeof(buf), stdin) != NULL) {
        write(cfd, buf, strlen(buf));
        len = read(cfd, buf, sizeof(buf));
        write(STDOUT_FILENO, buf, len);
    }

    close(cfd);

    return 0;
}

3.2 C/S数据模型(SOCK_DGRAM)

  因为Unix Domain Socket是用于IPC的,所以知道SOCK_STREAM类型的就够了,这里暂不做例子的代码编写了。

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

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

相关文章

float浮点/double双精度浮点和二进制的相互转换,小白也能看明白!

二进制文件包含了太多的数据&#xff0c;如何看懂二进制文件&#xff0c;决定于基础。 文章目录 前言1、重点知识1.1何为二进制文件1.2浮点和双精度的浮点如何生成二进制1.2.1 float和double的基础知识1.2.2 IEEE754约束的重点1.2.3 浮点是如何表示二进制 1.3 例子说明 双精度d…

The Open Graph protocol(开放图谱协议)的介绍及应用

介绍 Open Graph 协议使任何网页都可以成为社交中的丰富对象。例如&#xff0c;用于 Facebook 以允许任何网页具有与 Facebook 上任何其他对象相同的功能。 以下是把链接分享到钉钉&#xff0c;钉钉识别后显示的效果&#xff1a; 基本元数据 要将网页变成图形对象&#xff0…

【如何在三个月时间里,从零基础小白变身为专业的数据分析师?】

如何在三个月时间里&#xff0c;从零基础小白变身为专业的数据分析师&#xff1f; 随着数字经济、大数据时代的发展&#xff0c;数据已然成为当下时代最重要的盈利资源&#xff0c;让企业在做决策和计划方案时更有针对性和依据&#xff0c;能提前预测市场发展方向&#xff0c;做…

【C++学习】异常

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《C学习》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 异常 &#x1f96e;异常&#x1f362;自定义异常体系&#x1f362;C标准库的异常体系&#x1f362;异…

Linux 开机过程

参考链接 Linux引导过程 Linux 系统的运行级别 Linux 系统总共有设置了 7 种运行级别。分别是&#xff1a; 0 级&#xff1a;关机&#xff1b;1 级&#xff1a;单用户模式&#xff1b;2 级&#xff1a;无网络的多用户模式&#xff1b;3 级&#xff1a;多用户模式&#xff0…

EasyDSS调用录像回看接口出现报错“请先合成mp4”,是什么原因?

EasyDSS支持一站式的上传、转码、直播、回放、嵌入、分享功能&#xff0c;具有多屏播放、自由组合、接口丰富等特点。平台可以为用户提供专业、稳定的直播推流、转码、分发和播放服务&#xff0c;全面满足超低延迟、超高画质、超大并发访问量的要求。 有用户反馈&#xff0c;在…

内网搭建 SFTP 服务器

文章目录 1. 搭建SFTP服务器1.1 下载 freesshd服务器软件1.3 启动SFTP服务1.4 添加用户1.5 保存所有配置 2 安装SFTP客户端FileZilla测试2.1 配置一个本地SFTP站点2.2 内网连接测试成功 3 使用cpolar内网穿透3.1 创建SFTP隧道3.2 查看在线隧道列表 4. 使用SFTP客户端&#xff0…

Spring Security 6.x 系列【44】微服务篇之统一身份认证实现方案

有道无术,术尚可求,有术无道,止于术。 本系列Spring Boot 版本 3.0.4 本系列Spring Security 版本 6.0.2 源码地址:https://gitee.com/pearl-organization/study-spring-security-demo 文章目录 1. 单体架构2. 微服务架构2.1 概述2.2 统一身份认证3. 认证方式3.1 JWT3.2 …

【TI毫米波雷达笔记】IWR6843AOPEVM-G+DCA1000EVM的mmWave Studio数据读取、配置及避坑

【TI毫米波雷达笔记】IWR6843AOPEVM-GDCA1000EVM的mmWave Studio数据读取、配置及避坑 硬件方面连接好以后 就可以打开mmWave Studio了 配置 如果硬件配置成功 则可以通过mmWave Studio的Output看到配置信息 按照如图步骤 在radar api里面一步步来操作 其中 RS232选择波特率…

细节有惊喜!详解Web自动化框架UI自动截图与画面回放实现!

目录 前言&#xff1a; Web自动化测试框架基本结构及原理 UI自动截图实现方法 基于Selenium截图实现UI自动截图的过程如下&#xff1a; 基于Selenium截图的代码实现如下&#xff1a; 基于爬虫截图实现UI自动截图的流程如下&#xff1a; 基于爬虫截图的代码实现如下&#…

springboot+vue4S店车辆管理系统(java项目源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的4S店车辆管理系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风…

【评测】真我Realme GT Neo5手机性能评测

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 目录 购买记录 鲁大师评测 安兔兔评测 个人使用感受 优点 几个明显的体验感不好的点 大图、多图预警&#xff01; 配置说明&#xff1a; 运行内存&#xff1a;12GB存储容量&#xff1a;256GB充电功率&#…

4.spring总结

文章目录 一、spring总结1、Spring三级缓存解决什么问题&#xff1f;2、Spring支持的几种bean的作用域&#xff1f;3、pring bean是否是线程安全&#xff1f;4、*Spring框架中bean的生命周期*&#xff1f;5、哪些是重要的bean生命周期方法&#xff1f;你能重载它们吗&#xff1…

《微服务实战》 第二十三章 Redis RDB AOF

前言 Redis 提供了两种持久化机制&#xff1a;第一种是 RDB&#xff0c;又称快照&#xff08;snapshot&#xff09;模式&#xff0c;第二种是 AOF 日志&#xff0c;也就追加模式。 1、RDB RDB 即快照模式&#xff0c;它是 Redis 默认的数据持久化方式&#xff0c;它会将数据…

【Flutter 工程】004-代码生成:functional_widget

【Flutter 工程】004-代码生成&#xff1a;functional_widget 文章目录 【Flutter 工程】004-代码生成&#xff1a;functional_widget一、概述1、Flutter 开发痛点2、functional_widget 函数小部件3、主页 二、基本使用1、安装 functional_widget2、传统写法3、运行结果4、代码…

App Inventor 2 连接打印机(Printer),自定义打印的实现

本教程主要分享App Inventor 2连接网络打印机进行自定义打印的思路及方法&#xff0c;这里只进行思路的讲解及关键步骤的效果演示&#xff0c;细节需自行完善。主要用到社交应用组件中的信息分享器组件。 打印App示意图 连接打印机教程&#xff08;难度系数&#xff1a;★★&a…

值得尝试的Voyager Linux,没有Snap,出色的定制

一直以来我都不喜欢 Ubuntu GNOME 的默认外观&#xff0c;而且对Ubuntu强制使用Snap应用不是很喜欢&#xff0c;之前我用的是Ubuntu MATE这个发行版&#xff0c;直到我听说了Voyager Linux。今天特意安装试用了一下Voyager Linux 23.04&#xff0c;有几点功能我很喜欢&#xff…

适用于 Windows 电脑的 6 款最佳视频转换器

视频转换器可以帮助您转换和播放设备上不受支持的视频格式。减小视频文件大小、以通用格式组织所有视频或与他人共享文件以在不同设备上播放也很方便。 Windows 有很多视频转换器可供选择。有些是免费的&#xff0c;有些则提供适合专业用户的高级功能。在本指南中&#xff0c;…

Java程序设计入门教程--Java语言概述

目录 1.1 Java语言诞生与发展 1.2 Java语言的特点 1.3 运行机制与虚拟机 1.1 Java语言诞生与发展 Java语言的诞生最早可以追溯至1991年&#xff0c;那时称为OAK语言&#xff0c;是SUN公司为一些消费性电子产品而设计的一个通用环境&#xff0c;其最初目的只是为了开发一种独…

完美解决接口测试难题,数据驱动带签名混合封装框架实现

目录 前言&#xff1a; 一、框架概述 二、框架架构 三、代码实现 四、实战步骤 五、总结 前言&#xff1a; 接口自动化测试是保障软件质量的重要手段之一&#xff0c;其自动化程度越高&#xff0c;越能有效提高软件测试效率。而接口自动化测试中&#xff0c;接口测试框架…