当前位置: 首页 > news >正文

【Linux-网络】初识计算机网络 Socket套接字 TCP/UDP协议(包含Socket编程实战)

52bc67966cad45eda96494d9b411954d.png

🎬 个人主页:谁在夜里看海.

📖 个人专栏:《C++系列》《Linux系列》《算法系列》

⛰️ 道阻且长,行则将至


目录

📚一、初识计算机网络

📖 背景

📖 网络协议

🔖OSI七层模型

🔖TCP/IP五层(或四层)模型

📖 地址管理

🔖MAC地址

🔖IP地址

🔖分层寻址流程

📚二、socket套接字

📖IP与端口号

📖套接字

🔖任务划分

🔖概念

🔖工作原理

🔖分类

📚三、UDP协议

📖 特点

📖 工作流程

🔖服务器端

🔖客户端

📖 常见API

🔖1.创建套接字

🔖2.绑定 IP 地址和端口

🔖3.发送数据

🔖4.接受数据

🔖5.关闭套接字

📖 UDP网络程序

 🔖程序结构概述

🔖详细步骤

🔖具体代码实现

🔖运行步骤

📚四、TCP协议

📖 特点

📖 工作流程

🔖三次握手

🔖四次挥手

 📖常见API

🔖3.监听端口

🔖4. 接收连接请求

🔖5. 发送数据

🔖6. 接收数据 

🔖7.关闭套接字

📖 TCP网络程序

🔖程序结构概述

🔖详细步骤

🔖具体代码实现

🔖运行步骤

🔖改进(支持并发)


📚一、初识计算机网络

现如今互联网产业繁荣发达,渗透到了社会生活的方方面面,从个人日常生活到各行各业的发展,互联网已经成为了不可或缺的一部分。而这一切都与计算机网络的发展密不可分,互联网的核心功能之一就是信息传播,而计算机网络是信息传输的载体,它提供了信息的高速流通以及全球互联互通的能力。

不过计算机网络并不是计算机与生俱来的,下面我们就来谈谈它的诞生背景以及发展历程吧。

📖 背景

最早的计算机(1940s-1950s)是单机系统,不同计算机之间独立运行,数据存储在打孔卡或磁带等物理介质上,彼此之间没有之间的联系。

因此,当需要不同计算机互相协作处理数据时,只能异步进行,并且要经过多次数据在物理介质上写入与读取:

这种方式在小范围内尚可,但效率显然低下,而且很难实现数据的远程共享,因此,网络互联与局域网(LAN)的概念逐步形成,研究机构和企业内部开始构建计算机之间的连接。

网络互联:多台计算机通过网络连接在一起,数据通过网络进行传输。

局域网:计算机数量更多,通过交换机与路由器连接在一起。

现如今,计算机网络已广泛覆盖全球,形成局域网(LAN),广域网(WAN),互联网(Internet)等多种形式。

❓思考一个问题:数据在网络中是如何进行传输的呢?

数据在网络中传输时,最终是以二进制(0和1)的形式进行传输的,而这些二进制数据在物理介质上传输时,会被转换成不同的信号形式,如电信号、光信号、无线电波等,传输方式取决于使用的网络介质。

因此我们在传输数据时,先要将数据转换成信号,接收方需要将信号转换成数据,但是这当中该如何转换呢?传输方怎么将数据转换成信号的,接受方就得以逆向方式将信号解析成数据(参考二战时期发电报),于是我们就需要定义一套 数据->信号->数据 的规则。

理论上来说,每个人都可以自定义一套规则,就像方言一样,但是不同方言地区的人们存在交流障碍,使用不同协议的设备之间也无法实现数据交流,于是人们约定了一个共同的标准,大家共同遵守,这就是网络协议

📖 网络协议

引用上述方言的例子,全国范围内要实现语言互通,于是约定了普通话协议,通过这一个协议,就可以实现口头语言层面的互通;但是网络通信要复杂的多,需要分成更多的层次,协议分层的设计理念借鉴了模块化的思想,将复杂的网络通信任务拆分成多个层次,每一层都专注于处理特定的功能,并与相邻层次协同工作。

如果没有分层,网络通信会变得非常复杂,每台设备都需要理解整个通信过程的细节,分层后,每一层只需要关注自己特定的功能,而不必关心其他层的复杂逻辑。

🔖OSI七层模型

OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型, 是一个逻辑上的定义和规范,它把网络从逻辑上分为了7层,每一层都有相关、相对应的物理设备,比如路由器,交换机。

它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整,通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯。

分层名称功能功能具体解释
7应用层针对特定应用的协议电子邮件:电子邮件协议
6表示层设备固有数据格式和网络标准数据格式的转换接收不同形式的信号:文字、图像等
5会话层通信管理,负责建立和断开通信连接。管理传输层及以下的分层何时建立连接、断开连接以及保持多久连接。
4传输层管理两个节点之间的数据传输,负责可靠传输判断是否有数据丢失

3

网络层地址管理与路由选择经过哪个路由到达目的地址
2数据链路层互连设备之间传输和识别数据帧数据帧与比特流之间的转换
1物理层以“0”,“1”表示信号,界定连接器和网线的规格比特流与电子信号之间的切换
🔖TCP/IP五层(或四层)模型

TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求

物理层:负责光/电信号的传递方式.。比如现在以太网通用的网线(双绞线)、wifi无线网使用电磁波等。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。

数据链路层:负责设备之间的数据帧的传送和识别.。例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。交换机(Switch)工作在数据链路层。

网络层:负责地址管理和路由选择。例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)工作在网路层。

传输层:负责两台主机之间的数据传输。如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机。

应用层:负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。我们的网络编程主要就是针对应用层。

我们还可以通过网络通信中的设备来理解五层模型:

对于一台主机, 它的操作系统内核实现了从传输层到物理层的内容;

对于一台路由器, 它实现了从网络层到物理层;

对于一台交换机, 它实现了从数据链路层到物理层;

对于集线器, 它只实现了物理层;

📖 地址管理

❓为什么需要地址?

✅在网络中,不同设备有各自的地址,就像进程标识符唯一标识进程一般,地址唯一标识设备,根据源地址和目标地址,网络就可以确定数据的传输路径。这里我们要区分两个地址:IP地址与MAC地址。

🔖MAC地址

每个网络设备(如网卡、交换机、路由器等)在出厂时会由制造商根据全球唯一的规则分配MAC地址,且通常不可更改,换句话说,MAC地址唯一标识网络设备,由6个字节(48位)组成:

00:1A:2B:3C:4D:5E  或  00-1A-2B-3C-4D-5E

理论上来说,根据源MAC地址和目标MAC地址就可以确定传输路径,但是会有下面一系列问题:

① 在复杂的网络环境中,设备的位置并不是固定的,如果每次都要手动或自动更新网络中的设备位置,将带来极大的管理开销

② 网络中可能有成千上万甚至数百万台设备,如果要每个设备都保存其位置,存储和查找的开销会非常大

于是为了在网络中更好地定位设备与管理地址,引入了IP地址:

🔖IP地址

IP地址(Internet Protocol Address)是一种逻辑地址(虚拟地址),它可以在网络上唯一标识一台设备。IP地址不是物理意义上的地址,而是设备在接入网络后分配的“虚拟地址”,用于在网络中进行寻址和通信。

例如:你的手机的MAC地址不会改变,无论在哪里连接网络。是当你从家庭Wi-Fi切换到公共Wi-Fi时,手机的IP地址会发生变化,因为网络不同。

IP地址有两种类型:IPv4 vs IPv6

① IPv4(32位,常见)

