网络编程学习笔记

参考:
套接字通信部分
《TCP/IP 网络编程》以及《TCP/IP网络编程》学习笔记

socket 编程

1. 字节序

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,也就是说对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。
目前在各种体系的计算机中通常采用的字节存储机制主要有两种:Big-EndianLittle-Endian,下面先从字节序说起。

Little-Endian -> 主机字节序 (小端)

  • 数据的低位字节存储到内存的低地址位, 数据的高位字节存储到内存的高地址位
  • 我们使用的PC机,数据的存储默认使用的是小端

Big-Endian -> 网络字节序 (大端)

  • 据的低位字节存储到内存的高地址位, 数据的高位字节存储到内存的低地址位
  • 套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口

以 PC 机为例:

int a = 0x12345678;	// 从左往右,是从高位到低位
char *p = (char *) &a;
printf("sizeof(int, char) = %d, %d\n", sizeof(int), sizeof(char));
for(int i = 0; i < sizeof(int); ++i) {
	printf("%d %p : 0x%02x\n", i, p, *p);
	p++;
}
// 			运行结果
/*
		sizeof(int, char) = 4, 1
		0 000000000070fe10 : 0x78
		1 000000000070fe11 : 0x56
		2 000000000070fe12 : 0x34
		3 000000000070fe13 : 0x12
*/

大小端转换函数

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

// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int

// 主机字节序 -> 网络字节序
u_short htons (u_short hostshort );
u_long htonl ( u_long hostlong);

// 网络字节序 -> 主机字节序
u_short ntohs (u_short netshort );
u_long ntohl ( u_long netlong);

// linux函数, window上没有这两个函数
inet_ntop(); 
inet_pton();

// windows 和 linux 都使用, 只能处理ipv4的ip地址
// 点分十进制IP -> 大端整形
unsigned long inet_addr (const char FAR * cp);	// windows
in_addr_t     inet_addr (const char *cp);			// linux

// 大端整形 -> 点分十进制IP
// window, linux相同
char* inet_ntoa(struct in_addr in);

2. IP 地址转换

虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换:

主机字节序的IP地址 ---> 网络字节序

// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst); 

参数:

  • af: 地址族(IP地址的家族包括ipv4和ipv6)协议,AF_INET: ipv4格式的ip地址, AF_INET6: ipv6格式的ip地址
  • src: 传入参数, 对应要转换的点分十进制的ip地址: 192.168.1.100
  • dst: 传出参数, 函数调用完成, 转换得到的大端整形IP被写入到这块内存中

返回值:成功返回1,失败返回0或者-1;返回0是异常, 说明src指向的不是一个有效的ip地址。

#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址        
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数:

  • af: 地址族协议,AF_INET: ipv4格式的ip地址,AF_INET6: ipv6格式的ip地址
  • src: 传入参数,指向存储了大端的整形IP地址 的内存
  • dst: 传出参数, 指向存储了小端的点分十进制的IP地址 的内存
  • size: 修饰 dst 参数的, 标记 dst 指向的内存中最多可以存储多少个字节

返回值:

  • 成功: 返回指向 dst 对应的内存地址的指针, 通过返回值也可以直接取出转换得到的IP字符串
  • 失败: NULL

3. sockaddr 数据结构

在这里插入图片描述

  • AF_LOCAL:表示的是本地地址,对应的是 Unix 套接字,这种情况一般用于本地 socket 通信,很多情况下也可以写成 AF_UNIXAF_FILE
  • AF_INET:因特网使用的 IPv4 地址
  • AF_INET6:因特网使用的 IPv6 地址

这里的 AF_ 表示的含义是 Address Family,但是很多情况下,我们也会看到以PF_表示的,比如 PF_INETPF_INET6 等,实际上 PF_ 的意思是 Protocol Family,也就是协议族的意思。我们用 AF_xxx 这样的值来初始化 socket 地址,用 PF_xxx 这样的值来初始化 socket。我们在 <sys/socket.h> 头文件中可以清晰地看到,这两个值本身就是一一对应的。

在这里插入图片描述

通用套接字地址(struct sockaddr)

typedef unsigned short int sa_family_t;
struct sockaddr	{	//早期的 sockaddr
	sa_family_t 	sa_family;		/* adress family: AF_XXX */
	char 			sa_data[14];	/* 14 bytes of protocol */
};
// struct sockaddr 很多网络编程API诞生早于IPv4协议,那时候都使用的是sockaddr结构体
// 为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数
// 至于这个函数是sockaddr_in还是其他的,由地址族确定,然后函数内部再强制转化为所需的地址类型。

IPv4 套接字格式地址(struct sockaddr_in)

sin_len 成员表示地址结构的长度,它是一个无符号的八位整数。需要强调的是,这个成员并不是地址结构必须有的。假如没有这个成员,其所占的一个字节被并入到 sin_family 成员中;同时,在传递地址结构的指针时,结构长度需要通过另外的参数来传递。
sin_family 成员指代的是所用的协议族,在有 sin_len 成员的情况下,它是一个8位的无符号整数;在没有 sin_len 成员的情况下,它是一个16位的无符号整数。由于IP协议属于TCP/IP协议族,所以在这里该成员应该赋值为 AF_INET

typedef uint32_t in_addr_t;
struct in_addr	{	// IPv4地址
	in_addr_t 		s_addr;		/* 32-bit IPv4 address; 网络字节序 */
};

struct sockaddr_in	{	//IPv4的 sockaddr
	// 这个成员并不是地址结构必须有的
	uint8_t 		sin_len;	/* length of structure(地址结构) (16字节) */
	
