linux中epoll+socket实战

news2024/12/26 0:18:56

目录

  • 参考
  • 前言
    • 案例
  • 一、epoll的基本使用
    • 首先是epoll_create函数:
    • 然后是epoll_ctl函数:
    • 最后是epoll_wait函数:
    • 关于ET(边沿触发)、LT(水平触发)两种工作模式可以得出这样的结论:
  • 二、使用
    • 代码简易实现
  • 三、注意问题
    • 因TIME_WAIT导致的客户端断开之后无法立马重连
      • 问题描述
      • time_wait状态产生的原因
      • 原因分析
  • 四、socket
    • 缓冲区大小
  • set_keep_alive
  • set_linger

参考

Linux Epoll使用详解
利用Socket网络编程实现TCP时遇到的无法立刻建立第二次连接传输数据的问题
TCP面试常见题:time_wait状态产生的原因,危害,如何避免
计算机网络 | C++实现TCP/UDP的socket通信

前言

在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。

相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在linux/posix_types.h头文件有这样的声明:

#define __FD_SETSIZE    1024

表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。

在这里插入图片描述

案例

  • 单线程epoll:redis
    纯粹内容操作,只有一个epoll,没有多线程加锁以及切换
  • 多线程epoll:nettyserver
  • 多核epoll:ntyco
  • 多进程epoll:nginx

一、epoll的基本使用

epoll的接口非常简单,一共就三个函数:

  • epoll_create:创建一个epoll的句柄 ,相当于聘请了一个快读员
  • epoll_ctl:epoll的事件注册函数,相当于快递员对住户的增删改查
  • epoll_wait:等待事件的产生,相当于快读员等待不停的收发快递

首先是epoll_create函数:

int epoll_create (int __size)

它只有一个参数,用来告诉系统这个监听的数目一共有多大,但这个参数不同于select中的第一个参数。而是我们真实要监听的文件数量(事实上,在新版的Linux内核中,该值已经被忽略,只要大于0即可)

需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭

然后是epoll_ctl函数:

int epoll_ctl (int __epfd, int __op, int __fd,epoll_event *__event)

它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

  • 第一个参数是epoll_create的返回值
  • 第二个参数表示要执行的动作,有以下几种参数可填:
    EPOLL_CTL_ADD:注册新的文件标识符到epfd中
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件
    EPOLL_CTL_DEL:从epfd中删除一个文件标识符
  • 第三个参数是需要监听的文件标识符
  • 第四个参数是告诉内核需要监听什么事,

结构体epoll_event结构如下:

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

其中events参数可以是以下几个宏的集合:

  • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:表示对应的文件描述符可以写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

而data则一般用于携带附加数据,比如网络编程中的套接字文件的fd

最后是epoll_wait函数:

TCP接收到事件时会回调触发epoll_wait 中事件

