网络基础
分层模型
协议: 一组规则,两端都遵循这个协议进行数据传输。
OSI: 吾叔往床会变硬
TCP/IP : 网网传应
网络传输流程:数据->+应用层协议->+传输层协议->+网络层协议->+链路层协议
以太网帧协议—链路层
ARP协议–链路层
根据ip地址获取mac地址。
在通讯前必须获得目的主机的硬件地址(MAC地址)。ARP协议就起到这个作用。源主机发出ARP请求,询问“IP地址是123.46.76.22的主机的MAC地址是多少”
桢类型是0806。 然后把ARP发送到网络中,连接的路由器就会查看自己的IP跟123.46.76.22 是不是一样的,是一样的就ARP应答,回复自己的MAC地址
IP协议—网络层
4位版本:区别ipv4、ipv6
TTL:time to live,设置数据包在路由节点上的跳转上限,每经过一个路由节点TTL -1,到0的时候,该数据包被丢弃。
源ip,目的ip 都是32位即4字节。
UDP协议—传输层
UDP中
16位:源端口号
16位:目的端口号
IP地址:网络环境中,唯一标识一台主机
端口号:主机中,唯一标识一个进程
IP+端口:在网络环境中,唯一标识一个进程
TCP协议—传输层
16位:源端口号
16位:目的端口号
32位:序号
32位:确认序号
6位:标志号
16位:窗口大小
网络应用程序设计模式
C/S模式—客户端/服务器模型—得先下载客户端才能用
优点:提前缓存大量数据、协议选择灵活(可以自定义)、速度快
缺点:安全性差(自定义的协议可能窃取信息)、开发工作量大
B/S模式—浏览器/服务器模型—无时无地都可以用
优点:安全性好、跨平台、开发工作量小
缺点:无法缓存大量数据、严格遵守http
socket编程
Socket是应用层与TCP/IP协议簇通信的中间软件抽象层,它是一组接口。
1、套接字
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。网络通信中,套接字socket一定是成对出现的。
在系统编程中,管道是两个文件描述符,1个缓冲区。
而套接字socket:1个文件描述符指向一个套接字,该套接字内部由内核借助两个缓冲区实现。
2、预备知识
2.1网络字节序
小端法:(PC本地存储)高位存高地址,低位存低地址
大端法:(网络存储)高位存低地址,低位存高地址
网络的数据流采用大端字节序,而本地的数据流采用小端字节序,因此要通过函数来转换:
h是host本地,to,n是network网络,l是long 32位长整数,s是short,16位短整数
htonl --> 本地 -->网络 (IP)
htons --> 本地 --> 网络 (port端口)
ntohl --> 网络 --> 本地 (IP)
ntohs --> 网络 --> 本地 (port)
2.2 IP地址转换函数
因为Ip地址本地存的是string类型点分十进制,用的时候还得atoi到二进制传输再到网络中传输,很麻烦,所以封装了ip地址转换函数int inet_pton(int af, const char *src, void *dst);
本地字节序(string IP) —> 网络字节序
af:AF_INET、AF_INET6 协议类型ipv4还是ipv6
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址。
返回值:
成功: 1
异常: 0, 说明src指向的不是一个有效的ip地址。
失败:-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
网络字节序 —> 本地字节序(string IP)
af:AF_INET、AF_INET6
src: 网络字节序IP地址(传入)
dst:本地字节序(string IP)(传出)
size: dst 的大小。
返回值: 成功:dst。
失败:NULL
2.3 sockaddr地址结构
地址结构即:IP + port --> 在网络环境中唯一标识一个进程。
struct sockaddr_in addr;
初始化:
addr.sin_family = AF_INET/AF_INET6
man 7 ip
端口 addr.sin_port = htons(9527);
IP
三步初始化.sin_addr.s_addr
int dst;
inet_pton(AF_INET, “192.157.22.45”, (void )&dst);
addr.sin_addr.s_addr = dst;
或者直接这样:【】addr.sin_addr.s_addr = htonl(INADDR_ANY);
自动获取,取出系统中有效的任意IP地址。二进制类型。 客户端不能这样用 bind(fd, (struct sockaddr *)&addr, size);
将强转成
3、网络套接字函数
3.1 套接字连接流程
服务器端调用socket函数产生一个套接字,这个套接字作为参数传给accept函数,阻塞监听客户端连接,当有客户端连接,该函数返回一个新的套接字去和客户端连接,原来socket函数产生的那个套接字继续监听连接(接客的)。因此服务端一个socket建立会有两个套接字(socket函数产生的 起监听作用,accept函数返回的用来跟客户端连接)。服务端通过read accept返回的套接字读到客户端数据。
客户端的socket
只产生一个套接字。然后connetct
绑定服务器addr(IP和端口)。客户端可以不bind自身的addr,因为默认会采用“隐式绑定”
listen()
设置同时连接上限
3.2 socket函数,创建套接字
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
int domain: AF_INET(ipv4)、AF_INET6(ipv6)、AF_UNIX(本地套接字)
**int type :数据传输协议 **SOCK_STREAM(面向流式传输,TCP)、SOCK_DGRAM(面向报式UDP)
**int protocol:**默认传0,传0的时候,type的流式连接协议默认为TCP,报式默认为UDP
返回值:
返回新套接字对应的文件描述符
失败:-1 error 又可以用perror了
3.3 bind 给套接字绑定地址结构(ip+port)
#include <sys/types.h> /* See NOTES */
#include<arpa/inet.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:socket返回的套接字文件描述符
addr:地址结构体
addrlen: addr地址结构的大小 sizeof(addr);
3.4 listen 设定设置同时与服务器建立连接的上限数
(同时进行3次握手的客户端数量)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数:
1: 套接字文件描述符
2:int类型,指定同时连接最大数目,最大值为128
3.5 accept 监听客户端连接
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockdf: 接客的socket文件描述符
addr: 传出参数,返回成功与服务器连接的客户端的地址结构即IP地址和端口号
*addrlen: 传入传出参数(所以要传指针),传入sizeof(addr)大小,提前告知容器的大小,传出客户端addr的实际大小
返回值:
成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
3.6 connect 连接服务器端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
1:客户端socket文件描述符
2:服务器端的addr(因为要跟服务器连接嘛)
3:sizeof(addr)
返回值:
成功返回0,失败-1 errno
客户端bind可有可无,没有的bind自身addr的时候,系统会默认隐式绑定,动态分配端口。
实验
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#define SERV_PORT 9527
#include <arpa/inet.h>
int main(){
int sfd, cfd;
int ret;
// 获取套接字
sfd = socket(AF_INET, SOCK_STREAM,0);
if(sfd == -1){
perror("socket error\n");
exit(1);
}
// 初始化addr地址结构体
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定IP端口到服务器socket
bind(sfd, (struct sockaddr*)&addr,sizeof(addr) );
//设置最大同时连接
listen(sfd, 128);
//阻塞等待客户端连接
struct sockaddr_in caddr;
socklen_t caddr_len = sizeof(caddr);
cfd = accept(sfd, (struct sockaddr*)&caddr, &caddr_len);
if(cfd== -1){
perror("accept error\n");
exit(1);
}
// 读写数据
char buf[BUFSIZ];
while(1){
ret = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO,buf,ret);
for(int i = 0; i < ret; i++){
buf[i]= toupper(buf[i]);
}
write(cfd, buf, ret);
}
close(sfd);
close(cfd);
return 0;
}
TCP协议
为何TCP可靠,面向连接? 因为它会有回执,确认。
三次握手
- 三次握手中,ack需要+1,表示服务器期望接受的下一个字节的序列号。
- 而在握手成功后的数据传输过程中,确认号不需要加1,只是简单地告诉对方已经成功接收到数据,确认号的值就是对方发送的数据的序列号加上数据的长度。
- 三次握手是在内核完成的。体现在accept跟connect阶段,如果握手不成功,则accept跟connect返回-1
为什么要三次握手,两次不行吗
假设client有一个连接请求在某个网络节点滞留了很久,直到连接释放了才到达server。此时server会以为有一个新的连接,就会向client发送SYN+ack,如果只有两次,那这个连接就建立了,白白浪费了资源。
四次挥手
TCP是全双工的,每个方向都必须单独进行关闭。即双方都需要发送一个FIN来终止这个方向的连接。
为什么握手是3次,挥手要四次?
TCP是全双工模式,所以关闭的时候是半关闭。
这就意味着,关闭连接时,当Client端发出FIN报文段时,只是表示Client端告诉Server端数据已经发送完毕了。当Server端收到FIN报文并返回ACK报文段,表示它已经知道Client端没有数据发送了(此时**客户端半关闭完成(socket1个文件描述符,两个缓冲区rw,客户端半关闭,关的不是套接字,而是w缓冲区,这也是为什么后面服务器端发送FIN时,客户端能收到,并且回ACK),**客户端此时只能收数据,不能发数据了)
但是Server端还是可以发送数据到Client端的,所以Server很可能并不会立即关闭SOCKET,直到Server端把数据也发送完毕。当Server端也发送了FIN报文段时,这个时候就表示Server端也没有数据要发送了,就会告诉Client端,我也没有数据要发送了,然后客户端回一个ACK,服务器收到ACK此次TCP才关闭连接,如果服务器没收到ACK,会反复发FIN给客户端。
半关闭不是关连接,关的是缓冲区。 4次挥手完成,即服务端收到最终的ACK,连接才会关闭。
shutdown函数跟close函数区别
close关文件描述符的时候(假如有多个文件描述符指向同一个套接字),只关一个。
shutdown全关了。
滑动窗口(TCP流量控制机制)
如果发送端发送太快,接收端处理太慢,而接收端的缓冲区大小固定的,就会覆盖,造成数据丢失。因此,滑动窗口实时告诉对方剩余窗口有多大,防止发送方一直发。TCP通过滑动窗口解决这样问题。
比如下图,服务端窗口大小是6144,发送方连着发了5121,如果再发一个1024就6145了,比窗口还大了,所以此时发送方暂停了发送。因为TCP协议的窗口大小是16字节,所以TCP通信时最大窗口大小不能超过2的16次方65536.
socket函数与TCP连接对应关系
三次握手阶段:
- 客户端准备套接字,服务端进入了accept阻塞状态。
- 客户端调用connect对应三次握手客户端第一次发送SYN
- 三次握手完成,对应服务端的accept的传出参数返回客户端的地址结构体addr
四次挥手阶段:
- 客户端close(fd)对应 四次挥手的第一次FIN,然后服务端read()返回0。服务端再close(fd),对应服务端发送FIN
TCP状态转换图解析
实线:主动连接方(一般是客户端)
主动端一般是发送XX后状态变化,除了四次挥手的第二次挥手,接受被动端的ACK变为FIN_WAIT2时是收到
主动连接(对应三次握手): 初始CLOSED状态 – 发送SYN – 变成SYN_SENT状态 – 接收 ACK、SYN – 维持SYN_SENT状态 – 发送 ACK – 变成ESTABLISHED(数据通信态)
主动关闭(对应四次挥手): 初始ESTABLISHED(数据通信态) – 发送 FIN – 变为FIN_WAIT_1 – 接收ACK – 变为FIN_WAIT_2(半关闭)-- 接收对端发送的 FIN – 维持FIN_WAIT_2(半关闭)-- 回发ACK – 变为TIME_WAIT(只有主动关闭连接方,会经历该状态,上图中就只有客户端会经历)-- 等 2MSL时长 – 变成CLOSED状态 netstat -apn| grep 端口号
可以查看状态
虚线:被动连接方(一般是服务器端)
被动接收连接请求端(三次握手): CLOSE – (服务端一运行立马变为)LISTEN – 接收 SYN – 维持(LISTEN) – 发送 ACK、SYN – 变为(SYN_RCVD) – 接收ACK – ESTABLISHED(数据通信态)
被动关闭连接请求端(四次挥手): ESTABLISHED(数据通信态) – 接收 FIN – 维持ESTABLISHED(数据通信态) – 发送ACK – 变为(CLOSE_WAIT对应主动方的FIN_WAIT2 ,说明对端【主动关闭连接端】处于半关闭状态) – 发送FIN --变为(LAST_ACK) – 接收ACK – CLOSED状态
2MSL的必要性
一定出现主动关闭连接的一方:保障最后一个ACK能被接受(如果被动方没接收到最后一个ACK会一直重复发第三次挥手的FIN,等2MSL就是看被动方会不会重发第三次挥手的FIN)
TIME_WAIT 状态拓展
只有主动关闭方才会经历,linux下大概需要等待40s 。 如果 让服务器端作为主动关闭方,那么会出现一个现象:服务器马上重启时会出现端口占用,因为上一次服务器主动关闭等待的40s没有结束,端口还没有返回回来。
端口复用
** setsockopt,**在bind之前写入端口复用,这样就可以复用端口了
高并发服务器
read返回值总结
read = 0 一定是连接关闭了,而不是没有内容读,因为read读的时候是阻塞的读。
read > 0 实际读到的字节数目
read = -
多进程并发服务器
接客套接字生成一个套接字,在子进程中与客户端通信。
首先明确一个概念:系统编程阶段学了父子进程共享文件描述符表,文件描述符表是进程与文件之间的桥梁,通过文件描述符,进程可以对文件进行读取、写入和其他操作。如果子进程关闭了套接字的文件描述符,并不意味着套接字没了,只是子进程失去了访问套接字的桥梁而已,父进程的文件描述符还在,父进程依然可以访问该套接字。 所以要明确,close只是删除访问的桥梁。
其次,进程的回收如果用wait阻塞着收回,那新的客户端要连接了怎么办?因此通过信号来回收子进程。memset(地址,int , size)
或者bzero(&serve_addr,sizeof(serve_addr));
//将地址结构初始化,清零
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <errno.h>
#include <ctype.h>
#include <signal.h>
#include <sys/wait.h>
#define SEV_PORT 9527
void catch_child(int signum){
while(waitpid(0,NULL,WNOHANG) > 0);
return;
}
int main(){
int ret;
int sfd, cfd;
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(SEV_PORT);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
memset(&saddr,0 , sizeof(saddr)); // 或者bzero(&saddr,sizeof(serve_addr));
// 1. 服务端socket
sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){
perror("socket error\n");
exit(1);
}
// 2. 服务端bind 地址结构体
ret = bind(sfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1){
perror("bind error\n");
exit(1);
}
// 3.服务端listen设置最大同时连接
listen(sfd,128);
socklen_t caddr_len;
caddr_len = sizeof(caddr);
// 4.服务端accept循环阻塞等待客户端连接
while(1){
cfd = accept(sfd, (struct sockaddr*)&caddr,&caddr_len);
if(cfd == -1){
perror("accept error\n");
exit(1);
}
pid_t pid = fork();
// 子进程工作
if(pid == 0){
char buf[BUFSIZ];
close(sfd); // 子进程关掉接客套接字
while(1){
ret = read(cfd, buf, sizeof(buf));
if(ret == 0){
close(cfd);
exit(0);
}
for(int i =0; i < ret ; i++ ){
buf[i] = toupper(buf[i]);
}
write(cfd, buf, ret);
}
}
// 父进程通过信号回收子进程
else{
close(cfd);
//注册捕捉函数
struct sigaction act;
act.sa_handler = catch_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD,&act, NULL);
}
}
return 0;
}
多线程并发服务器
多进程中通过子进程实现服务器端功能,多线程就是在子线程的回调函数中实现服务器功能而已。
其次,线程的回收这里不能使用join,跟多进程的情况一样,如果使用了join会阻塞等待回收,如果此时有新客户端访问就访问不到了。多进程的回收使用信号回收防止阻塞,线程尽量不能跟信号扯上关系,因此采用deteach将子线程分离,自动回收PCB。
deteach相比join有个坏处就是不能查看回调函数的返回值(join通过参数2void**来回收),所以还有个方法:再新建一个子线程,用该子线程使用Join回收(线程可以兄弟之间相互回收),主线程不需要阻塞
多路I/O转接服务器
select
内核相当于秘书,监听有没有客户端连接、有没有数据传过来。
有客户端想连接了,内核汇报给服务器端,服务器端再accept。这样就避免了一直在accept阻塞。
函数解析#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:
- 参数1:nfds,永远是监听的所有文件描述符中, 最大文件描述符+1(内部是一个for循环)
- 参数2: fd_set *readfds,传入传出参数,是一个位图。
传入表示:要监听哪些文件描述符的读事件,set的位图上置1传入。
传出表示有哪些被监听的文件描述符活跃了。
客户端连接请求是通过读取套接字上的数据来完成的,所以接客fd也被放在这个读集合。客户端往服务器写的数据,服务器也是读。
比如这里传入 3.5.6号文件描述符,表示要监听3.5.6,传入位图3.5.6为1。传出只有5。6为1,因为监听的3.5.6中只有5.6活跃了。
- 参数5struct timeval *timeout,设置监听时常,NULL为一直监听,0监听一次后立马返回,常数位监听常数秒。
返回值:
>0:3个监听集合,监听到活跃的fd总数。
=0: 三个集合加起来,没有活跃的fd
-1: errno
对fd_set文件描述符监听集合的操作函数
**void FD_ZERO(fd_set *set); --将监听集合置0**
** fd_set rset; FD_ZERO(&rset); //将rset集合清空**
**void FD_SET(int fd, fd_set *set); --将fd添加到监听集合中(set想监听谁添加谁)**
** FD_SET(3,&rset);FD_SET(5,&rset);FD_SET(6,&rset); //将文件描述符3和5加到rset集合中**
**void FD_CLR(int fd, fd_set *set); --将一个fd从监听集合中移除**
(某个客户端关闭了,不需要监听了,就移除它的套接字fd)**int FD_ISSET(int fd, fd_set *set); --判断一个fd是否在监听集合中**
** 在返回1,不在返回0**
思路分析
int maxfd = 0;
lfd = socket() ; //创建套接字
maxfd = lfd; // 这个maxfd用来后面的select函数,以及for
bind(); //绑定地址结构
listen(); //设置监听上限
fd_set rset, allset; //创建r监听集合,allset记录每次要监听的集合
//因为rset会因为传出而变成监听到的活跃fd集合,所以用allset来复位rset,用于下次循环的监听列表
FD_ZERO(&allset); //将r监听集合清空
FD_SET(lfd, &allset); //将 lfd 添加至读集合中, lfd接客套接字一定是第一个要监听的
while(1) {
rset = allset; //select会改变rset,所以保存监听集合到allset
ret = select(maxfd+1, &rset, NULL, NULL, NULL); //监听
if(ret > 0) { //有监听的描述符活跃
if (FD_ISSET(lfd, &rset)) { // 1 在,0不在。lfd在rset,说明有客户端要连接
cfd = accept(); //建立连接,返回用于通信的文件描述符
maxfd = cfd; // 有新的fd产生,最大文件描述符就是新的fd了
//这个maxfd在后面的for遍历文件描述符时时有用
// 避免for1024次(文件描述符最多定义1024个)
FD_SET(cfd, &allset); //把刚连上的客户端套接字fd添加到监听通信描述符集合
}
for (i = lfd+1; i <= maxfd; i++){ // lfd后面的fd都是通讯套接字fd
FD_ISSET(i, &rset) //在传出活跃fd的rset中比对谁是活跃的fd
read() // 读活跃的fd
小 -- 大
write();
}
}
}
代码实现
int main()
{
int lfd, cfd, ret, len, j, i;
char buf[BUFSIZ];
//地址结构体
struct sockaddr_in serve_addr, client_addr;
socklen_t client_addr_len;
bzero(&serve_addr,sizeof(serve_addr)); //结构体清空
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = htons(SERVE_PORT);
serve_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//创建socket
lfd = socket(AF_INET,SOCK_STREAM,0);
int maxfd = lfd; //最大的文件描述符,用作读的上限以及select的参数
if(lfd==-1)
{
perror("socket error");
exit(1);
}
//绑定ip和端口
bind(lfd,(struct sockaddr *)&serve_addr,sizeof(serve_addr));
//设置上限
listen(lfd,128);
fd_set rset, allset; //设置监听的集合
FD_ZERO(&allset); //清空集合
FD_SET(lfd,&allset); //将lfd加入到监听集合
while(1)
{
rset = allset;
ret = select(maxfd+1,&rset,NULL,NULL,NULL);
if(ret<0)
{
perror("select error");
exit(1);
}
if(FD_ISSET(lfd,&rset)) //如果lfd在传出的rset中,表示有客户端要进行连接
{
client_addr_len = sizeof(client_addr);
cfd = accept(lfd,(struct sockaddr *)&client_addr,&client_addr_len);
FD_SET(cfd,&allset); //将cfd加入到监听集合
if(maxfd<cfd)
maxfd = cfd; //更新最大的文件描述符
//说明select只返回了lfd这一个,后续指令无需执行,continue跳出本次,继续while
if(ret==1)// 这个判断是在lfd活跃的if里面,如果lfd活跃并且ret=1,
// 说明除了lfd没有其他套接字fd活跃,就不需要后面的for了
continue;
}
for(i=lfd+1; i<=maxfd; i++) // 遍历lfd后面的套接字fd,看是否活跃
{
if(FD_ISSET(i,&rset)){ //找到满足读事件的那个描述符
len = read(i,buf,sizeof(buf));
if(len==0) //检测到客户端关闭了连接
{
close(i); //关闭该描述符
FD_CLR(i,&allset); //把断开的fd从监听中移除
}
//如果len不为0就表示有数据,循环更改数据
for(j=0; j<len; j++)
buf[j] = toupper(buf[j]);
//写回更改后的数据
write(i,buf,len);
write(STDOUT_FILENO,buf,len);
}
}
}
close(lfd);
return 0;
}
select优缺点
缺点
如果我想监听 3、1023,那么在后续的for查看哪些fd活跃的时候,for的判断条件就是1023了。然而中间的部分我并不想监听,这样效率就很低了。(当然,可以自定义查询数组,就是麻烦点)
- 文件描述符最多1024个,排除0,1,2,只剩下1024-3=1021个。所以监听上限受限
优点
跨平台,多个平台都可以用。win。linux。MacOS。unix。类unix。mips
自定义监听数组实现select
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "wrap.h"
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int i, j, n, maxi;
int nready, client[FD_SETSIZE]; /* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */
int maxfd, listenfd, connfd, sockfd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 端口复用
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port= htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
maxfd = listenfd; /* 起初 listenfd 即为最大文件描述符 */
maxi = -1; /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* 用-1初始化client[] */
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
while (1) {
rset = allset; /* 每次循环时都从新设置select监控信号集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL); //2 1--lfd 1--connfd
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) { /* 找client[]中没有使用的位置 */
client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
break;
}
if (i == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
if (connfd > maxfd)
maxfd = connfd; /* select第一个参数需要 */
if (i > maxi)
maxi = i; /* 保证maxi存的总是client[]最后一个元素下标 */
if (--nready == 0) // 如果nready =1说明只有lfd活跃,不用读,直接进入下次的while循环
continue;
}
for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */
if ((sockfd = client[i]) < 0)
continue; // 说明client[i]要么被关闭要么没活跃,进入下层for
if (FD_ISSET(sockfd, &rset)) {
if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */
Close(sockfd);
FD_CLR(sockfd, &allset); /* 解除select对此文件描述符的监控 */
client[i] = -1; //关闭的cfd置为-1,下次就不读它了
} else if (n > 0) {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
Write(STDOUT_FILENO, buf, n);
}
if (--nready == 0) // 上面的nready=1是只有lfd,直接Continue到下层while了,这里的nready =1 说明不是因为lfd活跃,而是cfd活跃了,并且只有一个cfd活跃,那么既然读完了就没必要再往下试探有没有其他cfd活跃了
break; /* 跳出for, 但还在while中 */
}
}
}
Close(listenfd);
return 0;
}
初始化client[]数组全为-1
1、lfd收到新客户端连接后将cfd加入监听数组
用client[]数组放要新客户端fd,并且用一个变量maxi记录数组的最大下标
2、查询有哪些fd活跃了
遍历client数组,取出来,如果client[i] < 0 说明不是监听数组的。如果 >0就read,如果read到0说明要关闭连接,那就把对应client[i]置为-1,下次遍历client就不会遍历它了。
poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
- pollfd结构体:(传进去的fds是pollfd数组)
struct pollfd {
int fd; fd 初始时,fds数组的所有结构体成员的.fd成员都是-1
short events; 监控类型 r/w/err, r是POLLIN,w是POLLOUT, err是POLLERR
short revents; 比如r活跃时,fd.revents & POLLIN 会为真
};
数组的第一个结构体成员,pfds[0].fd
一定是接客fd
监听事件pfds[0].event=POLLIN
- nfds_t nfds:监控数组中实际需要监控的fd个数
- timeout: 毫秒级等待,-1阻塞等,0立即返回,>0指定等待毫秒数
优缺点
优点
自带数组结构;
相比select,它把监听集合–跟–监听到的活跃集合 分离开; 可以拓展监听数目大小,超过1024上限(跟epoll一样的突破方式)
缺点
不能跨平台,在linux下用;
跟select一样无法直接定位监听事件,得轮询。
代码实现
1./* server.c */
2.#include <stdio.h>
3.#include <stdlib.h>
4.#include <string.h>
5.#include <netinet/in.h>
6.#include <arpa/inet.h>
7.#include <poll.h>
8.#include <errno.h>
9.#include "wrap.h"
10.
11.#define MAXLINE 80
12.#define SERV_PORT 6666
13.#define OPEN_MAX 1024
14.
15.int main(int argc, char *argv[])
16.{
17. int i, j, maxi, listenfd, connfd, sockfd;
18. int nready;
19. ssize_t n;
20. char buf[MAXLINE], str[INET_ADDRSTRLEN];
21. socklen_t clilen;
22. struct pollfd client[OPEN_MAX];
23. struct sockaddr_in cliaddr, servaddr;
24.
25. listenfd = Socket(AF_INET, SOCK_STREAM, 0);
26.
27. bzero(&servaddr, sizeof(servaddr));
28. servaddr.sin_family = AF_INET;
29. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
30. servaddr.sin_port = htons(SERV_PORT);
31.
32. Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
33.
34. Listen(listenfd, 20);
35.
36. client[0].fd = listenfd;
37. client[0].events = POLLRDNORM; /* listenfd监听普通读事件 */
38.
39. for (i = 1; i < OPEN_MAX; i++)
40. client[i].fd = -1; /* 用-1初始化client[]里剩下元素 */
41. maxi = 0; /* client[]数组有效元素中最大元素下标 */
42.
43. for ( ; ; ) {
44. nready = poll(client, maxi+1, -1); /* 阻塞 */
45. if (client[0].revents & POLLRDNORM) { /* 有客户端链接请求 */
46. clilen = sizeof(cliaddr);
47. connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
48. printf("received from %s at PORT %d\n",
49. inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
50. ntohs(cliaddr.sin_port));
51. for (i = 1; i < OPEN_MAX; i++) {
52. if (client[i].fd < 0) {
53. client[i].fd = connfd; /* 找到client[]中空闲的位置,存放accept返回的connfd */
54. break;
55. }
56. }
57.
58. if (i == OPEN_MAX)
59. perr_exit("too many clients");
60.
61. client[i].events = POLLRDNORM; /* 设置刚刚返回的connfd,监控读事件 */
62. if (i > maxi)
63. maxi = i; /* 更新client[]中最大元素下标 */
64. if (--nready <= 0)
65. continue; /* 没有更多就绪事件时,继续回到poll阻塞 */
66. }
67. for (i = 1; i <= maxi; i++) { /* 检测client[] */
68. if ((sockfd = client[i].fd) < 0)
69. continue;
70. if (client[i].revents & (POLLRDNORM | POLLERR)) {
71. if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
72. if (errno == ECONNRESET) { /* 当收到 RST标志时 */
73. /* connection reset by client */
74. printf("client[%d] aborted connection\n", i);
75. Close(sockfd);
76. client[i].fd = -1;
77. } else {
78. perr_exit("read error");
79. }
80. } else if (n == 0) {
81. /* connection closed by client */
82. printf("client[%d] closed connection\n", i);
83. Close(sockfd);
84. client[i].fd = -1;
85. } else {
86. for (j = 0; j < n; j++)
87. buf[j] = toupper(buf[j]);
88. Writen(sockfd, buf, n);
89. }
90. if (--nready <= 0)
91. break; /* no more readable descriptors */
92. }
93. }
94. }
95. return 0;
96.}
修改文件描述符上限
cat /proc/sys/fs/file-max
查看当前计算机可打开最大文件个数(受硬件影响)ulimit -a
当前用户下的进程默认打开文件描述符个数,缺省为1024(可修改)
修改方式:(通过修改配置文件)sudo vi /etc/security/limits.conf
在文件尾部写入以下配置,soft软限制,hard硬限制。如所示。* soft nofile 1024
设置默认值(可直接用命令改,但是不能突破hard设置的上限,改完后要注销用户)
ulimit -n xxx。此时如果ulimit -n 21000 就不行,因为比hard大了。(所以可以不用命令,直接去配置文件里面改)
ulimit -n 19000 就可以* hard nofile 20000
epoll
#include <sys/epoll.h>
其本质是红黑树和链表
三个函数
epoll_create创造树根
int epoll_create(int size);
//创建一棵监听红黑树
参数
size:创建的红黑树的监听节点数量(仅供内核参考)
返回值:成功–指向新创建的红黑树的根节点的fd;失败 -1 error
epoll_ctl操作监听树
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//操作监听红黑树(往树上挂event或者摘下来已经在上面的fd对应的event,或修改fd对应event的监听方式(event.events)),挂上去了还没有开始监听,监听是后面的wait。
- 当操作是DEL摘掉的时候,参数4event结构体直接写NULL就行了,写也没用
- 第一个操作的肯定是接客的lfd
参数
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(就是参数3的fd)
void *ptr:暂时不写
uint32_t u32
uint32_t u64
返回值:成功0,失败-1 error
epoll_wait监听
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数
epfd:epoll_create函数的返回值 epfd
events:传出参数,一个数组[],用于存放满足活跃的那些fd的event结构体。现在就不用像select/poll那样轮询查看哪些fd活跃了。这个数组里面存的都是活跃的event。event结构体里面可以看fd和活跃方式。
maxevents:数组元素的总个数(不用像poll那里传的实际要监听的个数nfds),struct epoll_event events[1024] ,数组大小1024,那就直接传1024.
timeout:
-1:阻塞
0:不阻塞
>0:超时时间(毫秒)
返回值:
>0:满足监听的总个数(活跃的fd数),可以用作循环上限
0:没有fd满足监听的事件
-1:失败,error
思路分析
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);
}
}
}
}
代码实现(ET版)
#include <stdio.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/epoll.h>
#include <fcntl.h>
#define OPEN_MAX 5000
#define SERVE_PORT 9527
int main()
{
// 所需要的变量
int lfd, cfd, efd, ret, wait_ret, i, sockfd, len;
char buf[1024];
// 地址结构体
struct sockaddr_in serve_addr, client_addr;
socklen_t client_addr_len;
serve_addr.sin_family = AF_INET; // IPV4
serve_addr.sin_port = htons(SERVE_PORT); // 绑定端口
serve_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定ip(ANY系统自动分配)
// 创建socket
lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0)
{
perror("socket error");
exit(1);
}
// bind绑定
bind(lfd, (struct sockaddr *)&serve_addr, sizeof(serve_addr));
// 设置上限
listen(lfd, 128);
// 创建红黑树
efd = epoll_create(1); // efd就是树的根节点
// 将lfd挂在树上
// epoll结构体 ep是epoll_wait所需的数组(存放满足事件的fd)
struct epoll_event tep, ep[128]; // tep是epoll_ctl的参数(传监听的事件)
tep.events = EPOLLIN;
tep.data.fd = lfd;
ret = epoll_ctl(efd, EPOLL_CTL_ADD, lfd, &tep);
if (ret < 0)
{
perror("epoll_ctl error");
exit(1);
}
// 循环去epoll_wait进行监听
while (1)
{
wait_ret = epoll_wait(efd, ep, 128, -1); // wait_ret就是实际满足事件的总个数
// 以wait_ret为上限去遍历事件
for (i = 0; i < wait_ret; i++)
{
// sockfd用于接收满足事件的fd
sockfd = ep[i].data.fd;
// 如果等于lfd,那就说明有客户端要来连接,就去accept
if (sockfd == lfd)
{
client_addr_len = sizeof(client_addr);
cfd = accept(lfd, (struct sockaddr *)&client_addr, &client_addr_len);
// 将cfd设置为非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 把新的cfd加入树中
tep.events = EPOLLIN | EPOLLET;
tep.data.fd = cfd;
ret = epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &tep);
if (ret < 0)
{
perror("epoll_ctl cfd error");
exit(1);
}
}
// 如果不是lfd,那就说明有读事件发生(读数据)
else
{
len = read(sockfd, buf, sizeof(buf));
if (len == 0) // 说明对方关闭连接(从树上摘下 & close)
{
epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
}
else if (len == -1)
{
perror("read error");
exit(1);
}
else // 读写数据
{
for (i = 0; i < len; i++)
buf[i] = toupper(buf[i]);
write(sockfd, buf, len);
write(STDIN_FILENO, buf, len);
}
}
}
}
return 0;
}
ET/LT 事件模型
ET边缘触发(高效模式)(在epoll_ctl中设置event结构体的events监控事件的时候 或上一个 EPOLLET)新的事件活跃了才会触发(不依靠缓冲区数据,依靠客户端行动)
比如客户端一次写入aaaa\nbbbb\n,但是服务器只读5个也就是aaaa\n,在while的下次循环中,上次写入但服务器没读的bbbb\n并不会触发监控,这就是ET边缘触发。直到下次客户端写入,才会因为fd活跃而被监控到,此时才把上次没读的bbbb\n读到(每次只读5个,客户端每次10个,当边缘触发读到bbbb\n 的时候,缓冲区可能已经写道dddd\n了)。
LT水平触发(默认模式)(依靠缓冲区数据)
客户端一次写入aaaa\nbbbb\n,但是服务器只读5个也就是aaaa\n,在while的下次循环中,上次写入但服务器没读的bbbb\n可以触发监控,这就是LT水平触发
ET边缘触发—非阻塞、忙轮询(想了很久)![image.png](https://img-blog.csdnimg.cn/img_convert/281bc647ad28b8f2d3a7c0476ea7b88a.png)
为何非阻塞
如果read改成readn(readn是一种如果设置n=500,没读够500就会在readn这里阻塞的一种读方式),此时如果客户端只写了498,那么就会readn这里阻塞。 由于在这里阻塞住,导致后面其他的事件触发不了epoll_wait,从而错过其他事件。因此,ET只支持非阻塞!(用fcntl修改阻塞属性),套接字在非阻塞模式下,如果没读到内容就会立马返回。
而如果是LT模式,分两次写入数据会触发epoll_wait,因为LT只要有数据可读,就会触发epoll_wait。
epoll_wait触发事件:
当客户端向服务器发送数据时,操作系统会将数据存放在套接字的接收缓冲区中,并不会立即触发任何事件。服务器在之后调用 epoll_wait 时,如果套接字由本来没有数据变成有数据可以读取,这时 epoll_wait 就会返回,并且返回值表明该套接字是可读的。如果客户端分两次写入数据,epoll_wait 只会在第一次写入数据后触发一次。这是因为 ET 模式下 epoll_wait 只会在文件描述符上状态发生变化时触发,而不会重复触发相同的事件。
当客户端第一次写入数据时,服务器端套接字变得可读,触发了 epoll_wait。但是即使客户端继续写入数据,服务器端套接字的可读状态并没有发生变化(因为 ET 模式只在状态变化时触发),所以不会再次触发 epoll_wait。除非在处理完第一次写入的数据后,重新调用 epoll_wait,此时如果客户端有更多数据写入,epoll_wait 可能会再次触发。
为何忙轮询读
在非阻塞的情况下,如果不忙轮询读,类似前面的例子,先写了498可以读到,因为套接字非阻塞,如果不在读套接字外面加一层while,read返回后立马就进入进入下轮的epoll_wait了。加了while可以保证第二次的循环不会覆盖第一次的数据。
优缺点
优点
高效,能突破1024描述符
缺点
无法跨平台
epoll反应堆(libevent核心思想实现代码)
反应堆的理解:加入IO转接之后,有了事件,server才去处理,这里反应堆也是这样,由于网络环境复杂,服务器处理数据之后,可能并不能直接写回去,比如遇到网络繁忙或者对方缓冲区(滑动窗口)已经满了这种情况,就不能直接写回给客户端。反应堆就是在处理数据之后,监听写事件,能写回客户端了,才去做写回操作。写回之后,在改回监听读事件,以此循环
- 泛型指针可以跟任何类型转换
- union允许在同一内存位置存储不同的数据类型。它类似于结构体(Struct),但不同之处在于,结构体中的每个成员都有自己的内存空间,而联合体中的所有成员共享同一块内存空间。
与epoll的不同
epoll在epoll_wait中,根据监听到的event[i].data.fd == lfd还是 cfd
来判断是活跃的是lfd还是cfd
epoll反应堆使用**(struct myevent_s*)event[i].data.ptr **
赋值给自定义结构体变量的指针 struct myevent_s *ev
__func___表示函数名的宏定义,__LINE___表示行号的宏定义
/*
*epoll基于非阻塞I/O事件驱动
*/
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#define MAX_EVENTS 1024 //监听上限数
#define BUFLEN 4096
#define SERV_PORT 8080
void recvdata(int fd, int events, void *arg);
void senddata(int fd, int events, void *arg);
/* 描述就绪文件描述符相关信息 */
struct myevent_s {
int fd; //要监听的文件描述符
int events; //对应的监听事件
void *arg; //泛型参数
void (*call_back)(int fd, int events, void *arg); //回调函数
int status; //是否在监听:1->在红黑树上(监听), 0->不在(不监听)
char buf[BUFLEN];
int len;
long last_active; //记录每次加入红黑树 g_efd 的时间值
};
int g_efd; //全局变量, 保存epoll_create返回的文件描述符
struct myevent_s g_events[MAX_EVENTS+1]; //自定义结构体类型数组. +1-->listen fd
/*将结构体 myevent_s 成员变量 初始化*/
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg)
{
ev->fd = fd;
ev->call_back = call_back;
ev->events = 0;
ev->arg = arg;
ev->status = 0;
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
ev->last_active = time(NULL); //调用eventset函数的时间
return;
}
/* 向 epoll监听的红黑树 添加一个 文件描述符 */
//eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
void eventadd(int efd, int events, struct myevent_s *ev)
{
struct epoll_event epv = {0, {0}};
int op;
epv.data.ptr = ev;
epv.events = ev->events = events; //EPOLLIN 或 EPOLLOUT
if (ev->status == 0) { //已经在红黑树 g_efd 里
op = EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1
ev->status = 1;
}
if (epoll_ctl(efd, op, ev->fd, &epv) < 0) //实际添加/修改
printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
else
printf("event add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op, events);
return ;
}
/* 从epoll 监听的 红黑树中删除一个 文件描述符*/
void eventdel(int efd, struct myevent_s *ev)
{
struct epoll_event epv = {0, {0}};
if (ev->status != 1) //不在红黑树上
return ;
//epv.data.ptr = ev;
epv.data.ptr = NULL;
ev->status = 0; //修改状态
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv); //从红黑树 efd 上将 ev->fd 摘除
return ;
}
/* 当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */
void acceptconn(int lfd, int events, void *arg) // lfd的回调函数
{
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) {
if (errno != EAGAIN && errno != EINTR) {
/* 暂时不做出错处理 */
}
printf("%s: accept, %s\n", __func__, strerror(errno));
return ;
}
do {
for (i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素
if (g_events[i].status == 0) //类似于select中找值为-1的元素
break; //跳出 for
if (i == MAX_EVENTS) {
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break; //跳出do while(0) 不执行后续代码
}
int flag = 0;
if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) { //将cfd也设置为非阻塞
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
/* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */
eventset(&g_events[i], cfd, recvdata, &g_events[i]); // 参数3是回调函数
eventadd(g_efd, EPOLLIN, &g_events[i]); //将cfd添加到红黑树g_efd中,监听读事件
} while(0);
printf("new connect [%s:%d][time:%ld], pos[%d]\n",
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
return ;
}
void recvdata(int fd, int events, void *arg) // 读的回调函数
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = recv(fd, ev->buf, sizeof(ev->buf), 0); //读文件描述符, 数据存入myevent_s成员buf中
eventdel(g_efd, ev); //将该节点从红黑树上摘除
if (len > 0) {
ev->len = len;
ev->buf[len] = '\0'; //手动添加字符串结束标记
printf("C[%d]:%s\n", fd, ev->buf);
eventset(ev, fd, senddata, ev); //设置该 fd 对应的回调函数为 senddata
eventadd(g_efd, EPOLLOUT, ev); //将fd加入红黑树g_efd中,监听其写事件
} else if (len == 0) {
close(ev->fd);
/* ev-g_events 地址相减得到偏移元素位置 */
printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
} else {
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return;
}
void senddata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = send(fd, ev->buf, ev->len, 0); //直接将数据 回写给客户端。未作处理
eventdel(g_efd, ev); //从红黑树g_efd中移除
if (len > 0) {
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
eventset(ev, fd, recvdata, ev); //将该fd的 回调函数改为 recvdata
eventadd(g_efd, EPOLLIN, ev); //从新添加到红黑树上, 设为监听读事件
} else {
close(ev->fd); //关闭链接
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return ;
}
/*创建 socket, 初始化lfd */
void initlistensocket(int efd, short port)
{
struct sockaddr_in sin;
int lfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(lfd, F_SETFL, O_NONBLOCK); //将socket设为非阻塞
memset(&sin, 0, sizeof(sin)); //bzero(&sin, sizeof(sin))
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(port);
bind(lfd, (struct sockaddr *)&sin, sizeof(sin));
listen(lfd, 20);
/* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */
eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);//初始化自定义结构体,初始化的参数是后面的lfd,acceptconn等
/* void eventadd(int efd, int events, struct myevent_s *ev) */
eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); // 把lfd挂到红黑树上
return ;
}
int main(int argc, char *argv[])
{
unsigned short port = SERV_PORT;
if (argc == 2)
port = atoi(argv[1]); //使用用户指定端口.如未指定,用默认端口
g_efd = epoll_create(MAX_EVENTS+1); //创建红黑树,返回给全局 g_efd
if (g_efd <= 0)
printf("create efd in %s err %s\n", __func__, strerror(errno));
initlistensocket(g_efd, port); //初始化监听socket
struct epoll_event events[MAX_EVENTS+1]; //保存已经满足就绪事件的文件描述符数组
printf("server running:port[%d]\n", port);
int checkpos = 0, i;
while (1) {
/* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */
long now = time(NULL); //当前时间
for (i = 0; i < 100; i++, checkpos++) { //一次循环检测100个。 使用checkpos控制检测对象
if (checkpos == MAX_EVENTS)
checkpos = 0;
if (g_events[checkpos].status != 1) //不在红黑树 g_efd 上
continue;
long duration = now - g_events[checkpos].last_active; //客户端不活跃的世间
if (duration >= 60) {
close(g_events[checkpos].fd); //关闭与该客户端链接
printf("[fd=%d] timeout\n", g_events[checkpos].fd);
eventdel(g_efd, &g_events[checkpos]); //将该客户端 从红黑树 g_efd移除
}
}
/*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/
int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
if (nfd < 0) {
printf("epoll_wait error, exit\n");
break;
}
for (i = 0; i < nfd; i++) {
/*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) { //读就绪事件
ev->call_back(ev->fd, events[i].events, ev->arg);
//lfd EPOLLIN
}
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) { //写就绪事件
ev->call_back(ev->fd, events[i].events, ev->arg);
}
}
}
/* 退出前释放所有资源 */
return 0;
}
函数解析
自定义结构体,void*ptr的时候就传它,之前epoll_wait返回是判断fd,这里把ptr拿出来交给ev。下图中两个红框,ev是ptr那里拿过来的,events数组是wait返回的,如果两者都是读,就读。两者都是写则写。
难点就在结构体的void*arg,它就是本身这个结构体本身。
eventadd:对epoll_ctl的封装
initial—> eventset—>eventadd—>while(1)->epoll_wait
ctags
线程池
有线程数组、任务队列、管理线程。一开始初始化线程数组有min个线程后,线程们都在内调函数worker里,阻塞在条件变量上(cond_wait-notempty)上,当有任务加到任务队列中,阻塞在cond上的线程数组被唤醒(cond_signal)从任务队列中取任务做。 管理线程有算法判断当前liveNum和busyNum的大小差,当不够的时候,管理线程会往线程数组中添加新线程,同样设置回调函数worker,当空闲太多的时候,通过cond_signal欺骗worker里面阻塞着的线程去自杀。
而这所有操作使用的参数,都通过一个线程池结构体存储,包括任务队列队头队尾、liveNum、busyNum、等等各个函数需要的参数都用该结构体存着。 整个线程池组开始要做的就是初始化该结构体函数,生成min的线程数组,生成空的任务队列,生成管理者线程,2把锁,2个条件变量。
另外,往任务队列添加任务是在外部main函数中使用的。
UDP与TCP通信的优缺点
TCP: 面向连接的,可靠**数据包**传输。对于不稳定的网络层,采取完全弥补的通信方式。 丢包重传。<br /> 优点:<br /> 稳定----数据流量稳定、速度稳定、顺序<br /> 缺点:传输速度慢。相率低。开销大。
- 使用场景:数据的完整型要求较高,不追求效率。大数据传输、文件传输。
UDP: 无连接的,不可靠的**数据报**传递。对于不稳定的网络层,采取完全不弥补的通信方式。 默认还原网络状况<br /> 优点:<br /> 传输速度块。效率高。开销小。<br /> 缺点:<br /> 不稳定----数据流量。速度。顺序。<br /> 使用场景:对时效性要求较高场合。稳定性其次。 游戏、视频会议、视频电话。 <br />腾讯、华为、阿里 --- 应用层数据校验协议,弥补udp的不足。
UDP实现C/S模型
默认多路IO,连接无状态(没有TCP时序图之类的)read、write``recv()/send()
只能用于 TCP 通信。
UDP中 recvfrom()、sendto()
替代 read、write
UDPserver端的accpet()
以及client端的connect()
被舍弃
UDP实现思路
nc 指令是TCP用的。
UDP不需要高并发就能多个连接
Server端:
socket()
bind()
listen() 可有可无
while(1){
recvfrom()
sendto()
}
Client端
socket()
初始化服务器地址结构的时候:不能INADDR_ANY初始化,要用inet_pton(AF…, “xxx”, &addr…);
while(1){
sendto()----地址结构放服务器的
recvfrom()-----地址结构放服务器的,传入传出(不关心服务器信息可以放NULL)
}
recvfrom函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
recvfrom
包含了TCP中的accept
做的事情
参数:
sockfd: 接客套接字lfd
buf:缓冲区地址
len:缓冲区大小
flags: 默认用0
src_addr:(struct sockaddr *)&addr 传出。 对端地址结构
addrlen:传入传出。
**返回值: **
成功接收数据字节数。 失败:-1 errn。 0: 对端关闭。
sento函数
包含了connect做的工作,在客户端中不需要bind 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 传入。 目标地址结构(比如服务器IP端口)
addrlen:地址结构长度。
**返回值:**成功写入数据字节数。 失败 -1, errno
close();
fgets函数:
char *fgets(char *str, int n, FILE *stream);
- str:指向用于存储读取行的字符数组的指针。
- n:要读取的最大字符数(包括空字符),通常是缓冲区的大小。
- stream:指向文件流的指针,表示从中读取文本的文件。、
- stdin 通常用于 C 语言标准库函数中,比如 fread()、fgets() 等,它们需要一个 FILE 结构指针参数,用于指定输入源。上面函数参数3用stdin
- STDIN_FILENO 可以用于低级 I/O 函数,比如 read(),这些函数通常需要一个文件描述符参数来指定输入源。
代码
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define SERV_PORT 9527
int main(){
int socked, n;
char buf[BUFSIZ];
// socked
socked = socket(AF_INET, SOCK_DGRAM,0);
// bind
struct sockaddr_in saddr, caddr;
socklen_t caddr_len;
caddr_len = sizeof(caddr);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
saddr.sin_port = htons(SERV_PORT);
bind(socked, (struct sockaddr*)&saddr, sizeof(saddr));
while(1){
n = recvfrom(socked, buf, sizeof(buf), 0, (struct sockaddr*)&caddr, &caddr_len);
if(n == -1){
printf("rcvdfrom error \n");
}
for(int i = 0; i < n; i++){
buf[i] = toupper(buf[i]);
}
sendto(socked, &buf, strlen(buf), 0, (struct sockaddr*)&caddr, caddr_len);
}
close(socked);
return 0;
}
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#define SERV_PORT 9527
int main(){
int cfd;
int n, ret;
char buf[BUFSIZ];
// socket
cfd = socket(AF_INET, SOCK_DGRAM,0);
struct sockaddr_in addr;
memset(&addr, 0 , sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET,"127.0.0.1", &addr.sin_addr.s_addr);
while(fgets(buf, BUFSIZ, stdin)!=NULL){
ret = sendto(cfd, buf, strlen(buf), 0, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1){
printf("sendto error\n");
exit(1);
}
n = recvfrom(cfd, buf, strlen(buf), 0, NULL,0); // 不关心服务器信息
if(n == -1){
printf("recvfrom error\n");
exit(1);
}
write(STDOUT_FILENO, buf,n);
}
close(cfd);
return 0;
}
本地套接字(进程间通信)
与之前使用socket()的不同在于:
- socket参数不同:
,本地套接字的domain是AF_UNIX/AF_LOCAL,参数2无所谓,
- 地址结构体不同:网络通信要用到的ip跟端口不需要了,结构体
sockaddr_in
变成了sockaddr_un
成员addr.sun_family = AF_UNIX; ``addr.sun_path = 路径名(使用strcpy(addr.sun_path, "xxxx"))
相当于把套接字绑定到一个文件上,其他进程通过这个路径与套接字通信 - bind的参数3addr大小不同
offsetof(struct sockaddr_un, sun_path)
:,之前使用sockaddr_in,大小固定可以用sizeof,而sockaddr_un的第二个成员变量由于文件路径不同而不同,所以长度要变成 2+strlen(路径名),这里传2是硬编码,不好,使用偏移位函数
offsetof(struct sockaddr_un, sun_path)
也就是计算结构体开始位置到sun_path的偏移位置,也就是16位=2字节。
- 后面的accept接受客户端访问时,传入传出参数
socklen_t* &len
要取出客户端的套接字文件路径时,要减掉前面的2字节AF_UNIX - 客户端要初始化两个地址结构体,一个是用于bind自身伪文件的地址结构体(在网络通信中客户端是调用connect时系统自动隐式绑定到一个临时端口上,这里不能隐式绑定,因为网络通信是动态端口,本地套接字是文件系统),一个是要访问服务端的地址结构体用于connect。
网络通信中bind是绑定套接字跟IP端口,在本地套接字中,bind绑定文件路径,相当于创建了一个套接字伪文件(大小为0),其他进程可以通过这个路径来连接套接字。一般本地套接字之前会调用unlink(路径)
如果原来存在这个文件就删了它的硬链接(close之前还可以用,close之后相当于删文件),防止路径已经被使用过。
libevent库
基于事件的异步通信模型
前提
zxvf先解压,然后安装源码包
源码包安装3步:
ldconfig更新共享库缓存
ldconfig 命令用于更新动态链接器运行时链接的共享库缓存。在 Linux 系统中,动态链接器用于在程序运行时将程序与共享库链接起来。共享库缓存包含系统中可用的共享库的列表和路径。当你安装了新的共享库或者修改了共享库的路径时,你可能需要手动运行 ldconfig 命令来更新共享库缓存,以便系统能够找到新安装的共享库。通常情况下,当你使用包管理器安装或卸载共享库时,会自动运行 ldconfig 来更新共享库缓存。
使用 ldconfig 命令通常需要 root 权限,因为它会修改系统的共享库缓存。一般来说,你只有在安装了新的共享库或者修改了共享库的路径时才需要手动运行 ldconfig 命令。
编译时 使用添加-levent
event的头文件很多都放在event2下,所以头文件用/
初识
libevent 特性:
基于事件的异步通信模型,linux所见皆文件,这个库任何都是事件。 异步意思函数的执行跟注册不是同一个事件,代码是达到某个条件才执行,这是依赖回调机制实现的异步通信。
简单来说,libevent库就是 先有一个底座,然后创建各种事件插到底座上去, 对这些事件进行监听,当条件满足,回调函数就触发了。
dispath自带循环并且监听,相当于epoll中的while(epoll_wait)
libevent框架
event五部分:
- 创建底座: event_base_new(), 返回结构体, struct event_base*一个变量接住
struct event_base* base = event_base_new()
struct event_base* base = bufferevent_socket_new()
- 创建事件: event/bufferevent
struct event *event_new(struct event_base *base, evutil_socket_t fd, short what, event_callback_fn callback, void *arg)
- 将事件添加到base上:
int event_add(struct event *ev, const struct timeval *tv);
- 循环监听事件满足:
int event_base_dispatch(struct event_base *base); 内部就是while(1){epoll}
- 释放event_base:
int event_base_free(struct event_base *base);
event事件的三个函数
event_new()创建事件
struct event *event_new(struct event_base *base, evutil_socket_t fd, short what, event_callback_fn callback, void *arg)
返回值struct event
参数: 1、底座 2、套接字(在网络通信中放监听套接字lfd,而bufferevent通常放用于通信的跟客户端已经建立连接的cfd) 3、事件监听什么 ,读写都需要的时候|起来4、5、 事件的回调函数以及参数。
不加typedef时,只是声明一个函数指针event_c…b…_fn,加了typedef之后,以后可以直接用event_callback_fn来声明函数指针。event_new的其他参数都是给这个回调函数参数传参的。
event_add()添加事件到base上
int event_add(struct event *ev, const struct timeval *tv);
参数: 1、事件结构体 (因为参1在new的时候已经指定了base,所以add函数都不需要base参数了)2、为NULL则不会超时,一直等事件触发,为非0则表示tv微妙后触发
event_free()释放事件
int event_free(struct event *ev);
event_del(*ev)从base上摘下来事件, 可以使未决事件变成非未决事件
不常用的函数
父进程中的base,在子进程中要初始化才能用(之前系统编程阶段说的父子进程共享全局变量在这里子要初始化才能用)
未决态/非未决态
bufferevent
特点介绍
带缓冲区的事件,借助队列实现的两个缓冲区,所以读过了就没了。
头文件 #include<event2/bufferevent.h>
一般用来跟socket搭配使用(上面的event配合fifo使用很少)
cfd在bufferlisten后返回,并且作为参数封装进bufferevent,后面使用的都是封装了cfd的函数(比如read跟write就没有cfd给他们传进行使用,所以封装了buffer自身的read和write)
- 结构体内带两个缓冲区,两个缓冲区能触发回调函数。同时,bufferevent不能使用普通的read和write函数,库封装了buffevent_read和bufferevent_write。两个缓冲区底层上还是在跟cfd交互。
read缓冲区的回调函数在缓冲区达到一定数量时候触发buffer_read回调函数
write缓冲区的回调函数在bufferevent_write写成功后才会调用。
bufferevent服务器端通信流程
流程中使用到的函数在后面有记录
- 创建base,
event_base_new()
- 创建服务器连接监听器,
evconnlistener_new_bind()
,监听到客户端连接时生成cfd,并且在回调函数中处理接受与客户端连接后的操作。 - 在监听器的回调中使用
bufferevent_socket_new()
创建一个新 bufferevent事件,将监听器返回的cfd封装到这个事件对象中 - 在监听器的回调中使用
bufferevent_setcb()
给这个事件对象的 read、write、event设置回调函数。(r、w的cb是缓冲区达到一定量时触发) - 在监听器的回调中设置 bufferevent的读写缓冲区 enable/disable(因为read缓冲区默认是disable)
- 在bufferevent_setcb中的读写回调中可以使用封装的读写函数
bufferevent_read()
/bufferevent_write()
读写缓冲区内容–>在bufferevent的读写回调中进行 - 启动循环监听
event_base_dispatch(base)
- 释放资源:监听器、base
bufferevent客户端通信流程
- 客户端不需要bind,也不需要监听器
- 直接connect到服务器端的IP端口地址结构体
- 客户端bufferevent不在回调函数内
函数
3个头文件汇总:
- 监听器
#include<event2/listener.h>
- bufferevent事件
#include<event2/bufferevent.h>
-
evconnlistener_new_bind()
**作用:**完成epoll中 socket(),bind(),listen(),accept()
四个函数的作用,创建lfd,绑定服务端ip端口地址结构,设置最大同时在线数,监听客户端连接请求返回cfd。
函数:
参数列表:
- 2是listener的回调函数,里面放着创建bufferevent事件之类的回调函数
- 3ptr是回调函数的参数,传base,从而给回调函数中的new做参数
- 4flags是设置LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,第一个设置关闭时自动释放底层套接字,第二个是设置端口复用
- 5backlog是listen()的参数2,设置最大同时在线数,-1表示最大值
- sa传入的服务器ip端口地址结构体,bind()的参数
- socklen服务器ip端口地址结构体的大小
- 返回值:成功创建的监听器
监听器的cp回调函数:
自动调用,参数分别是:之前返回监听器,cfd,客户端的地址结构体,长度,ptr是监听器的参数ptr,也就是传过来base。
bufferevent_socket_new()
创建bufferevent事件,在连接到客户端后创建,即在监听器的回调函数中创建struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options);
参数:
- 传入的是与客户端连接的套接字描述符 cfd,而不是用于监听的套接字描述符 lfd。
- BEV_OPT_CLOSE_ON_FREE ,option设置event释放的时候,自动关闭底层的套接字
- 返回值:创建号的事件对象(内含两个缓冲区)
buffevent_setcb()设置读写缓冲区回调函数
当缓冲区大小到一定程度就会触发readcb回调函数,而wirtecb回调函数是在写入完成后才触发(没什么用)。
前面event在新建事件的时候就设置好了回调: event_new( )参数中放入cb—>event_add()。
而bufferevent不同在setcb()函数中单独设置。
在listener的回调函数中直接调用语句就行。再内部的回调函数readcb之类的在全部定义即可。
void bufferevent_setcb(struct bufferevent * bufev,
bufferevent_data_cb readcb,
bufferevent_data_cb writecb,
bufferevent_event_cb eventcb,
void *cbarg );
参数:
- bufev 事件
- 读回调函数readcb(buffer_read一般在该回调函数触发时使用)
- 写回调函数writecb
- 可设置触发条件的回调函数,也可以传NULL
- 上述回调函数的参数
上面几种回调函数的设置:
read_cb:
write_cb(可以传NULL):int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);
eventcb:
参数3自己设置回调函数的函数原型
bufferevent缓冲区启动、关闭
读缓冲默认关闭,因此需要自己手动开启。
大作业简易web服务器
HTTP
请求消息
- 第一行是请求行。请求类型–访问资源—http版本
- GET中,第九行相当于哨兵,标志协议头的结束。而在POST中,比如像服务器发送注册的账号密码,就在第九行的后面一行写,这部分就是请求数据部分。
- 因为历史原因,每行结束标记是\r\n
响应消息
- 第一行状态行
- 第四行,形容返回给客户端的类型,让浏览器判断如何展示信息
模拟读