网络编程
day1笔记
协议:
一组规则。
分层模型结构:
1 | OSI七层模型: 物、数、网、传、会、表、应 |
c/s模型:
1 | client-server |
b/s模型:
1 | browser-server |
网络传输流程:
1 | 数据没有封装之前,是不能在网络中传递。 |
1 | ARP协议:根据 Ip 地址获取 mac 地址。 |
IP协议:
1 | 版本: IPv4、IPv6 -- 4位 |
IP地址:可以在网络环境中,唯一标识一台主机。
端口号:可以网络的一台主机上,唯一标识一个进程。
ip地址+端口号:可以在网络环境中,唯一标识一个进程。
UDP:
16位:源端口号。 2^16 = 65536
1 | 16位:目的端口号。 |
TCP协议:
1 | 16位:源端口号。 2^16 = 65536 |
网络套接字: socket
1 | 一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。) |
网络字节序:
1 | 小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678 |
IP地址转换函数:
1 | int inet_pton(int af, const char *src, void *dst); 本地字节序(string IP) ---> 网络字节序 |
sockaddr地址结构: IP + port –> 在网络环境中唯一标识一个进程。
1 | struct sockaddr_in addr; |
socket函数:
1 | #include <sys/socket.h> |
1 | 返回值: |
1 | int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用现有的 socket 与服务器建立连接 |
1 | addrlen:服务器的地址结构的大小 |
TCP通信流程分析:
1 | server: |
socket
1 |
|
第一个窗口
第二个窗口
nc 127.0.0.1 9527 客户端向服务器发送信息
1 |
|
客户端程序
1 |
|
TCP三次握手
三次握手 建立连接
\MyBlog\source_posts\网络编程\1708422833527.png)
\MyBlog\source_posts\网络编程\1708422817941.png)
四次握手 断开连接
\MyBlog\source_posts\网络编程\1708431321844.png)
导致四次握手才能断开连接的原因是什么:半关闭
每个socket套接字里面有两个缓冲区:写数据的缓冲区和都数据的缓冲区,
在客户端发送FIN信号之后,服务器端同意了,然后客户端关闭自己的写缓冲区,但是连接还是存在的,它可以发送不在写缓冲区的信号,比如在TCP头部的ACK信号
\MyBlog\source_posts\网络编程\1708431678033.png)
客户端:我不说话啦?
服务器:嗯
客户端:闭上了自己的嘴(但是两人的电话还没挂断)
除了SYN、ACK、FIN信号,还有一个滑动窗口win:用来告诉对方我能接收的最大数据是多少,够了这个数据就不要发了
\MyBlog\source_posts\网络编程\1708432554044.png)
这里假定左边是客户端右边是服务器,客户端是4096服务器是4144,发到第9个就不再发了,因为客户端发到第9个已经发了5121+1024-1=6144了,再发就超了
\MyBlog\source_posts\网络编程\1708432889242.png)
然后后面服务器不断处理读缓冲区的数据,从6144-6144=0变为2048再变为4096
滑动窗口就是为了防止数据丢失
三次握手与函数的对应关系:
\MyBlog\source_posts\网络编程\1708435420246.png)
三次握手四次挥手与TCP状态对应关系:
(参考上面那张图好看)
\MyBlog\source_posts\网络编程\1708501001865.png)
三次握手四次挥手与TCP状态对应关系
\MyBlog\source_posts\网络编程\1708502991908.png)
只有主动关闭连接这一方会经历FIN_WAIT2(半关闭)和TIME_WAIT状态和2MSL时长才能关闭
只有被动关闭一方有CLOSE_WAIT,此时主动关闭一方处于版关闭状态
所以如果先关闭服务器,想要接着重启会报错Address is already in use,因为他是主动关闭连接一方正在等待2MSL时长,而如果先关闭客户端,因为他的端口是系统自动分配的不固定,所以没事
端口复用:
服务器是在bind函数之前(可能客户端在connect函数之前)
1 | int opt = 1; |
SO_REUSEADDR 端口复用
心跳包:
如果对方异常断开,本机检测不到,一直等待,浪费资源,需要设置TCP的保持连接,作用就是每隔一定的时间间隔发送探测分节,如果连续发送多个探测分节对方还未回应,就将次连接断开
1 | int opt = 1; |
SO_KEEPALIVE表示保持连接,如果两个小时内这个套接字的口没有数据交换,发一次报文,对方如果不回复就自动关闭这个进程
也可以不用SO_KEEPALIVE自己写一个规定时间
心跳包:最小粒度
乒乓包:携带比较多的数据的心跳包
主动关闭端的2MSL时长:
保证最后一个ACK能成功被对端接收。(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)
半关闭
通信双方中,只有一端关闭通信。——FIN_WAIT_2
关闭使用close(cfd)或者shutdown(cfd,SHUT_RD)
两者的区别:
shutdown可以更精细的关闭写缓冲区或者读缓冲区
如果使用dup2(cfd,cfd1);
当使用close(cfd)之后虽然cfd关闭了但是cfd1还可以用
使用shutdown(cfd,SHUT_RD)cfd1也不能用了
day2笔记
三次握手:
1 | 主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。 |
四次挥手:
1 | 主动关闭连接请求端, 发送 FIN 标志位。 |
1 | 被动关闭连接请求端, 发送 FIN 标志位。 |
滑动窗口:
1 | 发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。 |
错误处理函数:
1 | 封装目的: |
1 | 【wrap.c】 【wrap.h】 |
1 | 存放网络通信相关常用 自定义函数 存放 网络通信相关常用 自定义函数原型(声明)。 |
readn:
读 N 个字节
readline:
1 | 读一行 |
read 函数的返回值:
1 | 1. > 0 实际读到的字节数 |
多进程并发服务器:server.c
1 | 1. Socket(); 创建 监听套接字 lfd |
多线程并发服务器: server.c
1 | 1. Socket(); 创建 监听套接字 lfd |
多路IO转接(响应式)服务器
设计两个监听 lfd和cfd
lfd是系统创建的,创建好交给小秘书让他监听,是否有人跟我连接,如果有,再调用accept函数,这样accept不用一直处于阻塞状态
cfd监听是都进行数据通信
过程:
如果有c2想向我连接,他先找小秘书,小秘书再找我,然后我调accept函数,就可以直接返回一个cfd2,不用阻塞等着,,然后产生的cfd2继续交给小秘书(select()函数)监听
三种响应模式:
响应式:上述带个秘书用来监听的模式(有学生听不懂问老师,老师再给解答)
阻塞式:一个进程在那里等着(老师问学生有没有听不懂,学生不回答,老师就一直等着)
非阻塞忙轮询:(老师问学生有没有听不懂,学生不回答,老师回去忙5分钟再去问学生)
小秘书有
poll()
,epoll()
,select()
select函数实现响应式服务器
select函数
原理: 借助内核, select 来监听, 客户端连接、数据通信事件。
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1
readfds: 读 文件描述符监听集合。 传入、传出参数
writefds:写 文件描述符监听集合。 传入、传出参数 NULL
exceptfds:异常 文件描述符监听集合 传入、传出参数 NULL
timeout: > 0: 设置监听超时时长。
NULL: 阻塞监听
0: 非阻塞监听,轮询
返回值:
> 0: 所有监听集合(3个)中, 满足对应事件的总数。
0: 没有满足监听条件的文件描述符
-1: errno
在执行完select函数之后,前面的文件描述符集合也会变化,变成发生事件的文件描述符,比如之前读事件描述符的输入3、4、5,发生读事件的是4、5,文件描述符就变成4、5
\MyBlog\source_posts\网络编程\1708518269532.png)如图所示请求nfds=6+1
如果有新的c4发出connect请求,就是让lfd发出读时间,这个select监听就是在监听这些文件描述符有没有读事件
原理: 借助内核, select 来监听, 客户端连接、数据通信事件。
设置集合的函数:
传入select的是文件描述符集合,因此要把需要监听的文件描述符加入集合
1 | void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。 |
1 | void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。 |
思路分析:
一般之考虑读事件,lfd连接请求是读事件,cfd读也是读事件
而cfd写事件一般不考虑,因为一般客户端请求连接接通之后,服务器向客户端写一般是都可以的,除了客户端发送FIN信号之后处于半关闭状态或者服务器写的太快了数据量超过滑动窗口的这两种情况
int maxfd = 0;
lfd = socket() ; 创建套接字
maxfd = lfd;
bind(); 绑定地址结构
listen(); 设置监听上限
fd_set rset, allset; 创建r监听集合,一个里面只有lfd,一个是lfd和后来select加上的cfd
FD_ZERO(&allset); 将r监听集合清空
FD_SET(lfd, &allset); 将 lfd 添加至读集合中。
while(1) {
rset = allset; 保存监听集合 ret = select(lfd+1, &rset, NULL, NULL, NULL); 监听文件描述符集合对应事件。 if(ret > 0) { 有监听的描述符满足对应事件 if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。 cfd = accept(); 建立连接,返回用于通信的文件描述符 maxfd = cfd; FD_SET(cfd, &allset); 添加到监听通信描述符集合中。 } for (i = lfd+1; i <= 最大文件描述符; i++){ FD_ISSET(i, &rset) 检测新加入的cfd是否有read、write事件 read() 小 -- 大 write(); } }
}
select实现代码:
F:\a语法基础\C++\Linux\linux网络编程资料\day3\4-源代码\select_concurrent
1 |
|
select优缺点:
缺点: 监听上限受文件描述符限制。 最大 1024.
检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
优点: 跨平台。win、linux、macOS、Unix、类Unix、mips
day3笔记
TCP状态时序图:
结合三次握手、四次挥手 理解记忆。
1. 主动发起连接请求端: CLOSE -- 发送SYN -- SEND_SYN -- 接收 ACK、SYN -- SEND_SYN -- 发送 ACK -- ESTABLISHED(数据通信态)
2. 主动关闭连接请求端: ESTABLISHED(数据通信态) -- 发送 FIN -- FIN_WAIT_1 -- 接收ACK -- FIN_WAIT_2(半关闭)
-- 接收对端发送 FIN -- FIN_WAIT_2(半关闭)-- 回发ACK -- TIME_WAIT(只有主动关闭连接方,会经历该状态)
-- 等 2MSL时长 -- CLOSE
3. 被动接收连接请求端: CLOSE -- LISTEN -- 接收 SYN -- LISTEN -- 发送 ACK、SYN -- SYN_RCVD -- 接收ACK -- ESTABLISHED(数据通信态)
4. 被动关闭连接请求端: ESTABLISHED(数据通信态) -- 接收 FIN -- ESTABLISHED(数据通信态) -- 发送ACK
-- CLOSE_WAIT (说明对端【主动关闭连接端】处于半关闭状态) -- 发送FIN -- LAST_ACK -- 接收ACK -- CLOSE
重点记忆: ESTABLISHED、FIN_WAIT_2 <--> CLOSE_WAIT、TIME_WAIT(2MSL)
netstat -apn | grep 端口号
2MSL时长:
一定出现在【主动关闭连接请求端】。 --- 对应 TIME_WAIT 状态。
保证,最后一个 ACK 能成功被对端接收。(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)
端口复用:
int opt = 1; // 设置端口复用。
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
半关闭:
通信双方中,只有一端关闭通信。 --- FIN_WAIT_2
close(cfd);
shutdown(int fd, int how);
how: SHUT_RD 关读端
SHUT_WR 关写端
SHUT_RDWR 关读写
shutdown在关闭多个文件描述符应用的文件时,采用全关闭方法。close,只关闭一个。
select多路IO转接:
原理: 借助内核, select 来监听, 客户端连接、数据通信事件。
void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。
fd_set rset;
FD_ZERO(&rset);
void FD_SET(int fd, fd_set *set); --- 将待监听的文件描述符,添加到监听集合中
FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);
void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。
FD_CLR(4, &rset);
int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。
返回值: 在:1;不在:0;
FD_ISSET(4, &rset);
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1
readfds: 读 文件描述符监听集合。 传入、传出参数
writefds:写 文件描述符监听集合。 传入、传出参数 NULL
exceptfds:异常 文件描述符监听集合 传入、传出参数 NULL
timeout: > 0: 设置监听超时时长。
NULL: 阻塞监听
0: 非阻塞监听,轮询
返回值:
> 0: 所有监听集合(3个)中, 满足对应事件的总数。
0: 没有满足监听条件的文件描述符
-1: errno
思路分析:
int maxfd = 0;
lfd = socket() ; 创建套接字
maxfd = lfd;
bind(); 绑定地址结构
listen(); 设置监听上限
fd_set rset, allset; 创建r监听集合
FD_ZERO(&allset); 将r监听集合清空
FD_SET(lfd, &allset); 将 lfd 添加至读集合中。
while(1) {
rset = allset; 保存监听集合
ret = select(lfd+1, &rset, NULL, NULL, NULL); 监听文件描述符集合对应事件。
if(ret > 0) { 有监听的描述符满足对应事件
if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。
cfd = accept(); 建立连接,返回用于通信的文件描述符
maxfd = cfd;
FD_SET(cfd, &allset); 添加到监听通信描述符集合中。
}
for (i = lfd+1; i <= 最大文件描述符; i++){
FD_ISSET(i, &rset) 有read、write事件
read()
小 -- 大
write();
}
}
}
select优缺点:
缺点: 监听上限受文件描述符限制。 最大 1024.
检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
优点: 跨平台。win、linux、macOS、Unix、类Unix、mips
poll函数实现响应式服务器(了解)
poll函数
poll是对select的改进,但是它是个半成品,相对select提升不大。最终版本是epoll,所以poll了解一下就完事儿,重点掌握epoll。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:监听的文件描述符【数组】
struct pollfd {
int fd: 待监听的文件描述符
short_events: 待监听的文件描述符对应的监听事件
取值:POLLIN、POLLOUT、POLLERR
short revnets: 传入时, 给0。如果满足对应事件的话, 返回 非0 –> POLLIN、POLLOUT、POLLERR
}
struct pollfd *fds 中的fds是个数组,调用也是fds[0].fd,fds[0].short_events调用
nfds: 监听数组的,实际有效监听个数。
timeout: > 0: 超时时长。单位:毫秒。
-1: 阻塞等待
0: 不阻塞
返回值:返回满足对应监听事件的文件描述符 总个数。
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。
拓展 监听上限。 超出 1024限制。
缺点:
不能跨平台。 Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。
思路分析:
\MyBlog\source_posts\网络编程\图片1.png)
poll实现代码
1 | /* server.c */ |
poll优缺点:
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。
拓展 监听上限。 超出 1024限制。
修改:
打开 sudo vi /etc/security/limits.conf, 写入:* soft nofile 65536 --> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】 * hard nofile 100000 --> 命令修改上限。
缺点:
不能跨平台。 Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。(和select相同)
突破 1024 文件描述符限制:
先查看:
cat /proc/sys/fs/file-max –> 当前计算机所能打开的最大文件个数。 受硬件影响。
ulimit -a ——> 当前用户下的进程,默认打开文件描述符个数。 缺省为 1024
\MyBlog\source_posts\网络编程\1708604512946.png)
修改:
打开 sudo vi /etc/security/limits.conf, 写入:
* soft nofile 65536 --> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效,往下调(1000->500)可以直接生效,往上调(1000->2000)需要注销用户,可以直接借用命令修改(open files后面有个(-n):ulimit -n 21000】
* hard nofile 100000 --> 命令修改上限。来限制上面修改的最大值
\MyBlog\source_posts\网络编程\1708605538370.png)
epoll函数实现响应式服务器(重要)
使用有大量连接但是只有少量活跃的情况
epoll函数
(平衡二叉树里面的特例:红黑树)
1、创建一棵监听红黑树
int epoll_create(int size);
size:创建的红黑树的监听节点数量。(仅供内核参考。)
返回值:指向新创建的红黑树的根节点的 fd。
失败: -1 errno
2、操作监听红黑树
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_create 函数的返回值。 epfd
op:对该监听红黑数所做的操作。
EPOLL_CTL_ADD 添加fd到 监听红黑树
EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。
EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
fd(fd111):
待监听的fd
event: 本质 struct epoll_event 结构体 的地址,这个参数是一个传出参数,是要把这个结构体放到树上去,将来如果对应的事件满足的话,其中的fd就会被设置的后面的epoll_wait()函数里面
成员 events:
EPOLLIN / EPOLLOUT / EPOLLERR
成员 data: 联合体(共用体):
int fd; 对应监听事件的 fd 对应前面的待监听的fd(fd111)
void *ptr; 后面再讲 libevent通过把它打造成回调函数的模式(epoll反应堆模型),它和fd是共用体,和fd共享同一块地址空间
uint32_t u32; 这个不用
uint64_t u64; 这个不用
返回值:成功 0; 失败: -1 errno
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd *fds 中的fds是个数组,调用也是fds[0].fd,fds[0].short_events调用,这个0也是一个结构体 的地址,这个fds作为数组就是这些地址的集合
数组是一连串连续的地址,数组名是首地址,指针指向下一个地址
3、阻塞监听。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd:epoll_create 函数的返回值。 epfd
events:传出参数,【数组】, 满足监听条件的 那些 fd 结构体。,第二个函数中的event是一个结构体,而这个events是一个数组,用来存内核得到事件的集合
maxevents:数组 元素的总个数。 1024
struct epoll_event evnets[1024]
timeout:
-1: 阻塞
0: 不阻塞
>0: 超时时间 (毫秒)
返回值:
> 0: 满足监听的 总个数。 可以用作循环上限。
0: 没有fd满足监听事件
-1:失败。 errno
思路分析
基本就是为kfd(cfd)创建一个结构体,然后挂到红黑树上,再在while(1)里面执行监听操作,如果有事件发生,再执行accept挂到红黑树(lfd)或者进行读写操作(cfd)
lfd = socket(); 监听连接事件lfd
bind();
listen();
int epfd = epoll_create(1024); epfd, 监听红黑树的树根。
struct epoll_event tep, ep[1024]; tep, 临时变量,反复使用,用来设置单个fd属性, ep 是 epoll_wait() 传出的满足监听事件的数组。
tep.events = EPOLLIN; 初始化 lfd的监听属性。
tep.data.fd = lfd
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep); 将 lfd 添加到监听红黑树上。
while (1) {
ret = epoll_wait(epfd, ep,1024, -1); 实施监听
for (i = 0; i < ret; i++) {
if (ep[i].data.fd == lfd) { // lfd 满足读事件,有新的客户端发起连接请求
cfd = Accept();
tep.events = EPOLLIN; 初始化 cfd的监听属性。
tep.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);
} else { cfd 们 满足读事件, 有客户端写数据来。
n = read(ep[i].data.fd, buf, sizeof(buf));
if ( n == 0) {
close(ep[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL); // 将关闭的cfd,从监听树上摘下。
} else if (n > 0) {
小--大
write(ep[i].data.fd, buf, n);
}
}
}
}
epoll实现代码
1 |
|
epoll的触发模式:
ET(edge-triggered)模式:
边沿触发:
缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
LT(level triggered)模式:
水平触发 -- 默认采用模式。
缓冲区剩余未读尽的数据会导致 epoll_wait 返回。
两者的区别在于:
如果创建文件描述符,先写入”aaaa\n”,又写入”bbbb\n”,让这个文件描述符上树,在另一变只读取了”aaaa\n”,还剩下”bbbb\n”,如果是LT可以读,但是ET读不了,只有再往后写ET也读不了,只有再往后写”cccc\n”才能读”bbbb\n”
如果对方发来的1000个字节,我只需要读前500个字节剩下的想丢弃,那么我用ET模式,但是要ET就必须用非阻塞模式
server边沿触发,编译运行,结果如下:
\MyBlog\source_posts\网络编程\图片2.png)
运行后,每过5秒钟服务器才输出一组字符,这是就是边沿触发的效果。
更改服务器为水平触发模式,运行程序,如下:
\MyBlog\source_posts\网络编程\图片3.png)
运行后,每5秒输出两组字符串,这是因为只写入了两组,这个模式的服务器,缓冲区有多少读多少。
结论:
epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 --- 忙轮询。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; //设置方式:原来是event.events = EPOLLIN 改成这个
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
int flg = fcntl(cfd, F_GETFL);
flg |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flg);
优点:
高效。突破1024文件描述符。
缺点:
不能跨平台。 Linux。
文件描述符:
文件描述符可以来源于管道运算符pipe(进程间通信),也可以来源于套接字socket(网络通信)
readn阻塞:
readn调用的阻塞,比如设定读500个字符,但是因为只发送了498,就只读到498个字符,完事儿阻塞了,等另剩下的2个字符,然而在server代码里,一旦read变为readn阻塞了,它就不会被唤醒了,因为epoll_wait因为readn的阻塞不会循环执行,读不到新数据。有点死锁的意思,差俩字符所以阻塞,因为阻塞,读不到新字符。
LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET(edge-triggered):ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).
如果要在ET的情况下用fcntl函数实现非阻塞:
\MyBlog\source_posts\网络编程\1708659878332.png)
三个参数分别是文件描述符、要干的事情和变参,两个作用,一是设置阻塞非阻塞,二是dup2(cfd,cfd1)函数复制文件描述符那里用过
设置阻塞非阻塞:
先F_GETFL()
把flag的值作为参数返回出来,这个flag的值本质上是一个位图,他有一个位表示阻塞还是非阻塞,默认是零的话给他位或变为1.
设置非阻塞模式之后,没有读到字节就直接返回了,但是可能读到498字节,剩下的两个字节是还没有发过来,如果想要再读剩下的两个字节,那么需要忙轮询
在设置边沿触发之后,用accept得到cfd,设置cfd非阻塞,添加下面三句代码
1 | event.events = EPOLLIN | EPOLLET; //设置方式:原来是event.events = EPOLLIN 改成这个 |
这样可以设置那个套接字为非阻塞状态,只要readn读完就返回而不是阻塞在那里,没有发送数据的话后面的epoll_wait该等还是等着
epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 --- 忙轮询。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
int flg = fcntl(cfd, F_GETFL);
flg |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flg);
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
epoll优缺点:
优点:
高效。突破1024文件描述符。
缺点:
不能跨平台。 Linux。
epoll反应堆模型
epoll ET模式 + 非阻塞、轮询 + void *ptr。
不看了79-84集https://www.bilibili.com/video/BV1iJ411S7UA?p=85&vd_source=4e66034f3b307a25122b6a56d66b54c8
1 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
原来
socket、bind、listen -- epoll_create 创建监听 红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--
-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足
-- read() --- 小->大 -- write回去。
反应堆:不但要监听 cfd 的读事件、还要监听cfd的写事件。
反应堆的理解:加入IO转接之后,有了事件,server才去处理,这里反应堆也是这样,由于网络环境复杂,服务器处理数据之后,可能并不能直接写回去,比如遇到网络繁忙或者对方缓冲区已经满了这种情况,就不能直接写回给客户端。反应堆就是在处理数据之后,监听写事件,能写会客户端了,才去做写回操作。写回之后,再改为监听读事件。如此循环。
socket、bind、listen -- epoll_create 创建监听 红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--
-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足
-- read() --- 小->大 -- cfd从监听红黑树上摘下 -- EPOLLOUT -- 回调函数 -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听写事件
-- 等待 epoll_wait 返回 -- 说明 cfd 可写 -- write回去 -- cfd从监听红黑树上摘下 -- EPOLLIN
-- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听读事件 -- epoll_wait 监听
day4笔记
多路IO转接:
select:
poll:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:监听的文件描述符【数组】
struct pollfd {
int fd: 待监听的文件描述符
short events: 待监听的文件描述符对应的监听事件
取值:POLLIN、POLLOUT、POLLERR
short revnets: 传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR
}
nfds: 监听数组的,实际有效监听个数。
timeout: > 0: 超时时长。单位:毫秒。
-1: 阻塞等待
0: 不阻塞
返回值:返回满足对应监听事件的文件描述符 总个数。
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。
拓展 监听上限。 超出 1024限制。
缺点:
不能跨平台。 Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。
read 函数返回值:
> 0: 实际读到的字节数
=0: socket中,表示对端关闭。close()
-1: 如果 errno == EINTR 被异常终端。 需要重启。
如果 errno == EAGIN 或 EWOULDBLOCK 以非阻塞方式读数据,但是没有数据。 需要,再次读。
如果 errno == ECONNRESET 说明连接被 重置。 需要 close(),移除监听队列。
错误。
突破 1024 文件描述符限制:
cat /proc/sys/fs/file-max --> 当前计算机所能打开的最大文件个数。 受硬件影响。
ulimit -a ——> 当前用户下的进程,默认打开文件描述符个数。 缺省为 1024
修改:
打开 sudo vi /etc/security/limits.conf, 写入:
* soft nofile 65536 --> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】
* hard nofile 100000 --> 命令修改上限。
epoll:
int epoll_create(int size); 创建一棵监听红黑树
size:创建的红黑树的监听节点数量。(仅供内核参考。)
返回值:指向新创建的红黑树的根节点的 fd。
失败: -1 errno
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作监听红黑树
epfd:epoll_create 函数的返回值。 epfd
op:对该监听红黑数所做的操作。
EPOLL_CTL_ADD 添加fd到 监听红黑树
EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。
EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
fd:
待监听的fd
event: 本质 struct epoll_event 结构体 地址
成员 events:
EPOLLIN / EPOLLOUT / EPOLLERR
成员 data: 联合体(共用体):
int fd; 对应监听事件的 fd
void *ptr;
uint32_t u32;
uint64_t u64;
返回值:成功 0; 失败: -1 errno
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞监听。
epfd:epoll_create 函数的返回值。 epfd
events:传出参数,【数组】, 满足监听条件的 哪些 fd 结构体。
maxevents:数组 元素的总个数。 1024
struct epoll_event evnets[1024]
timeout:
-1: 阻塞
0: 不阻塞
>0: 超时时间 (毫秒)
返回值:
> 0: 满足监听的 总个数。 可以用作循环上限。
0: 没有fd满足监听事件
-1:失败。 errno
epoll实现多路IO转接思路:
lfd = socket(); 监听连接事件lfd
bind();
listen();
int epfd = epoll_create(1024); epfd, 监听红黑树的树根。
struct epoll_event tep, ep[1024]; tep, 用来设置单个fd属性, ep 是 epoll_wait() 传出的满足监听事件的数组。
tep.events = EPOLLIN; 初始化 lfd的监听属性。
tep.data.fd = lfd
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep); 将 lfd 添加到监听红黑树上。
while (1) {
ret = epoll_wait(epfd, ep,1024, -1); 实施监听
for (i = 0; i < ret; i++) {
if (ep[i].data.fd == lfd) { // lfd 满足读事件,有新的客户端发起连接请求
cfd = Accept();
tep.events = EPOLLIN; 初始化 cfd的监听属性。
tep.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);
} else { cfd 们 满足读事件, 有客户端写数据来。
n = read(ep[i].data.fd, buf, sizeof(buf));
if ( n == 0) {
close(ep[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL); // 将关闭的cfd,从监听树上摘下。
} else if (n > 0) {
小--大
write(ep[i].data.fd, buf, n);
}
}
}
}
epoll 事件模型:
ET模式:
边沿触发:
缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
LT模式:
水平触发 -- 默认采用模式。
缓冲区剩余未读尽的数据会导致 epoll_wait 返回。
结论:
epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 — 忙轮询。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
int flg = fcntl(cfd, F_GETFL);
flg |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flg);
优点:
高效。突破1024文件描述符。
缺点:
不能跨平台。 Linux。
epoll 反应堆模型:
epoll ET模式 + 非阻塞、轮询 + void *ptr。
原来: socket、bind、listen -- epoll_create 创建监听 红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--
-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足
-- read() --- 小->大 -- write回去。
反应堆:不但要监听 cfd 的读事件、还要监听cfd的写事件。
socket、bind、listen -- epoll_create 创建监听 红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--
-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足
-- read() --- 小->大 -- cfd从监听红黑树上摘下 -- EPOLLOUT -- 回调函数 -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听写事件
-- 等待 epoll_wait 返回 -- 说明 cfd 可写 -- write回去 -- cfd从监听红黑树上摘下 -- EPOLLIN
-- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听读事件 -- epoll_wait 监听
eventset函数:
设置回调函数。 lfd --》 acceptconn()
cfd --> recvdata();
cfd --> senddata();
eventadd函数:
将一个fd, 添加到 监听红黑树。 设置监听 read事件,还是监听写事件。
网络编程中: read --- recv()
write --- send();
线程池
\MyBlog\source_posts\网络编程\1708830507101.png)
如果服务器起一个线程去消化客户端产生的线程,这样就可以把服务器解放出来
但是多线程多进程比多路IO转接效率低
同时在一个进程中维护n给线程进程,在通信的时候cpu要在多个线程之间切换,系统资源消耗比较大
换成多路IO转接只需要一个进程线程就可以了,系统资源占用比较小,内核会自动帮你维护
也就是说每次创建线程销毁线程是开销大的一个原因,如果每次创建一堆线程(线程池),就可以减少开销
\MyBlog\source_posts\网络编程\1708831987508.png)
线程池的步骤
首先创建一些线程池,里面有一些初始化的最小线程数、指定最大线程数,所有的线程都通过条件变量(任务队列是否为空)阻塞在任务队列上面,等任务满足了就自动调用回调函数去处理,任务多了就给线程池扩容,任务少了就给线程池瘦身,管理扩容和瘦身是用管理线程做的,管理线程借助忙线程数、活着的线程数和任务队列中的实际任务数之间按照某个算法算一个比例出来,达到某一个比例的时候,我们就要扩容或者瘦身。销毁线程是在任务队列里面发一个假消息去给阻塞在任务队列为空但是线程不为空的线程,让它以为任务来了,然后把要删除的线程数置成非零值,这样线程就会被wait唤醒,然后进入if语句判断销毁线程数不是0就会进入if语句里面,然后pthread就自动退出了。
线程池相关函数
struct threadpool_t {
pthread_mutex_t lock; /* 用于锁住本结构体 */
pthread_mutex_t thread_counter; /* 记录忙状态线程个数de琐 -- busy_thr_num */
pthread_cond_t queue_not_full; /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
pthread_cond_t queue_not_empty; /* 任务队列里不为空时,通知等待任务的线程 */
pthread_t *threads; /* 存放线程池中每个线程的tid。数组 */
pthread_t adjust_tid; /* 存管理线程tid */
threadpool_task_t *task_queue; /* 任务队列(数组首地址) */
int min_thr_num; /* 线程池最小线程数 */
int max_thr_num; /* 线程池最大线程数 */
int live_thr_num; /* 当前存活线程个数 */
int busy_thr_num; /* 忙状态线程个数 */
int wait_exit_thr_num; /* 要销毁的线程个数 */
int queue_front; /* task_queue队头下标 */
int queue_rear; /* task_queue队尾下标 */
int queue_size; /* task_queue队中实际任务数 */
int queue_max_size; /* task_queue队列可容纳任务数上限 */
int shutdown; /* 标志位,线程池使用状态,true或false */
};
typedef struct {
void *(*function)(void *); /* 函数指针,回调函数 */
void *arg; /* 上面函数的参数 */
} threadpool_task_t; /* 各子线程任务结构体 */
rear = 5 % 5
线程池模块分析:
1. main();
创建线程池。
向线程池中添加任务。 借助回调处理任务。
销毁线程池。
2. pthreadpool_create();
创建线程池结构体 指针。
初始化线程池结构体 { N 个成员变量 }
创建 N 个任务线程。
创建 1 个管理者线程。
失败时,销毁开辟的所有空间。(释放)
3. threadpool_thread()
进入子线程回调函数。
接收参数 void *arg --》 pool 结构体
加锁 --》lock --》 整个结构体锁
判断条件变量 --》 wait -------------------170
4. adjust_thread()
循环 10 s 执行一次。
进入管理者线程回调函数
接收参数 void *arg --》 pool 结构体
加锁 --》lock --》 整个结构体锁
获取管理线程池要用的到 变量。 task_num, live_num, busy_num
根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。
5. threadpool_add ()
总功能:
模拟产生任务。 num[20]
设置回调函数, 处理任务。 sleep(1) 代表处理完成。
内部实现:
加锁
初始化 任务队列结构体成员。 回调函数 function, arg
利用环形队列机制,实现添加任务。 借助队尾指针挪移 % 实现。
唤醒阻塞在 条件变量上的线程。
解锁
6. 从 3. 中的wait之后继续执行,处理任务。
加锁
获取 任务处理回调函数,及参数
利用环形队列机制,实现处理任务。 借助队头指针挪移 % 实现。
唤醒阻塞在 条件变量 上的 server。
解锁
加锁
改忙线程数++
解锁
执行处理任务的线程
加锁
改忙线程数——
解锁
7. 创建 销毁线程
管理者线程根据 task_num, live_num, busy_num
根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。
如果满足 创建条件
pthread_create(); 回调 任务线程函数。 live_num++
如果满足 销毁条件
wait_exit_thr_num = 10;
signal 给 阻塞在条件变量上的线程 发送 假条件满足信号
跳转至 --170 wait阻塞线程会被 假信号 唤醒。判断: wait_exit_thr_num > 0 pthread_exit();
实现代码:
1 |
|
好再空过去看了也记不住
92-98
TCP通信和UDP通信各自的优缺点:
1 | TCP: 面向连接的,可靠数据包传输。对于不稳定的网络层,采取完全弥补的通信方式。 丢包重传。 |
一般中小型企业选择TCP,在数据完整性的基础上,只需要增大服务器的处理能力就能达到需求,但是TCP协议用到极致也不会提高多少,所以大型企业还是用UDP进行通讯
\MyBlog\source_posts\网络编程\1708852255296.png)
UDP: 无连接的,不可靠的数据报传递。对于不稳定的网络层,采取完全不弥补的通信方式。 默认还原网络状况
优点:
传输速度块。效率高。开销小。
缺点:
不稳定。
数据流量。速度。顺序。
UDP在发送时会根据网络状况动态的去选择路由节点进行巡逻,不同的包发送路径不同
使用场景:对时效性要求较高场合。稳定性其次。
游戏、视频会议、视频电话。 腾讯、华为、阿里 --- 应用层数据校验协议,弥补udp的不足。
UDP通信
\MyBlog\source_posts\网络编程\图片4.png)
UDP实现的 C/S 模型:
recv()/send() 只能用于 TCP 通信。 替代 read、write
accpet(); ---- Connect(); ---被舍弃
server:
lfd = socket(AF_INET, STREAM, 0); SOCK_DGRAM --- 报式协议。
bind();
listen(); --- 可有可无
while(1){
read(cfd, buf, sizeof) --- 被替换 --- recvfrom() --- 涵盖accept传出地址结构。
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:(struct sockaddr *)&addr 传出。 对端地址结构
addrlen:传入传出。
返回值: 成功接收数据字节数。 失败:-1 errn。 0: 对端关闭。
小-- 大
write();--- 被替换 --- sendto()---- connect
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
src_addr:(struct sockaddr *)&addr 传入。 目标地址结构
addrlen:地址结构长度。
返回值:成功写出数据字节数。 失败 -1, errno
}
close();
client:
connfd = socket(AF_INET, SOCK_DGRAM, 0);
sendto(‘服务器的地址结构’, 地址结构大小)
recvfrom()
写到屏幕
close();
服务器代码
1 |
|
客户端代码
1 |
|
本地套接字和网络套接字
IPC通信:
管道pipe:易用性最强
有名管道fifo:可以在没有血缘关系的进程之间进行通信
共享内存映射mmap:可以在没有血缘关系的进程之间进行通信并且可以反复读取
信号:开销最小的
本地套接字(domain):稳定性最好 ——是用CS模型实现本地套接字来实现进程间通讯
本地套接字对比网络编程 TCP C/S模型,就是服务器和客户端分别生成一个套接字文件,相互连接进行通讯。这个套接字文件是伪文件,它不占用磁盘大小,是通过调用bind函数创建出来的
网络通信:
- 多进程:也就是为每个客户端分配一个进程来处理请求(记得回收子进程)
- 多线程:当服务器与客户端 TCP 完成连接后,通过 pthread_create() 函数创建线程,然后将「已连接 Socket」的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。
为每个请求分配一个进程/线程的方式不合适,那有没有可能只使用一个进程来维护多个 Socket 呢?答案是有的,那就是 I/O 多路复用技术:
- select/poll:存在缺点–当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。
- epoll:很好的解决了 select/poll 的问题
- epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。
- epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。
Libevent库
本地套接字对比网络编程 TCP C/S模型
注意以下几点:
1. int socket(int domain, int type, int protocol);
参数 domain:AF_INET --> AF_UNIX/AF_LOCAL
type: SOCK_STREAM/SOCK_DGRAM 都可以。
2. bind() 地址结构: sockaddr_in --> sockaddr_un
struct sockaddr_in srv_addr; --> struct sockaddr_un srv_adrr;
srv_addr.sin_family = AF_INET; --> srv_addr.sun_family = AF_UNIX;
srv_addr.sin_port = htons(8888); strcpy(srv_addr.sun_path, "srv.socket")
由于不需要网络通信了,也不需要ip地址和端口号,这是在本地生成一个套接字,名称是 "srv.socket",位置在srv_addr.sun_path是默认的
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); len = offsetof(struct sockaddr_un, sun_path) + strlen("srv.socket");
bind(fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)); --> bind(fd, (struct sockaddr *)&srv_addr, len);
此时结构体的大小不再用sizeof计算,len = offsetof(struct sockaddr_un, sun_path) + strlen("srv.socket");
由于sock_addr数据结构:现在用的是第三个sockaddr_un,求大小的时候是加上地址便宜,后面的108字节路径名就是”srv.socket”。
\MyBlog\source_posts\网络编程\图片5.png)
3. bind()函数调用成功,会创建一个 socket。因此为保证bind成功,通常我们在 bind之前, 可以使用 unlink("srv.socket"); 这个函数是将个文件的硬连接计数-1,是为了能够顺利创建"srv.socket"文件,就先删一下"srv.socket"文件保证创建成功
4. 客户端不能依赖 “隐式绑定”。并且应该在通信建立过程中,创建且初始化2个地址结构:
1) client_addr --> bind()
2) server_addr --> connect();
(这两个地址结构分别根据之前定义的两个socket名绑定对应的文件)
实现代码
服务器代码:
1 |
|
客户端代码
1 |
|
对比本地套接字和网络套接字
\MyBlog\source_posts\网络编程\1709000247505.png)
\MyBlog\source_posts\网络编程\1709000229274.png)
libevent库
开源。精简。跨平台(Windows、Linux、maxos、unix)。专注于网络通信。
源码包安装:
参考 README、readme
./configure 检查安装环境 生成 makefile
make 生成 .o 和 可执行文件
sudo make install 将必要的资源cp至系统指定目录。
进入 sample 目录,运行demo验证库安装使用情况。
编译使用库的 .c 时,需要加 -levent 选项。不然会出现未定义的引用,因此需要建立软连接 将缺的那个库添加到默认库的文件夹下。建立动态库应该是libevent.so,静态库应该是libevent.a,在连接的时候需要指定库名,去掉lib前缀,去掉.so后缀 event
库名 libevent.so --> /usr/local/lib 查看的到。
特性:
基于“事件”异步通信模型。— 回调。
事件就是这个库名event,它在里边把所有的东西全部封装成事件(struct event event_new();event_init()
),所见皆事件。之前说的linux系统是所有东西都是文件,所见皆文件
同步/异步通信模型
同步通信模型:有时效性的,并行的去访问,为了防止数据错乱需要安排先后顺序
异步通信模型:异步通信主要依赖回调机制。
函数的编写时机和调用时机不是同一时间的,当封装好了一个函数放在这里,这个函数不是 我封装事件或者代码满足条件的时候(阻塞或者if语句?)去执行,只有在某个条件到达的时 候才会执行这个代码。这个代码是内核调用的。
类似于信号捕捉,只要把函数注册上,只要条件满足了这个回调函数就自动被调用了,函数注册的时间和函数调用的时间不是一个时间
函数指针
未决和非未决:
非未决: 没有资格被处理(前期准备条件都已经准备完了,就等着事件被触发
未决: 有资格被处理,但尚未被处理(还没有准备好,即使事件被触发了也没有被触发的机会
event_new --> event ---> 非未决 --> event_add --> 未决 --> dispatch() && 监听事件被触发 --> 激活态
--> 执行回调函数 --> 处理态 --> 非未决 event_add && EV_PERSIST --> 未决 --> event_del --> 非未决
\MyBlog\source_posts\网络编程\1709035249826.png)
带缓冲区的事件 bufferevent
#include <event2/bufferevent.h>
read/write 两个缓冲. 借助 队列.
原理:bufferevent有两个缓冲区∶也是队列实现,读走没,先进先出。
读︰有数据-->读回调函数被调用-->使用bufferevent_read() -->读数据。
写∶使用bufferevent_write() -->向写缓冲中写数据-->该缓冲区有数据自动写出-->写完,回调函数被调用(鸡肋)。
buf是一个结构体
\MyBlog\source_posts\网络编程\1709036653944.png)
bufferevent相关函数
空
启动和关闭缓冲区
因为套接字里面要求缓冲区可以双向全双工和半关闭
而bufferevent打造出来的缓冲区相当于fd(套接字)也封装到它的内部,它的read和write也支持半关闭
web大练习
借助浏览器和自己电脑上的服务器完成一些通信
自己的服务器上的一个文件夹里有一些文件,可以从浏览器上通过域名解析得到ip(公网ip)地址和端口号进行访问,但是公网ip要申请,所以还是用局域网,127.0.0.1这个
准备工作:html语言
前端,搭建网页
http超文本传输协议
请求协议
\MyBlog\source_posts\网络编程\图片6.png)
应答协议
\MyBlog\source_posts\网络编程\图片7.png)
实现思路:
与高并发服务器类似
只是在得到cfd之后的read阶段,read的内容是http请求头,而且对象也是一个文件描述符,如果读一行对象是文件的话可以用fget()函数,但是这是文件描述符,我们根据read()函数自己写了封装了一个readline()函数,但是他默认的是在linux系统下读取文件描述符对应的文件,这个文件以\n结尾,但是http是\r\n