int epoll_wait (int __epfd, struct epoll_event *__events,int __maxevents, int __timeout)
  • 第一个参数就是前面epoll_create函数的返回值
  • 参数events用来从系统内核得到事件的集合
  • maxevents告知内核这个events有多大,不能大于创建epoll_create()时的size
  • 参数timeout是超时时间(毫秒,0会立即返回,-1将永久阻塞(直到有事件发生)
  • 该函数返回需要处理的事件数目,如返回0表示已超时。

关于ET(边沿触发)、LT(水平触发)两种工作模式可以得出这样的结论:

ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.

二、使用

前提先包含一个头文件:

#include <sys/epoll.h> 

然后通过create_epoll来创建一个epoll,其参数为你epoll所支持的最大数量(已忽略)。

这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close来关闭这个创建出来的epoll句柄

之后在你的网络主循环里面,每次调用epoll_wait来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中kdpfd为用epoll_create创建之后的句柄
events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。
max_events是当前需要监听的所有socket句柄数。
最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件发生,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则等待。
一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。

epoll_wait函数之后应该是一个循环,遍利所有的事件。

代码简易实现


#include<sys/socket.h> // socket依赖
#include<arpa/inet.h> // socket依赖
#include <unistd.h> // close依赖
#include<sys/epoll.h>
#include<cstring>
#include<iostream>

#include "log.h"
using namespace std;

#define MAX_LISTEN_SOCKET 10
#define SOCKET_PORT 5000

int main(){

    int sockfd = 0;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    LOG_INFO("监听套接字文件描述符:%d\n", sockfd);

    sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(SOCKET_PORT);
	// addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	addr.sin_addr.s_addr = htons(INADDR_ANY);

	// 一般在一个端口释放后需要等一段时间才能重新启用,因此需要借助SO_REUSEADDR来使端口重新启用。解决服务端异常退出之后,再次启动服务端,客户端无法使用同一个端口连接socket的问题
	int out = 1;
    int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &out, sizeof(out));
	if (ret < 0) {
		LOG_ERROR("setsockopt");
		return -1;
	}
	ret = bind(sockfd, (sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		LOG_ERROR("绑定失败!\n");
		return -1;
	}
    ret = listen(sockfd, 5);
    if (ret == -1) {
		LOG_ERROR("监听失败!\n");
		return -1;
	}
	sockaddr_in cliAddr;
	socklen_t len = sizeof(cliAddr);


    // 创建epoll ,int epoll_create(int size); size参数 相当于提供给内核一个提示,当前需要监听的fd个数
    int fdEp = epoll_create(MAX_LISTEN_SOCKET);
	epoll_event eve;
	eve.data.fd = sockfd;
	eve.events = EPOLLIN ;

    // 注册事件
	ret = epoll_ctl(fdEp, EPOLL_CTL_ADD, sockfd, &eve);
    if(ret == -1) {
        LOG_ERROR("epoll注册失败\n");
        close(sockfd);
		return -1;
    }
	LOG_INFO("开始监听!");
    epoll_event events[MAX_LISTEN_SOCKET];
    while (1) {
		/* timeout 500 epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
		*   __epfd:由epoll_create 生成的epoll专用的文件描述符 
			__events:分配好的epoll_event结构体数组 用于接收fd对象的缓冲区
			__maxevents:每次能够处理的事件数 这一次调用可以接收多少准备好的fd对象,通常设置为events参数的长度
			__timeout:如果没有准备好的事件对象,那么等待多久返回,-1阻塞,0非阻塞
			返回值:返回events缓冲区中有效的fd个数,也即准备好事件的fd个数
		*/
		int eventCount = epoll_wait(fdEp, events, MAX_LISTEN_SOCKET, 500);
        if (eventCount == -1) {
			LOG_ERROR("select 出错!\n");
			break;
		}else if (eventCount == 0) {
			continue;
		}
		LOG_INFO("监听到事件数量:%d\n",eventCount);
        for (int i = 0; i < eventCount; i++) {
			// 如果是服务器fd并且是读事件,则接收连接
			if (events[i].data.fd == sockfd) {
				// 是否读事件
				if(events[i].events & EPOLLIN){
					int clisock = accept(sockfd, (sockaddr*)&cliAddr, &len);
					if (clisock == -1) {
						LOG_ERROR("接收客户端错误\n");
						continue;
					}else{
						LOG_INFO("接收到客户端连接");
					}

					eve.data.fd = clisock;
					eve.events = EPOLLIN;
					epoll_ctl(fdEp, EPOLL_CTL_ADD, clisock, &eve);
				}else{
					LOG_INFO("服务器其他事件");
				}

			}
			else {
				// 对非服务器socket进行处理
				if (events[i].events & EPOLLIN) {
					char buf[0xFF]{};
					size_t len = recv(events[i].data.fd, buf, 0xFF, 0);
					if (len <= 0) {
						LOG_INFO("客户端已经断开连接!");
						epoll_ctl(fdEp, EPOLL_CTL_DEL, events[i].data.fd, NULL);
						close(events[i].data.fd);
						break;
					}else{
						LOG_INFO("客户端接收到数据:%s",buf);
					}
					send(events[i].data.fd, buf, len, 0);
				}
				else if(events[i].events & EPOLLOUT) {
					LOG_INFO("客户端 EPOLLOUT");
				}
				else {
					LOG_INFO("客户端其他事件");
				}
				
			}
		}
    }
    close(fdEp);
	close(sockfd);
    return 0;
}

三、注意问题

因TIME_WAIT导致的客户端断开之后无法立马重连

问题描述

在测试时发现,当客户端和服务器端建立第一次连接并成功发送数据后,关闭连接,想要立刻建立第二次连接发送数据时,客户端会无法再次连接。大约2分钟(MSL)之后才可以重新连接

time_wait状态产生的原因

  • 1)为实现TCP全双工连接的可靠释放
    由TCP状态变迁图可知,假设发起主动关闭的一方(client)最后发送的ACK在网络中丢失,由于TCP协议的重传机制,执行被动关闭的一方(server)将会重发其FIN,在该FIN到达client之前,client必须维护这条连接状态,也就说这条TCP连接所对应的资源(client方的local_ip,local_port)不能被立即释放或重新分配,直到另一方重发的FIN达到之后,client重发ACK后,经过2MSL时间周期没有再收到另一方的FIN之后,该TCP连接才能恢复初始的CLOSED状态。如果主动关闭一方不维护这样一个TIME_WAIT状态,那么当被动关闭一方重发的FIN到达时,主动关闭一方的TCP传输层会用RST包响应对方,这会被对方认为是有错误发生,然而这事实上只是正常的关闭连接过程,并非异常。

  • 2)为使旧的数据包在网络因过期而消失
    为说明这个问题,我们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP连接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我们先关闭,接着很快以相同的四元组建立一条新连接。本文前面介绍过,TCP连接由四元组唯一标识,因此,在我们假设的情况中,TCP协议栈是无法区分前后两条TCP连接的不同的,在它看来,这根本就是同一条连接,中间先释放再建立的过程对其来说是“感知”不到的。这样就可能发生这样的情况:前一条TCP连接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当做当前TCP连接的正常数据接收并向上传递至应用层(而事实上,在我们假设的场景下,这些旧数据到达remote peer前,旧连接已断开且一条由相同四元组构成的新TCP连接已建立,因此,这些旧数据是不应该被向上传递至应用层的),从而引起数据错乱进而导致各种无法预知的诡异现象。作为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种情况的发生,这正是TIME_WAIT状态存在的第2个原因。

  • 3)总结
    具体而言,local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。