	sa_family_t 	sin_family;	/* AF_INET */
	in_port_t 		sin_port;	/* 16-bit TCP or UDP port number; 网络字节序  */
	struct in_addr 	sin_addr;	/* 32-bit IPv4 address; 网络字节序  */
	
	// sin_zero成员是不使用的, 通常会将它置为0
	// 它的存在只是为了与通用套接字地址结构 struct sockaddr 在内存中对齐
	char			sin_zero[8];/* unused */
};

由于sock API的实现早于ANSI C标准化,那时还没有 void *类型,因此像 bind、accept 函数的参数都用 struct sockaddr * 类型表示, 在传递参数之前要强制转换一下如:

struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)); 

本地套接字地址(struct sockaddr_un)

struct sockaddr_un {
    unsigned short 	sun_family; 	 /* 固定为 AF_LOCAL */
    char 			sun_path[108];   /* 路径名 */
};

IPv6 套接字地址(struct sockaddr_in6)格式

整个结构体长度是 28 个字节,其中流控信息和域 ID 先不用管,这两个字段,一个在 glibc 的官网上根本没出现,另一个是当前未使用的字段。这里的地址族显然应该是 AF_INET6,端口同 IPv4 地址一样,关键的地址从 32 位升级到 128 位,这个数字就大到恐怖了,完全解决了寻址数字不够的问题。

struct sockaddr_in6	{
	sa_family_t 	sin6_family; 		/* 16-bit */
	in_port_t 		sin6_port;  		/* 传输端口号 # 16-bit */
	uint32_t 		sin6_flowinfo; 		/* IPv6流控信息 32-bit*/
	struct in6_addr sin6_addr; 	 		/* IPv6地址 128-bit */
	uint32_t 		sin6_scope_id; 		/* IPv6域ID 32-bit */
};

4. 套接字函数

使用套接字通信函数需要包含头文件 <arpa/inet.h>,包含了这个头文件 <sys/socket.h> 就不用在包含了。

socket()

// 创建一个套接字
int socket(int domain, int type, int protocol);

参数:

  • domain:地址族协议,AF_INET: 使用IPv4格式的ip地址,AF_INET6: 使用IPv6格式的ip地址
  • type:SOCK_STREAM: 使用流式的传输协议;SOCK_DGRAM: 使用报式(报文)的传输协议
  • protocol: 一般写 0 即可, 使用默认的协议,SOCK_STREAM: 流式传输默认使用的是 TCP ;SOCK_DGRAM: 报式传输默认使用的 UDP
    • 因为有这种情况:同一协议族中存在多个数据传输方式相同的协议,所以还需要第三个参数 protocol 来指定具体协议。
    • 但是 PF_INET(IPv4 协议族)下的 SOCK_STREAM 传输方式只对应 IPPROTO_TCP 一种协议,SOCK_DGRAM 传输方式也只对应 IPPROTO_UDP 一种协议,所以参数 protocol 只要设为 0 即可。
  • 返回值:成功: 可用于套接字 通信的文件描述符;失败: -1

函数的返回值是一个文件描述符,通过这个文件描述符可以操作内核中的某一块内存,网络通信是基于这个文件描述符来完成的。

bind()

给创建好的套接字分配地址信息(IP地址和端口号)

// 将文件描述符和本地的IP与端口进行绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd: 文件描述符, 通过 socket() 调用得到的返回值
  • addr: 传入参数, 要绑定的 IP 和端口信息需要初始化到这个结构体中,IP 和端口要转换为网络字节序
  • addrlen: 参数 addr 指向的内存大小, sizeof(struct sockaddr)

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

TCP - listen()

把套接字转换成可接受状态,进入等待连接请求状态,此时的套接字才是服务器端套接字此时的由socket返回的文件描述符才是用于监听的文件描述符

// 给监听的套接字设置监听
int listen(int sockfd, int backlog);

参数:

  • sockfd: 文件描述符, 可以通过调用socket() 得到,在监听之前必须要绑定 bind()
  • backlog: 同时能处理的最大连接要求,最大值为128

返回值:函数调用成功返回 0,调用失败返回 -1

等待连接请求状态:当服务器在此状态下时,在调用 accept函数受理连接请求前,请求会处于等待状态。注意:这里说的是让来自客户端的请求处于等待状态,以等待服务器端受理它们的请求。
连接请求等待队列:还未受理的连接请求在此排队,backlog 的大小决定了队列的最大长度,一般频繁接受请求的 Web 服务器的 backlog 至少为 15。

TCP - accept()

accept 函数会受理连接请求等待队列中待处理的客户端连接请求,它从等待队列中取出 1 个连接请求,创建套接字并完成连接请求。如果等待队列为空,accpet 函数会阻塞,直到队列中出现新的连接请求才会返回。

它会在内部产生一个新的套接字并返回其文件描述符,该套接字用于与客户端建立连接并进行数据 I/O。新的套接字是在 accept 函数内部自动创建的,并自动与发起连接请求的客户端建立连接。

accept 执行完毕后会将它所受理的连接请求对应的客户端地址信息存储到第二个参数 addr 中。

// 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

  • sockfd: 监听的文件描述符
  • addr: 传出参数, 里边存储了建立连接的客户端的地址信息
  • addrlen: 传入传出参数,用于存储addr指向的内存大小

返回值:函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信,调用失败返回 -1

这个函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。

在这里插入图片描述

read & recv

// 接收数据
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);

参数:

  • sockfd: 用于通信的文件描述符, accept() 函数的返回值
  • buf: 指向一块有效内存, 用于存储接收数据
  • size: 参数 buf 指向的内存的容量
  • flags: 特殊的属性, 一般不使用, 指定为 0