格式:192.168.1.1(点分十进制表示法),地址数量约43亿个,目前已接近耗尽。

② IPv6(128位,未来趋势)

格式:2001:db8::ff00:42:8329(冒号分隔的十六进制表示),地址数量极其极其极其庞大,庞大到几乎可以给每一粒沙子分配一个地址。

🔖分层寻址流程

区分了上述两个地址之后,我们来介绍数据传输的两个阶段:

① 网络层(IP地址):

        负责端到端的寻址和路径选择,确保数据能够从源设备到达目标设备,即“逻辑寻址”。数据包从源IP地址到目标IP地址,经过多个路由器跳跃,逐步向目的地靠近。

② 数据链路层(MAC地址):

        负责局域网(LAN)内的寻址和传输,即在同一网段内将数据正确传递到目标设备。每当数据包到达一个新的局域网时,源设备通过ARP协议查询目标设备的MAC地址,并在数据链路层进行传输。

❓为什么在“宏观”上使用IP地址寻址,在局部使用MAC地址:

IP地址提供了一种分层的、逻辑化的地址体系,使设备能够在全球范围内被唯一标识和访问,而无需关心设备的物理位置;

交换机工作在数据链路层,它维护一个MAC地址表(CAM表),记录设备的MAC地址及其连接端口,使用硬件逻辑快速查找并转发数据。如果用IP地址寻址,交换机将不得不解析IP并进行复杂的处理,增加延迟和处理开销。

📚二、socket套接字

📖IP与端口号

我们需要清楚一个概念:网络通信到底是谁在通信,是主机设备吗?并不是,例如当你想给别人发信息的时候,是不是需要打开通信软件,在通信软件上编辑信息并发送,这个通信的过程其实是软件,也就是进程在执行,所以网络通信的本质就是进程间通信

然而当我们的数据发送到目标主机时,目标主机怎么知道这个数据是归哪个进程的呢?就是通过端口号来标识进程,定位数据的发送位置:

端口号用来唯一标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理,端口号和IP地址结合可以标识网络上某一台主机的某一个进程,所以在网络通信时,端口号和IP地址两者缺一不可。

❓问题来了,我们知道在操作系统中,进程标识符PID也可以唯一标识进程,那为什么在网络通信时不可以直接用PID,而是引入一个新概念——端口号呢?

✅PID是系统内部管理的,PID的分配、管理方式完全依赖于具体的操作系统,如果网络通信直接使用PID来标识进程,那么不同的操作系统(Windows、Linux、macOS)在进程管理上的不同实现将导致兼容性问题,使得应用无法跨平台运行。

引入端口号的目的是解耦:端口号是跨平台、跨操作系统的标准,无论在哪个系统上,端口号的功能和意义都是一致的。因此,应用程序可以在不同的主机、不同的操作系统上使用相同的端口号。

🔖另外:一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。

📖套接字

我们现在已知,网络通信的本质是不同主机上的进程进行通信。不同主机上的进程要实现跨网络的通信,需要经过OSI七层模型,在每一层呢都需要对数据包进行封装,例如:

① 传输层:添加端口号,用于标识通信的具体进程;

② 网络层:添加IP地址,用于标识源主机与目标主机。等等

如果每一层的封装操作都由用户手动执行,不仅繁琐复杂,而且许多操作是重复的。因此,为了简化流程,操作系统可以自动处理一部分常规任务,而用户只需执行必要的步骤。

🔖任务划分

在网络通信中,用户和操作系统的职责可以划分为:

用户需要完成的任务:

        用户必须决定用哪个进程进行通信,而进程在网络中通过端口号唯一标识,因此封装端口号的工作需要用户手动执行

操作系统可以自动完成的任务:

        例如,IP地址是主机的固定属性,用户无需手动绑定,这项工作可以交由操作系统自动完成。而传输层以下(网络层、数据链路层、物理层)的封装与传输操作,也可以交由操作系统处理,从而简化用户的操作流程。

因此,我们只需要在用户层提供一个接口,用户完成端口号的封装操作后,通过调用接口,操作系统就可以自动完成后续的封装传输操作,而这个接口就是套接字 Socket:

🔖概念

Socket(套接字) 是计算机网络中进程间通信的接口,它提供了一种在网络中不同主机上的进程之间进行数据收发的机制。

在网络通信过程中,Socket充当了应用层与传输层之间的桥梁,帮助应用程序与底层网络协议进行交互,使开发者无需关心底层的复杂细节,只需通过Socket提供的接口进行数据传输。

🔖工作原理

Socket的工作原理如下:

① 创建Socket:应用程序调用操作系统提供的API创建一个套接字对象。

② 绑定地址与端口(服务器端):将Socket绑定到指定的IP地址和端口,使其可以接收来自其他设备的连接请求。

③ 连接(客户端)/监听(服务器):客户端发起连接请求,服务器监听并等待连接。

④ 数据传输:双方建立连接后,进行数据的发送与接收。

🔖分类

Socket分为以下两种类型:

① 流式Socket(TCP):采用TCP协议,提供面向连接、可靠的数据传输;

② 数据报Socket(UDP):采用UDP协议,提供无连接不可靠高效的数据传输。

两种类型都有各自的应用场景,TCP 适用于对数据完整性和顺序性要求较高的场景,而 UDP 适用于对实时性要求较高,而不需要保证可靠性的场景。下面我们分别来介绍这两种协议:

📚三、UDP协议

UDP(User Datagram Protocol,用户数据报协议) 是一种无连接、不可靠、面向报文的传输层协议。它提供了一种简单、高效的通信方式,适用于对实时性要求高,但不需要保证数据完整性的场景,如视频通话、在线游戏、实时广播等。

📖 特点

① 无连接:UDP在通信前不需要建立连接,直接发送数据,不需要"握手"过程。

② 不可靠:UDP不保证数据包的到达、顺序,数据可能会丢失、重复、乱序

③ 面向报文:UDP以报文(Datagram)为单位进行发送,发送方的一个报文就是接收方接收到的完整报文,不会合并或拆分数据。

④ 速度快、开销小:UDP头部仅8字节,相较于TCP的20~60字节,协议开销小,传输效率高。

📖 工作流程

❓提问:进行网络通信的前提是,知晓对方主机的IP地址以及处理通信的进程端口号,但是这些信息我们应该怎么获取呢,由对方告知我们吗,那对方想告知我们是不是也要通过网络通信,那是不是也要知晓我们的IP与端口号信息......这样就陷入一个死循环。了解了Socket的编程模型,就可以回答上述问题了:

Socket的使用可以分为服务器端客户端两种编程模型。服务器端常保持运行状态,并且端口号公开,这样客户端就可以随时向服务端发送数据,客户端发送的数据包还包含了源IP与端口号信息,因此服务器端接受数据后就可以向客户端发送数据了。

那么客户端与客户端之间的通信呢?需要通过服务端,因为客户端只知道服务端的地址信息,而服务端保存了所有与他建立通信信道的客户端地址信息,因此客户端之间的通信实际上是:客户端1->服务端->客户端2。

由此以来,我们就得到了UDP报文通信的工作流程:

🔖服务器端