原因分析

  • 1、TCP的可靠连接
    众所周知,TCP是一种可靠的连接,而断开连接需要“四次挥手”,为保证传输的可靠性,需要有一个阶段来保证能够对出错或丢失的数据包进行重发,那么这个阶段就是TIME_WAIT状态。

  • 2、允许老的重复分解在网络中消失
    关于这句话,看起来可能没那么容易理解,按通俗的话来讲就是:在第一次使用bind()函数绑定端口后,再次启动程序,导致地址复用,从而bind()函数error。

  • MSL(Maximum Segment Lifetime)最大报文生存时间
    每个TCP实现必须选择一个MSL。它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL时间。RFC 793指出MSL为2分钟,现实中常用30秒或1分钟。

  • 2MSL
    当TCP执行主动关闭,并发出最后一个ACK,该链接必须在TIME_WAIT状态下停留的时间为2MSL。这样可以(1)让TCP再次发送最后的ACK以防这个ACK丢失(被动关闭的一方超时并重发最后的FIN);保证TCP的可靠的全双工连接的终止。(2)允许老的重复分节在网络中消失。参考文章《unix网络编程》(3)TCP连接的建立和终止 在TIME_WAIT状态 时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置 SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

有了这个思路,那么如果能够让一个端口在短时间内连续使用,我们的问题不就可以解决了吗?

SO_REUSEADDR,用于对TCP套接字处于TIME_WAIT状态下的socket,当你想要在端口被释放后立即可以使用,那么应该在bind之前调用SO_REUSEADDR。

int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &out, sizeof(out));

总结:一般在一个端口释放后需要等一段时间才能重新启用,因此需要借助SO_REUSEADDR来使端口重新启用。

四、socket

缓冲区大小

建立一个socket,通过getsockopt获取缓冲区的值如下:
发送缓冲区大小:SNDBufSize = 16384
接收缓冲区大小:RCVBufSize = 87380
通过代码设置缓存区

