第十一章 网络基础
1. 网卡、MAC地址、IP地址
网卡:网络适配器,用于收发数据
1)MAC地址(6个字节48位)
网卡物理地址,是唯一的、不变的,标识网卡的id,使用ifconfig命令后如下图,一般用来找到主机
2)IP地址
IP地址是标识主机的id,是虚拟的,会改变。ipv4有32位,ipv6有128位。
IP分为子网id(网段)和主机id,需要和子网掩码一起来看,被子网掩码中连续的1覆盖的是子网id,被连续的0覆盖的是主机id。
- 10.1.1.2和255.255.255.0
3)lo本地回环
ping命令用于测试两台主机的连通性
Linux设置IP:ifconfig [网卡名称] [IP地址] netmask [子网掩码]
功能:测试本机网络配置,能ping通127.0.0.1说明本机网卡和IP协议安装无误
注意:127.0.0.1~127.255.255.254中的任何地址都将回环到本地主机中
4)IP地址分类
A类:默认8bit子网ID,第一位为0(广域网)1.0.0.0 到 126.255.255.255
B类:默认16bit子网ID,前两位为10(城域网)128.0.0.0 到 191.255.255.255
C类:默认24bit子网ID,前三位为110(局域网)192.0.0.0 到 223.255.255.255
D类:前四位为1110,多播地址
E类:前五位为11110,保留为今后使用(A、B、C类最常用)
5)其他
桥接模式:直接连接物理网络,会给虚拟机分配IP
NAT模式:共享主机的IP,使用虚拟网络,只与主机通信
2. 端口
标识某应用程序(进程)的缓冲区,在主机间通信时会使用,每次启动进程时端口号是不会变的,一个进程可能有多个端口
port:两个字节0~65535(0~1023 知名端口,减少使用)
FTP:21,HTTP:80等
3. OSI七层模型(Open System Internet)、TCP/IP四层模型
1)“物数网传会表应”
物理层:双绞线(网线)接口类型,光纤的传输速率等
数据链路层:mac负责收发数据
网络层:ip给两台主机提供连接和路径选择
传输层:port区分数据递送到哪一个应用程序(进程)
会话层:建立连接
表示层:解码
应用层:应用程序获取数据
2)在实际开发中,使用TCP/IP四层模型
网络接口层:也叫链路层,对应“物数”
网络层:对应“网”
传输层:对应“传”
应用层:对应“会表应”
4. 协议及一些协议的报头
规定数据传输的方式和格式
应用层:FTP文件传输协议、HTTP超文本传输协议、NFS网络文件系统
传输层:TCP传输控制协议(丢包重传)、UDP用户数据报协议(高效但不重传)
网络层:IP因特网互联协议、ICMP因特网控制报文协议(ping)、IGMP因特网组管理协议
链路层:ARP地址解析协议(找mac地址)、RARP反向地址解析协议(通过mac找ip)
对应的一些单词:File Transfer Protocol、Hyper Text Transfer Protocol、Network File System;Transmission Control Protocol、User Datagram Protocol;Internet Protocol、Internet Control Message Protocol、Internet Group Management Protocol;Address Resolution Protocol、Reverse Address Resolution Protocol
1)UDP报头(8字节)
2)TCP报头(20字节)
SYN:同步序列号标志位,tcp三次握手中,第一次会将SYN=1 ACK=0,表示这是一个连接请求报文段,对方会将SYN=1,ACK=1,表示同意连接,连接完成之后将SYN=0。
FIN:在tcp四次挥手时第一次将FIN=1,表示此报文段的发送方数据已经发送完毕,这是一个释放链接的标志。
ACK:当ACK=1时,确认序列号ack有效,TCP规定:所有建立连接的ACK必须全部置为1。
序号(seq):占32位4个字节,序号范围 ,序号增加到
后,下个序号又回到 0。TCP 是面向字节流的,通过 TCP 传送的字节流中的每个字节都按顺序编号,而报头中的序号字段值则指的是本报文段数据的第一个字节的序号。例如:我们的seq = 201,携带的数据有100,那么最后一个字节的序号就为300,那么下一个报文段就应该从301开始。
确认号(ack):占32位4个字节,期望收到对方下个报文段的第一个数据字节的序号。当标志位ACK值为1时,才能产生有效的确认号ack。并且:ack=seq+1。
RST:当RST=1时,表明TCP连接出现严重错误,此时必须释放连接,之后重新连接,又叫重置位。
URG:紧急指针标志位,当URG=1时,通知系统中有紧急数据,应当尽快传送,这时会将紧急数据插入到本报文段数据的最前面。
PSH:推送操作,提示接收端应用程序立即从TCP缓冲区把数据读走。
3)IP报头(20字节)
TTL:数据生存时间,一般是64或128,经过一个路由器就会减1,防止网络阻塞
4)MAC头部(链路层)(14个字节)
5. ARP地址解析协议
ARP地址解析协议:通过IP找MAC
ARP请求包:
6. B/S和C/S
1)B/S模式
性能较低,服务器做计算,客户端安全,开发周期短
2)C/S模式
性能较好,本地做计算,但客户端容易篡改数据,开发周期长
第十二章 Socket和TCP
1. 进程间通信的回顾
无名管道、有名管道、mmap映射、文件、信号、消息队列、进程间共享内存,这些只能用于本机的进程间通信。而socket解决的是不同主机进程间通信问题。
2. socket套接字
socket总是成对出现(pair),是一个伪文件
3. 字节序
1)大小端
小端:低位存低地址,高位存高地址
大端:低位存高地址,高位存低地址
(图中的L、H是地址的高低)
单字节的数据不用转换
4. 字节序转换函数
转换的时候注意长度,比如IPv4是4字节,端口号是2字节
5. 点分十进制串转换(IP地址的转换)
支持IPv4和IPv6
1)inet_pton()函数
将点分十进制串 转成32位网络大端的数据
参数:
af:AF_INET IPv4,AF_INET6 IPv6
src:点分十进制串的首地址
dst:32位网络数据的地址
返回值:1成功
2)inet_ntop()函数
将32位网络大端数据转成十进制串
参数:
af:AF_INET
src:32位大端网络数据的地址
dst:存储点分十进制串的地址
size:字符串大小(INET_ADDSRTLEN宏的值16用于IPv4)
返回值:存储点分十进制串的首地址
6. IPv4结构体、通用套接字结构体
协议、端口、IP地址(使用man 7 ip查看)
sin_family:协议 AF_INET
sin_port:端口
sin_addr:IP地址
在使用send函数时,由于IPv4和IPv6结构体不同,需要使用到通用套接字结构体
7. TCP通信
Transmission Control Protocol传输控制协议
特点:出错重传,每次发送数据对方都会回ACK(确认包),可靠
1)创建套接字(使用man socket 2查看)
参数:
domain:协议,AF_INET
type:套接字通信类型(原始套接字通信,UDP,TCP),SOCK_STREAM流式套接字(TCP),SOCK_DGRAM(UDP),SOCK_RAW(原始,要组深层的包)
protocol:0
返回值:成功返回文件描述符,失败返回-1
2)连接服务器(使用man connect 2查看)
参数:
sockfd:套接字文件描述符
addr:通用套接字结构体(IPv4和IPv6需要类型转换)
addrlen:套接字结构体长度
返回值:0成功,-1失败
8. TCP服务器通信
1)服务器和客户端通信流程
- 创建套接字
- 给套接字绑定,bind的内容有IP地址、端口号(固定的)
- 监听,①将套接字由主动变被动(变为监听套接字);②创建连接队列(三次握手后放到已完成连接队列)
- 提取连接Accept(),从已完成连接队列提取连接得到一个新的已连接套接字,后面用此套接字和客户端通信
- 读写
- 关闭
2)bind()函数(使用man bind 2查看)
参数:
sockfd:套接字文件描述符
addr:通用套接字结构体地址(注意转型)
addrlen:套接字大小
返回值:成功返回0,失败返回-1
3)listen()函数
参数:
sockfd:套接字文件描述符
backlog:Max(已完成连接队列容量+未完成连接队列容量),此处可填128
4)accept()函数
功能:从已完成连接队列提取新的连接
参数:
socket:套接字文件描述符
address:获取客户端的IP信息和端口信息,通用套接字结构体
address_len:客户端的结构体的大小,可以声明一个socklen_t类型变量来容纳
返回值:成功,返回新的已连接套接字的文件描述符
9. 粘包及其处理方法
服务器在客户端读取前,发送了两次数据到客户端的缓冲区,导致客户端无法区分。
解决方案:
- 报头+数据:报头可以用四个字节长度:例如:0010(字节)字符串转成整型长度,四个字节长度+数据部分。(内容也有细则,例如每一部分有多长,代表什么数据)
- 添加结尾标记。(不建议用,效率低)
- 发送定长数据:数据包定长。(数据量不一样)
10. 包裹函数
对Socket通信所用到的一些函数的封装,详见下面一篇文章
socket编程中函数封装的思想,异常处理和wrapSocket.c-CSDN博客https://blog.csdn.net/m0_75034791/article/details/135266762
11. TCP通信时序(三握四挥)
1)三次握手
简单说,三次握手就是建立连接的过程,而四次挥手就是断开连接的过程。
先回看下TCP协议报头:
SYN:请求报文段,ACK:确认报文段,序列号:图中seq
确认序列号(图中ack)的含义:1. 确认收到对方的报文;2. 期望下一次对方的序列号为我的确认序列号。
确认序列号 = 对方发过来的序列号 + 标志位长度SYN(1) + 数据长度
问题1:如果客户端发送SYN后,客户端不反回ACK?
未完成连接队列会持续被占用,如果占满,那么服务器就再收不到请求了,这也是早期SYN Flood攻击(DDos攻击的一种)的原理。
问题2:为什么不是两次握手?
因为SYN为1的报文段按规范不能携带数据,所以在前两次握手后,目前服务器只知道客户端可以发送、自己可以收发,而不知道客户端是否能正常接收,所以需要第三次握手。
如图:
初始时:客户端处于Closed状态,服务器处于Listen状态;
第一次握手:客户端发送SYN报文给服务器,初始序列号为x(seq=x), 此时客户端进入SYN_SENT状态;客户端知道自己发送正常,服务器知道自己接收正常,客户端发送正常。
第二次握手:服务器通过自己的SYN报文给与客户端确认和响应,服务器进入SYN_RECV状态;客户端可以知道服务器收发正常,自己收发正常;服务器知道自己收发正常,但不知道客户端接收正常,因此需要第三次握手。服务器发送报文的四个参数具体含义如下:
SYN=1,表示为连接请求报文,也不能携带数据;
seq=y,服务端的序列号为y;
ACK=1,表示确认客户端序列号有效,此时确认号(ack)有值;
ack=seq+1:ack的值为客户端传来的序列号(seq)加1,即ack=x+1;
第三次握手:客户端收到服务器的SYN+ACK的包,此时客户端处于ESTABLISHED(已确认)状态,表示客户端和服务器都表示同意连接,因此客户端发送一个ACK报文,ack仍为序列号+1,即y+1,初始序列号为x,因此客户端发送的第二次报文,序列号要+1,即x+1;这时服务器可以确认客户端收发正常;第三次握手可以携带数据。
按RFC 793(Transmission Control Protocol)的规定,SYN=1的报文不能携带数据。
2)四次挥手
与三次握手不同的是,断开连接的报文由客户端或服务器发起都是可能的。(上图以客户端请求断开为例)
close()关闭的是应用层发讯,而其他层还可以收发。
问题1:客户端在发送ACK后等待2MSL才会关闭所有层?
主动请求断开的一方会等待2MSL。
在第三次挥手之后,服务器收到客户端ACK才会关闭,而此时客户端会等待两倍的最大报文生存时长(2*MSL),以等待服务器是否会发送FIN报文(再次发FIN一般是没收到客户端ACK)。
问题2:为什么挥手比握手多一次?
握手时没有数据传输,但挥手时有数据传输,所以SYN和ACK可以一起发送,而FIN和ACK不能,(在最后的数据传输完后,才会发送FIN)。
问题3:为什么三次挥手不行?
因为在服务器接收到FIN后,会等到服务器所有报文发完了才会发送FIN,所以会先发一个ACK,让客户端知道服务区接受了它的FIN报文,等其他报文发完,服务器才会发送FIN给客户端。
3)半连接队列、全连接队列
完成第一次和第二次握手之后,将socket放到半连接队列;
完成三次握手之后,socket会从半连接队列移动到全连接队列;在调用accept()之后,会创建新的socket来通信。
4)半双工、全双工
全双工:客户端在给服务器发送消息的同时,服务器也可以给客户端发送消息。
半双工:可以互相发,但不能同时发。
12. 滑动窗口
滑动窗口(Sliding window)是一种流量控制技术。即接收方会告诉发送方在某一时刻能送多少包(win窗口尺寸),当滑动窗口为0时,发送方一般不能再发送数据。
注意:通信双方都有发送缓冲区和接收缓冲区TCP重传机制、滑动窗口、拥塞控制https://blog.csdn.net/m0_75034791/article/details/136574036
13. 多进程实现并发服务器
父进程fork之后,只保留父进程的监听套接字,只保留每个子进程中的已连接套接字。回收子进程资源的时候将涉及到SIGCHLD。
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include "wrapSocket.h"
#include <signal.h>
#include <sys/wait.h>
void free_process(int sig)
{
pid_t pid;
while(1)
{
waitpid(-1, NULL, WNOHANG);
if(pid <= 0)//没有等待的子进程或者没有进程退出(没有要回收的)
{
break;
}
else
{
printf("child pid = %d\n",pid);
}
}
}
int main ()
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_BLOCK,&set,NULL);
//创建套接字,绑定
int lfd = tcp4bind(8080,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("");
return 0;
}
else if (pid == 0)//子进程
{
//关闭lfd
close(lfd);
while (1)
{
char buf[1024] = "";
int n = read(cfd,buf,sizeof(buf));
if(n < 0)
{
perror("");
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);
//回收子进程资源,注册SIGCHLD信号的回调
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;
}
14. 多线程实现并发服务器
#include <stdio.h>
#include <pthread.h>
#include "wrapSocket.h"
typedef struct client_info
{
int cfd;
struct sockaddr_in client_addr;
}CINFO;
void* client_func(void *arg);
int main(int argc, char *argv[])
{
if(argc < 2)
{
printf("argc < 2???\n ./a.out 8080 \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 tid;
info = malloc(sizeof(CINFO));
info->cfd = cfd;
info->client_addr = cliaddr;
pthread_create(&tid,&attr,client_func,info);
}
return 0;
}
void* client_func(void *arg)
{
CINFO *info = (CINFO *)arg;
char ip[16] = "";
printf("new client ip=%s port=%d\n",
inet_ntop(AF_INET,&(info->client_addr.sin_addr.s_addr),ip,16),
ntohs(info->client_addr.sin_port));
while(1)
{
char buf[1024] = "";
int count = 0;
count = read(info->cfd,buf,sizeof(buf));
if(count < 0)
{
perror("");
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);
}
在主线程提取已连接套接字cfd时,要注意用于存放client信息的结构体存放的位置,因为每个子线程创建需要时间,而他们的栈区又是不共享的,为了不让主线程在子线程创建前就拷贝其中的信息(可能会覆盖原来的),所以要把这个结构体存放在堆区,并且记得最后手动释放。
第十三章 TCP状态转移和IO多路复用
1. TCP状态转换图、netstat命令
netstat命令的使用
netstat -a #列出所有端口
netstat -at #列出所有的TCP端口
netstat -au #列出所有的UDP端口
netstat -l #只显示监听端口
netstat -s #显示所有端口的统计信息
netstat -p #显示PID和进程名称
netstat -n #直接使用IP地址,而不通过域名服务器
2. 半关闭、2MSL
1)半关闭
在FIN_WAIT2时,主动方应用层写端关闭。
在请求断开连接时,系统会进行相关的处理,但是如果要自己实现,则需要调用函数:
2)2MSL
2MSL(Maximum Segment Lifetime)TIME_WAIT状态,通常为几分钟。
让4次握手的关闭流程更加可靠,在主动方发送FIN报文并收到被动方ACK报文和FIN报文并发送ACK后(即第四次握手后),会进入TIME_WAIT状态,等待2MSL。此举是为了确保被动方收到了最后一次ACK报文(没收到的话被动方会重复发送FIN报文)。保持2MSL是确保最后一次的ACK能最大可能被接收到。
3. 心跳包
场景:如果对方异常断开,本机检测不到,一直等待,浪费资源
需要设置TCP的保持连接,作用就是每隔一段时间发送探测分节,如果连续发送多个探测分节对方还未回,就将此连接断开。
心跳包:最小粒度(携带数据要少)
乒乓包:携带比较多数据的心跳包
1)设置套接字选项函数setsockopt()
功能:设置与某个套接字关联的选项
参数:
sock:将要被设置或获取选项的套接字
level:选项所在的协议层,SOL_SOCKET(通用套接字选项)等
optname:需要访问的选项名
optval:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值得缓冲。
optlen:对于get方法,作为入口参数时,选项值的最大长度。作为出口参数时,选项值得实际长度。
返回值:成功返回0,异常返回-1和errno。
2)设置心跳包(有更高需求一般需要自己编写)
SO_KEEPALIVE保持连接检测对方主机是否崩溃,避免阻塞。设置该选项后,如果2个小时内在此套接字接口的任一方向都没有数据交换,TCP就自动给对方发一个保持存活探测分节(keepalive probe)。正常:ACK响应;对方已崩溃且已重新启动:RST响应
4. 端口复用
端口重新启用,也是调用setsockopt()函数(1启用,0禁用)。
在上方写的wrapSocket.c中的tcp4bind()函数中也封装了这两行。
5. 多路IO转接技术(高并发服务器)
1)一些实现高并发的方法
①阻塞等待:
比如一个进程服务一个客户端,客户端不发数据的时候就阻塞等待,很浪费资源。
②非阻塞忙轮询:
消耗CPU,一个进程负责所有,包括监听和收发和多个客户端之间的数据。
③多路IO转接(多路IO复用):
通过内核,poll、epoll、select三种多路IO转接技术,监听文件描述符中的读写缓冲区属性变化。
(epoll linux,windows select跨平台,poll较少用)
6. selectAPI
1)存放在PCB块的文件描述符表
2)select函数
功能:监听多个文件描述符的属性变化(读、写、异常)
参数:
nfds:最大文件描述符+1
readfds:需要监听的读的文件描述符集合
writefds:需要监听的写的文件描述符的集合 NULL
exceptfds:需要监听的异常的文件描述符的集合 NULL
timeout:多长时间监听一次,固定的时间,限时等待,NULL永久监听
返回值:返回变化的文件描述符的个数
注意:变化的文件描述符会存在监听的集合中,为变化的文件描述符会被删除。
7. select的代码实现、优缺点
1)代码实现
#include <stdio.h>
#include <sys/select.h>
#include <sys/types.h>
#include <unistd.h>
#include "wrapSocket.h"
#include <sys/times.h>
#define PORT 8080
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);
//将lfd添加到oldset集合中
FD_SET(lfd,&oldset);
while(1)
{
rset = oldset;//将oldset赋值给需要监听的rset
int n = select(maxfd+1,&rset,NULL,NULL,NULL);
if(n < 0)
{
perror("");
break;
}
else if (n == 0)
{
continue;//没有变化,返回重新监听
}
else//监听到了文件描述符的变化
{
//lfd变化:有新连接
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));
//将cfd添加至oldset集合中,以用来下次监听
FD_SET(cfd,&oldset);
//更新maxfd
if(cfd > maxfd)
maxfd = cfd;
//如果只有lfd变化,则continue
if(--n == 0)
continue;
}
//cfd变化:有数据收发(遍历lfd之后的文件描述符是否在rset中)
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)//出错,将cfd关闭,从oldset中删除cfd
{
perror("");
Close(i);
FD_CLR(i,&oldset);
}
else if(ret == 0)
{
printf("client close\n");
Close(i);
FD_CLR(i,&oldset);
}
else
{
printf("%s\n",buf);
Write(i,buf,ret);
}
}
}
}
}
return 0;
}
2)优缺点、可能的问题
优点:跨平台
缺点:
①文件描述符1024的限制(FD_SETSIZE限制)
②只返回变化的文件描述符的个数,具体是哪个需要遍历
③每次都需要将需要监听的文件描述符,从应用层拷贝到内核
④大量并发,少量活跃问题(问题2)
问题1:假设现在4-1023文件描述符需要监听,但是5-1000这些文件描述符关闭了?--①将要监听的添加进自定义数组,②使用dup2()重定向文件描述符
问题2:假设现在4-1023个文件描述符需要监听,但是只有5,1002发来消息?--无法优化
8. poll
优点:①相对于select没有最大文件描述符的限制②请求和返回分离
缺点:①每次都需要将需要监听的文件描述符拷贝到内核,②每次都需要将数组中的元素遍历一边才知道哪一个变化了,③大量并发,少量活跃会有效率低的问题(如同select)
功能:监听多个文件描述符的属性变化
参数:
fds:监听的数组的首地址
nfds:数组中有效元素的最大下标+1
timout:超时时间,-1永久监听
数组元素:
9. epoll
1)epoll工作流程:
- 创建一棵红黑树(在应用层不需要关注)
- 将需要监听的文件描述符上树
- 监听
特点:①没有文件描述符1024的限制;②以后每次监听不需要将此需要监听的文件描述符拷贝到内核;③返回的是已经变化的文件描述符,不需要遍历
2)epoll的相关API
i)创建红黑树
参数:
size:监听的文件描述符上限,在Linux2.6.8版本后写1(大于0)即可
返回值:树的句柄(类似于文件描述符)
ii)上树、下树、修改节点
参数:
epfd:树的句柄
op:ADD上树,DEL下树,MOD修改
fd:上树、下树、修改节点的文件描述符
event:需要上树的节点的信息,这个结构体包括感兴趣的事件类型和用户数据等
例子:
iii)监听epoll_wait()
功能:监听树上文件描述符的变化
参数:
epfd:树的句柄
events:接收变化的节点的首地址(一般用结构体数组接回来)
maxevents:数组元素的个数
timeout:超时时间,-1永久监听
10. epoll的水平触发和边缘触发
水平触发LT:只要读缓冲区有数据就会触发epoll_wait
边缘触发ET:数据来一次,epoll_wait只触发一次
2)监听写缓冲区
水平触发:只要可以写,就会触发
边缘触发:数据从有到无,就会触发
3)添加边缘触发
epoll_wait是一个系统调用,要尽可能减少使用(使用边缘触发)。通过修改上树前创建的结构体,加上“ | EPOLLET”。
struct epoll_event ev,evs[1024];
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
可是当入输入的内容超出每次读取的长度,会堆积在读缓冲区,则需要循环读取。而在循环读取到没有内容可读时,read()又会被阻塞,此时就无法返回去监听其他套接字。为了避免上述现象,又需要设置已连接套接字的属性,如下:
//设置cfd为非阻塞
int flag = fcntl(cfd, F_GETFL);//获取cfd的标签位
flags |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flags);
最后,上一部分的代码就成了这样,这种“边缘触发 + 非阻塞”也叫做“高速模式”:
#include <stdio.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include "wrapSocket.h"
int main(int argc, char *argv[])
{
// 创建套接字
int lfd = tcp4bind(8080, NULL);
// 监听
Listen(lfd, 128);
// 创建树
int epfd = epoll_create(1);
// 将lfd上树
struct epoll_event ev, evs[1024];
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
// while监听
while (1)
{
int nready = epoll_wait(epfd, evs, 1024, -1);
if (nready < 0)
{
perror("");
break;
}
else if (nready == 0)
{
continue;
}
else // 有文件描述符变化
{
for (int i = 0; i < nready; i++)
{
// 判断lfd变化,且是读事件变化
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);
//设置已连接套接字非阻塞
int flags = fcntl(cfd, F_GETFL);
flags |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flags);
printf("new client ip=%s port=%d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
ntohs(cliaddr.sin_port));
// 将cfd上树
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
else if (evs[i].events & EPOLLIN) // cfd变化,且是读事件变化
{
while (1)
{
char buf[1024] = "";
//如果读一个缓冲区,缓冲区没有数据,就阻塞等待,
//如果是非阻塞,返回值为-1,并设置errno为EAGAIN
int n = read(evs[i].data.fd, buf, sizeof(buf));
if (n < 0) // 出错,节点下树
{
if(errno == EAGAIN )//缓冲区读干净了,跳出循环,继续监听
{
break;
}
//普通错误
perror("");
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
break;
}
else if (n == 0) // 客户端关闭
{
printf("client close\n");
close(evs[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
break;
}
else
{
//printf("%s\n", buf);
write(STDOUT_FILENO, buf, 1024);
write(evs[i].data.fd, buf, n);
}
}
}
}
}
}
return 0;
}
第十四章 反应堆模型和线程池模型、UDP通信、本地套接字
1. epoll反应堆
把文件描述符、事件、回调函数用结构体封装。
下文将会提到libevent库,就是对epoll reactor的实现。
2. 线程池的概念
事先创建几个线程,一个任务队列。线程池中的线程不停地取任务队列中的任务;有任务来就往队列中添加(生产者和消费者模型),省去了创建线程和销毁线程的时间和资源。
3. UDP通信
1)UDP和TCP
TCP:丢包重传 面向连接(电话模型)
UDP:丢包不重传(邮件模型)
tcp通信流程:
服务器:创建流式套接字 绑定 监听 提取 读写 关闭
客户端:创建流式套接字 连接 读写 关闭
收发数据:
i)read recv
flags==MSG_PEEK读数据不会删除缓冲区的数据,通常填0即可。
ii)write send
flags==1紧急数据,通常填0即可。
udp通信流程:
服务器:创建报式套接字 绑定 读写 关闭
客户端:创建报式套接字 读写 关闭
发数据:
dest_addr:目的地的地址信息
addrlen:结构体大小
收数据:
src_addr:对方的地址信息
addrlen:结构体大小的地址
2)UDP通信服务器、客户端代码实现
因为不需要建立连接,而是像发送信件一样通讯,UDP中不存在客户端与服务器一说,而我们一般把先发送信息的称为客户端。
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.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(8080);
myaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(fd, (struct sockaddr *)&myaddr, sizeof(myaddr));
if(ret < 0)
{
perror("");
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);
memset(buf, 0, sizeof(buf));
if(n < 0)
{
perror("");
break;
}
else
{
printf("%s\n",buf);
sendto(fd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
}
}
//关闭
close(fd);
return 0;
}
4. 本地套接字
1)Unix domain socket
本地套接字通信,全双工,套接字用一个文件来标识,文件在绑定前不能创建。
创建本地套接字用于tcp通信:
int socket(int domain, int type, int protocol);
参数:
domain:AF_UNIX(本地套接字)
type:SOCK_STREAM(流式套接字)
protocol:0
返回值:文件描述符
绑定:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:本地套接字
addr:本地套接字结构体地址
addrlen:sockaddr_un大小
监听
提取:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
读写
关闭
2)代码实现:本地套接字单任务tcp服务器
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
unlink("sock.s");//删除可能已经创建的sock.s文件
//创建unix流式套接字
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
//绑定
struct sockaddr_un myaddr;
myaddr.sun_family = AF_UNIX;
strcpy(myaddr.sun_path, "sock.s");//绑定前,文件不能存在
//sizeof(myaddr)也可以是int len = offsetof(struct sockaddr_un, sun_path) + strlen(myaddr.sun_path);
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)
{
printf("err or client close\n");
break;
}
else
{
printf("%s\n", buf);
send(cfd, buf, n, 0);
}
}
//关闭
close(cfd);
close(lfd);
return 0;
}
在另一个终端使用"nc -U sock.s"来测试连接。
需要注意的点:
客户端可以隐式绑定,但是服务器不可以;绑定指定文件时,这个文件必须不存在(在代码中使用unlink删除)。
3)代码实现:本地套接字客户端
#include <stdio.h>
#include <stddef.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <sys/un.h>
int main(int argc, char *argv[])
{
unlink("sock.c");
//创建unix流式套接字
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));
//读写
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)
{
printf("err or server close\n");
break;
}
else
{
printf("%s\n", buf);
}
}
//关闭
close(cfd);
return 0;
}
第十五章 libevent
1. libevent安装
2. libevent事件触发流程
在使用libevent的函数之前,需要先申请一个event_base结构,相当于盖房子时的地基,在event_base基础上有一个事件集合,可以检测哪个事件是激活的(就绪)。
1)创建、释放根节点
//创建event_base根节点
struct event_base *event_base_new(void);
//释放根节点
void event_base_free(struct event_base *);
//如果fork出子进程,想在子进程继续使用event_base,
//那么子进程需要对event_base重新初始化(较少用)
int event_reinit(struct event_base *base);
2)循环监听
//效果如同while(1){epoll_wait};
int event_base_dispatch(stuct event_base *base);
3)退出循环监听
两个接口一个是等待固定时间退出,一个是立即退出。
4)事件触发流程
3. libeventAPI
1)初始化上树节点
参数:
base:event_base根节点
fd:上树的文件描述符
events:监听的事件
cb:回调函数
arg:回调函数的参数
返回值:初始化好的节点的地址
2)节点上树
参数:
ev:上树的节点的地址
timeout:NULL永久监听
3)下树
参数:
ev:下树节点的地址
4)释放节点
4. 使用libevent编写tcp服务器流程
创建套接字——绑定——监听——创建event_base根节点——初始化上树节点lfd——上树——循环监听——收尾
#include <stdio.h>
#include <event.h>
#include "wrapSocket.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\n");
//下树
}
else
{
printf("%s\n", buf);
Write(cfd, buf, sizeof(buf));
}
}
void lfdcb(int lfd, short event, void *arg)
{
struct event_base *base = (struct event_base *)arg;
//提取新的cfd
int cfd = Accept(lfd, NULL, NULL);
//cfd上树
struct event *ev = event_new(base, cfd, EV_READ | EV_PERSIST, cfdcb, NULL);
event_add(ev, NULL);
}
int main(int argc, char *argv[])
{
//创建套接字
//绑定
int lfd = tcp4bind(8080, NULL);
//监听
Listen(lfd, 128);
//创建event_base根节点
struct event_base *base = event_base_new();
//初始化lfd上树节点
struct event *ev = event_new(base, lfd, EV_READ | EV_PERSIST, lfdcb, NULL);
//上树
event_add(ev, NULL);
//循环监听
event_base_dispatch(base);//阻塞
//收尾
close(lfd);
event_base_free(base);
return 0;
}
5. bufferevent事件介绍
普通的event事件:文件描述符 事件(底层缓冲区的读或写)触发 回调
高级的event事件:bufferevent事件
核心:一个文件描述符、两个缓冲区、三个回调
6. bufferevent监听流程
7. buffereventAPI
1)创建新的节点
参数:
base:event_base根结点
fd:要初始化上树的文件描述符
options:
返回值:新建节点的地址
2)设置节点的回调
参数:
bufev:新建的节点的地址
readcb:读回调
writecd:写回调
eventcb:异常回调
cbarg:传给回调函数的参数
3)设置事件使能
4)发送数据
5)接收数据
6)创建套接字、连接服务器
填-1是因为还没有文件描述符
8. 链接监听器(创建、绑定、监听、提取)
功能:创建套接字、绑定、监听、提取
参数:
base:event_base根节点
cd:提取套接字(cfd)后调用的回调
ptr:传给回调函数的参数
flags:
backlog:监听队列的长度,填-1自动填充
sa:绑定的地址信息
socklen:sa的大小
返回值:链接监听器的地址
9. 对sample中hello-world.c的阅读
/*
This example program provides a trivial server program that listens for TCP
connections on port 9995. When they arrive, it writes a short message to
each client connection, and closes each connection once it is flushed.
Where possible, it exits cleanly in response to a SIGINT (ctrl-c).
*/
#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 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
//创建event_base根节点
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;
}
//创建信号触发的节点(Ctrl+c信号)
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;
//将fd上树
//新建一个bufferevent节点
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, NULL, conn_writecb, conn_eventcb, NULL);
bufferevent_enable(bev, EV_WRITE);//设置写事件使能
bufferevent_disable(bev, EV_READ);//设置读事件非使能
bufferevent_write(bev, MESSAGE, strlen(MESSAGE));//给cfd(bev)发送消息"hello world"
}
static void
conn_writecb(struct bufferevent *bev, void *user_data)
{
struct evbuffer *output = bufferevent_get_output(bev);//获取缓冲区类型
if (evbuffer_get_length(output) == 0) {//判断缓冲区是否有数据
printf("flushed answer\n");
bufferevent_free(bev);//释放节点,自动关闭
}
}
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));/*XXX win32*/
}
/* None of the other events can happen here, since we haven't enabled
* timeouts */
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);//退出循环监听
}