返回值:

  • >0:实际接收的字节数
  • 0:对方断开了连接
  • -1:接收数据失败了

如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞等待数据到达,数据到达后函数解除阻塞,开始接收数据,当发送端断开连接,接收端无法接收到任何数据,但是这时候就不会阻塞了,函数直接返回0。

write & send

// 发送数据
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
  • 参数:
  • fd: 通信的文件描述符, accept() 函数的返回值
  • buf: 传入参数, 要发送的字符串
  • len: 要发送的字符串的长度
  • flags: 特殊的属性, 一般不使用, 指定为 0

返回值:

  • >0:实际发送的字节数,和参数len是相等的
  • -1:发送数据失败了

write 函数和 Windows 的 send 函数并不会在完成向对方主机的数据传输时返回,而是在数据移到输出缓冲时。但是 TCP 会保证对输出缓冲数据的传输,所以说 write 函数在数据传输完成时返回。

connect()

向服务器端发送连接请求

// 成功连接服务器之后, 客户端会自动随机绑定一个端口
// 服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd: 通信的文件描述符, 通过调用 socket() 函数就得到了
  • addr: 存储了要连接的服务器端的地址信息: IP 和 端口,这个IP和端口也需要转换为大端然后再赋值
  • addrlen: addr指针指向的内存的大小 sizeof(struct sockaddr)

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

UDP - recvfrom()

理解:接收端本来是不知道发送端的地址的,但调用完 recvfrom 函数后,发送端的地址信息就会存储到参数 src_addr 指向的结构体中。

// 接收数据, 如果没有数据,该函数阻塞
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

参数:

  • sockfd: 基于udp的通信的文件描述符
  • buf: 指针指向的地址用来存储接收的数据
  • len: buf 指针指向的内存的容量, 最多能存储多少字节
  • flags: 设置套接字属性,一般使用默认属性,指定为 0 即可
  • src_addr: 发送数据的一端的地址信息,IP和端口都存储在这里边, 是大端存储的
    • 如果这个参数中的信息对当前业务处理没有用处, 可以指定为NULL, 不保存这些信息
  • addrlen: 类似于accept()函数的最后一个参数, 是一个传入传出参数
    • 传入的是src_addr参数指向的内存的大小, 传出的也是这块内存的大小
    • 如果src_addr参数指定为NULL, 这个参数也指定为NULL即可

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

UDP - sendto()

UDP 套接字不会保持连接状态,因此每次传输数据时都要添加目标地址信息(相当于寄信前在信封上写收信地址)。

// 发送数据函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

参数:

  • sockfd: 基于udp的通信的文件描述符
  • buf: 这个指针指向的内存中存储了要发送的数据
  • len: 要发送的数据的实际长度
  • flags: 设置套接字属性,一般使用默认属性,指定为 0 即可
  • dest_addr: 接收数据的一端对应的地址信息, 大端的IP和端口
  • addrlen: 参数 dest_addr 指向的内存大小

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

TCP 通信

TCP是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。

  • 面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。
  • 安全:tcp通信过程中,会对发送的每一数据包都会进行校验, 如果发现数据丢失, 会自动重传
  • 流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致

在这里插入图片描述

在这里插入图片描述

创建套接字后,并不马上分为服务端和客户端。如果紧接着调用 bindlisten 函数,将成为服务器端套接字;如果调用 connect 函数,将成为客户端套接字。

TCP 服务器端的两种文件描述符

  • 监听的文件描述符:只需要有一个;负责检测客户端连接请求, 检测到之后调用accept建立新的连接
  • 通信的文件描述符:负责和建立连接的客户端通信;如果有N个客户端和服务器建立了新的连接, 通信的文件描述符就有 N 个,每个客户端和服务器都对应一个通信的文件描述符

在这里插入图片描述
文件描述符对应的内存结构:

  • 一个文件文件描述符对应两块内存, 一块内存是读缓冲区, 一块内存是写缓冲区
  • 读数据: 通过文件描述符将内存中的数据读出, 这块内存称之为读缓冲区
  • 写数据: 通过文件描述符将数据写入到某块内存中, 这块内存称之为写缓冲区

监听的文件描述符:

  • 客户端的连接请求会发送到服务器端监听的文件描述符读缓冲区
  • 读缓冲区中有数据, 说明有新的客户端连接
  • 调用accept()函数, 这个函数会检测监听文件描述符的读缓冲区
    • 检测不到数据, 该函数阻塞
    • 如果检测到数据, 解除阻塞, 新的连接建立

通信的文件描述符:

  • 客户端和服务器端都有通信的文件描述符
  • 发送数据:调用函数 write() / send(),数据进入到内核中
    • 数据并没有被发送出去, 而是将数据写入到了通信的文件描述符对应的写缓冲区中
    • 内核检测到通信的文件描述符写缓冲区中有数据, 内核会将数据发送到网络中
  • 接收数据: 调用的函数 read() / recv(), 从内核读数据
    • 数据如何进入到内核程序猿不需要处理, 数据进入到通信的文件描述符的读缓冲区中
    • 数据进入到内核, 必须使用通信的文件描述符, 将数据从读缓冲区中读出即可

TCP 套接字中的 I/O 缓冲

在使用 read / write 函数对套接字进行读写数据时,实际上读写的是套接字输入 / 输出缓冲中的内容
在这里插入图片描述
套接字 I/O 缓冲的特性:

  • I/O 缓冲在每个套接字中单独存在。
  • I/O 缓冲在创建套接字时自动生成。
  • 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
  • 关闭套接字将丢失输入缓冲中的数据。

为 Windows 套接字编程设置头文件和库