bool Socket::set_send_buffer(int size)
{
    int buff_size = size;
    if (setsockopt(m_sockfd, SOL_SOCKET, SO_SNDBUF, &buff_size, sizeof(buff_size)) < 0)
    {
        LOG_ERROR("socket set send buffer error: errno=%d errstr=%s", errno, strerror(errno));
        return false;
    }
    return true;
}

bool Socket::set_recv_buffer(int size)
{
    int buff_size = size;
    if (setsockopt(m_sockfd, SOL_SOCKET, SO_RCVBUF, &buff_size, sizeof(buff_size)) < 0)
    {
        LOG_ERROR("socket set recv buffer error: errno=%d errstr=%s", errno, strerror(errno));
        return false;
    }
    return true;
}

如果接收缓存区设置过小,比如设置10,会导致recv循环读取返回值为-1,此时socket并没有断开,循环继续是可以读取到数据的,但是会导致比如上传文件时速度变慢,所以缓存区大小建议默认或者设置大一些

set_keep_alive

set_linger

/* 优雅关闭: 直到所剩数据发送完毕或超时 */

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

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

相关文章

基于html+css的图展示71

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

某音X-Bogus算法研究 2023-05-15

本文以教学为基准&#xff0c;研究JavaScript算法及反爬策略、本文提供的可操作性不得用于任何商业用途和违法违规场景。 如有侵权&#xff0c;请联系我进行删除。 今天我们分析一下douyin个人主页数据获取。 大多数小伙伴应该都知道想要拿到douyin的数据也不是那么容易的&a…

近世代数 笔记与题型连载 第十三章(环与域)

文章目录 基本概念1.环1.1.环的定义1.2.环的性质1.3.几种特殊的环1.4.子环 2.域2.1.域的定义2.2.环与域的同态 相关题型1.验证一个代数系统是否是一个环2.判断一个代数系统是否是整环3.判断一个代数系统是否是另一个代数系统的子环4.判断一个代数系统是否是域 基本概念 1.环 …

sed编辑器命令

sed编辑器 sed是一种流编辑器&#xff0c;流编辑器会在编辑器处理数据之前基于预先提供的一组规则来编辑数据流。 sed编辑器可以根据命令来处理数据流中的数据&#xff0c;这些命令要么从命令行中输入&#xff0c;要么存储在一个命令文本文件中。 sed 的工作流程主要包括读取…

Nat. Commun | 中国海洋大学张伟鹏组揭示海洋生物被膜群落硫氧化主要菌群及其作用机制...

海洋生物被膜玫瑰杆菌的厌氧硫氧化机制 Anaerobic thiosulfate oxidation by the Roseobacter group is prevalent in marine biofilms Article&#xff0c;2022-04-11&#xff0c;Nature Communications&#xff0c;[IF 17.7] DOI&#xff1a;10.1038/s41467-023-37759-4 原文…

Apache Hive函数高阶应用、性能调优

Hive的函数高阶应用 explode函数 explode属于UDTF函数&#xff0c;表生成函数&#xff0c;输入一行数据输出多行数据。 功能&#xff1a; explode() takes in an array (or a map) as an input and outputs the elements of the array (map) as separate rows.--explode接收…

「 操作系统 」CPU缓存一致性协议MESI详解

「 操作系统 」CPU缓存一致性协议MESI详解 参考&鸣谢 缓存一致性协议MESI 小天 CPU缓存一致性协议MESI 枫飘雪落 CPU缓存一致性协议(MESI) 广秀 2.4 CPU 缓存一致性 xiaoLinCoding 文章目录 「 操作系统 」CPU缓存一致性协议MESI详解一、计算机的缓存一致性二、CPU高速缓存…

100种思维模型之长远思考思维模型-63

古语有云&#xff1a;“人无远虑&#xff0c;必有近忧&#xff01;” 任正非说&#xff1a;不谋长远者&#xff0c;不足以谋一时&#xff01; 长远思考思维&#xff0c;一个提醒我们要运用长远眼光&#xff0c;树立宏大目标&#xff0c;关注长期利益的思维模型 01何谓长远思考…

深度学习架构的对比分析