① 创建 UDP 套接字(socket()

② 绑定 IP 地址和端口(bind()

③ 接收客户端数据(recvfrom()

④ 处理数据并发送响应(sendto()

⑤ 关闭套接字(close()

🔖客户端

① 创建 UDP 套接字(socket()

② 发送数据到服务器(sendto()

③ 接收服务器响应(recvfrom()

④ 关闭套接字(close()

下面我们来具体介绍UDP通信的常见API:

📖 常见API

🔖1.创建套接字
int socket(int domain, int type, int protocol);

① 参数1:domain,表示IP协议类型,AF_INET(IPv4)、AF_INET6(IPv6)

② 参数2:type,表示通信协议类型,SOCK_DGRAM(UDP 套接字)

③ 参数3:protocal,通常为 0(系统自动选择协议)

④ 返回值:成功返回 socket 描述符,失败返回 -1

socket描述符是什么:

✅与文件描述符的使用方式相似,都是指向内核中相关数据结构的整形索引,套接字描述符指向一个socket数据结构,该结构包含网络通信所需的各种信息,如协议类型、端口号、IP地址等。

示例:

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {perror("socket 创建失败");exit(1);
}
🔖2.绑定 IP 地址和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

① 参数1:sockfd,套接字文件描述符

② 参数2:addr,服务器地址结构(sockaddr_in

③ 参数3:addlen,地址结构大小 

④ 返回值:成功返回 0,失败返回 -1

示例:

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind 失败");exit(1);
}
🔖3.发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

① sockfd:套接字描述符

② buf:需要发送的数据

③ len:数据长度

④ flags:一般设置为 0

⑤ dest_addr:目标服务器地址

⑥ addrlen:目标地址大小 

返回值:成功返回发送的字节数,失败返回 -1

示例:

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr);char *message = "Hello, UDP Server!";
sendto(sockfd, message, strlen(message), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
🔖4.接受数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

① sockfd:套接字描述符

② buf:用于存储接收到的数据

③ len:缓冲区大小

④ flags:一般设置为 0

⑤ src_addr:发送方地址

⑥ addrlen:地址长度(调用前需赋初值)

返回值:成功返回接受的字节数,失败返回 -1

示例:

char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);ssize_t received = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_addr, &client_len);
buffer[received] = '\0';
printf("收到客户端数据: %s\n", buffer);
🔖5.关闭套接字
int close(int sockfd);

① sockfd:需要关闭的 socket 描述符

② 返回值:成功返回 0,失败返回 -1

示例:

close(sockfd);

📖 UDP网络程序

下面我们来实现一个简单的基于UDP协议的英译汉服务器:

 🔖程序结构概述

该 UDP 英译汉服务器主要由以下几个部分组成:

① Socket 封装层(udp_socket.hpp):提供 UDP 套接字的基本操作封装,如 socketbindsendtorecvfromclose

② 服务器封装 (udp_server.hpp):创建服务器,绑定地址,接收客户端请求并进行处理。

③ 客户端封装 (udp_client.hpp):负责发送用户输入的查询词,接收服务器返回的结果。

④ 服务器主程序 (dict_server.hpp):读取用户输入,进行英译汉查询,返回翻译结果。

⑤ 客户端主程序 (dict_client.hpp):允许用户输入单词,查询服务器,并输出翻译结果。

🔖详细步骤

服务器端:

① 实现 UdpSocket 套接字封装

class UdpSocket {
public:bool Socket();    // 创建 UDP socketbool Bind(const string &ip, uint16_t port);  // 绑定 IP 和端口bool RecvFrom(string *buf, string *ip, uint16_t *port); // 接收数据bool SendTo(const string &buf, string &ip, uint16_t port); // 发送数据bool Close();     // 关闭 socket
private:int _fd;          // 套接字文件描述符
};

② 实现 UdpServer 服务器端封装

class UdpServer {
public:bool Start(const string &ip, uint16_t port, Handler handler);
private:UdpSocket _sock;
};

其中 Start() 内部逻辑为:

1. 绑定服务器 IP 和端口。

2. 进入循环,等待客户端请求。

3. 解析客户端请求,调用 Translate() 函数处理。

4. 发送翻译结果给客户端。

③ 服务器主程序 

int main(int argc, char* argv[]) {unordered_map<string, string> g_dict = {{"hello", "你好"},{"world", "世界"},{"apple", "苹果"}};UdpServer server;server.Start(argv[1], atoi(argv[2]), Translate);return 0;
}

客户端:

① 实现 UdpClient 客户端封装

class UdpClient {
public:bool SendTo(const string& buf);bool RecvFrom(string *buf);
private:UdpSocket _sock;
};

负责发送单词给服务器,并接收服务器返回的翻译结构。

② 客户端主程序

int main(int argc, char* argv[]) {UdpClient client(argv[1], atoi(argv[2]));for(;;){cout << "请输入单词: ";string word;cin >> word;client.SendTo(word);string result;client.RecvFrom(&result);cout << "翻译: " << result << endl;}return 0;
}
🔖具体代码实现

封装socket套接字:

// udp_socket.hpp
#pragma once
#include <iostream>
using namespace std;
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;class UdpSocket
{
public:UdpSocket():_fd(-1){};bool Socket(){_fd = socket(AF_INET, SOCK_DGRAM, 0);if(_fd < 0){perror("socket");return false;}return true;}bool Close(){close(_fd);return true;}bool Bind(const string &ip, uint16_t port){sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);int ret = bind(_fd, (sockaddr*)&addr, sizeof(addr));if(ret < 0){perror("bind");return false;}return true;}bool RecvFrom(string *buf, string *ip = NULL, uint16_t *port = NULL){char tmp[1024 * 10] = {0};sockaddr_in peer;socklen_t peerlen = sizeof(peer);ssize_t read_size = recvfrom(_fd, tmp, sizeof(tmp)-1, 0, (sockaddr*)&peer, &peerlen);if(read_size < 0){perror("recvform");return false;}buf->assign(tmp, read_size);if(ip != NULL)*ip = inet_ntoa(peer.sin_addr);if(port != NULL)*port = ntohs(peer.sin_port);return true;}bool SendTo(const string &buf, string &ip, uint16_t port){sockaddr_in peer;peer.sin_family = AF_INET;peer.sin_addr.s_addr = inet_addr(ip.c_str());peer.sin_port = htons(port);ssize_t write_size = sendto(_fd, buf.data(), buf.size(), 0, (sockaddr*)&peer, sizeof(peer));if(write_size < 0){perror("sendto");return false;}return true;}private:int _fd;
};

封装服务器:

// udp_server.hpp
#pragma once
#include "udp_socket.hpp"
#include <functional>
typedef function<void(const string&, string *resp)> Handler;class UdpServer
{
public:UdpServer(){assert(_sock.Socket());}~UdpServer(){_sock.Close();}bool Start(const string &ip, uint16_t port, Handler handler){bool ret = _sock.Bind(ip, port);if(!ret) return false;for(;;){string req;string remote_ip;uint16_t remote_port;bool ret = _sock.RecvFrom(&req, &remote_ip, &remote_port);if(!ret) continue;string resp;handler(req, &resp);_sock.SendTo(resp, remote_ip, remote_port);printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port,req.c_str(), resp.c_str());}_sock.Close();return true;}
private:UdpSocket _sock;
};

封装客户端:

// udp_client.hpp
#pragma once
#include "udp_socket.hpp"class UdpClient
{
public:UdpClient(const string& ip, uint16_t port):_ip(ip),_port(port){assert(_sock.Socket());}~UdpClient(){_sock.Close();}bool RecvFrom(string *buf){return _sock.RecvFrom(buf);}bool SendTo(const string& buf){return _sock.SendTo(buf, _ip, _port);}
private:UdpSocket _sock;string _ip;uint16_t _port;
};

服务器程序:

// dict_server.cc#include "udp_server.hpp"
#include <unordered_map>unordered_map<string, string> g_dict;void Translate(const string& req, string *resp){auto it = g_dict.find(req);if(it == g_dict.end()){*resp = "未查到!";return;}*resp = it->second;
}int main(int argc, char* argv[])
{if(argc != 3){cout << "Usage ./dict_server [ip] [port]" << endl;return 1;}g_dict.insert(make_pair("hello", "你好"));g_dict.insert(make_pair("world", "世界"));g_dict.insert(make_pair("C++", "最好的编程语言"));g_dict.insert(make_pair("apple", "苹果"));g_dict.insert(make_pair("banana", "香蕉"));UdpServer server;server.Start(argv[1], atoi(argv[2]), Translate);return 0;
}

客户端程序:

// dict_client.cc#include "udp_client.hpp"int main(int argc, char* argv[])
{if(argc != 3){cout << "Usage ./dict_client [ip] [port]" << endl;return 1;}UdpClient client(argv[1], atoi(argv[2]));for(;;){string word;cout << "请输入您要查询的单词:";cin >> word;if(!cin){cout << "Good Bye" <<endl;break;}client.SendTo(word);string result;client.RecvFrom(&result);cout << word << " 意思是" << result << endl;}return 0;
}
🔖运行步骤

1. 编译项目

# makefile.PHONY: all clean
all: dict_client dict_serverdict_client: dict_client.ccg++ -o $@ $^dict_server: dict_server.ccg++ -o $@ $^clean:rm -f dict_client dict_server

2. 启动服务器

./dict_server [本地IP地址] [端口号]

3.运行客户端

./dict_client [服务器IP地址] [端口号]

运行结果展示:

如此一来,一个基于UDP协议的建议英译汉服务器就创建好了。

下面我们来介绍一下TCP协议:

📚四、TCP协议

TCP协议(Transmission Control Protocol,传输控制协议)是一种面向连接、可靠的传输层协议,它提供了可靠的数据传输服务,确保数据按照顺序到达,且没有丢失。TCP广泛用于要求数据可靠性和顺序性的场景,如HTTP、FTP、SMTP等应用。

📖 特点

① 面向连接:TCP通信开始之前需要先建立连接,通过“三次握手”过程来确保双方准备就绪,保证数据的可靠传输。

② 可靠性:TCP通过序列号、确认应答(ACK)、重传等机制确保数据的完整性和顺序,数据可以在丢失时进行重传。

③ 流量控制与拥塞控制:TCP提供流量控制(防止接收方来不及处理过多的数据)和拥塞控制(防止网络发生拥塞,保证网络稳定性)。

④ 面向字节流:TCP是面向字节流的协议,数据在传输过程中没有边界,接收方接收到的数据是一个字节流,接收方需要根据实际的协议或应用层协议来分割数据。

⑤ 全双工通信:TCP连接是全双工的,数据可以在任意时刻双向流动。

📖 工作流程

我们在编写基于UDP协议的建议服务器时会发现:服务器与客户端之间每一次发送数据时(send())都需要显示传入目标IP与端口号信息;然而TCP通信提供的是可靠的连接通信信道,也就是在每一次通信时,不需要显示传参地址信息,那么具体是怎么实现的呢?

服务器首先创建一个监听套接字,用于监听客户端的连接请求,收到连接请求后,会创建一个新的套接字,存储了对方的地址信息,之后的网络通信就可以通过该套接字完成

而通信连接的建立需要经过三次握手,连接的断开需要经过四次挥手:

🔖三次握手

① 第一次握手:客户端发送SYN包到服务器,表示请求建立连接。(确认客户端可以正常发送)

 第二次握手:服务器接收到SYN包后,返回SYN-ACK包,表示同意建立连接。(确认服务端可以正常接收与正常发生) 

 第三次握手:客户端接收到SYN-ACK包后,返回ACK包,表示连接建立成功。(确认客户端可以正常接收) 

⚠️注:三次握手步骤缺一不可,因为建立连接要确保客户端与服务端都能正常接收、发送数据。 

🔖四次挥手

① 第一次挥手:客户端发送FIN包表示关闭连接。(确认客户端已经准备好关闭连接,且没有数据需要发送)

② 第二次挥手:服务器接收到FIN包,返回ACK包,表示收到关闭请求。(确认服务器已经收到关闭请求,但仍然可能有数据需要发送)

③ 第三次挥手:服务器发送FIN包请求关闭连接。(确认服务器完成数据发送,并准备关闭连接)

④ 第四次挥手:客户端接收到FIN包,返回ACK包,连接终止。 (确认客户端收到关闭请求并且关闭连接)

⚠️注:四次挥手同样缺一不可,因为每一方需要有足够的时间来完成数据的发送与接收,确保双方都能完全关闭连接并释放资源。 

 📖常见API

其中创建套接字(socket())、绑定本地(bind())与UDP通信一样,就不再赘述。

🔖3.监听端口
int listen(int sockfd, int backlog);

参数1sockfd,套接字描述符

参数2backlog,连接请求队列的大小(即最多等待多少个连接)

返回值:成功返回0,失败返回-1

示例: 

if (listen(sockfd, 5) == -1) {perror("listen 失败");exit(1);
}
🔖4. 接收连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数1sockfd,监听套接字描述符

参数2addr,客户端地址结构(sockaddr_in

参数3addrlen,地址结构大小

返回值:成功返回客户端套接字描述符,失败返回-1

示例:

int client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
if (client_sock == -1) {perror("accept 失败");exit(1);
}
🔖5. 发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

sockfd:套接字描述符

buf:数据缓冲区

len:数据长度

flags:一般设置为0

返回值:成功返回发送的字节数,失败返回-1

示例:

send(sockfd, "Hello, TCP!", 12, 0);
🔖6. 接收数据 
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

sockfd:套接字描述符

buf:接收数据的缓冲区

len:缓冲区大小

flags:一般设置为0

返回值:成功返回接收的字节数,失败返回-1

示例:

char buffer[1024];
ssize_t bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);
🔖7.关闭套接字
int close(int sockfd);

sockfd:需要关闭的套接字描述符

返回值:成功返回0,失败返回-1

示例:

close(sockfd);

📖 TCP网络程序

下面我们来实现基于TCP协议的英译汉服务器:

🔖程序结构概述

该TCP英译汉服务器主要由以下几个部分组成:

Socket封装层 (tcp_socket.hpp):提供TCP套接字的基本操作封装,如socketbindsendrecvclose等。

服务器封装 (tcp_server.hpp):创建服务器,绑定地址,接收客户端请求并进行处理。

客户端封装 (tcp_client.hpp):负责发送用户输入的查询词,接收服务器返回的翻译结果。

服务器主程序 (dict_server.cpp):读取用户输入,进行英译汉查询,返回翻译结果。

客户端主程序 (dict_client.cpp):允许用户输入单词,查询服务器,并输出翻译结果。

🔖详细步骤

服务器端

① 实现TcpSocket套接字封装

class TcpSocket {
public:TcpSocket() :_fd(-1) {}TcpSocket(int fd) :_fd(fd) {}bool Socket();bool Close();bool Bind(const string &ip, uint16_t port);bool Listen(int backlog) const;bool Accept(TcpSocket *new_sock, string *ip = NULL, uint16_t *port = NULL);bool Recv(string* buf);bool Send(const string &buf);bool Connect(const string &ip, uint16_t port);int GetFd() const;private:int _fd;
};

② 实现TcpServer服务器端封装 

class TcpServer {
public:TcpServer(const string& ip, uint16_t port) : _ip(ip), _port(port) {}bool Start(Handler handler);
private:TcpSocket _sock;string _ip;uint16_t _port;
};

③ 服务器主程序 

int main(int argc, char* argv[]) {if (argc != 3) {printf("Usage: ./dict_server [ip] [port]\n");return 1;}unordered_map<string, string> g_dict = {{"hello", "你好"},{"world", "世界"},{"apple", "苹果"}};TcpServer server(argv[1], atoi(argv[2]));server.Start(Translate);return 0;
}

客户端

① 实现TcpClient客户端封装 

class TcpClient {
public:TcpClient(const string &ip, uint16_t port) : _ip(ip), _port(port) {assert(_sock.Socket());}~TcpClient() { _sock.Close(); }bool Connect();bool Recv(string *buf);bool Send(const string &buf);private:TcpSocket _sock;string _ip;uint16_t _port;
};

② 客户端主程序 

int main(int argc, char* argv[]) {if (argc != 3) {printf("Usage: ./dict_client [ip] [port]\n");return 1;}TcpClient client(argv[1], atoi(argv[2]));bool ret = client.Connect();if (!ret)return 1;for (;;) {cout << "请输入您要查询的单词:";string word;cin >> word;if (!cin)break;client.Send(word);string result;client.Recv(&result);cout << word << " 的意思是:" << result << endl;}return 0;
}
🔖具体代码实现

封装socket套接字:

// tcp_socket.hpp#pragma once
#include <iostream>
using namespace std;
#include <string>
#include <stdlib.h>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;#define CHECK_RET(exp) if(!exp) {\return false;\
}class TcpSocket
{
public:TcpSocket() :_fd(-1) { }TcpSocket(int fd) :_fd(fd) { }bool Socket(){_fd = socket(AF_INET, SOCK_STREAM, 0);if(_fd < 0){perror("socket");return false;}printf("Open fd = %d\n", _fd);return true;}bool Close(){close(_fd);printf("Close fd = %d\n", _fd);return true;}bool Bind(const string &ip, uint16_t port){sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);int ret = bind(_fd, (sockaddr*)&addr, sizeof(addr));if(ret < 0){perror("bind");return false;}return true;}bool Listen(int backlog) const{int ret = listen(_fd ,backlog);if(ret < 0){perror("listen");return false;}return true;}bool Accept(TcpSocket *new_sock, string *ip = NULL, uint16_t *port = NULL){sockaddr_in peer;socklen_t peer_len = sizeof(peer);int new_fd = accept(_fd, (sockaddr*)&peer, &peer_len);if(new_fd < 0){perror("accept");return false;}new_sock->_fd = new_fd;if(ip != NULL)*ip = inet_ntoa(peer.sin_addr);if(port != NULL)*port = ntohs(peer.sin_port);return true;}bool Recv(string* buf){buf->clear();char tmp[1024*10] = {0};ssize_t num = recv(_fd, tmp, sizeof(tmp), 0);if(num < 0){perror("recv");return false;}if(num == 0)return false;buf->assign(tmp, num);return true;}bool Send(const string &buf){ssize_t num = send(_fd, buf.data(), buf.size(), 0);if(num < 0){perror("send");return false;}return true;}bool Connect(const string &ip, uint16_t port){sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);int ret = connect(_fd, (sockaddr*)&addr, sizeof(addr));if(ret < 0){perror("connect");return false;}return true;}int GetFd() const{return _fd;}private:int _fd;
};

封装服务器:

// tcp_server.hpp#pragma once
#include "tcp_socket.hpp"
#include <functional>
typedef function<void(const string &req, string *resp)> Handler;class TcpServer
{
public:TcpServer(const string& ip, uint16_t port):_ip(ip),_port(port){}bool Start(Handler handler){CHECK_RET(_sock.Socket());CHECK_RET(_sock.Bind(_ip, _port));CHECK_RET(_sock.Listen(5));for(;;){TcpSocket new_sock;string peer_ip;uint16_t peer_port = 0;bool ret = _sock.Accept(&new_sock, &peer_ip, &peer_port);if(!ret)continue;printf("[Client %s:%d] Connect!\n", peer_ip.c_str(), peer_port);// 在链路中循环收发数据for(;;){string req;bool ret = new_sock.Recv(&req);if(!ret){printf("[Client %s:%d] Disconnect!\n", peer_ip.c_str(), _port);new_sock.Close();break;}string resp;handler(req, &resp);new_sock.Send(resp);printf("[%s:%d] req: %s, resp: %s\n",_ip.c_str(), _port, req.c_str(), resp.c_str());}}return true;}private:TcpSocket _sock;string _ip;uint16_t _port;
};

封装客户端:

// tcp_client.hpp#pragma once
#include "tcp_socket.hpp"class TcpClient
{
public:TcpClient(const string &ip, uint16_t port):_ip(ip),_port(port){assert(_sock.Socket());}~TcpClient(){_sock.Close();}bool Connect(){return _sock.Connect(_ip, _port);}bool Recv(string *buf){return _sock.Recv(buf);}bool Send(const string &buf){return _sock.Send(buf);}private:TcpSocket _sock;string _ip;uint16_t _port;
};

服务器程序:

#include "tcp_thread_server.hpp"
#include <unordered_map>unordered_map<string, string> g_dict;void Translate(const string& req, string *resp)
{auto it = g_dict.find(req);if(it == g_dict.end()){*resp = "未找到";return;}*resp = it->second;return;
}int main(int argc, char* argv[])
{if(argc != 3){printf("Usage: ./dict_server [ip] [port]\n");return 1;}g_dict.insert(make_pair("hello", "你好"));g_dict.insert(make_pair("world", "世界"));g_dict.insert(make_pair("C++", "世界上最好的语言!"));g_dict.insert(make_pair("apple", "苹果"));g_dict.insert(make_pair("banana", "香蕉"));TcpServer server(argv[1], atoi(argv[2]));server.Start(Translate);return 0;
}

客户端程序:

#include "tcp_client.hpp"int main(int argc, char* argv[])
{if(argc != 3){printf("Usage: ./dict_client [ip] [port]\n");return 1;}TcpClient client(argv[1], atoi(argv[2]));bool ret = client.Connect();if(!ret)return 1;for(;;){cout << "请输入您要查询的单词:";string word;cin >> word;if(!cin)break;client.Send(word);string result;client.Recv(&result);cout << word << " 的意思是:" << result << endl;}         return 0;
}
🔖运行步骤

1. 编译项目

# makefile.PHONY:all clean
all:dict_server dict_clientdict_server:dict_server.ccg++ -o $@ $^dict_client:dict_client.ccg++ -o $@ $^clean:rm -f dict_server dict_client

2. 启动服务器

./dict_server [本地IP地址] [端口号]

3.运行客户端

./dict_client [服务器IP地址] [端口号]

运行结果展示:

🔖改进(支持并发)

一个服务器肯定是要支持并发访问,但是我们实现的英译汉服务器支持吗:

我们发现,当启动第二个客户端, 尝试连接服务器时,其不能正确的和服务器进行通信,分析原因,是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接 受新的请求

所以我们要对服务器程序进行改进,使其支持并发访问的情况,我们可以使用多进程多线程

多进程:

在多进程模型中,我们让服务器每接受一个客户端连接请求后,创建一个新的子进程来处理该客户端的请求。父进程会继续监听新的连接请求,从而保证每个连接都可以被独立处理。

#pragma once
#include "tcp_socket.hpp"
#include <signal.h>
#include <functional>
typedef function<void(const string &req, string *resp)> Handler;class TcpServer
{
public:TcpServer(const string& ip, uint16_t port):_ip(ip),_port(port){signal(SIGCHLD, SIG_IGN);}void ProcessServer(TcpSocket &sock, string &ip, uint16_t port, Handler handler){int pid = fork();if(pid > 0){sock.Close();return;}else if(pid == 0){// 在链路中循环收发数据for(;;){string req;bool ret = sock.Recv(&req);if(!ret){printf("[Client %s:%d] Disconnect!\n", ip.c_str(), port);exit(0);break;}string resp;handler(req, &resp);sock.Send(resp);printf("[%s:%d] req: %s, resp: %s\n",ip.c_str(), port, req.c_str(), resp.c_str());}}elseperror("fork");}bool Start(Handler handler){CHECK_RET(_sock.Socket());CHECK_RET(_sock.Bind(_ip, _port));CHECK_RET(_sock.Listen(5));for(;;){TcpSocket new_sock;string peer_ip;uint16_t peer_port = 0;bool ret = _sock.Accept(&new_sock, &peer_ip, &peer_port);if(!ret)continue;printf("[Client %s:%d] Connect!\n", peer_ip.c_str(), _port);ProcessServer(new_sock, peer_ip, peer_port, handler);}return true;}private:TcpSocket _sock;string _ip;uint16_t _port;
};

运行结果展示:

我们可以看到,此时两个客户端可以并发访问服务器。

多线程: 

在多线程模型中,我们让服务器为每个客户端连接创建一个新的线程来处理请求,而父线程则继续监听新的连接请求。与多进程相比,多线程会消耗更少的系统资源(因为线程共享进程的内存空间)。 

#pragma once
#include "tcp_socket.hpp"
#include <functional>
typedef function<void(const string &req, string *resp)> Handler;class TcpServer
{
public:TcpServer(const string& ip, uint16_t port):_ip(ip),_port(port){}typedef struct ThreadArg{TcpSocket _sock;string _ip;uint16_t _port;Handler _handler;} ThreadArg;bool Start(Handler handler){CHECK_RET(_sock.Socket());CHECK_RET(_sock.Bind(_ip, _port));CHECK_RET(_sock.Listen(5));for(;;){ThreadArg *arg = new ThreadArg();arg->_handler = handler;bool ret = _sock.Accept(&arg->_sock, &arg->_ip, &arg->_port);if(!ret)continue;printf("[Client %s:%d] Connect!\n", arg->_ip.c_str(), arg->_port);pthread_t tid;if(pthread_create(&tid, NULL, ThreadExec, arg) != 0)perror("pthread_create");pthread_detach(tid);}return true;}static void *ThreadExec(void *arg){ThreadArg *p = reinterpret_cast<ThreadArg*>(arg);// 在链路中循环收发数据for(;;){string req;bool ret = p->_sock.Recv(&req);if(!ret){printf("[Client %s:%d] Disconnect!\n", p->_ip.c_str(), p->_port);break;}string resp;p->_handler(req, &resp);p->_sock.Send(resp);printf("[%s:%d] req: %s, resp: %s\n",p->_ip.c_str(), p->_port, req.c_str(), resp.c_str());}// 释放内存,关闭描述符p->_sock.Close();delete p;return NULL;}private:TcpSocket _sock;string _ip;uint16_t _port;
};

运行结果展示:

此时两个客户端也可以并发访问服务器了


以上就是【初识计算机网络 & Socket套接字 & TCP/UDP协议】的全部内容,欢迎指正~ 

码文不易,还请多多关注支持,这是我持续创作的最大动力!  

相关文章:

【Linux-网络】初识计算机网络 Socket套接字 TCP/UDP协议(包含Socket编程实战)

&#x1f3ac; 个人主页&#xff1a;谁在夜里看海. &#x1f4d6; 个人专栏&#xff1a;《C系列》《Linux系列》《算法系列》 ⛰️ 道阻且长&#xff0c;行则将至 目录 &#x1f4da;一、初识计算机网络 &#x1f4d6; 背景 &#x1f4d6; 网络协议 &#x1f516;OSI七层…...

三数之和(15)

15. 三数之和 - 力扣&#xff08;LeetCode&#xff09; 可以一起总结的题目&#xff1a;三角形的最大周长&#xff08;976&#xff09;-CSDN博客 解法&#xff1a; class Solution { public:vector<vector<int>> threeSum(vector<int>& nums) {vector…...

6 Flink 状态管理

6 Flink 状态管理 1. State-Keyed State2. State-Operator State3. Broadcast State 我们前面写的 wordcount 的例子&#xff0c;没有包含状态管理。如果一个task在处理过程中挂掉了&#xff0c;那么它在内存中的状态都会丢失&#xff0c;所有的数据都需要重新计算。从容错和消…...

物联网 STM32【源代码形式-使用以太网】连接OneNet IOT从云产品开发到底层MQTT实现,APP控制 【保姆级零基础搭建】

物联网&#xff08;IoT&#xff09;‌是指通过各种信息传感器、射频识别技术、全球定位系统、红外感应器等装置与技术&#xff0c;实时采集并连接任何需要监控、连接、互动的物体或过程&#xff0c;实现对物品和过程的智能化感知、识别和管理。物联网的核心功能包括数据采集与监…...

elasticsearch8.15 高可用集群搭建(含认证Kibana)

文章目录 1.资源配置2.系统参数优化3.JDK17安装4.下载&安装ES 8.155.生成ES的证书(用于ES节点之间进行安全数据传输)6.修改ES 相关配置文件7.创建es用户并启动8.配置ES的账号和密码(用于ES服务端和客户端)9.下载和安装Kibana10.编辑Kibana配置文件11.启动Kiabana12.访问Kia…...

如何实现滑动网格的功能

文章目录 1 概念介绍2 使用方法3 示例代码 我们在上一章回中介绍了SliverList组件相关的内容&#xff0c;本章回中将介绍SliverGrid组件.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1 概念介绍 我们在本章回中介绍的SliverGrid组件是一种网格类组件&#xff0c;主要用来…...

DBASE DBF数据库文件解析

基于Java实现DBase DBF文件的解析和显示 JDK19编译运行&#xff0c;实现了数据库字段和数据解析显示。 首先解析数据库文件头代码 byte bytes[] Files.readAllBytes(Paths.get(file));BinaryBufferArray bis new BinaryBufferArray(bytes);DBF dbf new DBF();dbf.VersionN…...

linux中统计文件中特定单词或字符串的出现次数

在 Linux 中&#xff0c;可以使用 grep 和 wc 命令组合来统计一个文件中特定单词或字符串的个数。假设想统计文件 example.txt 中字符串 “example_string” 出现的次数&#xff0c;可以使用以下命令&#xff1a; grep -o -w example_string example.txt | wc -l这里是每个选项…...

视觉状态空间模型(VMamba)的解读

在计算机视觉领域&#xff0c;设计计算高效的网络架构一直是研究的热点。今天&#xff0c;我想和大家分享一篇发表在 NIPS 2024 上的论文——VMamba&#xff1a;Visual State Space Model&#xff0c;这篇论文提出了一种新的视觉骨干网络&#xff0c;具有线性时间复杂度&#x…...

几种K8s运维管理平台对比说明

目录 深入体验**结论**对比分析表格**1. 功能对比****2. 用户界面****3. 多租户支持****4. DevOps支持** 细对比分析1. **Kuboard**2. **xkube**3. **KubeSphere**4. **Dashboard****对比总结** 深入体验 KuboardxkubeKubeSphereDashboard 结论 如果您需要一个功能全面且适合…...

nodejs:js-mdict 的下载、安装、测试、build

js-mdict 项目的目录结构&#xff1a;js-mdict 项目教程 js-mdict 下载地址: js-mdict-master.zip 先解压到 D:\Source\ js-mdict 6.0.2 用了 ts (TypeScript) 和 Jest&#xff0c;增加了应用开发的难度&#xff0c;因为先要了解 ts 和 Jest。 参阅&#xff1a;测试与开发&a…...

Vue3 表单:全面解析与最佳实践

Vue3 表单&#xff1a;全面解析与最佳实践 引言 随着前端技术的发展&#xff0c;Vue.js 已经成为最受欢迎的前端框架之一。Vue3 作为 Vue.js 的最新版本&#xff0c;带来了许多改进和新的特性。其中&#xff0c;表单处理是 Vue 应用中不可或缺的一部分。本文将全面解析 Vue3 …...

JavaWeb入门-请求响应(Day3)

(一)请求响应概述 请求(HttpServletRequest):获取请求数据 响应(HttpServletResponse):设置响应数据 BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器就可访问,应用程序的逻辑和数据都存储在服务端(维护方便,响应速度一般) CS架构:Client/ser…...

【单层神经网络】基于MXNet库简化实现线性回归

写在前面 同最开始的两篇文章 完整程序及注释 导入使用的库# 基本 from mxnet import autograd, nd, gluon # 模型、网络 from mxnet.gluon import nn from mxnet import init # 学习 from mxnet.gluon import loss as gloss # 数据集 from mxnet.gluon…...

一元函数微积分的几何应用:二维平面光滑曲线的曲率公式

文章目录 前言曲率和曲率半径的定义曲率计算公式参数方程形式直角坐标显式方程形式极坐标形式向量形式 前言 本文将介绍二维平面光滑曲线的曲率定义以及不同形式的曲率及曲率半径公式的推导。 曲率和曲率半径的定义 &#xff08;关于二维平面光滑曲线的定义以及弧长公式请参…...

编程题-最接近的三数之和

题目&#xff1a; 给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数&#xff0c;使它们的和与 target 最接近。 返回这三个数的和。 假定每组输入只存在恰好一个解。 解法一&#xff08;排序双指针&#xff09;&#xff1a; 题目要求找…...

【LLM-agent】(task4)搜索引擎Agent

note 新增工具&#xff1a;搜索引擎Agent 文章目录 note一、搜索引擎AgentReference 一、搜索引擎Agent import os from dotenv import load_dotenv# 加载环境变量 load_dotenv() # 初始化变量 base_url None chat_model None api_key None# 使用with语句打开文件&#xf…...

string类详解

为什么学习string类&#xff1f; 1.1 C语言中的字符串 C语言中&#xff0c;字符串是以\0结尾的一些字符的集合&#xff0c;为了操作方便&#xff0c;C标准库中提供了一些str系列的库函数&#xff0c;但是这些库函数与字符串是分离开的&#xff0c;不太符合OOP的思想&#xf…...

【含文档+PPT+源码】基于微信小程序农家乐美食餐厅预约推广系统

项目介绍 本课程演示的是一款基于微信小程序农家乐美食餐厅预约推广系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Java 学习者。 1.包含&#xff1a;项目源码、项目文档、数据库脚本、软件工具等所有资料 2.带你从零开始部署运行本套系统 …...

享元模式——C++实现

目录 1. 享元模式简介 2. 代码示例 1. 享元模式简介 享元模式是一种结构型模式。 享元模式用于缓存共享对象&#xff0c;降低内存消耗。共享对象相同的部分&#xff0c;避免创建大量相同的对象&#xff0c;减少内存占用。 享元模式需要将对象分成内部状态和外部状态两个部分…...

《苍穹外卖》项目学习记录-Day11订单统计

根据起始时间和结束时间&#xff0c;先把begin放入集合中用while循环当begin不等于end的时候&#xff0c;让begin加一天&#xff0c;这样酒吧把这个区间内的时间放到List集合。 查询每天的订单总数也就是查询的时间段是大于当天的开始时间&#xff08;0点0分0秒&#xff09;小…...

Python3 OS模块中的文件/目录方法说明十六

一. 简介 前面文章简单学习了 Python3 中 OS模块中的文件/目录的部分函数。 本文继续来学习 OS 模块中文件、目录的操作方法&#xff1a;os.unlink() 方法、os.utime()方法。 二. Python3 OS模块中的文件/目录方法 1. os.unlink() 方法 os.unlink() 方法用于删除文件,如果文…...

(二)QT——按钮小程序

目录 前言 按钮小程序 1、步骤 2、代码示例 3、多个按钮 ①信号与槽的一对一 ②多对一&#xff08;多个信号连接到同一个槽&#xff09; ③一对多&#xff08;一个信号连接到多个槽&#xff09; 结论 前言 按钮小程序 Qt 按钮程序通常包含 三个核心文件&#xff1a; m…...

图论——spfa判负环

负环 图 G G G中存在一个回路&#xff0c;该回路边权之和为负数&#xff0c;称之为负环。 spfa求负环 方法1:统计每个点入队次数, 如果某个点入队n次, 说明存在负环。 证明&#xff1a;一个点入队n次&#xff0c;即被更新了n次。一个点每次被更新时所对应最短路的边数一定是…...

96,【4】 buuctf web [BJDCTF2020]EzPHP

进入靶场 查看源代码 GFXEIM3YFZYGQ4A 一看就是编码后的 1nD3x.php 访问 得到源代码 <?php // 高亮显示当前 PHP 文件的源代码&#xff0c;用于调试或展示代码结构 highlight_file(__FILE__); // 关闭所有 PHP 错误报告&#xff0c;防止错误信息泄露可能的安全漏洞 erro…...

Rust 的基本类型有哪些,他们存在堆上还是栈上,是否可以COPY?

Rust 的基本类型主要包括以下几类&#xff1a; 1. 整数类型&#xff08;Integer&#xff09; Rust 提供了有符号和无符号的整数类型&#xff1a; 有符号整数&#xff08;i8, i16, i32, i64, i128, isize&#xff09;无符号整数&#xff08;u8, u16, u32, u64, u128, usize&a…...

函数与递归

函数与递归 声明或者定义应该在使用之前&#xff08;不单单针对于函数&#xff09; 函数对全局变量做出的改变还是不会随着函数结束而消失的 函数声明在main函数里面也是可以的 引用变量和引用实体的变化是一样的 传址调用比传值调用效率高 重载函数->编译器会根据传递…...

UE5 蓝图学习计划 - Day 11:材质与特效

在游戏开发中&#xff0c;材质&#xff08;Material&#xff09;与特效&#xff08;VFX&#xff09; 是提升视觉体验的关键元素。Unreal Engine 5 提供了强大的 材质系统 和 粒子系统&#xff08;Niagara&#xff09;&#xff0c;让开发者可以通过蓝图控制 动态材质、光效变化、…...

DeepSeek 详细使用教程

1. 简介 DeepSeek 是一款基于人工智能技术的多功能工具&#xff0c;旨在帮助用户高效处理和分析数据、生成内容、解答问题、进行语言翻译等。无论是学术研究、商业分析还是日常使用&#xff0c;DeepSeek 都能提供强大的支持。本教程将详细介绍 DeepSeek 的各项功能及使用方法。…...

低代码系统-产品架构案例介绍、炎黄盈动-易鲸云(十二)

易鲸云作为炎黄盈动新推出的产品&#xff0c;在定位上为低零代码产品。 开发层 表单引擎 表单设计器&#xff0c;包括设计和渲染 流程引擎 流程设计&#xff0c;包括设计和渲染&#xff0c;需要说明的是&#xff1a;采用国际标准BPMN2.0&#xff0c;可以全球通用 视图引擎 视图…...

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】1.27 线性代数王国:矩阵分解实战指南

1.27 线性代数王国&#xff1a;矩阵分解实战指南 #mermaid-svg-JWrp2JAP9qkdS2A7 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JWrp2JAP9qkdS2A7 .error-icon{fill:#552222;}#mermaid-svg-JWrp2JAP9qkdS2A7 .erro…...

【C语言】动态内存管理

1、为什么存在动态内存分配&#xff1f;2、动态内存管理函数介绍&#xff08;1&#xff09;malloc&#xff08;2&#xff09;free&#xff08;3&#xff09;calloc&#xff08;4&#xff09;realloc 3、常见动态内存错误&#xff08;1&#xff09;使用free释放动态内存开辟的一…...

Cocoa和Cocoa Touch是什么语言写成的?什么是Cocoa?编程语言中什么是框架?为什么苹果公司Cocoa类库有不少NS前缀?Swift编程语言?

Cocoa和Cocoa Touch是什么语言写成的? 二者主要都是用Objective-C语言编写而成的。 什么是Cocoa? Cocoa是苹果操作系统macOS和iOS上的应用程序开发框架集合&#xff0c;核心语言是Objective-C编程语言&#xff0c;在移动平台被称为Cocoa Touch&#xff0c;Cocoa包含多个子框架…...

Qt Creator 中使用 vcpkg

Qt Creator 中使用 vcpkg Qt Creator 是一个跨平台的轻量级 IDE&#xff0c;做 Qt 程序开发的同学们肯定对这个 IDE 都比较属于。这个 IDE 虽然没有 Visual Stdio 功能那么强&#xff0c;但是由于和 Qt 集成的比较深&#xff0c;用来开发 Qt 程序还是很顺手的。 早期&#xf…...

Python GUI 开发 | PySide6 PyQt6 学习手册

本文是个 Python GUI 开发的目录&#xff0c;方便读者系统性学习的&#xff0c;笔者后续会满满填充此目录中的内容&#xff0c;感兴趣的小伙伴可以关注一手。&#xff08;主要是偏向 PySide6 方向的&#xff09; 0x01&#xff1a;PySide6 & PyQt6 基础入门 0x0101&#xff…...

Xposed-Hook

配置 Xposed 模块的 AndroidManifest.xml&#xff1a; <?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.com/apk/res/android"package"your.package.name"><applicationandr…...

鸿蒙物流项目之基础结构

目录&#xff1a; 1、项目结构2、三种包的区别和使用场景3、静态资源的导入4、颜色样式设置5、修改项目名称和图标6、静态包基础目录7、组件的抽离8、在功能模块包里面引用静态资源包的组件 1、项目结构 2、三种包的区别和使用场景 3、静态资源的导入 放在har包中&#xff0c;那…...

CSS 中调整元素大小的全面指南

CSS 中调整元素大小的全面指南 1. 原始尺寸&#xff08;固有尺寸&#xff09;示例代码&#xff1a;图像的固有尺寸 2. 设置具体的尺寸示例代码&#xff1a;设置固定宽度和高度 3. 使用百分比示例代码&#xff1a;使用百分比设置宽度 4. 使用百分比作为外边距和内边距示例代码&a…...

文字投影效果

大家好&#xff0c;我是喝西瓜汁的兔叽&#xff0c;今天给大家分享一个常见的文字投影效果。 效果展示 我们来实现一个这样的文字效果。 思路分析 这样的效果如何实现的呢? 实际上是两组相同的文字&#xff0c;叠合在一块&#xff0c;只不过对应的css不同罢了。 首先&…...

【Redis】set 和 zset 类型的介绍和常用命令

1. set 1.1 介绍 set 类型和 list 不同的是&#xff0c;存储的元素是无序的&#xff0c;并且元素不允许重复&#xff0c;Redis 除了支持集合内的增删查改操作&#xff0c;还支持多个集合取交集&#xff0c;并集&#xff0c;差集 1.2 常用命令 命令 介绍 时间复杂度 sadd …...

Clion开发STM32时使用stlink下载程序与Debug调试

一、下载程序 先创建一个文件夹&#xff1a; 命名&#xff1a;stlink.cfg 写入以下代码: # choose st-link/j-link/dap-link etc. #adapter driver cmsis-dap #transport select swdsource [find interface/stlink.cfg]transport select hla_swdsource [find target/stm32f4x.…...

HarmonyOS:给您的应用添加通知

一、通知介绍 通知旨在让用户以合适的方式及时获得有用的新消息&#xff0c;帮助用户高效地处理任务。应用可以通过通知接口发送通知消息&#xff0c;用户可以通过通知栏查看通知内容&#xff0c;也可以点击通知来打开应用&#xff0c;通知主要有以下使用场景&#xff1a; 显示…...

scrape登录(js逆向)

url:链接 首先登录抓包 可以 看到token进行了加密 可以直接去全局搜索token 可以看到在这里进行了加密 进入encode&#xff0c;处理from后进行加密 首先这一段复制js&#xff0c;缺什么补什么&#xff0c; code cb_utob function(e) {if (e.length < 2) {var r e.cha…...

后台管理系统通用页面抽离=>高阶组件+配置文件+hooks

目录结构 配置文件和通用页面组件 content.config.ts const contentConfig {pageName: "role",header: {title: "角色列表",btnText: "新建角色"},propsList: [{ type: "selection", label: "选择", width: "80px&q…...

RDP协议详解

以下内容包含对 RDP&#xff08;Remote Desktop Protocol&#xff0c;远程桌面协议&#xff09;及其开源实现 FreeRDP 的较为系统、深入的讲解&#xff0c;涵盖协议概要、历史沿革、核心原理、安全机制、安装与使用方法、扩展与未来发展趋势等方面&#xff0c; --- ## 一、引…...

Spring Boot - 数据库集成06 - 集成ElasticSearch

Spring boot 集成 ElasticSearch 文章目录 Spring boot 集成 ElasticSearch一&#xff1a;前置工作1&#xff1a;项目搭建和依赖导入2&#xff1a;客户端连接相关构建3&#xff1a;实体类相关注解配置说明 二&#xff1a;客户端client相关操作说明1&#xff1a;检索流程1.1&…...

虚幻UE5手机安卓Android Studio开发设置2025

一、下载Android Studio历史版本 步骤1&#xff1a;虚幻4.27、5.0、5.1、5.2官方要求Andrd Studio 4.0版本&#xff1b; 5.3、5.4、5.5官方要求的版本为Android Studio Flamingo | 2022.2.1 Patch 2 May 24, 2023 虚幻官网查看对应Andrd Studiob下载版本&#xff1a; https:/…...

Flutter开发环境配置

下载 Flutter SDK 下载地址&#xff1a;https://docs.flutter.cn/get-started/install M1/M2芯片选择带arm64字样的Flutter SDK。 解压 cd /Applications unzip ~/Downloads/flutter_macos_arm64_3.27.3-stable.zip执行 /Applications/flutter/bin/flutterManage your Flut…...

[免费]微信小程序智能商城系统(uniapp+Springboot后端+vue管理端)【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序智能商城系统(uniappSpringboot后端vue管理端)&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序智能商城系统(uniappSpringboot后端vue管理端) Java毕业设计_哔哩哔哩_bilibili 项目介绍…...

rust操作pgsql、mysql和sqlite

rust中&#xff0c;有很多技术可以操作pgsql、mysql和sqlite&#xff0c;以sqlx为主流技术。我们以sqlx操作sqlite为示例&#xff0c;操作pgsql和mysql的办法是一样的。 Cargo.toml: [package] name "test" version "0.1.0" edition "2021"…...