目录
------------------------------------------------------------------------
------------------------------------------------------------------------
------------------------------------------------------------------------
知识点
网络协议模型
-
数据链路层:
-
ARP(address resolve protocol)
-
(1)主机向自己所在的网络发送一个ARP请求,请求包含目标机器的网络地址,网络上所有其他机器都将收到这个请求,但是只有目标机器(符合网络地址)才会作出 ARP应答,应答内容包含目标机器的MAC地址。
-
(2)维护一个ARP高速缓存,包含 【IP -> MAC】的映射。
-
arp -a # 查看arp缓存内容 sudo arp -d 192.168.1.109 # 删除某个IP的mac地址 sudo arp -s 192.168.1.109 08:00:27:53:10:67 # 创建某个IP的mac地址 # 测试arp请求,须删除arp对应缓存 # laptop 执行 tcpdump -i eth0 -ent '(dst 192.168.1.109 and src 192.168.1.108) or (dst 192.168.1.108 and src 192.168.1.109)' telnet 192.168.1.109 echo
-
-
(3)
-
-
-
-
封装成帧(18字节)后在物理层上传输:
-
-
-
应用层:
-
DNS(Domain name system)
-
(1)包信息
-
-
(2)DNS服务器的IP地址:
-
cat /etc/resolv.conf #可能包含首选DNS服务器和备选DNS服务器
-
-
(3)访问DNS服务器的客户端程序host:
-
host -t A www.baidu.com # -t A 表示使用A类型,通过机器的域名获得IP地址
-
-
(4)使用tcpdump抓包:
-
sudo tcpdump -i eth0 -nt -s 500 port domain host -t A www.baidu.com
-
-
-
网络层协议:
-
无状态服务:1、无须保持通信状态而分配内核资源。2、传输数据无须携带状态信息。
-
无连接服务:1、不长久地维持对方的信息。 2、每次发送需要明确IP地址。
-
IPV4包
-
-
IP数据包分片实验
-
sudo tcpdump -ntv icmp ping www.baidu.com -s 1473
-
-
ICMP (Internet control message protocol)
-
-
-
传输层:
-
TCP
-
-
Linux超时重传(时间间隔 1s - 2s - 4s - 8s - 16s - 32s)
-
-
访问不存在端口:返回RST
-
-
网络字节序 (很绕的概念)
-
一种数据存储格式,与之对应的还有本机字节序,按序列内部组织方式,分成:
-
大端序列:高位数据存储在低位内存中。
-
小端序列:反过来
-
数据:int : 0x12345678 : 4 bytes
-
内存增长方向:低 -|字节|-|字节|-|字节|-|字节|-> 高
-
大端: 低 - 12 - 34 - 56 - 78 -> 高 (符合右手书写习惯)
-
小端: 低 - 78 - 56 - 34 - 12 -> 高
-
其中,网络字节序列属于大端序列,本机则两种都有可能。
-
-
// Linux 采用以下4种函数完成主机字节序与网络字节序之间的转换 #include <netinet/in.h> unsigned long int htol(unsigned long int hostlong); // IP unsigned short int htos(unsigned long short hostshort); // Port unsigned long int ntohl(unsigned long int netlong); unsigned short int ntohs(unsigned long short netshort);
-
// 查看主机序列 与 网络序列 #include <arpa/inet.h> #include <stdio.h> int main() { int a = 0x12345678; char *p = (char *)(&a); printf("主机字节序:%0x %0x %0x %0x\n", p[0], p[1], p[2], p[3]); int b = htonl(a); //将主机字节序转化成了网络字节序 p = (char *)(&b); printf("网络字节序:%0x %0x %0x %0x\n", p[0], p[1], p[2], p[3]); return 0; } // 主机字节序:78 56 34 12 // 网络字节序:12 34 56 78
-
#include <iostream> using namespace std; bool TestEndin() { union // 不加类型名称,可以直接使用变量名调用 { int numbers = 0x10000000; char pointer; }; // 假定写入的数据是从高位数据开始写入 if (pointer == 1) // 若从低位开始存放数据,则说明是大端模式 return true; return false; // 否则,从高位开始存放数据,说明是小端模式 } int main() { if (TestEndin()) cout << "BigEndin" <<endl; else cout << "SmallEndin" <<endl; }
什么是TCP
-
TCP是传输控制层协议,这是一种面向连接的,提供可靠传输的协议。
-
面向连接:三次握手 --》 传输数据 --》 四次挥手
-
什么是面向连接?经过TCP三次握手之后,双方建立的资源就是所谓的链接,它不是一条真实的链路。
-
-
可靠传输:
-
确认机制
-
超时重传
-
流量控制
-
拥塞控制
-
-
交换机与路由器
-
交换机:主要管理MAC地址。
-
路由器:管理IP地址。
-
家里路由器:交换机(四个接口) + 路由器
TCP三次握手
-
目的:只建立一次链接。
-
TCP机制:
-
(1)不带数据的ACK报文不会重传,(且不消耗序列号)。
-
(2)双方序列号都确定后,才进行链接传输。
-
-
握手过程:
-
client --- | SYN = 1,seq = x | --->>> server
-
client <<<--- | ACK = 1,SYN = 1,seq = y,ack = x + 1| --- server
-
client --- | ACK = 1,seq = x + 1,ack = y + 1| --->>> server
-
-
为什么三次:
-
正常讲应该是:4次,分别为: SYN -> ACK -> SYN -> ACK。但是,为了调高效率,将发送的ACK报文,添加上了SYN数据,合并这两次的传输,因为变成了3次,更多参考TCP机制。
-
TCP四次挥手
-
挥手过程:
-
client --- | FIN = 1,seq = u | --->>> server
-
client <<<--- | ACK = 1,seq = v,ack = u + 1 | --- server
-
server进入close-wait状态,并可能仍要向client发送数据
-
-
client <<<--- | FIN = 1,seq = w | --- server
-
client --- | ACK = 1,seq = u + 1,ack = w + 1 | --->>> server
-
clinet进入time-wait等待2MSL(Maximum Segment Lifetime)后,释放TCP连接
-
-
-
细节:
-
client等待2MSL(2MSL:2个最大报文段的生存时间)
-
(1)确保C端确认报文到达S端,若S端收到,会重新发送FIN报文。
-
(2)防止已经失效的请求连接报文出现在本连接中,2MSL足够让所有C端产生的报文在网络中消失。
-
-
TCP与UDP区别 概念 适用范围
-
TCP(20bytes) UDP(8bytes) 面向连接 是,需要建立连接后才能传输数据 否 可靠交付 是,保证可靠交付——确认、超时重传、流量控制、拥塞控制 否,只检验和丢失报文等处理 工作效率 低,控制多、网络开销大、系统开销大 高,传输控制简单、系统开销小 实时性 低 高 安全性 机制多,容易被利用而被攻击 机制少,相对安全 适用场景 要求传输质量,对实时性要求不高 相反 示例 HTTP,HTTPS,FTP等传输文件的协议以及POP,SMTP等邮件传输协议 比如视频传输、实时通信等 传输单位 数据流模式 数据报模式
TCP可靠性保证
-
(1)序列号、确认机制、超时重传。
-
双方确认序列号后,才能进行信息传输,并根据序列号判断报文是否是需要的。
-
通过确认机制,来确定哪些报文到达,哪些需要重传。
-
-
(2)checksum校验和。
-
三部分进行校验和:TCP伪首部、TCP首部、TCP数据。
-
-
(3)流量控制。
-
流量窗口,指无需等待确认信号的情况下,发送端还能发送的最大数据量。
-
该机制设置了大量缓冲区。
-
-
(4)拥塞控制。
-
慢开始:从1开始,以2的倍数增加窗口值。
-
拥塞避免:拥塞窗口到达阈值后,每次增加1个窗口值;发生超时重传时,阈值为当前窗口一半,回到慢开始。
-
快重传:收到三个连续的重复确认,立即重传当前编号的下一个报文段,而不等超时重传。
-
快恢复:收到三个连续的重复确认,将阈值与窗口值设置为当前阈值的一半,然后执行拥塞避免。
-
这里每次,指的是收到一个确认报文,即为1次。
-
-
TCP三次握手时产生的队列
-
(1)半连接队列:SYN队列。
-
服务端接受客户端的SYN请求后,内核会将该连接存储到半连接队列中。
-
同时,服务器会为该连接分配TCB(传输控制块),至少要280字节,甚至1300字节。
-
-
(2)全连接队列:ACCEPT队列。
-
服务端接受客户端的ACK后,将连接从SYN队列中移除,然后将创建一个新的完全连接,并将其添加到accept队列,等待进程调用accept函数将连接取出。
-
-
-
(3)实战部分:
-
https://zhuanlan.zhihu.com/p/144785626 (暂时不往后看了)
-
SYN 攻击
-
(1)属于DOS攻击的一种,DOS攻击(Denial of Service,即拒接服务),另外,DDOS (Distribute)分布式攻击,指多台机器对服务器发起DOS攻击。
-
(2)而SYN攻击,指的是向服务器的TCP端口发送很多SYN请求,但是却不完成3次握手过程。
-
不完成握手的方法可以是:使用假的IP地址;
-
-
(3)攻击的是服务器的SYN队列。
-
(4)解决方式:
-
4-1、延迟分配TCB。等待正常连接建立后,再分配TCB。
-
4-2、SYN Cache技术,HASH表保存半连接信息,直到收到正确的回应ACK报文再分配TCB。
-
4-3、SYN Cookie技术:
-
考虑了报文中的一些固定信息,加上一些服务端的固定信息,计算出一个序列号,然后发送给客户端。
-
这时并不保存和分配任何信息。
-
接受最后一次ACK报文时,通过相同方式的计算,查看得到的结果是否时客户端发送SYN报文中的序列号。
-
确认则通过匹配,否则拒绝匹配。
-
-
设置以下参数
-
net.ipv4.tcp_syncookies = 1
-
net.ipv4.tcp_max_syn_backlog = 8192
-
net.ipv4.tcp_synack_retries = 2
-
-
发生RST情况
-
(1)客户端 established 而 服务端 closed(断电重启)。
-
客户端发送任何信息给服务端,服务器都发回RST。
-
若客户端接受的到RST,则立即释放该TCP的端口号和内存资源,并使用当前窗口号,重新建立TCP连接。
-
HTTP协议(超文本传输协议)
-
(1)超文本传输协议,从服务器传输超文本到本地浏览器的协议,它基于TCP/IP,属于应用层的协议。
-
(1-1)HTTP协议工作在 客户端 - 服务器模型上,浏览器作为HTTP客户端,向服务端发送HTTP请求。
-
-
(2)简单快速
-
请求服务时,只需要传输 请求方法 和 路径,由于HTTP协议简单,使得HTTP服务器的程序规模小,通讯快速。
-
请求方法:GET、POST、HEAD
-
-
(3)灵活:允许传输任意类型的数据对象。
-
(4)无连接:含义,每次连接只处理一个请求,并在接受客户端确认后,断开连接。
-
(5)无状态(cookie解决该问题):对事务处理没有记忆,若后续要处理前面的信息,则必须重传,导致传输数据量大,但应答速度块。
-
-
(1)HTTP代理服务:
-
(1-1)正向代理:客户端,设置并发HTTP请求到正向代理服务器,由正向代理服务器返回请求的目标资源。(透明代理为正向代理的一种特殊情况)
-
(1-2)反向代理:服务端,存在反向代理服务器,接受HTTP请求,根据请求类型,将请求转发到内部服务器中,获取目标资源给客户端。
-
(1-3)开源软件:squid(均支持)、varnish(反向代理)
-
-
(2)传输过程:
-
IP层的源IP与目的IP一般是持久不变的,而帧头部的源MAC与目的MAC是一直变化的。
-
-
(3)请求方法:
# HTTP请求 Get http://www.baidu.com/index.html HTTP/1.0 User-Agent: Wget/1.12 (linux-gnu) Host: www.baidu.com # 头部必须信息 Connection: close # 空行 # 消息体
-
GET :申请获取资源、不对服务器造成任何影响
-
HEAD :类似GET,仅要求服务器头部信息,不需要任何实际内容
-
POST :向服务器提交数据,影响服务器(创建新资源或者更新原有资源)
-
PUT :上传资源
-
DELETE : 删除资源
-
TRACE :要求服务器返回原始的HTTP请求内容,可以查看中间服务器对HTTP请求的影响。
-
OPTIONS :查看服务器对某个特定URL都支持哪些请求方法,设置URL为*,则获得服务器所有请求方法。
-
CONNECT :用于某些代理服务器,它们能把请求的连接转化为一个安全隧道。
-
PATCH :对资源做部分修改。
-
注:加粗为安全方法,不对服务器产生影响。
-
-
(4)Connecton:
-
(值为close)-短链接:旧版HTTP,1个TCP连接只能服务1个HTTP,随后WEB服务端主动关闭TCP。
-
(keep-alive)-长连接:1个TCP可以处理多个HTTP,节约TCP建立时间,加快传输效率。
-
-
(5)状态码 与 状态信息:
# HTTP应答 HTTP/1.0 200 ok Server: BWS/1.0 # 服务器名称 Content-Length: 8024 Content-Type: text/html;charset = gbk Set-Cookie: ... Via: 1.0 localhost (squid/3.0 STABLE18) # 所经过的所有代理服务器 # 空行 # 请求文档内容
-
1xx信息:100 Continue : 服务器收到请求行和头部信息,告诉客户端继续发送数据部分信息
-
2xx成功:200 OK : 请求成功
-
3xx重定向:
-
301 Moved Permanently : 资源被转移了,请求将被重定向
-
302 Found : 通知资源在其他地方可以找到,但必须以GET获取
-
304 Not Modified : 申请的资源没有更新,和之前获得的相同
-
307 Temporary Redirect : 类似302,但可以使用原始的请求方法
-
-
4xx客户端错误:
-
400 Bad Request : 通常客户请求错误
-
401 Unauthorized : 请求需要认证信息
-
403 Forbidden : 没有权限访问资源
-
404 Not Found : 资源没找到
-
407 Proxy Authentication Required : 客户端需要代理服务器的认证
-
-
5xx服务端错误:
-
500 Internet Server Error : 通常服务器错误
-
503 Service Unavailable : 暂时无法访问服务器
-
-
-
(6)Cookie
-
目的:保持HTTP连接状态
-
HTTP应答:set-Cookie - 用以标识每个客户端
-
HTTP请求:每个请求需要附带Cookie信息
-
-
------------------------------------------------------------------------
网络编程模块
知识点
TCP编程模型
-
(1)客户端编程模型 附录—客户端编程模型
-
-
(2)服务端编程模型附录—服务端编程模型
-
-
(3)使用telnet,可以直接连接服务器,配合wireshark,抓包观察连接过程。
-
telnet 222.201.187.181 15522
-
TCP三次握手
-
(1)由内核完成,客户端仅仅是调用了connect函数;握手过程在connect函数执行过程中完成,而服务端则是在调用accept函数执行过程中完成。
TCP四次挥手
-
(1)双端分别调用close(fd)后,执行挥手,既可以是服务端先调用close(),也可以是客户端执行close()。
-
(2)客户端调用close()关闭后,服务端调用的read()随即返回0,不再阻塞。
套接字地址结构
-
(1)通用地址结构:
-
struct sockaddr { sa_family_t sa_family; char sa_data[14]; }
-
-
(2)IPv4地址结构:
-
struct in_addr { in_addr_t s_addr; } struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[0]; }
-
-
(3)IPv6地址结构
-
struct in6_addr { in_addr_t s6_addr[16]; } struct sockaddr_in6 { sa_family_t sin_family; in_port_t sin_port; struct in6_addr sin6_addr; uint32_t sin6_flowinfo; uint32_t sin6_scope_id; }
-
-
(4)地址转换函数
-
inet_addr(); inet_aton(); inet_ntoa(); inet_pton(); inet_ntop();
-
进程与内核传递地址结构
-
(1)由于数据经过层层封装后,最后通过网卡发送出去,而网卡等硬件设备的控制权在内核手上,因此必定会存在进程数据结构传递到内核的阶段。同样,接收数据后,也由内核先解包后传递到进程空间。
-
(2)进程 --> 内核:bind、connect、sendto
-
(3)内核 --> 进程:accept、recvfrom、getsockname、getpeername
socket()函数
-
// 1、通用socket地址 #include <bits/socket.h> struct sockaddr // 表示sock地址的结构体 { sa_family_t sa_family; // 地址族类型变量 对应 协议族类型 char sa_data[14]; // 存放socket的地址值 } /* 常见协议族: | 协议族 | 地址族 | 描述 | | PF_UNIX | AF_UNIX | UNIX本地域协议族 | | PF_INET | AF_INET | TCP/IPv4 协议族 | | PF_INET6| AF_INET6| TCP/IPv6 协议族 | */ // 2、通用socket地址2 略 // 3、专用socket地址 // 3-1、UNIX本地域协议族 #include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; // 地址族:AF_UNIX char sun_path[108]; // 文件地址名 }; // 3-2、TCP/IP struct sockaddr_in { sa_family_t sin_family; // 地址族:AF_INET u_int16_t sin_port; // 端口号,要使用网络字节序 struct in_addr sin_addr; // IPv4地址结构体 }; struct in_addr { u_int32_t s_addr; // IPv4地址 };
-
// IP地址转换函数 #include <arpa/inet.h> in_addr_t inet_addr(const char *strptr); // 字符串转网络字节序整数 int inet_aton(const char *cp, struct in_addr* inp); // 同上,但错误存储在inp指向的地址 char * inet_ntoa(struct in_addr in); // 网络字节序整数转换成字符串 --> 此代码不可重入 // 还有 int inet_pton(int af, const char* src, void* dst); const char* inet_ntop(int af, const char* src, char * dst, socklen_t cnt);
-
-
int socket(int domain, int type, int protocol); int domain : AF_INET IPv4协议 | AF_INET6 IPv6协议 | AF_LOCAL/AF_UNIX 本地套接字 | AF_PACKET int type : SOCK_STREAM 字节流 | SOCK_DGRAM 数据报 | SOCK_RAW 原始套接字 int protocol : --
connect()函数
-
(1)没有与之相对应的端口发生连接,hardware error,客户端收到RST,返回ECONNREFUSED。
-
(2)发送请求时IP不可达(no route to host),协议ICMP,software error,通常是发送arp请求没有响应。
bind()函数
-
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 发送服务端地址给内核 // IP地址可以使用通配地址,端口号0表示由内核分配临时端口 // INADDR_ANY,表示由内核选择IP,可以选择主机任意IP // 分配的临时端口,使用getsockname来返回临时端口 // 通常错误:Address already in use // 使用套接字选项来设置:setsockopt | SO_REUSEADDR
listen()函数
-
int listen(int sockfd, int backlog); // backlog 的理解 // 内核会维护两个队列: // 1. 半连接队列:客户端发送了SYN,被服务端接受后,就会进入半连接队列。 // 2. 全连接队列:三次握手完成后,半连接队列的结点将进入全完成队列。
Accept()函数
-
// 从Listen监听队列中获取一个连接,而不关心其状态。
-
UDP编程模型
-
(1)客户端:socket() --> sendto() --> recvfrom --> close()
-
(2)服务端:socket() --> bind() --> recvfrom() --> sendto() -->close()
-
(3)服务端通过recvfrom()获知客户端的地址,然后sendto()进行消息交互。
-
(4)客户端首先发送sendto()给服务端,然后通过recvfrom()获取消息。
recvfrom()
-
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags struct sockaddr *src_addr, socklen_t *addrlen); // 从内核中获取 buf数据、源地址和长度
sendto()
-
ssize_t sendto(int sockfd, void *buf, size_t len, int flags struct sockaddr *dest_addr, socklen_t addrlen); // 向内核传输 buf数据、目标地址和长度
------------------------------------------------------------------------
多进程并发服务器
知识点
accept后的处理
-
(1)多进程:在接收通信套接字后,使用fork()创建子进程。
-
(2)子进程必须先关闭 监听套接字,因为子进程复制时,套接字引用计数+1,这么做是为了避免套接字无法关闭。
-
(3)父进程也需要关闭 通信套接字。
-
(4)创建listen的过程,将其封装,如改—服务端编程模型
-
(5)另外,需要注意子进程先结束时,要避免其成为僵尸进程。
一个服务器对多个客户端
-
(1)服务端可以使用多进程的模式,对客户端请求做出响应。
-
(2)进行多次交互时,记得清空buf,以免发生意想不到的错误。
I/O复用—select
-
(1)修改了服务端模型,使用I/O符用来代替多进程模型。SELECT-服务端模型
-
(2)很多细节:
-
(2-1)FD_ZERO(&global_rdfs); // 全局文件描述符置0;重置
-
(2-2)FD_SET(listen_fd, &global_rdfs); // 设定想要关注的文件描述符
-
(2-3)select(max_fd + 1, ¤t_rdfs, NULL, NULL, NULL) // 注意第一个参数 ==》 最大文件描述符 + 1
-
(2-4)for (int i = 0; i <= max_fd; ++i) // 循环查看是哪个描述符,产生了新的内容,注意大于等于
-
(2-5) if ( FD_ISSET(i, ¤t_rdfs) ) // 查看是否是第 i 个描述符产生了新内容
-
-
(3)select 最大描述符数量 --> 1024(2048,看系统),太大容易给客户端造成延时的现象。
-
用户数量 ≈ 1000
-
-
(4)缓冲区的数据没有读完,select仍然会有提示有数据产生。
-
(5)poll 与select 的区别在于,poll使用链表来保存fd,可以存的fd更多。
I/O符用—epoll
-
(1)EPOLL-服务端模型,~容错处理没有完善。
-
(2)可以使用的量
-
-
(3)epoll编程模型框架
-
// 1. 创建 epoll,MAX_EVENTS 指定 epoll 容纳的文件描述符数量 epoll_fd = epoll_create(MAX_EVENTS); // 2. 设置 监听描述符为非阻塞 fcntl(listen_fd, F_SETFL, O_NONBLOCK); // 3. 添加 监听描述符 为epoll 关注对象 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &tmp_ev); // 4. 循环中,获取有相应的文件描述符 while (1) { fd_nums = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // events 事件列表,用于存储触发事件的相关信息 ... } // 5. 循环遍历所有文件描述符 for (int i = 0; i < fd_nums; ++i) { // 6.获取对应文件描述符信息,并进行操作。 if (events[i].data.fd == listen_fd) { // 有客户端连接,添加到epoll中 tmp_ev.data.fd = sock_fd; tmp_ev.events = EPOLLIN | EPOLLET; // ET边缘触发 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &tmp_ev); } else { // 传递fd,随后处理数据 process_data(events[i].data.fd); // 若要关闭连接 // 删除对文件描述符的关注 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &tmp_ev); close(events[i].data.fd); } }
-
-
(4)LT(Level Triggered) 与 ET(Edge Triggered)
-
LT :默认模式,select, poll 也采用这种模式,当缓冲还有数据没有读完时,内核会继续通知服务器。
-
ET :高速模式,缓冲有数据时,内核只通知服务器1次,不管缓冲数据是否被读完。
-
模型小结
-
(1)PPC(process per connection) | TPC (thread per connection)
-
(2)select | poll
-
(3)epoll
I/O小结
-
bio, 阻塞io
-
多线程,切换开销大
-
nio,非阻塞方式询问io
-
多路复用,select/poll,循环访问fd,且有限个数,但可以修改,ulimited。
-
epoll,网卡进入信息后,结合事件+中断,DMA等技术,不需要循环遍历,直接返回有响应的fd。
2020/12/28 小结
-
(1)select
-
1-1、FD上限1024。
-
1-2、重复初始化:每次调用select(),都需要将fd集合从用户态拷贝到内核态。
-
1-3、内核遍历集合:逐个排查FD,查看时候有消息产生。
-
1-4、外面也需要循环遍历:查看是哪个FD产生了消息。
-
-
(2)poll
-
2-1、取消FD上限为1024。
-
-
(3)epoll (仅支持linux)
-
3-1、取消FD上限为1024
-
3-2、使用epoll_ctl()注册FD,用户态到内核态只需要1次拷贝。
-
3-3、epoll_wait只关心“就绪”的文件描述符,而不需要遍历所有FD。
-
通过设定FD的回调函数,将就绪的FD加入到就绪队列中。
-
-
-
(4)select、poll和epoll 小结
-
4-1、并非选择epoll最好,因为回调函数也需要消耗。
-
4-2、若FD较少或者(FD很多且活跃),三种都可以用。
-
4-3、出现较多空连接或者死链接,可以使用epoll。
-
-
(5)信号驱动I/O
-
设定信号处理函数,接受成功信号后,通过信号处理函数来完成后续的操作。(工程少用)
-
不是异步,因为接受数据(即从内核空间到用户空间这段,需要阻塞)
-
-
-
(6)异步I/O (linux下,支持两种异步I/O)
-
6-1、用户态实现的aio_read、aio_write等。
-
实现原理是,创建一个新的线程用于阻塞等待接受数据。
-
-
6-2、内核实现异步I/O接口。
-
-
------------------------------------------------------------------------
附录
客户端编程模型
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
const int MAX_BUFFER_SIZE = 1024;
// 处理错误函数
void handdle_error (char * msg)
{
perror(msg);
abort();
}
int main(int argc, char * argv[])
{
struct sockaddr_in server_addr;
// 创建套接字文件描述符
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0)
handdle_error(const_cast<char*>("socket failed"));
// 设定服务端IP及端口号
bzero(&server_addr, sizeof(server_addr)); // memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(15522);
// 连接服务端
if (connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) // sizeof(server_addr) 的长度是分了区别使用的协议
handdle_error(const_cast<char*>("connect failed"));
// 读取套接字文件描述符
char buffer[MAX_BUFFER_SIZE];
bzero(buffer, sizeof(buffer)); // 若不将buffer置0,输出将出现乱码
int bytes = read(socket_fd, buffer, MAX_BUFFER_SIZE); // 表示最大读取 MAX_BUFFER_SIZE个字节
if (bytes < 0)
handdle_error(const_cast<char*>("read failed"));
if (0 == bytes)
handdle_error(const_cast<char*>("read 0 bytes"));
// 客户端本地显示信息
printf("Receive bytes: %d\n", bytes);
printf("Time : %s\n", buffer);
// 关闭套接字文件描述符
close(socket_fd);
}
服务端编程模型
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctime>
const int MAX_BUFFER_SIZE = 1024;
const int MAX_LISTEN_QUE = 5;
// 错误处理函数
void handdle_error (char * msg)
{
perror(msg);
abort();
}
int main (int argc, char * argv[])
{
struct sockaddr_in server_addr, client_addr;
// 监听套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0)
handdle_error(const_cast<char*>("Listen_fd failed"));
// 设置端口地址可重用
int opt;
if ((setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) < 0)
handdle_error(const_cast<char*>("reset socket reuse falied"));
// 设定本机地址
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 若有多个IP地址,均用
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(15522);
// 绑定当前主机的套接字地址
if (bind(listen_fd, (struct sockaddr *) &server_addr, sizeof(struct sockaddr)) < 0)
handdle_error(const_cast<char*>("Bind server failed"));
// 监听
listen(listen_fd, MAX_LISTEN_QUE); // 创建监听队列,并赋值长度
char buffer[MAX_BUFFER_SIZE];
time_t real_time;
while (1)
{
socklen_t len;
// 接受端口输入
int socket_fd = accept(listen_fd, (struct sockaddr*) &client_addr, &len); // 获取客户端信息及结构体长度
if (socket_fd < 0)
handdle_error(const_cast<char*>("accept socket_fd failed"));
// 写入信息
real_time = time(nullptr);
snprintf(buffer, sizeof(buffer), "%s", ctime(&real_time)); // 把时间字符串写入缓冲中,然后写入套接字文件描述符
write(socket_fd, buffer, strlen(buffer)); // 控制写入字符个数
close(socket_fd); // 必须关闭,不然占用文件描述符个数
}
close(listen_fd);
}
改—服务端编程模型
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctime>
const int MAX_QUEUE = 5;
const int MAX_BUFER_SIZE = 100;
// 错误处理函数
void handdle_error (char * msg)
{
perror(msg);
abort();
}
// 建立监听,获取监听文件描述符
int listen_and_get_sockfd()
{
int listen_fd, opt = 1;
sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(15520);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if ( (listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
handdle_error(const_cast<char*>("create listen socket error"));
if ( setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
handdle_error(const_cast<char*>("setsockopt error"));
if ( bind(listen_fd, (struct sockaddr *) &server_addr, sizeof(struct sockaddr)) < 0)
handdle_error(const_cast<char*>("bind error"));
listen(listen_fd, MAX_QUEUE);
return listen_fd;
}
int main(int argc, char * argv[])
{
int listen_fd, sock_fd;
socklen_t len;
sockaddr_in client_addr;
char buf[MAX_BUFER_SIZE];
time_t real_time;
bzero(buf, sizeof(buf));
listen_fd = listen_and_get_sockfd();
while (1)
{
if ( (sock_fd = accept(listen_fd, (sockaddr *)&client_addr, &len)) < 0)
handdle_error( const_cast<char*>("create socket error") );
real_time = time(nullptr);
snprintf(buf, sizeof(buf), "%s", ctime(&real_time));
write(sock_fd, buf, strlen(buf));
close(sock_fd);
}
close(listen_fd);
}
SELECT-服务端模型
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctime>
const short PORT = 15521;
const int MAX_LISTEN_QUEUE = 5;
const int MAX_BUFFER_SIZE = 1024;
void handdle_error(char * msg)
{
perror(msg);
abort();
}
int listen_and_get_sockfd()
{
int listen_fd, opt = 1;
sockaddr_in server_addr;
socklen_t len = sizeof(sockaddr_in);
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
if ( ( listen_fd = socket(AF_INET, SOCK_STREAM, 0) ) < 0 )
{
handdle_error(const_cast<char*> ("create listen_fd falied"));
}
if ( setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
{
handdle_error(const_cast<char*>("setsockopt error"));
}
if ( bind(listen_fd, (sockaddr*) &server_addr, len) < 0 )
{
handdle_error(const_cast<char*> ("bind failed"));
}
if ( listen(listen_fd, MAX_LISTEN_QUEUE) < 0)
{
handdle_error(const_cast<char*> ("listen failed"));
}
return listen_fd;
}
int main()
{
int sock_fd, max_fd = 0;
sockaddr_in client;
socklen_t len = sizeof(sockaddr_in);
char buf[MAX_BUFFER_SIZE];
fd_set global_rdfs, current_rdfs;
bzero(&client, sizeof(client));
int listen_fd = listen_and_get_sockfd();
FD_ZERO(&global_rdfs); // 全局文件描述符置0;重置
FD_SET(listen_fd, &global_rdfs); // 设定想要关注的文件描述符
max_fd = max_fd > listen_fd? max_fd : listen_fd; // 记录最大的文件描述符的数量
while (1)
{
current_rdfs = global_rdfs; // 重置
if( select(max_fd + 1, ¤t_rdfs, NULL, NULL, NULL) < 0)
{
// 注意第一个参数 ==》 最大文件描述符 + 1 ,易错点
// 阻塞监听max_fd个文件描述符,与recv阻塞监听的效果类似,不同在于这里监听的对象不止一个
// 后面三个NULL,对应 写文件描述符集,异常文件描述符集,阻塞
handdle_error(const_cast<char*> ("select failed"));
}
for (int i = 0; i <= max_fd; ++i) // 循环查看是哪个描述符,产生了新的内容
{
if ( FD_ISSET(i, ¤t_rdfs) ) // 查看是否是第 i 个描述符产生了新内容
{
if (i == listen_fd) // 如果是描述符是 监听描述符
{
if( (sock_fd = accept(listen_fd, (sockaddr*) &client, &len)) < 0)
{
handdle_error(const_cast<char*> ("accpet failed"));
}
FD_CLR(i, ¤t_rdfs); // 清除这部分产生的内容信息,防止下次信号。
max_fd = max_fd > sock_fd? max_fd : sock_fd; // 查看文件描述符是否增加
FD_SET(sock_fd, &global_rdfs); // 加入到关注文件描述列表
printf("client in : %d ", sock_fd);
}
else // 其它文件描述符
{
// printf("read socket : %s", i )
bzero(buf, sizeof(buf));
int bytes = recv(i, buf, MAX_BUFFER_SIZE, 0);
if (bytes < 0)
{
handdle_error(const_cast<char*> ("recv failed"));
}
if (bytes == 0) // 接受FIN信号
{
FD_CLR(i, &global_rdfs); // 取消关注该文件描述符号
close(i);
continue;
}
printf("buf : %s\n", buf);
send(i, buf, strlen(buf), 0);
}
}
}
}
}
/*
BUG 日志:
1. 轮询fd的时候,由于 < max_fd,导致一直没有结果出现,加上<=后,正常工作。
*/
EPOLL-服务端模型
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctime>
#include <sys/epoll.h>
#include <fcntl.h>
const short PORT = 15521;
const int MAX_LISTEN_QUEUE = 5;
const int MAX_BUFFER_SIZE = 1024;
const int MAX_EVENTS = 100;
const int ERR = -1;
const int EXIT = -2;
const int OK = 0;
void handdle_error(char * msg)
{
perror(msg);
abort();
}
int listen_and_get_sockfd()
{
int listen_fd, opt = 1;
sockaddr_in server_addr;
socklen_t len = sizeof(sockaddr_in);
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
if ( ( listen_fd = socket(AF_INET, SOCK_STREAM, 0) ) < 0 )
{
handdle_error(const_cast<char*> ("create listen_fd falied"));
}
if ( setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
{
handdle_error(const_cast<char*>("setsockopt error"));
}
if ( bind(listen_fd, (sockaddr*) &server_addr, len) < 0 )
{
handdle_error(const_cast<char*> ("bind failed"));
}
if ( listen(listen_fd, MAX_LISTEN_QUEUE) < 0)
{
handdle_error(const_cast<char*> ("listen failed"));
}
return listen_fd;
}
int process_data(int fd)
{
char buf[MAX_BUFFER_SIZE];
int bytes;
bytes = recv(fd, buf, MAX_BUFFER_SIZE, 0);
if (bytes < 0)
{
perror("recv error");
return ERR;
}
if (bytes == 0)
{
return EXIT;
}
printf("buf : %s", buf);
send(fd, buf, bytes, 0);
return OK;
}
int main()
{
int sock_fd, fds, tmp_return_value;
sockaddr_in client;
socklen_t len = sizeof(sockaddr_in);
struct epoll_event tmp_ev, events[MAX_EVENTS]; // 创建时间的临时对象
bzero(&client, sizeof(client));
int epoll_fd = epoll_create(MAX_EVENTS); // 创建 epoll,指定epoll容纳的文件描述符数量
if (epoll_fd < 0)
{
handdle_error(const_cast<char*>("create epoll error"));
}
int listen_fd = listen_and_get_sockfd();
fcntl(listen_fd, F_SETFL, O_NONBLOCK); // 设置监听文件描述为 非阻塞模式
tmp_ev.data.fd = listen_fd; // 设置文件描述符
tmp_ev.events = EPOLLIN; // 设置事件的类型 EPOLLIN 表示有数据进来,可以读取
tmp_return_value = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &tmp_ev); // 将文件描述符及其对应的事件,添加到epoll中,予以关注
if (tmp_return_value < 0)
{
handdle_error(const_cast<char*>("control epoll error"));
}
while (1)
{
// 检测超时 time_out
fds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 返回有相应的文件描述符
// 表示事件,-1为永远等待,
if (fds < 0)
{
handdle_error(const_cast<char*> ("epoll wait error"));
}
for (int i = 0; i < fds; ++i)
{
if (events[i].data.fd == listen_fd)
{
sock_fd = accept(listen_fd, (sockaddr*) &client, &len);
if (sock_fd < 0)
{
handdle_error(const_cast<char*> ("accept error"));
}
tmp_ev.data.fd = sock_fd;
tmp_ev.events = EPOLLIN | EPOLLET; // Edge trigger 边缘触发模式
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &tmp_ev); // 添加到关注列表中
}
else
{
tmp_return_value = process_data(events[i].data.fd); // 处理数据
if (tmp_return_value == EXIT)
{
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &tmp_ev); // 删除对文件描述符的关注
close(events[i].data.fd); // 关闭文件描述符
}
}
}
}
}