深度学习的概念源于人工神经网络的研究&#xff0c;含有多个隐藏层的多层感知器是一种深度学习结构。深度学习通过组合低层特征形成更加抽象的高层表示&#xff0c;以表征数据的类别或特征。它能够发现数据的分布式特征表示。深度学习是机器学习的一种&#xff0c;而机器学习是…

浅谈数据资产测绘系统的作用和挑战

随着数据被定义为第五大生产要素&#xff0c;数据已经成为数字经济发展的核心驱动力。数据资源的充分利用和开放共享给政企单位带来便利的同时&#xff0c;也带来了相应的数据安全风险。因此&#xff0c;摸清并动态掌握数据资产情况&#xff0c;持续进行数据资产测绘就成为企业…

Golang每日一练(leetDay0066) 有效电话号码、转置文件

目录 193. 有效电话号码 Valid Phone Numbers &#x1f31f; 194. 转置文件 Transpose File &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 193. 有效电话号…

IDEA常用配置和插件总结

文章目录 1\. 配置1.1 设置编译版本1.2 设置编码1.3 自动导包1.4 自动编译1.5 设置主题1.6 设置字体字号1.7 滚轮修改字体大小1.8 控制台字体1.9 行号与方法分隔符1.10 忽略大小写字母1.11 多行显示1.12 设置 Maven1.13 GitHub 账户1.14 配置 Git1.15 配置文件隐藏1.16 配置相同…

java中List与AbstractList

一、List 接口 List 接口继承了 Collection 接口&#xff0c;在 Collection 接口的基础上增加了一些方法。相对于 Collection 接口&#xff0c;我们可以很明显的看到&#xff0c;List 中增加了非常多根据下标操作集合的方法&#xff0c;我们可以简单粗暴的分辨一个方法的抽象方…

C++——动态管理(类和对象收尾)

作者&#xff1a;几冬雪来 时间&#xff1a;2023年5月14日 内容&#xff1a;C内存管理讲解 目录 前言&#xff1a; 1.类的对象&#xff08;收尾&#xff09;&#xff1a; 1.友元函数&#xff1a; 2.内部类&#xff1a; 3.匿名对象&#xff1a; 4.优化&#xff1a; 2.…

常见基础算法

一、排序 & 查找算法 1.1 冒泡排序 相邻的数据进行比较。每次遍历找到一个最大值。 public void sort(int[] nums) {if (nums null) {return;}for (int i 0; i < nums.length; i) {for (int j 0; j < nums.length - 1 - i; j) {if (nums[j] > nums[j 1]…

Python每日一练(20230515) 只出现一次的数字 I\II\III

目录 1. 只出现一次的数字 Single Number 2. 只出现一次的数字 II Single Number II 3. 只出现一次的数字 III Single Number III &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 leetcod…

开源项目ChatGPT-website再次更新,累计下载使用1600+

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…

数据交换方式(电路,报文,虚电路分组交换,数据报分组交换)

电路交换&#xff1a; 电路交换是通信网中最早出现的一种交换方式&#xff0c;在进行数据传输前&#xff0c;两个结点之间必须先建立一条专用&#xff08;双方独占&#xff09;的物理通信链路。该线路在整个数据传输期间一直被独占&#xff0c;用户始终占用端到端的固定传输带…

python实现带有操作界面的计算器程序,实现基本的数值计算,支持负数、小数、加减乘除等运算。

一、程序要求 python实现带有操作界面的计算器程序,实现基本的数值计算,支持负数、小数、加减乘除等运算。 预期计算器界面如下: 二、代码实现 1、python3自带tkinter,可以用来做可视化界面: import tkinter as tk import re 2、新建窗口对象,设置高宽、设置标题和背景…

【分布族谱】正态分布和对数正态分布的关系

文章目录 正态分布对数正态分布的推导测试 正态分布 正态分布&#xff0c;最早由棣莫弗在二项分布的渐近公式中得到&#xff0c;而真正奠定其地位的&#xff0c;应是高斯对测量误差的研究&#xff0c;故而又称Gauss分布。。测量是人类定量认识自然界的基础&#xff0c;测量误差…