要在 Windows 上进行套接字编程,需要:

  1. 链接 ws2_32.lib 库。在 VS 中通过:项目–>属性–>配置属性–>链接器–>输入–>附加依赖项 添加 ws2_32.lib 库即可。
  2. 导入头文件 WinSock2.h。Windows 中有一个 winsock.h 和一个 WinSock2.h。其中 WinSock2.h 是较新版本,用来代替前者的。
  3. 实际上 client 在 windows 上还需要通过:项目–>属性–>配置属性–>C++ 将 SDL 检查设为否,否则使用旧函数inet_addr()会报错。

将 Linux 平台下的示例代码转换成 Windows 平台:

  • 通过 WSAStartupWSACleanup 函数初始化并清除套接字相关库
  • 把数据类型和变量名切换为 Windows 风格
  • 数据传输中用 recv / send 函数而非 read / write 函数
  • 关闭套接字时用 closesocket 函数而非 close 函数

服务器端通信流程

  1. 创建用于监听的套接字, 这个套接字是一个文件描述符
  2. 将得到的监听的文件描述符和本地的 IP、 端口进行绑定
  3. 设置监听(成功之后开始监听, 监听的是客户端的连接)
  4. 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的),没有新连接请求就阻塞
  5. 通信,读写操作默认都是阻塞的
  6. 断开连接, 关闭套接字

服务器端:

本文代码给出的都是 windows 系统下的,在命令行中执行类似如下代码:

hello_server_win 5000 # 在端口 5000 处接收连接请求

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
    WSADATA wsaData;
    SOCKET hServSock, hClntSock;	// windows系统下的,SOCKET就是int
    SOCKADDR_IN servAddr, clntAddr;

    int szClntAddr;
    char message[] = "Hello World!";

    if (argc != 2)  // 检查参数数量
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  // 初始化 Winsock 相关库
        ErrorHandling("WSAStartup() error!");

    hServSock = socket(PF_INET, SOCK_STREAM, 0);    // 创建套接字
    if (hServSock == INVALID_SOCKET)
        ErrorHandling("socket() error");
	// 网络地址信息初始化
    memset(&servAddr, 0, sizeof(servAddr));	// 主要为了把zero数组清空
    servAddr.sin_family = AF_INET;                  // 设置协议族
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 设置 IP 地址 0.0.0.0
    servAddr.sin_port = htons(atoi(argv[1]));       // 设置端口号

    if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)    // 为套接字分配地址和端口
        ErrorHandling("bind() error");

    if (listen(hServSock, 5) == SOCKET_ERROR)       // 使套接字转换为可接收连接的状态
        ErrorHandling("listen() error");

    szClntAddr = sizeof(clntAddr);
    hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);   // 接受连接请求,函数返回客户端的套接字
    if (hClntSock == INVALID_SOCKET)
        ErrorHandling("accept() error");

    send(hClntSock, message, sizeof(message), 0);   // 向客户端发送信息
    closesocket(hClntSock);     // 关闭客户端套接字
    closesocket(hServSock);     // 关闭服务器端套接字
    WSACleanup();       // 注销 Winsock 相关库
    return 0;
}


void ErrorHandling(const char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

初始化服务器端套接字时应分配所属计算机的IP地址,因为初始化时使用的IP地址非常明确,那为何还要进行IP初始化呢?如前所述,同一计算机中可以分配多个IP地址,实际IP地址个数与计算机中安装的NIC的数量相等。即使是服务器端套接字,也需要决定应接收那个IP传来的(哪个NIC传来的)数据。因此服务器端套接字初始化过程要求IP地址信息。另外,如果只有一个NIC,直接使用 INADDR_ANY。

在这里插入图片描述

客户端的通信流程

在单线程的情况下客户端通信的文件描述符有一个, 没有监听的文件描述符

  1. 创建一个通信的套接字
  2. 连接服务器, 需要知道服务器绑定的IP和端口
  3. 进行通信
  4. 断开连接, 关闭文件描述符(套接字)

客户端:
在命令行中执行类似如下代码:

hello_client.exe 127.0.0.1 5000 # 向 127.0.0.1 5000 请求连接

#pragma execution_character_set("utf-8")

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[30];
	int strLen;

	if (argc != 3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error");

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]);   // 这里对书中代码进行了一些修改(源代码编译会报错,根据报错提示修改为当前代码)
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("connect() error!");

	strLen = recv(hSocket, message, sizeof(message) - 1, 0);
	if (strLen == -1)
		ErrorHandling("read() error!");
	printf("Message from server: %s \n", message);

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

迭代回声服务器端/客户端

回声服务器端:它会将客户端传输的字符串数据原封不动地传回客户端,像回声一样。

实现迭代服务器端:调用一次 accept 函数只会受理一个连接请求,如果想要继续受理请求,最简单的方法就是循环反复调用 accept 函数,在前一个连接 close 之后,重新 accept。
在不使用多进程/多线程情况下,同一时间只能服务于一个客户端。

迭代回声服务器端与回声客户端的基本运行方式:

  1. 服务器端同一时刻只与一个客户端相连接,并提供回声服务。
  2. 服务器端依次向 5 个客户端提供服务,然后退出。
  3. 客户端接收用户输入的字符串并发送到服务器端。
  4. 服务器端将接收到的字符串数据传回客户端,即”回声“。
  5. 服务器端与客户端之间的字符串回声一直执行到客户端输入 Q 为止。

服务器端:

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

void ErrorHandling(const char* message);
constexpr int BUF_SIZE = 1024;

