网络编程
网络编程
ZEROKO14网络基础
- OSI七层,TCP/IP四层模型结构
- 常见网络协议格式
- 网络字节序和主机字节序之间的转换(大端法和小端法)
- tcp服务器端/客户端通信流程
分层模型
网络分层OSI 7层模型
OSI是Open System Interconnection的缩写, 意为开放式系统互联. 国际标准化组织(ISO)制定了OSI模型, 该模型定义了不同计算机互联的标准, 是设计和描述计算机网络通信的基本框架.
- 物理层—双绞线,光纤(传输介质),将模拟信号转换为数字信号(通过调制解调器modemn)
- 数据链路层—数据校验,定义了网络传输的基本单位-帧 ARP,RARP协议
- 网络层—定义网络,两台机器之间传输的路径选择点到点的传输 IP协议
- 传输层—传输数据 TCP,UDP,端到端的传输 (不需要考虑中间经过的点) TCP,UDP协议
- 会话层—通过传输层建立数据传输的通道.
- 表示层—编解码,翻译工作.
- 应用层—为客户提供各种应用服务,email服务,ftp服务,ssh服务
详解
| 分层 | 功能 |
|---|---|
| 物理层 | 主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。 |
| 数据链路层 | 定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1 |
| 网络层 | 在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。 |
| 传输层 | 定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。 |
| 会话层 | 通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。 |
| 表示层 | 可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换。 |
| 应用层 | 是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。 |
TCP/IP 4层模型
上面的OSI模型只是一个理想模型,实际上现在应用的是TCP/IP四层模型
TCP/IP网络协议栈分为应用层(Application)、传输层(Transport)、网络层(Network)和链路层(Link)四层
分层的含义如下图:
传输层及其以下的机制由内核提供,应用层由用户进程提供(后面将介绍如何使用socket API编写应用程序),应用程序对通讯数据的含义进行解释,而传输层及其以下处理通讯的细节,将数据从一台计算机通过一定的路径发送到另一台计算机。应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示:
通信过程: 其实就是发送端层层打包, 接收方层层解包.
不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做帧(frame)。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理。
TCP/IP协议分层通讯全过程
链路层有以太网、令牌环网等标准,链路层负责网卡设备的驱动、帧同步(即从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。交换机是工作在链路层的网络设备,可以在不同的链路层网络之间转发数据帧(比如十兆以太网和百兆以太网之间、以太网和令牌环网之间),由于不同链路层的帧格式不同,交换机要将进来的数据包拆掉链路层首部重新封装之后再转发。
链路层有以太网、令牌环网等标准,链路层负责网卡设备的驱动、帧同步(即从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。交换机是工作在链路层的网络设备,可以在不同的链路层网络之间转发数据帧(比如十兆以太网和百兆以太网之间、以太网和令牌环网之间),由于不同链路层的帧格式不同,交换机要将进来的数据包拆掉链路层首部重新封装之后再转发。
网络层的IP协议是构成Internet的基础。Internet上的主机通过IP地址来标识,Inter-net上有大量路由器负责根据IP地址选择合适的路径转发数据包,数据包从Internet上的源主机到目的主机往往要经过十多个路由器。路由器是工作在第三层的网络设备,同时兼有交换机的功能,可以在不同的链路层接口之间转发数据包,因此路由器需要将进来的数据包拆掉网络层和链路层两层首部并重新封装。IP协议不保证传输的可靠性,数据包在传输过程中可能丢失,可靠性可以在上层协议或应用程序中提供支持。
网络层负责点到点(ptop,point-to-point)的传输(这里的“点”指主机或路由器),而传输层负责端到端(etoe,end-to-end)的传输(这里的“端”指源主机和目的主机)。传输层可选择TCP或UDP协议。
TCP是一种面向连接的、可靠的协议,有点像打电话,双方拿起电话互通身份之后就建立了连接,然后说话就行了,这边说的话那边保证听得到,并且是按说话的顺序听到的,说完话挂机断开连接。也就是说TCP传输的双方需要首先建立连接,之后由TCP协议保证数据收发的可靠性,丢失的数据包自动重发,上层应用程序收到的总是可靠的数据流,通讯之后关闭连接。
UDP是无连接的传输协议,不保证可靠性,有点像寄信,信写好放到邮筒里,既不能保证信件在邮递过程中不会丢失,也不能保证信件寄送顺序。使用UDP协议的应用程序需要自己完成丢包重发、消息排序等工作。
目的主机收到数据包后,如何经过各层协议栈最后到达应用程序呢?其过程如下图所示:
以太网驱动程序首先根据以太网首部中的“上层协议”字段确定该数据帧的有效载荷(payload,指除去协议首部之外实际传输的数据)是IP、ARP还是RARP协议的数据报,然后交给相应的协议处理。假如是IP数据报,IP协议再根据IP首部中的“上层协议”字段确定该数据报的有效载荷是TCP、UDP、ICMP还是IGMP,然后交给相应的协议处理。假如是TCP段或UDP段,TCP或UDP协议再根据TCP首部或UDP首部的“端口号”字段确定应该将应用层数据交给哪个用户进程。IP地址是标识网络中不同主机的地址,而端口号就是同一台主机上标识不同进程的地址,IP地址和端口号合起来标识网络中唯一的进程。
虽然IP、ARP和RARP数据报都需要以太网驱动程序来封装成帧,但是从功能上划分,ARP和RARP属于链路层,IP属于网络层。虽然ICMP、IGMP、TCP、UDP的数据都需要IP协议来封装成数据报,但是从功能上划分,ICMP、IGMP与IP同属于网络层,TCP和UDP属于传输层。
协议
协议事先约定好, 大家共同遵守的一组规则, 如交通信号灯.从应用程序的角度看, 协议可理解为数据传输和数据解释的规则;可以简单的理解为各个主机之间进行通信所使用的共同语言.
当原始协议经过不断增加完善改进, 最终形成了一个稳定的完整的传输协议, 被广泛应用于各种文件传输, 该协议逐渐就成了一个标准协议.
几种常见的标准协议
- 传输层 常见协议有TCP/UDP协议。
- 应用层 常见的协议有HTTP协议,FTP协议。
- 网络层 常见协议有IP协议、ICMP协议、IGMP协议。
- 网络接口层 常见协议有ARP协议、RARP协议。
TCP协议注重数据的传输。http协议着重于数据的解释。
具体协议含义
TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。FTP文件传输协议(File Transfer Protocol)IP协议是因特网互联协议(Internet Protocol)ICMP协议是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。RARP是反向地址转换协议,通过MAC地址确定IP地址。
网络相关名词
设备相关
- 路由(名词)
数据包从源地址到目的地址所经过的路径,由一系列路由节点组成。 - 路由(动词)
某个路由节点为数据包选择投递方向的选路过程。
路由节点
一个具有路由能力的主机或路由器,它维护一张路由表,通过查询路由表来决定向哪个接口发送数据包。
以太网交换机工作原理
以太网交换机是基于以太网传输数据的交换机,以太网采用共享总线型传输媒体方式的局域网。以太网交换机的结构是每个端口都直接与主机相连,并且一般都工作在全双工方式。交换机能同时连通许多对端口,使每一对相互通信的主机都能像独占通信媒体那样,进行无冲突地传输数据。
以太网交换机工作于OSI网络参考模型的第二层(即数据链路层),是一种基于MAC(Media Access Control,介质访问控制)地址识别、完成以太网数据帧转发的网络设备。
交换机刚启动时,MAC地址表中无表项。当接入PC的时候,交换机开始进行学习MAC地址.
交换机对数据帧的转发与过滤:
单播帧的转发:
- PCA发出目的到PCD的单播数据帧
- 交换机根据帧中的目的地址,从相应的端口E1/0/4发送出去
- 交换机不在其他端口上转发此单播数据帧
广播、组播和未知单播帧的转发:
交换机会把广播、组播和未知单播帧从所有其他端口发送出去(除了接收到帧的端口)
路由表(Routing Table)
在计算机网络中,路由表或称路由择域信息库(RIB)是一个存储在路由器或者联网计算机中的电子表格(文件)或类数据库。路由表存储着指向特定网络地址的路径。
路由条目
路由表中的一行,每个条目主要由**目的网络地址(Destination)、子网掩码(Genmask)、下一跳地址(GateWay)、发送接口(Iface)**四部分组成,如果要发送的数据包的目的网络地址匹配路由表中的某一行,就按规定的接口发送到下一跳地址。
缺省路由条目
路由表中的最后一行,主要由下一跳地址和发送接口两部分组成,当目的地址与路由表中其它行都不匹配时,就按缺省路由条目规定的接口发送到下一跳地址。
路由器工作原理
路由器(Router)是连接因特网中各局域网、广域网的设备,它会根据信道的情况自动选择和设定路由,以最佳路径,按前后顺序发送信号的设备。
传统地,路由器工作于OSI七层协议中的第三层,其主要任务是接收来自一个网络接口的数据包,根据其中所含的目的地址,决定转发到下一个目的地址。因此,路由器首先得在转发路由表中查找它的目的地址,若找到了目的地址,就在数据包的帧格前添加下一个MAC地址,同时IP数据包头的TTL(Time To Live)域也开始减数, 并重新计算校验和。当数据包被送到输出端口时,它需要按顺序等待,以便被传送到输出链路上。
路由器在工作时能够按照某种路由通信协议查找设备中的路由表。如果到某一特定节点有一条以上的路径,则基本预先确定的路由准则是选择最优(或最经济)的传输路径。由于各种网络段和其相互连接情况可能会因环境变化而变化,因此路由情况的信息一般也按所使用的路由信息协议的规定而定时更新。
网络中,每个路由器的基本功能都是按照一定的规则来动态地更新它所保持的路由表,以便保持路由信息的有效性。为了便于在网络间传送报文,路由器总是先按照预定的规则把较大的数据分解成适当大小的数据包,再将这些数据包分别通过相同或不同路径发送出去。当这些数据包按先后秩序到达目的地后,再把分解的数据包按照一定顺序包装成原有的报文形式。路由器的分层寻址功能是路由器的重要功能之一,该功能可以帮助具有很多节点站的网络来存储寻址信息,同时还能在网络间截获发送到远地网段的报文,起转发作用;选择最合理的路由,引导通信也是路由器基本功能;多协议路由器还可以连接使用不同通信协议的网络段,成为不同通信协议网络段之间的通信平台。
路由和交换之间的主要区别就是交换发生在OSI参考模型第二层(数据链路层),而路由发生在第三层,即网络层。这一区别决定了路由和交换在移动信息的过程 中需使用不同的控制信息,所以两者实现各自功能的方式是不同的。
如上图图所示:路由器A和B是经过配置的路由在他们的路由表中就保存了相应的网段和接口,如果主机1.1要发送数据包给主机3.1:
- 因为IP地址不在同一网段,主机就会将数据包发送给本网段的网关路由器A。
- 路由器A接收到数据包,查看数据包IP首部中的目标IP地址,在查找自己的路由表。数据包的目标IP地址是3.1.属于3.0网段路由器A在路由表中查到3.0网段转发的接口是S0接口。于是,路由器就将数据包从S0接口转发出去。
- 每个路由器但是按这个步骤去转发数据的,直到到达了路由器B,用同样的方法,从E0口转发出去,主机3.1接受到这个数据包。
[同网段和不同网段主机通信的区别就在于] 同网段直接查找主机,而不同网段需要将数据包发送给网关。
其他名词
半双工/全双工
- Full-duplex(全双工)全双工是在通道中同时双向数据传输的能力。
- Half-duplex(半双工)在通道中同时只能沿着一个方向传输数据。
DNS服务器
DNS 是域名系统 (Domain Name System) 的缩写,是因特网的一项核心服务,它作为可以将域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP地址串。
它是由解析器以及域名服务器组成的。域名服务器是指保存有该网络中所有主机的域名和对应IP地址,并具有将域名转换为IP地址功能的服务器。
操作系统中可通过hosts文件手动设置如何解析域名
- Linux/Mac:
/etc/hosts - Windows:
C:\Windows\System32\drivers\etc\hosts - Android系统:
/system/etc/
hosts文件格式: IP地址 域名(#行表示注释)
修改hosts后需要刷新dns缓存使之生效:
- Windows:
ipconfig/flushdns - Mac:
sudo killall -HUP mDNSResponder
windows下如何修改hosts文件
host文件要先修改属性,取消“只读”再修改
使用管理员身份运行一个文件编辑程序,再打开hosts文件进行修改,要么就是修改hosts文件的权限(不建议)
局域网(LAN)
local area network,一种覆盖一座或几座大楼、一个校园或者一个厂区等地理区域的小范围的计算机网。
- 覆盖的地理范围较小,只在一个相对独立的局部范围内联,如一座或集中的建筑群内。
- 使用专门铺设的传输介质进行联网,数据传输速率高(10Mb/s~10Gb/s)
- 通信延迟时间短,可靠性较高
- 局域网可以支持多种传输介质
广域网(WAN)
wide area network,一种用来实现不同地区的局域网或城域网的互连,可提供不同地区、城市和国家之间的计算机通信的远程计算机网。
覆盖的范围比局域网(LAN)和城域网(MAN)都广。广域网的通信子网主要使用分组交换技术。
广域网的通信子网可以利用公用分组交换网、卫星通信网和无线分组交换网,它将分布在不同地区的局域网或计算机系统互连起来,达到资源共享的目的。如互联网是世界范围内最大的广域网。
- 适应大容量与突发性通信的要求;
- 适应综合业务服务的要求;
- 开放的设备接口与规范化的协议;
- 完善的通信服务与网络管理。
MTU
MTU:通信术语 最大传输单元(Maximum Transmission Unit,MTU)
是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡、串口等)。
以下是一些协议的MTU:
FDDI协议:4352字节
以太网(Ethernet)协议:1500字节
PPPoE(ADSL)协议:1492字节
X.25协议(Dial Up/Modem):576字节
Point-to-Point:4470字节
网络相关命令
ping命令
查看网络信息
- mac/Linux:
ifconfig(interface configuration) - windows:
ipconfig - linux下还可以使用
nmcli命令(NetworkManager)(部分linux系统需要安装)
查询路由表
- Windows:
route print - Mac:
netstat -rn - Linux:
route -n
查询经过的路由器信息
- Mac/Linux:
traceroute + IP/域名 - Linux:
tracepath + IP/域名 - Windows:
tracert + IP/域名
打印公网ip
Linux/Mac: echo "cat</dev/tcp/ns1.dnspod.net/6666"|bash
网络程序的设计模式
C/S设计模式
传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
优点:
- 客户端在本机上可以保证性能, 可以将数据缓存到本地, 提高数据的传输效率, 提高用户体验效果.
- 客户端和服务端程序都是由同一个开发团队开发, 协议选择比较灵活.
缺点:
- 服务器和客户端都需要开发,工作量相对较大, 调试困难, 开发周期长;
- 从用户的角度看, 需要将客户端安装到用户的主机上, 对用户主机的安全构成威胁.
B/S设计模式
浏览器(browser)/服务器(server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。
优点:
- 无需安装客户端, 可以使用标准的浏览器作为客户端;
- 只需要开发服务器,工作量相对较小;
- 由于采用标准的客户端, 所以移植性好, 不受平台限制.
- 相对安全,不用安装软件
缺点:
- 由于没有客户端, 数据缓冲不尽人意, 数据传输有限制, 用户体验较差;
- 通信协议选择只能使用HTTP协议,协议选择不够灵活;
对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯公司所采用的通信协议,即为ftp协议的修改剪裁版。
因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。
C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。 B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。
B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。
因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。
tcp原理
三次握手四次挥手
建立连接需要三次握手,断开连接需要四次挥手
段2的箭头上标着SYN, 8000(0), ACK1001, ,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是1001,带有一个mss(Maximum Segment Size,最大报文长度)选项值为1024。
SYS—–>synchronous
ACK—–>acknowledgement
FIN——>finish
ACK表示确认序号, 确认序号的值是对方发送的序号值+数据的长度, 特别注意的是SYN和FIN本身也会占用一位.
建立三次握手的过程
客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的段1
客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯
服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。
客户必须再次回应服务器端一个ACK报文,这是报文段3
客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为“三方握手(three-way-handshake)”。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。
在TCP通讯中,如果一方收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。例如,服务器并没有任何进程使用8080端口,我们却用telnet客户端去连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端的telnet程序收到RST段后报告错误Connection refused
数据传输的过程
- 客户端发出段4,包含从序号1001开始的20个字节数据
- 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback
- 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据
在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。
关闭连接(四次挥手)的过程
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
- 客户端发出段7,FIN位表示关闭连接的请求
- 服务器发出段8,应答客户端的关闭连接请求
- 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求
- 客户端发出段10,应答服务器的关闭连接请求
建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。
当双方刚好同时关闭的时候(概率很低),是存在ACK和FIN,ACK包合并为同一个发送的情况的,这种情况只需要三步挥手便结束了.
[重点的问题理解]
为什么四次挥手中,被动被请求关闭连接的一方要发送**一个确认包和一个请求关闭连接包,**而不能仅仅发一个请求连接包?
因为第一个确认包仅仅表示收到了对方关闭连接的请求,但自身却还可能有未发送完的数据,第二个请求关闭连接包才真正表示自身的发送也完成可以关闭了.即表示第一个包和第二个包之间依然可以发送未发送完的数据包.
为什么最先请求关闭连接的一方在第四次挥手发送确认包后还要等待2倍MSL时间?
让四次挥手的过程更可靠, 确保最后一个发送给对方的ACK到达;
若对方没有收到ACK应答, 对方会再次发送FIN请求关闭, 此时在2MS时间内被动关闭方仍然可以发送ACK给对方
补充解释:发送的ACK有可能丢失,如果ACK丢失,另一方会重发关闭连接请求包,两个MSL时间可以大幅度增加接收到重发的关闭连接请求包的情况的可能.但是情况依旧不完美,因为重发的关闭请求包也存在丢失的可能,如果也丢失了,重发的时间又超过了2倍MSL时间,那么他将等不到发回来的确定包,即等不到关闭的时机.(脑补应该用心跳包解决)
为了保证在2MS时间内, 不能启动相同的SOCKET-PAIR.
TIME_WAIT一定是出现在主动关闭的一方, 也就是说2MS是针对主动关闭一方来说的;由于TCP有可能存在丢包重传, 丢包重传若发给了已经断开连接之后相同的socket-pair(该连接是新建的, 与原来的socket-pair完全相同, 双方使用的是相同的IP和端口), 这样会对之后的连接造成困扰, 严重可能引起程序异常.因此很多操作系统实现的时候, 只要端口被占用, 服务就不能启动.通过这个方式尽可能解决该程序异常. (实际上服务端每次都是使用的同一个端口,而客户端一般设置为由内核分配随机端口,会避免这种问题发生.因此实际上很难发生这样的情况)
主动断开方,查看 netstat -anp | grep 端口号会发现状态为TIME_WAIT
tcp状态转换
socket-pair的概念: 客户端与服务端连接其实是一个连接对, 可以通过使用netstat -anp | grep 端口号进行查看(linux),他也可以查询是什么进程占用着端口
说明: 上图中粗线表示主动方, 虚线表示被动方, 细线部分表示一些特殊情况
三次握手过程
客户端–>服务端
客户端:
SYN_SENT— connect()服务端:
LISTEN— listen()SYN_RCVD— 收到连接请求的时候状态转换
数据传输过程中
状态不会发生变化,一直都是ESTABLISHED状态
四次挥手过程
客户端<–>服务端
主动关闭方
FIN_WAIT_1— close()FIN_WAIT_2— 收到被动接收方确认断开连接请求的时候状态转换TIME_WAIT— 收到被动接收方的断开请求时候的状态转换被动关闭方
CLOSE_WAIT— 收到断开连接请求的时候状态转换LAST_ACK— close()
主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是**maximum segment lifetime(最大分节生命期)**,这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。
滑动窗口
主要作用: 滑动窗口主要是进行流量控制的.
见下图:如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会导致接收缓冲区满而丢失数据。TCP协议通过“滑动窗口(Sliding Window)”机制解决这一问题。
- 发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
- 发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
- 接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
- 接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
- 发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。
- 接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
- 接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
- 接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
- 接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。
win表示告诉对方我这边缓冲区大小是多少, mss表示告诉对方我这边最多一次可以接收多少数据, 你最好不要超过这个长度.
在客户端给服务端发包的时候, 不一定是非要等到服务端返回响应包, 由于客户端知道服务端的窗口大小, 所以可以持续多次发送, 当发送数据达到对方窗口大小了就不再发送, 需要等到对方进行处理, 对方处理之后可继续发送.
从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
mss和MTU的区别
MTU: 最大传输单元 Maximum Transmission Unit,MTU)
是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为 单位). 最大传输单元这个参数通常与通信接口有关(网络接口卡、串 口等), 这个值如果设置为太大会导致丢包重传的时候重传的数据量较大, 图中的最大值是1500, 其实是一个经验值.mss: 最大报文长度只是在建立连接的时候, 告诉对方我最大能够接收多少数据, 在数据通信的过程中就没有mss了.
概括来讲,MTU是以太网数据链路层中约定的数据载荷部分最大长度,数据不超过它时就无需分片。
MSS是传输层的概念,由于数据往往很大,会超出MTU,所以我们之前在网络层中学习过IP分片的知识,将很大的数据载荷分割为多个分片发送出去。
TCP为了IP层不用分片主动将数据包切割为MSS大小。
一个等式可见他两关系匪浅:
$$
MSS = MTU - IP header头大小 - TCP 头大小
$$
以太网帧格式
以太网帧格式就是包装在网络接口层(数据链路层)的协议
以太网帧的类型为 0x0806: 表示 ARP 类型;0X8035 表示 RARP 类型
其中的源地址和目的地址是指网卡的硬件地址(也叫MAC地址),长度是48位,是在网卡出厂时固化的。可在shell中使用ifconfig命令查看,“HWaddr 00:15:F2:14:9E:3F”部分就是硬件地址。协议字段有三种值,分别对应IP、ARP、RARP。帧尾是CRC校验码。
以太网帧中的数据长度规定最小46字节,最大1500字节,ARP和RARP数据包的长度不够46字节,要在后面补填充位(填充位的内容没有定义,与具体实现相关)。最大值1500称为以太网的最大传输单元(MTU),不同的网络类型有不同的MTU,如果一个数据包从以太网路由到拨号链路上,数据包长度大于拨号链路的MTU,则需要对数据包进行分片(fragmentation)。ifconfig命令输出中也有“MTU:1500”。注意,MTU这个概念指数据帧中有效载荷的最大长度,不包括帧头长度。
以ARP为例介绍以太网帧格式
ARP数据报格式
地址解析协议ARP (Address Resolution Protocol)
在网络通讯时,源主机的应用程序知道目的主机的IP地址和端口号,却不知道目的主机的硬件地址,而数据包首先是被网卡接收到再去处理上层协议的,如果接收到的数据包的硬件地址与本机不符,则直接丢弃。因此在通讯前必须获得目的主机的硬件地址。ARP协议就起到这个作用。源主机发出ARP请求,询问“IP地址是192.168.0.1的主机的硬件地址是多少”,并将这个请求广播到本地网段(以太网帧首部的硬件地址填FF:FF:FF:FF:FF:FF表示广播),目的主机接收到广播的ARP请求,发现其中的IP地址与本机相符,则发送一个ARP应答数据包给源主机,将自己的硬件地址填写在应答包中。
每台主机都维护一个ARP缓存表,可以用arp -a命令查看。缓存表中的表项有过期时间(一般为20分钟),如果20分钟内没有再次使用某个表项,则该表项失效,下次还要发ARP请求来获得目的主机的硬件地址。
ARP数据报的格式如下所示:
源MAC地址、目的MAC地址在以太网首部和ARP请求中各出现一次,对于链路层为以太网的情况是多余的,但如果链路层是其它类型的网络则有可能是必要的。硬件类型指链路层网络类型,1为以太网,协议类型指要转换的地址类型,0x0800为IP地址,后面两个地址长度对于以太网地址和IP地址分别为6和4(字节),op字段为1表示ARP请求,op字段为2表示ARP应答。
目的端mac地址是通过发送端发送ARP广播, 接收到该ARP数据的主机先判断是否是自己的IP, 若是则应答一个ARP应答报文, 并将mac地址填入应答报文中; 若目的IP不是自己的主机IP, 则直接丢弃该ARP请求报文.
如上图所示,ARP协议目的:解决同一个局域网上的主机或路由器的ip地址和硬件地址的映射问题
抓包命令
抓包命令: tcpdump -ntx
思考题:如果源主机和目的主机不在同一网段,ARP请求的广播帧无法穿过路由器,源主机如何与目的主机通信?
那么就要通过ARP找到一个位于本局域网上的某个路由器的硬件地址,然后把分组发送个这个路由器,让这个路由器把分组转发给下一个网络,剩下的工作就由下一个网络来做
IP段格式
IP协议是TCP/IP协议族的基石,它为上层提供无状态、无连接、不可靠的服务
- 无状态:指IP通信双方不同步传输数据的状态信息,因此所有IP数据报的发送,传输,接收都是相互独立的。这种服务最大缺点是无法处理乱序和重复的IP数据报。优点是简单高效,和UDP协议与HTTP协议相同,都是无状态协议。
- 无连接:指IP通信双方都不长久的维持对方的任何信息。这表示上层协议每次发送数据,都需要明确指定对方的IP地址。
- 不可靠:指IP协议不能IP数据报能准确到达接收端,只是会尽最大努力。一旦发送失败,就通知上层协议,而不会试图重发。
- 4位版本号:指定IP协议的版本,对于IPv4来说,其值为4,其它IPv4扩展版本则具有不同的版本号(如SIP协议和PIP协议)
- 4位头部长度:表示IP头部有多少个32bit字(4字节)。因为4位最大15,所以IP头部最长为60字节。
- 8位服务类型:3位优先级权字段(现已被忽略),4位TOS字段和1位保留字段(必须置0).4位TOS字段分别表示:最小延迟,最大吞吐量、最高可靠性和最小费用,其中最多1个能置为1。
- 16位总长度:指整个IP数据包的长度,字节为单位。最长65535字节,由于长度超过MTU的数据报将被分片传输,所以实际传输的长度没有达到最大值。
- 16位标识:唯一标识主机发送的每个数据报。初始值由系统随机生成,每发送一个数据报,其值加一。该值在数据报分片时被复制到每个分片中,因此同一个数据报的所有分片标识值都相同
- 3位标志:第一位保留,第二位表示是否禁止分片,如果设置了该位,IP数据报长度超过MTU将被丢弃,返回错误。第三位表示更多分片,除了最后一个分片,其它都要置它为1.
- 13位分片偏移:该分片相较于原始IP数据报开始处(仅指数据部分)的偏移。实际偏移值是该值左移3位得到。因此除了最后一个分片,每个分片的数据部分长度必须是8的整数倍。
- 8位生存时间:数据报到达目的地之前允许经过的路由器跳数。每经过一个路由,该值减一,为0时被丢弃。并返回TCMP错误报文。
- 8位协议:用于区分上层协议。ICMP为1,TCP为6,UDP为17。
- 16位头部校验和:由发送端填充,接收端对其使用CRC算法检验数据是否被损坏。
- 32位源端IP地址:标识数据报的发送端。在传输过程中保持不变
- 32位源目的端IP地址:标识数据报的接收端。在传输过程中保持不变
- 选项字段:可变长的可选信息,最多40字节。可用的IP选项有:
- 记录路由:将数据包经由的所有路由器IP填入该段。
- 时间戳:将数据报在每个路由器被转发时的时间填入该段。
- 松散源路由选择:指定路由器IP地址列表,数据报发送过程中必须经过其中所有路由器
- 严格源路由选择:类似上面,数据报只能经过被指定的路由器。
/////////////////////////////////////////////未解决///////////////////////////////
思考题:想一想,前面讲了以太网帧中的最小数据长度为46字节,不足46字节的要用填充字节补上,那么如何界定这46字节里前多少个字节是IP、ARP或RARP数据报而后面是填充字节?
ip分片
当IP数据报的长度超过帧的MTU时,它将被分片传输。分片可能发生在发送端,也可能发生在中转路由器上,而且在传输过程中可能被多次分片,只有在最终目标机器上,这些分片才会在内核中被IP模块重新组装。
IP头部中的数据报标识、标志、和片偏移为IP的分片和重组提供了足够的信息。
一个数据报的每个分片都具有自己的IP头部,且具有相同的标识,但具有不同的片偏移,除了最后一个分片之外都设置了MF标志。
ip路由的工作模式
IP模块收到来自数据链路层的IP数据报,首先对数据报的头部做CRC校验,无误后开始分析头部具体信息。
如果IP数据报头部设置了源站选路选项,则IP模块调用数据报转发子模块来处理该数据报。
如果该数据报的头部目标IP地址是本机的某个IP地址,或者广播地址,则IP模块根据数据报协议字段来决定发送给哪个上层应用。如果不是本机,则掉用数据报转发子模块来处理该数据报。
数据报转发模块检查系统是否允许转发,不允许则丢弃。允许则将该数据报执行一些操作,就将它交给IP数据报输出模块。
IP数据报根据路由表计算下一跳路由。
IP输出队列存放所有等待发送的IP数据报。
路由机制
- 查找路由表中和数据报的目标IP完全匹配的主机IP地址,如果找到,就直接使用该项,没有就到第二步
- 查找路由表中和目标IP具有相同的网路ID的IP地址,如Gateway,有就使用,否则来到第三步
- 选择默认路由项,这一般为网关。
执行route命令可查看路由表
Destination:目标网络或主机Gateway:网关地址,*表示目标与本机在同一个网络上,不需要路由。Genmask:网络掩码Flags:路由标志,U:该路由活动,H:该路由目标是一个主机,G:该路由目标是网关,D:该路由是重定向产生的,M:该路由被重定向修改过Metric:路由距离,达到目标网络所需的中转数Ref:路由项被引用的次数Use:该路由项被使用的次数Iface:该路由对应的输出网卡接口
IPv6
IPv6由40个字节的固定头部和可变长的扩展头部组成。
- 4位版本号:IP协议版本,IPv6值为6
- 8位通信类型:指示数据通信类型和优先级
- 20位流标志:用于某些对连接服务质量有特殊要求的通信。
- 16位净荷长度:指IPv6扩展头部和应用程序数据长度之和,不包含固定头部长度
- 8位下一个包头:指紧跟IPv6固定头部的包头类型,如扩展头或上层协议。
- 8位跳数限制:和IPv4的TTL含义相同
- 后两项IP地址:IPv6地址一般用16进制字符串表示,用‘:’分割为8组,每组两个字节。
UDP数据报格式
无连接的,不安全,不可靠的
UDP(UserDatagramProtocol)是一个简单的面向消息的传输层协议,尽管UDP提供标头和有效负载的完整性验证(通过校验和),但它不保证向上层协议提供消息传递,并且UDP层在发送后不会保留UDP 消息的状态。因此,UDP有时被称为不可靠的数据报协议。如果需要传输可靠性,则必须在用户应用程序中实现。
UDP使用具有最小协议机制的简单无连接通信模型。UDP提供数据完整性的校验和,以及用于在数据报的源和目标寻址不同函数的端口号。它没有握手对话,因此将用户的程序暴露在底层网络的任何不可靠的方面。如果在网络接口级别需要纠错功能,应用程序可以使用为此目的设计的传输控制协议(TCP)。
UDP是基于IP的简单协议,不可靠的协议。
UDP的优点:简单,轻量化。
UDP的缺点:没有流控制,没有应答确认机制,不能解决丢包、重发、错序问题。
并不是所有使用UDP协议的应用层都是不可靠的,应用程序可以自己实现可靠的数据传输,通过增加确认和重传机制,所以使用UDP 协议最大的特点就是速度快。
其他特点:
UDP 没有拥塞控制
网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用是很重要的。
UDP 支持一对一、一对多、多对一和多对多的交互通信。
[UDP的应用] UDP协议一般作为流媒体应用、语音交流、视频会议所使用的传输层协议,还有许多基于互联网的电话服务使用的VOIP(基于IP的语音)也是基于UDP运行的.这些实时应用要求源主机以恒定的速率发送数据,并且允许在网络出现拥塞时丢失一部分数据,但却不允许数据有太大的时延。UDP 协议正好适合这种要求。
UDP头部格式
- 通过IP地址来确定网络环境中的唯一的一台主机;
- 主机上使用端口号来区分不同的应用程序.
- [IP+端口] 唯一确定 [一台主机上的一个服务(应用程序)].
/etc/services中列出了所有规定的服务端口和对应的传输层协议,这是由IANA(Internet Assigned Numbers Authority)规定的,其中有些服务既可以用TCP也可以用UDP,为了清晰,IANA规定这样的服务采用相同的TCP或UDP默认端口号,而另外一些TCP和UDP的相同端口号却对应不同的服务。很多服务有规定的端口号,然而客户端程序的端口号却不必是规定的,往往是每次运行客户端程序时由系统自动分配一个空闲的端口号,用完就释放掉,称为ephemeral的端口号
发送端的UDP协议层只管把应用层传来的数据封装成段交给IP协议层就算完成任务了,如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息。
接收端的UDP协议层只管把收到的数据根据端口号交给相应的应用程序就算完成任务了,如果发送端发来多个数据包并且在网络上经过不同的路由,到达接收端时顺序已经错乱了,UDP协议层也不保证按发送时的顺序交给应用层。
通常接收端的UDP协议层将收到的数据放在一个固定大小的缓冲区中等待应用程序来提取和处理,如果应用程序提取和处理的速度很慢,而发送端发送的速度很快,就会丢失数据包,UDP协议层并不报告这种错误。
因此,使用UDP协议的应用程序必须考虑到这些可能的问题并实现适当的解决方案,例如等待应答、超时重发、为数据包编号、流量控制等。一般使用UDP协议的应用程序实现都比较简单,只是发送一些对可靠性要求不高的消息,而不发送大量的数据。例如,基于UDP的TFTP协议一般只用于传送小文件(所以才叫trivial的ftp),而基于TCP的FTP协议适用于各种文件的传输。
举例:
1 | //基于UDP的TFTP协议帧 |
wireshark抓包
udp连接机制
服务器在特定端口上收到UDP 数据包时,将通过以下两个步骤进行响应
- 服务器首先检查是否有任何当前侦听指定端口请求的程序正在运行。
- 如果该端口上没有程序正在接收数据包,则服务器将以 ICMP (ping) 数据包作为响应,以告知发送方目标不可达。
导致>>UDP洪水攻击 解决方案:大多数操作系统限制ICMP 数据包的响应速率
TCP数据流格式
稳定的, 安全的, 可靠的 TCP原理跳转
- TCP协议用于1对1,即不能用于基于广播和多播的应用程序
- TCP连接双方的收发数据次数不一定相同,即发送多次的数据包,可能会被对方1次全部接收
- TCP在发送数据报后,必须得到接收方的应答,才认为传输成功,所以是可靠的
- TCP采用超时重传机制,超过时间没收到应答,就会重新发送。
tcp头部结构
16位端口号:指定数据从哪个端口来,发送到哪个端口。
32位序号:一次TCP通信中,每段字节流的编号。如A与B通信,第一个报文中,序列值被系统初始化为随机值,之后的传输(A到B),该序号值将被设定为初始值加上第一个字节在整个字节流中的偏移。
32位确认号:用作对另一方发送的TCP报文段的响应,其值是收到的TCP报文段序号值加一。
4位头部长度:标识该TCP头部有多少个32bit字(4字节),即最长为60字节
6位标志位:
-
URG:标识紧急指针是否有效 -
ACK:标识确认号是否有效 -
PSH:提示接收端应立即从TCP接收缓存区中读走数据 -
RST:表示要求对方重新建立连接 -
SYN:表示请求建立一个连接 -
FIN:通知对方关闭连接16位窗口大小:TCP流量控制的一个手段,告诉对方本地TCP接收缓存区还能容纳多少字节的数据
16位校验和:由发送端填充,接收端对TCP报文段执行CRC算法校验数据是否损坏。
16位紧急指针:正的偏移量,它和序号字段的值相加表示最后一个紧急数据的下一个字节序号
选项:为可变长的可选信息
典型选项结构
- kind:说明选项的类型
- length:选项的总长度
- info:选项的具体信息
常见TCP的7种选项:
kind的常见类型解释
- kind=0:选项表结束
- kind=1:空操作,一般用于将TCP选项的总长度填充为4字节的整数倍
- kind=2:最大报文段长度选项,初次连接,双方通过此选项协商最大报文长度,TCP通常设置此为MTU-40字节,避免被分片
- kind=3:窗口扩大因子选项。TCP头部通知窗口为N,扩大因子为M,则实际接收通知窗口为N*(2的M次方),且只能出现在同步报文段中,否则被忽略
- kind=4:选择性确定(Selective Acknowledge,SACK)。如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP报文段后续所有报文段。而该选项则可解决这种问题。
- kind=5:SACK实际工作选项。告诉发送端本端已经收到的数据块,从而让发送端只发送丢失的数据
- kind=8:时间戳。提供通信双方较为精确的回路时间。
TCP连接机制
/////////////////////////////////
tcp协议未完待续
///////////////////////////////////
ip地址转换
下面函数名的理解:
p->表示点分十进制的字符串形式
to->到
n->表示network网络
inet_pton函数
将字符串形式的点分十进制IP转换为大端模式的网络IP(整形4字节数)
1 | int inet_pton(int af, const char *src, void *dst); |
参数说明:
af: AF_INET或AF_INET6,分别对应ipv4和ipv6
src: 字符串形式的点分十进制的IP地址
dst: 存放转换后的变量的地址
inet_ntop函数
网络IP转换为字符串形式的点分十进制的IP
1 | const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); |
参数说明:
af: AF_INET
src: 网络的整形的IP地址
dst: 转换后的IP地址,一般为字符串数组
size: dst的长度
返回值:
成功–返回指向dst的指针
失败–返回NULL, 并设置errno
另外有两个函数,与上面二者类似,仅有如下不同:inet_aton和inet_ntoa只支持ipv4类型的地址转换,而inet_pton和inet_ntop支持ipv4和ipv6类型的地址转换
网络字节序
- 大端: 低位地址存放高位数据, 高位地址存放低位数据(网络字节序)
- 小端: 低位地址存放低位数据, 高位地址存放高位数据
数据的高低位:右边为低位,左边为高位,E.g. 0x1234,34为低位数据,12为高位数据
网络传输用的是大端法, 如果机器用的是小端法, 则需要进行大小端的转换.
大小端转换
1 |
|
上述的几个函数, 如果本来不需要转换函数内部就不会做转换.
同步与异步
- 同步 : 发送一个请求,等待返回,然后再发送下一个请求
- 异步 : 发送一个请求,不等待返回,随时可以再发送下一个请求
同步可以避免出现死锁,读脏数据的发生,一般共享某一资源的时候用,如果每个人都有修改权限,同时修改一个文件,有可能使一个人读取另一个人已经删除的内容,就会出错,同步就会按顺序来修改。
异步则是可以提高效率了,现在cpu都是双核,四核,异步处理的话可以同时做多项工作,当然必须保证是可以并发处理的。
实际项目开发中会优先选择异步交互模型
四种情况
- 同步非阻塞: 若客户端发送数据之后, read函数不阻塞(文件描述符设置为非阻塞,但是一直循环read空转)
- 同步阻塞:客户端发送数据之后, read数据, 若对方不发送应答数据, 就一直阻塞.
- 异步阻塞: 比如: select poll epoll, 若没有事件发生, select 或者epoll可以一直阻塞
- 异步非阻塞: 比如: 将epoll设置非阻塞, 不管有没有事件发生都会立刻返回
异步需要有第三方参与通知才能实现
阻塞与非阻塞
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:
- 阻塞是指IO操作需要彻底完成后才返回到用户空间
- 非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成
DNS解析过程
在浏览器中输入www.magedu.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。
如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
Windows和Linux系统都会在本地缓存dns解析的记录,提高速度。
如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找 TCP/IP 参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
如果要查询的域名,不由本地DNS服务器区域解析,但该DNS服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。
如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(没有设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至13台根DNS,根DNS服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责 .com域的这台服务器。这台负责 .com域的服务器收到请求后,如果自己无法解析,它就会找一个管理 .com域的下一级DNS服务器地址(magedu.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找magedu.com域服务器,重复上面的动作进行查询,直至找到www.magedu.com主机。
如果用的是转发模式(设置转发器),此DNS服务器就会把请求转发至上一级ISP DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管是本地DNS服务器用是是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机。
域名解析服务器
- Pod DNS+:
- 首选:119.29.29.29
- 备选:182.254.116.116
- 114DNS:
- 首选:114.114.114.114
- 备选:114.114.114.115
- 阿里 AliDNS:
- 首选:223.5.5.5
- 备选:223.6.6.6
hosts文件:存储的是域名和IP的对应关系
- windows目录:
C:\Windows\System32\drivers\etc\hosts - mac/linux目录:
/etc/hosts
URL和URN
URL(Uniform Resource Locator): 统一资源定位符
表示资源位置的字符串:
协议://IP地址/路径和文件名http://www.ietf.org/rfc/rfc2396.txttelnet://192.0.2.16:80/ftp://ftp.is.co.za/rfc/rfc1808.txt
URN(Uniform Resource Name): 统一资源名称
P2P下载中使用的磁力链接
URI(Uniform Resource Identifier): 统一资源标识符
是一个紧凑的字符串用来标示抽象或物理资源, URL是URI的一种
让URI能成为URL的当然就是那个“访问机制”,“网络位置”。e.g.
http://orftp://URI可以没有协议,没有地址(IP/域名)
URI和URL的关系
从字面的包含关系上来说,URI包含URL
字符串长度来说,URL包含URI
红色部分+绿色部分 = URL
绿色部分 = URI
URL的静态请求: http://localhost/login.html
URL的动态请求: http://localhost/login?user=zhang&age=12
http:协议localhost:域名/login:服务器端要处理的指令(包括这里往下的部分都存在于http协议中的请求行的第二部分请求资源中)?:连接符,后面的内容是客户端给服务端提交的数据&:分隔符
正向/反向代理
- 正向代理是为客户端服务的
- 反向代理是为服务端服务的
正向代理
正向代理是位于客户端和原始服务器之间的服务器,为了能够从原始服务器获取请求的内容,客户端需要将请求发送给代理服务器,然后再由代理服务器将请求转发给原始服务器,原始服务器接受到代理服务器的请求并处理,然后将处理好的数据转发给代理服务器,之后再由代理服务器转发发给客户端,完成整个请求过程。
正向代理的典型用途就是为在防火墙内的局域网客户端提供访问Internet的途径 , 比如:
- 学校的局域网
- 单位局域网访问外部资源
反向代理
反向代理方式是指代理原始服务器来接受来自Internet的链接请求,然后将请求转发给内部网络上的原始服务器,并将从原始服务器上得到的结果转发给Internet上请求数据的客户端。那么顾名思义,反向代理就是位于Internet和原始服务器之间的服务器,对于客户端来说就表现为一台服务器,客户端所发送的请求都是直接发送给反向代理服务器,然后由反向代理服务器统一调配
防盗链
盗链 是指服务提供商自己不提供服务的内容,通过技术手段绕过其它有利益的最终用户界面(如广告),直接在自己的网站上向最终用户提供其它服务提供商的服务内容,骗取最终用户的浏览和点击率。受益者不提供资源或提供很少的资源,而真正的服务提供商却得不到任何的收益。
socket编程
传统的进程间通信借助内核提供的IPC机制进行, 但是只能限于本机通信, 若要跨机通信, 就必须使用网络通信.( 本质上借助内核-内核提供了socket伪文件的机制实现通信—-实际上是使用文件描述符), 这就需要用到内核提供给用户的socket API函数库.因为socket的伪文件机制, 所以可以使用文件描述符相关的函数read write
如下图, 一个文件描述符操作两个缓冲区, 这点跟管道是不同的, 管道是两个文件描述符操作一个内核缓冲区.
相关结构体
socket编程用到的重要的结构体:struct sockaddr
sockaddr结果参数使用sockaddr_in结构体变量来填充就可以了,内部划分得更细致
1 | //struct sockaddr结构说明: |
sockaddr_in在头文件
#include<netinet/in.h>或#include <arpa/inet.h>中定义有时候没有包括#include <netinet/in.h> ,而包括#include <arpa/inet.h>,在server端和client端一般要将端口号从主机序转换成网络序,那么需要htons( )函数就需要头文件 #include <arpa/inet.h>,就自然不用另一个了。可以通过编译啦。
查询函数所需的头文件用
man htons命令但是有些系统是需要netinet/in.h而不能是arpa/inet.h
主要函数
socket函数
创建socket
1 | int socket(int domain, int type, int protocol); |
参数说明:
domain: 协议版本
AF_INET IPV4
AF_INET6 IPV6
AF_UNIX AF_LOCAL本地套接字使用
type:协议类型
SOCK_STREAM 流式, 默认使用的协议是TCP协议
SOCK_DGRAM 报式, 默认使用的是UDP协议
protocal:
一般填0, 表示使用对应类型的默认协议.
返回值:
成功: 返回一个大于0的文件描述符
失败: 返回-1, 并设置errno
当调用socket函数以后, 返回一个文件描述符, 内核会提供与该文件描述符相对应的读和写缓冲区, 同时还有两个队列, 分别是请求连接队列和已连接队列.
bind函数
将socket文件描述符和IP,PORT绑定
1 | int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
参数说明:
socket: 调用socket函数返回的文件描述符
addr: 本地服务器的IP地址和PORT,
1 | struct sockaddr_in serv; |
addrlen: addr变量的占用的内存大小
返回值:
成功: 返回0
失败: 返回-1, 并设置errno
listen函数
将套接字由主动态变为被动态
1 | int listen(int sockfd, int backlog); |
参数说明:
sockfd: 调用socket函数返回的文件描述符
backlog: 同时请求连接的最大个数(还未建立连接) ,最大是128
返回值:
成功: 返回0
失败: 返回-1, 并设置errno
accept函数
获得一个连接, 若当前没有连接则会阻塞等待.
1 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
函数参数:
sockfd: 调用socket函数返回的文件描述符
addr: 传出参数, 保存客户端的地址信息
addrlen: 传入传出参数, addr变量所占内存空间大小
返回值:
成功: 返回一个新的文件描述符,用于和客户端通信
失败: 返回-1, 并设置errno值.
accept函数是一个阻塞函数, 若没有新的连接请求, 则一直阻塞.
从已连接队列中获取一个新的连接, 并获得一个新的文件描述符, 该文件描述符用于和客户端通信. (内核会负责将请求队列中的连接拿到已连接队列中)
connect函数
连接服务器
1 | int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
函数参数: (和bind函数参数类型一样)
sockfd: 调用socket函数返回的文件描述符
addr: 服务端的地址信息
addrlen: addr变量的内存大小
返回值:
成功: 返回0
失败: 返回-1, 并设置errno值
errno:
EACCES:拒绝连接。通常出现在尝试连接受保护的端口(如1023以下的端口)时。EADDRINUSE:地址已经被使用。通常出现在本地地址或远程地址已经被其他进程占用时。EADDRNOTAVAIL:地址不可用。通常出现在尝试连接不存在的本地地址或远程地址时。EAGAIN或EWOULDBLOCK:非阻塞Socket正在尝试连接,但连接还没有完成。这是一个临时性错误,应该重试connect操作。EALREADY:Socket已经处于连接状态。EBADF:无效的Socket文件描述符。ECONNREFUSED:连接被拒绝。通常出现在远程主机处于关闭状态、没有进程在监听指定端口、防火墙屏蔽了连接请求等情况下。EFAULT:指向sockaddr结构体的指针无效。EINPROGRESS:非阻塞Socket正在尝试连接,但连接还没有完成。这是一个临时性错误,应该使用select函数等待Socket变为可写状态后再进行下一步操作。EINTR:connect操作被信号中断,应该重试connect操作。EISCONN:Socket已经处于连接状态。ENETUNREACH:网络不可达。通常出现在远程主机处于离线状态、路由不可达等情况下。ENOTSOCK:文件描述符不是一个Socket。ETIMEDOUT:连接超时。通常出现在远程主机未响应连接请求、网络传输故障等情况下。
如果远程主机没有响应,或者网络故障等情况下,阻塞connect函数会导致程序长时间处于阻塞状态,可能会导致程序无响应或者崩溃。此外,如果在网络连接过程中出现了异常或错误,阻塞connect函数可能会直接返回错误程序需要通过错误处理机制进行处理。
相比之下,非阻塞connect和select的方式可以通过监控连接状态,及时处理连接异常,收到连接成功后再进行后续的操作,从而避免了阻塞和崩溃等问题的出现。超时限制也可以避免长时间等待,因此,使用非阻塞connect和select的方式可以提高程序的健壮性和稳定性。
读写相关函数
读取数据和发送数据:
1 | ssize_t read(int fd, void *buf, size_t count); |
注意: 如果写缓冲区已满, write也会阻塞, read读操作的时候, 若读缓冲区没有数据会引起阻塞.
在进行 socket 编程中,send 函数用于向连接的另一端发送数据。当调用 send 函数后,数据并不会立即发送出去,而需要等待网络连接稳定,缓冲区可用等条件满足后才会实际发送数据。这时候,如果出现网络故障或者对方出现延迟,就可能导致 send 发送数据失败,并且可能陷入阻塞状态,导致程序无法继续执行。
因此,为了避免 send 函数陷入阻塞状态,我们通常使用 select 函数设置发送数据超时时间,如果在规定时间内没有发送成功,则认为发送失败并中止发送。这其实也是一种优化策略,如果继续等待缓冲区可用或者网络连接稳定,可能会导致程序长时间无响应,用户体验也会变得很差。
另外,通过使用 select 函数进行超时检测,可以减少 CPU 的使用率,避免空等待占用过多的系统资源,提高程序的并发性能和稳定性。
总之,使用 select 对 send 函数进行超时检测,是一种优化策略,可以避免 send 函数陷入阻塞状态,提高程序的响应速度和稳定性。
read函数同理,同样需要超时检测优化
确保write和read发送数据完全
1 | /* |
测试工具
nc命令
Netcat号称TCP/IP的瑞士军刀,基本系统中有自带windows可以安装Nmap 官方的 ncat,它是 nc 的增强版,Windows 友好、支持 UDP/SSL 等,在安装nmap的时候默认勾选上安装ncat的
1
2
3
4
5 ncat用法
udp监听端口
nc -u -v -l 监听端口号
udp连接目标ip:端口
ncat -v -u 目标ip 目标端口
以客户端的方式连接小工具
ubuntu系统安装方式: sudo apt-get -y install netcat-traditional
$$
nc;;ip地址;端口号
$$
例如: nc 192.168.0.2 8888
作为服务端监听端口号:
$$
nc\ \ -l\ \ -p\ \ 端口号
$$
如: nc -l -p 23456
对外开放23456端口: nc -l 0.0.0.0 23456
nc -v -v -w3 -z 192.168.123.2 8080-8083
- 两次
-v是让它报告详细内容 -w3是设置扫描超时时间为 3 秒
监视udp数据
A主机上监听udp的8080端口: nc -u -l -p 8080
B主机上进行udp连接: nc -u 192.168.123.2 8080
服务端(监听)
1
nc -ul 8888
-u:UDP 模式-l:监听模式客户端(发送)
1
nc -u 127.0.0.1 8888
连接后输入文字并回车,服务端如果能收到,说明链路通畅
netstat命令
测试过程中可以使用netstat命令查看监听状态和连接状态
netstat命令:
a表示显示所有,n表示显示的时候以数字的方式来显示p表示显示进程信息(进程名和进程PID)
一般用 netstat -anp | grep 端口号来查看该端口对应的连接情况
netstat -tunlp命令查看网络连接情况,仅服务器
ESTABLISHED表示链接建立了
mac下netstat命令为简化版,用lsof命令取而代之
Mac/Linux
虽然 UDP 没有连接,但操作系统会维护通信五元组(源 IP、源端口、目的 IP、目的端口、协议)的映射表,用于数据转发。
1 | Linux/macOS |
-a(all):显示监听和非监听(已通信)的套接字-n(numeric):不解析域名和服务名,显示数字-u(udp):仅显示 UDP 协议-l:Listening(只看正在监听的)-p:Process(显示进程名和 PID)(ss才支持)
状态指标说明
- Netid: 网络协议类型。UDP 通常显示为 udp。
- State: 套接字状态。
- UNCONN:未连接(最常见,UDP 是无连接的)。
- ESTAB:已连接(如果 UDP 套接字调用了 connect() 锁定了对端)。
- Recv-Q: 接收队列。表示内核已经收到,但还没有被应用程序读取的数据字节数。如果这个数值持续很高,说明应用处理速度跟不上网络接收速度,可能会导致丢包。
- Send-Q: 发送队列。表示应用程序已经交给内核,但还没有发送出去的数据字节数。UDP 没有 TCP 的发送窗口和 ACK 机制,所以这个值通常为 0 或瞬间存在。
- Local Address:Port: 本地地址和端口。格式为 IP:端口。0.0.0.0:1234 表示监听本机所有网卡的 1234 端口。
- Peer Address:Port: 对端地址和端口。对于普通 UDP(未调用 connect),通常显示为 :(通配符),因为 UDP 可以接收来自任何地址的数据。
- Process: (需加 -p 参数才显示) 使用该套接字的进程名和 PID。这是排查问题最有用的一列。
下面通常是使用了 -e (–extended) 或 -a 参数后显示的内核内部信息,日常排查用得较少
- Inode:套接字在系统中的 “文件节点号”。在 Linux 中,套接字也是一种特殊文件。你可以通过这个号码在
/proc/net/udp中找到更底层的信息,或者通过ls -l /proc/<pid>/fd/来反向查找是哪个进程打开的。 - Refs (References):引用计数。表示有多少个文件描述符(fd)指向了这个套接字。
- Nextref:内核链表指针的下一个引用,对普通用户无实际意义。
- Conn:内核内部的连接 ID 或指针,UDP 通常显示为
0或-。
Windows
1 | netstat -ano -p udp |
-p udp:仅显示 UDP-o:显示所属进程 ID (PID),方便去任务管理器查是哪个程序。
tcpdump
排查思路:
- 在发送端抓包:看数据是否真的从网卡发出去了(检查 IP、端口是否写错)
- 在接收端抓包:看数据是否到达(如果没到,中间网络 / 防火墙可能丢了;如果到了但应用没处理,是应用层 Bug)
iproute2
iproute2 是 Linux 下的现代网络管理工具集,通过 Netlink 套接字与内核通信,是老旧的 net-tools(ifconfig、route、netstat 等)的官方替代品。它支持网络命名空间、策略路由、流量控制等现代特性,效率更高、功能更全,几乎所有 Linux 发行版默认预装。
| 工具 | 核心用途 | 替代的旧命令 | 高频场景 |
|---|---|---|---|
| ip | 全能网络管理(接口、地址、路由、邻居) | ifconfig、route、arp | ip addr、ip link、ip route |
| ss | 套接字状态统计 | netstat | ss -ulnp(查 UDP 监听进程) |
| tc | 流量控制(QoS、限速、队列) | tc(旧版) | 带宽限制、优先级调度 |
| bridge | 网桥管理 | brctl | 搭建软件网桥、容器网络 |
| nstat/rtstat | 网络 / 路由统计 | netstat -s | 内核网络统计信息 |
| devlink | 网卡设备链路管理 | - | 高级网卡功能、驱动配置 |
ss
Socket Statistics: 用来替代老旧的 netstat 命令的现代工具
优势:
- 速度快:当服务器有成千上万个连接时,
netstat会慢得卡死,而ss依然很快。- 信息全:显示的信息比
netstat更详细、更准确。出身:它是 Linux 内核网络工具包
iproute2的一部分,就像ifconfig被ip addr替代一样,ss是未来的标准。
1 | linux系统都预装了,如果没有 |
最实用的排查命令: ss -ulnp 一眼就能看到哪个端口被哪个程序占用了
其他系统替代品
- Linux:ss 最强
- macOS:用 lsof /netstat 也可以安装:
brew search iproute2 - Windows:用 netstat -ano
1 | lsof 是 Mac 上最接近 ss 的神器,能看到进程名、PID |
iproute2mac 是 macOS 上的一个开源命令行包装器(CLI wrapper),核心作用是让你在 Mac 上直接使用 Linux 风格的 ip、ss 等网络命令,不用再记 macOS 原生的 ifconfig、netstat、route 等旧语法
| 命令 | 作用 | 对应 macOS 原生命令 |
|---|---|---|
| ip addr | 查看 / 配置网卡、IP 地址 | ifconfig |
| ip link | 管理网卡状态(up/down) | ifconfig en0 up |
| ip route | 查看 / 操作路由表 | netstat -rn / route |
| ip neigh | 查看 ARP 表(IP ↔ MAC) | arp -a |
| ss | 查看套接字 / 连接(替代 netstat) | netstat -an |
Socat
Socket CA是一个功能强大的多用途网络工具,被称为”瑞士军刀”级别的数据流转发器
- 双向数据流:在两个数据源之间建立双向通道
- 协议丰富:支持TCP、UDP、SSL、串口、文件等数十种协议
- 高度灵活:通过各种选项可以精确控制数据传输
- 生产就绪:支持fork、守护进程、日志等生产环境特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 网络协议
TCP, TCP-LISTEN, UDP, UDP-LISTEN
SSL, OPENSSL, TLS
SCTP, DCCP
文件/设备
FILE:/path/to/file
STDIN, STDOUT, STDERR
EXEC:"command"
SYSTEM:"shell command"
特殊
GOPEN:/dev/ttyS0 # 串口
PTY # 伪终端
TUN # 隧道接口适用场景:
- 网络调试和协议分析
- 端口转发和代理
- 协议转换(如TCP转UDP)
- 串口通信
- SSL/TLS测试
- 简单的负载均衡
- 容器网络调试
1 | Netcat:简单的网络工具 |
案例
服务器开发流程
创建socket,返回一个文件描述符lfd –
socket() – 该文件描述符用于监听客户端连接
将lfd和IP PORT进行绑定 –
bind()将lfd由主动变为被动监听 –
listen()接受一个新的连接,得到一个文件描述符 –
accept() – 该文件描述符是用于和客户端进行通信的
关闭文件描述符 –
close(lfd) close(cfd)
客户端开发流程
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
linux/Mac代码
服务器
1 |
|
客户端
1 |
|
windows代码
代码上和linux有一定差异,但差异基本不大
服务器
- 加载套接字库,创建套接字(WSAStartup()/socket())
- 绑定套接字到一个IP地址和一个端口上(bind())
- 将套接字设置为监听模式等待连接请求(listen())
- 请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
- 用返回的套接字和客户端进行通信(send()/recv())
- 返回,等待另一个连接请求;
- 关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())
1 |
|
客户端
- 加载套接字库,创建套接字(WSAStartup()/socket())
- 向服务器发出连接请求(connect())
- 和服务器进行通信(send()/recv())
- 关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())
1 |
|
网络开发的注意点及完整案例
当read读文件描述符为非阻塞状态的时候,若对方没有发送数据,会立刻返回, errno设置为 EAGAIN,这个错误我们要忽略.
防止阻塞被信号打断
像accept,read, write 这样的能够引起阻塞的函数,若被信号打断,由于信号的优先级较高, 会优先处理信号, 信号处理完成后,会使accept或者read解除阻塞, 然后返回, 此时返回值为 -1,设置errno=EINTR;errno=ECONNABORTED表示连接被打断,异常.
阻塞函数在阻塞期间若收到信号,会被信号中断,errno设置为EINTR,这个错误不应该被视为错误.
1 |
|
头文件
1 |
|
粘包
接收缓冲区中,对方发送数据连续发了两次,然后读数据的时候第一次没有读完,剩余的数据在第二次读走了,这种情况就属于粘包.
粘包: 多次数据发送, 收尾相连, 接收端接收的时候不能正确区分第一次发送多少, 第二次发送多少.
本质上是同一个ip端口两种业务的数据的区分问题.
解决方法:
方案1: 包头+数据(最推荐)
如4位的数据长度+数据 ———–> 00101234567890
其中0010表示数据长度, 1234567890表示10个字节长度的数据.
另外, 发送端和接收端可以协商更为复杂的报文结构, 这个报文结 构就相当于双方约定的一个协议.方案2:添加结尾标记
如结尾最后一个字符为\n $等.
方案3:数据包定长
如发送方和接收方约定, 每次只发送128个字节的内容, 接收方接收定 长128个字节就可以了.
学习目标
- 熟练掌握TCP状态转换图
- 熟练掌握端口复用的方法
- 了解半关闭的概念和实现方式
- 了解多路IO转接模型
- 熟练掌握select函数的使用
- 熟练使用
fd_set相关函数的使用 - 能够编写select多路IO转接模型的代码
多并发服务器
如何支持多个客户端 — 支持多并发的服务器
由于accept和read函数都会阻塞, 如当read的时候, 不能调用accept接受新的连接, 当accept阻塞等待的时候不能read读数据.
两种思路
-
可以将accept和read函数设置为非阻塞, 调用fcntl函数可以将文件描述符设置为非阻塞, 让后再while循环中忙轮询.
-
让父进程accept接受新连接, 然后fork子进程, 让子进程处理通信, 子进程处理完成后退出, 父进程使用SIGCHLD信号回收子进程.
-
让主线程接受新连接, 让子线程处理与客户端通信; 使用多线程要将线程设置为分离属性, 让线程在退出之后自己回收资源.
在一次发送大量数据(超过发送缓冲区大小)的情况下,如果使用阻塞方式,程序一直阻塞,直到所有的数据都写入到缓冲区中。例如,要发送M字节数据,套接字发送缓冲区大小为B字节,只有当对端向本机返回ack表明其接收到大于等于M-B字节时,才意味着所有的数据都写入到缓冲区中。很明显,如果一次发送的数据量非常大,比如M=10GB、B=64KB,则:1)一次发送过程中本机线程会在一个fd上阻塞相当长一段时间,其他fd得不到及时处理;2)如果出现发送失败,无从得知到底有多少数据发送成功,应用程序只能选择重新发送这10G数据,结合考虑网络的稳定性,只能呵呵;
总之,上述两点都是无法接受的。因此,对性能有要求的服务器一般不采用阻塞而采用非阻塞。
设置非阻塞实现
1 | //设置文件描述符为非阻塞函数 |
此代码实现了接受多个客服端连接并接受客户端发过来的数据的功能.但建立连接的过程必须和读取数据的问题完全分离,即先建立指定连接数目的连接,然后再开始通信过程(此后无法继续建立新连接),建立连接和通信是割裂的.开始通信后没有一个好的时机供建立新的连接.如果是客户端通信内容决定建立连接的时机的话就可以用这种方式.(而且也没有好的时机做清理工作)
这种实现方法不是一个好的实现方法
但是其实还有select方法,可以解决上述问题
多进程实现
不设置非阻塞状态的话都要考虑防止阻塞被信号打断
- 注册子进程回收
- 创建socket,得到一个监听的文件描述符lfd — socket()
- 将lfd和IP和端口port进行绑定 — bind()
- 设置监听 — listen()
- 进入while循环
- 接受新客户端到来 — accept()
fork子进程收发数据- 子进程关闭监听文件描述符后收发数据,最后要设置关闭描述符退出子进程
exit(0);—read()/write() - 父进程关闭通信文件描述符
- 子进程关闭监听文件描述符后收发数据,最后要设置关闭描述符退出子进程
下面代码结合了防止阻塞被信号打断的代码
1 |
|
多线程实现
不设置非阻塞状态的话都要考虑防止阻塞被信号打断
注册子进程回收
创建socket,得到一个监听的文件描述符lfd — socket()
将lfd和IP和端口port进行绑定 — bind()
设置监听 — listen()
进入while循环
接受新客户端到来 — accept()
pthread_create创建子线程收发数据- 收发数据 —read()/write()
- 关闭传入的描述符
pthread_detach设置线程为分离属性(退出时自动释放)
[注意] 主线程和子线程共享文件描述符
1 |
|
半关闭状态
如果一方close,另一方没有close,则认为是半关闭状态,处于半关闭状态的时候,可以接受数据,但是不能发送数据.相当于把文件描述符的写缓存区操作关闭了.
注意:半关闭一定是出现在主动关闭的一方
shutdown函数
1 | int shutdown(int socketfd, int how); |
- 第一个参数为socket返回的文件描述符
- 第二个参数可以设置为
SHUT_RD/SHUT_WR/SHUT_RDWR分别表示关闭接受缓存区/关闭发送缓冲区/都关闭
返回值:成功返回0;失败返回-1,并设置errno
shutdown和close的区别:
shutdown能够把文件描述符上的读或者写操作关闭,而close关闭文件描述符只是将连接的引用计数的值减1,当减到0就真正关闭文件描述符了.
如: 调用dup函数或者dup2函数可以复制一个文件描述符, close其中一个并不影响另一个文件描述符,而shutdown就不同了,一旦shutdown了其中一个文件描述符,对所有的文件描述符都有影响(只是关闭了用户层,用户层没法再发了,内核层还是可以发的,tcp四次挥手最后一个ack就是这样理解)
端口复用
解决端口复用的问题: bind error: Address already in use, 发生这种情况是在服务端主动关闭连接以后, 接着立刻启动就会报这种错误.
测试: 启动服务端和客户端, 然后先关闭服务端, 再次启动服务端, 此时服务端报错: bind error: Address already in use; 若是先关闭的客户端, 再关闭的服务端, 此时启动服务端就不会报这个错误.
setsockopt函数
1 | int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); |
返回值 : 成功返回0 ; 失败返回-1,并设置errno
1 | //调用案例 |
函数说明可参看<<UNIX环境高级编程>>
由于错误是bind函数报出来的, 该函数调用要放在bind之前, socket之后调用.
getsockopt函数
函数用于获取任意类型、任意状态套接口的选项当前值,并把结果存入option_value。
1 | int getsockopt(int socket, |
- 成功,它将返回0,表示获取选项值成功。
- 失败,它将返回-1,并设置errno变量来指示错误原因
在使用非阻塞Socket时,我们通常会使用getsockopt函数和SO_ERROR选项来检查Socket的状态,以便在Socket出错时及时处理错误。例如,当使用select或poll等多路复用函数时,我们可以先调用getsockopt函数来获取Socket的错误状态,然后根据错误状态来判断Socket是否可以进行读写操作。
获取socket错误状态 例子如下:
1 | int err; |
心跳包
用于检测长连接是否正常的手段
长连接和短连接的概念:
连接建立之后一直不关闭为长连接
连接收发数据完毕之后就关闭为短连接
长连接 通常用于通信双方数据交换频繁的情况下
如何检查与对方的网络连接是否正常?
一般心跳包用于长连接.
1 | keepAlive = 1; |
上面为官方提供的方式,由于不能实时的检测网络情况, 一般不用这种方法
在应用程序中自己定义心跳包,使用灵活,能实时把控
通信双方需要协商规则(协议)
高并发服务器模型select
一种多路io复用技术:同时监听多个文件描述符,将监控的操作交给内核去处理
优点:跨平台使用,windows,linux,macos都支持select
缺点:受最大描述符数量1024的限制
select函数
函数介绍: 委托内核监控该文件描述符对应的读,写或者错误事件或者连接请求的发生.(内核收到了告诉程序,避免了accept阻塞)
只要是文件描述符,select函数都可以派上用场.
1 | int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
参数说明:
nfds: 最大的文件描述符+1
readfds: 读文件描述符集合, 是一个传入传出参数
- 传入: 指的是告诉内核哪些文件描述符需要监控
- 传出: 指的是内核告诉应用程序哪些文件描述符发生了变化
writefds: 写文件描述符集合(传入传出参数)
同上
execptfds: 异常文件描述符集合(传入传出参数)
同上
timeout:
NULL–表示永久阻塞, 直到有事件发生>0–表示阻塞的时长,遇到事件发生或者超时就返回
返回值:
成功,返回发生变化的文件描述符的个数,表示至少有一个套接字已经准备好了可读,可写或异常事件.
超时:限时内没有任何发生变化的文件描述符个数,返回0
失败,返回-1, 并设置errno值.
如果select()函数返回1,并且FD_ISSET()函数返回true,则表示该套接字已经准备好了可读、可写或异常事件。此时需要进一步调用getsockopt()函数,并检查SO_ERROR选项是否为0来判断该套接字是否处于异常状态。如果SO_ERROR的值为0,则表示该套接字没有发生错误;否则,SO_ERROR的值将是相应的错误码,表示该套接字已经发生了错误。
操作文件描述符集的宏如下:
void FD_CLR(int fd, fd_set *set);将fd从set集合中清除.
int FD_ISSET(int fd, fd_set *set);功能描述: 判断fd是否在集合中
返回值: 如果fd在set集合中, 返回1, 否则返回0.
void FD_SET(int fd, fd_set *set);将fd设置到set集合中.
void FD_ZERO(fd_set *set);初始化set集合(清空).
在linux中的定义位置:/usr/include/x86_64-linux-gnu/sys/select.h和/usr/include/x86_64-linux-gnu/bits/select.h(最简单的方法就是使用预处理将头文件和宏全部替换掉gcc -E select.c -o select.i, 直接就可以看到最终的定义了)
1 | typedef struct |
从上面的文件中可以看出, 这几个宏本质上还是位操作.(与信号集操作类似)
select开发服务端代码
1 |
|
关于select的思考:
问题: 如果有效的文件描述符比较少, 会使循环的次数太多.(大多数都是初始连接一次就)
解决办法: 可以将有效的(排除了被取消掉的文件描述符)文件描述符放到一个数组当中, 这样遍历效率就高了.(连接不频繁,而通信频繁,也可以针对通信开数组)
select优点
- 一个进程可以支持多个客户端
- select支持跨平台
select缺点
代码编写困难
会涉及到用户区到内核区的来回拷贝
当客户端多个连接, 但少数活跃的情况, select效率较低
例如: 作为极端的一种情况, 3-1023文件描述符全部打开, 但是只有1023有发送数据, select就显得效率低下
最大支持1024个客户端连接
select最大支持1024个客户端连接不是有文件描述符表最多可以支持1024个文件描述符限制的, 而是由
FD_SETSIZE=1024限制的.
FD_SETSIZE=1024 fd_set(fd_set这个文件描述符表中一共有1024个bit位)使用了该宏, 当然可以修改内核, 然后再重新编译内核, 一般不建议这么做.
练习
编写代码, 让select监控标准输入, 监控网络, 如果标准输入有数据就写入网络, 如果网络有数据就读出网络数据, 然后打印到标准输出.
1 | int main(int argc,char** argv) |
poll与epoll
poll与epoll可以突破最大描述符数量1024个的限制
- poll函数
- epoll多路IO模型
- 了解poll ET/LT触发模式并实现
- 理解epoll边缘非阻塞模式并实现
- 了解epoll反应堆模型设计思想
- 能看懂epoll反应堆模型的实现代码
linux下常用epoll unix下常用select
效率来看: epoll>poll>select
多路IO-poll
跟select类似, 监控内核监控事件,实现多路IO, 但poll不能跨平台
1 | int poll(struct pollfd *fds, nfds_t nfds, int timeout); |
参数说明
fds: 传入传出参数, 实际上是一个结构体数组1
2
3
4
5
6struct pollfd
{
int fd; /* file descriptor */ //监控的文件描述符
short events; /* requested events */ //要监控的事件---不会被修改
short revents; /* returned events */ //返回发生变化的事件 ---由内核返回
};fds.fd: 要监控的文件描述符fds.events:(多个之间用或相连)POLLIN—->读事件POLLOUT—->写事件POLLERR—->异常事件POLLIN|POLLHUP—->管道断开连接其他更多宏参考
man 2 pollfds.revents: 返回的事件
nfds: 数组实际有效内容的个数,就是fds数组下标的最大值+1timeout: 超时时间, 单位是毫秒.-1:永久阻塞, 直到监控的事件发生0: 不管是否有事件发生, 立刻返回>0: 直到监控的事件发生或者超时
返回值
- 成功:返回就绪事件的个数
- 失败: 返回-1
若timeout=0, poll函数不阻塞,且没有事件发生, 此时返回-1, 并且errno=EAGAIN, 这种情况不应视为错误.
说明
- 当poll函数返回的时候, 结构体当中的
fd和events没有发生变化, 究竟有没有事件发生由revents来判断, 所以poll是请求和返回分离. struct pollfd结构体中的fd成员若赋值为-1, 则poll不会监控.- 相对于select, poll没有本质上的改变; 但是poll可以突破1024的限制.
如何突破1024的限制(开发流程一般是没有权限改的)
在/proc/sys/fs/file-max查看一个进程可以打开的socket描述符上限.
如果需要可以修改配置文件: /etc/security/limits.conf
加入如下配置信息, 然后重启终端即可生效.
1 | * soft nofile 1024 |
代码
1 | int main(int argc,char** argv) |
多路IO-epoll
将检测文件描述符的变化委托给内核去处理, 然后内核将发生变化的文件描述符对应的事件返回给应用程序.
底层实现是红黑二叉树
优点:epoll的并发量大于poll和select
缺点:只支持linux系统
相关函数
epoll_create函数
创建一棵epoll数,返回一个树根节点
1 | int epoll_create(int size); |
参数 size: 最大节点数, 此参数在linux 2.6.8已被忽略, 但必须传递一个大于0的数.
返回值
- 成功: 返回一个大于0的文件描述符, 代表整个树的树根.
- 失败: 返回-1, 并设置errno值.
epoll_ctl函数
将要监听的节点在epoll树上添加, 删除和修改
1 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
参数说明
epfd: epoll树根op:EPOLL_CTL_ADD: 添加事件节点到树上EPOLL_CTL_DEL: 从树上删除事件节点EPOLL_CTL_MOD: 修改树上对应的事件节点
fd: 事件节点对应的文件描述符event: 要操作的事件节点 (epoll_event结构体),如果是要下树的话,这里只需要填NULL
1 | typedef union epoll_data { |
p.s. 客户端关闭既会触发服务器的EPOLLIN事件,也会触发EPOLLOUT事件
EPOLLOUT的理解:传输大量数据的时候,没有办法一次将数据全部发送出去就需要将剩下的数据缓存起来,等内核通知缓冲区可写的时候再继续发送(EPOLLOUT事件表示fd的发送缓冲区可写,在一次发送大量数据(超过发送缓冲区大小)的情况下很有用)你需要将一个10G大小的文件返回给用户,那么你简单send这个文件是不会成功的。
这个场景下,你send 10G的数据,send返回值不会是10G,而是大约256k,表示你只成功写入了256k的数据。接着调用send,send就会返回EAGAIN,告诉你socket的缓冲区已经满了,此时无法继续send。
此时异步程序的正确处理流程是调用epoll_wait,当socket缓冲区中的数据被对方接收之后,缓冲区就会有空闲空间可以继续接收数据,此时epoll_wait就会返回这个socket的EPOLLOUT事件,获得这个事件时,你就可以继续往socket中写出数据。
epoll_wait函数
等待内核返回事件发生
1 | int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); |
参数说明:
epfd: epoll树根events: 传出参数, 其实是一个事件结构体数组(epoll_event数组结构体)maxevents: epoll_event数组的项最大数量timeout-1: 表示永久阻塞0: 立即返回>0: 表示超时等待事件
返回值:
- 成功: 返回发生事件的个数
- 失败: 若timeout=0, 没有事件发生则返回; 返回-1, 设置errno值
**[注意] **epoll_wait的events是一个传出参数, 调用epoll_ctl传递给内核什么值, 当epoll_wait返回的时候, 内核就传回什么值,不会对struct event的结构体变量的值做任何修改.
实现案例
1 |
|
可能存在的问题:若每一个连接上处理的时间比较长,会导致后面的连接上发来的数据得不到及时的处理
解决方法:可以让主线程处理epoll_wait,让子线程收发数据
ET工作模式
epoll有两种工作模式:ET和LT模式
- 水平触发(
LT): 高电平代表1 (epoll默认是LT模式)
只要缓冲区(内核的读缓冲区)中有数据, 就一直通知 - 边缘触发(
ET): 电平有变化就代表1
缓冲区中有数据只会通知一次, 之后再有数据才会通知.(若是读数据的时候没有读完, 则剩余的数据不会再通知, 直到有新的数据到来)
epoll_event结构体中的events按位或EPOLLET来设置边缘触发.
边缘非阻塞模式优势: 提高效率
et模式案例
ET模式由于只通知一次, 所以在读的时候要循环读, 直到读完, 但是当读完之后read就会阻塞, 所以应该将该文件描述符设置为非阻塞模式(fcntl函数).
read函数在非阻塞模式下读的时候, 若返回-1, 且
errno为EAGAIN, 则表示当前资源不可用, 也就是说缓冲区无数据(缓冲区的数据已经读完了); 或者当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲区中已没有数据可读了,也就可以认为此时读事件已处理完成。
epoll的边缘触发模式的非阻塞模式案例
1 |
|
epoll反应堆
epoll反应堆的核心思想是: 在调用epoll_ctl函数的时候, 将events上树的时候,利用epoll_data_t的ptr成员, 将一个文件描述符,事件和回调函数封装成一个结构体, 然后让ptr指向这个结构体, 然后调用epoll_wait函数返回的时候, 可以得到具体的events, 然后获得events结构体中的events.data.ptr指针, ptr指针指向的结构体中有回调函数, 最终可以调用这个回调函数.
其实就是自己定义结构记录回调等信息放入ptr指向中(原本用于存放fd的联合体),在main中根据epoll通知,取ptr信息来自己调用回调以此实现了一个架构体系
核心代码如下:
1 | // epoll基于非阻塞I/O事件驱动 |
线程池
一个抽象的概念, 若干个线程组合到一起, 形成线程池.
为什么需要线程池?
多线程版服务器一个客户端就需要创建一个线程! 若客户端太多, 显然不太合适.
什么时候需要创建线程池呢?简单的说,如果一个应用需要频繁的创建和销毁线程,而任务执行的时间又非常短,这样线程创建和销毁的带来的开销就不容忽视,这时也是线程池该出场的机会了。如果线程创建和销毁时间相比任务执行时间可以忽略不计,则没有必要使用线程池了,此时可以考虑协程(C++20)。
实现的时候类似于生产者和消费者.
wrap.c
1 |
|
wrap.h
1 |
|
实现原理
线程池和任务池:
任务池相当于共享资源, 所以需要使用互斥锁, 当任务池中没有任务的时候需要让线程阻塞, 所以需要使用条件变量.
如何让线程执行不同的任务?
使用回调函数, 在任务中设置任务执行函数, 这样可以起到不同的任务执行不同的函数.
线程相关函数
- 创建子线程
pthread_create - 线程退出
pthread_exit - 设置子线程为分离属性
pthread_detach
涉及到任务池共享资源的互斥访问,因此需要互斥锁相关的函数
- 初始化互斥锁
pthread_mutex_init - 互斥锁加/解锁
pthread_mutex_lock/unlock - 销毁互斥锁
pthread_mutex_destroy
若任务池已满,主线程应该阻塞等待子线程处理任务
若任务池空了,子线程应该阻塞等待主线程往任务池中添加任务
因此需要涉及条件变量相关函数
- 堵塞
pthread_cond_wait - 唤醒至少一个阻塞在该条件变量上的线程
pthread_cond_signal - 初始化条件变量
pthread_cond_init - 销毁条件变量
pthread_cond_destroy - 唤醒所有条件变量
pthread_cond_broadcast
案例
[[linux基础以及系统编程#线程相关函数|pthread是linux支持的线程操作api]]
简单版本代码:
1 |
|
复杂版本
比起简单版本,添加了动态线程管理
1 |
|
截取一点点的效果图(效果过长)
UDP通信
UDP:用户数据报协议
面向无连接的,不稳定,不可靠,不安全的数据报传递—更像是收发短信
UDP传输不需要建立连接,传输效率更高,在稳定的局域网内环境相对可靠
因为UDP不需要维护连接,因此udp天然支持多客户端
相关函数
recvfrom函数
接收消息 (调用该函数相当于TCP通信的recv+accept函数)
1 | ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); |
sockfd套接字buf要接受的缓冲区len缓冲区的长度flags标志位一般填0src_addr传入传出参数 原地址传出参数 (存储发送数据过来的主机的信息)addrlen传入传出参数 发送方地址长度
返回值成功: 返回读到的字节数失败: 返回 -1 设置errno
sendto函数
发送数据
1 | ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen); |
sockfd套接字dest_addr目的地址(决定了发给谁)addrlen目的地址长度
返回值成功: 返回写入的字节数失败: 返回-1,设置errno
UDP服务器编码流程:
UDP客户端编码流程:
测试命令
1 | nc -u ip号 端口号 |
案例
下面是服务端收,客户端发简单案例
udp服务端简单代码
1 |
|
udp客户端简单代码
1 |
|
本地socket通信
也是一种IPC机制(进程间通信机制)
通过查询: man 7 unix 可以查到unix本地域socket通信相关信息
需要头文件 #include <sys/un.h> 和 #include <sys/socket.h>
本地套接字服务器的流程:
- 可以使用TCP的方式, 必须按照tcp的流程
- 也可以使用UDP的方式, 必须按照udp的流程
AF_INET: ipv4地址域类型struct sockaddr_inAF_INET6: ipv6地址域类型struct sockaddr_in6AF_UNIX: 本地 地址域类型struct sockaddr_un
唯一的区别只是bind函数传参数不同:本地socket通信使用的是sockaddr_un结构体,如下:
1 | struct sockaddr_un serv; |
tcp的本地套接字服务器流程:
- 创建套接字
socket(AF_UNIX,SOCK_STREAM,0) - 绑定
bind用的结构体是struct sockaddr_un - 侦听
listen - 获得新连接
accept - 循环通信
read-write - 关闭文件描述符
close
tcp本地套接字客户端流程:
调用
socket创建套接字调用
bind函数将socket文件描述和socket文件进行绑定.不是必须的, 若无显示绑定会进行隐式绑定,但服务器不知道谁连接了,就无法给客户端发送消息,原因是网络编程的时候客户端信息操作系统会自动分配,而本地通信并不会
调用
connect函数连接服务端循环通信
read-write关闭文件描述符
close
需要注意的是: bind函数会自动创建socket文件(大小为0), 若在调用bind函数之前socket文件已经存在, 则调用bind会报错, 可以使用unlink函数在bind之前先删除文件.
测试本地socket通信:nc -U socket文件名.s
代码案例
TCP
服务端
1 |
|
客户端
1 |
|
UDP
服务端
1 |
|
客户端
1 |
|
此处代码只需要服务端收,客户端发,如果服务端也需要发送数据给指定客户端,那么客户端也要bind创建自己的sock文件.(供服务端标识发给谁)
长度固定为99,是因为客户端sendto函数传参为固定99,而应该传 strlen(buf)+1,这样才会显示内容长度
第三方库
libevent库
- 事件驱动, 高性能, 轻量级, 专注于网络
- 源代码精炼, 易读
- 跨平台(提供了不同的版本,支持linux,unix,windows,mac等)
- 支持多种I/O多路复用技术, 如epoll select poll等
- 支持I/O和信号等事件
libevent的核心实现:
在linux上, 其实质就是epoll反应堆.
libevent是事件驱动, epoll反应堆也是事件驱动, 当要监测的事件发生的时候, 就会调用事件对应的回调函数, 执行相应操作. 特别提醒: 事件回调函数是由用户开发的, 但不是由用户显示去调用的, 而是由libevent去调用的
libevent安装
libevent源码下载主要分2个大版本:
1.4.x系列, 较为早期版本, 适合源码学习2.x系列, 较新的版本, 代码量比1.4版本多很多, 功能也更完善。
从官网http://libevent.org上下载安装文件之后, 将安装文件上传到linux系统上;源码包的安装,以2.1.12版本为例,在官网可以下载到源码包libevent-2.1.12-stable.tar.gz, 安装步骤与第三方库源码包安装方式基本一致。
解压libevent-2.1.12-stable.tar.gz:
- 解压: tar -zxvf libevent-2.1.12-stable.tar.gz
- cd到libevent-2.1.12-stable目录下, 查看README文件, 该文件里描述了安装的详细步骤, 可参照这个文件进行安装.
进入源码目录:
执行配置
./configure, 检测安装环境, 生成makefile.执行
./configure的时候也可以指定路径,./configure --prefix=/usr/xxxxx, 这样就可以安装到指定的目录下, 但是这样在进行源代码编译的时候需要指定用-I头文件的路径和用-L库文件的路径. 若默认安装不指定–prefix, 则会安装到系统默认的路径下, 编译的时候可以不指定头文件和库文件所在的路径.执行make命令编译整个项目文件.
通过执行make命令, 会生成一些库文件(动态库和静态库)和可执行文件.
执行
sudo make install进行安装安装需要root用户权限, 这一步需要输入当前用户的密码
执行这一步, 可以将刚刚编译成的库文件和可执行文件以及一些头文件拷贝到/usr/local目录下:
—-头文件拷贝到了/usr/local/include目录下;
—-库文件拷贝到了/usr/local/lib目录下.
libevent库的使用
进入到libevent-2.1.12-stable/sample下, 可以查看一些示例源代码文件.
使用libevent库编写代码在编译程序的时候需要指定库名:-levent;
安装文件的libevent库文件所在路径:libevent-2.1.12-stable/.libs;
编写代码的时候用到event.h头文件, 或者直接参考sample目录下的源代码文件也可以.
1 |
编译源代码文件(以hello-world.c文件为例)gcc hello-world.c -levent !!!!!需要指定库-levent!!!!
由于安装的时候已经将头文件和库文件拷贝到了系统头文件所在路径/usr/local/include和系统库文件所在路径/usr/local/lib, 所以这里编译的时候可以不用指定-I和-L.
event_base相关结构与函数
event_base结构
使用libevent 函数之前需要分配一个或者多个 event_base 结构体, 每个event_base结构体持有一个事件集合,可以检测以确定哪个事件是激活的, event_base结构相当于epoll红黑树的树根节点, 每个event_base都有一种用于检测某种事件已经就绪的 “方法”(回调函数)
通常情况下可以通过event_base_new函数获得event_base结构。
相关函数
event_base_new获得event_base结构event_base_free释放event_base指针event_reinit重新初始化event_get_supported_methods获得当前系统(或者称为平台)支持的方法有哪些-
event_base_get_method获得当前base节点使用的多路io方法 event_base_loop进入循环等待事件(万能)event_base_dispatch进入循环等待事件(常用)event_base_loopexit退出循环等待(回调执行结束后终止循环)event_base_loopbreak退出循环等待(立即终止循环)
event_base函数
获得event_base结构
1 | struct event_base *event_base_new(void); //event.h的L:337 可以找到 |
event_base_free函数
释放event_base指针
1 | void event_base_free(struct event_base *); //event.h的L:561 |
event_reinit函数
如果有子进程, 且子进程也要使用base, 则子进程需要对event_base重新初始化, 此时需要调用event_reinit函数.
1 | int event_reinit(struct event_base *base); //event.h的L:349 |
event_get_supported_methods函数
得当前系统(或者称为平台)支持的方法有哪些
1 | const char **event_get_supported_methods(void); |
对于不同系统而言, event_base就是调用不同的多路IO接口去判断事件是否已经被激活, 对于linux系统而言, 核心调用的就是epoll, 同时支持poll和select.
编写代码获得当前系统支持的多路IO方法和当前所使用的方法:
linux下打印如下: epoll poll select
event_base_get_method函数
获得当前base节点使用的多路io方法
1 | const char * event_base_get_method(const struct event_base *base); |
event_base_loop函数
进入循环等待事件(该函数一般不用,用event_base_dispatch代替)
1 | int event_base_loop(struct event_base *base, int flags); //event.h的L:660 |
参数说明:
base: 由event_base_new函数返回的指向event_base结构的指针flags的取值:默认不设置时等同于
event_base_dispatch#define EVLOOP_ONCE 0x01只触发一次, 如果事件没有被触发, 阻塞等待
#define EVLOOP_NONBLOCK 0x02非阻塞方式检测事件是否被触发, 不管事件触发与否, 都会立即返回.
event_base_dispatch函数
进入循环等待事件(阻塞)
1 | int event_base_dispatch(struct event_base *base); //event.h的L:364 |
调用该函数, 相当于没有设置标志位的event_base_loop。程序将会一直运行, 直到没有需要检测的事件了, 或者被结束循环的API终止。
event_base_loopexit函数
退出循环等待(回调执行结束后终止循环,也就是等待处理完再终止)
1 | int event_base_loopexit(struct event_base *base, const struct timeval *tv); |
如果正在执行激活事件的回调函数, 那么event_base_loopexit将在事件回调执行结束后终止循环(如果tv时间非NULL, 那么将等待tv设置的时间后立即结束循环), 而event_base_loopbreak会立即终止循环。
event_base_loopbreak函数
退出循环等待(立即终止循环)
1 | int event_base_loopbreak(struct event_base *base); |
使用libevent库的流程
创建根节点–
event_base_new设置监听事件和数据可读可写的事件的回调函数
设置了事件对应的回调函数以后, 当事件产生的时候会自动调用回调函数
事件循环–
event_base_dispatch相当于while(1), 在循环内部等待事件的发生, 若有事件发生则会触发事件对应的回调函数。
释放根节点–
event_base_free释放由event_base_new和event_new创建的资源, 分别调用event_base_free和event_free函数.
事件驱动event
事件驱动实际上是libevent的核心思想
主要几个状态:
无效的指针: 此时仅仅是定义了struct event *ptr;非未决:相当于创建了事件, 但是事件还没有处于被监听状态, 类似于我们使用epoll的时候定义了struct epoll_event ev并且对ev的两个字段进行了赋值, 但是此时尚未调用epoll_ctl对事件上树.未决:就是对事件开始监听, 暂时未有事件产生。相当于调用epoll_ctl对要监听的事件上树, 但是没有事件产生.激活:代表监听的事件已经产生, 这时需要处理, 相当于调用epoll_wait函数有返回, 当事件被激活以后, libevent会调用该事件对应的回调函数.
event相关函数和结构
libevent的事件驱动对应的结构体为struct event, 对应的函数在图上也比较清晰, 下面介绍一下主要的函数:
自定义回调函数格式:
1 | typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg); |
event_new函数
创建event结构指针, 同时指定对应的地基base, 对应的文件描述符, 要监听的是什么事件, 以及回调函数和回调函数的参数。
1 | struct event *event_new( |
base: 对应的根节点–地基fd: 要监听的文件描述符,(或信号)events:要监听的事件1
2
3
4
5
6
7
//若要想设置持续的读事件则: EV_READ | EV_PERSISTcb回调函数, 原型如下:typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);p.s.回调函数的参数就对应于event_new函数的fd, event和arg
arg传入上面回调函数的参数arg
event_add函数
将非未决态事件转为未决态, 相当于调用epoll_ctl函数(EPOLL_CTL_ADD), 开始监听事件是否产生, 相当于epoll的上树操作.
1 | int event_add(struct event *ev, const struct timeval *timeout); |
ev: 调用event_new创建的事件timeout: 限时等待事件的产生, 也可以设置为NULL, 没有限时。
event_del函数
将事件从未决态变为非未决态, 相当于epoll的下树(epoll_ctl调用EPOLL_CTL_DEL操作)操作。
1 | int event_del(struct event *ev); |
参数: ev指的是由event_new创建的事件.
event_free函数
释放由event_new申请的event节点。
1 | void event_free(struct event *ev); |
参数同上
libevent开发流程
编写一个基于event实现的tcp服务器
创建socket —
socket设置端口复用 —
setsockopt绑定 —
bind设置监听 —
listen创建地基 —
event_base_new创建lfd对应的事件 —
event_new该事件编写回调函数为accept接受新描述符,构造新事件(处理读写的回调函数)并上树
第六步事件上树(就是上地基) —
event_add进入事件循环 —
event_base_dispatch退出循环(可以写到回调里面) —
event_base_free释放资源 —
event_free
libevent代码案例
1 | //编写libevent服务端 |
bufferevent
bufferevent实际上也是一个event, 只不过比普通的event高级一些, 它的内部有两个缓冲区, 以及一个文件描述符(网络套接字)。一个网络套接字有读和写两个缓冲区, bufferevent同样也带有两个缓冲区, 还有就是libevent事件驱动的核心回调函数, 那么四个缓冲区以及触发回调的关系如下:
从图中可以得知, 一个bufferevent对应两个缓冲区, 三个回调函数, 分别是写回调, 读回调和事件回调.
bufferevent有三个回调函数:
- 读回调 – 当bufferevent将底层读缓冲区的数据读到自身的读缓冲区时触发读事件回调.
- 写回调 – 当bufferevent将自身写缓冲的数据写到底层写缓冲区的时候触发写事件回调, 由于数据最终是写入了内核的写缓冲区中, 应用程序已经无法控制, 这个事件对于应用程序来说基本没什么用, 只是通知功能.
- 事件回调 – 当bufferevent绑定的socket连接, 断开或者异常的时候触发事件回调.
主要使用的函数如下:
bufferevent相关函数
- bufferevent_socket_new
- bufferevent_free
- bufferevent_setcb
- bufferevent_write
- bufferevent_write_buffer
- bufferevent_read
- bufferevent_read_buffer
- bufferevent_enable
- bufferevent_disable
- bufferevent_get_output
注意,bufferevent的读写函数本身就是非堵塞的
bufferevent_socket_new函数
bufferevent_socket_new 对已经存在socket创建bufferevent事件, 可用于后面讲到的连接监听器的回调函数中.
1 | struct bufferevent *bufferevent_socket_new( |
-
base:对应根节点 -
fd:文件描述符 -
options: bufferevent的选项BEV_OPT_CLOSE_ON_FREE– 释放bufferevent自动关闭底层接口(当bufferevent被释放以后, 文件描述符也随之被close)BEV_OPT_THREADSAFE– 使bufferevent能够在多线程下是安全的
bufferevent_socket_connect函数
该函数客户端使用,封装了底层的socket与connect接口, 通过调用此函数, 可以将bufferevent事件与通信的socket进行绑定
1 | int bufferevent_socket_connect( |
bev– 需要提前初始化的bufferevent事件serv– 对端(一般指服务端)的ip地址, 端口, 协议的结构指针socklen– 描述serv的长度
说明: 调用此函数以后, 通信的socket与bufferevent缓冲区做了绑定, 后面调用了bufferevent_setcb函数以后, 会对bufferevent缓冲区的读写操作的事件设置回调函数, 当往缓冲区中写数据的时候会触发写回调函数, 当数据从socket的内核缓冲区读到bufferevent读缓冲区中的时候会触发读回调函数.
bufferevent_free函数
释放bufferevent
1 | void bufferevent_free(struct bufferevent *bufev); |
bufferevent_setcb函数
bufferevent_setcb用于设置bufferevent的回调函数
1 | void bufferevent_setcb( |
readcb,writecb,eventcb分别对应了读回调, 写回调, 事件回调cbarg代表回调函数的参数。
两种回调函数的函数原型
1 | //读写回调 |
bufferevent_write函数
将data的数据写到bufferevent的写缓冲区
1 | int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size); |
size 为写多长
bufferevent_write_buffer函数
将data的数据写到bufferevent的写缓冲区的另外一个写法
1 | int bufferevent_write_buffer(struct bufferevent *bufev, struct evbuffer *buf); |
实际上 ,bufferevent的内部的两个缓冲区结构就是struct evbuffer。
bufferevent_read函数
将bufferevent的读缓冲区数据读到data中, 同时将读到的数据从bufferevent的读缓冲清除。
1 | size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size); |
size为要读的最大长度
返回实际读到的字节数
bufferevent_read_buffer函数
将bufferevent读缓冲数据读到buf中, 接口的另外一种。
1 | int bufferevent_read_buffer(struct bufferevent *bufev, struct evbuffer *buf); |
size为要读的最大长度
返回实际读到的字节数
bufferevent_enable函数
设置事件生效,使回调会被触发
1 | int bufferevent_enable(struct bufferevent *bufev, short event); |
event参数参考event_new函数的events参数
bufferevent_disable函数
设置事件不生效,使回调不会被触发
1 | int bufferevent_disable(struct bufferevent *bufev, short event); |
同上
bufferevent_get_output函数
获取bufferevent里的写缓冲区指针
1 | struct evbuffer* bufferevent_get_output(struct bufferevent* bufev); |
bufferevent_get_input函数
获取bufferevent里的读缓冲区指针
1 | struct evbuffer* bufferevent_get_input(struct bufferevent* bufev); |
evbuffer_get_length函数
获取bufferevent里的缓冲区数据长度
1 | int evbuffer_get_length(struct evbuffer* buf) |
链接监听器
链接监听器封装了底层的socket通信相关函数, 比如socket, bind, listen, accept这几个函数。链接监听器创建后实际上相当于调用了socket, bind, listen, 此时等待新的客户端连接到来, 如果有新的客户端连接, 那么内部先进行调用accept处理, 然后调用用户指定的回调函数。可以先看看函数原型, 了解一下它是怎么运作的:
所在头文件: event2/listener.h
链接监听器相关函数
evconnlistener_new_bind函数
在当前没有套接字的情况下对链接监听器进行初始化
1 | struct evconnlistener* evconnlistener_new_bind( |
最后2个参数
sa,socklen实际上就是bind使用的关键参数,backlog是listen函数的关键参数(略有不同的是, 如果backlog是
-1, 那么监听器会自动选择一个合适的值, 如果填0, 那么监听器会认为listen函数已经被调用过了)ptr是回调函数的参数cb是有新连接之后的回调函数但是注意这个回调函数触发的时候, 链接器已经处理好新连接了, 并将与新连接通信的描述符交给回调函数
回调函数格式:
typedef void (*evconnlistener_cb)(struct evconnlistener *evl, evutil_socket_t fd, struct sockaddr *cliaddr, int socklen, void *ptr);flags需要参考几个值:LEV_OPT_LEAVE_SOCKETS_BLOCKING文件描述符为阻塞的LEV_OPT_CLOSE_ON_FREE关闭时自动释放监听描述符LEV_OPT_REUSEABLE端口复用LEV_OPT_THREADSAFE分配锁, 线程安全
上面的flags参数注意区分与bufferevent_socket_new的options参数(BEV开头)不同
evconnlistener_new函数
在当前有套接字的情况下对链接监听器进行初始化
1 | struct evconnlistener *evconnlistener_new( |
evconnlistener_new函数与前一个函数不同的地方在与后2个参数, 使用本函数时, 认为socket已经初始化好, 并且bind完成, 甚至也可以做完listen, 所以大多数时候, 我们都可以使用第一个函数。
参数说明参考evconnlistener_new_bind
上面两个函数的回调函数
1 | typedef void (*evconnlistener_cb)(struct evconnlistener *evl, evutil_socket_t fd, struct sockaddr *cliaddr, int socklen, void *ptr); |
回调函数fd参数是与客户端通信的描述符, 并非是等待连接的监听的那个描述符, 所以cliaddr对应的也是新连接的对端地址信息, 已经是accept处理好的。
evconnlistener_free函数
释放链接监听器
1 | void evconnlistener_free(struct evconnlistener *lev); |
evconnlistener_enable函数
使链接监听器生效
1 | int evconnlistener_enable(struct evconnlistener *lev); |
evconnlistener_disable函数
使链接监听器失效
1 | int evconnlistener_disable(struct evconnlistener *lev); |
bufferevent和链接器服务器流程
主流程
- 创建地基 —
event_base_new - 创建socket,设置复用,listen,监听描述符上树实现监听调用accept(有客户端链接会调用回调函数<客户端连接回调流程>) —
evconnlistener_new_bind - 进入事件循环,等待事件发生 —
event_base_dispatch - 跳出循环,释放资源 —
evconnlistener_free;event_base_free
客户端连接回调流程
- 创建bufferevent(将通信文件描述符和bufferevent绑定) —
bufferevent_socket_new - 设置回调函数:可以是读回调;写回调;信号事件回调 —
bufferevent_setcb - 使回调函数生效(不写也行,默认生效) —
bufferevent_enable
读回调流程
- 读bufferevent的读缓冲区数据 —
bufferevent_read - 其他业务流程
最终代码案例
服务器代码
1 | //编写libevent服务端 |
客户端代码
1 | //编写libevent客户端 |
通信效率优化
通信效率:单位时间内客户端或者服务端接收或者发送数据的量.
多线程处理效率优于多进程(多线程更节省资源)
多路IO复用技术:既不使用多线程,也不使用多进程,在一个进程或一个线程中让多个客户端同时请求服务(都是委托内核进行监控,若有事件发生则通知应用程序)
客户端优化
客户端的效率优化
单进程只处理一个连接
多线程使用同一个连接
多线程使用多个连接 (使用到连接池)
缺点:如果频繁创建连接和销毁连接会有时间消耗
连接池+线程池(避免上述缺点)
连接池只用于客户端
连接池不用于服务端,因为连接是只有客户端发起连接请求之后才会有的.
连接池思想
- 一个数据结构保存连接信息
- 创建连接池操作—poolInit()
- 获取连接池操作 (如队头取)
- 使用完将连接存放的操作 (如存到队尾)
- 可以根据实际需要动态调整连接的数量
- 销毁连接池
在网络编程中,connect()函数用于建立与远程主机的连接。当connect()函数返回-1时,并且errno被设置为
EINPROGRESS,表示连接操作正在进行中,但是仍未完成。这种情况通常发生在非阻塞模式下调用connect()函数时。在非阻塞模式下,connect()函数立即返回,不会阻塞程序,同时设置errno为EINPROGRESS。此时需要通过select()或poll()等函数检查套接字的可写性,以确定连接是否已经建立。如果连接成功建立,套接字将变成可写状态;如果连接建立失败,则套接字将变成可读状态,并且errno被设置为相应的错误码。
需要注意的是,在阻塞模式下调用connect()函数,如果连接建立失败,connect()函数将会阻塞程序并返回相应的错误码,不会设置errno为
EINPROGRESS。
客户端案例
TcpClient.h
1 |
|
TcpClient.cpp
1 |
|
服务端优化
服务端案例
需配合客户端案例使用
TcpServer.h
1 |
|
TcpServer.cpp
1 |
|
web服务器
能够解析http协议的软件的电脑
http协议
- 请求协议
- 应答协议(响应)
1 | //请求 |
http请求消息
我们要开发的服务器与浏览器通信采用的就是http协议,在浏览器想访问一个资源的时候,在浏览器输入访问地址(例如http://127.0.0.1:8000),地址输入完成后当敲击回车键的时候,浏览器就将请求消息发送给服务器
服务器socket收到的数据如下:
这个消息看起来很乱很复杂,对应的就是我们说的请求消息.
请求消息分为四部分内容:
- 请求行 说明请求类型,要访问的资源(请求指令或请求文件等资源,如果是get类型,问号后还会带数据),以及使用的http版本
- 请求头 说明服务器使用的附加信息,都是键值对,比如表明浏览器类型
- 空行 不能省略-而且是
\r\n,包括请求行和请求头中每一行都是以\r\n结尾(\r是移动到末尾,\n是换行) - 请求体(请求数据) 表明请求的特定数据内容,可以省略-如登陆时,会将用户名和密码内容作为请求数据
1 | GET / |
请求类型
http协议有很多种请求类型,对我们来说常见的用的最多的是get和post请求。常见的请求类型如下:
Get请求指定的页面信息,请求数据被包含到url中,并返回实体主体.在http协议中数据被包含在请求行的要访问资源中由于get请求不存在请求体,所以他的请求头中没有
Content-Length和Content-Type键值对.Post向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体(请求数据)中。POST请求可能会导致新的资源的建立和/或已有资源的修改。Head类似于get请求,但是响应消息没有内容,只是获得报头Put从客户端向浏览器传送的数据取代指定的文档内容Delete请求服务器删除指定的页面ConnectHTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器Options允许客户端查看浏览器的性能Trace回显服务器收到的请求,主要用于测试和诊断
get 和 post 请求都是请求资源,而且都会提交数据,如果提交密码信息用get请求,就会在url中明文显示,而post则不会显示出涉密信息.
请求头中重要内容
User-Agent:请求载体的身份标识(用啥发送的请求)Referer:防盗链(这次请求是从哪个页面来的?往往用于反爬)cookie:本地字符串数据信息(用户登录信息,反爬的token)session是服务器为了保存用户状态而创建的一个拥有唯一id特殊对象,并将它以cookie的方式发送给浏览器,当浏览器再次访问此网站时,网站的服务器就会依据session的id找到对应的session对象
其中session是存在服务器上的,cookie是存放在用户端的
session可以类比成酒店前台电脑里的信息,cookie相当于房卡
Content-Type:如果有请求体则必须有该项 表示返回的数据是什么类型的(均以\r\n结尾),如:text/plain纯文本text/html网页text/jsonjson数据- …
Content-Length:表示请求体(请求数据)的长度
Content-Type
- Http协议规定POST提交的数据必须放在消息主题(entity-body)中,但协议并没有规定数据必须使用什么编码方式
- 开发者可以自己决定消息主题的格式
- 数据发送出去,还要服务端解析成功才有意义,服务端通常是根据请求头中的
Content-Type字段来获知请求中的消息主体是用何种方式编码,再对主体进行解析
Content-Type四种常用方式
application/x-www-form-urlencoded1
2
3
4
5
6
7#请求行
POST http://www.example.com
#请求头
Content-Type: application/x-www-form-urlencoded;charset=utf-8
#空行
#请求数据(向服务器提交的数据)
title=test&user=kevin&passwd=32222application/json1
2
3
4POST / HTTP/1.1
Content-Type: application/json;charset=utf-8
{"title":"test","sub":[1,2,3]}text/xml1
2
3
4
5
6
7
8
9
10POST / HTTP/1.1
Content_Type: text/xml
<methodcall>
<methodname color="red">examples.getStateName</methodname>
<params>
<value><i4>41</i4></value>
</params>
</methodcall>multipart/form-data传输大文件(大数据块)常用的格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15POST /
Content-Type: multipart/form-data
#发送的数据(两张图片)
------WebKitFormBoundaryPpL3BfPQ4cHShsBz \r\n
Content-Disposition: from-data; name="file";filename="qw.png"
Content-Type: image/png\r\n; md5="xxxxxxxxx"\r\n
..........具体的文件内容..........
..........具体的文件内容..........
------WebKitFormBoundaryPpL3BfPQ4cHShsBz--
Content-Disposition: from-data; name="file";filename="qw2.png"
Content-Type: image/png\r\n; md5="xxxxxxxxx"\r\n
..........具体的文件内容..........
..........具体的文件内容..........
------WebKitFormBoundaryPpL3BfPQ4cHShsBz--
上面的 ..........具体的文件内容..........指代具体要传的文件内容
http响应消息
响应消息是代表服务器收到请求消息后,给浏览器做的反馈,所以响应消息是服务器发送给浏览器的,响应消息也分为四部分:
- 状态行 包括http版本号,状态码,状态信息
- 消息报头 说明客户端要使用的一些附加信息,也是键值对(Content-Type是必填的)
- 空行 \r\n 同样不能省略
- 响应正文 服务器返回给客户端的文本信息
http常见状态码
http状态码由三位数字组成,第一个数字代表响应的类别,有五种分类:
| 分类 | 描述 |
|---|---|
| 1** | 信息,服务器收到请求,需要请求者继续执行操作 |
| 2** | 成功,操作被成功接收并处理 |
| 3** | 重定向,需要进一步的操作以完成请求 |
| 4** | 客户端错误,请求包含语法错误或无法完成请求 |
| 5** | 服务器错误,服务器在处理请求的过程中发生了错误 |
注意:看到2开头表示成功;看到4开头表示是前端的问题;看到5开头表示后端接口的问题
常见的状态码如下:
| 状态码 | 含义 | 说明 |
|---|---|---|
| 200 | OK | 客户端请求成功 |
| 301 | Moved Permanently | 重定向 |
| 400 | Bad Request | 客户端请求有语法错误,不能被服务器所理解 |
| 401 | Unauthorized | 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用 |
| 403 | Forbidden | 服务器收到请求,但是拒绝提供服务 |
| 404 | Not Found | 请求资源不存在,eg:输入了错误的URL |
| 500 | Internal Server Error | 服务器发生不可预期的错误 |
| 503 | Server Unavailable | 服务器当前不能处理客户端的请求,一段时间后可能恢复正常 |
| 状态码 | 描述 |
|---|---|
| 100 | 继续。客户端应继续其请求 |
| 101 | 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议 |
| — | —————————————– |
| 200 | 请求成功。一般用于GET与POST请求 |
| 201 | 已创建。成功请求并创建了新的资源 |
| 202 | 已接受。已经接受请求,但未处理完成 |
| 203 | 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本 |
| 204 | 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 |
| 205 | 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域 |
| 206 | 部分内容。服务器成功处理了部分GET请求 |
| — | —————————————– |
| 300 | 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择 |
| 301 | 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替 |
| 302 | 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI |
| 303 | 查看其它地址。与301类似。使用GET和POST请求查看 |
| 304 | 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 |
| 305 | 使用代理。所请求的资源必须通过代理访问 |
| 306 | 已经被废弃的HTTP状态码 |
| 307 | 临时重定向。与302类似。使用GET请求重定向 |
| — | —————————————– |
| 400 | 客户端请求的语法错误,服务器无法理解 |
| 401 | 请求要求用户的身份认证 |
| 402 | 保留,将来使用 |
| 403 | 服务器理解请求客户端的请求,但是拒绝执行此请求 |
| 404 | 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置”您所请求的资源无法找到”的个性页面 |
| 405 | 客户端请求中的方法被禁止 |
| 406 | 服务器无法根据客户端请求的内容特性完成请求 |
| 407 | 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权 |
| 408 | 服务器等待客户端发送的请求时间过长,超时 |
| 409 | 服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突 |
| 410 | 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置 |
| 411 | 服务器无法处理客户端发送的不带Content-Length的请求信息 |
| 412 | 客户端请求信息的先决条件错误 |
| 413 | 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息 |
| 414 | 请求的URI过长(URI通常为网址),服务器无法处理 |
| 415 | 服务器无法处理请求附带的媒体格式 |
| 416 | 客户端请求的范围无效 |
| 417 | 服务器无法满足Expect的请求头信息 |
| — | —————————————– |
| 500 | 服务器内部错误,无法完成请求 |
| 501 | 服务器不支持请求的功能,无法完成请求 |
| 502 | 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应 |
| 503 | 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中 |
| 504 | 充当网关或代理的服务器,未及时从远端服务器获取请求 |
| 505 | 服务器不支持请求的HTTP协议的版本,无法完成处理 |
http常见文件类型分类
http与浏览器交互时,为使浏览器能够识别文件信息,所以需要传递文件类型,这也是响应消息必填项 Content-Type ,常见的类型如下:
普通文件: text/plain; charset=utf-8*.html: text/html; charset=utf-8*.jpg: image/jpeg*.gif: image/gif*.png: image/png*.wav: audio/wav*.avi: video/x-msvideo*.mov: video/quicktime*.mp3: audio/mpeg
特别说明 (编码集)
charset=iso-8859-1西欧的编码,说明网站采用的编码是英文;charset=gb2312说明网站采用的编码是简体中文;charset=utf-8代表世界通用的语言编码;可以用到中文、韩文、日文等世界上所有语言编码上charset=euc-kr说明网站采用的编码是韩文;charset=big5说明网站采用的编码是繁体中文;
响应头中重要内容
cookie:本地字符串数据信息(用户登录信息,反爬的token)- 各种莫名其妙的字符串(一般都是token字样,防止各种攻击和反爬)
Location:重定向的url,浏览器收到的响应中如果包含Location字段,则会重定向到Location指定的url地址(浏览器自动进行,用户不可见)
cookie详解
设置Cookie
Set-Cookie是HTTP响应头中的一个字段,用于在客户端(通常是浏览器)上设置一个cookie。
Cookie是一种在客户端存储数据的机制,用于跟踪和识别用户会话。
Set-Cookie字段的格式如下:
Set-Cookie: key=value; Expires=expirationTime; Max-Age=maxAge; Domain=domainName; Path=path; Secure; HttpOnly
key=value: 设置cookie的键值对,用于存储数据。Expires=expirationTime: 指定cookie的过期时间,格式为日期时间字符串。一旦过期,浏览器将删除该cookie。Max-Age=maxAge: 指定cookie的最大存活时间,以秒为单位。在指定的时间过后,浏览器将删除该cookie。Domain=domainName: 指定cookie的作用域,即可以访问该cookie的域名。Path=path: 指定cookie的路径,即可以访问该cookie的路径。Secure: 表示该cookie只能通过HTTPS连接传输。HttpOnly: 表示该cookie只能通过HTTP协议传输,JavaScript无法访问。
通过Set-Cookie字段,服务器可以向客户端发送cookie,以便在后续的请求中识别和跟踪用户
使用Cookie
当向服务器发送HTTP请求时,可以在请求头中使用Cookie字段来发送cookie信息。下面是一个示例HTTP请求头,其中包含了使用cookie的部分:
1 | GET /example HTTP/1.1 |
在上面的示例中,Cookie字段包含了两个cookie键值对:key1=value1和key2=value2。这些cookie将会被发送到服务器,以便服务器可以识别和跟踪用户的会话。
以python为例:
1 | #使用requests库发送第一次HTTP请求,并获取返回的cookie信息。然后在第二次HTTP请求中,通过设置cookies参数为第一次请求获取到的cookie信息,即可在下一次请求中带上cookie信息 |
get_dict()是requests库中Response对象的一个方法,用于获取响应中的cookie信息并以字典形式返回。具体来说,get_dict()方法会将响应中的所有cookie信息转换为一个字典,其中键为cookie的名称,值为cookie的值。这样可以方便地获取和处理响应中的cookie信息,并在需要时将其用于下一次请求中。
cookies = response.cookies.get_dict(): 这句代码将response对象中的所有Cookie信息转换为字典格式,并将其赋值给变量cookies。字典格式的Cookie信息以键值对的形式存储,其中键是Cookie的名称,值是Cookie的值。这种方式可以方便地对Cookie进行访问和操作。cookies = response.cookies: 这句代码直接将response对象中的所有Cookie信息赋值给变量cookies,但是此时cookies并不是字典格式,而是一个RequestsCookieJar对象,它是requests库中用于存储Cookie信息的数据结构。虽然RequestsCookieJar对象提供了一些方法来操作Cookie,但直接访问和操作Cookie可能相对繁琐。
https协议
https = http + SSL/TLS
SSL是TlS的前身
现在绝大部分浏览器都不支持SSL,而是支持TLS,但SSL名声很大,所以很多人把名字混用)
SSL证书: 保存在服务器上的数据文件,表明域名属于谁,日期,还包含了特定的公钥和私钥,要让他生效需要向CA(Certificate Authority)申请.
CA 签发证书的过程:
- ⾸先 CA 会把持有者的公钥、⽤途、颁发者、有效时间等信息打成⼀个包,然后对这些信息进行 Hash 计算,得到⼀个 Hash 值;
- 然后 CA 会使⽤自己的私钥将该 Hash 值加密,⽣成 Certificate Signature,也就是 CA 对证书做了签名;
- 最后将 Certificate Signature 添加在⽂件证书上,形成数字证书;
客户端校验服务端的数字证书的过程:
⾸先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1(数字摘要);
通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使⽤ CA 的公钥解密 Certificate
Signature 内容,得到⼀个 Hash 值 H2(数字摘要)。
最后⽐较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。
TLS握手过程
具体tls握手过程随版本变化,这里以tls1.2为例
tls握手在tcp三次握手之后
web测试工具
curl
一个非常实用的、用来与服务器之间传输数据的工具;支持的协议包括 (DICT, FILE, FTP, FTPS, GOPHER, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMTP, SMTPS, TELNET and TFTP),curl设计为无用户交互下完成工作;curl提供了一大堆非常有用的功能,包括代理访问、用户认证、ftp上传下载、HTTP POST、SSL连接、cookie支持、断点续传…。
格式: curl [options] [URL...]
curl url或curl URL?a=1&b=nihao发送GET请求curl -X POST -d 'a=1&b=nihao' URL发送post请求curl -H "Content-Type: application/json" -X POST -d '{"abc":123,"bcd":"nihao"}' URL发送json格式请求
其中,-H代表header头,-X是指定什么类型请求(POST/GET/HEAD/DELETE/PUT/PATCH),-d代表传输什么数据。这几个是最常用的。-v 显示连接的详细信息
查看所有curl命令: man curl或者curl -h
可以使用
-k或--insecure选项来忽略证书验证。如:curl -k https://localhost
web服务器开发
实现目标
简单功能: 首先解析浏览器发来的请求数据,得到请求的文件名
若文件存在
判断文件类型:
若是普通文件,则发送文件内容给浏览器
若是目录文件,则发送文件列表构成的文件给浏览器
若文件不存在,则发送一个错误页给浏览器
我们要开发web服务器已经明确要使用http协议传送html文件,那么我们如何搭建我们的服务器呢?注意http只是应用层协议,我们仍然需要选择一个传输层的协议来完成我们的传输数据工作,所以开发协议选择是TCP+HTTP,也就是说服务器搭建浏览依照TCP,对数据进行解析和响应工作遵循HTTP的原则.
这样我们的思路很清晰,编写一个TCP并发服务器,只不过收发消息的格式采用的是HTTP协议,如下图:
为了支持并发服务器,我们可以有多个选择,比如多进程服务器,多线程服务器,select,poll,epoll等多路IO工具都可以,甚至如果读者觉得libevent非常熟练的话,也可以使用libevent进行开发.
读取到内容后的流程
socket读取到内容后的流程如下
[注意点]
浏览器中的每一次访问都是一个独立的访问,遇上一次的访问没有关系,即浏览器的任何链接跳转,都是关闭连接后建立新连接
在浏览器中访问的资源是一个中文的名称的话,需要进行编码的转换
web服务端资源目录取决于web服务器程序的工作目录,可以通过
chdir函数切换工作目录若浏览器关闭读端,会导致服务器进程退出,原因是当浏览器关闭读端后,而服务端还继续往一个关闭读端的链接写数据会收到到
SIGPIPE信号,这个信号的默认处理动作是使进程终止解决方案: 忽略该信号
signal(SIGPIPE,SIG_IGN);
基于epoll的web服务器
由于我们知道epoll在大量并发少量活跃的情况下效率很高,所以本文以epoll为例,介绍epoll开发的主体流程:
基于select的web服务器
1 |
|
中文汉字编码问题
举例:浏览器请求的是localhost:8080/苦瓜.txt ,web服务器收到之后会显示: %E8%8B%A6%E7%93%9C.txt,需要进行转换,utf-8格式的一个中文汉字占3个字节,首先需要去掉文件名中的%,然后逐个将字符串形式的(例如E8)转换成数值,然后保存到数组当中.
1 | //16进制单字符转化为10进制单字符, return 0不会出现 |
调用 strdecode 把代表unicode的字符串变成真正的unicode编码(其实就是16进制存储)
一些特殊符号
相关知识
html中的 空格 (space)是 
%20URL编码中的空格(space)%21—!%22—"%23—#%24—$%25—%%26—&%27—'%28—(
基于libevent的web服务器
1 | //编写libevent服务端 |
与select,epoll的大同小异,对比select其实只是将read/write函数相对应换成bufferevent_read/bufferevent_write,并将httprequest的参数的文件描述符换成struct bufferevent *
web服务器框架
- tomcat服务器
- apache组织产品,开源的免费服务器
- weblogic服务器
- bea公司,收费的服务器
- 不交费的话访问量受到限制
- IIS服务器
- Internet Information Server
- 微软公司主推的服务器
- nginx
- 小巧且高效的HTTP服务器
- 也可以做一个高效的负载均衡反向代理
- 邮件服务器
其他的还有:(下面的都可以实现路由到函数的功能)
- cppcms:一个轻量级、高性能的 C++ Web 框架,具有类似 Flask 的路由和模板引擎功能。
- Pistache:一个高性能、非阻塞的 C++ Web 框架,具有类似 Flask 的路由和中间件功能。
- oatpp:一个现代、高性能的 C++ Web 框架,具有类似 Flask 的路由和依赖注入功能。
- Casablanca:一个跨平台、高性能的 C++ Web 框架,具有类似 Flask 的路由和异步 I/O 功能。
Flask是python用的web框架
Nginx
[[nginx#基本介绍|nginx详解跳转]]
REST API
Boost.Asio 是一个用于建立异步 I/O 应用程序的 C++ 库,它提供了网络编程、串口编程、进程间通信等功能。在这个示例中,我们使用了 Boost.Asio 库提供的异步 I/O 操作来处理请求和响应,从而构建一个简单的 REST API。
1 | boost/asio.hpp |
包含了许多常用的 Boost.Asio 头文件。 -
1 | boost/asio/ssl.hpp |
包含了使用 Boost.Asio 进行 SSL 通信的头文件。
CGI
通用网关接口(Common Gateway Interface)描述了客户端和服务器程序之间传输数据的一种标准,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据.CGI独立于任何语言,CGI程序可以用任何脚本语言或者是完全独立编程语言实现,只要这个语言可以在这个系统上运行
- 用户通过浏览器访问服务器,发送了一个请求
- 服务器接受数据并解析
- nginx对于一些登录数据不知道如何处理,nginx将这些数据转发给了cgi程序
- 服务器端会创建一个cgi进程(每一个客户端请求,都会创建一个cgi进程)
- CGI进程执行后返回结果,返还nginx,自身销毁,nginx回复客户端.
因此CGI会频繁创建与销毁,效率低,由此fastCGI诞生.
fastCGI
快速通用网关接口(Fast Common Gateway Interface)是通用网关接口(CGI)的改进,描述了客户端和服务器程序之间传输数据的一种标准.FastCGI致力于减少Web服务器与CGI程序之间互动的开销,从而使服务器可以同时处理更多的Web请求.与为每个请求创建一个新的进程不同,FastCGI使用持续的进程来处理一连串的请求.这些进程由FastCGI进程管理器管理,而不是web服务器
与CGI的区别:
- CGI是短生存期应用程序
- FastCGI是长生存期应用程序,像一个常驻(long-live)型的CGI,可以一直执行,不需要每次都创建与销毁
- 用户通过浏览器访问服务器,发送了一个请求
- 服务器接受数据并解析
- nginx对于一些登录数据不知道如何处理,nginx将这些数据通过套接字转发给了fastCGI进程管理器
- fastCGI程序启动(不是由web服务器直接启动,而是通过fastCGI进程管理器启动)
- fastCGI程序执行完毕后,通过套接字将处理结果返回给web服务器,服务器回复客户端
安装与配置
需要安装两个
fastCGI框架
根目录/libfcgi/.libs/libfcgi++.so根目录/libfcgi/.libs/libfcgi.sospawn-fcgi (fastCGI进程管理器)
nginx不能像apache那样直接执行外部可执行程序,但nginx可以作为代理服务器,将请求转发给后端服务器,这也是nginx的主要作用之一.其中nginx就支持fastCGI代理,接受客户端的请求,然后将请求转发给后端fastcgi进程.下面介绍如何使用C/C++编写cgi/fastcgi,并部署到nginx中.
**
fastCGI**进程由fastCGI进程管理器管理,而不是nginx.这样就需要一个FastCGI管理器,管理我们编写的fastcgi程序.我们使用spawn-fcgi作为FastCGI进程管理器**
spawn-fcgi**是一个通用的FastCGI进程管理器,简单小巧,原先是属于lighttpd的一部分,后来由于使用比较广泛,所以就迁移出来作为独立新项目了.spawn-fcgi使用pre-fork模型,功能主要是打开监听端口,绑定地址,然后fork-and-exec创建我们编写的fastcgi应用程序进程,退出完成工作.fastcgi应用程序初始化,然后进入死循环侦听socket的连接请求
- 客户端访问,发送请求
- nginx web服务器,无法处理用户提交的数据,转发数据
- spawn-fcgi 通信过程中的服务器角色
- 被动接受数据
- 在spawn-fcgi启动的时候给其绑定ip和端口
- fastCGI程序
- 程序员写的login.c -> 可执行程序login
- 使用spawn-fcgi进程管理器启动login程序得到一个进程
nginx的数据转发配置
1 | location /login |
spawn-fcgi启动
1 | 前提条件,程序员已经写好fastCGI可执行程序 |
如果返回spawn-fcgi: child spawned successfully: PID: 464863表示成功;如果显示退出,bing返回错误码127,即fastCGI程序有问题.
fastCGI环境变量
| 环境变量 | 说明 |
|---|---|
| SCRIPT_FILENAME | 脚本文件请求的路径 |
| QUERY_STRING | 请求的参数;如?app=123 |
| REQUEST_METHOD | 请求的动作(GET/POST) |
| CONTENT_TYPE | 请求头中的Content-Type字段 |
| CONTENT_LENGTH | 请求头中的Content-length字段 |
| SCRIPT_NAME | 脚本名称 |
| REQUEST_URI | 请求的地址不带参数 |
| DOCUMENT_URI | 与$uri相同 |
| DOCUMENT_ROOT | 网站的根目录.在server配置中root指令中指定的值 |
| SERVER_PROTOCOL | 请求使用的协议,通常是HTTP/1.0或HTTP/1.1 |
| GATEWAY_INTERFACE | cgi版本号 |
| SERVER_SOFTWARE | nginx版本号,可修改,隐藏 |
| REMOTE_ADDR | 客户端IP |
| REMOTE_PORT | 客户端端口 |
| SERVER_ADDR | 服务器IP地址 |
| SERVER_PORT | 服务器端口 |
| SERVER_NAME | 服务器名,域名在server配置中指定的server_name |
| … | … |
fastCGI程序开发
参考fastCGI安装的根目录/sample/echo.c
要包含的头文件
1 |
开发fastCGI需要使用到的库文件(二选一)
1 | 库文件 |
fastCGI如何处理数据
在FastCGI程序开发中,标准输入和标准输出被重定向到了socket连接。
1 | while(FCGI_Accept()>=0) |
简单案例
1 |
|
分布式服务器
- nginx
- 能处理静态请求 -> html,jpg
- 动态请求无法处理
- 服务器集群之后,每台服务器上部署的内容必须相同
- FastCGI
- 帮助服务器处理动态请求
- 反向代理服务器
- 客户端并不能直接访问到web服务器,直接访问到的是反向代理服务器
- 客户端将请求发送给反向代理服务器,反向代理服务器将客户端请求转发给服务器
- 关系型[[数据库]]
- 存储文件属性信息
- 用户属性信息
- redis - 非关系型[数据库]
- 提高程序效率
- 存储的是服务器经常要从关系型数据库中读取的数据
- FASTDFS - 分布式文件系统
- 存储文件内容
- 供用户下载
分布式文件系统
文件系统的全部, 不在同一台主机上,而是在很多台主机上,多个分散的文件系统组合在一起,形成了一个完整的文件系统。
分布式文件系统:
- 需要有网络
- 多台主机
不需要在同一地点 - 需要管理者
- 编写应用层的管理程序
不需要编写
常用分布式文件系统
| 分布式文件系统 | github star | os支持 |
|---|---|---|
| minio | 25.1k | win/linux |
| fastdfs | 7k | win |
| ceph | 8.6k | win/linux |
| GlusterFS | 2.9k | win/linux |
FastDFS
是用c语言编写的一款开源的分布式文件系统。
开发者:余庆 - 淘宝的架构师
为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,注重高可用、高性能等指标
- 冗余备份: 纵向扩容
- 线性扩容: 横向扩容
可以很容易搭建一套高性能的文件服务器集群提供文件 上传、下载 等服务。E.g.图床,网盘
fastDFS框架中的三个角色(进程)
- 追踪器(Tracker) - 管理存储节点的管理者 - 守护进程
- 存储节点(storage) -守护进程
- 客户端 - 通过连接追踪器找到目标存储节点,通过连接存储节点进行上传与下载操作
- 文件上传
- 文件下载
三者的关系
追踪器
最先启动追踪器
存储节点
第二个启动的角色
存储节点启动之后, 会单独开一个线程向追踪器汇报信息
- 汇报当前存储节点的容量, 和剩余容量
- 汇报数据的同步情况
- 汇报数据被下载的次数
客户端
最后启动
上传
- 连接追踪器, 询问存储节点的信息
- 我要上传1G的文件, 询问那个存储节点有足够的容量
- 追踪器查询, 得到结果
- 追踪器将查到的存储节点的IP+端口发送给客户端
- 通过得到IP和端口连接存储节点
- 将文件内容发送给存储节点
下载
- 连接追踪器, 询问存储节点的信息
- 问一下, 要下载的文件在哪一个存储节点
- 追踪器查询, 得到结果
- 追踪器将查到的存储节点的IP+端口发送给客户端
- 通过得到IP和端口连接存储节点
- 下载文件
FastDFS集群
- 集群的优点:避免单点故障(主要是针对tracker)
- 多个tracker靠被轮询工作
- 实现集群的方式:修改配置文件
fastDFS管理存储节点的方式:通过分组的方式完成
集群方式扩容方式
横向扩容:增加容量
添加一台新的主机 -> 容量增加
新主机作为一个新的组
不同的组间的主机之间不需要互相通信
纵向扩容:数据备份
将新的主机放到现有的组中
一个组中的多台主机之间的关系是相互备份的关系
因此同一个组间的主机之间需要互相通信
$$
每一个组的大小 = 组中最小的主机的大小
$$
$$
fastDFS集群存储容量 = 每一个组的大小之和
$$
因此上图中fastDFS集群的最大存储容量为2T(500G+1.5T)
fastDFS安装
下载源码后,按照INSTALL文件的描述进行安装与配置
fastDFS安装的所有可执行程序位置: /usr/bin/fdfs_*
主要配置项
追踪器配置文件
/etc/fdfs/tracker.conf1
2
3
4
5
6
7#将追踪器和部署主机的IP地址进行绑定,也可以不写
#如果不写会自动绑定当前主机IP,如果为云服务器建议不写
bing_addr=
#追踪器监听的端口(默认22122)
port=22122
#追踪器存储日志信息的目录,xxx.pid文件,必须是一个存在的目录,并且用户需要对该路径中的文件有读写权限
base_path=/home/yuqing/fastdfs存储节点配置文件
/etc/fdfs/storage.conf1
2
3
4
5
6
7
8
9
10
11
12
13
14#当前存储节点对应的主机属于哪一个组
group_name=group1
#当前存储节点和部署主机的IP进行绑定,如果为云服务器建议不写
bing_addr=
#存储节点监听的端口(默认23000),监听客户端的连接
port=23000
#存储节点存储日志信息的目录,xxx.pid文件,必须是一个存在的目录,并且用户需要对该路径中的文件有读写权限
base_path=/home/yuqing/fastdfs
#存储节点提供的存储文件的路径个数
store_path_count = 1
#具体的存储路径,上面项为2的话,存储路径还需要填写store_path1
store_path0 = /home/yuqing/fastdfs
#设置追踪器的ip地址与端口 (该项可以多次,即轮询多个追踪器,用于多个追踪服务器。追踪服务器的值格式为“HOST:PORT”,HOST可以是主机名或IP地址,并且HOST可以是用逗号分隔的双IP或主机名,双IP必须是内部(内网)IP和外部(外网)IP,或两种不同类型的内部(内网)IP。例如:192.168.2.100,122.244.141.46:22122 另一个例子:192.168.1.10,172.17.4.21:22122)
tracker_server = 192.168.209.121:22122客户端配置文件
/etc/fdfs/client.conf1
2
3
4
5#客户端存储日志信息的目录,xxx.pid文件,必须是一个存在的目录,并且用户需要对该路径中的文件有读写权限
base_path = /home/yuqing/fastdfs
#设置追踪器的ip地址与端口 (填写规则同上存储节点配置文件)
tracker_server = 192.168.0.196:22122
FastDFS启动
- 启动追踪器
- 启动存储节点
- 启动客户端
格式为:程序 配置文件路径 [stop/restart]([stop/restart]省略表示第一次启动) 如:fdfs_storaged /etc/fdfs/storage.conf
1 | step 6. run the server programs |
上传下载代码实现
使用多进程方式实现
exec函数组函数
- execl
- execlp
父进程->创建子进程:执行execlp("fdfs_upload_file","xx",arg,NULL),有结果输出,用dup2管道重定向标准输出到管道的写端.父进程读管道读端
使用fastDFS API实现
余庆未提供api文档,只能通过看调用案例找API
安装文件夹中有test文件夹,该文件夹中的是调用案例
参考项目中自带的案例来实现
fastDFS配合fastCGI项目
在fastDFS存储节点上安装Nginx,需要交叉编译nginx和nginx的fastDFS插件.
1
2
3
4交叉编译nginx和nginx的fastDFS插件,在nginx目录中执行下面命令:
./configure --add-module=/root/Desktop/fastdfs-nginx-module/src --with-openssl=../nginxNeed/openssl-1.1.1u --with-pcre=../nginxNeed/pcre2-10.42 --with-zlib=../nginxNeed/zlib-1.2.13
make
make install从fastDFS插件的源码目录的src目录中找
mod_fastdfs.conf执行sudo cp mod_fastdfs.conf /etc/fdfs,修改mod_fastdfs.conf(参考storage.conf来修改)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#日志位置
base_path=/home/xxx/fastdfs/storage
#配置追踪器的ip和端口
tracker_server=xxx.xxx.xxx.xxx:22122
#存储节点绑定的端口
storage_server_port=23000
#当前存储节点所属组
group_name=group1
#客户端下载文件时候,这个下载的url中是否包含组的名字(仅在只有同一个组的时候,这个选项可以设为false)
url_have_group_name=true
#存储节点绑定的IP地址
bind_addr=
#存储节点上存储路径的个数
store_path_count=1
#存储路径的详细位置(可以多个)
store_path0=/home/xxx/fastdfs/storage
#store_path1=...重新启动nginx后,执行
ps aux | grep nginx发现没有worker进程,查看/usr/local/nginx/logs/error.log日志,报错如下:1
2
3
4
5
6
7ERROR - file: ini_file_reader.c, line: 1051, include file "http.conf" not exists, line: "#include http.conf"
#解决方式如下:从fastDFS源码安装目录找./conf,执行下面命令:
sudo cp http.conf /etc/fdfs
ERROR - file: shared_func.c, line: 1301, file /etc/fdfs/mime.types not exist
#解决方式如下:从fastDFS源码安装目录找./conf,执行下面命令:
sudo cp mime.types /etc/fdfs资源在存储节点的存储目录中,store_path0等中,需要告诉nginx服务器资源在哪,
nginx.conf添加location1
2
3
4
5
6
7#location /group1/M00/00/00/
#由于/00/00经常变化并不是固定目录,因此:
location /group1/M00/{
#告诉服务器资源的位置
root /opt/fastdfs/data;
ngx_fastdfs_module;#标记需要通过fastdfs插件处理
}添加了之后重启效果如下:
ngx_http_fastdfs_set pid=699791
日志实现案例
源文件
1 |
|
头文件log.h
1 |
|
使用方式
msgInit("日志文件名")初始化日志文件msglog([MSG_INFO或MSG_BOTH],"",...)输出内容到日志文件msgLogClose()关闭日志文件
报文编解码
- 报文: 网络通信过程中接受发送的数据
- 编码: 将传输的数据转换为字节流的过程
ASN.1: 通用的一种编码格式
两台机器通信有如下要考虑
- 网络传输的时候需要用大端模式
- 字节序对齐不同,比如结构体
- 等等….
常用序列化方式
XML
XML(Extensible Markup Language),类似于html
XML是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点.XML历史悠久,其1.0版本早在1998年就形成标准,并被广泛使用至今
XML最初产生的目标是对互联网文档进行标记,所以它的设计理念中就包含了对于人和机器都具备可读性.但是,当这种标记文档的设计被用来序列化对象的时候,就显得冗长而复杂
XML基本格式:
1 |
|
Json
Json(JavaScript Object Notation)
JSON起源于弱类型语言Javascript,它的产生来自于一种称之为”关联数组(Associative array)”的概念,其本质是采用**”键值对”**的方式来描述对象
JSON格式保持了XML的人眼可读的优点,非常符合工程师对对象的理解
相对于XML而言,序列化后的数据更加简洁(XML所产生序列化之后文件的大小接近JSON的两倍),而且其协议比较简单,解析速度比较快
JSON格式具备Javascript的先天性支持,所以被广泛应用于Web browser的应用场景中,是Ajax的事实标准协议.
说明
- json对象格式的字符串都是以键值对的形式存在的—>
key:value,其中key都是以字符串的形式存在的,而value部分可以包含:int bool char double等多种类型,value还可以是子对象或者数组 - 在json文件中,只能是json对象和json数组,不能既有json对象也有json数组
- 在json对象内部,可以包含子json对象,也可以有子json数组
- 在json数组内部,可以有子json对象,也可以有子json数组
Protocol Buffer
Protocol Buffer(简称Protobuf)是Google公司内部的混合语言数据标准,它是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或RPC数据交换格式
- 高效的序列化:protobuf使用二进制编码,相比于文本格式如JSON和XML,它的序列化后的数据体积更小,传输效率更高。
- 跨平台和语言无关:protobuf定义了一种独立于语言和平台的数据描述语言,可以在不同的编程语言之间进行数据的序列化和反序列化,使得不同系统之间的数据交换更加方便。
- 可扩展性:protobuf支持向后兼容和向前兼容的数据格式演化,可以在不破坏现有数据结构的情况下进行数据模型的更新和扩展。
- 向后兼容性(Backward compatibility):新版本的数据模型可以解析旧版本的数据,即新模型可以正确地处理旧数据,不会导致解析错误或数据丢失。这意味着旧版本的数据可以与新版本的代码一起使用。
- 向前兼容性(Forward compatibility):旧版本的数据可以被新版本的数据模型解析,即旧模型可以正确地处理新数据,不会导致解析错误或数据丢失。这意味着新版本的数据可以与旧版本的代码一起使用。
- 代码生成:protobuf提供了代码生成工具(protoc命令行工具),可以根据定义的数据结构自动生成对应的类代码,简化了数据的编码和解码过程,提高了开发效率。
- 可读性:protobuf的定义文件采用了类似于IDL(接口定义语言)的结构,可以清晰地描述数据的结构和字段含义,便于理解和维护。
ASN.1和Protocol的主要应用场景区别
- ASN.1:ASN.1广泛应用于电信领域、安全协议、网络管理等场景,例如在SNMP(Simple Network Management Protocol)和TLS(Transport Layer Security)中使用。
- Protocol Buffers:Protocol Buffers被广泛用于分布式系统通信、数据存储、配置文件、RPC等领域。
其他
- ASN.1抽象语法标记(Abstract Syntax Notation One)
- boost序列化的类
- 自定义的格式
ASN.1
ASN.1抽象语法标记,是一种ISO/ITU-T标准,是描述在网络上传输信息格式的标准方法.描述了一种对数据进行表示,编码,传输和解码的数据格式.它提供了一整套正规的格式用于描述对象的结构,而不管语言上如何执行及这些数据的具体指代,也不用去管到底是什么样的应用程序
本质解决的是在网络上传输数据结构的数据
ASN.1有两部分
- 一部分描述信息内数据,数据类型及序列格式(相当于属性)
- 一部分描述如何将各部分组成消息(相当于方法)
标准的ASN.1编码规则有:
- 基本编码规则(BER,Basic Encoding Rules)
- 规范编码规则(CER,Canonical Encoding Rules)
- 唯一编码规则(DER,Distinguished Encoding Rules)
- 压缩编码规则(PER,Packed Encoding Rules)
- XML编码规则(XER,XML Encoding Rules)
编码格式(TLV格式,即Tag,Length,Value组成的格式)
| tag(type) | Length | value |
|---|---|---|
| 数据类型 | 数据长度 | 数据的值 |
下面结构体编码图示:
1 | typedef struct _Persion |
TLV节点对应的结构体
1 | tupedef struct ITCAST_ANYBUF_ |
原理:
- 定义一个结点以TLV形式存储编码后的数据
- 被编码对象的每一个成员属性都会成为一个结点。从对象的一个成员开始到最后一个成员,所有新建的结点会连成一条双向链表,便于后续对象成员依次解码
- 整个对象也会被编码,即将链表的首结点封装成一个新的节点,作为传出参数传出,便于解码时以对象为单位进行解码
TLV原理图:
相关函数
编解码函数中包括对各种基本数据类型的编解码函数,用于对对象的成员属性进行分别编码;同时还包括对对象进行编解码的函数,即对成员属性编码后形成的链表首结点进行编码
- DER_ItAsn1_WriteInteger 整形->ITCAST_ANYBUF
- DER_ItAsn1_ReadInteger ITCAST_ANYBUF->整形
- DER_ItAsn1_WritePrintableString
- DER_ItAsn1_ReadPrintableString
- DER_ITCAST_String_To_AnyBuf char* ->ITCAST_ANYBUF
- EncodeChar/DecodeChar char* <->ITCAST_ANYBUF
- DER_ItAsn1_WriteSequence ITCAST_ANYBUF链表 -> 发送的字节流
- DER_ItAsn1_ReadSequence 接受的字节流 -> ITCAST_ANYBUF链表
- DER_ITCAST_FreeQueue 释放内存
DER_ItAsn1_WriteInteger
1 | ITCAST_INT DER_ItAsn1_WriteInteger(ITCAST_UINT32 integer,ITASN1_INTEGER **ppDerInteger); |
功能:对整形数进行编码操作 int-> ITCAST_ANYBUF
函数参数:
integer: 输入参数, 表示待编码的整形数据
ppDerInteger: 传出参数, ITCAST_ANYBUF指针的指针, 编码之后的数据,用于构造链表
返回值:
成功或者失败
DER_ItAsn1_ReadInteger
1 | ITCAST_INT DER_ItAsn1_ReadInteger(ITASN1_INTEGER *pDerInteger, ITCAST_UINT32 *pInteger); |
函数说明: 对整形数据解码 int-> ITCAST_ANYBUF
参数说明:
pDerInteger: 传入参数, ITCAST_ANYBUF指针, 表示待解码的数据
pInteger: 传出参数, 表示解码之后的数据
返回值:
成功或者失败
调用例子
1 | ITCAST_ANYBUF p; |
DER_ItAsn1_WritePrintableString
1 | ITCAST_INT DER_ItAsn1_WritePrintableString(ITASN1_PRINTABLESTRING *pPrintString, ITASN1_PRINTABLESTRING **ppDerPrintString); |
函数说明: 编码字符串数据
函数参数:
pPrintString: 输入参数, ITCAST_ANYBUF指针,表示要编码的数据
ppDerPrintString: 输出参数, ITCAST_ANYBUF指针的指针, 表示编码成链表格式之后的数据,用于构造链表
返回值:
成功或者失败
DER_ItAsn1_ReadPrintableString
1 | ITCAST_INT DER_ItAsn1_ReadPrintableString(ITASN1_PRINTABLESTRING *pDerPrintString, ITASN1_PRINTABLESTRING **ppPrintString); |
函数说明: 解码函数, 将ANYCAST_ANYBUF类型解码到第二个参数
参数说明:
pDerPrintString: 输入参数,ITCAST_ANYBUF指针, 表示待解码的链表数据
ppPrintString: 输出参数, ITCAST_ANYBUF指针的指针, 存放解码之后的数据
返回值:
成功或者失败
DER_ITCAST_String_To_AnyBuf
1 | ITCAST_INT DER_ITCAST_String_To_AnyBuf(ITCAST_ANYBUF **pOriginBuf, unsigned char * strOrigin, int strOriginLen); |
函数说明: 原始字节流转换为ITCAST_ANYBUF类型字节流,将char *—->ITCAST_ANYBUF类型
函数参数:
pOriginBuf: 传出参数, ITCAST_ANYBUF指针的指针
strOrigin: 传入参数, 待转换的字符串
strOriginLen: 传入参数, strOrigin的字符串长度
返回值:
成功或者失败
EncodeChar/DecodeChar
EncodeChar
1 | int EncodeChar(char *pData, int dataLen, ITCAST_ANYBUF **outBuf); |
函数说明: 将char *类型数据进行编码,相当于集成了DER_ITCAST_String_To_AnyBuf和(DER_ItAsn1_WritePrintableString
函数参数:
pData: 输入参数, 指的是待编码的字符串
dataLen: 输入参数, 指的是pData的长度
outBuf: 输出参数, ITCAST_ANYBUF指针的指针, TLV格式
DecodeChar
DecodeChar基本同理,某些情况下相当于集成了DER_ITCAST_String_To_AnyBuf和DER_ItAsn1_ReadPrintableString,又有所区别
1 | int DecodeChar(ITCAST_ANYBUF *inBuf, char **Data, int *pDataLen); |
解码上两种方式的区别体现
如下结构体中
1 | typedef struct _Temp |
解码name时不可以使用DecodeChar来解码,p可以.原因如下:
1 | int DecodeChar(char** p) |
DER_ItAsn1_WriteSequence
1 | ITCAST_INT DER_ItAsn1_WriteSequence(ITASN1_SEQUENCE *pSequence, ITCAST_ANYBUF **ppDerSequence); |
函数说明: 序列化链表, 将ITCAST_ANYBUF链表序列化成ITCAST_ANYBUF类型的字节流数据
函数参数:
pSequence: 输入参数, ITCAST_ANYBUF指针类型,待序列化的数据
ppDerSequence: 输出参数, ITCAST_ANYBUF指针的指针类型, 序列化之后的数据
DER_ItAsn1_ReadSequence
1 | ITCAST_INT DER_ItAsn1_ReadSequence(ITCAST_ANYBUF *pDerSequence, ITASN1_SEQUENCE **ppSequence); |
函数说明: 反序列化,将ITCAST_ANYBUF类型的字节流数据反序列化成ITCAST_ANYBUF链表
参数说明:
pDerSequence:输入参数, 开始需要将char *—>ITCAST_ANYBUF类型
ppSequence: 输出参数, 获得链表头节点
DER_ITCAST_FreeQueue
1 | ITCAST_INT DER_ITCAST_FreeQueue(ITCAST_ANYBUF *pAnyBuf); |
函数说明:释放内存
使用案例
简单使用案例
1 |
|
main函数
1 |
|
执行结果
进一步封装
封装uml图如下:
其中的SequenceASN1部分:
SequenceASN1.h
1 |
|
SequenceASN1.cpp
1 |
|
CSharp网络通信
主要是下面几种方式,它们都位于System.Net.Sockets命名空间中
- Socket
- TcpClient 和 TcpListener
- UdpClient 和 UdpListene
IPAddress 是 .NET 中的一个类,属于 System.Net 命名空间。
类型:
IPAddress是一个结构(struct),用于表示 IPv4 或 IPv6 地址。用途:它提供了处理 IP 地址的功能,例如解析字符串形式的 IP 地址、比较 IP 地址、获取地址的字节表示等。
IPEndPoint: 表示一个网络端点,它结合了一个 IP 地址和一个端口号。
TcpClient/TcpListener
TcpClient
| 属性及方法 | 说明 |
|---|---|
| Available属性 | 获取已经从网络接收且可供读取的数据量 |
| Client属性 | 获取或设置基础Socket |
| Connected属性 | 获取一个值,该值指示TepClient的基础Socket是否已连接到远程主机 |
| RecieveBufferSize属性 | 获取或设置接收缓冲区的大小 |
| RecieveTimeout属性 | 获取或设置在初始化一个读取操作后TcpClient等待接收数据的时间量 |
| SendBufferSize属性 | 获取或设置发送缓冲区的大小 |
| SendTimeout属性 | 获取或设置TcpClient等待发送操作成功完成的时间量 |
| BeginConnect方法 | 开始一个对远程主机连接的异步请求 |
| Close方法 | 释放此TcpClient实例,而不关闭基础连接 |
| Connec方法 | 使用指定的主机名和端口号将客户端连接到TCP主机 |
| EndConnect方法 | 异步接受传入的连接尝试 |
| GetStream方法 | 返回用于发送和接收数据的NetworkStream |
TcpListener
| 属性及方法 | 说明 |
|---|---|
| LocalEndpoint属性 | 获取当前TcpListener的基础EndPoint |
| Server属性 | 获取基础网络Socket |
| AcceptSocket/AcceptTcpClient方法 | 接受挂起的连接请求 |
| BeginAcceptSocket/BeginAcceptTcpClient方法 | 开始一个异步操作来接受一个传入的连接尝试 |
| EndAcceptSocket方法 | 异步接受传入的连接尝试,并创建新的Socket来处理远程主机通信 |
| EndAcceptTcpClient方法 | 异步接受传入的连接尝试,并创建新的TcpClient来处理远程主机通信 |
| Start方法 | 开始侦听传入的连接请求 |
| Stop方法 | 关闭侦听器 |
案例
服务端
1 | // Server |
客户端
1 | // Client |
二进制读写
1 | //reader和writer与上面定义一致 |
Socket
Socket的优势:
- 协议不仅支持 TCP,还可以处理其他协议,如 UDP、ICMP 等
- 更精细度的资源管理,如可以直接处理底层的协议、套接字选项(如缓冲区大小、超时设置等)
- 支持更完整的异步操作,可以使用 BeginConnect、EndConnect、BeginReceive、EndReceive 等方法实现非阻塞的网络通信
- 更强的自定义能力,如可以直接控制连接的建立、数据传输等过程,支持更多高级特性,比如多播、广播、TCP 队列控制等
常用属性及说明
| 属性 | 说明 |
|---|---|
| AddressFamily | 获取Socket的地址族 |
| Availabe | 获取已经从网络接收且可供读取的数据量 |
| Connected | 获取一个值,该值指示Socket是在上次Send还是Receive操作时连接到远程主机 |
| Handle | 获取Socket的操作系统句柄 |
| LocalEndPoint | 获取本地终结点 |
| ProtocolType | 获取Socket的协议类型 |
| RemoteEndPoint | 获取远程终结点 |
| SendTimeout | 获取或设置一个值,该值指定之后同步Send调用将超时的时间长度 |
常用方法及说明
| 方法 | 说明 |
|---|---|
| Accept | 为新建连接创建新的Socket |
| BeginAccept | 开始一个异步操作来接受一个传入的连接尝试 |
| BeginConnect | 开始一个对远程主机连接的异步请求 |
| BeginDisconnect | 开始异步请求从远程终结点断开连接 |
| BeginReceive | 开始从连接的Socket中异步接收数据 |
| BeginSend | 将数据异步发送到连接的Socket |
| BeginSendFile | 将文件异步发送到连接的Socket |
| BeginSendTo | 向特定远程主机异步发送数据 |
| Close | 关闭Socket连接并释放所有关联的资源 |
| Connect | 建立与远程主机的连接 |
| Disconnect | 关闭套接字连接并允许重用套接字 |
| EndAccept | 异步接受传入的连接尝试 |
| EndConnect | 结束挂起的异步连接请求 |
| EndDisconnect | 结束挂起的异步断开连接请求 |
| EndReceive | 结束挂起的异步读取 |
| EndSend | 结束挂起的异步发送 |
| EndSendFile | 结束文件的挂起异步发送 |
| EndSendTo | 结束挂起的、向指定位置进行的异步发送 |
| Listen | 将Socket置于侦听状态 |
| Receive | 接收来自绑定的Socket的数据 |
| Send | 将数据发送到连接的Socket |
| SendFile | 将文件和可选数据异步发送到连接的Socket |
| SendTo | 将数据发送到特定终结点 |
| Shutdown | 禁用某Socket上的发送和接收 |
案例
客户端
1 | using Socket client = new( |
服务器
1 | using Socket listener = new( |
常用网络相关命令
sudo lsof -i:端口号 查询端口占用
windows排查端口占用:
1 | 通过端口号查找占用的进程的pid |
windows用于ping测试ip地址的脚本
1 | @echo off |
UDP开发相关
UDP无连接特性
服务端建立连接后,只有客户端先发送消息,后续客户端与服务端才能!
服务端启动监听
命令:
nc -u -l 62334操作系统为
nc创建了一个 UDP Socket,并将其绑定(bind())到本地的 62334 端口,此时服务器不知道客户端的 IP 和端口,因此他如果主动往外发消息,没有客户端能收到客户端发送命令
命令:
ncat -v -u 192.168.8.159 62334客户端操作系统分配了一个随机的高位端口(例如 54321),并准备好向 192.168.8.159:62334 发送数据
因为是 UDP,此时网络上没有任何数据包飞向服务端。客户端仅仅是“瞄准”了服务端,但还没“开枪”。服务端对此一无所知
客户端发送内容(破冰时刻)
在客户端输入文字并回车: 客户端终于发出了第一个 UDP 数据包
网络数据包结构: 这个数据包包含了:
- 源 IP: 客户端的 IP
- 源端口: 客户端的随机端口(如 54321)
- 目标 IP: 192.168.8.159
- 目标端口: 62334
服务端收到内容
nc通过recvfrom()函数不仅读到了客户端发来的消息,更重要的是,它顺带提取出了这个包的“源 IP 和源端口”建立“伪连接”与释放缓存:
- 服务端终于睁开眼睛,看清了是谁在发消息
- 为了后续方便,很多
nc实现会在这时调用connect()函数在这个 UDP Socket 上绑定客户端的地址(这叫 UDP 伪连接,不产生网络流量,只在系统内核中记录对端地址) - 随后,
nc发现自己的内存缓冲区里还有之前用户敲下的、没发出去的文字!于是,它立刻把这些缓冲的数据打包,通过sendto()发送给刚刚获取到的客户端地址。
宏观视角: 服务端收到了消息,同时客户端屏幕上突然弹出了服务端很久之前发出的消息。
后续双向通信畅通无阻
UDP的connect()系统调用
1 | // 客户端执行 ncat -u 192.168.8.159 62334 时 |
- UDP的
connect()不进行网络握手 - 只是在内核设置默认目标地址
- 此后这个套接字只能接收来自该地址的数据包
UDP问题排查
UDP 是无连接协议,确实没有 TCP 那样的 “连接状态” 概念(如 ESTABLISHED / TIME_WAIT),但我们可以通过以下方式来监控流量、排查问题
抓包分析
Wireshark (图形化,推荐)
tcpdump 命令行,Linux/macOS)
UDP打洞
TCP开发相关
TCP实时断线检测
TCP 是面向连接的协议,但它本身没有内置的 “实时断线检测” 机制
当网线物理拔除时,两端并不会立刻感知(因为没有任何数据包能传递 “断开” 信号);
如果此时程序正阻塞在
Read()调用上,底层 TCP 栈会一直等待数据,不会主动中断 —— 就会 “永久阻塞”无论用
C#、Java、C++、Python等任何语言操作 TCP 连接,只有显式设置读写超时,才能打破这种永久阻塞:- C#:
NetworkStream.ReadTimeout/WriteTimeout本质是封装了 Windows/Linux 系统的setsockopt(SO_RCVTIMEO/SO_SNDTIMEO)系统调用; - Java:
Socket.setSoTimeout()同样是设置SO_RCVTIMEO/SO_SNDTIMEO; - C++:直接调用
setsockopt设置接收 / 发送超时; - Python:
socket.settimeout()也是对底层超时参数的封装。
没有超时的情况下,所有语言的 TCP
Read()都会一直阻塞,直到:- 对方主动发送 FIN/RST 包(正常断开)
- 操作系统的 TCP 保活机制(TCP Keep-Alive)检测到连接失效(默认超时很长,通常分钟级)
- 手动设置的读写超时到期
- C#:
内核假死
可以执行的做法
防卡死探测 (DataAvailable): 在真正调用可能死锁的 stream.Read 前,程序会先用 stream.DataAvailable 试探。只有当缓冲区真的有数据时才会去取。如果没有数据,我们采用 await Task.Delay(20) 秒级放弃时间片。从此以后 Read 函数再也不可能卡死!
不指望系统叫醒我,我自己拿秒表计!只要网卡敢 20 毫秒没有新数据,我就不往它那个可能卡死的底层 Read() 坑里跳,一旦时间到了 1000 毫秒都没数据进账,我自己引爆炸弹(TimeoutException)!”
这就是为什么我们在已有底层超时的双重保险下,仍需要通过 DataAvailable + 自定义计时 的手法来确保程序“宁可错杀、绝不假死”的根本底层原因
另外可以
用独立线程包裹 Read 操作,做 “线程级超时”
内核态卡死无法通过普通超时解决,但可以在用户态通过 “监控线程” 强制终止卡死的读取线程
开启 TCP Keep-Alive(底层链路检测)
TCP Keep-Alive 是操作系统级别的链路检测,能更早感知物理链路断开,减少内核态假死的概率
应用层心跳(终极兜底)
在 TCP 之上加应用层心跳包(比如每 2 秒发送一个小数据包,对方回复确认),如果连续 3 次收不到心跳回复,直接判定连接失效,强制关闭并重建连接 —— 这是不受内核态影响的用户态检测手段,可靠性最高
通过 “独立线程监控 + TCP Keep-Alive + 应用层心跳” 三层机制,可完全规避内核态假死的风险,其中应用层心跳是最可靠的终极兜底手段
域名相关
托管域名
**托管域名(Domain Hosting)**是指将已注册域名的管理权委托给专业的第三方服务商,由其负责域名的解析、维护和技术支持,使用户无需自行处理复杂的DNS配置 ,其核心功能包括:
- 域名解析管理:将域名转换为对应的IP地址,确保用户通过域名访问网站时能正确指向服务器
- DNS记录维护:管理A记录(指向IP地址)、MX记录(邮件服务器)、CNAME记录(别名)等,支持灵活配置
- 安全与稳定性保障:通过DNSSEC(DNS安全扩展)、DDoS防护等措施防止域名劫持或攻击,确保服务高可用
- 自动化服务:提供域名续费提醒、备份恢复等,减少人工操作成本
NS记录的定义与作用
NS(Name Server)记录是DNS系统中的一种资源记录,用于指定负责解析域名的权威DNS服务器 ,其核心功能包括:
- 授权解析权:明确域名由哪些DNS服务器管理(如
ns1.cloudflare.com),指导用户请求的查询路径- 冗余与负载均衡:支持配置多个NS记录,当主服务器故障时,备用服务器可接管,保障解析不中断
- 子域名管理:若子域名需独立解析(如
blog.example.com),可通过NS记录指向其他DNS服务商示例:若域名使用Cloudflare的DNS服务,则需在注册商处设置NS记录为
ns1.cloudflare.com和ns2.cloudflare.com,此后所有DNS查询由Cloudflare处理
DNSChecker
全球DNS解析检测与诊断工具,主要用于验证域名解析记录的全球传播状态、检查DNS配置准确性,并提供多种网络诊断功能。以下是其核心功能与使用场景的详细解析
免费且跨平台
授权机制
OAuth2.0
client_id和client_secret可以获得
重定向url: 授权服务器的地址
基本知识
在 OAuth 出现之前,互联网应用普遍采用“客户端-服务器”认证模型。如果用户希望让第三方应用(例如一个照片打印服务)访问其存储在 Google Photos 上的照片,用户通常需要将 Google 的账号和密码直接提供给第三方应用
RFC 6749 定义的 OAuth 2.0 框架通过引入“授权层”和“访问令牌”的概念,彻底解决了上述问题。它将客户端与资源所有者分离,使得第三方应用只能在用户许可的范围内(Scope)、在有限的时间内(Expiration)访问特定的资源 。
角色
OAuth 2.0 定义了四个关键角色,理解它们之间的互动是架构设计的基础 :
- 资源所有者 (Resource Owner): 通常指能够授予受保护资源访问权限的终端用户。在协议交互中,他们的主要动作是“同意”或“拒绝”授权请求。虽然在某些模式(如客户端凭证模式)中不存在人类用户,但从逻辑上讲,所有权的概念依然存在。
- 资源服务器 (Resource Server): 托管受保护资源的服务器,通常表现为 RESTful API。它不负责处理用户的登录或授权逻辑,只负责验证请求中携带的访问令牌(Access Token),并根据令牌的权限范围(Scope)返回资源或拒绝访问 。
- 客户端 (Client): 代表资源所有者请求访问受保护资源的应用程序。这里需要特别注意,“客户端”是一个相对概念,它不仅指前端浏览器应用(SPA),也包括后端 Web 应用、移动 App 甚至服务器守护进程。客户端的核心特征是它持有用于访问资源的凭证(令牌),但不持有资源本身 。
- 授权服务器 (Authorization Server): 这是 OAuth 架构的大脑。它负责验证资源所有者的身份(认证),获取其授权同意,并最终向客户端颁发访问令牌。在许多现代架构中(如使用 Auth0、Okta 或 Keycloak),授权服务器是独立于资源服务器的组件,但在小型系统中,它们可能部署在同一应用内 。
许可类型
OAuth 2.0 的灵活性体现在它支持多种“许可类型”(Grant Types),每种类型对应不同的应用场景和信任模型。
隐式模式 (Implicit Grant) —— 已被废弃
资源所有者密码凭证模式 (ROPC) —— 强烈不推荐
ROPC 模式允许客户端直接收集用户的用户名和密码,并发送给授权服务器换取令牌
授权码模式 (Authorization Code Grant)
这是功能最完整、安全性最高、适用范围最广的流程,主要用于服务器端 Web 应用(Confidential Clients)。
流程详解:
- 启动:客户端将用户引导至授权服务器的
/authorize端点,携带response_type=code。 - 认证:用户在授权服务器登录,并批准客户端的请求。
- 颁发代码:授权服务器将用户重定向回客户端的
redirect_uri,并在查询参数中附带一个临时的code(授权码)。 - 交换令牌:客户端后端收到
code后,通过后端通道向/token端点发起 POST 请求,使用code加上自身的client_secret换取access_token。
核心优势:
- 令牌隔离:访问令牌直接从授权服务器传输到客户端后端,从未暴露在浏览器或用户代理中,极大降低了令牌泄露的风险 。
- 客户端认证:在交换令牌步骤,客户端必须出示密钥(Client Secret),确保只有合法的客户端才能获得令牌。
客户端凭证模式 (Client Credentials Grant)
此模式用于机器对机器(M2M)通信,不涉及用户上下文。
流程: 客户端向令牌端点发送 grant_type=client_credentials 和 Client ID/Secret,直接获取令牌。
用途: 用于后台服务执行系统级任务(如每日数据同步),或者微服务之间的相互调用。此时,令牌代表的是“应用本身”的权限,而非“用户”的权限 。
攻防流程图参考
上述传统的授权码模式在这里存在漏洞,流程会被攻击,攻击流程流程如下:
这里最大的问题是客户端根本就不知道授权码是属于谁的,因此可以被浏览器插件获取到该授权码去暗渡陈仓
为了解决这个问题,RFC 7636 引入了 PKCE (Proof Key for Code Exchange,发音为 “pixy”) 机制 : 把授权码和各个用户对应起来,这样攻击者就没法拿受害者的授权码直接去使用
PKCE机制: 会在用户访问重定向url之前生成一次性的PKCE字符串,每个用户请求的PKCE字符串都不一样,并且会将这个PKCE字符串哈希计算成另一串字符串
- 客户端和浏览器之间的保护通过state来进行保护,防止攻击者先走一遍流程拿到自己的授权码,再利用授权码做事
- PKCE 通过在授权请求和令牌请求之间建立加密绑定,防止了授权码注入攻击
CSRF 攻击与 State 参数
如果攻击者构造一个恶意的重定向 URL(包含攻击者自己的授权码),并诱导受害者点击。受害者的客户端可能会使用攻击者的授权码换取令牌,导致受害者不知不觉地登录了攻击者的账号。这被称为登录 CSRF (Login CSRF)。
防御: 客户端在发起授权请求时,生成一个随机的、不可预测的
state参数,并将其存储在本地(如 HttpOnly Cookie)。授权服务器在回调时原样返回该参数。客户端必须验证返回的state与存储的值是否完全一致 。这里的state如果以Cookie的形式保存在浏览器,必须要用Secure和Http Only
1 Set-Cookie: sessionid=abc123; Path=/; HttpOnly; Secure; SameSite=Strict
sessionid=abc123:Cookie 的键值对(会话 ID)Path=/:该 Cookie 对网站所有路径生效HttpOnly:针对 XSS 攻击:禁止 JS 访问 Cookie,避免脚本窃取登录凭证等敏感信息Secure:针对 明文传输风险:仅允许 Cookie 在 HTTPS 连接中传输,防止 HTTP 明文被中间人拦截SameSite=Strict(额外补充):防跨站请求伪造 (CSRF),是现代浏览器推荐的补充属性
更近一步
混合攻击 (Mix-up Attack) 与 RFC 9207
这是一类针对多身份提供商(IdP)环境的复杂攻击,极具隐蔽性。
攻击场景: 假设一个客户端支持“Google 登录”和“Malicious IdP 登录”。
- 诱导:攻击者诱导受害者点击“使用 Malicious IdP 登录”。
- 篡改:在重定向过程中,攻击者(作为 Malicious IdP)将受害者的浏览器重定向到 Google 的授权端点,而不是 Malicious IdP 的端点。
- 授权:受害者在 Google 页面看到客户端请求权限,并未起疑(因为客户端是合法的),于是授权。
- 混淆:Google 返回授权码给客户端。
- 泄露:客户端误以为这个流程是针对 Malicious IdP 的(因为这是它发起的请求),于是将这个 Google 的授权码发送到了 Malicious IdP 的令牌端点(攻击者控制)。
- 利用:攻击者截获 Google 授权码,并在自己的服务器上换取受害者的 Google 令牌 。
防御机制:RFC 9207 (Issuer Identification) OAuth 2.0 新增了
iss响应参数。
- 当授权服务器返回授权码时,必须同时返回
iss参数,值为授权服务器的标识符(Issuer URL)。- 客户端在收到回调时,必须校验返回的
iss是否与它最初打算发送请求的 IdP 一致。- 在上例中,客户端预期的是 Malicious IdP,但收到的
iss是 Google。由于不匹配,客户端将中止流程,从而挫败攻击 。
引伸到数据库模式设计建议
用户表 (Users):
mfa_secret: 切勿明文存储。应使用可逆加密算法(如 AES-GCM)存储,密钥由 Key Management Service (KMS) 管理。password_hash: 使用 Argon2id 或 BCrypt 存储。
OAuth 客户端表 (OAuthClients):
client_secret: 必须像密码一样哈希存储。这意味着即使是数据库管理员也无法恢复原始 Secret。redirect_uris: 存储为严格匹配的白名单列表,禁止使用通配符(如https://*.example.com),以防子域名劫持导致的 Token 泄露。
令牌表 (Tokens):
- 如果是使用 Reference Token,则需要存储令牌数据。
- 建议存储令牌的哈希值(如 SHA-256)作为索引,而非令牌本身。这样即使数据库泄露,攻击者也无法利用这些令牌访问 API。
后端架构模式:BFF (Backend for Frontend)
对于 SPA 应用,当前最推荐的架构并非直接在浏览器中处理 OAuth 令牌,而是采用 BFF 模式。
架构逻辑:
- SPA 不直接与授权服务器通信。
- SPA 所有的请求都发给同域下的轻量级后端(BFF)。
- BFF 负责与授权服务器进行 OAuth 交互(交换 Code、刷新 Token)。
- BFF 获取 Access Token 后,不将其传给浏览器,而是创建一个加密的、HttpOnly、SameSite=Strict 的 Session Cookie 发送给浏览器。
- 浏览器使用 Cookie 访问 BFF,BFF 在服务端解密 Cookie,取出 Access Token,代理请求到下游的资源服务器。
优势: 这种架构将令牌完全移出了浏览器的上下文,彻底杜绝了 XSS 攻击导致令牌被盗的可能性,是目前浏览器应用安全的最高标准 。
手动获取授访问码
以微软邮箱授权为例
code通过浏览器访问授权链接→登录授权→从地址栏提取;- 用 Python 脚本将
code兑换为access_token和refresh_token; - 长期使用靠
refresh_token刷新access_token,无需重复登录; - 最终用
access_token调用 Graph API 读取邮件。
获取授权码
浏览器打开链接完成授权
1 | https://login.microsoftonline.com/common/oauth2/v2.0/authorize? |
页面跳转到微软登录页,正常登录,将会弹出授权弹窗:“是否允许【你的应用名】访问你的邮件?”,点击「接受」;
浏览器会跳转到http://localhost(这是自己配置的重定向URI)(会显示 “无法访问”,但没关系)
此时可以看到浏览器地址栏的URL为:
1 | http://localhost?code=AAABAAAAvPM1KaPlrEqdFSBzjqfTGBCmLdgfSTLE7890&session_state=123456 |
找到?code=后面、&前面的一串字符,就是code,即授权码(示例:AAABAAAAvPM1KaPlrEqdFSBzjqfTGBCmLdgfSTLE7890);
注意:code有效期只有 5 分钟,且只能用一次,复制后立刻去第二步
用 code 兑换 token
新建一个get_token.py文件,复制下面代码,替换其中的你的client_id、你的client_secret、你的code:
1 | import requests |
运行脚本,获取 token
- 确保电脑安装了 Python(3.7+)和 requests 库(没装的话执行
pip install requests); - 打开命令行,执行
python get_token.py; - 运行成功后,控制台会输出:
access_token:短期访问令牌(调用 API 读邮件用);refresh_token:M.C509 开头的长期刷新令牌(核心!保存好);
用 refresh_token 刷新 access_token(长期使用)
当
access_token过期(1 小时后),不用重新获取 code,直接用refresh_token刷新,脚本如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 import requests
# ---------------------- 替换这里的参数 ----------------------
CLIENT_ID = "你的Azure应用client_id"
CLIENT_SECRET = "你的Azure应用客户端密码"
REFRESH_TOKEN = "第二步拿到的refresh_token" # M.C509开头的字符串
# -----------------------------------------------------------
TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
data = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"refresh_token": REFRESH_TOKEN,
"grant_type": "refresh_token",
"scope": "Mail.Read offline_access"
}
response = requests.post(TOKEN_URL, data=data)
new_tokens = response.json()
print("===== 新令牌 =====")
print(f"新access_token:\n{new_tokens['access_token']}\n")
print(f"新refresh_token(可选更新):\n{new_tokens.get('refresh_token', '无')}")用 access_token 读取 Outlook 邮件(验证效果)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 import requests
ACCESS_TOKEN = "第二步/第三步拿到的access_token"
# 读取收件箱前10封邮件
MAIL_URL = "https://graph.microsoft.com/v1.0/me/messages?$top=10"
headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
response = requests.get(MAIL_URL, headers=headers)
mails = response.json()
# 打印邮件标题和发件人
print("===== 你的邮件 =====")
for mail in mails.get("value", []):
print(f"标题:{mail['subject']}")
print(f"发件人:{mail['from']['emailAddress']['address']}\n")
核心注意事项
code只能用一次,超时 / 重复使用都会报错,需重新获取;refresh_token要妥善保存,泄露后别人可能访问你的邮箱,可在 Azure 里随时吊销;- 所有参数(client_id、redirect_uri 等)必须和 Azure 配置完全一致,否则会提示 “重定向 URI 不匹配”“权限不足”。
2FA
双因素认证的核心在于生成一个短期有效、不可预测的一次性密码(OTP)。在现代开发中,基于哈希的消息认证码(HMAC)是构建 OTP 的数学基础
HOTP:基于计数器的 OTP (RFC 4226)
HOTP 是 OTP 的基础算法,最初为硬件令牌设计。
其核心公式为:
$$\text{HOTP}(K, C) = \text{Truncate}(\text{HMAC-SHA-1}(K, C))$$
- $K$: 共享密钥(Secret Key)。
- $C$: 移动因子(Moving Factor),即计数器。
同步问题: HOTP 依赖于客户端和服务器端的计数器 $C$ 保持一致。每生成一次验证码,客户端 $C$ 加 1;每验证一次,服务器 $C$ 加 1。如果用户按下了生成按钮但没有登录,客户端的 $C$ 就会超前。服务器必须实现一个“前瞻窗口”(Look-ahead Window),尝试验证 $C+1, C+2…C+s$ 的值,以恢复同步 。
TOTP:基于时间的 OTP (RFC 6238)
Time-based One-Time Password 即TOTP算法. 原理就是服务器给了一个密钥(二维码里面就是这个密钥), 然后根据当前时间戳, 两者结合生成了一个临时密钥, 服务器也用同样的算法得到这个临时密钥, 匹配成功就通过(因为时间误差的关系实际上服务器会运算多个密钥, 匹配其中的一个就算过了)TOTP 是 HOTP 的时间变体,解决了计数器同步的难题,是目前 Google Authenticator、Microsoft Authenticator 等软件令牌的标准。
算法步骤详解 :
时间步长计算:
定义时间步长 $X$(默认为 30 秒)。计算当前的时间计数器 $T$:
$$T = \lfloor \frac{\text{CurrentUnixTime} - T0}{X} \rfloor$$
其中 $T0$ 通常为 0。这意味着 $T$ 是一个每 30 秒递增 1 的整数。
数据打包 (关键步骤): 在进行 HMAC 运算前,必须将整数 $T$ 转换为字节流。RFC 6238 规定,$T$ 必须被编码为 8 字节的大端序 (Big-Endian) 字节数组。 在 Python 中,这对应
struct.pack(">Q", T);在 Java 中,需要通过ByteBuffer或位移操作实现。如果这一步出错(如使用了小端序或字符串形式),生成的验证码将完全错误 。HMAC 计算:
$$\text{HS} = \text{HMAC-SHA-1}(K, T_{\text{bytes}})$$
结果
HS是一个 20 字节(160 位)的哈希值。虽然 RFC 允许使用 SHA-256 或 SHA-512,但大多数验证器应用(如 Google Authenticator)默认仅支持 SHA-1 。动态截断 (Dynamic Truncation):
为了将 160 位的哈希值转换为 6 位数字,需要进行截断操作。
- 取偏移量:取
HS的最后一个字节的低 4 位:offset = HS & 0x0F。 - 取 4 字节:从
HS中提取从offset开始的 4 个字节:P = HS[offset]...HS[offset+3]。 - 去符号位:将
P视为大端整数,并屏蔽最高位(符号位),因为不同语言对有符号/无符号整数的处理不同,去符号位能保证兼容性:Binary = P & 0x7FFFFFFF。 - 取模:
OTP = Binary mod 10^6。
- 取偏移量:取
时钟漂移 (Clock Drift): 由于网络延迟或设备时钟不同步,服务器通常会验证 $T$ 以及相邻的时间步(如 $T-1$ 和 $T+1$),这提供了 ±30 秒的容错窗口 。
2FA 的实时钓鱼 (Real-time Phishing Proxy)
传统的 TOTP 2FA 容易受到中间人攻击(MitM)。工具如 Modlishka 或 Evilginx2 可以建立一个反向代理站点,实时转发用户输入的用户名、密码以及 TOTP 验证码到真实网站。一旦真实网站验证通过并返回 Session Cookie,代理站点会截获该 Cookie,从而劫持会话 。
防御: 这是 TOTP 协议层面的局限。唯一彻底的防御是采用 FIDO2 / WebAuthn(如 Passkeys)。FIDO 协议在硬件底层绑定了域名(Origin Binding)。由于钓鱼网站的域名与真实网站不同,浏览器和认证器会拒绝生成签名,从根本上阻断了此类攻击 。




























































