网络基础 mac地址 (6个字节 48位)标记网卡的id,;理论上这个id全球唯一
mac地址一般用来标识主机的id,这个id是物理地址,不会改变。
ip地址,ipv4(4个字节,32位),ipv6(16个字节,128位) IP地址是标识主机的id,这个id是虚拟的,会改变。
一个IP将其分为子网id和主机id
子网id和主机id需要和子网掩码一起来看,
ping命令用来测试两台主机的网络联通性(windows和Linux都适用)。
Linux下设置ip命令
1 ifconfig ens33 192.168.131.133 netmask 255.255.255.0
桥接模式(Bridged Networking) 桥接模式是将虚拟机/容器的网络桥接到物理网卡 上,使其就像是局域网中的一台独立主机。
工作原理:
虚拟机通过虚拟网桥(Bridge)连接到宿主机的物理网卡,相当于直接插入交换机,与宿主机处于同一物理局域网 中。
网络特点:
虚拟机可获得与宿主机同网段的IP地址 (例如通过 DHCP)
可以被局域网中其他设备访问(可 ping 通)
能访问局域网和外网
优点:
网络透明,虚拟机像一台真实的独立主机
易于与其他局域网设备通信
缺点:
需要宿主网络支持(如局域网允许多个 IP)
有可能增加网络冲突和安全风险
NAT 模式(网络地址转换) NAT 模式是虚拟机/容器通过宿主机的网络连接上网,共享宿主机的 IP 。
工作原理:
宿主机充当“路由器”,将虚拟机的私有 IP 转换为宿主的公网 IP,进行地址转换。
网络特点:
虚拟机使用私有 IP 地址(如 192.168.xx.xx)
能访问外网,但外部主机无法主动访问虚拟机
类似家庭内的多台设备通过路由器上网
优点:
设置简单,几乎无须额外配置
安全性高,外部无法直接访问虚拟机
缺点:
虚拟机不能被外部主机主动访问(除非端口转发)
网络透明度低,不适合复杂网络测试
端口(port) 作用:用来标识应用程序(进程)
port:两个字节 0-65535
0-1023 知名端口
自定义端口1024-65535(避免冲突)
一个应用可以有多个端口,一个端口只能有一个应用程序。
OSI七层模型 为了使各种不同的计算机之间可以互联,ARPANet指定了一套计算机通信协议,即TCP/IP协议(族);
为了减少协议设计的复杂性,大多数网络模型均采用分层的方式来组织。
每一层利用下一层提供的服务来为上一层提供服务,本层服务的实现细节对上层屏蔽。
物理层:双绞线(网线)接口类型,光纤的传输速率等等
数据链路层:mac 负责收发数据
网络层:ip 给两台主机提供路径选择。
传输层:port 区分数据递送到那个应用程序。
会话层:建立连接
表示层:解码
应用层:应用程序
TCP/IP四层模型
协议 规定了数据传输的方式和格式
这里主要按照TCP/IP四层模型有的协议。
应用层协议:
FTP:文件传输协议
HTTP:超文本传输协议
NFS:网络文件系统
传输层协议:
TCP:传输控制协议 头部20个字节
UDP:用户数据报协议 头部8个字节
网络层:
IP:英特网互联协议 头部20个字节
ICMP:英特网控制报文协议 ping命令就是这个协议 IGMP:英特网组管理协议
链路层:
ARP:地址解析协议 通过ip找mac地址
RARP:反向地址解析协议 通过mac找ip
硬件接口:mac头 头部14个字节
目的mac地址为ff:ff:ff:ff:ff:ff 局域网内其他主机无条件接受。(常在ARP协议上使用)
网络设计模式 B/S browser/server
C/S client/server
C/S 性能较好 客户端容易篡改数据 开发周期较长
B/S 性能低 客户端安全 开发周期短
TCP三次握手 tcp建立连接的过程 SYN是请求建立连接的标识,ACK就是确认。
mss:最大报文长度,一般出现在三次握手的前两次,用来告知对方传送数据的最大长度。
MTU:最大传输单元 由网卡限制
TCP四次挥手 tcp断开连接,这个哪一方发起断开都可以,就以客户端申请断开为例子,三次握手一定是客户端发起。
就是两端的某一段调用了close函数。
FIN是请求断开连接的标识
滑动窗口 TCP报头上有一个窗口尺寸的标识,这个是说接收方告知发送方:我的接收缓冲区还能接收多少字节。
在ACK标识回复时会带有win剩余多少空间。
这样发送方就会发送以避免拥堵,因为知道接收方还剩下多少空间,接收方是把当前接收缓冲区的一块数据提取出来,才会回复给发送方ACK携带当前缓冲区大小告知对方。
TCP状态转换
如果想要查看TCP应用的状态
1 netstat -anp | grep 8000(端口)
半关闭 主动方在FIN_WAIT_2状态之后,主动方不可以在应用层发送数据了,但是应用层还可以接受数据,这个状态叫半关闭。
有人就有疑惑了,那为什么在TIME_WAIT状态还能发,因为它发的是ACK,主动方不可以在应用层发送数据了,指的是应用层的协议不可以接受了,但是在底层,传输层TCP协议还是可以发出,所以ACK是可以发的 。
1 2 3 4 5 6 7 8 #include <sys/socket.h> int shutdown (int sockfd, int how) ;参数: sockfd:需要关闭的socket的描述符 how:允许为shutdown操作选择以下几种方式: SHUT_RD(0 ):关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被丢弃。 SHUT_WR(1 ):关闭sockfd上的写功能,此选项将不允许sockfd进行写操作。进程不能再对此套接字发出写操作 SHUT_RDWR(2 ):关闭sockfd的读写功能。相当于调用shutdown两次:首先是SHUT_RD,然后是SHUT_WR。
TCP异常断开 心跳包(TCP 的 keepalive 机制) SO_KEEPALIVE保持连接检测对方主机是否崩溃,避免服务器永远阻塞于TCP连接的输入。设置该选项后,如果两小时内在此套接口的任一方向都没有数据交换,TCP会自动给对方发一个保持存活探测分节,这是一个对方必须响应的TCP分节。他会导致以下三种情况:
对方接收一切正常:以期望的ACK响应。2小时后,TCP将发出另一个。
对方已崩溃且已重新启动:以RST响应。套接口的待处理错误设置为ECONNRESET,套接口本身被关闭。
对方无任何响应:源自berkeley的TCP发送另外8个探测分节,相隔75秒一个,试图得到一个响应。在发出第一个探测分节11分钟15秒后若仍无响应就放弃。套接口的待处理错误被置为ETIMEOUT,套接口本身就关闭。若ICMP错误是host unreachable(主机不可达),说明对方主机没有崩溃,但是不可达,这种情况下待处理错误设置为EHOSTUNREACH.
所以我们可以设置SO_KEEPALIVE属性使得我们在两小时后发现对方的TCP连接是否依然存在。
1 2 keepAlive=1 ; setsockopt(listenfd,SOL_SOCKET,SO_KEEPALIVE,(void *)&keepAlive,sizeof (keepAlive));
但是说真的,这个函数两小时检测一次,对于现在来说,有点太长了,所以可以自己在应用层写对应的心跳包。
心跳包:最小粒度,携带的数据部分一定要少。
乒乓包:携带比较多的数据的心跳包。
端口复用 端口重新启用,谁最后启用的端口谁用。前面启用的作废,用不了了。为什莫需要端口复用呢,在server的TCP连接没有完全断开之前不允许重新监听。比如我强制退出server,再启动就不会说端口被占用了。
1 2 3 在server代码的socket()和bind()调用之间插入如下代码: int opt=1 ;setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (opt));
UDP TCP:传输控制协议 安全可靠 丢包重传 面向连接(电话模型)
UDP:用户数据报协议 不安全不可靠 丢包不重传 快 不面向连接(邮件模型)
tcp通信流程: 服务器:创建流式套接字 绑定 监听 提取 读写 关闭
客户端:创建流式套接字 连接 读写 关闭
收发数据:
read recv
1 2 3 4 5 #include <sys/socket.h> ssize_t recv (int sockfd, void buf[.len], size_t len, int flags) ;flags==MSG_PEEK 读数据不会删除缓冲区的数据 一般填0
write send
1 2 3 #include <sys/socket.h> ssize_t send (int sockfd, const void buf[.len], size_t len, int flags) ;
udp通信流程:
服务器:创建报式套接字 绑定 读写 关闭
客户端:创建报式套接字 读写 关闭
发数据:
1 2 3 4 5 6 #include <sys/socket.h> ssize_t sendto (int sockfd, const void buf[.len], size_t len, int flags, const struct sockaddr * dest_addr, socklen_t addrlen) ;dest_addr:目的地的地址信息 addrlen:结构体大小
收数据:
1 2 3 4 5 6 7 #include <sys/socket.h> ssize_t recvfrom (int sockfd, void buf[restrict .len], size_t len, int flags, struct sockaddr * _Nullable restrict src_addr, socklen_t * _Nullable restrict addrlen) ;src_addr:对方的地址信息 addrlen:结构体大小地址
创建报式套接字
1 2 3 4 5 int socket (int domain,int type,int protocol) ;参数: domain:AF_INET type:SOCK_DGRAM protocol:0
UDP一般是一对一的。一个套接字服务一个客户端,不像TCP,后来可以分配套接字。
UDP服务器和客户端代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <stdio.h> #include <sys/socket.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> int main (int argc,char *argv[]) { int fd=socket(AF_INET,SOCK_DGRAM,0 ); struct sockaddr_in myaddr ; myaddr.sin_family=AF_INET; myaddr.sin_port=htons(8000 ); myaddr.sin_addr.s_addr=inet_addr("127.0.0.1" ); int ret=bind(fd,(struct sockaddr*)&myaddr,sizeof (myaddr)); if (ret<0 ){ perror("bind" ); return 0 ; } char buf[1500 ]="" ; struct sockaddr_in cliaddr ; socklen_t len=sizeof (cliaddr); while (1 ){ int n=recvfrom(fd,buf,sizeof (buf),0 ,(struct sockaddr*)&cliaddr,&len); if (n<0 ){ perror("" ); break ; } else { printf ("%s\n" ,buf); sendto(fd,buf,n,0 ,(struct sockaddr*)&cliaddr,len); } } close(fd); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <sys/socket.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <string.h> int main (int argc,char *argv[]) { int fd=socket(AF_INET,SOCK_DGRAM,0 ); struct sockaddr_in myaddr ; myaddr.sin_family=AF_INET; myaddr.sin_port=htons(9000 ); myaddr.sin_addr.s_addr=inet_addr("127.0.0.1" ); int ret=bind(fd,(struct sockaddr*)&myaddr,sizeof (myaddr)); if (ret<0 ){ perror("bind" ); return 0 ; } char buf[1500 ]="" ; struct sockaddr_in dstaddr ; dstaddr.sin_family=AF_INET; dstaddr.sin_port=htons(8000 ); dstaddr.sin_addr.s_addr=inet_addr("127.0.0.1" ); int n=0 ; while (1 ){ n=read(STDIN_FILENO,buf,sizeof (buf)); sendto(fd,buf,n,0 ,(struct sockaddr*)&dstaddr,sizeof (dstaddr)); memset (buf,0 ,sizeof (buf)); int n=recvfrom(fd,buf,sizeof (buf),0 ,NULL ,NULL ); if (n<0 ){ perror("" ); break ; } else { printf ("%s\n" ,buf); } } close(fd); }
1 2 3 4 5 6 7 8 hello hello hhhhh hhhhh woshinidia woshinidia
本地套接字通信 unix domain socket又是一个新的IPC方式, “本地套接字”通常指的是 Unix 域套接字(Unix Domain Socket,UDS) ,是一种用于同一台主机内部进程间通信(IPC) 的套接字,与 TCP/IP 套接字相比,它不经过网络协议栈,速度更快、效率更高。
全双工
套接字用文件来标识,这个文件在绑定之前是不能存在的。
作用 :实现本地进程间通信,替代管道、共享内存等传统 IPC 手段。
协议族 :使用 AF_UNIX
或 AF_LOCAL
而不是 AF_INET
。
通信方式 :支持 SOCK_STREAM
(类似 TCP)和 SOCK_DGRAM
(类似 UDP)。
地址类型 :使用文件系统中的路径名作为标识(如 /tmp/mysock
)。
本地套接字实现tcp通信
创建本地套接字
绑定
监听
提取
读写
关闭
创建本地套接字用于tcp通信
1 2 3 4 5 int socket (int domain,int type,int protocol) ;参数: domain:AF_UNIX type:SOCK_STREAM protocol:0
绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <sys/socket.h> int bind (int sockfd, const struct sockaddr * addr, socklen_t addrlen) ;参数: sockfd:本地套接字 addr:本地套接字结构体地址 struct sockaddr_un { sa_family_t sun_family; char sun_path[108 ]; }; addrlen:sockaddr_un大小
提取
1 2 3 4 #include <sys/socket.h> int accept (int sockfd, struct sockaddr * _Nullable restrict addr, socklen_t * _Nullable restrict addrlen) ;addr:struct sockaddr_un 结构体来接
本地套接字实现tcp服务器和客户端代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <stdio.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/un.h> int main (int argc,char *argv[]) { unlink("sock.s" ); int lfd=socket(AF_UNIX,SOCK_STREAM,0 ); struct sockaddr_un myaddr ; myaddr.sun_family=AF_UNIX; strcpy (myaddr.sun_path,"sock.s" ); bind(lfd,(struct sockaddr*)&myaddr,sizeof (myaddr)); listen(lfd,128 ); struct sockaddr_un cliaddr ; socklen_t len=sizeof (cliaddr); int cfd=accept(lfd,(struct sockaddr*)&cliaddr,&len); printf ("new client file=%s\n" ,cliaddr.sun_path); char buf[1500 ]="" ; while (1 ){ int n=recv(cfd,buf,sizeof (buf),0 ); if (n<=0 ){ break ; }else { printf ("%s\n" ,buf); send(cfd,buf,n,0 ); } } close(cfd); close(lfd); }
1 2 3 4 new client file= hellop nc -U sock.s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <arpa/inet.h> #include <sys/socket.h> #include <stdlib.h> #include <string.h> #include <sys/un.h> #include <unistd.h> int main (int argc,char *argv[]) { unlink("sock.c" ); int cfd=socket(AF_UNIX,SOCK_STREAM,0 ); struct sockaddr_un myaddr ; myaddr.sun_family=AF_UNIX; strcpy (myaddr.sun_path,"sock.c" ); if (bind(cfd,(struct sockaddr*)&myaddr,sizeof (myaddr))<0 ){ perror("" ); return 0 ; } struct sockaddr_un seraddr ; seraddr.sun_family=AF_UNIX; strcpy (seraddr.sun_path,"sock.s" ); connect(cfd,(struct sockaddr *)&seraddr,sizeof (seraddr)); char buf[1500 ]="" ; while (1 ){ char buf[1500 ]="" ; int n=read(STDIN_FILENO,buf,sizeof (buf)); send(cfd,buf,n,0 ); memset (buf,0 ,sizeof (buf)); n=recv(cfd,buf,sizeof (buf),0 ); if (n<=0 ){ break ; } else { printf ("%s\n" ,buf); } } close(cfd); return 0 ; }
Socket编程 无名管道,命名管道,文件,信号,消息队列,共享内存只能用于本机的进程间通信。
不同主机间进程通信方法:socket
socket是一个伪文件。
socket必须成对出现。
大小端: 小端:低位存低地址,高位存高地址
大端:高位存低地址,低位存高地址
网络上走的数据都是大端的,主机的主机字节序是未知的,需要转换。
协议头部:如 TCP/IP 头、DNS 头等 → 这些字段是标准协议的,必须按规定的大端(网络字节序)
数据部分(Payload):完全由你应用程序自己定义
网络字节序和主机字节序的转换
1 2 3 4 5 6 #include <arpa/inet.h> uint32_t htonl (uint32_t hostlong) ;uint16_t htons (uint16_t hostshort) ;uint32_t ntohl (uint32_t netlong) ;uint16_t ntohs (uint16_t netshort) ;
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <arpa/inet.h> #include <stdio.h> int main (int argc,char *argv[]) { char buf[4 ]={192 ,168 ,1 ,2 }; int num=*(int *)buf; int sum=htonl(num); unsigned char *p=∑ printf ("%d %d %d %d\n" ,*p,*(p+1 ),*(p+2 ),*(p+3 )); unsigned short a=0x0102 ; unsigned short b=htons(a); printf ("%x\n" ,b); return 0 ; }
说明我的电脑是小端。
IP地址转换函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <arpa/inet.h> int inet_pton (int af,const char *src,void *dst) ;功能:将点分十进制串转成32 位网络大端的数据 参数:af: AF_INET IPV4 AF_INET6 IPV6 src:点分十进制串的首地址 dst:32 位网络数据的首地址 返回值: 成功:返回1 失败:返回0 const char *inet_ntop (int af,const void *src,char *dst,socklen_t size) ;功能:将32 位大端的网络数据转成点分十进制串 参数:af: AF_INET IPV4 AF_INET6 IPV6 src:32 位网络数据的首地址 dst: 点分十进制串的首地址 size:存储点分制串数组的大小 通常写16 返回值: 存储点分制串数组的首地址。 支持IPV4和IPV6。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> #include <arpa/inet.h> int main (int argc,char *argv[]) { char buf[]="192.168.1.4" ; unsigned int num=0 ; inet_pton(AF_INET,buf,&num); unsigned char *p=(unsigned char *)# printf ("%d %d %d %d\n" ,*p,*(p+1 ),*(p+2 ),*(p+3 )); char ip[16 ]="" ; inet_ntop(AF_INET,&num,ip,16 ); printf ("%s\n" ,ip); return 0 ; }
ipv4套接字结构体 1 2 3 4 5 6 7 8 9 10 struct sockaddr_in { sa_family_t sin_family; 对应的协议(ipv4) in_port_t sin_port; 端口 struct in_addr sin_addr ; ip地址 }; struct in_addr { uint32_t s_addr; ip地址 };
ipv6套接字结构体太多了,这里不展示
因为有这两种套接字结构体,所以我们要统一,方便函数编写。
通用套接字结构体
1 2 3 4 struct sockaddr { sa_family_t sa_family; char sa_data[14 ]; };
网络套接字函数 tcp 特点:出错重传 每次发送数据,对方都会回ACK 可靠
网络通信流程
创建套接字函数socket() 1 2 3 4 5 6 7 8 9 #include <sys/socket.h> int socket (int domain,int type,int protocol) ;功能:创建套接字 参数:domain:AF_INET type:SOCK_STREAM 流式套接字 用于TCP通信 protocol:0 返回值: 成功:返回文件描述符 失败:返回-1
连接服务器函数connect() 1 2 3 4 5 6 7 8 9 10 #include <sys/socket.h> int connect (int sockfd, const struct sockaddr * addr, socklen_t addrlen) ;功能:连接服务器 参数: sockfd:socket套接字 addr:ipv4套接字结构体地址 addrlen:ipv4套接字结构体的长度
tcp客户端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <arpa/inet.h> #include <sys/socket.h> #include <stdlib.h> #include <string.h> #include <stdio.h> #include <unistd.h> int main (int argc,char *argv[]) { int sock_fd; sock_fd=socket(AF_INET,SOCK_STREAM,0 ); if (sock_fd==-1 ){ printf ("连接失败" ); }else { printf ("连接成功" ); } struct sockaddr_in addr ; addr.sin_family=AF_INET; addr.sin_port=htons(8000 ); inet_pton(AF_INET,"127.0.0.1" ,&addr.sin_addr.s_addr); connect(sock_fd,(struct sockaddr *)&addr,sizeof (addr)); char buf[1024 ]="" ; while (1 ){ int n=read(STDIN_FILENO,buf,sizeof (buf)); write(sock_fd,buf,n); n=read(sock_fd,buf,sizeof (buf)); write(STDOUT_FILENO,buf,n); } close(sock_fd); return 0 ; }
tcp服务器通信流程
对于得到一个新的连接套接字(提取连接部分),服务器是要接受多个客户端连接的,所以先连接上,再分配新的连接和客户端通信。
bind函数 给套接字绑定固定的端口和ip
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <sys/socket.h> int bind (int sockfd, const struct sockaddr * addr, socklen_t addrlen) ;功能:给套接字绑定固定的端口和ip 参数: sockfd:套接字 addr:ipv4套接字结构体地址 addrlen:ipv4结构体的大小 返回值: 成功:返回0 失败:返回-1
listen函数 1 2 3 4 5 6 7 8 #include <sys/socket.h> int listen (int sockfd, int backlog) ;功能: 监听套接字 参数: sockfd:套接字 backlog:已完成连接队列和未完成连接队列之和的最大值 一般写128
accept函数 1 2 3 4 5 6 7 8 9 10 11 12 #include <sys/socket.h> int accept (int sockfd, struct sockaddr * _Nullable restrict addr, socklen_t * _Nullable restrict addrlen) ;功能: 从已完成连接队列提取新的连接,如果没有新的连接,accept会阻塞。 参数: sockfd:套接字 addr:获取的客户端的ip和端口信息 ipv4套接字结构体 addrlen:ipv4套接字结构体的大小的地址 返回值: 新的已连接套接字的文件描述符
tcp服务器代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <stdio.h> #include <arpa/inet.h> #include <sys/socket.h> #include <unistd.h> int main (int argc,char *argv[]) { int lfd=socket(AF_INET,SOCK_STREAM,0 ); struct sockaddr_in addr ; addr.sin_family=AF_INET; addr.sin_port=htons(8000 ); inet_pton(AF_INET,"127.0.0.1" ,&addr.sin_addr.s_addr); int ret=bind(lfd,(struct sockaddr *)&addr,sizeof (addr)); if (ret<0 ){ perror("bind" ); exit (0 ); } listen(lfd,128 ); struct sockaddr_in cliaddr ; socklen_t len=sizeof (cliaddr); int cfd=accept(lfd,(struct sockaddr *)&cliaddr,&len); char ip[16 ]="" ; printf ("new client ip=%s port=%d\n" ,inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16 ),ntohs(cliaddr.sin_port)); char buf[1024 ]="" ; while (1 ){ bzero(buf,sizeof (buf)); int n=read(STDIN_FILENO,buf,sizeof (buf)); write(cfd,buf,n); int readn=read(cfd,buf,sizeof (buf)); if (readn==0 ){ printf ("客户端关闭" ); } printf ("%s\n" ,buf); } close(lfd); close(cfd); return 0 ; }
1 2 3 nc 127.0.0.1 8000 hello world
1 2 3 new client ip=127.0.0.1 port=52486 hello world
多进程实现并发服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include "wrap.h" void free_process (int sig) { pid_t pid; while (1 ){ pid=waitpid(-1 ,NULL ,WNOHANG); if (pid<=0 ){ break ; printf ("haha" ); }else { printf ("child pid =%d" ,pid); } } } int main (int argc, char *argv[]) { sigset_t set ; sigemptyset(&set ); sigaddset(&set ,SIGCHLD); sigprocmask(SIG_BLOCK,&set ,NULL ); int lfd = tcp4bind(8000 , NULL ); Listen(lfd, 128 ); struct sockaddr_in cliaddr ; socklen_t len = sizeof (cliaddr); while (1 ) { char ip[16 ] = "" ; int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len); printf ("new client ip=%s port=%d\n" , inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16 ), ntohs(cliaddr.sin_port)); pid_t pid; pid = fork(); if (pid < 0 ) { perror("fork:" ); exit (0 ); } else if (pid == 0 ) { close(lfd); while (1 ) { char buf[1024 ] = "" ; int n = read(cfd, buf, sizeof (buf)); if (n < 0 ) { perror("read:" ); close(cfd); exit (0 ); } else if (n == 0 ) { printf ("client close\n" ); close(cfd); exit (0 ); } else { printf ("%s\n" , buf); write(cfd, buf, n); } } } else { close(cfd); struct sigaction act ; act.sa_flags=0 ; act.sa_handler=free_process; sigemptyset(&act.sa_mask); sigaction(SIGCHLD,&act,NULL ); sigprocmask(SIG_UNBLOCK,&set ,NULL ); } } return 0 ; }
我这里不知道为什么在子进程结束后回调函数没有触发。
多线程实现并发服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <stdio.h> #include <pthread.h> #include "wrap.h" typedef struct c_info { int cfd; struct sockaddr_in cliaddr ; }CINFO; void *client_fun (void *arg) ;int main (int argc,char *argv[]) { if (argc<2 ){ printf ("argc<2\n" ); return 0 ; } pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); short port=atoi(argv[1 ]); int lfd=tcp4bind(port,NULL ); Listen(lfd,128 ); struct sockaddr_in cliaddr ; socklen_t len=sizeof (cliaddr); CINFO *info; while (1 ){ int cfd=Accept(lfd,(struct sockaddr *)&cliaddr,&len); pthread_t pthid; info=malloc (sizeof (CINFO)); info->cfd=cfd; info->cliaddr=cliaddr; pthread_create(&pthid,NULL ,client_fun,info); } return 0 ; } void *client_fun (void *arg) { CINFO *info=(CINFO *)arg; char buf[16 ]="" ; printf ("new client ip=%s port=%d\n" ,inet_ntop(AF_INET,&(info->cliaddr.sin_addr.s_addr),buf,16 ),ntohs(info->cliaddr.sin_port)); while (1 ){ char buf[1024 ]="" ; int count=0 ; count=read(info->cfd,buf,sizeof (buf)); if (count<0 ){ perror("read:" ); break ; }else if (count==0 ){ printf ("client close\n" ); break ; }else { printf ("%s\n" ,buf); write(info->cfd,buf,count); } } close(info->cfd); free (info); }
高并发服务器 为了实现更好的并发:以下由三种方法
阻塞等待:消耗资源,效率不高
非阻塞忙轮询:消耗cpu
多路IO转接(多路IO复用):这种较为理想,epoll,select,poll都是 依赖 Linux 内核事件机制 实现的 I/O 多路复用模型,他们可以监听到任意socket的读写缓冲区发生了变化,应用层进而去处理。这样实现高并发。
多路IO转接服务器也叫做多任务IO服务器,该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,而是由内核替应用程序监视
windows 使用select select跨平台
poll 用的较少
epoll linux
select
select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。
解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型 ,会大大降低服务器响应效率,不应在select上投入更多精力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <sys/select.h> typedef fd_set;int select (int nfds, fd_set * _Nullable restrict readfds, fd_set * _Nullable restrict writefds, fd_set * _Nullable restrict exceptfds, struct timeval * _Nullable restrict timeout) ;功能:监听多个文件描述符的属性变化(读,写,异常) 参数:nfds:最大文件描述符+1 readfds:需要监听的读的文件描述符存放集合 writefds:需要监听的写的文件描述符存放集合 NULL exceptfds:需要监听的异常的文件描述符存放集合 NULL timeout:多长时间监听一次 固定的时间,限时等待 NULL 永久监听 返回值:返回的是变化的文件描述符的个数,变化的文件描述符会在readfds保存,但是没有变化的文件描述符会被删掉。 struct timeval { time_t tv_sec; suseconds_t tv_usec; }; void FD_CLR (int fd, fd_set * set ) ;int FD_ISSET (int fd, fd_set * set ) ;void FD_SET (int fd, fd_set * set ) ;void FD_ZERO (fd_set * set ) ;
基于select的并发服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <stdio.h> #include <sys/select.h> #include <sys/types.h> #include <unistd.h> #include "wrap.h" #include <sys/time.h> #define PORT 8888 int main (int argc,char *argv[]) { int lfd=tcp4bind(PORT,NULL ); Listen(lfd,128 ); int maxfd=lfd; fd_set oldset,rset; FD_ZERO(&oldset); FD_ZERO(&rset); FD_SET(lfd,&oldset); while (1 ){ rset=oldset; int n=select(maxfd+1 ,&rset,NULL ,NULL ,NULL ); if (n<0 ){ perror("select:" ); break ; }else if (n==0 ){ continue ; }else { if (FD_ISSET(lfd,&rset)){ struct sockaddr_in cliaddr; socklen_t len=sizeof (cliaddr); char ip[16 ]="" ; int cfd=Accept(lfd,(struct sockaddr*)&cliaddr,&len); printf ("new client ip=%s port=%d\n" ,inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16 ),ntohs(cliaddr.sin_port)); FD_SET(cfd,&oldset); if (cfd>maxfd){ maxfd=cfd; } if (--n==0 ){ continue ; } } for (int i=lfd+1 ;i<=maxfd;i++){ if (FD_ISSET(i,&rset)){ char buf[1500 ]="" ; int ret=Read(i,buf,sizeof (buf)); if (ret<0 ){ perror("Read" ); close(i); FD_CLR(i,&oldset); continue ; }else if (ret==0 ){ printf ("client close\n" ); close(i); FD_CLR(i,&oldset); }else { printf ("%s\n" ,buf); Write(i,buf,ret); } } } } } }
1 2 3 4 new client ip=127.0.0.1 port=54938 hello client close
select的优缺点:
优点:跨平台
缺点:
文件描述符1024的限制 由于FD_SETSIZE的限制。只是返回变化的文件描述符的个数,具体是哪些变化,需要遍历。
每次都需要将需要监听的文件描述符集合由应用层拷贝到内核,也比较费时间。
当大量并发,少量活跃,select效率低。
假设现在4-1023个文件描述符需要监听,但是5-1000这些文件描述符关闭了?
列一个数组存储监听的文件描述符列表,文件描述符关闭就把他的值从数组覆盖掉,再确认那些文件描述符读或写,直接遍历这个数组,效率高些,但是还是很一般。
其实我们可以在文件描述符表把后面的文件描述符提前,让文件描述符更紧凑,这样效率更高,我们可以使用dup2函数把前面空余的文件描述符复制后面的,再把后面的关掉。
假设4-1023个文件描述符需要监听,但是只有5,1002发来消息:无解
poll 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <poll.h> int poll (struct pollfd * fds, nfds_t nfds, int timeout) ;功能:监听多个文件描述符的属性变化 参数: fds:监听的数组首元素地址 nfds:数组有效元素的最大下标+1 timeout:超时时间 -1 是永久监听 >=0 限时等待 数组元素:struct pollfd struct pollfd { int fd; 需要监听的文件描述符 short events; 需要监听的文件描述符什么事件 POLLIN 读事件 POLLOUT 写事件 short revents; 返回监听到的事件 };
poll相对于select的优缺点:
优点:
没有文件描述符1024的限制
请求和返回是分离的
缺点和select一样:
每次都需要将需要监听的文件描述符从应用层拷贝到内核
每次都需要将数组的元素遍历一遍才知道那一个文件描述符变化了
大量并发,少量活跃,效率低。
epoll
红黑树的节点不只是文件描述符。
epoll API
1.创建红黑树
1 2 3 4 5 6 #include <sys/epoll.h> int epoll_create (int size) ;参数: size:监听的文件描述符上限,2.6 版本之后写1 即可会自动扩展 返回值: 返回树的句柄
2.上树 下树 修改节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <sys/epoll.h> int epoll_ctl (int epfd, int op, int fd, struct epoll_event * _Nullable event) ;参数: epfd:树的句柄 op:选项:EPOLL_CTL_ADD 上树 EPOLL_CTL_MOD 修改节点 EPOLL_CTL_DEL 下树 fd:上树下树的文件描述符 event:上树的节点 struct epoll_event { uint32_t events; 需要监听的事件 EPOLLIN 读事件 EPOLLOUT 写事件 epoll_data_t data; 需要监听的文件描述符 }; union epoll_data { void * ptr; int fd; uint32_t u32; uint64_t u64; }; typedef union epoll_data epoll_data_t ;
3.监听
1 2 3 4 5 6 7 8 9 10 11 #include <sys/epoll.h> int epoll_wait (int epfd, struct epoll_event * events, int maxevents, int timeout) ;功能:监听树上文件描述符的变化 参数: epfd:树的句柄 events:接收变化节点的数组的首地址 maxevents:数组元素的个数 timeout:-1 永久监听 大于等于0 限时等待 返回值: 返回的是变化的文件描述符个数
示例代码:父子进程通过管道通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/epoll.h> #include <sys/types.h> int main (int argc,char *argv[]) { int fd[2 ]; pipe(fd); pid_t pid; pid=fork(); if (pid<0 ){ perror("fork:" ); return 1 ; }else if (pid==0 ){ close(fd[0 ]); char buf[5 ]; char ch='a' ; while (1 ){ sleep(3 ); memset (buf,ch++,sizeof (buf)); write(fd[1 ],buf,5 ); } }else { close(fd[1 ]); int epfd=epoll_create(1 ); struct epoll_event ev ,evs [1]; ev.data.fd=fd[0 ]; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_ADD,fd[0 ],&ev); while (1 ){ int n=epoll_wait(epfd,evs,1 ,-1 ); if (n==1 ){ char buf[128 ]="" ; int ret=read(fd[0 ],buf,sizeof (buf)); if (ret<=0 ){ close(fd[0 ]); epoll_ctl(epfd,EPOLL_CTL_DEL,fd[0 ],&ev); break ; }else { printf ("%s\n" ,buf); } } } } return 0 ; }
1 2 3 4 5 6 7 8 aaaaa bbbbb ccccc ddddd eeeee fffff ggggg hhhhh
基于epoll的高并发服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include <stdio.h> #include "wrap.h" #include <sys/epoll.h> int main (int argc,char *argv[]) { int lfd=tcp4bind(8000 ,NULL ); Listen(lfd,128 ); int epfd=epoll_create(1 ); struct epoll_event ev ,evs [1024]; ev.data.fd=lfd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); while (1 ){ int nready=epoll_wait(epfd,evs,1024 ,-1 ); if (nready<0 ){ perror("epoll_wait:" ); break ; }else if (nready==0 ){ continue ; }else { for (int i=0 ;i<nready;i++){ if (evs[i].data.fd==lfd&&evs[i].events & EPOLLIN){ struct sockaddr_in cliaddr; char ip[16 ]="" ; socklen_t len=sizeof (cliaddr); int cfd=Accept(lfd,(struct sockaddr*)&cliaddr,&len); printf ("new client ip=%s port=%d\n" ,inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16 ),ntohs(cliaddr.sin_port)); ev.data.fd=cfd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev); }else if (evs[i].events & EPOLLIN) { char buf[1024 ]="" ; int n=read(evs[i].data.fd,buf,sizeof (buf)); if (n<0 ){ perror("read:" ); epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]); }else if (n==0 ){ printf ("client close" ); close(evs[i].data.fd); epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]); }else { printf ("%s\n" ,buf); write(evs[i].data.fd,buf,n); } } } } } return 0 ; }
1 2 new client ip=127.0.0.1 port=56746 hello
epoll的两种工作方式 在电路中,存在高电平和低电平,水平触发:持续的高电平或者低电平。边沿触发:电平有高到低的一个变化 或者由低到高的变化。对于epoll_wait的水平触发 LT,边沿触发 ET,对于读缓冲区使用水平触发,epoll_wait(系统调用)触发很频繁,所以使用边沿触发。写缓冲区一般用边沿触发
如何使用边沿触发呢,读缓冲区默认是水平触发。我们可以在上树时,需要设置需要监听的事件,再加上个EPOLLET
1 ev.events=EPOLLIN | EPOLLET
基于epoll的高并发服务器(加入边沿触发和优化代码) 将cfd设置边沿触发。设置边沿触发就要非阻塞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 #include <stdio.h> #include "wrap.h" #include <sys/epoll.h> #include <sys/fcntl.h> int main (int argc, char *argv[]) { int lfd = tcp4bind(8000 , NULL ); Listen(lfd, 128 ); int epfd = epoll_create(1 ); struct epoll_event ev , evs [1024]; ev.data.fd = lfd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); while (1 ) { int nready = epoll_wait(epfd, evs, 1024 , -1 ); printf ("epoll_wait .........................\n" ); if (nready < 0 ) { perror("epoll_wait:\n" ); break ; } else if (nready == 0 ) { continue ; } else { for (int i = 0 ; i < nready; i++) { if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN) { struct sockaddr_in cliaddr; char ip[16 ] = "" ; socklen_t len = sizeof (cliaddr); int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len); printf ("new client ip=%s port=%d\n" , inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16 ), ntohs(cliaddr.sin_port)); int flags = fcntl(cfd, F_GETFL); flags |= O_NONBLOCK; fcntl(cfd, F_SETFL, flags); ev.data.fd = cfd; ev.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); } else if (evs[i].events & EPOLLIN) { while (1 ) { char buf[4 ] = "" ; int n = read(evs[i].data.fd, buf, sizeof (buf)); if (n < 0 ) { if (errno==EAGAIN){ break ; } perror("read:" ); epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); close(evs[i].data.fd); break ; } else if (n == 0 ) { printf ("client close" ); close(evs[i].data.fd); epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); break ; } else { write(STDOUT_FILENO,buf,n); write(evs[i].data.fd, buf, n); } } } } } } return 0 ; }
epoll反应堆Reactor epoll反应堆就是把这三个东西:文件描述符 事件 回调函数封装在一起。
结构体来封装。
反应堆模式(Reactor Pattern)是一种事件驱动的设计模式,用于处理并发 I/O 操作。核心思想:
事件驱动 :等待多个事件(如网络连接的读写事件)发生;
事件分发 :事件就绪时,分发给相应的处理器执行;
非阻塞 I/O :避免线程阻塞,提高资源利用率。
线程池 线程池,事先创建几个线程,不停取任务,如果没有任务休眠,省去了不停的创建线程销毁线程的事件和资源
注意:线程池 处理的单个任务所需要处理的时间必须很短。
一个锁
两个条件变量
循环队列
libevent libevent
是一个 高性能事件通知库 ,用于在不同平台上进行 异步事件驱动编程 ,特别适合开发网络服务(如 HTTP 服务器、聊天服务等)。它封装了底层的 select
、poll
、epoll
、kqueue
等系统调用,提供了统一、跨平台的接口。
libevent事件触发流程
libevent的使用 1 2 3 4 5 6 7 8 9 10 11 创建event_base根节点 struct event_base *event_base_new (void ) ;返回值就是event_base根节点,因为libevent底层默认使用epoll,所以定义了一个自己封装的节点 释放根节点 void event_base_free (struct event_base *) ;循环监听 int event_base_dispatch (struct event_base *base) ;相当于while (1 ){epoll_wait()} 退出循环监听 int event_base_loopexit (struct event_base *base,const struct timeval*tv) ;int event_base_loopbreak (struct event_base *base) ;
1 2 3 4 5 6 7 8 9 10 初始化上树节点 struct event*event_new (struct event_base *base,evutil_socket_t fd, short events,event_callback_fn cb,void *arg) ;参数: base:event_base根节点 fd:上树的文件描述符 events:监听的事件 EV_TIMEOUT 超时事件 EV_READ 读事件 EV_WRITE 写事件 EV_SIGNAL 信号事件 EV_PERSIST 周期性触发 cb:回调函数 typedef void (*event_callback_fn) (evutil_socket_t fd,short events,void *arg) ; 返回值:初始化好的节点的地址
1 2 3 4 5 节点上树 int event_add (struct event*ev,const struct timeval *timeout) ;参数: ev:上树节点的地址 timeout:NULL 永久监听 固定时间 限时等待
1 2 3 4 节点下树 int event_del (struct event *ev) ;参数: ev:下树节点的地址
1 2 释放节点 void event_free (struct event *ev) ;
libevent编写tcp服务器代码 创建套接字
绑定
监听
创建event_base根节点
初始上树节点 lfd
上树
循环监听
收尾
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include "wrap.h" #include <event.h> void cfdcb (int cfd,short event,void *arg) { char buf[1500 ]="" ; int n=Read(cfd,buf,sizeof (buf)); if (n<=0 ){ perror("err or close" ); } else { printf ("%s\n" ,buf); Write(cfd,buf,n); } } void lfdcb (int lfd,short event,void *arg) { struct event_base *base = (struct event_base *)arg; int cfd=Accept(lfd,NULL ,NULL ); struct event *ev = event_new(base,cfd,EV_READ|EV_PERSIST,cfdcb,NULL ); event_add(ev,NULL ); } int main () { int lfd=tcp4bind(8000 ,NULL ); Listen(lfd,128 ); struct event_base *base = event_base_new(); struct event *ev = event_new(base,lfd,EV_READ|EV_PERSIST,lfdcb,base); event_add(ev,NULL ); event_base_dispatch(base); event_free(ev); close(lfd); return 0 ; }
*在下树的时候有问题,下树时需要所属的struct event ev,但是回调函数也是在event_new函数执行后注册,但是那个时候ev还没有生成,所以不能依靠传参,所以我们要定义一个全局数组,数组成员是自定义结构体包含fd和ev,ev生成后放进数组里,执行读回调后,通过fd查找对应得ev下树。
还有一种就是不用定义数组,直接自定义结构体包含fd和ev传过去。结构体在传之前先malloc,直接定义会因为函数结束而被释放。之前是因为修改的是ev,传的也是ev冲突了,这下改成传的是结构体指针,改的结构体的ev,这不冲突,所以可行。
bufferevent事件 普通的event事件 文件描述符 事件(底层缓冲区的读事件或者写事件)触发 回调
高级的event事件 bufferevent事件
bufferevent = 封装好的一套“异步读写 + 自动缓冲 + 错误处理 + 回调触发 + 高并发”解决方案,是 libevent 最推荐用的通信组件。
不同点
核心:一个文件描述符 两个缓冲区 三个回调
bufferevent监听流程
bufferevent API 1 2 3 4 5 6 7 8 9 10 11 12 创建新的节点 struct bufferevent *bufferevent_socket_new ( struct event_base *base, evutil_socket_t fd, int options ) ;参数: base:event_base 根节点 fd:要初始化上树的文件描述符 options:BEV_OPT_CLOSE_ON_FREE 在 bufferevent_free() 时自动关闭 socket(常用) BEV_OPT_THREADSAFE 让 bufferevent 在多线程中线程安全 返回值:新建节点的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 设置节点的回调 void bufferevent_setcb ( struct bufferevent *bev, bufferevent_data_cb readcb, bufferevent_data_cb writecb, bufferevent_event_cb eventcb, void *cbarg ) ;参数: bev:新建的节点的地址 readcb:读回调 writecb:写回调 eventcb:异常回调 cbarg:传给回调函数的参数 回调函数: typedef void (*bufferevent_data_cb) (struct bufferevent *bev, void *ctx) ; typedef void (*bufferevent_event_cb) (struct bufferevent *bev, short events, void *ctx) ; 事件回调 events会写明触发回调的原因
1 2 3 4 5 6 让事件使能 int bufferevent_enable (struct bufferevent *bev, short events) ;int bufferevent_disable (struct bufferevent *bev, short events) ;参数: bev:新建的节点的地址 events:使生效或使失效的事件 EV_READ EV_WRITE
1 2 3 发送数据 将数据写入 bufferevent 的输出缓冲区,异步发送(不会立即写到 socket 上)发送后会触发回调 int bufferevent_write (struct bufferevent *bev, const void *data, size_t size) ;
1 2 3 4 接受数据 size_t bufferevent_read (struct bufferevent *bev, void *data, size_t size) ;将从 bev 的读缓冲区中读取最多 size 字节的数据,复制到 data。同时将读到的数据从bufferevent的读缓冲区清除 返回实际读取的字节数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 连接侦听器 创建套接字 绑定 监听 提取 struct evconnlistener *evconnlistener_new_bind ( struct event_base *base, evconnlistener_cb cb, void *ctx, unsigned flags, int backlog, const struct sockaddr *sa, int socklen ) ;参数: base:根节点 cb:提取套接字cfd(连接到来)调用的回调 回调函数: typedef void evconnlistener_cb ( struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *addr, int socklen, void *ctx ) ; 参数: listener:连接侦听器的地址 fd:提取后的新文件描述符cfd addr:客户端的地址信息 socklen:addr的大小 ctx:evconnlistener_new_bind传给回调函数的参数 ctx:传给回调函数的参数 flags: LEV_OPT_CLOSE_ON_FREE 关闭时自动释放(推荐) LEV_OPT_REUSEABLE 设置 SO_REUSEADDR 端口复用(推荐) LEV_OPT_THREADSAFE 线程安全 LEV_OPT_LEAVE_SOCKETS_BLOCKING 文件描述符为阻塞的 backlog:-1 监听队列长度自动填充 sa:绑定的地址信息 socklen:sa的大小 返回值:连接侦听器的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 这个是用来写tcp客户端的 创建套接字 连接服务器 但是要先有节点 struct bufferevent *bufferevent_socket_new ( struct event_base *base, evutil_socket_t fd, int options ) ;这里fd为-1. int bufferevent_socket_connect ( struct bufferevent *bev, const struct sockaddr *address, int addrlen ) ;参数: bev:新建的节点 address:服务器的地址信息 addrlen:address长度
基于hello-world.c的服务端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 #include <string.h> #include <errno.h> #include <stdio.h> #include <signal.h> #ifndef _WIN32 #include <netinet/in.h> # ifdef _XOPEN_SOURCE_EXTENDED # include <arpa/inet.h> # endif #include <sys/socket.h> #endif #include <event2/bufferevent.h> #include <event2/buffer.h> #include <event2/listener.h> #include <event2/util.h> #include <event2/event.h> static const char MESSAGE[] = "Hello, World!\n" ;static const int PORT = 9995 ;static void listener_cb (struct evconnlistener *, evutil_socket_t , struct sockaddr *, int socklen, void *) ;static void conn_writecb (struct bufferevent *, void *) ;static void conn_eventcb (struct bufferevent *, short , void *) ;static void conn_readcb (struct bufferevent *, void *) ;static void signal_cb (evutil_socket_t , short , void *) ;int main (int argc, char **argv) { struct event_base *base ; struct evconnlistener *listener ; struct event *signal_event ; struct sockaddr_in sin = {0 }; #ifdef _WIN32 WSADATA wsa_data; WSAStartup(0x0201 , &wsa_data); #endif base = event_base_new(); if (!base) { fprintf (stderr , "Could not initialize libevent!\n" ); return 1 ; } sin .sin_family = AF_INET; sin .sin_port = htons(PORT); listener = evconnlistener_new_bind(base, listener_cb, (void *)base, LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE, -1 , (struct sockaddr*)&sin , sizeof (sin )); if (!listener) { fprintf (stderr , "Could not create a listener!\n" ); return 1 ; } signal_event = evsignal_new(base, SIGINT, signal_cb, (void *)base); if (!signal_event || event_add(signal_event, NULL )<0 ) { fprintf (stderr , "Could not create/add a signal event!\n" ); return 1 ; } event_base_dispatch(base); evconnlistener_free(listener); event_free(signal_event); event_base_free(base); printf ("done\n" ); return 0 ; } static void listener_cb (struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *sa, int socklen, void *user_data) { struct event_base *base = user_data; struct bufferevent *bev ; bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); if (!bev) { fprintf (stderr , "Error constructing bufferevent!" ); event_base_loopbreak(base); return ; } bufferevent_setcb(bev, conn_readcb, conn_writecb, conn_eventcb, NULL ); bufferevent_enable(bev, EV_WRITE | EV_READ); } static void conn_readcb (struct bufferevent *bev, void *user_data) { char buf[1500 ]="" ; int n=bufferevent_read(bev,buf,sizeof (buf)); printf ("%s\n" ,buf); bufferevent_write(bev, buf,n); } static void conn_writecb (struct bufferevent *bev, void *user_data) { struct evbuffer *output = bufferevent_get_output(bev); if (evbuffer_get_length(output) == 0 ) { } } static void conn_eventcb (struct bufferevent *bev, short events, void *user_data) { if (events & BEV_EVENT_EOF) { printf ("Connection closed.\n" ); } else if (events & BEV_EVENT_ERROR) { printf ("Got an error on the connection: %s\n" , strerror(errno)); } bufferevent_free(bev); } static void signal_cb (evutil_socket_t sig, short events, void *user_data) { struct event_base *base = user_data; struct timeval delay = { 2 , 0 }; printf ("Caught an interrupt signal; exiting cleanly in two seconds.\n" ); event_base_loopexit(base, &delay); }
webserver服务器流程
html html 超文本标签语句(超文本标记语言)
1 2 3 4 5 6 7 8 9 10 11 <html > <head > <meta http-equiv ="content-Type" content ="text/html;charset=utf8" > <title > 我是一个html</title > <body > <font size ='7' color ="red" > hello world</font > <br /> <font size ='7' color ="red" > hello world</font > </body > </head > </html >
HTTP协议 http请求
1 2 3 4 5 6 7 请求行:GET /demo.html HTTP/1.1\r\n 请求方式 /请求的内容 版本\r\n 请求头 空行\r\n 数据
部分
内容示例
请求行
GET /index.html HTTP/1.1
包含:方法、请求目标路径、HTTP版本号
请求头
一组键值对,每行一个,例如:Host: www.example.com``User-Agent: curl/7.81.0
空行
请求头与请求体之间必须有一个空行
请求体
(可选)提交的数据,比如表单、JSON、文件上传等
http应答
1 2 3 4 5 6 7 状态行:HTTP/1.1 200 OK\r\n 版本 状态码 状态信息\r\n 消息报头: 文件类型(必填的) 文件的长度(可填可不填,填了要求一定对) 空行\r\n 发送文件
部分
示例与说明
状态行
HTTP/1.1 200 OK
包含:协议版本、状态码、状态短语
响应头
键值对形式,如:Content-Type: text/html``Content-Length: 123
空行
用于分隔响应头和响应体
响应体
实际返回的内容,如网页HTML、JSON数据、图片等(可选)
状态码
状态码
名称
含义描述
200
OK
请求成功,服务器正常返回了请求的数据。
301
Moved Permanently
资源已永久移动到新位置,浏览器会自动跳转,搜索引擎会更新索引。
302
Found
资源临时移动,原 URL 仍然有效,浏览器会自动跳转。
400
Bad Request
请求语法错误或参数无效,服务器无法理解请求。
401
Unauthorized
请求需要身份认证(未登录或 token 无效)。
403
Forbidden
已认证但没有权限访问该资源。
404
Not Found
请求的资源不存在,可能是 URL 错误或资源已删除。
500
Internal Server Error
服务器内部错误,可能是代码异常或服务器故障。
读取目录下的所有文件名
以前用readdir函数,还有一个scandir
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <dirent.h> struct dirent { ino_t d_ino; off_t d_off; unsigned short d_reclen; unsigned char d_type; char d_name[256 ]; }; struct dirent **mylist ;int scandir (const char * restrict dirp, struct dirent ** * restrict namelist, int ( * filter)(const struct dirent * ), int ( * compar)(const struct dirent ** , const struct dirent ** )) ;参数: dirp:目录下的路径名 namelist:mylist地址 filter:过滤的函数入口地址 compar:排序函数入口地址 alphasort(字母排序) 返回值:读取文件的个数
webserver代码(基于epoll) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include "wrap.h" #include <sys/epoll.h> #include <sys/fcntl.h> #include <sys/stat.h> #include <dirent.h> #include <signal.h> #define PORT 8889 void send_header (int cfd, int code, char *info, char *filetype, int length) { char buf[1024 ] = "" ; int len = sprintf (buf, "HTTP/1.1 %d %s\r\n" , code, info); send(cfd, buf, len, 0 ); len = sprintf (buf, "Content-Type:%s\r\n" , filetype); send(cfd, buf, len, 0 ); if (length > 0 ) { len = sprintf (buf, "Content-Length:%d\r\n" , length); send(cfd, buf, len, 0 ); } send(cfd, "\r\n" , 2 , 0 ); } void send_file (int cfd, char *path, struct epoll_event *ev, int epfd, int flag) { int fd = open(path, O_RDONLY); if (fd < 0 ) { perror("" ); return ; } char buf[1024 ] = "" ; int len = 0 ; while (1 ) { len = read(fd, buf, sizeof (buf)); if (len < 0 ) { perror("" ); break ; } else if (len == 0 ) { break ; } else { send(cfd, buf, len, 0 ); } } close(fd); if (flag == 1 ) { close(cfd); epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, ev); } } void read_client_request (int epfd, struct epoll_event *ev) { char buf[1024 ] = "" ; char tmp[1024 ] = "" ; int n = Readline(ev->data.fd, buf, sizeof (buf)); if (n <= 0 ) { printf ("close or err\n" ); epoll_ctl(epfd, EPOLL_CTL_DEL, ev->data.fd, ev); close(ev->data.fd); return ; } printf ("%s\n" , buf); int ret = 0 ; while ((ret = Readline(ev->data.fd, tmp, sizeof (tmp))) > 0 ); char method[256 ] = "" ; char content[256 ] = "" ; char protocol[256 ] = "" ; sscanf (buf, "%[^ ] %[^ ] %[^ \r\n]" , method, content, protocol); printf ("[%s] [%s] [%s]\n" , method, content, protocol); if (strcasecmp(method, "get" ) == 0 ) { char *strfile = content + 1 ; if (*strfile == 0 ) { strfile = "./" ; } struct stat s ; if (stat(strfile, &s) < 0 ) { printf ("file not found\n" ); send_header(ev->data.fd, 404 , "Not Found" , "/text/html" , 0 ); send_file(ev->data.fd, "client.c" , ev, epfd,1 ); } else { if (S_ISREG(s.st_mode)) { printf ("file\n" ); send_header(ev->data.fd, 200 , "ok" , "/text/html" , 0 ); send_file(ev->data.fd, strfile, ev, epfd,1 ); } else if (S_ISDIR(s.st_mode)) { printf ("dir\n" ); send_header(ev->data.fd, 200 , "ok" , "/text/html" , 0 ); send_file(ev->data.fd, "dir_header.html" , ev, epfd,0 ); struct dirent **mylist =NULL ; int n=scandir(strfile,&mylist,NULL ,alphasort); char buf[1024 ]="" ; int len=0 ; for (int i=0 ;i<n;i++){ printf ("%s\n" ,mylist[i]->d_name); if (mylist[i]->d_type==DT_DIR){ len=sprintf (buf,"<li><a href=%s/ >%s</a><li>" ,mylist[i]->d_name,mylist[i]->d_name); }else { len=sprintf (buf,"<li><a href=%s>%s</a><li>" ,mylist[i]->d_name,mylist[i]->d_name); } send(ev->data.fd,buf,len,0 ); free (mylist[i]); } free (mylist); send_file(ev->data.fd, "dir_tail.html" , ev, epfd,1 ); } } } } int main () { signal(SIGPIPE,SIG_IGN); char pwd_path[256 ] = "" ; char *path = getenv("PWD" ); strcpy (pwd_path, path); strcat (pwd_path, "/web-http" ); chdir(pwd_path); int lfd = tcp4bind(PORT, NULL ); Listen(lfd, 128 ); int epfd = epoll_create(1 ); struct epoll_event ev , evs [1024]; ev.data.fd = lfd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); while (1 ) { int nready = epoll_wait(epfd, evs, 1024 , -1 ); if (nready < 0 ) { perror("" ); break ; } else { for (int i = 0 ; i < nready; i++) { if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN) { struct sockaddr_in cliaddr; char ip[16 ] = "" ; socklen_t len = sizeof (cliaddr); int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len); printf ("new client ip=%s port=%d\n" , inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16 ), ntohs(cliaddr.sin_port)); int flag = fcntl(cfd, F_GETFL); flag |= O_NONBLOCK; fcntl(cfd, F_SETFL, flag); ev.data.fd = cfd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); } else if (evs[i].events & EPOLLIN) { read_client_request(epfd, &evs[i]); } } } } }
这个代码还有URL转码,函数没有实现。发送大文件,无法传输的问题,已写明解决方法(基于bufferevent)。
为什么服务端 send()
写不进去,错误是“写缓冲区满”,难道不是因为“客户端浏览器的读缓冲区满”吗?
正确解释如下:send() 写的是 内核发送缓冲区
当你调用 send(fd, buf, len, 0)
时:
数据不是直接“发到浏览器”,而是先写入 服务端内核的 socket 发送缓冲区 ;
只有写进这个缓冲区成功,send()
才会返回你“写成功了”;
如果这个缓冲区满了,send()
就会返回 -1
,并设置 errno = EAGAIN
。
这就是“写缓冲区满”的真正含义 —— 是你本地操作系统的内核缓冲区满了 ,跟浏览器还没关系。
完结撒花