int main(int argc, char* argv[])
{
    WSADATA wsaData;
    SOCKET hServSock, hClntSock;
    SOCKADDR_IN servAddr, clntAddr;

    int szClntAddr;
    char message[BUF_SIZE];
    int str_len;

    if (argc != 2)  // 检查参数数量
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  // 初始化 Winsock 相关库
        ErrorHandling("WSAStartup() error!");

    hServSock = socket(PF_INET, SOCK_STREAM, 0);    // 创建套接字
    if (hServSock == INVALID_SOCKET)
        ErrorHandling("socket() error");

    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;                  // 设置协议族
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 设置 IP 地址
    servAddr.sin_port = htons(atoi(argv[1]));       // 设置端口号

    if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)    // 为套接字分配地址和端口
        ErrorHandling("bind() error");

    if (listen(hServSock, 5) == SOCKET_ERROR)       // 使套接字转换为可接收连接的状态
        ErrorHandling("listen() error");


    szClntAddr = sizeof(clntAddr);

    for (int i = 0; i < 5; ++i) {
        hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);   // 接受连接请求,函数返回客户端的套接字
        if (hClntSock == INVALID_SOCKET)
            ErrorHandling("accept() error");
        else printf("Connnected client %d\n", i + 1);
        
        while ((str_len = recv(hClntSock , message, BUF_SIZE, 0)) != 0) {
            send(hClntSock, message, str_len, 0);
        }

        closesocket(hClntSock);
    }

    closesocket(hServSock);     // 关闭服务器端套接字
    WSACleanup();       // 注销 Winsock 相关库
    return 0;
}


void ErrorHandling(const char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

客户端:

#pragma execution_character_set("utf-8")

#include <stdio.h>
#include <stdlib.h>
#include <ws2tcpip.h>

void ErrorHandling(const char* message);
constexpr int BUF_SIZE = 1024;

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[BUF_SIZE];
	int str_len;

	if (argc != 3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error");

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]);   // 这里对书中代码进行了一些修改(源代码编译会报错,根据报错提示修改为当前代码)
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("connect() error!");
	else printf("Connected....");

	while (1) {
		fputs("Input Message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);
		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
			break;

		send(hSocket, message, strlen(message), 0);
		str_len = recv(hSocket, message, BUF_SIZE - 1, 0);
		if (str_len == -1)
			ErrorHandling("read() error!");

		message[str_len] = 0;
		printf("Message from server: %s", message);
	}

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
回声客户端存在的问题(拆包和粘包)
send(hSocket, message, strlen(message), 0);
str_len = recv(hSocket, message, BUF_SIZE - 1, 0);

在本章的回声客户端的实现中有上面这段代码,它有一个错误假设:每次调用 read、write 函数时都会执行实际的 I/O 操作。

但是注意:TCP 是面向连接的字节流传输,不存在数据边界。所以多次 write 的内容可能一直存放在发送缓存中,某个时刻再一次性全都传递到服务器端,这样的话客户端前几次 read 都不会读取到内容,最后才会一次性收到前面多次 write 的内容。还有一种情况是服务器端收到的数据太大,只能将其分成多个数据包发送给客户端,然后客户端可能在尚未收到全部数据包时旧调用 read 函数

理解:问题的核心在于 write 函数实际上是把数据写到了发送缓存中,而 read 函数是从接收缓存读取数据。并不是直接对 TCP 连接的另一方进行数据读写。实际上就是没有考虑拆包和粘包的情况。

解决方法的核心: 提前确定接收数据的大小。
客户端上一次使用 write 从套接字发送了多少字节,紧接着就使用 read 从套接字读取多少字节。

// 接受完所以数据才打印
int recv_len = 0, recv_cnt;
while(recv_len < str_len) {
	str_len = recv(hSocket, &message[recv_len], BUF_SIZE - 1, 0);
	recv_len += recv_cnt;
}
message[str_len] = 0;
printf("Message from server: %s", message);

回声客户端可以提前知道接收的数据长度,但是更多情况下这不可能。这种情况下,要解决拆包和粘包的问题,就要定义应用层协议。
应用层协议实际就是在服务器端/客户端的实现过程中逐步定义的规则的集合。
在应用层协议中可以定好数据边界的表示方法、数据的长度范围等。

计算器服务端 / 客户端(实现应用层协议的例子)

为实现计算器功能,需要定义一个简单的应用层协议,用来约定在服务器端和客户端之间传输数据的规则。
协议内容包括:

  • 客户端用 1 个字节整数形式传递操作数的个数。
  • 客户端向服务器端传送的每个操作数占用 4 字节。
  • 传递完操作数后紧跟着传递一个占用 1 字节的运算符。
  • 操作符选用 *+- 其中之一
  • 服务器端以 4 字节整数向客户端传回运算结果。
  • 客户端得到运算结果后终止与服务器端的连接。

服务器端:

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

void ErrorHandling(const char* message);
int Calculate(int cnt, int nums[], char op);

constexpr int BUF_SIZE = 1024;
constexpr int opsz = 4;

int main(int argc, char* argv[])
{
    WSADATA wsaData;
    SOCKET hServSock, hClntSock;
    SOCKADDR_IN servAddr, clntAddr;

    int szClntAddr;
    char message[BUF_SIZE];
    int str_len;

    if (argc != 2)  // 检查参数数量
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  // 初始化 Winsock 相关库
        ErrorHandling("WSAStartup() error!");

    hServSock = socket(PF_INET, SOCK_STREAM, 0);    // 创建套接字
    if (hServSock == INVALID_SOCKET)
        ErrorHandling("socket() error");

    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;                  // 设置协议族
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 设置 IP 地址
    servAddr.sin_port = htons(atoi(argv[1]));       // 设置端口号

    if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)    // 为套接字分配地址和端口
        ErrorHandling("bind() error");

    if (listen(hServSock, 5) == SOCKET_ERROR)       // 使套接字转换为可接收连接的状态
        ErrorHandling("listen() error");


    szClntAddr = sizeof(clntAddr);

    int opnd_cnt = 0, recv_len = 0, recv_cnt, result;
    for (int i = 0; i < 5; ++i) {
        hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);   // 接受连接请求,函数返回客户端的套接字
        if (hClntSock == INVALID_SOCKET)
            ErrorHandling("accept() error");
        else printf("Connnected client %d\n", i + 1);

        recv(hClntSock, (char*) &opnd_cnt, 1, 0);// 先读一个字节,读出数组元素个数

        while (opnd_cnt * opsz + 1 > recv_len) {
            // 用messgae存剩下的消息,最后一个字节为操作符
            recv_cnt = recv(hClntSock, &message[recv_len], BUF_SIZE - 1, 0);
            recv_len += recv_cnt;
        }
        result = Calculate(opnd_cnt, (int*)message, message[recv_len - 1]);
        send(hClntSock, (char*)&result, sizeof(result), 0);

        printf("end\n");
        closesocket(hClntSock);
    }

    closesocket(hServSock);     // 关闭服务器端套接字
    WSACleanup();       // 注销 Winsock 相关库
    return 0;
}

