文章目录
- 四、网络和网络编程
- (一) 网络协议
- 1.基础概念
- 2.网络协议和网络模型:OSI七层模型、TCP/IP四层协议
- 3.TCP协议
- (1)TCP协议的特点
- (2)TCP协议的首部格式
- (3)TCP状态图
- (4)为什么要三次握手?2次行不行?
- (5)为什么要四次挥手?
- (6)快速重传
- (7)滑动窗口机制
- (8)WireShark抓包
- (9)tcpdump抓包
- (二) Socket网络编程
- 1.网络地址的结构
- 2.Socket编程基本原理
- 3.TCP通信
- (1)socket:创建监听套接字
- (2)bind():服务器绑定IP地址和端口
- (3)listen() :监听
- ①DDOS攻击
- (4)connect()
- (5)accept()
- (6)recv():接收数据
- (7)send():发送数据
- ①写broken pipe:SIGPIPE信号的处理
- (8)close()
- (9)TIMEWAIT 和 setsockopt():设置套接字属性
- (10)TCP通信的通用步骤
- 4.UDP通信
- (1)sendto()
- (2)recvfrom()
- (3)UDP通信
- (4)UDP特点
- (三) IO多路复用
- 0.IO多路复用 (I/O multilplexing)
- (1)5种IO模型
- (2)读写事件(网络IO事件)
- (3)实现大并发:多线程 / IO多路复用
- ①一个进程最多能够创建多少个线程?
- 1.select
- (1)函数原型
- (2)数据类型
- (3)fd_set的原理
- (4)返回值
- (5)select的返回时机
- (6)原理
- (7)select的缺陷
- (8)select 的应用场景
- (9)select实现一对一的聊天
- (10)select实现多人聊天室
- 2.poll
- 3.epoll
- (1)epoll_create()、epoll_create1()
- (2)epoll_ctl
- (3)epoll_wait
- (4)底层实现
- (5)epoll_wait的底层实现与select有显著区别
- (6)epoll_wait比起select的优势
- (7)epoll_wait的两种就绪方式:水平触发和边缘触发
- ①水平触发 (强监督)
- ②边缘触发 (弱监督)
- (8)设置为非阻塞式:fcntl()
- (9)epoll实现多人聊天室
- (四) HTTP
- 0.互联网的诞生
- 1.HTML
- 2.URI/URL
- 3.HTTP协议
- 4.HTTP报文
- (1)HTTP请求报文
- (2)HTTP响应报文
- (3)作业:http服务器,发送图片给客户端
- 5.HTTPS的原理
- (1)对称加密
- (2)非对称加密
- 6.Restful编程风格
- (五) 进程池和线程池
- 1.进程池
- (1)进程池模型
- (2)父进程的工作流程
- (3)子进程的工作流程
- 2.进程间传递文件描述符
- (0)提出背景
- (1)socketpair()
- (2)sendmsg()
- ①结构体msghdr
- ②结构体 struct iovec
- ③结构体 struct cmshdr
- ④代码
- (3)recvmsg()
- 3.传输文件问题
- (1)小文件传输:粘包问题
- 1.TCP粘包问题
- 2.小火车协议
- (2)大文件传输:半包问题
- 1.半包问题
- 2.优化:零拷贝技术
- 3.发送方:循环发送sendn、3种零拷贝技术
- ①循环发送sendn
- ②mmap
- ③sendfile
- ④splice
- 4.接收方:循环接收recvn、waitall、mmap、splice
- ①recvn
- ②recv(waitall)
- ③mmap
- ④splice
- 4.进程池的退出
- (0)设计思想:异步拉起同步
- (1)暴力退出:用kill()直接杀死子进程
- (2)优雅退出:通知子进程exit
- 5.线程池
- 5 线程池
- 5.1 模型
- 5.2 流程
- 5.3 数据结构
- 5.4 接口实现
- 5.4.1 任务队列
- 5.4.2 线程池
- 5.4.3 main()函数
- 5.5 线程池退出
四、网络和网络编程
(一) 网络协议
1.基础概念
1.计算机网络发展历史
2.ifconfig命令(Windows版本是ipconfig)可以展示本地网卡的信息,其中ens33(有些系统是使用eth0)是表示以太网网卡,而lo表示loopback本地回环设备。
3.发起请求的那一端称作客户端,响应的那一端称作服务端
4.ARP (地址解析协议):IP地址→MAC地址
RARP (逆地址解析协议):MAC地址→IP地址
5.DNS:域名系统,Domain Name System
6.网卡:网络接口卡,Network Interface Card,NIC
7.MAC帧
2.网络协议和网络模型:OSI七层模型、TCP/IP四层协议
这部分请查看计算机网络专栏的1-6章:https://blog.csdn.net/edward1027/category_11786323.html
1.TCP/IP协议族概览
2.以太网
3.IP协议
IPv4:Internet Protocol Version 4,互联网协议
4.TCP协议
5.UDP协议
UDP是一个数据报的协议
3.TCP协议
TCP:Transmission Control Protocol,传输控制协议
(1)TCP协议的特点
TCP是一个①可靠传输的、②面向连接的、③全双工、④具有流量控制的、⑤字节流的(流式协议)、⑥二进制的协议。
TCP协议是字节流协议,数据之间没有边界的,发生TCP粘包问题。
(2)TCP协议的首部格式
1.端口号
2.序号
序号:对要发送的数据进行编号,防止数据到达对端之后发生乱序的情况。
特指要发送的数据的第一个编号,之后的编号递增。
当连接建立的时候,第一个序号的生成采用的是随机算法得到。
3.确认号
TCP的确认号,是期待下一个的序号
GBN的确认号,是累计确认
剩余部分,见计网专栏第五章
(3)TCP状态图
11个状态:5 + 6
3次握手:5个状态
4次挥手:6个状态
(4)为什么要三次握手?2次行不行?
Q1:为什么要三次握手?2次行不行?
A1:三次握手确保了双方都能够收发数据并同步彼此的序列号,保证了通信的可靠性。
两次不行。防止失效的连接请求报文段被服务端接收,从而产生错误。
(1)情形一:客户端发送的SYN报文延迟到达。
若只有2次握手,若因为网络拥塞,客户端发送的SYN报文已经超时还没收到ACK,则客户端会重发SYN报文。这次被正确接收并建立连接。连接断开后,此时第一次的SYN报文到达客户端,服务器会认为这是新的连接请求,于是发送ACK,认为连接已经建立,但此次ACK会被客户端丢弃。造成服务器空等客户端发送数据,造成资源浪费。
(2)情形二:服务器端发送的ACK报文丢失。
客户端向服务器发送SYN报文后,服务器端返回ACK报文,并认为连接已经建立,开始等客户端传输数据。若此次ACK报文丢失,没有发送到客户端,则客户端认为连接没有建立,不会发送数据,造成服务器空等。客户端超时重发SYN,服务器也会因为认为连接已经建立而丢弃SYN报文,不予回复。
(5)为什么要四次挥手?
1.Q1:为什么要四次挥手?
A1:TCP是一个全双工的连接:
TCP连接是全双工的,即双向通信。每个方向都需要独立关闭,这意味着每个方向都需要发送和确认一个FIN报文段。这样可以确保双方都完成数据传输。
控制报文 + 数据报文。前两次挥手关闭一条通道,不能再发送数据报文,仅能发送控制报文。
三次握手和四次挥手,都是控制报文,不携带数据。
2.Q2:服务器可以主动断开连接吗?为什么?
A2:服务器可以主动断开连接,但是会进入到TIME_WAIT状态,该状态会持续2MSL的超时时间。此时服务器的资源会被浪费。因此一般情况下,不建议服务器主动断开连接。
3.Q3:为什么需要TIME_WAIT状态,该状态可以删除吗?
A3:
(1)情况1:第四次挥手丢失,造成服务器无法关闭。
不可以。若不等待2MSL时间,则若第四次挥手的ACK可能到达不了服务器,则服务器无法进入CLOSED状态。客户端发送ACK后,直接进入CLOSED状态,也无法接收到服务器重发的FIN。
(2)情况2:旧连接的数据被新连接接收,造成数蹿链。
若不等待2MSL时间,又重新建立第二次连接。第二次建立的新连接的五元组信息,可能与第一次连接完全相同。而第一次重发的数据延迟到达,被认为是第二次的数据,造成数据蹿链。
4.Q4:什么叫一个TCP连接?
A4:在操作系统看来,一个TCP连接由五元组信息组成:协议、源IP、源端口、目的IP、目的端口
5.Q5:当服务器后台出现大量的TIME_WAIT状态的连接,该怎么处理?
①缩短MSL的超时时间,RFC793规定是2分钟。实际使用时,可以修改为15秒、30秒、1分钟。
②排查服务器主动断开连接的原因。
(6)快速重传
收到三个冗余ACK,立即重传。
TCP的选择性确认SACK
(7)滑动窗口机制
(8)WireShark抓包
抓包的四次挥手:(实际上是按三次挥手来的,若无数据传输,则二三次挥手可以合并)
(9)tcpdump抓包
Xshell中:
1.tcpdump抓包
sudo tcpdump -i ens33 -S -w test.cap
2.下载到Windows本地
sz test.cap
3.用wireshark解析抓的包
(二) Socket网络编程
1.网络地址的结构
1.网络地址结构体
(1)struct in_addr
struct in_addr:专门用于表示一个IPv4地址。
struct in_addr{uint32_t s_addr; // 32-bit IPv4 address
};
(2)struct sockaddr
这是一个通用的地址结构体,用于表示各种协议族的地址,如IPv4、IPv6等。
struct sockaddr {sa_family_t sa_family; // 地址族。对于IPv4是AF_INETchar sa_data[14]; // 包含协议相关地址信息的数组
};
由于sa_data的长度固定且无法具体化为特定的地址格式,我们通常不直接使用struct sockaddr,而是使用它的具体实现,如struct sockaddr_in。
(3)struct sockaddr_in
struct sockaddr_in:用于表示一个完整的IPv4套接字地址,包括IP地址和端口号。
这是专门用于表示IPv4地址的结构体,继承了struct sockaddr,并添加了IPv4特有的字段。
struct sockaddr_in {sa_family_t sin_family; // 地址族,IPv4使用AF_INETin_port_t sin_port; // 端口号,使用网络字节序存储struct in_addr sin_addr; // IPv4地址,使用struct in_addr结构体表示char sin_zero[8]; // 填充字段,使结构体大小与struct sockaddr一致
};struct in_addr{uint32_t s_addr; // 32-bit IPv4 address
};
(4)填充一个网络地址
void test2(){//填充一个网络地址struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET;addr.sin_port = htons(8080);addr.sin_addr.s_addr = inet_addr("192.168.248.136");//打印网络地址printf("ip: %s, port: %d\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
}
2.大端模式、小端模式
大端序(Big Endian、网络字节序):低地址低有效位,高地址高有效位
小端序(Little Endian、主机字节序):低地址低有效位,高地址高有效位
字符串不存在大小端模式
3.大小端转换:
#include <arpa/inet.h>
//h:host n:net s:short,16bit l:long,32bit
uint16_t htons(uint16_t hostshort); //主机字节序->网络字节序
uint32_t htonl(uint32_t hostlong); //主机字节序->网络字节序
uint16_t ntohs(uint16_t netshort); //网络字节序->主机字节序
uint32_t ntohl(uint32_t netlong); //网络字节序->主机字节序
4.32位网络字节序IP地址、点分十进制的IP地址 互相转换的函数
①inet_ntoa()
:struct in_addr类型的IPv4地址 → 点分十进制字符串
②inet_aton()
:点分十进制的IPv4地址字符串 → struct in_addr类型的二进制格式,转换的IPv4地址存储在第二个参数中。
③inet_addr()
:点分十进制的IPv4地址字符串 → 网络字节序的二进制格式,返回值为IPv4地址
④inet_ntop()
:二进制格式的 IPv4或IPv6 地址 → 点分十进制或冒号十六进制的字符串
#include <arpa/inet.h>char* inet_ntoa(struct in_addr in);
int inet_aton(const char* cp, struct in_addr* inp);
in_addr_t inet_addr(const char* cp);
const char* inet_ntop(int af, const void* src, char* dst, socklen_t size);
af:地址族(AF_INET 用于 IPv4,AF_INET6 用于 IPv6)。
src:指向包含二进制格式地址的缓冲区。
dst:指向用于存储结果字符串的缓冲区。
size:dst 缓冲区的大小。
5.域名
gethostbyname
(1)函数原型
#include <netdb.h>struct hostent *gethostbyname(const char* name);struct hostent {char *h_name; // 官方的主机名char **h_aliases; // 主机名的别名列表int h_addrtype; // 主机地址的类型int h_length; // 地址的长度char **h_addr_list; // 主机地址列表
};
(2)返回值
①成功,返回一个指向 hostent 结构的指针,该结构包含了主机名和地址信息
②解析失败,
。如果解析失败,则返回 NULL。
2.Socket编程基本原理
Socket编程基于BSD(Berkeley Software Distribution)套接字接口,它提供了一种通用的网络编程接口,使得应用程序能够通过网络进行通信。Socket编程基本原理包括以下几个步骤:
1.创建Socket:应用程序通过socket()
系统调用创建一个Socket,该调用返回一个Socket描述符,用于后续的通信。
2.绑定地址:对于服务器端,需要使用bind()
系统调用将Socket绑定到一个特定的IP地址和端口号上,以便客户端能够连接。
3.监听连接请求(仅适用于TCP):对于TCP服务器,需要使用listen()
系统调用开始监听客户端的连接请求。
4.接受连接(仅适用于TCP):使用accept()
系统调用接受客户端的连接请求,返回一个新的Socket描述符,用于与客户端进行通信。
5.发起连接(仅适用于TCP客户端):对于TCP客户端,使用connect()
系统调用向服务器端发起连接请求。
6.发送和接收数据:通过send()
和recv()
系统调用(或者对于UDP,sendto()
和recvfrom()
系统调用)发送和接收数据。
7.关闭连接:通信结束后,使用close()
系统调用关闭Socket连接。
3.TCP通信
1.服务器端:
①创建一个套接字:使用socket()
系统调用创建一个TCP Socket。
②绑定地址:使用bind()
系统调用将套接字Socket绑定到服务器的IP地址和端口号上。
③监听连接请求:使用listen()
系统调用开始监听客户端的连接请求。
④接受连接:使用accept()
系统调用接受客户端的连接请求,返回一个新的Socket描述符。
⑤发送和接收数据:使用send()
和recv()
系统调用发送和接收数据。
⑥关闭连接:通信结束后,使用close()
系统调用关闭Socket连接。
2.客户端:
①创建Socket:使用socket()
系统调用创建一个TCP Socket。
②发起连接:使用connect()
系统调用向服务器端发起连接请求。
③发送和接收数据:使用send()
和recv()
系统调用发送和接收数据。
④关闭连接:通信结束后,使用close()
系统调用关闭Socket连接。
(1)socket:创建监听套接字
1.函数原型
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
①domain:AF_INET
代表 IPv4,AF_INET6 代表 IPv6
②type:SOCK_STREAM
代表TCP,SOCK_DGRAM
代表UDP
③protocol:参数3通常为 0,表示使用默认协议。
//创建监听套接字
int listenfd = socket(AF_INET, SOCK_STREAM, 0); //TCP协议,流式协议
socket(AF_INET, SOCK_DGRAM, 0); //UDP协议,包式协议,数据报
2.返回值:
①创建socket成功,返回 大于0的文件描述符fd,整型
②创建socket失败,返回-1,并设置errno
3.示意图
内核态下,服务器端和客户端各自有 接收缓冲区 和 发送缓冲区
一个套接字,对应到内核中的接收缓冲区和发送缓冲区
(2)bind():服务器绑定IP地址和端口
1.作用:服务器将套接字与网络地址进行绑定。
该函数只有服务器端需要进行调用,因为服务器需要将自己的网络地址暴露给客户端。
服务器的特点:被动等待请求
客户端不需要使用bind,操作系统会自动分配一个网络地址。
2.函数原型
int bind(int sockfd, const struct sockaddr* addr,socklen_t addrlen);
int ret = bind(listenfd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
①sockfd:指定一个套接字
②addr:通用的sockaddr,实际绑定时,会使用struct sockaddr_in
③addrlen:sizeof(struct sockaddr_in)
3.返回值
①成功,返回0
②失败,返回-1,同时指定errno (全局变量)
(3)listen() :监听
1.作用:使套接字进入监听状态,等待客户端连接
listen 用于将套接字设置为监听模式,准备接受客户端连接。
accept 用于从监听套接字的连接队列中取出一个已完成的连接,并为该连接创建一个新的套接字进行通信。
2.函数原型
int listen(int sockfd, int backlog);
①参数1 sockfd:监听某一个套接字
②参数2 backlog:能够监听的连接个数,即半连接队列的数量 + 全连接队列的数量
设置为1,则可以有两个连接能建立(ESTABLISHED),第三个连接处于SYN_SENT
实际工作中,如果考虑高并发的情况,可以将backlog的值设置为一万以上的值。
3.返回值
①成功,返回0
②失败,返回-1,并设置errno
4.listen的底层原理
①半连接队列:完成第一次握手和第两次握手
②全连接队列:三次握手
一旦启用了 listen 之后,操作系统就知道该套接字是服务端的套接字,操作系统内核就不再启用其发
送和接收缓冲区,转而在内核区维护两个队列结构:半连接队列和全连接队列。半连接队列用于管理成功第一次握手的连接,全连接队列用于管理已经完成三次握手的队列。
backlog 在有些操作系统用来指明半连接队列和全连接队列的长度之和,一般填一个正数即可。如果队列已经满了,那么服务端受到任何再发起的连接都会直接丢弃(大部分操作系统中服务端不会回复RST,以方便客户端自动重传)
①DDOS攻击
攻击者只发送一次握手,不发第三次握手。当SYN队列中的连接数量等于backlog时,就不能再接收新的连接了。
(4)connect()
1.作用:客户端使用 connect 来建立和TCP服务端的连接。默认阻塞。
2.函数原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
①参数1 sockfd:客户端的套接字
②参数2 addr:指定服务器的网络地址
③参数3 addrlen:网络地址的长度
3.返回值
①成功,返回0
②失败,返回-1,并设置errno
netstat -ano | grep 8080
(5)accept()
默认阻塞,当没有客户端的连接请求,则阻塞。
accept 函数由服务端调用,用于从全连接队列中取出下一个已经完成的TCP连接。如果全连接队列为
空,那么 accept 会陷入阻塞。一旦全连接队列中到来新的连接,此时 accept 操作就会就绪。
1.函数原型
#include <sys/types.h>
#include <sys/socket.h>int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
①参数1 sockfd:指定监听的套接字 listenfd
②参数2 addr:传入传出参数,存放客户端的网络地址。一个指向 struct sockaddr 类型的指针,用于存储连接客户端的地址信息。如果不需要获取客户端地址信息,可以传递 NULL。
③参数3 addrlen:传入传出参数,addr的长度信息
假如不需要知道客户端的网络地址,将参数2和参数3设置为NULL
2.返回值
①成功,返回一个>0的套接字文件描述符peerfd,该套接字用于与客户端通信 (进行数据的接收和发送)。
②失败,返回-1,并设置errno
3.两类套接字:listenfd 和 peerfd
①listenfd:完成新连接的建立,执行三次握手。只有一个。[类似ftp控制连接]
②peerfd:执行数据的接收和发送。可以有多个。[类似ftp的数据连接]
int peerfd = accept(listenfd, (struct sockaddr*)&clientAddr, sizeof(clientAddr));
(6)recv():接收数据
1.作用:将内核态的接收缓冲区的数据,拷贝到用户态的接收缓冲区
2.函数原型
#include <sys/types.h>
#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);
(1)参数
①参数1 sockfd :Socket描述符。服务器端用peerfd接收数据; 客户端用clientfd来接收数据
②参数2 buf :指定用户态的接收缓冲区,用于存储接收数据的缓冲区。
③参数3 len:指定的是用户态接收缓冲区的大小
④参数4 flags:接收控制标志,通常为0。
特殊情况,MSG_PEEK、MSG_WAITALL、MSG_DONTWAIT等
(2)返回值
①成功,大于0,表示接收的数据长度
等于0,表示连接已断开
②失败,小于0,发生错误,并设置errno
3.recv也是阻塞式。当内核态的接收缓冲区没有数据时,recv()处于阻塞状态。
4.如何确定内核缓冲区中数据的长度:
recv的第四个参数设为MSG_PEEK
会从内核态缓冲区拷贝到用户态缓冲区,但不会移走内核缓冲区中的数据。
(7)send():发送数据
1.作用:将用户态发送缓冲区的数据,拷贝到内核态的发送缓冲区
2.函数原型
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
①参数1 sockfd:服务器端用peerfd发送数据; 客户端用clientfd来发送数据
②参数2 buf:用户态的发送缓冲区的首地址
③参数3 len:用户态的发送缓冲区的长度
④参数4 flags:一般为0
3.返回值
①大于等于0,表示发送的字节数。(应该<=len)
②小于0,发生错误,并设置errno
4.send默认也是阻塞式函数。当内核态的发送缓冲区满,则send()处于阻塞状态。
5.send和recv的工作原理:
在用户态和内核态之间的接收缓冲区、发送缓冲区进行数据的拷贝
①写broken pipe:SIGPIPE信号的处理
6.Q:往一个已经断开的连接上继续发送数据,会造成什么影响?
A:第一次send可以正常运行,第二次send使服务器程序直接崩溃。
原理:第一次send时,由于客户端已经断开,内核会给服务器返回一个RST
第二次send,返回SIGPIPE信号,是写broken pipe行为。导致进程崩溃。
原因:第一次send时,还不知道连接已经断开了。这时内核会给服务器返回一个RST报文。
故第二次send,服务器进程会收到SIGPIPE信号,默认处理方式:直接导致进程崩溃
解决方案:在服务器程序的开始位置,使用 signal()将SIGPIPE信号忽略
//直接忽略掉SIGPIPE信号 (注册SIGPIPE信号)
signal(SIGPIPE, SIG_IGN);
(8)close()
1.作用
①close 函数用于关闭文件描述符,包括套接字描述符,并释放与之相关的所有系统资源。
②套接字是用文件描述符表示的,文件描述符是一种稀缺资源,不用的情况下要记得回收,否则很可能会耗尽文件描述符
2.函数原型
int close(int fd);
3.返回值
成功:如果关闭成功,close 函数返回 0。
失败:如果关闭失败,close 函数返回 -1,并设置 errno 以指示错误类型。
(9)TIMEWAIT 和 setsockopt():设置套接字属性
1.现象:
当服务器主动断开连接时,会进入TIME_WAIT状态,此时再想启动服务器进程,会报错:bind:Address already in use
解决:
设置在bind()之前
#include <sys/types.h>
#include <sys/socket.h>int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
举例:
//设置套接字属性,网络地址可重用
int reuse = 1; //1表示有效
int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
if(ret == -1) error(1, errno, "setsockopt");
(10)TCP通信的通用步骤
1.server.c
①创建监听套接字
②填充网络地址
③设置网络地址可重用
④绑定网络地址
⑤监听
int tcpInit(const char* ip, const char* port){//创建监听套接字int listenfd = socket(AF_INET, SOCK_STREAM, 0);if(listenfd == -1) error(1, errno, "socket");//填充网络地址struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(serveraddr));serveraddr.sin_family = AF_INET;serveraddr.sin_port = htons(atoi(port));serveraddr.sin_addr.s_addr = inet_addr(ip);//设置网络地址可重用int reuse = 1;int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));if(ret == -1) error(1, errno, "setsockopt");//绑定网络地址ret = bind(listenfd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));if(ret == -1) error(1, errno, "bind");//监听ret = listen(listenfd, 10);if(ret == -1) error(1, errno, "listen");return listenfd;
}
4.UDP通信
服务器端和客户端:
①创建Socket:使用socket()
系统调用创建一个UDP Socket。
②绑定地址(服务器端):使用bind()
系统调用将Socket绑定到服务器的IP地址和端口号上。
③发送和接收数据:使用sendto()
和recvfrom()
系统调用发送和接收数据。
④关闭Socket:通信结束后,使用close()
系统调用关闭Socket连接。
①UDP是一种保留消息边界的协议,无论用户态空间分配的空间是否足够 recvfrom 总是会取出
一个完整UDP报文,那么没有拷贝的用户态内存的数据会直接丢弃。
sendto几次,就需要recvfrom几次。
②服务器需要先接收一次客户端的消息,才能得到客户端的网络地址。
(1)sendto()
1.函数原型
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
举例:
int ret = sendto(serverfd, buff2, strlen(buff2), 0, (const struct sockaddr*)&clientaddr, sizeof(clientaddr));
(2)recvfrom()
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
recvform()默认是阻塞式的
(3)UDP通信
(4)UDP特点
1.Q:UDP是一个无连接的协议,那如何保证数据的可靠到达呢?
A:UDP是一个传输层的协议,即在传输层无法解决数据可靠传输的特性,要达到数据可靠传输的效果,只能向上层抛出,交给上层处理,即交给应用层处理。因此UDP通信要比TCP难得多。
应用层必须要将TCP的功能实现一次。
2.Q2:UDP的使用场景
A:UDP无法保证数据的可靠到达,可以应用在允许数据丢失(丢包)的场景:做直播,实时会议,游戏 等 对网络要求不高的场合。
(三) IO多路复用
0.IO多路复用 (I/O multilplexing)
(1)5种IO模型
①阻塞IO:[类似独占查询]
②非阻塞式IO:配合轮询。[类似定时查询]
③IO多路复用:监听多个I/O事件,将多个阻塞点变成一个阻塞点。select、poll、epoll
④信号驱动IO:阻塞点主动发生信号,是异步方式。[类似中断]
⑤异步IO:不需要CPU主动处理 [类似DMA方式]
(2)读写事件(网络IO事件)
①当内核的接收缓冲区中有数据时,表示该套接字可读,即读事件已就绪。
②当内核的发生缓冲区非满,还有空间,表示该套接字可写,即写事件已就绪。
标准输入:缓冲区有数据,可读
磁盘文件:open,fd读入内核缓冲区,表示可读
(3)实现大并发:多线程 / IO多路复用
①一个进程最多能够创建多少个线程?
每一个线程都有自己独立的栈空间,32位系统的虚拟地址空间为4G,内核态占1G,用户态占3G。则最多有3G/线程栈空间大小
ulimit -a
,查看线程栈空间大小
1.select
一句话介绍:select是实现I/O多路复用的机制之一,将多个阻塞点变为一个阻塞点
(1)函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
select(6, &readfds, &writefds, NULL, &timeout)
①nfds:监听的最大文件描述符+1 [为了内核提升效率,只检查前几nfds个文件描述符]
②readfds:监听的读事件的集合,不需要设为NULL。[传入(调用时),表示对哪些文件描述符的读事件感兴趣 ;传出(函数返回时),读事件已就绪的文件描述符。]
③writefds:监听的写事件的集合,不需要设为NULL。[传入(调用时),表示对哪些文件描述符的写事件感兴趣 ;传出(函数返回时),写事件已就绪的文件描述符。]
④exceptfds:监听异常事件的集合,不需要设为NULL。[传入(调用时),表示对哪些文件描述符的异常事件感兴趣 ;传出(函数返回时),发生异常事件的文件描述符。]
⑤timieout:指定等待时间的结构体。如果为NULL,select将无限期等待;如果设置了时间值,select将在指定时间后返回。超时时间,最多阻塞的时间长度。[超过就不要了]。
i.定时等待:{秒,微秒}
ii.无限等待:若为NULL,则无限期阻塞
iii.立即返回:若为{0,0},则不阻塞,立刻返回
timieout也是传入传出参数,传入时是超时时间,传出时是剩余时间。返回值为0,则表示超时(时间用完,但没有时间就绪)。
fds:文件描述符集合 (file descriptor set)
select是同步的,当select返回时,说明有事件就绪了。【中途会切换到其他进程,到下次select检查时,若有一个或多个事件就绪,就返回。若超时,返回0。若未超时,继续切换其他进程等待(事件就绪)】
多个阻塞点,变成了select一个阻塞点
(2)数据类型
fd_set 是一个用于描述一组文件描述符(file descriptors)的数据结构,通常用于select系统调用,以监控多个文件描述符的状态(如可读、可写和异常条件)
select 会在返回时修改传入的 fd_set 集合,只保留那些发生了指定事件的文件描述符。对于没有事件发生的文件描述符,它们在集合中的位置会被清除(即置为 0)【刚开始监听的位图全部置为1,未就绪的事件会被select清除为0】
(1)fd_set:传入传出参数(指针),大小为1024的位图
①FD_ZERO(&set)
②FD_SET(fd, &set)
③FD_ISSET(fd, &set)
④FD_CLR(fd, &set)
(2)struct timeval:{tv_sec, tv_usec}
(3)fd_set的原理
fd_set是一个位图,大小为1024bit,实际上就是一个数组
typedef struct {unsigned long fds_bits[1024 / (8 * sizeof(unsigned long))];
} fd_set;
①FD_ZERO
:初始化 fd_set,将其所有位清零。
void FD_ZERO(fd_set *set);
②FD_SET
:在 fd_set 中添加一个文件描述符。
void FD_SET(int fd, fd_set *set);
③FD_CLR
:从 fd_set 中移除一个文件描述符。
void FD_CLR(int fd, fd_set *set);
④FD_ISSET
:检查 fd_set 中是否包含某个文件描述符。
int FD_ISSET(int fd, fd_set *set);
(4)返回值
①成功,返回就绪事件的个数。
如果超时,返回0
③失败,返回-1,并设置errno
(5)select的返回时机
①有事件就绪
②超时时间到达
③被信号中断
(6)原理
select 是一种系统调用,用于在多个文件描述符上进行多路复用,以便监视多个文件描述符的可读、可写或异常状态。
(7)select的缺陷
①监听数量有限:监听的文件描述符的个数是有限的。fd_set是大小为1024的位图,最大只能监听1024个fd。
②内核中的轮询机制、用户态的轮询机制
效率低:返回值只能表示就绪的事件数量,但不知道具体是哪个事件就绪。需要遍历fd_set,找到就绪的文件描述符。时间复杂度为O(n)。(若场景为10万个事件在连接,但只有10个事件就绪,也需要遍历10万个事件)
③复制开销:每次调用 select 都需要将文件描述符集合fd_set从用户态空间复制到内核态空间,修改完后再从内核态空间复制回态用户空间,开销较大。
每次调用完select函数之后,rdset都会被修改
内核的轮询机制
(8)select 的应用场景
select 主要用于网络服务器和客户端,允许在单线程中高效处理多个连接或文件描述符的 I/O 操作。
select 主要用于需要同时监视多个文件描述符的场景,如网络服务器需要同时处理多个客户端连接。通过使用 select,程序可以在一个线程中处理多个连接,而不需要为每个连接创建一个线程,从而减少资源开销。
(9)select实现一对一的聊天
select监听,将多个阻塞点变为一个阻塞点。
①fd_set 通常与 select 函数一起使用,以实现非阻塞的 I/O 操作
github代码:https://github.com/WangEdward1027/MultiplayerChatRoom/tree/main/select
(10)select实现多人聊天室
用一个数组,保存 listenfd、已经建立连接的peerfd。
服务器只需要进行回显,不用监听STDIN_FILENO
int conn[1000] = {0};
引入数组的原因:比起位图,方便遍历。数组里直接保存里fd,而位图中不方便获得为1的那个fd是第几位。
数组保存所有活跃的文件描述符,而fd_set用于select函数的调用。这样在遍历和操作时使用数组,而在监视文件描述符状态变化时使用fd_set,两者结合可以充分利用各自的优势。
2.poll
底层实现:链表
监听数量没有上限。但是内核机制也是要遍历每一个fd
3.epoll
e是event事件,poll是轮询
(1)epoll_create()、epoll_create1()
1.函数原型
(1)epoll_create()、epoll_create1()
作用:创建epoll实例
#include <sys/epoll.h>int epoll_create(int size);
int epoll_create1(int flags);
int epfd = epoll_create1(0);
参数flags 一直指定为0
返回值:
①成功,返回大于0的文件描述符epfd
②失败,返回-1
(2)epoll_ctl
1.作用:增删改内核的红黑树
2.函数原型
#include <sys/epoll.h>int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
举例:
struct epoll_event ev;
memset(&ev, 0, sizeof(ev));
ev.events = EPOLLIN; //套接字可读, 关注listenfd的读事件
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
①参数1:epfd指定epoll实例
②参数2:op操作
EPOLL_CTL_ADD
:增加一个监听,添加一个fd到监听红黑树中,
EPOLL_CTL_MOD
:修改一个监听
EPOLL_CTL_DEL
:删除一个监听
③参数3:fd 指定要处理的文件描述符
④参数4 :指的是fd对应的value。参数3和参数4是键值对信息,要挂到红黑树上。
typedef union epoll_data{void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event {uint32_t events; // 要监听的事件类型epoll_data_t data; // 用户数据
};
要监听的事件:
①EPOLLIN
读事件,0x001
②EPOLLIOUT
写事件,0x004
③EPOLLIET
边缘触发,1u << 31
④EPOLLERR
发生错误
联合体 union
n个字段,共享同一片地址空间。但同一时间只能表示一个字段。
3.返回值:
①成功,返回0
②失败,返回-1,并设置errno
(3)epoll_wait
1.作用:监听响应
阻塞等待,阻塞式函数
#include <sys/epoll.h>int epoll_wait(int epfd, struct epoll_event* events,int maxevents, int timeout);
①参数1:epfd 指定 epoll的实例
②参数2:events 代表的是用户态空间的struct epoll_event数组的首地址
③参数3:maxevents 数组的长度
④参数4:timeout是超时时间,-1为无限期等待。单位是毫秒ms
返回值:
①大于0,表示的是就绪的文件描述符的数量。
返回值n>0,需要处理两类文件描述符:
②等于0,epoll_wait超时
③小于0,发生错误,返回-1,并设置errno
(4)底层实现
内核态维护红黑树和就绪链表
①用红黑树保存监听事件的集合,没有数量上限。
②用就绪链表保存已经就绪的事件集合
③epoll_wait不需要重复监听文件描述符,永久化注册文件描述符。因为红黑树在内核中。
events数组就是由内核的就绪链表而来的。
(5)epoll_wait的底层实现与select有显著区别
(1)select要在内核主动轮询所有fd,判断是否发生了事件
(2)epoll的底层实现有一个回调机制。一旦监听的某一个文件描述符发生了事件,会主动通知epoll进行处理。大大提升了效率。
(6)epoll_wait比起select的优势
①事件驱动:epoll 使用事件驱动机制,只有在有事件发生时才通知用户程序,避免了无效的扫描。
在用户态对于要处理的文件描述符可以精确的知道,不需要遍历所有已经建立好的连接。
②epoll_wait不需要重复监听文件描述符。
③由于内核使用红黑树进行存储,没有文件描述符上限的限制
④内核空间直接操作:epoll在内核态操作红黑树和就绪链表,减少了用户空间和内核空间之间的复制开销。
⑤边缘触发模式:epoll 支持边缘触发(edge-triggered)模式,进一步减少系统调用次数,提高效率。
(7)epoll_wait的两种就绪方式:水平触发和边缘触发
①水平触发 (强监督)
EPOLLLT,level trigger,强监督
内核缓冲区有数据,就一直通知:
当有事件发生时,内核的接收缓冲区有数据,如果不去处理,epoll_wait会一直返回,通知相应的连接进行处理
②边缘触发 (弱监督)
EPOLLET,edge-triggered,弱监督
内核缓冲区收到新数据,才通知一次:
当内核的接收缓冲区收到数据时,会通知一次。如果不去接收数据,epoll也不会再通知处理。
直到内核的接收缓冲区又收到了新的数据,才会再次触发,epoll_wait再返回一次。
ev.events = EPOLLIN | EPOLLET;
epoll_wait 的就绪触发有两种方式:一种是默认的水平触发方式(Level-triggered),另一种是边缘触发
模式(Edge-triggered)。
以读事件为例子:水平触发模式下,只要缓冲区当中存在数据,就可以使epoll_wait 就绪;在边缘触发的情况下,如果缓冲区中存在数据,但是数据一直没有增多,那么epoll_wait 就不会就绪,只有缓冲区的数据增多的时候,即下图中绿色的上升沿部分时,才能使epoll_wait 就绪。
(8)设置为非阻塞式:fcntl()
1.将一个文件描述符设置为非阻塞
一旦peerfd设置为非阻塞之后,调用recv/send就不会再阻塞
2.只希望某一次调用recv函数时,采用非阻塞,可以设置第四个参数MSG_DONTWAIT
(9)epoll实现多人聊天室
github代码:https://github.com/WangEdward1027/MultiplayerChatRoom/tree/main/epoll_new
(四) HTTP
0.互联网的诞生
1989年,蒂姆·伯纳斯-李(Tim Berners-Lee)提出了互联网超链接文档系统的三大技术:
①HTML:超文本标记语言,是一棵DOM(document)树,对文档进行展示
②URI/URL:统一资源定位符,对文档进行定位
③HTTP:超文本传输协议,对文档进行传输
蒂姆把这个系统称为“万维网”(World Wide Web),也就是我们现在所熟知的 Web。
1.HTML
超文本标记语言(HTML5,Hypertext Markup Language),是一棵DOM(文档, document)树,对文档进行展示
<html><head> <title> ... </title>... </head><body><form>...</form></body>
</html>
Web文档:一般用HTML文件描述,布局描述文件是CSS文件,网页脚本文件则是由JavaScript语言
编写的程序。
网页的构成:网页三剑客
(1)HTML:超文本标记语言(Hyper Text Markup Language),是一棵DOM (文档, document)树,有n个结点 [网页的骨架]
(2)CSS:层叠样式表(Cascading Style Sheets),来描述一个网页、HTML文件、XML文件的表现与展示效果,用来对文档进行布局,解决了结点的样式 (字体大小、颜色、位置) [网页的面貌、布局]
(3)JavaScript:脚本语言 (动态语言) [灵魂]
①脚本语言,动态地修改DOM树上结点的信息
②又是动态语言,不需要编译,直接解释执行
2.URI/URL
1.URI:统一资源标识符,Uniform Resource Identifier。URI的两种子类型,URL和URN(Uniform Resource Name)。
URL:统一资源定位符,Uniform Resource Locator。URL不仅标识资源,还提供了访问资源的具体方法。
2.URI/URL的基本结构:
(1)方案(scheme):指定使用的协议,如http、https、ftp等
(2)用户信息(User Information):包括用户名和密码,格式为username:password@,此部分经常被忽略
(3)主机(host):资源所在的主机,域名 或 IP地址
(4)端口号(port):指定访问资源的端口号
(5)路径(path):指定资源所在的具体路径,例如 /path/to/resource
①静态资源:直接存储在服务器上的文件,*.html、*.gif、*.jpg、*.mp4
②动态资源:并没有直接存储在服务器上,而需要经过业务逻辑的计算后得到的
(6)查询(querry):查询字符串,用于传递参数。以?
开头,以键值对形式出现 key =
value,多个查询词之间用 &
进行连接。例如:?key1=value1&key2=value2
(7)片段(fragment):指定资源内部的一个部分。前面的连接符是 #
,分段字段和服务端无关,客户端用其来定位网页的阅读起始位置
举例:
例1:
https://username:password@www.example.com:8080/path/to/resource?key1=value1&key2=value2#section1
①方案 (Scheme):https
://
②用户信息 (User Info):username:
password@
③主机 (Host):www.example.com
:
④端口 (Port):8080
⑤路径 (Path):/
path/to/resource
?
⑥查询 (Query):key1=value1&key2=value2
#
⑦片段 (Fragment):section1
例2:
https://www.baidu.com/s?wd=同济大学
s代表search,wd代表word
3.HTTP协议
1.HTTP,超文本传输协议,HyperText Transfer Protocol
(1)超文本,不仅仅是文本,还包含图片、视频、二进制信息
(2)传输:通信的双方基于C-S模型(Client-Server),一个请求对应一个响应。
(3)协议:HTTP是应用层协议,基于传输层的TCP协议
2.HTTP协议的特点:C/S模型、可靠、无状态、文本协议、明文传输的
(1)客户端-服务端模型 (C/S模型)
设计思想:函数调用/返回,本地和远程类似 RPC(远程过程调用,Remote Procedure Call)
(2)可靠性:基于TCP协议进行传输
(3)无状态协议:每一个HTTP请求都是独立的,不依赖于上下文。
将HTTP设计为无状态协议的好处:
①简化服务器的设计
②在进行扩展时,可以方便实现大并发
③MVC模型
①简化实现:每个请求独立,服务器无需维护复杂的会话管理逻辑,从而简化了服务器的实现和维护。
②易于扩展、负载均衡、故障恢复:易于横向扩展,即增加更多服务器来处理请求。易于实现负载均衡。某个服务器出现故障,客户端可以轻松重试请求或将请求发送到另一个服务器。
③系统架构:三层架构,MVC模型
1>视图展示层:VIEW
2>业务逻辑层:CONTROL
3>数据持久层:MODEL
客户端有需要一些技术支撑:
token技术
cookie技术
session技术
Local Storage技术
(4)文本协议:报文头是文本的(字符串)
在有些情况下,可以对HTTP传输的数据进行加密。 TLS/SSL就是一种加密工具,工作在HTTP和TCP之间。我们把基于TLS协议的HTTP称作HTTPS
协议,它提供了更好的安全性。
HTTP协议与TCP协议的区别?
①HTTP是应用层协议,TCP是传输层协议
②HTTP协议无状态,TCP协议是有状态的
③HTTP协议本身是无连接的、无状态的。HTTP协议在发送请求时不需要建立持久连接,每个请求都是独立的。TCP协议是有连接的
④HTTP是文本协议,TCP协议是字节流的、二进制的
3.两种提升服务器性能的方式:
①垂直扩展:提升单台服务器的CPU、内存、磁盘
②水平扩展:买多台服务器,负载均衡
4.HTTP组件:客户端、服务器、代理
(1)客户端:
①浏览器:网址部分输入IP:端口号
②Postman:
③curl命令行 (Client URL):curl http://IP:端口号
(2)服务器:
①Nginx:基于事件驱动,支持高并发
②Apache:底层实现采用子进程模型。基于进程的模型,每一个请求都用一个子进程进行交互。
(3)代理:处于客户端与服务器之间的位置
①负载均衡:nginx本身就可以作为反向代理服务器,用来做负载均衡
②日志记录
③信息过滤
④权限控制
5.如何发起一个HTTP请求?
①通过URL发起
②通过HTML中的一些元素,如 表单
③通过JavaScript语言
6.实现一个简单的HTTP服务器
基于TCP协议,TCP套接字通信流程
4.HTTP报文
(1)HTTP请求报文
1.HTTP请求报文的组成部分:
①起始行(start line) / 请求行 (Request line):方法、路径、协议版本
②首部字段 (Request Headers)
③空行 (empty line)
④消息体 / 报文体 / 请求体 (Request Body)
HTTP请求报文的格式:
起始行 方法 路径 协议版本 \r\n
首部字段 key1:value1 \r\nkey2:value2 \r\n
\r\n (空行)
[报文体]
举例:
GET /index.html HTTP/1.1
Host: www.example.com
Accept: text/html
Connection: keep-alive
\r:表示回车(Carriage Return),在ASCII编码中对应的值是13,0dx
\n:表示换行(Line Feed),在ASCII编码中对应的值是10,0ax
2.HTTP常用的请求方法:
①POST:向服务器提交一个新的信息 (Create)
②GET:查看服务器的一个资源 (Retrieve)。GET请求不携带消息体。
③PUT:修改服务器上的一个资源 (Update)
④DELETE:删除服务器上的一个资源 (Delete)
⑤HEAD:请求服务器的头信息。类似于GET方法,但服务器仅返回响应头,不返回响应主体。
常用于获取资源的元数据。
⑥OPTIONS
⑦PATCH
⑧CONNECT
⑨TRACE
3.POST请求与GET请求的区别:
①POST请求比GET请求 多了消息体
②用途:GET用于请求数据,POST用于提交数据。
③参数位置:GET参数在URL中,POST参数在请求主体中。
4.HTTP协议版本
①0.9版本:只有一个GET方法,相当于测试版本。基于TCP。
②1.0版本:包含所有方法,但仅支持短连接。一次请求/响应结束后,TCP会断开连接。效率较低。
③1.1版本:包含所有方法,支持长连接(持久连接,Connect: keep alive)。目前应用最普遍。当Connection: close 表示要断开TCP连接。复用TCP通道,在一个TCP通道中可以发生多次HTTP请求。
④2.0版本:支持二进制信息
⑤3.0版本:支持基于UDP协议传输数据。在网络稳定性越来越好的今天,不担心丢包的情况下,UDP比TCP传输更快,不需要建立连接,少了7次交互。
QUIC协议,QUIC 是 Quick UDP Internet Connections 的缩写,谷歌发明的新传输协议。
5.首部字段
Accept字段:表示客户端能够解析的媒体类型
*/* 代表能够接受任意类型
Accept:text/plain
Accept:text/html
Accept:image/png
Accept:application/json
MIME类型
类型/子类型
6.消息体 / 报文体(body)
(1)GET请求不携带报文体,发给服务器消息比较少时用GET请求,使用查询词
(2)POST请求携带消息体
消息体的三种类型:
①raw类型:text/plain、application/json (JSON格式字符串)
[key:content-type,value是text/plain 或 application/json]
②application/x-www-form-urlencoded:携带的数据量较小
③form-data:传传输文件可以有多个(文件、图片)。传递的数据量较大。
Q:form-data的边界信息(boundary)起什么作用,有什么限制?
A:当传递文件时,为了获取文件的起始位置和结束位置,就加上了边界信息。
boundary起到了分隔不同部分数据的作用。每个部分都包含自己的头部信息和内容,boundary字符串用来明确区分这些部分,以便接收方能够正确解析和处理每个部分的数据。
限制:文件内容不能包含边界
(2)HTTP响应报文
1.HTTP响应报文的组成:
①起始行(start line) / 状态行(Status Line)
②首部字段 / 响应头部字段 (Response Headers):Server、Content-Type、Content-Length
③空行 (Empty Line)
④消息体 / 响应主体 (Response Body)
HTTP响应报文的格式:
起始行 协议版本 状态码 原因短语 \r\n
响应头部字段
Server: \r\n
Content-Type: \r\n
Content-Length: \r\n
\r\n (空行)
响应数据
2.HTTP状态码
①1xx:连接建立中的情况
②2xx:连接成功,200表示OK;
③3xx:重定向,301 Moved Permanently:请求的资源已永久移动到新位置
④4xx:客户端错误;400 Bad Request:服务器无法理解请求的格式;404 Not Found:请求的资源在服务器上不存在
⑤5xx:服务器错误
(3)作业:http服务器,发送图片给客户端
github代码:https://github.com/WangEdward1027/HTTP
效果展示:
Edge浏览器
5.HTTPS的原理
HTTP是明文传输的,为了保证数据的安全性,必须对HTTP数据进行加解密的功能/服务。
因此google提出了HTTPS
= HTTP + TLS/SSL
HTTPS的实现原理:
将非对称加密算法和对称加密算法结合起来:
①先用非对称加密算法,传输通信双方的对称加密算法的密钥 [安全性高]
②后续再用对称加密算法进行通信 [效率高]
(1)对称加密
通信双方,只有1把密钥
优势:效率很高
劣势:不够安全,密钥的传输是有风险的
加密运算:异或运算
发送端:a^b = c,接收端:c^b =a
(2)非对称加密
RSA算法,公钥和私钥。通信双方共有4把密钥,双方都有公钥、私钥
过程:接收方产生公钥和私钥,将公钥传输给发送方。发送方根据公钥进行加密并发送。接收方根据私钥解密。[公钥和私钥具有数学相关性,但公钥无法推理出私钥]
举例:从gitee远程拷贝到xshell:Xshell作为接收方,生成公钥和私钥,公钥明文,私钥密文看不见。把公钥粘贴给gitee,这时候再使用git clone命令就可以远程拷贝了。gitee根据公钥加密,xshell根据私钥解密。
优势:安全,安全性很高
劣势:效率低
6.Restful编程风格
HTTP协议能够进行数据传输的方式很灵活:①URL中的查询词 ②路径 ③首部字段 ④报文体,不够统一。
业界提出了Restful编程风格:
①用路径表示要访问的资源
②用方法表示对于资源的操作
③用请求报文的消息体来传递参数
④用响应报文的消息体来返回结果
看起来很像函数调用与返回。
实际工作中,以公司的约定为准:例如 阿里巴巴的云计算,OSS对象存储服务,严格遵循Restful风格。
(五) 进程池和线程池
1.进程池
(1)进程池模型
(2)父进程的工作流程
1.创建n个子进程,创建n条管道
2.创建监听套接字listenfd,进行监听
3.创建epoll实例,监听 listenfd 和 n条管道
4.当epoll_wait返回时,两种情况:
①listenfd:获取新客户端连接 peerfd的到来,将peerfd交给空闲的子进程进行任务处理。父进程调用sendFd函数通知子进程。父进程还要更新子进程的状态为BUSY
②管道:当管道中有数据时,就认为是可读。读取管道中的数据,父进程将子进程状态改为FREE。
(3)子进程的工作流程
1.当子进程运行时,就阻塞在管道上(recvFd),等待任务到来
2.recvFd返回后,子进程得到与客户端交互的peerfd
3.执行文件的发送操作
4.通知父进程,发送操作已经完成 (往管道上写了一个数据1)
2.进程间传递文件描述符
(0)提出背景
匿名管道pipe无法实现进程间文件描述符的传递,需要使用socketpair
创建局限于父子进程间的全双工的套接字对 (可认为是一对全双工匿名管道
),再利用sendmsg
和recvmsg
来发送和接收数据
。
(1)socketpair()
1.函数原型:
#include <sys/types.h>
#include <sys/socket.h>int socketpair(int domain, int type, int protocol, int sv[2]);
(1)参数:
①domain:表示通信域,为AF_LOCAL
或AF_UNIX
②type:表示套接字类型,为SOCK_STREAM
或 SOCK_DGRAM
③protocol:通常设置为 0,表示使用默认协议
④sv:一个整数数组,长度为 2。调用成功后,sv[0] 和 sv[1] 将成为一对相互连接的套接字的文件描述符
(2)返回值
①成功,返回0
②失败,返回-1,并设置errno
(3)举例
int pipefds[2];
socketpair(AF_LOCAL, SOL_SOCKET, 0, pipefds);
(2)sendmsg()
1.函数原型
#include <sys/types.h>
#include <sys/socket.h>ssize_t sendmsg(int sockfd, const struct msghdr* msg, int flags);
(1)参数:
①sockfd: 套接字文件描述符
②msg: 指向 msghdr 结构的指针,该结构包含了要发送的消息信息
③flags: 发送消息时使用的标志,可以是 0 或以下标志的组合,例如 MSG_DONTWAIT, MSG_EOR
(2)返回值
①成功,返回成功发送的字节数 (>=0)。这个值表示从 iovec 结构数组中实际发送的数据总字节数
②失败,返回-1,并设置errno
①结构体msghdr
struct msghdr {void* msg_name; //(1)可选的套接字地址socklen_t msg_namelen; //(1)地址的长度struct iovec* msg_iov; //(2)描述消息数据的数组int msg_iovlen; //(2)msg_iov 数组的元素数量void* msg_control; //(3)附加的控制信息(可选)socklen_t msg_controllen; //(3)控制信息的长度(所占用的总字节数)int msg_flags; //(4)消息的标志位
};
(1)第一组参数:套接字地址成员(未使用)
(2)第二组参数:IO向量缓冲区 (聚集写,需要用iovec结构体
填充)
①msg_iov
:指向 struct iovec
结构体数组的指针。iovec 结构体用于描述一块或多块数据缓冲区。通过该字段可以实现分散/聚集 I/O
②msg_iovlen
:msg_iov 数组元素的个数,即数据缓冲区的数量
(3)第三组参数:附属数据缓冲区,控制信息 (需要用cmsghdr结构体
填充)
①msg_control
:控制信息,用cmsghdr
结构体来填充。[指向辅助数据(控制信息)的缓冲区指针,用于传递文件描述符、IP选项等额外信息。]
②msg_controllen
:控制信息缓冲区的长度。如果 msg_control 为 NULL,则应设置为 0
(4)第四组参数:接收信息标记位 (未使用)
②结构体 struct iovec
struct iovec 是一个缓冲区集合
struct iovec {void *iov_base; // 指向缓冲区的指针size_t iov_len; // 缓冲区的长度
};
writev
聚集写:
使用writev系统调用来高效地将多个内存区域写入同一个文件。这种方法比多次调用write更高效,因为它减少了系统调用的次数。减少了内核态和用户态的切换,提高了系统效率。
#include <func.h>
#include <sys/uio.h>int main()
{char buff1[64] = "hello";char buff2[64] = "world";struct iovec iovs[2];iovs[0].iov_base = buff1;iovs[0].iov_len = strlen(buff1);iovs[1].iov_base = buff2;iovs[1].iov_len = strlen(buff2);int fd = open("file.txt", O_RDWR);ERROR_CHECK(fd, -1, "open");//聚集写的操作,//writev是一个系统调用,它可以提高程序的执行效率//(如果不使用writev,那就需要多次write操作,//才能完成数据的写入)int ret = writev(fd, iovs, 2);printf("write %d bytes.\n", ret);return 0;
}
readv
分散读:
将文件读入不同的缓冲区中,注意设置每个缓冲区的读入大小
#include <func.h>
#include <sys/uio.h>int main()
{char buff1[5] = {0};char buff2[6] = {0};struct iovec iovs[2];iovs[0].iov_base = buff1;iovs[0].iov_len = 5;iovs[1].iov_base = buff2;iovs[1].iov_len = 5;int fd = open("file.txt", O_RDWR);ERROR_CHECK(fd, -1, "open");int ret = readv(fd, iovs, 2);printf("read %d bytes.\n", ret);printf("buff1: %s\n", buff1);printf("buff2: %s\n", buff2);return 0;
}
③结构体 struct cmshdr
作用:传递文件描述符
struct cmsghdr {socklen_t cmsg_len; // 控制消息数据的长度,包括头部int cmsg_level; // 控制消息的协议层int cmsg_type; // 控制消息的类型// 后面跟着数据的实际内容/* followed byunsigned char cmsg_data[]; */
};
使用宏函数
在使用 struct cmsghdr 时,通常会用到一些宏函数来方便处理控制消息。这些宏函数包括:
CMSG_LEN:计算控制消息的总长度。
CMSG_DATA:获取控制消息数据的指针。
CMSG_FIRSTHDR:获取第一个控制消息头部。
CMSG_NXTHDR:获取下一个控制消息头部。
CMSG_SPACE:计算控制消息所需的内存空间。
④代码
int sendFd(int pipefd, int fd)
{char buff[4] = {0};//构建第二组成员struct iovec iov;iov.iov_base = buff;iov.iov_len = sizeof(buff);//构建第三组成员size_t len = CMSG_LEN(sizeof(fd));struct cmsghdr * cmsg = calloc(1, len);cmsg->cmsg_len = len;cmsg->cmsg_level = SOL_SOCKET;cmsg->cmsg_type = SCM_RIGHTS;int * p = (int *)CMSG_DATA(cmsg);*p = fd;struct msghdr msg;memset(&msg, 0, sizeof(msg));msg.msg_iov = &iov;msg.msg_iovlen = 1;msg.msg_control = cmsg;msg.msg_controllen = len;//发送数据int ret = sendmsg(pipefd, &msg, 0);printf("sendFd %d bytes.\n", ret);free(cmsg);//回收堆空间return 0;
}
(3)recvmsg()
1.函数原型
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
2.参数同sendmsg
3.代码
int recvFd(int pipefd, int * pfd)
{char buff[4] = {0};//构建第二组成员struct iovec iov;iov.iov_base = buff;iov.iov_len = sizeof(buff);//构建第三组成员size_t len = CMSG_LEN(sizeof(int));struct cmsghdr * cmsg = calloc(1, len);cmsg->cmsg_len = len;cmsg->cmsg_level = SOL_SOCKET;cmsg->cmsg_type = SCM_RIGHTS;struct msghdr msg;memset(&msg, 0, sizeof(msg));msg.msg_iov = &iov;msg.msg_iovlen = 1;msg.msg_control = cmsg;msg.msg_controllen = len;//接收数据, 默认情况下是阻塞的int ret = recvmsg(pipefd, &msg, 0);printf("recvFd %d bytes.\n", ret);*pfd = *(int*)CMSG_DATA(cmsg);free(cmsg);//回收堆空间return 0;
}
3.传输文件问题
(1)小文件传输:粘包问题
1.TCP粘包问题
服务器两次send之后,客户端收到数据时,不知道消息的边界在那里。导致数据解析出现bug
2.小火车协议
解决方案:在应用层认为设计消息的边界,自己定义 小火车协议
因为长度信息占用的字节是固定的,先解析长度信息,再读取对应字节数的文件名/文件内容。
(2)大文件传输:半包问题
1.半包问题
1.半包问题原因:传输大文件时,使用了send函数 或 recv函数
(1)使用send函数,但发送缓冲区不够:比如内核的发送缓冲区只有500字节可用,而此次send发送的数据是1000字节,则本次调用send函数只能拷贝500字节,造成有500字节漏掉未发生。所以一定要判断send函数的返回值。
改进(解决方案):sendn函数
(2)使用recv函数,但网络延迟:发送方的一次send,不能被接收方的一次recv接收 (因为网络问题MTU限制,包被切片成多个)。而send的后半个包延迟到达时,被第二个recv接收,被小火车协议错误解读为下一个包的长度。
第三个参数表示的是这一次调用最多能拷贝的字节数。所以也要判断recv函数的返回值。
改进(解决方案):recvn函数
2.优化:零拷贝技术
1.思想:减少了一次内核态到用户态空间的数据拷贝
2.统一的协议:大火车协议
先发总长度,后面跟的全是数据
3.三种零拷贝技术:
①mmap:将用户态直接映射到内核空间 [服务器端、客户端]
②sendfile:[仅服务器端]
③splice:[服务器端、客户端]
3.发送方:循环发送sendn、3种零拷贝技术
①循环发送sendn
②mmap
链接:https://blog.csdn.net/Edward1027/article/details/139006808
③sendfile
优势:只有3个参数。(参数少,犯错概率小,发送方推荐使用sendfile)
限制:只能在服务器端使用;限制2GB以内的文件;仅支持将数据从普通文件拷贝到套接字
#include <sys/sendfile.h>ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
④splice
需要借助匿名管道
#include <fcntl.h>
#include <unistd.h>ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out,size_t len, unsigned int flags);
md5sum filename
计算文件的哈希值,判断两个文件是否相同。
4.接收方:循环接收recvn、waitall、mmap、splice
①recvn
②recv(waitall)
③mmap
mmap既可以发送端用,也可以接收端用
④splice
同上文,发送端splice
4.进程池的退出
(0)设计思想:异步拉起同步
异步的信号,进程同步
流程:
0.父进程先注册SIGUSR1信号,并设置信号处理函数 [信号处理函数的执行流程]
1.父进程设置一条退出的匿名管道,epoll要监听该匿名管道
2.通过终端给父进程发起SIGUSR1信号。信号处理函数的执行流程:往退出的匿名管道中写一个数据,比如1
3.在父进程的epoll循环中,会收到匿名管道就绪的通知。之后就去执行进程退出的流程。
①暴力退出:给所有子进程发送SIGUSR1信号,暴力终结每一个子进程。
父进程再回收子进程的资源,父进程再退出。整个进程池就退出了。
②温和退出:父进程通过sendFd函数发送一个进程池退出的标志位,子进程通过recvFd函数收到该标志位之后,在完成任务后,退出子进程的任务执行流。
父进程再回收子进程的资源,父进程再退出。整个进程池就退出了。
(1)暴力退出:用kill()直接杀死子进程
父进程直接强制使子进程退出,不管子进程是否正在执行任务
(2)优雅退出:通知子进程exit
父进程通知子进程退出,当子进程执行完任务后自行退出。
5.线程池
1.图
补ppt63页
2.内核通过LWP
(轻量级进程,Light Weight Process)识别线程。进程内部通过pthread_t tid
识别线程。
3.线程池的退出:
(1)父子进程,父进程接收kill信号,子进程执行线程池的退出
5 线程池
5.1 模型
5.2 流程
主线程:
- 初始化线程池
- 启动线程池
- 创建监听的套接字
- 创建epoll的实例,添加监听
- 接收客户端peerfd的到来
- 再将peerfd加入到任务队列中去
子线程:
-
先从任务队列中获取任务节点, 拿到peerfd
-
执行文件发送任务
-
文件发送完毕之后,进入等待状态,试图继续获取下一个任务节点
5.3 数据结构
- 任务节点
- 任务队列
接口:
-
queueInit()
-
queueDestroy()
-
taskEnqueue()
-
taskDequeue()
-
taskSize()
-
queueEmpty()
- 线程池
接口:
5.4 接口实现
5.4.1 任务队列
void queueInit(task_queue_t* queue)
void queueDestroy(task_queue_t* queue)
taskSize() queueIsEmpty()
void taskEnqueue(task_queue_t* queue, int peerfd)
int taskDequeue(task_queue_t* queue)
void broadcastAll(task_queue_t* queue)
5.4.2 线程池
void* threadFunc(void* arg)
void threadpoolInit(threadpool_t* threadpool, int num) void threadpoolDestroy(threadpool_t* threadpool)
void threadpoolStart(threadpool_t* threadpool)
void threadpoolStop (threadpool_t* threadpool)
5.4.3 main()函数
int exitPipe[2];void sighandler(int num) {printf("sig %d is coming.\n", num);// 写入管道,通知子进程退出int one;write(exitPipe[1], &one, sizeof(one));
}int main(int argc, char* argv[]) {// ip port threadnumARGS_CHECK(argc, 4);// exitPipe 用于父子进程之间pipe(exitPipe);pid_t pid = fork();if (pid > 0) {// 父进程中// 只在父进程中注册10号信号signal(SIGUSR1, sighandler);wait(NULL); // 等待子进程执行完退出exit(0);}// 在子进程中进行线程池的操作// 初始化线程池threadpool_t threadpool;threadpoolInit(&threadpool, atoi(argv[3]));// 启动线程池threadpoolStart(&threadpool);// 创建TCP监听套接字int listenfd = tcpInit(argv[1], argv[2]);printf("server start listening.\n");// 创建epoll实例int epfd = epoll_create1(0);ERROR_CHECK(epfd, -1, "epoll_create1");// 添加监听epollAddFd(epfd, listenfd);epollAddFd(epfd, exitPipe[0]);struct epoll_event* pEventArr = (struct epoll_event*)calloc(EVENT_ARR_SIZE, sizeof(struct epoll_event));while (1) {int nready = epoll_wait(epfd, pEventArr, EVENT_ARR_SIZE, -1);for (int i = 0; i < nready; i++) {int fd = pEventArr[i].data.fd;if (fd == listenfd) {int peerfd = accept(listenfd, NULL, NULL);// 添加到任务队列taskEnqueue(&threadpool.queue, peerfd);} else if (fd == exitPipe[0]) {// 执行退出printf("read exitPipe[0]");int howmany = 0;// 读取管道中的数据,避免重复通知read(exitPipe[0], &howmany, sizeof(howmany));// 执行退出流程threadpoolStop(&threadpool);threadpoolDestroy(&threadpool);close(listenfd);close(epfd);exit(0);}}}return 0;
}
5.5 线程池退出
流程:
流程图:
- 在父进程注册SIGUSR1
- 子进程注册监听exitPipe[0],
- 子进程监听到exitPipe[0]