int Calculate(int cnt, int nums[], char op) {
    int result = nums[0];
    if (op == '+') {
        for (int i = 1; i < cnt; ++i) result += nums[i];
    }
    else if (op == '-') {
        for (int i = 1; i < cnt; ++i) result -= nums[i];
    }
    else {
        for (int i = 1; i < cnt; ++i) result *= nums[i];
    }
    return result;
}

void ErrorHandling(const char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

客户端:

#pragma execution_character_set("utf-8")

#include <stdio.h>
#include <stdlib.h>
#include <ws2tcpip.h>

void ErrorHandling(const char* message);
constexpr int BUF_SIZE = 1024;
constexpr int opsz = 4;
constexpr int rlt_size = 4;

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[BUF_SIZE];
	int str_len;

	if (argc != 3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error");

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]);   // 这里对书中代码进行了一些修改(源代码编译会报错,根据报错提示修改为当前代码)
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("connect() error!");
	else printf("Connected....");

	// 发送消息
	int opnd_cnt, result;

	fputs("Operand count:", stdout);
	scanf("%d", &opnd_cnt);
	message[0] = (char)opnd_cnt;

	for (int i = 0; i < opnd_cnt; ++i) {
		printf("Operand %d : ", i + 1);
		scanf("%d", (int*)&message[i * opsz + 1]);
	}
	fgetc(stdin);	// 吃掉回车
	fputs("Operaotr: ", stdout);
	scanf("%c", &message[opnd_cnt * opsz + 1]);

	send(hSocket, message, opnd_cnt * opsz + 2, 0);
	recv(hSocket, (char*) & result, rlt_size, 0);
	printf("Operation result: %d\n", result);

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

UDP 通信

UDP套接字的特点
区分 TCP 与 UDP 的一个典型比喻:UDP 好比寄信,TCP 好比打电话:

  • UDP:寄信前要在信封上填好寄信人和收信人的地址,然后放进邮筒。不能确认对方是否收到信件,并且邮寄过程中新建可能丢失。
  • TCP:首先要拨打电话号码,打通后才能开始通话,但打通后的通话是可靠的。

TCP 和 UDP 最重要的区别在于流控制
理解:这里的流控制应该包含了 TCP 的可靠传输、流量控制、拥塞控制等机制,这些机制都是在流上实现的。

UDP的高效使用
网络实时传输多媒体数据一般使用 UDP。
TCP 比 UDP 慢的两个原因:

  • TCP 数据传输前后要进行连接的建立与释放
  • TCP 数据传输过程中为了保证可靠性而添加的流控制

当收发的数据量小但需要频繁连接时,UDP 的高效体现地更明显

UDP服务器端和客户端均只需 1 个套接字
TCP 中,服务器端和客户端的套接字是一对一的关系,服务器端每向一个客户端提供服务,就需要分配一个新的套接字(accept创建的)。而 UDP 的服务器端和客户端均只需 1 个套接字,服务器端只要有一个 UDP 套接字就可以和多台主机通信。

UDP客户端套接字的地址分配
在 TCP 的客户端中 conncect 函数会自动完成给套接字分配 IP 地址和端口号的过程,UDP 中则是 sendto 函数来完成此功能。如果调用 sendto 函数时发现尚未给套接字分配地址信息,就会在首次调用 sendto 函数时给套接字分配 IP 地址和端口

存在数据边界的UDP套接字
UDP 套接字编程时,接收端输入函数的调用次数必须和发送端输出函数的调用次数相同,这样才能接收完发送端发送的数据。

已连接UDP套接字和未连接UDP套接字
通过 sendto 函数传输数据的过程包括三个阶段:

  • 向 UDP 套接字注册目标 IP 和端口号;(注意:是将 UDP 套接字与目标的地址信息相关联,不是给 UDP 分配地址信息。前者每次 sendto 都会执行,后者只有首次调用且套接字尚未分配地址时才会执行一次)。
  • 传输数据;
  • 删除 UDP 套接字中注册的目标地址信息。

当多次通过 sendto 向同一个目标发送信息时,每次 sendto 都进行上面的步骤 1 和 3,就会很浪费时间。
因此当要长时间与同一主机通信时,将 UDP 变为已连接套接字会提高效率。

创建已连接 UDP 套接字
创建 UDP 套接字只需要对 UDP 套接字调用 connect 函数,但是这并不意味着要与对方的 UDP 套接字连接,这只是向 UDP 套接字注册目标 IP 和端口信息

connect(sock, (struct sockaddr*)&adr, sizeof(adr)); // 注意:adr 是目标的地址信息

使用已连接的 UDP 套接字进行通信时, sendto 函数就不会再执行步骤 1 和步骤 3,每次只要传输数据即可。因为已经指定了收发对象,所以不止可以用 sendto、recvfrom,也可以用 write、read 函数进行通信

使用 UDP 进行通信,服务器和客户端的处理步骤比 TCP 要简单很多,不存在请求连接和受理过程,并且两端是对等的 (通信的处理流程几乎是一样的),也就是说并没有严格意义上的客户端和服务器端,只是在提供服务的一端称为服务器端

UDP 的通信流程如下:
在这里插入图片描述

在UDP通信过程中,服务器和客户端都可以作为数据的发送端和数据接收端,假设服务器端是被动接收数据,客户端是主动发送数据,那么在服务器端就必须绑定固定的端口了

服务器端通信流程

服务器端可以同时与多个客户端进行通信

假设服务器端是接收数据的角色:

  1. 创建通信的套接字
  2. 使用通信的套接字和本地的IP和端口绑定,IP和端口需要转换为大端(可选)
  3. 进行通信
  4. 关闭套接字(文件描述符)
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUF_SIZE 30
void error_handling(const char* message);

int main(int argc, char* argv[])
{
    WSADATA wsaData;

    int serv_sock;
    char message[BUF_SIZE];
    int str_len;
    int clnt_adr_sz;
    struct sockaddr_in serv_adr, clnt_adr;

    if (argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  // 初始化 Winsock 相关库
        error_handling("WSAStartup() error!");

    serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (serv_sock == -1)
        error_handling("UDP socket creation error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");

    while (1)
    {
        clnt_adr_sz = sizeof(clnt_adr);
        str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); // 接收数据同时获取发送端地址
        sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);
    }

    closesocket(serv_sock); // 上面的 while 是无限循环,这里的 colse 函数没什么实际意义。
    WSACleanup();
    return 0;
}

void error_handling(const char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

客户端通信流程

假设客户端是发送数据的角色:

  1. 创建通信的套接字
  2. 进行通信
  3. 关闭套接字(文件描述符)
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>


#define BUF_SIZE 30
void error_handling(const char* message);

int main(int argc, char* argv[])
{
    WSADATA wsaData;


    int sock;
    char message[BUF_SIZE];
    int str_len;
    int adr_sz;

    struct sockaddr_in serv_adr, from_adr;

    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  // 初始化 Winsock 相关库
        error_handling("WSAStartup() error!");

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    while (1)
    {
        fputs("Insert message(q to quit): ", stdout);
        fgets(message, sizeof(message), stdin);
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;

        sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
        adr_sz = sizeof(from_adr);
        str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz);
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }

    closesocket(sock);
    WSACleanup();
    return 0;
}

void error_handling(const char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

这是一个使用已连接 UDP 套接字的例子,在上边代码的基础上修改得到

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>


#define BUF_SIZE 30
void error_handling(const char* message);

int main(int argc, char* argv[])
{
    WSADATA wsaData;


    int sock;
    char message[BUF_SIZE];
    int str_len;
    int adr_sz;

    struct sockaddr_in serv_adr, from_adr;

    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  // 初始化 Winsock 相关库
        error_handling("WSAStartup() error!");

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); // 将套接字变为已连接套接字

    while (1)
    {
        fputs("Insert message(q to quit): ", stdout);
        fgets(message, sizeof(message), stdin);
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;

        //sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
        send(sock, message, strlen(message), 0);
        adr_sz = sizeof(from_adr);
        //str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz);
        str_len = recv(sock, message, BUF_SIZE, 0);
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }

    closesocket(sock);
    WSACleanup();
    return 0;
}

void error_handling(const char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何使用PHPStudy本地快速搭建网站并实现远程访问

文章目录 [toc]使用工具1. 本地搭建web网站1.1 下载phpstudy后解压并安装1.2 打开默认站点&#xff0c;测试1.3 下载静态演示站点1.4 打开站点根目录1.5 复制演示站点到站网根目录1.6 在浏览器中&#xff0c;查看演示效果。 2. 将本地web网站发布到公网2.1 安装cpolar内网穿透2…

【第三章】软件设计师 之 数据库系统

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 1、数据库系统前言 2、三级模式 - 两级映射…

选购护眼台灯,全网都没有说清一个关键点!——照度均匀度

网上关于护眼台灯的选购推荐帖子多如牛毛&#xff0c;好台灯选购要点大体可归纳为以下五点&#xff1a; RG0无蓝光危害&#xff08;豁免级蓝光危害&#xff0c;RG1为低蓝光危害、RG2、RG3分别为中度和高危危害&#xff09; 无眩光&#xff0c;无可视频闪&#xff08;不刺眼…

matlab 多自由度的车辆垂向振动模型 车辆平稳性研究

1、内容简介 略 17-可以交流、咨询、答疑 多自由度的车辆垂向振动模型 多自由度的车辆垂向振动模型&#xff0c;包含四分之一车体模型、半车模型和整车模型 垂向振动模型、四分之一车体模型、半车模型和整车模型 2、内容说明 略 3、仿真分析 略 4、参考论文 略 链接&…

内存映射:PS和PL DDR3的一些区别

之前写的一些资料&#xff1a; PS与PL互联与SCU以及PG082-CSDN博客 参考别人的资料&#xff1a; PL读写PS端DDR的设计_pl读写ps端ddr数据-CSDN博客 xilinx sdk、vitis查看地址_vitis如何查看microblazed地址_yang_wei_bk的博客-CSDN博客 可见&#xff0c;PS端的DDR3需要从…

git push origin masterEverything up-to-date

按住这个看一下很简单的问题&#xff0c;我在网上看了很多就是没找到能用的&#xff0c;最后找到了这个看起来写的很简单的一个文章&#xff0c;但他写的真的有用。 出现的问题 解决步骤

JavaScript逆向之Hook技术

Hook技术&#xff1a; 背景&#xff1a; ​ 在js逆向的过程种&#xff0c;当我们遇到加密参数&#xff0c;可以使用关键字全局搜素&#xff0c;跟栈&#xff0c;还有一种就是hook技术。跟栈就是比较麻烦&#xff0c;需要我们一个个找&#xff0c;hook技术就比较厉害了&#x…

【Linux】Kali(WSL)基本操作与网络安全入门

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍WSL安装Kali及基本操作。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路…

信捷 XDH 输出点流水灯

本文以XDH 为例&#xff0c;实现输出点流水灯&#xff0c;测试输出点是否正常。 用到了FOR NEXT循环和偏移量实现。 程序下载链接如下&#xff1a; https://download.csdn.net/download/weixin_39926429/88527971

PyCharm因安装了illuminated Cloud插件导致加载项目失败

打开Pycharm时会有弹窗提示&#xff1a; The license for Illuminated Cloud is invalid or has expired. All Illuminated Cloud features will be disabled. 这个弹窗会导致你加载项目一直失败&#xff0c;close project 也关不掉&#xff0c;我都是用任务管理器杀死进程的…

详解Redis持久化(上篇——RDB持久化)

Redis持久化的作用和意义 Redis 持久化是一种机制&#xff0c;用于将内存中的数据写入磁盘&#xff0c;以保证数据在服务器重启时不会丢失。持久化是为了解决内存数据库&#xff08;如 Redis&#xff09;在服务器关闭后&#xff0c;数据丢失的问题。 Redis 持久化的主要作用和…

统计分钟级别的视频在线用户数+列炸裂+repeat函数

统计分钟级别的视频在线用户数 1、原始数据如下&#xff1a; uid vid starttime endtime select aa as uid,v00l as vid,2023-10-25 12:00 as starttime,2023-10-2512:15 as endtime union select bb as uid,v002 as vid,2023-10-25 12:05 as starttime,2023-10-25 12:19 …

【计算机组成原理】

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

springboot苍穹外卖实战:九、缓存菜品(手动用redisTemplate实现缓存逻辑)+缓存套餐(Spring cache实现)

缓存菜品 缺点 缓存和数据库的数据一致性通常解决方案&#xff1a;延时双删、异步更新缓存、分布式锁。 该项目对于缓存菜品的处理较为简单&#xff0c;实际可以用管道技术提高redis的操作效率、同时cache自身有注解提供使用。 功能设计与缓存设计 建议这部分去看下原视频&…

LeetCode算法心得——高级访客(模拟枚举+小窗口)

大家好&#xff0c;我是晴天学长&#xff0c;今天的周赛第二题&#xff0c;需要的小伙伴可以关注支持一下哦&#xff01;后续会继续更新的。&#x1f4aa;&#x1f4aa;&#x1f4aa; 1) .高级访客 给你一个长度为 n 、下标从 0 开始的二维字符串数组 access_times 。对于每个 …

NetSuite 固定资产报表自定义原理及应用

NetSuite固定资产模块一直处于功能迭代更新中&#xff0c;目前23.2的版本能够支持报表的局部自定义&#xff0c;比如增加原值或已折旧期间&#xff0c;甚至固定资产自定义字段等。但是当我们在实际项目中&#xff0c;会遇到一些挑战&#xff0c;例如&#xff1a; 固定资产原值…

Python:Unittest框架快速入门:用例、断言、夹具、套件、HTML报告、ddt数据驱动

快速看了套Unittest的入门教程 软件测试全套资料赠送_哔哩哔哩_bilibili软件测试全套资料赠送是快速入门unittest测试框架&#xff01;全实战详细教学&#xff0c;仅此一套&#xff01;的第1集视频&#xff0c;该合集共计11集&#xff0c;视频收藏或关注UP主&#xff0c;及时了…

servlet 的XML Schema从哪边获取

servlet 6.0的规范定义&#xff1a; https://jakarta.ee/specifications/servlet/6.0/ 其中包含的三个XML Schema&#xff1a;web-app_6_0.xsd、web-common_6_0.xsd、web-fragment_6_0.xsd。但这个页面没有给出下载的链接地址。 正好我本机有Tomcat 10.1.15版本的源码&#…

【Web自动化测试】如何生成高质量的测试报告

运行了所有测试用例&#xff0c;控制台输入的结果&#xff0c;如果很多测试用例那也不能够清晰快速的知道多少用例通过率以及错误情况。 web自动化测试实战之批量执行测试用例场景: 运行 AllTest.py 文件后得到的测试结果不够专业&#xff0c;无法直观的分析测试结果,我们能否…

文心一言 VS 讯飞星火 VS chatgpt (133)-- 算法导论11.2 5题

五、用go语言&#xff0c;假设将一个具有n个关键字的集合存储到一个大小为 m 的散列表中。试说明如果这些关键字均源于全域U&#xff0c;且|U|>nm&#xff0c;则U 中还有一个大小为n 的子集&#xff0c;其由散列到同一槽位中的所有关键字构成&#xff0c;使得链接法散列的查…