技术 网络 网络编程 ZEROKO14 2024-07-16 2025-04-28 网络基础
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地址 .
交换机对数据帧的转发与过滤:
交换机原理详解跳转
路由表(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的应用] 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 00 05 5 d 67 d0 b1 00 05 5 d 61 58 a8 08 00 45 00 00 53 93 25 00 00 80 11 25 ec c0 a8 00 37 c0 a800 01 05 d4 00 45 00 3f ac 40 00 01 'c' ':' '\'' q' 'w' 'e' 'r' 'q' '.' 'q' 'w' 'e' 00 'n' 'e' 't' 'a' 's' 'c' 'i' 'i' 00 'b' 'l' 'k' 's' 'i' 'z' 'e' 00 '5' '1' '2' 00 't' 'i' 'm' 'e' 'o' 'u' 't' 00 '1' '0' 00 't' 's' 'i' 'z' 'e' 00 '0'
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 2 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 2 3 4 5 6 #include <arpa/inet.h> uint32_t htonl (uint32_t hostlong) ; uint16_t htons (uint16_t hostshort) ; uint32_t ntohl (uint32_t netlong) ; uint16_t ntohs (uint16_t netshort) ;
上述的几个函数, 如果本来不需要转换函数内部就不会做转换.
同步与异步
同步 : 发送一个请求,等待返回,然后再发送下一个请求
异步 : 发送一个请求,不等待返回,随时可以再发送下一个请求
同步可以**避免出现死锁 **,读脏数据的发生,一般共享某一资源的时候用,如果每个人都有修改权限,同时修改一个文件,有可能使一个人读取另一个人已经删除的内容,就会出错,同步就会按顺序来修改。
异步则是可以提高效率 了,现在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.txt
telnet://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:// or ftp://
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct sockaddr { sa_family_t sa_family; char sa_data[14 ]; } struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr ; }; struct in_addr { uint32_t s_addr; };
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 2 3 4 5 6 struct sockaddr_in serv ;serv.sin_family = AF_INET; serv.sin_port = htons(8888 ); inet_pton(AF_INET, "127.0.0.1" , &serv.sin_addr.s_addr);
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 2 3 4 5 6 7 ssize_t read (int fd, void *buf, size_t count) ;ssize_t write (int fd, const void *buf, size_t count) ;ssize_t recv (int sockfd, void *buf, size_t len, int flags) ;ssize_t send (int sockfd, const void *buf, size_t len, int flags) ;
注意: 如果写缓冲区已满, write也会阻塞 , read读操作的时候, 若读缓冲区没有数据会引起阻塞 .
在进行 socket 编程中,send 函数用于向连接的另一端发送数据。当调用 send 函数后,数据并不会立即发送出去,而需要等待网络连接稳定,缓冲区可用等条件满足后才会实际发送数据。这时候,如果出现网络故障或者对方出现延迟,就可能导致 send 发送数据失败,并且可能陷入阻塞状态,导致程序无法继续执行。
因此,为了避免 send 函数陷入阻塞状态,我们通常使用 select 函数设置发送数据超时时间,如果在规定时间内没有发送成功,则认为发送失败并中止发送。这其实也是一种优化策略,如果继续等待缓冲区可用或者网络连接稳定,可能会导致程序长时间无响应,用户体验也会变得很差。
另外,通过使用 select 函数进行超时检测,可以减少 CPU 的使用率,避免空等待占用过多的系统资源,提高程序的并发性能和稳定性。
总之,使用 select 对 send 函数进行超时检测,是一种优化策略,可以避免 send 函数陷入阻塞状态,提高程序的响应速度和稳定性。
read函数同理,同样需要超时检测优化
确保write和read发送数据完全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 int TcpSocket::writen (const void *buf, int count) { size_t nleft = count; ssize_t nwritten; char *bufp = (char *)buf; while (nleft > 0 ) { if ((nwritten = write (m_socket, bufp, nleft)) < 0 ) { if (errno == EINTR) continue ; return -1 ; } else if (nwritten == 0 ) continue ; bufp += nwritten; nleft -= nwritten; } return count; } int TcpSocket::readn (void *buf, int count) { size_t nleft = count; ssize_t nread; char *bufp = (char *)buf; while (nleft > 0 ) { if ((nread = read (m_socket, bufp, nleft)) < 0 ) { if (errno == EINTR) continue ; return -1 ; } else if (nread == 0 ) return count - nleft; bufp += nread; nleft -= nread; } return count; }
测试工具 nc命令以客户端的方式连接小工具
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命令其他功能参考
netstat命令测试过程中可以使用netstat命令查看监听状态和连接状态 netstat命令:
a表示显示所有,
n表示显示的时候以数字的方式来显示
p表示显示进程信息(进程名和进程PID)
一般用 netstat -anp | grep 端口号来查看该端口对应的连接情况
netstat -tunlp命令查看网络连接情况,仅服务器
ESTABLISHED表示链接建立了
mac下netstat命令为简化版,用lsof命令取而代之
案例 服务器开发流程
创建socket,返回一个文件描述符lfd – socket()
– 该文件描述符用于监听客户端连接
将lfd和IP PORT进行绑定 – bind()
将lfd由主动变为被动监听 – listen()
接受一个新的连接,得到一个文件描述符 – accept()
– 该文件描述符是用于和客户端进行通信的
读写
关闭文件描述符 – close(lfd) close(cfd)
客户端开发流程
创建socket,返回一个文件描述符cfd – socket()
—该文件描述符是用于和服务端通信
连接服务器 –connect()
读写
关闭文件描述符 – close(fd)
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
linux/Mac代码 服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <arpa/inet.h> #include <sys/socket.h> #include <iostream> #include <unistd.h> #include <string.h> #include <sys/types.h> using namespace std ; int main (int argc,char ** argv) { int lfd = socket(AF_INET,SOCK_STREAM,0 ); std ::cout <<"lfd:" <<lfd<<std ::endl ; struct sockaddr_in serv ; bzero(&serv,sizeof (serv)); serv.sin_family = AF_INET; serv.sin_port = htons(23456 ); serv.sin_addr.s_addr = htonl(INADDR_ANY); bind(lfd,(sockaddr*)&serv,sizeof (serv)); listen(lfd,2 ); std ::cout <<"等待客户端链接..." <<std ::endl ; int fd =accept(lfd,NULL ,NULL ); std ::cout <<"客户端已链接" <<std ::endl ; char buf[1024 ]; while (1 ) { memset (buf,0 ,sizeof (buf)); int readSize = read(fd,buf,sizeof (buf)); std ::cout <<"readSize:" <<readSize<<std ::endl ; std ::cout <<"buf:" <<buf<<std ::endl ; } close(lfd); close(fd); return 0 ; }
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> using namespace std ; #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET,SOCK_STREAM,0 ); cout <<"fd:" <<fd<<endl ; struct sockaddr_in serv ; bzero(&serv,sizeof (serv)); serv.sin_family = AF_INET; serv.sin_port = htons(23456 ); inet_pton(AF_INET, "192.168.0.115" , &serv.sin_addr.s_addr); int iRet = connect(fd,(sockaddr*)&serv,sizeof (serv)); cout <<"connect iRet:" <<iRet<<endl ; char buf[999 ]={0 }; send(fd,"hello world!" ,strlen ("hello world!" )+1 ,0 ); while (1 ) { sleep(1 ); } close(fd); return 0 ; }
代码上和linux有一定差异,但差异基本不大
服务器
加载套接字库,创建套接字(WSAStartup()/socket())
绑定套接字到一个IP地址和一个端口上(bind())
将套接字设置为监听模式等待连接请求(listen())
请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
用返回的套接字和客户端进行通信(send()/recv())
返回,等待另一个连接请求;
关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include <stdio.h> #include <winsock2.h> #pragma comment(lib,"ws2_32.lib" ) int main (int argc, char * argv[]) { WORD sockVersion = MAKEWORD(2 , 2 ); WSADATA wsaData; if (WSAStartup(sockVersion, &wsaData) != 0 ) { return 0 ; } SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (slisten == INVALID_SOCKET) { printf ("socket error !" ); return 0 ; } sockaddr_in sin ; sin .sin_family = AF_INET; sin .sin_port = htons(23456 ); sin .sin_addr.S_un.S_addr = INADDR_ANY; if (bind(slisten, (LPSOCKADDR)&sin , sizeof (sin )) == SOCKET_ERROR) { printf ("bind error !" ); } if (listen(slisten, 5 ) == SOCKET_ERROR) { printf ("listen error !" ); return 0 ; } SOCKET sClient; sockaddr_in remoteAddr; int nAddrlen = sizeof (remoteAddr); char revData[255 ]; while (true ) { printf ("等待连接...\n" ); sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen); if (sClient == INVALID_SOCKET) { printf ("accept error !" ); continue ; } printf ("接受到一个连接:%s \r\n" , inet_ntoa(remoteAddr.sin_addr)); int ret = recv(sClient, revData, 255 , 0 ); if (ret > 0 ) { revData[ret] = 0x00 ; printf (revData); } const char * sendData = "你好,TCP客户端!\n" ; send(sClient, sendData, strlen (sendData), 0 ); closesocket(sClient); } closesocket(slisten); WSACleanup(); return 0 ; }
客户端
加载套接字库,创建套接字(WSAStartup()/socket())
向服务器发出连接请求(connect())
和服务器进行通信(send()/recv())
关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <WINSOCK2.H> #include <STDIO.H> #include <iostream> #include <string> using namespace std ; #pragma comment(lib, "ws2_32.lib" ) int main () { WORD sockVersion = MAKEWORD(2 , 2 ); WSADATA data; if (WSAStartup(sockVersion, &data) != 0 ) { return 0 ; } while (true ) { SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sclient == INVALID_SOCKET) { cout << WSAGetLastError() << endl ; printf ("invalid socket!" ); return 0 ; } sockaddr_in serAddr; serAddr.sin_family = AF_INET; serAddr.sin_port = htons(23456 ); serAddr.sin_addr.S_un.S_addr = inet_addr("192.168.0.115" ); if (connect(sclient, (sockaddr *)&serAddr, sizeof (serAddr)) == SOCKET_ERROR) { cout << WSAGetLastError() << endl ; printf ("connect error !" ); closesocket(sclient); getchar(); return 0 ; } string data; cin >> data; const char * sendData; sendData = data.c_str(); send(sclient, sendData, strlen (sendData), 0 ); char recData[255 ]; int ret = recv(sclient, recData, 255 , 0 ); if (ret > 0 ) { recData[ret] = 0x00 ; printf (recData); } closesocket(sclient); } WSACleanup(); return 0 ; }
网络开发的注意点及完整案例 当read读文件描述符为非阻塞状态的时候,若对方没有发送数据,会立刻返回, errno设置为 EAGAIN,这个错误我们要忽略.
防止阻塞被信号打断 像accept,read, write 这样的能够引起阻塞的函数,若被信号打断,由于信号的优先级较高, 会优先处理信号, 信号处理完成后,会使accept或者read解除阻塞, 然后返回, 此时返回值为 -1,设置errno=EINTR;errno=ECONNABORTED表示连接被打断,异常.
阻塞函数在阻塞期间若收到信号,会被信号中断,errno设置为EINTR,这个错误不应该被视为错误.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <strings.h> void perr_exit (const char *s) { perror(s); exit (-1 ); } int Accept (int fd, struct sockaddr *sa, socklen_t *salenptr) { int n; again: if ((n = accept(fd, sa, salenptr)) < 0 ) { if ((errno == ECONNABORTED) || (errno == EINTR)) goto again; else perr_exit("accept error" ); } return n; } int Bind (int fd, const struct sockaddr *sa, socklen_t salen) { int n; if ((n = bind(fd, sa, salen)) < 0 ) perr_exit("bind error" ); return n; } int Connect (int fd, const struct sockaddr *sa, socklen_t salen) { int n; if ((n = connect(fd, sa, salen)) < 0 ) perr_exit("connect error" ); return n; } int Listen (int fd, int backlog) { int n; if ((n = listen(fd, backlog)) < 0 ) perr_exit("listen error" ); return n; } int Socket (int family, int type, int protocol) { int n; if ((n = socket(family, type, protocol)) < 0 ) perr_exit("socket error" ); return n; } ssize_t Read (int fd, void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = read(fd, ptr, nbytes)) == -1 ) { if (errno == EINTR) goto again; else return -1 ; } return n; } ssize_t Write (int fd, const void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = write(fd, ptr, nbytes)) == -1 ) { if (errno == EINTR) goto again; else return -1 ; } return n; } int Close (int fd) { int n; if ((n = close(fd)) == -1 ) perr_exit("close error" ); return n; } ssize_t Readn (int fd, void *vptr, size_t n) { size_t nleft; ssize_t nread; char *ptr; ptr = (char *)vptr; nleft = n; while (nleft > 0 ) { if ((nread = read(fd, ptr, nleft)) < 0 ) { if (errno == EINTR) nread = 0 ; else return -1 ; } else if (nread == 0 ) break ; nleft -= nread; ptr += nread; } return n - nleft; } ssize_t Writen (int fd, const void *vptr, size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = (const char *)vptr; nleft = n; while (nleft > 0 ) { if ( (nwritten = write(fd, ptr, nleft)) <= 0 ) { if (nwritten < 0 && errno == EINTR) nwritten = 0 ; else return -1 ; } nleft -= nwritten; ptr += nwritten; } return n; } static ssize_t my_read (int fd, char *ptr) { static int read_cnt; static char *read_ptr; static char read_buf[100 ]; if (read_cnt <= 0 ) { again: if ( (read_cnt = read(fd, read_buf, sizeof (read_buf))) < 0 ) { if (errno == EINTR) goto again; return -1 ; } else if (read_cnt == 0 ) return 0 ; read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return 1 ; } ssize_t Readline (int fd, void *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = (char *)vptr; for (n = 1 ; n < maxlen; n++) { if ( (rc = my_read(fd, &c)) == 1 ) { *ptr++ = c; if (c == '\n' ) break ; } else if (rc == 0 ) { *ptr = 0 ; return n - 1 ; } else return -1 ; } *ptr = 0 ; return n; } int tcp4bind (short port,const char *IP) { struct sockaddr_in serv_addr ; int lfd = Socket(AF_INET,SOCK_STREAM,0 ); bzero(&serv_addr,sizeof (serv_addr)); if (IP == NULL ){ serv_addr.sin_addr.s_addr = INADDR_ANY; }else { if (inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0 ){ perror(IP); exit (1 ); } } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(port); Bind(lfd,(struct sockaddr *)&serv_addr,sizeof (serv_addr)); return lfd; }
头文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #ifndef __WRAP_H_ #define __WRAP_H_ #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <strings.h> void perr_exit (const char *s) ;int Accept (int fd, struct sockaddr *sa, socklen_t *salenptr) ;int Bind (int fd, const struct sockaddr *sa, socklen_t salen) ;int Connect (int fd, const struct sockaddr *sa, socklen_t salen) ;int Listen (int fd, int backlog) ;int Socket (int family, int type, int protocol) ;ssize_t Read (int fd, void *ptr, size_t nbytes) ;ssize_t Write (int fd, const void *ptr, size_t nbytes) ;int Close (int fd) ;ssize_t Readn (int fd, void *vptr, size_t n) ;ssize_t Writen (int fd, const void *vptr, size_t n) ;ssize_t my_read (int fd, char *ptr) ;ssize_t Readline (int fd, void *vptr, size_t maxlen) ;int tcp4bind (short port,const char *IP) ;#endif
粘包 接收缓冲区中,对方发送数据连续发了两次,然后读数据的时候第一次没有读完,剩余的数据在第二次读走了,这种情况就属于粘包.
粘包: 多次数据发送, 收尾相连, 接收端接收的时候不能正确区分第一次发送多少, 第二次发送多少.
本质上是同一个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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 void setfdUnblock (int fd) { int flag = fcntl(fd, F_GETFL, 0 ); flag|=O_NONBLOCK; fcntl(fd, F_SETFL, flag); } int main (int argc,char ** argv) { vector <int > fds; int lfd = socket(AF_INET,SOCK_STREAM,0 ); setfdUnblock(lfd); std ::cout <<"lfd:" <<lfd<<std ::endl ; struct sockaddr_in serv ; bzero(&serv,sizeof (serv)); serv.sin_family = AF_INET; serv.sin_port = htons(23456 ); serv.sin_addr.s_addr = htonl(INADDR_ANY); bind(lfd,(sockaddr*)&serv,sizeof (serv)); listen(lfd,2 ); std ::cout <<"等待客户端链接..." <<std ::endl ; again: int fd =accept(lfd,NULL ,NULL ); if (fd==-1 ){ goto again; } std ::cout <<"客户端已链接:" <<fd<<std ::endl ; if (find(fds.begin(),fds.end(),fd)==fds.end()) { std ::cout <<"准备添加的fd:" <<fd<<std ::endl ; setfdUnblock(fd); fds.push_back(fd); } if (fds.size()<2 ) { goto again; } std ::cout <<fds.size()<<std ::endl ; for (size_t i = 0 ; i < fds.size(); i++) { std ::cout <<fds[i]<<std ::endl ; } while (1 ) { char buf[1024 ]; memset (buf,0 ,sizeof (buf)); int readSize =0 ; for (size_t i = 0 ; i < fds.size(); i++) { readSize = read(fds[i],buf,sizeof (buf)); if (readSize>0 ) { std ::cout <<"readSize:" <<readSize<<std ::endl ; std ::cout <<"buf:" <<buf<<std ::endl ; } } } close(lfd); close(fd); return 0 ; }
此代码实现了接受多个客服端连接并接受客户端发过来的数据的功能.但建立连接的过程必须和读取数据的问题完全分离,即先建立指定连接数目的连接 ,然后再开始通信过程(此后无法继续建立新连接),建立连接和通信是割裂的.开始通信后没有一个好的时机供建立新的连接.如果是客户端通信内容决定建立连接的时机的话就可以用这种方式.(而且也没有好的时机做清理工作)
这种实现方法不是一个好的实现方法
但是其实还有select方法 ,可以解决上述问题
多进程实现 不设置非阻塞状态的话都要考虑防止阻塞被信号打断
注册子进程回收
创建socket,得到一个监听的文件描述符lfd — socket()
将lfd和IP和端口port进行绑定 — bind()
设置监听 — listen()
进入while循环
接受新客户端到来 — accept()
fork子进程收发数据
子进程关闭监听文件描述符后收发数据,最后要设置关闭描述符退出子进程exit(0); —read()/write()
父进程关闭通信文件描述符
下面代码结合了防止阻塞被信号打断 的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 #include <iostream> using namespace std ; #include <math.h> #include <time.h> #include <unistd.h> #include <dirent.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <signal.h> #include <sys/time.h> #include <pthread.h> #include <sys/socket.h> #include <arpa/inet.h> #include "mysocket.h" void sigFunc (int signo) { int iRet=0 ; do { iRet = waitpid(-1 ,NULL ,WNOHANG); if (iRet>0 ) std ::cout <<"[" <<getpid()<<"]主进程回收子进程[" <<iRet<<"]" <<std ::endl ; } while ((iRet!=0 )&&(iRet!=-1 )); if (iRet==-1 ) { } } int main2 () { while (1 ) { char buf[99 ]; gets(buf); cout <<buf<<endl ; cout <<kmpSearch(buf,"kill" )<<endl ; } return 0 ; } void acceptClient2Str (struct sockaddr_in* client,char * str) { char sip[16 ]; memset (sip,0 ,sizeof (sip)); sprintf (str,"%s:%hu" ,inet_ntop(AF_INET,&client->sin_addr.s_addr,sip,sizeof (sip)),ntohs(client->sin_port)); } int main (int argc,char ** argv) { struct sigaction sg ; sigemptyset(&sg.sa_mask); sg.__sigaction_u.__sa_handler=sigFunc; sg.sa_flags = 0 ; int iRet= sigaction(SIGCHLD,&sg,NULL ); if (!iRet) std ::cout <<"[" <<getpid()<<"]主进程回收子进程回调注册成功" <<std ::endl ; int lfd = Socket(AF_INET,SOCK_STREAM,0 ); std ::cout <<"lfd:" <<lfd<<std ::endl ; struct sockaddr_in serv ; bzero(&serv,sizeof (serv)); serv.sin_family = AF_INET; serv.sin_port = htons(23456 ); serv.sin_addr.s_addr = htonl(INADDR_ANY); cout <<Bind(lfd,(sockaddr*)&serv,sizeof (serv))<<endl ; Listen(lfd,2 ); std ::cout <<"等待客户端链接..." <<std ::endl ; struct sockaddr_in client ; socklen_t len; char clientIp[20 ]={0 }; while (1 ) { len = sizeof (client); int fd =Accept(lfd,(struct sockaddr*)&client,&len); int pid = fork(); if (pid<0 ) { exit (-1 ); } else if (pid>0 ) { close(fd); } else if (pid==0 ) { acceptClient2Str(&client,clientIp); std ::cout <<"创建子进程[" <<getpid()<<"]与[" <<clientIp<<"]建立连接" <<std ::endl ; close(lfd); while (1 ) { char buf[1024 ]; memset (buf,0 ,sizeof (buf)); int readSize =0 ; readSize = Read(fd,buf,sizeof (buf)); if (readSize>0 ) { std ::cout <<"============[" <<getpid()<<"]子进程读[" <<clientIp<<"]数据如下:========" <<std ::endl ; std ::cout <<"readSize:" <<readSize<<std ::endl ; std ::cout <<"buf:" <<buf<<std ::endl ; std ::cout <<"===============================================" <<std ::endl ; } else if (readSize==0 ) { std ::cout <<"[" <<clientIp<<"]断开连接" <<std ::endl ; close(fd); exit (0 ); } } close(fd); exit (0 ); } } close(lfd); return 0 ; }
多线程实现 不设置非阻塞状态的话都要考虑防止阻塞被信号打断
注册子进程回收
创建socket,得到一个监听的文件描述符lfd — socket()
将lfd和IP和端口port进行绑定 — bind()
设置监听 — listen()
进入while循环
接受新客户端到来 — accept()
pthread_create创建子线程收发数据
收发数据 —read()/write()
关闭传入的描述符
pthread_detach设置线程为分离属性(退出时自动释放)
[注意] 主线程和子线程共享文件描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include <iostream> using namespace std ; #include <math.h> #include <time.h> #include <unistd.h> #include <dirent.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <signal.h> #include <sys/time.h> #include <pthread.h> #include <sys/socket.h> #include <arpa/inet.h> #include "mysocket.h" #include <iomanip> void acceptClient2Str (struct sockaddr_in* client,char * str) { char sip[16 ]; memset (sip,0 ,sizeof (sip)); sprintf (str,"%s:%hu" ,inet_ntop(AF_INET,&client->sin_addr.s_addr,sip,sizeof (sip)),ntohs(client->sin_port)); } int a=30 ;void * threadFunc (void * fdAddr) { char buf[99 ]={0 }; int fd = (int64_t )fdAddr; while (1 ) { int nRet = read(fd,buf,sizeof (buf)); if (nRet==0 ) { std ::cout <<"线程id[" <<pthread_self()<<"]退出连接" <<std ::endl ; close(fd); break ; } else if (nRet>0 ) { std ::cout <<"读到的数据为:" <<buf<<" 长度:" <<nRet<<std ::endl ; } } } int main (int argc,char ** argv) { int cfd = Socket(AF_INET,SOCK_STREAM,0 ); struct sockaddr_in serv ; serv.sin_family=AF_INET; serv.sin_port=htons(23456 ); inet_pton(AF_INET,"127.0.0.1" ,&serv.sin_addr.s_addr); Bind(cfd,(const sockaddr*)&serv,sizeof (serv)); Listen(cfd,2 ); pthread_t threadID; std ::cout <<"等待接受连接..." <<std ::endl ; char clientIp[20 ]; while (1 ) { sockaddr_in client; socklen_t len=sizeof (client); int fd = Accept(cfd,(sockaddr*)&client,&len); acceptClient2Str(&client,clientIp); pthread_create(&threadID,NULL ,threadFunc,(void *)fd); pthread_detach(threadID); std ::cout <<"接收到[" <<clientIp<<"]连接,创建线程[0x" ; cout << setbase(16 ) << (int64_t )threadID<<"]处理" <<endl ; } return 0 ; }
半关闭状态 如果一方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 2 3 4 5 6 7 8 9 setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (int )); int opt = 1 ;setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (int ));
函数说明可参看<<UNIX环境高级编程>>
由于错误是bind函数报出来的, 该函数调用要放在bind之前, socket之后调用.
getsockopt函数 函数用于获取任意类型、任意状态套接口 的选项当前值,并把结果存入option_value。
1 2 3 4 5 6 int getsockopt (int socket, int level, int option_name, void *restrict option_value, socklen_t *restrict optio n_len) ;
成功,它将返回0,表示获取选项值成功。
失败,它将返回-1,并设置errno变量来指示错误原因
在使用非阻塞Socket时,我们通常会使用getsockopt函数和SO_ERROR选项来检查Socket的状态,以便在Socket出错时及时处理错误。例如,当使用select或poll等多路复用函数时,我们可以先调用getsockopt函数来获取Socket的错误状态,然后根据错误状态来判断Socket是否可以进行读写操作。
获取socket错误状态 例子如下:
1 2 3 int err;socklen_t socklen = sizeof (err);int sockoptret = getsockopt(m_socket, SOL_SOCKET, SO_ERROR, &err, &socklen);
心跳包 用于检测长连接是否正常的手段
长连接和短连接的概念:
连接建立之后一直不关闭为长连接 连接收发数据完毕之后就关闭为短连接
长连接 通常用于通信双方数据交换频繁的情况下
如何检查与对方的网络连接是否正常? 一般心跳包用于长连接.
1 2 keepAlive = 1 ; setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof (keepAlive));
上面为官方提供的方式,由于不能实时的检测网络情况, 一般不用这种方法
在应用程序中自己定义心跳包,使用灵活,能实时把控
通信双方需要协商规则(协议)
高并发服务器模型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 2 3 4 5 typedef struct { long int __fds_bits[1024 /(8 *8 ))];}
从上面的文件中可以看出, 这几个宏本质上还是位操作.(与信号集操作类似)
select开发服务端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 #include <iostream> using namespace std ; #include <math.h> #include <time.h> #include <unistd.h> #include <dirent.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <signal.h> #include <sys/time.h> #include <pthread.h> #include <sys/socket.h> #include <arpa/inet.h> #include "mysocket.h" #include <iomanip> #include <sys/select.h> int main (int argc,char ** argv) { int lfd = Socket(AF_INET,SOCK_STREAM,0 ); struct sockaddr_in serv ; serv.sin_family = AF_INET; serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(23456 ); int opt = 1 ; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (int )); Bind(lfd,(struct sockaddr*)&serv,sizeof (serv)); Listen(lfd,2 ); int maxfd = lfd; fd_set setReal; FD_ZERO(&setReal); FD_SET(lfd,&setReal); std ::cout <<"服务器开启,等待连接" <<std ::endl ; while (1 ) { fd_set setR = setReal; int fdChangeNum = select(maxfd+1 ,&setR,NULL ,NULL ,NULL ); if (fdChangeNum<0 ) { if (errno==EINTR) { continue ; } break ; } if (FD_ISSET(lfd,&setR)) { std ::cout <<"客户端连接存在,返回:" <<fdChangeNum<<std ::endl ; int cfd = accept(lfd,NULL ,NULL ); std ::cout <<"接受到新的连接,文件描述符为" <<cfd<<std ::endl ; FD_SET(cfd,&setReal); if (maxfd<cfd) { maxfd = cfd ; } if (--fdChangeNum==0 ) { continue ; } } char str[99 ]={0 }; for (int i = lfd+1 ; i <= maxfd; i++) { if (FD_ISSET(i,&setR)) { std ::cout <<"数据通信存在,返回值为:" <<fdChangeNum<<std ::endl ; int iRet = Read(i,str,sizeof (str)); if (iRet<=0 ) { std ::cout <<"断开连接" <<std ::endl ; close(i); FD_CLR(i,&setReal); continue ; } cout <<"[返回" <<iRet<<"]读到的数据为:" <<str<<endl ; } } } close(lfd); return 0 ; }
关于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位)使用了该宏, 当然可以修改内核, 然后再重新编译内核, 一般不建议这么做.
windows下的select模型相关知识外链跳转
练习 编写代码, 让select监控标准输入, 监控网络, 如果标准输入有数据就写入网络, 如果网络有数据就读出网络数据, 然后打印到标准输出.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 int main (int argc,char ** argv) { int lfd = Socket(AF_INET,SOCK_STREAM,0 ); struct sockaddr_in serv ; serv.sin_family = AF_INET; serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(23456 ); int opt = 1 ; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (int )); Bind(lfd,(struct sockaddr*)&serv,sizeof (serv)); Listen(lfd,2 ); std ::cout <<"服务器开启,等待连接" <<std ::endl ; int cfd = Accept(lfd,NULL ,NULL ); std ::cout <<"接受到新的连接,文件描述符为" <<cfd<<std ::endl ; fd_set setRReal; FD_ZERO(&setRReal); FD_SET(cfd,&setRReal); FD_SET(STDIN_FILENO,&setRReal); while (1 ) { fd_set setR = setRReal; int iRet = select(cfd+1 ,&setR,NULL ,NULL ,NULL ); char str[99 ]={0 }; if (FD_ISSET(cfd,&setR)) { int iRet = read(cfd,str,sizeof (str)); if (iRet<= 0 ) break ; cout <<str<<endl ; } if (FD_ISSET(STDIN_FILENO,&setR)) { gets(str); strcat (str,"\n" ); write(cfd,str,strlen (str)+1 ); } } close(cfd); close(lfd); return 0 ; }
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) ;
参数说明
返回值
若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 2 3 4 * soft nofile 1024 * hard nofile 100000
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 int main (int argc,char ** argv) { int lfd = Socket(AF_INET,SOCK_STREAM,0 ); struct sockaddr_in serv ; serv.sin_family = AF_INET; serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(23456 ); int opt = 1 ; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (int )); Bind(lfd,(struct sockaddr*)&serv,sizeof (serv)); Listen(lfd,2 ); std ::cout <<"服务器开启,等待连接" <<std ::endl ; struct pollfd client [1024]; for (size_t i = 0 ; i < 1024 ; i++) { client[i].fd = -1 ; } int max = 0 ; client[0 ].fd = lfd; client[0 ].events = POLLIN; while (1 ) { int iRet = poll(client,max+1 ,-1 ); if (iRet < 0 ) { if (errno == EINTR) { continue ; } break ; } if (client[0 ].revents == POLLIN) { int cfd = Accept(lfd,NULL ,NULL ); std ::cout <<"接受连接成功,fd:" <<cfd<<std ::endl ; size_t i; for (i = 0 ; i < 1024 ; i++) { if (client[i].fd == -1 ) { client[i].fd = cfd; client[i].events = POLLIN; if (i>max) { max = i; } break ; } } if (i==1024 ) { close(cfd); continue ; } if (--iRet==0 ) continue ; } char str[99 ] = {0 }; for (size_t j = 1 ; j <= max; j++) { if (client[j].revents == (POLLHUP|POLLIN)) { } if (client[j].revents & POLLIN) { int iReadRet = Read(client[j].fd, str, sizeof (str)); std ::cout <<iReadRet<<std ::endl ; if (iReadRet <= 0 ) { std ::cout <<"断开连接" <<std ::endl ; close(client[j].fd); client[j].fd = -1 ; if (j == max) max--; continue ; } cout << "读到的数据:" << str << endl ; if (--iRet == 0 ) break ; } } } close(lfd); return 0 ; }
多路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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t ; struct epoll_event { uint32_t events; epoll_data_t 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <sys/epoll.h> using namespace std ; int main () { int lfd = socket(AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (int )); sockaddr_in serv; serv.sin_family = AF_INET; serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(23456 ); bind(lfd, (sockaddr *)&serv, sizeof (serv)); listen(lfd, 2 ); int epfd = epoll_create(1 ); epoll_event ev; ev.events = EPOLLIN; ev.data.fd = lfd; epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); epoll_event events[1024 ]; while (1 ) { int nready = epoll_wait(epfd, events, 1024 , -1 ); if (nready < 0 ) { if (errno == EINTR) continue ; break ; } for (int i = 0 ; i < nready; i++) { int sockfd = events[i].data.fd; if (sockfd == lfd) { int cfd = accept(lfd, NULL , NULL ); ev.data.fd = cfd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); continue ; } char buf[999 ] = {0 }; int n = read(sockfd, buf, sizeof (buf)); if (n <= 0 ) { close(sockfd); epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL ); perror("read error of client close" ); continue ; } else { cout << buf << endl ; } } } close(lfd); return 0 ; }
可能存在的问题:若每一个连接上处理的时间比较长,会导致后面的连接上发来的数据得不到及时的处理
解决方法:可以让主线程处理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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <sys/epoll.h> #include <fcntl.h> using namespace std ; int main () { int lfd = socket(AF_INET,SOCK_STREAM,0 ); int opt = 1 ; setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (int )); sockaddr_in serv; serv.sin_family = AF_INET; serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(23456 ); bind(lfd,(sockaddr*)&serv,sizeof (serv)); listen(lfd,2 ); int epfd = epoll_create(1 ); epoll_event ev; ev.events = EPOLLIN; ev.data.fd = lfd; epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); epoll_event events[1024 ]; while (1 ) { int nready = epoll_wait(epfd,events,1024 ,-1 ); if (nready<0 ) { if (errno == EINTR) continue ; break ; } for (int i=0 ;i<nready;i++) { int sockfd = events[i].data.fd; if (sockfd == lfd) { int cfd = accept(lfd,NULL ,NULL ); ev.data.fd = cfd; ev.events = EPOLLIN | EPOLLET; epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev); int flag = fcntl(cfd,F_GETFL); flag |= O_NONBLOCK; fcntl(cfd,F_SETFL,flag); continue ; } char buf[999 ]={0 }; while (1 ) { int n = read(sockfd,buf,2 ); if (n == -1 ) { break ; } else if (n<=0 ) { cout <<"断开" <<endl ; close(sockfd); epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL ); perror("read error of client close" ); break ; } else { cout <<buf<<endl ; } } } } close(lfd); return 0 ; }
epoll反应堆 epoll反应堆的核心思想是: 在调用epoll_ctl函数的时候, 将events上树的时候,利用epoll_data_t的ptr成员, 将一个文件描述符,事件和回调函数封装成一个结构体, 然后让ptr指向这个结构体, 然后调用epoll_wait函数返回的时候, 可以得到具体的events, 然后获得events结构体中的events.data.ptr指针, ptr指针指向的结构体中有回调函数, 最终可以调用这个回调函数.
其实就是自己定义结构记录回调等信息放入ptr指向中(原本用于存放fd的联合体),在main中根据epoll通知,取ptr信息来自己调用回调以此实现了一个架构体系
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/epoll.h> #include <fcntl.h> #include <errno.h> #include <time.h> #include <ctype.h> #include "wrap.h" #define MAX_EVENTS 1024 #define BUFLEN 4096 void recvdata (int fd, int events, void *arg) ;void senddata (int fd, int events, void *arg) ;struct myevent_s { int fd; int events; void *arg; void (*call_back)(int fd, int events, void *arg); int status; char buf[BUFLEN]; int len; long last_active; }; int g_efd; int g_lfd; struct myevent_s g_events [MAX_EVENTS +1]; void eventset (struct myevent_s *ev, int fd, void (*call_back)(int , int , void *), void *arg) { ev->fd = fd; ev->call_back = call_back; ev->events = 0 ; ev->arg = arg; ev->status = 0 ; ev->last_active = time(NULL ); return ; } void eventadd (int efd, int events, struct myevent_s *ev) { struct epoll_event epv = {0 , {0 }}; int op; epv.data.ptr = ev; epv.events = ev->events = events; if (ev->status == 1 ) { op = EPOLL_CTL_MOD; } else { op = EPOLL_CTL_ADD; ev->status = 1 ; } if (epoll_ctl(efd, op, ev->fd, &epv) < 0 ) { printf ("event add failed [fd=%d], events[%d]\n" , ev->fd, events); } else { printf ("event add OK [fd=%d], op=%d, events[%0X]\n" , ev->fd, op, events); } return ; } void eventdel (int efd, struct myevent_s *ev) { struct epoll_event epv = {0 , {0 }}; if (ev->status != 1 ) return ; epv.data.ptr = ev; ev->status = 0 ; epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv); return ; } void acceptconn (int lfd, int events, void *arg) { struct sockaddr_in cin ; socklen_t len = sizeof (cin ); int cfd, i; cfd = Accept(lfd, (struct sockaddr *)&cin , &len); do { for (i = 0 ; i < MAX_EVENTS; i++) { if (g_events[i].status == 0 ) { break ; } } if (i == MAX_EVENTS) { printf ("%s: max connect limit[%d]\n" , __func__, MAX_EVENTS); break ; } int flags = 0 ; flags = fcntl(cfd, F_GETFL, 0 ); flags |= O_NONBLOCK; if ((flags = fcntl(cfd, F_SETFL, flags)) < 0 ) { printf ("%s: fcntl nonblocking failed, %s\n" , __func__, strerror(errno)); break ; } eventset(&g_events[i], cfd, recvdata, &g_events[i]); eventadd(g_efd, EPOLLIN, &g_events[i]); }while (0 ); printf ("new connect [%s:%d][time:%ld], pos[%d]\n" , inet_ntoa(cin .sin_addr), ntohs(cin .sin_port), g_events[i].last_active, i); return ; } void recvdata (int fd, int events, void *arg) { int len; struct myevent_s *ev = (struct myevent_s *)arg; memset (ev->buf, 0x00 , sizeof (ev->buf)); len = Read(fd, ev->buf, sizeof (ev->buf)); eventdel(g_efd, ev); if (len > 0 ) { ev->len = len; ev->buf[len] = '\0' ; printf ("C[%d]:%s\n" , fd, ev->buf); eventset(ev, fd, senddata, ev); eventadd(g_efd, EPOLLOUT, ev); } else if (len == 0 ) { Close(ev->fd); printf ("[fd=%d] pos[%ld], closed\n" , fd, ev-g_events); } else { Close(ev->fd); printf ("read [fd=%d] error[%d]:%s\n" , fd, errno, strerror(errno)); } return ; } void senddata (int fd, int events, void *arg) { int len; struct myevent_s *ev = (struct myevent_s *)arg; int i=0 ; for (i=0 ; i<ev->len; i++) { ev->buf[i] = toupper (ev->buf[i]); } len = Write(fd, ev->buf, ev->len); if (len > 0 ) { printf ("send[fd=%d]-->[%d]:[%s]\n" , fd, len, ev->buf); eventdel(g_efd, ev); eventset(ev, fd, recvdata, ev); eventadd(g_efd, EPOLLIN, ev); } else { Close(ev->fd); eventdel(g_efd, ev); printf ("send[fd=%d] error %s\n" , fd, strerror(errno)); } return ; } void initlistensocket () { g_lfd = Socket(AF_INET, SOCK_STREAM, 0 ); eventset(&g_events[MAX_EVENTS], g_lfd, acceptconn, &g_events[MAX_EVENTS]); eventadd(g_efd, EPOLLIN, &g_events[MAX_EVENTS]); struct sockaddr_in servaddr ; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(23456 ); Bind(g_lfd, (struct sockaddr *)&servaddr, sizeof (servaddr)); Listen(g_lfd, 20 ); return ; } int main (int argc, char *argv[]) { g_efd = epoll_create(MAX_EVENTS+1 ); if (g_efd<0 ) { perror("create epoll error" ); return -1 ; } initlistensocket(); struct epoll_event events [MAX_EVENTS +1]; int checkpos = 0 , i; while (1 ) { long now = time(NULL ); for (i = 0 ; i < 100 ; i++, checkpos++) { if (checkpos == MAX_EVENTS) { checkpos = 0 ; } if (g_events[checkpos].status != 1 ) { continue ; } long duration = now - g_events[checkpos].last_active; if (duration >= 60 ) { Close(g_events[checkpos].fd); printf ("[fd=%d] timeout\n" , g_events[checkpos].fd); eventdel(g_efd, &g_events[checkpos]); } } int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1 , 1000 ); if (nfd < 0 ) { printf ("epoll_wait error, exit\n" ); break ; } for (i = 0 ; i < nfd; i++) { struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr; if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) { ev->call_back(ev->fd, events[i].events, ev); } if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) { ev->call_back(ev->fd, events[i].events, ev); } } } Close(g_efd); Close(g_lfd); return 0 ; }
线程池 一个抽象的概念, 若干个线程组合到一起, 形成线程池.
为什么需要线程池? 多线程版服务器一个客户端就需要创建一个线程! 若客户端太多, 显然不太合适. 什么时候需要创建线程池呢?简单的说,如果一个应用需要频繁的创建和销毁线程,而任务执行的时间又非常短,这样线程创建和销毁的带来的开销就不容忽视,这时也是线程池该出场的机会了。如果线程创建和销毁时间相比任务执行时间可以忽略不计,则没有必要使用线程池了,此时可以考虑协程(C++20)。 实现的时候类似于生产者和消费者.
wrap.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <strings.h> void perr_exit (const char *s) { perror(s); exit (-1 ); } int Accept (int fd, struct sockaddr *sa, socklen_t *salenptr) { int n; again: if ((n = accept(fd, sa, salenptr)) < 0 ) { if ((errno == ECONNABORTED) || (errno == EINTR)) goto again; else perr_exit("accept error" ); } return n; } int Bind (int fd, const struct sockaddr *sa, socklen_t salen) { int n; if ((n = bind(fd, sa, salen)) < 0 ) perr_exit("bind error" ); return n; } int Connect (int fd, const struct sockaddr *sa, socklen_t salen) { int n; if ((n = connect(fd, sa, salen)) < 0 ) perr_exit("connect error" ); return n; } int Listen (int fd, int backlog) { int n; if ((n = listen(fd, backlog)) < 0 ) perr_exit("listen error" ); return n; } int Socket (int family, int type, int protocol) { int n; if ((n = socket(family, type, protocol)) < 0 ) perr_exit("socket error" ); return n; } ssize_t Read (int fd, void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = read(fd, ptr, nbytes)) == -1 ) { if (errno == EINTR) goto again; else return -1 ; } return n; } ssize_t Write (int fd, const void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = write(fd, ptr, nbytes)) == -1 ) { if (errno == EINTR) goto again; else return -1 ; } return n; } int Close (int fd) { int n; if ((n = close(fd)) == -1 ) perr_exit("close error" ); return n; } ssize_t Readn (int fd, void *vptr, size_t n) { size_t nleft; ssize_t nread; char *ptr; ptr = vptr; nleft = n; while (nleft > 0 ) { if ((nread = read(fd, ptr, nleft)) < 0 ) { if (errno == EINTR) nread = 0 ; else return -1 ; } else if (nread == 0 ) break ; nleft -= nread; ptr += nread; } return n - nleft; } ssize_t Writen (int fd, const void *vptr, size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while (nleft > 0 ) { if ( (nwritten = write(fd, ptr, nleft)) <= 0 ) { if (nwritten < 0 && errno == EINTR) nwritten = 0 ; else return -1 ; } nleft -= nwritten; ptr += nwritten; } return n; } static ssize_t my_read (int fd, char *ptr) { static int read_cnt; static char *read_ptr; static char read_buf[100 ]; if (read_cnt <= 0 ) { again: if ( (read_cnt = read(fd, read_buf, sizeof (read_buf))) < 0 ) { if (errno == EINTR) goto again; return -1 ; } else if (read_cnt == 0 ) return 0 ; read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return 1 ; } ssize_t Readline (int fd, void *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1 ; n < maxlen; n++) { if ( (rc = my_read(fd, &c)) == 1 ) { *ptr++ = c; if (c == '\n' ) break ; } else if (rc == 0 ) { *ptr = 0 ; return n - 1 ; } else return -1 ; } *ptr = 0 ; return n; } int tcp4bind (short port,const char *IP) { struct sockaddr_in serv_addr ; int lfd = Socket(AF_INET,SOCK_STREAM,0 ); bzero(&serv_addr,sizeof (serv_addr)); if (IP == NULL ){ serv_addr.sin_addr.s_addr = INADDR_ANY; }else { if (inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0 ){ perror(IP); exit (1 ); } } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(port); Bind(lfd,(struct sockaddr *)&serv_addr,sizeof (serv_addr)); return lfd; }
wrap.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #ifndef __WRAP_H_ #define __WRAP_H_ #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <strings.h> void perr_exit (const char *s) ;int Accept (int fd, struct sockaddr *sa, socklen_t *salenptr) ;int Bind (int fd, const struct sockaddr *sa, socklen_t salen) ;int Connect (int fd, const struct sockaddr *sa, socklen_t salen) ;int Listen (int fd, int backlog) ;int Socket (int family, int type, int protocol) ;ssize_t Read (int fd, void *ptr, size_t nbytes) ;ssize_t Write (int fd, const void *ptr, size_t nbytes) ;int Close (int fd) ;ssize_t Readn (int fd, void *vptr, size_t n) ;ssize_t Writen (int fd, const void *vptr, size_t n) ;ssize_t my_read (int fd, char *ptr) ;ssize_t Readline (int fd, void *vptr, size_t maxlen) ;int tcp4bind (short port,const char *IP) ;#endif
实现原理 线程池和任务池: 任务池相当于共享资源, 所以需要使用互斥锁, 当任务池中没有任务的时候需要让线程阻塞, 所以需要使用条件变量.
如何让线程执行不同的任务? 使用回调函数, 在任务中设置任务执行函数, 这样可以起到不同的任务执行不同的函数.
线程相关函数
创建子线程 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> int beginNum=0 ;typedef struct _PoolTask { int tasknum; void * arg; void (*task_func)(void * arg); }PoolTask; typedef struct _ThreadPool { int maxJobNum; int jobNum; PoolTask* tasks; int jobPush; int jobPop; int threadNum; pthread_t * threads; int shutDown; pthread_mutex_t poolLock; pthread_cond_t emptyTask; pthread_cond_t notEmptyTask; }ThreadPool; void * threadRun (void * arg) { ThreadPool *thePool = arg; PoolTask *theTask = malloc (sizeof (PoolTask)); while (1 ) { pthread_mutex_lock(&thePool->poolLock); while (thePool->jobNum == 0 && !thePool->shutDown) pthread_cond_wait(&thePool->notEmptyTask, &thePool->poolLock); if (thePool->jobNum) { memcpy (theTask,&thePool->tasks[thePool->jobPop % thePool->maxJobNum],sizeof (PoolTask)); thePool->jobPop++; thePool->jobNum--; pthread_cond_signal(&thePool->emptyTask); } if (thePool->shutDown) { pthread_mutex_unlock(&thePool->poolLock); free (theTask); pthread_exit(NULL ); } pthread_mutex_unlock(&thePool->poolLock); theTask->task_func(theTask->arg); } } ThreadPool* createThreadPool (int threadNum,int maxTaskNum) { ThreadPool* pool = malloc (sizeof (ThreadPool)); memset (pool,0 ,sizeof (ThreadPool)); pool->maxJobNum = maxTaskNum; pool->threadNum = threadNum; pool->tasks = malloc (maxTaskNum*sizeof (PoolTask)); pool->threads = malloc (threadNum*sizeof (pthread_t )); if (pthread_mutex_init(&pool->poolLock,NULL )) { free (pool->tasks); free (pool->threads); perror("[error]" ); return NULL ; } pthread_cond_init(&pool->emptyTask,NULL ); pthread_cond_init(&pool->notEmptyTask,NULL ); pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); for (int i = 0 ; i < threadNum; i++) { int iRet = pthread_create(&pool->threads[i],&attr,threadRun,(void *)pool); } return pool; } void destroyThreadPool (ThreadPool *pool) { pool->shutDown = 1 ; pthread_cond_broadcast(&pool->notEmptyTask); pthread_cond_destroy(&pool->notEmptyTask); pthread_cond_destroy(&pool->emptyTask); pthread_mutex_destroy(&pool->poolLock); free (pool->tasks); free (pool->threads); free (pool); } void addTask (ThreadPool *pool,void taskRun (void *),void * arg) { pthread_mutex_lock(&pool->poolLock); while (pool->maxJobNum<=pool->jobNum) pthread_cond_wait(&pool->emptyTask,&pool->poolLock); int taskPos = pool->jobPush%pool->maxJobNum; pool->jobPush++; pool->tasks[taskPos].tasknum = beginNum++; pool->tasks[taskPos].arg = arg; pool->tasks[taskPos].task_func = taskRun; pool->jobNum++; pthread_mutex_unlock(&pool->poolLock); pthread_cond_signal(&pool->notEmptyTask); } void taskRun (void * taskNum) { printf ("当前执行任务编号为:%d\n" ,(int )taskNum); } int main () { ThreadPool* pool = createThreadPool(3 ,10 ); for (int i = 0 ; i < 20 ; i++) { addTask(pool,taskRun,i); } sleep(10 ); printf ("主线程结束!\n" ); destroyThreadPool(pool); return 0 ; }
复杂版本 比起简单版本,添加了动态线程管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <errno.h> #define DEFAULT_TIME 10 #define MIN_WAIT_TASK_NUM 10 #define DEFAULT_THREAD_VARY 10 #define true 1 #define false 0 typedef struct _threadpool_task { void * arg; void * (*function)(void * arg); }threadpool_task; typedef struct threadpool_t { pthread_mutex_t lock; pthread_mutex_t threadCounter; pthread_cond_t queueNotFull; pthread_cond_t queueNotEmpty; pthread_t *threads; pthread_t adjustTid; threadpool_task *taskQueue; int minThrNum; int maxThrNum; int liveThrNum; int busyThrNum; int waitExitThrNum; int queueFront; int queueRear; int queueSize; int queueMaxSize; int shutdown; }threadpool_t ; void * threadPoolThread (void * threadPool) { threadpool_t * pool = (threadpool_t *)threadPool; threadpool_task task; while (true ) { pthread_mutex_lock(&pool->lock); while (pool->queueSize == 0 && !pool->shutdown) { pthread_cond_wait(&pool->queueNotEmpty,&pool->lock); if (pool->waitExitThrNum > 0 ) { pool->waitExitThrNum -- ; if (pool->liveThrNum>pool->minThrNum) { printf ("[%s():%d] thread 0x%x is exiting for adjust\n" ,__FUNCTION__,__LINE__,(unsigned int )pthread_self()); pool->liveThrNum--; pthread_mutex_unlock(&pool->lock); pthread_exit(NULL ); } } } if (pool->shutdown) { pthread_mutex_unlock(&pool->lock); printf ("[%s():%d] thread 0x%x is exiting for shutdown\n" ,__FUNCTION__,__LINE__,(unsigned int )pthread_self()); pthread_exit(NULL ); } if (pool->queueSize>0 ) { memcpy (&task,&pool->taskQueue[pool->queueFront],sizeof (threadpool_task)); pool->queueFront = (pool->queueFront+1 )%pool->queueMaxSize; pool->queueSize--; pthread_cond_signal(&pool->queueNotFull); } pthread_mutex_unlock(&pool->lock); pthread_mutex_lock(&pool->busyThrNum); pool->busyThrNum++; pthread_mutex_unlock(&pool->busyThrNum); (*task.function)(task.arg); pthread_mutex_lock(&pool->busyThrNum); pool->busyThrNum--; pthread_mutex_unlock(&pool->busyThrNum); } } int isThreadAlive (pthread_t tid) { int kill_rc = pthread_kill(tid,0 ); if (kill_rc!=0 ) { if (kill_rc == ESRCH) return false ; } return true ; } void * adjustThread (void * threadPool) { printf ("[%s():%d] start manager thread 0x%x...\n" ,__FUNCTION__,__LINE__,(unsigned int )pthread_self()); threadpool_t * pool = (threadpool_t *)threadPool; while (!pool->shutdown) { sleep(DEFAULT_TIME); pthread_mutex_lock(&pool->lock); int queueSize = pool->queueSize; int liveThrNum = pool->liveThrNum; pthread_mutex_unlock(&pool->lock); pthread_mutex_lock(&pool->threadCounter); int busyThrNum = pool->busyThrNum; pthread_mutex_unlock(&pool->threadCounter); if (queueSize>=MIN_WAIT_TASK_NUM && liveThrNum<pool->maxThrNum) { pthread_mutex_lock(&pool->lock); int add = 0 ; for (int i = 0 ; i < pool->maxThrNum&&add<DEFAULT_THREAD_VARY&&pool->liveThrNum<pool->maxThrNum; i++) { if (pool->threads[i]==0 || !isThreadAlive(pool->threads[i])) { printf ("[%s():%d] create new Thread\n" ,__FUNCTION__,__LINE__); pthread_create(&pool->threads[i],NULL ,threadPoolThread,(void *)pool); add++; pool->liveThrNum++; } } pthread_mutex_unlock(&pool->lock); } if ((busyThrNum * 2 ) < liveThrNum && liveThrNum > pool->minThrNum&&DEFAULT_THREAD_VARY<liveThrNum) { pthread_mutex_lock(&(pool->lock)); pool->waitExitThrNum = DEFAULT_THREAD_VARY; pthread_mutex_unlock(&(pool->lock)); for (int i = 0 ; i < DEFAULT_THREAD_VARY; i++) { pthread_cond_signal(&(pool->queueNotEmpty)); } } } printf ("[%s():%d] manager thread 0x%x exit for shutdown\n" ,__FUNCTION__,__LINE__,(unsigned int )pthread_self()); return NULL ; } int threadpool_free (threadpool_t *pool) { if (pool == NULL ) return -1 ; if (pool->taskQueue) free (pool->taskQueue); if (pool->threads) { free (pool->threads); pthread_mutex_lock(&pool->lock); pthread_mutex_destroy(&pool->lock); pthread_mutex_lock(&pool->threadCounter); pthread_mutex_destroy(&pool->threadCounter); pthread_cond_destroy(&pool->queueNotEmpty); pthread_cond_destroy(&pool->queueNotFull); } free (pool); pool=NULL ; return 0 ; } threadpool_t * threadpool_creat (int minThrNum,int maxThrNum,int queueMaxSize) { threadpool_t * pool = NULL ; do { if ((pool = malloc (sizeof (threadpool_t ))) == NULL ) { printf ("[%s():%d] malloc threadpool fail\n" ,__FUNCTION__,__LINE__); break ; } memset (pool, 0 , sizeof (threadpool_t )); pool->minThrNum = minThrNum; pool->maxThrNum = maxThrNum; pool->queueMaxSize = queueMaxSize; pool->liveThrNum = minThrNum; pool->threads = malloc (sizeof (pthread_t ) * maxThrNum); if (pool->threads == NULL ) { printf ("[%s:%d]malloc threads fail\n" , __LINE__, __FUNCTION__); break ; } memset (pool->threads,0 ,sizeof (pthread_t )*maxThrNum); pool->taskQueue = malloc (sizeof (threadpool_task)*queueMaxSize); if (pool->taskQueue == NULL ) { printf ("[%s:%d]malloc taskQueue fail\n" , __LINE__, __FUNCTION__); break ; } if (pthread_mutex_init(&pool->lock,NULL )!=0 || pthread_mutex_init(&pool->threadCounter,NULL )!=0 || pthread_cond_init(&pool->queueNotEmpty,NULL )!= 0 || pthread_cond_init(&pool->queueNotFull,NULL )!= 0 ) { printf ("[%s():%d] init the lock or cond fail\n" ,__FUNCTION__,__LINE__); break ; } pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); for (int i = 0 ; i < minThrNum; i++) { pthread_create(&pool->threads[i],&attr,threadPoolThread,pool); printf ("[%s():%d] start thread 0x%x...\n" ,__FUNCTION__,__LINE__,(unsigned int )pool->threads[i]); } pthread_create(&pool->adjustTid,&attr,adjustThread,pool); return pool; } while (0 ); threadpool_free(pool); return NULL ; } void threadpool_add (threadpool_t * pool,void *(*function)(void * arg),void * arg) { pthread_mutex_lock(&pool->lock); while (pool->queueSize == pool->queueMaxSize && !pool->shutdown) { pthread_cond_wait(&pool->queueNotFull,&pool->lock); } if (pool->shutdown) { pthread_cond_broadcast(&pool->queueNotEmpty); pthread_mutex_unlock(&pool->lock); return ; } pool->taskQueue[pool->queueRear].function = function; pool->taskQueue[pool->queueRear].arg = arg; pool->queueRear = (pool->queueRear + 1 )%pool->queueMaxSize; pool->queueSize++; pthread_cond_signal(&pool->queueNotEmpty); pthread_mutex_unlock(&pool->lock); return ; } int threadpool_destroy (threadpool_t * pool) { int i; if (pool == NULL ) return -1 ; pool->shutdown = true ; for (i = 0 ; i < pool->liveThrNum; i++) { pthread_cond_broadcast(&(pool->queueNotEmpty)); } while (isThreadAlive(pool->adjustTid)); for (i = 0 ; i < pool->maxThrNum; i++) { if (pool->threads[i]!=NULL ) { while (isThreadAlive(pool->threads[i])); } } threadpool_free(pool); return NULL ; } void taskRun (int i) { printf (">>>执行任务<<< 任务编号为:%d\n" ,i); sleep(1 ); printf (">>>执行完成<<< 任务编号为:%d\n" ,i); } int main (int argc,char * argv[]) { threadpool_t * pool = threadpool_creat(20 ,50 ,100 ); for (int i = 0 ; i < 500 ; i++) { threadpool_add(pool,taskRun,i); } threadpool_destroy(pool); return 0 ; }
截取一点点的效果图(效果过长)
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 标志位一般填0
src_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服务器编码流程:
创建套接字 type=SOCK_DGRAM (datagrams数据报) – socket
绑定ip和端口 – bind
收消息–recvfrom 发消息–sendto
关闭套接字–close
UDP客户端编码流程:
创建套接字 type=SOCK_DGRAM (datagrams数据报) – socket
收消息–recvfrom 发消息–sendto
关闭套接字–close
测试命令
案例 下面是服务端收,客户端发简单案例
udp服务端简单代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> void acceptClient2Str (struct sockaddr_in* client,char * str) { char sip[16 ]; memset (sip,0 ,sizeof (sip)); sprintf (str,"%s:%hu" ,inet_ntop(AF_INET,&client->sin_addr.s_addr,sip,sizeof (sip)),ntohs(client->sin_port)); } int main (int argc,char * argv[]) { int lfd = socket(AF_INET,SOCK_DGRAM,0 ); struct sockaddr_in serv ; serv.sin_family = AF_INET; serv.sin_port = htons (23456 ); serv.sin_addr.s_addr = htonl(INADDR_ANY); bind(lfd,&serv,sizeof (serv)); char buf[99 ]={0 }; printf ("等待接受信息\n" ); while (1 ) { socklen_t len=sizeof (serv); int iReaded = recvfrom(lfd,buf,99 ,0 ,&serv,&len); char ipStr[20 ]={0 }; acceptClient2Str(&serv,ipStr); if (iReaded>0 ) printf ("[%s():%d] 数据来源:%s 长度:%d 内容:%s\n" ,__FUNCTION__,__LINE__,ipStr,iReaded,buf); } close(lfd); return 0 ; }
udp客户端简单代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> int main (int argc,char * argv[]) { int lfd = socket(AF_INET,SOCK_DGRAM,0 ); char buf[99 ]={0 }; while (1 ) { scanf ("%s" ,buf); printf ("%s\n" ,buf); struct sockaddr_in serv ; serv.sin_family = AF_INET; serv.sin_port = htons(23456 ); inet_pton(AF_INET,"127.0.0.1" ,&serv.sin_addr.s_addr); serv.sin_addr.s_addr = htonl(INADDR_ANY); socklen_t len = sizeof (serv); sendto(lfd, buf, sizeof (buf), 0 , &serv, len); } return 0 ; }
本地socket通信 也是一种IPC机制(进程间通信机制)
通过查询: man 7 unix 可以查到unix本地域socket通信相关信息
需要头文件 #include <sys/un.h> 和 #include <sys/socket.h>
本地套接字服务器的流程:
可以使用TCP的方式, 必须按照tcp的流程
也可以使用UDP的方式, 必须按照udp的流程
AF_INET : ipv4地址域类型 struct sockaddr_in
AF_INET6: ipv6地址域类型 struct sockaddr_in6
AF_UNIX: 本地 地址域类型 struct sockaddr_un
唯一的区别只是bind函数传参数不同 :本地socket通信使用的是sockaddr_un结构体,如下:
1 2 3 4 struct sockaddr_un serv ;serv.sun_family = AF_UNIX; strcpy (serv.sun_path ,"./serv.sock" ); bind(lfd,&serv,sizeof (serv));
tcp的本地套接字服务器流程:
tcp本地套接字客户端流程:
调用socket 创建套接字
调用bind 函数将socket文件描述和socket文件进行绑定.
不是必须的, 若无显示绑定会进行隐式绑定,但服务器不知道谁连接了,就无法给客户端发送消息,原因是网络编程的时候客户端信息操作系统会自动分配,而本地通信并不会
调用connect 函数连接服务端
循环通信read-write
关闭文件描述符close
需要注意的是: bind函数会自动创建socket文件 (大小为0), 若在调用bind函数之前socket文件已经存在, 则调用bind会报错 , 可以使用unlink函数在bind之前先删除文件.
测试本地socket通信:nc -U socket文件名.s
代码案例 TCP 服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/un.h> #define filename "./serv.sock" int main (int argc,char * argv[]) { int lfd = socket(AF_UNIX,SOCK_STREAM,0 ); struct sockaddr_un serv ; bzero(&serv,sizeof (serv)); serv.sun_family = AF_UNIX; strcpy (serv.sun_path ,filename); printf ("%s\n" ,serv.sun_path); unlink(filename); int ret = bind(lfd,&serv,sizeof (serv)); if (ret<0 ){ perror("bind error" ); return -1 ; } listen(lfd,2 ); socklen_t len = sizeof (serv); int cfd = accept(lfd,&serv,&len); if (cfd==-1 ) { perror("accept wrong:" ); } char buf[99 ]={0 }; printf ("等待接受连接 cfd为:%d\n" ,cfd); while (1 ) { socklen_t len=sizeof (serv); int iReaded = recv(cfd,buf,sizeof (buf),0 ); if (iReaded>0 ) printf ("[%s():%d] 长度:%d 内容:%s\n" ,__FUNCTION__,__LINE__,iReaded,buf); } close(cfd); close(lfd); return 0 ; }
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/un.h> #define filename "../test2/serv.sock" int main (int argc,char * argv[]) { int cfd = socket(AF_UNIX,SOCK_STREAM,0 ); char buf[99 ]={0 }; struct sockaddr_un serv ; bzero(&serv, sizeof (serv)); serv.sun_family = AF_UNIX; strcpy (serv.sun_path, filename); printf ("%s\n" , serv.sun_path); socklen_t len = sizeof (serv); connect(cfd, &serv, len); while (1 ) { scanf ("%s" ,buf); printf ("%s\n" ,buf); send(cfd, buf, strlen (buf)+1 ,0 ); } return 0 ; }
UDP 服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/un.h> #define filename "./serv.sock" int main (int argc,char * argv[]) { int lfd = socket(AF_UNIX,SOCK_DGRAM,0 ); struct sockaddr_un serv ; bzero(&serv,sizeof (serv)); serv.sun_family = AF_UNIX; strcpy (serv.sun_path ,filename); printf ("%s\n" ,serv.sun_path); unlink(filename); bind(lfd,&serv,sizeof (serv)); char buf[99 ]={0 }; printf ("等待接受连接\n" ); while (1 ) { socklen_t len=sizeof (serv); int iReaded = recvfrom(lfd,buf,sizeof (buf),0 ,&serv,&len); char ipStr[20 ]={0 }; if (iReaded>0 ) printf ("[%s():%d] 长度:%d 内容:%s\n" ,__FUNCTION__,__LINE__,iReaded,buf); } close(lfd); return 0 ; }
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/un.h> #define filename "../test2/serv.sock" int main (int argc,char * argv[]) { int cfd = socket(AF_UNIX,SOCK_DGRAM,0 ); char buf[99 ]={0 }; while (1 ) { scanf ("%s" ,buf); printf ("%s\n" ,buf); struct sockaddr_un serv ; bzero(&serv,sizeof (serv)); serv.sun_family = AF_UNIX; strcpy (serv.sun_path,filename); printf ("%s\n" ,serv.sun_path); socklen_t len = sizeof (serv); sendto(cfd, buf, sizeof (buf), 0 , &serv, len); } return 0 ; }
此处代码只需要服务端收,客户端发,如果服务端也需要发送数据给指定客户端,那么客户端也要bind创建自己的sock文件.(供服务端标识发给谁)
长度固定为99,是因为客户端sendto函数传参为固定99,而应该传 strlen(buf)+1,这样才会显示内容长度
第三方库 第三方库的安装方式参考libevent库的安装方式
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 #include <event2/event.h>
编译源代码文件(以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函数 获得event_base结构
1 2 3 4 struct event_base *event_base_new (void ) ; 返回值: 成功返回event_base结构体指针; 失败返回NULL ;
event_base_free函数 释放event_base指针
1 void event_base_free (struct event_base *) ;
event_reinit函数 如果有子进程, 且子进程也要使用base, 则子进程需要对event_base重新初始化, 此时需要调用event_reinit函数.
1 2 3 4 5 6 int event_reinit (struct event_base *base) ; 函数参数: 由event_base_new返回的执行event_base结构的指针 返回值: 成功返回0 , 失败返回-1
event_get_supported_methods函数 得当前系统(或者称为平台)支持的方法有哪些
1 2 3 const char **event_get_supported_methods (void ) ;返回值: 返回二维数组, 类似与main函数的第二个参数**argv.
对于不同系统而言, event_base就是调用不同的多路IO接口去判断事件是否已经被激活, 对于linux系统而言, 核心调用的就是epoll, 同时支持poll和select.
编写代码获得当前系统支持的多路IO方法和当前所使用的方法 :
linux下打印如下: epoll poll select
event_base_get_method函数 获得当前base节点使用的多路io方法
1 2 3 4 5 const char * event_base_get_method (const struct event_base *base) ;函数参数: event_base结构的base指针. 返回值: 获得当前base节点使用的多路io方法的指针
event_base_loop函数 进入循环等待事件(该函数一般不用,用event_base_dispatch 代替)
1 int event_base_loop (struct event_base *base, int flags) ;
参数说明:
event_base_dispatch函数 进入循环等待事件(阻塞)
1 2 3 int event_base_dispatch (struct event_base *base) ; 函数参数: event_base结构的base指针.
调用该函数, 相当于没有设置标志位的event_base_loop。程序将会一直运行, 直到没有需要检测的事件了, 或者被结束循环的API终止。
event_base_loopexit函数 退出循环等待(回调执行结束后 终止循环,也就是等待处理完再终止)
1 2 3 4 5 6 7 8 int event_base_loopexit (struct event_base *base, const struct timeval *tv) ;函数参数: event_base结构的base指针. tv为timeval结构体指针(如下) struct timeval { long tv_sec; long tv_usec; };
如果正在执行激活事件的回调函数, 那么event_base_loopexit将在事件回调执行结束后终止循环(如果tv时间非NULL, 那么将等待tv设置的时间后立即结束循环), 而event_base_loopbreak会立即终止循环。
event_base_loopbreak函数 退出循环等待(立即终止循环)
1 2 3 int event_base_loopbreak (struct event_base *base) ;函数参数: 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 2 3 4 5 6 struct event *event_new ( struct event_base *base, evutil_socket_t fd, short events, event_callback_fn cb, void *arg) ;
base: 对应的根节点–地基
fd: 要监听的文件描述符,(或信号)
events:要监听的事件
1 2 3 4 5 6 7 #define EV_TIMEOUT 0x01 #define EV_READ 0x02 #define EV_WRITE 0x04 #define EV_SIGNAL 0x08 #define EV_PERSIST 0x10 #define EV_ET 0x20
cb 回调函数, 原型如下:
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> #include <event2/event.h> struct event * connevs [100] = { NULL };void readcb (evutil_socket_t fd, short events, void *arg) { int n; char buf[1024 ]; memset (buf, 0x00 , sizeof (buf)); n = read(fd, buf, sizeof (buf)); if (n<=0 ) { printf ("客户端断开连接\n" ); close(fd); event_del(connevs[fd]); } else { printf ("读到[%d]%s\n" ,n,buf); } } void conncb (evutil_socket_t fd, short events, void *arg) { struct event_base *base = (struct event_base *)arg; int cfd = accept(fd, NULL , NULL ); if (cfd>0 ) { printf ("客户端连接成功\n" ); connevs[cfd] = event_new(base, cfd, EV_READ|EV_PERSIST, readcb, NULL ); if (connevs[cfd]==NULL ) { event_base_loopexit(base, NULL ); } event_add(connevs[cfd], NULL ); } } int main () { int lfd = socket(AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); struct sockaddr_in serv ; bzero(&serv, sizeof (serv)); serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(8888 ); serv.sin_family = AF_INET; bind(lfd, (struct sockaddr*)&serv, sizeof (serv)); listen(lfd, 120 ); struct event_base *base = event_base_new(); if (base==NULL ) { printf ("event_base_new error\n" ); return -1 ; } printf ("[支持的方式:" ); const char **p = event_get_supported_methods(); int i = 0 ; while (p[i]!=NULL ) { printf ("[%s] " , p[i++]); } printf ("]\n" ); printf ("[使用的方式:%s]\n" , event_base_get_method(base)); struct event *ev = event_new(base, lfd, EV_READ|EV_PERSIST, conncb, base); if (ev==NULL ) { printf ("event_new error\n" ); return -1 ; } event_add(ev, NULL ); printf ("等待连接中...\n" ); event_base_dispatch(base); event_base_free(base); event_free(ev); close(lfd); return 0 ; }
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 2 3 4 struct bufferevent *bufferevent_socket_new ( struct event_base *base, evutil_socket_t fd, int options) ;
base :对应根节点
fd :文件描述符
options : bufferevent的选项
BEV_OPT_CLOSE_ON_FREE– 释放bufferevent自动关闭底层接口(当bufferevent被释放以后, 文件描述符也随之被close )
BEV_OPT_THREADSAFE – 使bufferevent能够在多线程下是安全的
bufferevent_socket_connect函数 该函数客户端使用 ,封装了底层的socket与connect接口, 通过调用此函数, 可以将bufferevent事件与通信的socket进行绑定
1 2 3 4 int bufferevent_socket_connect ( struct bufferevent *bev, struct sockaddr *serv, int socklen) ;
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 2 3 4 5 6 void bufferevent_setcb ( struct bufferevent *bufev, bufferevent_data_cb readcb, bufferevent_data_cb writecb, bufferevent_event_cb eventcb, void *cbarg) ;
readcb, writecb, eventcb分别对应了读回调, 写回调, 事件回调
cbarg代表回调函数的参数。
两种回调函数的函数原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef void (*bufferevent_data_cb) (struct bufferevent *bev, void *ctx) ;typedef void (*bufferevent_event_cb) (struct bufferevent *bev, short what, void *ctx) ;
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里的读缓冲区指针
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 2 3 4 5 6 7 8 struct evconnlistener* evconnlistener_new_bind ( struct event_base *base, evconnlistener_cb cb, void *ptr, unsigned flags, int backlog, const struct sockaddr *sa, int socklen) ;
最后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 2 3 4 5 6 7 struct evconnlistener *evconnlistener_new ( struct event_base *base, evconnlistener_cb cb, void *ptr, unsigned flags, int backlog, evutil_socket_t fd) ;
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> #include <event2/bufferevent.h> #include <event2/buffer.h> #include <event2/listener.h> #include <event2/util.h> #include <event2/event.h> void acceptClient2Str (struct sockaddr_in* client,char * str) { char sip[16 ]; memset (sip,0 ,sizeof (sip)); sprintf (str,"%s:%hu" ,inet_ntop(AF_INET,&client->sin_addr.s_addr,sip,sizeof (sip)),ntohs(client->sin_port)); } void read_cb (struct bufferevent *bev, void *ctx) { char str[99 ]={0 }; int n =bufferevent_read(bev,str,99 ); printf ("读到:<%d>%s\n" ,n,str); } void event_cb (struct bufferevent *bev, short what, void *ctx) { if (what & BEV_EVENT_EOF) { printf ("有客户端断开连接\n" ); } } void listener_cb (struct evconnlistener *evl, evutil_socket_t fd, struct sockaddr *cliaddr, int socklen, void *ptr){ char str[16 ]={0 }; acceptClient2Str(cliaddr,str); printf ("客户端[%s]连接成功,占用%d描述符\n" ,str,fd); struct event_base *base = ptr; struct bufferevent *bev = bufferevent_socket_new(base,fd,BEV_OPT_CLOSE_ON_FREE); if (!bev) { printf ("Error constructing bufferevent!\n" ); event_base_loopbreak(base); return ; } bufferevent_setcb(bev,read_cb,NULL ,event_cb,NULL ); bufferevent_enable(bev,EV_READ); } int main () { struct sockaddr_in serv ; bzero(&serv, sizeof (serv)); serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(23456 ); serv.sin_family = AF_INET; struct event_base *base = event_base_new(); if (base==NULL ) { printf ("event_base_new error\n" ); return -1 ; } struct evconnlistener * listener = evconnlistener_new_bind(base,listener_cb,base, LEV_OPT_CLOSE_ON_FREE|LEV_OPT_REUSEABLE,-1 ,&serv,sizeof (serv)); printf ("等待连接中...\n" ); event_base_dispatch(base); printf ("跳出循环\n" ); evconnlistener_free(listener); event_base_free(base); return 0 ; }
客户端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> #include <event2/bufferevent.h> #include <event2/buffer.h> #include <event2/listener.h> #include <event2/util.h> #include <event2/event.h> void acceptClient2Str (struct sockaddr_in* client,char * str) { char sip[16 ]; memset (sip,0 ,sizeof (sip)); sprintf (str,"%s:%hu" ,inet_ntop(AF_INET,&client->sin_addr.s_addr,sip,sizeof (sip)),ntohs(client->sin_port)); } void cmd_msg_cb (evutil_socket_t fd, short events, void *arg) { char msg[1024 ]; int ret = read(fd,msg,sizeof (msg)); if (ret<0 ) { perror("read fail" ); exit (1 ); } struct bufferevent * bev = arg; bufferevent_write(bev,msg,ret); } void server_msg_cb (struct bufferevent *bev, void *ctx) { char msg[1024 ]; int n =bufferevent_read(bev,msg,sizeof (msg)); msg[n] = '\0' ; if (n>0 ) { printf ("[%d]%s\n" ,n,msg); } } void event_cb (struct bufferevent *bev, short what, void *ctx) { if (what & BEV_EVENT_EOF) printf ("连接断开\n" ); else if (what & BEV_EVENT_ERROR) printf ("发生错误\n" ); else if (what & BEV_EVENT_CONNECTED) { printf ("连接成功\n" ); return ; } bufferevent_free(bev); struct event *ev = (struct event*)ctx; event_free(ev); } int main (int argc,char ** argv) { if (argc<3 ) printf ("please input 2 parameter\n" ); struct event_base * base = event_base_new(); struct bufferevent * bev = bufferevent_socket_new(base,-1 ,BEV_OPT_CLOSE_ON_FREE); struct event * ev_cmd = event_new(base,STDIN_FILENO,EV_READ | EV_PERSIST,cmd_msg_cb,bev); event_add(ev_cmd,NULL ); struct sockaddr_in serv ; serv.sin_family = AF_INET; serv.sin_port = htons(atoi(argv[2 ])); inet_aton(argv[1 ],&serv.sin_addr.s_addr); bufferevent_socket_connect(bev,&serv,sizeof (serv)); bufferevent_setcb(bev,server_msg_cb,NULL ,event_cb,(void *)ev_cmd); bufferevent_enable(bev,EV_READ | EV_PERSIST); event_base_dispatch(base); printf ("跳出循环\n" ); event_base_free(base); event_free(ev_cmd); bufferevent_free(bev); return 0 ; }
通信效率优化 通信效率:单位时间内客户端或者服务端接收或者发送数据的量.
多线程处理效率优于多进程 (多线程更节省资源)
多路IO复用技术 :既不使用多线程,也不使用多进程,在一个进程或一个线程中让多个客户端同时请求服务(都是委托内核进行监控,若有事件发生则通知应用程序)
客户端优化 客户端的效率优化
单进程只处理一个连接
多线程使用同一个连接
多线程使用多个连接 (使用到连接池)
缺点:如果频繁创建连接和销毁连接会有时间消耗
连接池+线程池(避免上述缺点)
连接池只用于客户端
连接池不用于服务端,因为连接是只有客户端发起连接请求之后才会有的.
连接池思想
一个数据结构保存连接信息
创建连接池操作—poolInit()
获取连接池操作 (如队头取)
使用完将连接存放的操作 (如存到队尾)
可以根据实际需要动态调整连接的数量
销毁连接池
在网络编程中,connect()函数用于建立与远程主机的连接。当connect()函数返回-1时,并且errno被设置为EINPROGRESS,表示连接操作正在进行中,但是仍未完成。
这种情况通常发生在非阻塞模式下调用connect()函数时。在非阻塞模式下,connect()函数立即返回,不会阻塞程序,同时设置errno为EINPROGRESS。此时需要通过select()或poll()等函数检查套接字的可写性,以确定连接是否已经建立。如果连接成功建立,套接字将变成可写状态;如果连接建立失败,则套接字将变成可读状态,并且errno被设置为相应的错误码。
需要注意的是,在阻塞模式下调用connect()函数,如果连接建立失败,connect()函数将会阻塞程序并返回相应的错误码,不会设置errno为EINPROGRESS。
客户端案例 TcpClient.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #pragma once static const int TIMEOUT = 1000 ;class TcpClient { public : enum ErrorType { ParamError = 3001 , TimeoutError, PeerCloseError, MallocError }; TcpClient (); TcpClient (int connfd); ~TcpClient (); int connectToHost (char *ip, unsigned short port, int timeout = TIMEOUT) ; int sendMsg (char *sendData, int dataLen, int timeout = TIMEOUT) ; int recvMsg (char **recvData, int &recvLen, int timeout = TIMEOUT) ; void disConnect () ; void freeMemory (char **buf) ; private : int blockIO (int fd) ; int noBlockIO (int fd) ; int readTimeout (unsigned int wait_seconds) ; int writeTimeout (unsigned int wait_seconds) ; int connectTimeout (struct sockaddr_in *addr, unsigned int wait_seconds) ; int readn (void *buf, int count) ; int writen (const void *buf, int count) ; private : int m_fd; };
TcpClient.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 #include "TcpClient.h" #include <iostream> #include <fcntl.h> #include <arpa/inet.h> using namespace std;#include <unistd.h> #include <string.h> TcpClient::TcpClient () { } TcpClient::TcpClient (int connfd) : m_fd (connfd) { } TcpClient::~TcpClient () { } int TcpClient::connectToHost (char *ip, unsigned short port, int timeout) { int ret = 0 ; if (ip == NULL || port <= 0 || port > 65535 || timeout < 0 ) { ret = ParamError; return ret; } m_fd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); if (m_fd < 0 ) { ret = errno; printf ("func socket() err: %d\n" , ret); return ret; } sockaddr_in serAddr; serAddr.sin_family = AF_INET; serAddr.sin_port = htons (port); serAddr.sin_addr.s_addr = inet_addr (ip); ret = connectTimeout (&serAddr, timeout); if (ret < 0 ) { if (ret == -1 && errno == ETIMEDOUT) { ret = TimeoutError; return ret; } else std::cout << "func connect_timeout() err:" << errno << std::endl; } return ret; } int TcpClient::sendMsg (char *sendData, int dataLen, int timeout) { int ret = 0 ; if (sendData == NULL || dataLen <= 0 ) { ret = ParamError; return ret; } ret = writeTimeout (timeout); if (ret == 0 ) { unsigned char *netdata = (unsigned char *)malloc (dataLen + 4 ); if (netdata == NULL ) { ret = MallocError; std::cout << "func Client_sendMsg() err:" << ret << std::endl; return ret; } int netlen = htonl (dataLen); memcpy (netdata, &netlen, 4 ); memcpy (netdata + 4 , sendData, dataLen); writen (netdata, dataLen + 4 ); if (netdata != NULL ) { free (netdata); netdata = NULL ; } } if (ret < 0 ) { if (ret == -1 && errno == ETIMEDOUT) { ret = TimeoutError; printf ("func sckClient_send() mlloc Err:%d\n " , ret); return ret; } return ret; } return ret; } int TcpClient::recvMsg (char **recvData, int &recvLen, int timeout) { int ret = 0 ; if (recvData == NULL || recvLen == 0 ) { ret = ParamError; printf ("func sckClient_rev() timeout , err:%d \n" , TimeoutError); return ret; } ret = readTimeout (timeout); if (ret != 0 ) { if (ret == -1 || errno == ETIMEDOUT) { ret = TimeoutError; return ret; } else { return ret; } } int netdatalen = 0 ; ret = readn (&netdatalen, 4 ); if (ret == -1 ) { return ret; } else if (ret < 4 ) { ret = PeerCloseError; return ret; } int n = ntohl (netdatalen); char *tmpBuf = (char *)malloc (n + 1 ); if (tmpBuf == NULL ) { ret = MallocError; return ret; } ret = readn (tmpBuf, n); if (ret == -1 ) { return ret; } else if (ret < n) { ret = PeerCloseError; return ret; } *recvData = tmpBuf; recvLen = n; tmpBuf[n] = '\0' ; return 0 ; } void TcpClient::disConnect () { if (m_fd >= 0 ) { close (m_fd); } } void TcpClient::freeMemory (char **buf) { if (*buf != NULL ) { free (*buf); *buf = NULL ; } } int TcpClient::writeTimeout (unsigned int wait_seconds) { int ret = 0 ; if (wait_seconds > 0 ) { fd_set write_fdset; struct timeval timeout; FD_ZERO (&write_fdset); FD_SET (m_fd, &write_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0 ; do { ret = select (m_fd + 1 , NULL , &write_fdset, NULL , &timeout); } while (ret < 0 && errno == EINTR); if (ret == 0 ) { ret = -1 ; errno = ETIMEDOUT; } else if (ret == 1 ) ret = 0 ; } return ret; } int TcpClient::connectTimeout (sockaddr_in *addr, unsigned int wait_seconds) { int ret; if (wait_seconds > 0 ) blockIO (m_fd); ret = connect (m_fd, (struct sockaddr *)addr, sizeof (struct sockaddr_in)); if (ret < 0 && errno == EINPROGRESS) { fd_set connect_fdset; struct timeval timeout; FD_ZERO (&connect_fdset); FD_SET (m_fd, &connect_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0 ; do { ret = select (m_fd + 1 , NULL , &connect_fdset, NULL , &timeout); } while (ret < 0 && errno == EINTR); if (ret == 0 ) { ret = -1 ; errno = ETIMEDOUT; } else if (ret < 0 ) { return -1 ; std::cout << "select err" << std::endl; } else if (ret == 1 ) { int err; socklen_t socklen = sizeof (err); int sockoptret = getsockopt (m_fd, SOL_SOCKET, SO_ERROR, &err, &socklen); if (sockoptret == -1 ) { std::cout << "getsockopt err" << std::endl; return -1 ; } if (err == 0 ) ret = 0 ; else { errno = err; ret = -1 ; } } } if (wait_seconds > 0 ) { noBlockIO (m_fd); } return ret; } int TcpClient::readn (void *buf, int count) { size_t nleft = count; ssize_t nread; char *bufp = (char *)buf; while (nleft > 0 ) { if ((nread = read (m_fd, bufp, nleft)) < 0 ) { if (errno == EINTR) continue ; return -1 ; } else if (nread == 0 ) return count - nleft; bufp += nread; nleft -= nread; } return count; } int TcpClient::writen (const void *buf, int count) { size_t nleft = count; ssize_t nwritten; char *bufp = (char *)buf; while (nleft > 0 ) { if ((nwritten = write (m_fd, bufp, nleft)) < 0 ) { if (errno == EINTR) continue ; return -1 ; } else if (nwritten == 0 ) continue ; bufp += nwritten; nleft -= nwritten; } return count; } int TcpClient::blockIO (int fd) { int flags = fcntl (fd, F_GETFL); if (flags == -1 ) { return -1 ; } flags |= O_NONBLOCK; if (fcntl (fd, F_SETFL, flags) == -1 ) { return -1 ; } return 0 ; } int TcpClient::noBlockIO (int fd) { int flags = fcntl (fd, F_GETFL); if (flags == -1 ) { return -1 ; } flags &= ~O_NONBLOCK; if (fcntl (fd, F_SETFL, flags) == -1 ) { return -1 ; } return 0 ; } int TcpClient::readTimeout (unsigned int wait_seconds) { int ret = 0 ; if (wait_seconds > 0 ) { fd_set read_fdset; struct timeval timeout; FD_ZERO (&read_fdset); FD_SET (m_fd, &read_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0 ; do { ret = select (m_fd + 1 , &read_fdset, NULL , NULL , &timeout); } while (ret < 0 && errno == EINTR); if (ret == 0 ) { ret = -1 ; errno = ETIMEDOUT; } else if (ret == 1 ) ret = 0 ; } return ret; }
服务端优化 服务端案例 需配合客户端案例 使用
TcpServer.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #pragma once #include <iostream> using namespace std;#include "TcpClient.h" #include <arpa/inet.h> class TcpServer { public : TcpServer (); ~TcpServer (); int setListen (unsigned short port) ; TcpClient* acceptConn (int timeout = 10000 ) ; void closefd () ; private : int acceptTimeout (int wait_seconds) ; private : int m_lfd; struct sockaddr_in m_addrCli; };
TcpServer.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 #include "TcpServer.h" #include <string.h> #include <unistd.h> TcpServer::TcpServer () { } TcpServer::~TcpServer () { } int TcpServer::setListen (unsigned short port) { int ret = 0 ; struct sockaddr_in servaddr; memset (&servaddr, 0 , sizeof (struct sockaddr_in)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (port); servaddr.sin_addr.s_addr = htonl (INADDR_ANY); m_lfd = socket (PF_INET, SOCK_STREAM, 0 ); if (m_lfd < 0 ) return errno; int on = 1 ; ret = setsockopt (m_lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on)); if (ret < 0 ) return errno; ret = bind (m_lfd, (const sockaddr *)&servaddr, sizeof (struct sockaddr_in)); if (ret < 0 ) return errno; ret = listen (m_lfd, SOMAXCONN); if (ret < 0 ) return errno; return ret; } TcpClient *TcpServer::acceptConn (int timeout) { int connfd = acceptTimeout (timeout); if (connfd < 0 ) { if (connfd == -1 && errno == ETIMEDOUT) { printf ("func accept_timeout() timeout err \n" ); } else { std::cout << "acceptConn err" << std::endl; } return NULL ; } return new TcpClient (connfd); } void TcpServer::closefd () { close (m_lfd); } int TcpServer::acceptTimeout (int wait_seconds) { int ret; socklen_t addrlen = sizeof (struct sockaddr_in); if (wait_seconds > 0 ) { fd_set accept_fdset; struct timeval timeout; FD_ZERO (&accept_fdset); FD_SET (m_lfd, &accept_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0 ; do { std::cout << wait_seconds << std::endl; ret = select (m_lfd + 1 , &accept_fdset, NULL , NULL , &timeout); } while (ret < 0 && errno == EINTR); if (ret == -1 ) return -1 ; else if (ret == 0 ) { errno = ETIMEDOUT; return -1 ; } } ret = accept (m_lfd, (struct sockaddr *)&m_addrCli, &addrlen); if (ret == -1 ) { ret = errno; return ret; } return ret; }
web服务器 能够解析http协议的软件的电脑
http协议
1 2 3 4 5 6 7 8 9 10 11 请求行 -> 请求方式(get/post) 请求url地址 协议 请求头 -> 放一写服务器要使用的附加信息 请求体 -> 一般放一写请求参数 状态行 -> 协议 状态码 响应头 -> 放一些客户端要使用的附加信息 响应体 ->服务器返回的真正客户端要用的内容(HTML,json)等
http请求消息 我们要开发的服务器与浏览器通信采用的就是http协议,在浏览器想访问一个资源的时候,在浏览器输入访问地址(例如http://127.0.0.1:8000),地址输入完成后当敲击回车键的时候,浏览器就将请求消息发送给服务器
服务器socket收到的数据如下:
这个消息看起来很乱很复杂,对应的就是我们说的请求消息. 请求消息分为四部分内容:
请求行 说明请求类型 ,要访问的资源 (请求指令或请求文件等资源,如果是get类型,问号后还会带数据),以及使用的http版本
请求头 说明服务器使用的附加信息,都是键值对 ,比如表明浏览器类型
空行 不能省略-而且是\r\n,包括请求行和请求头中每一行都是以\r\n结尾(\r是移动到末尾,\n是换行)
请求体(请求数据) 表明请求的特定数据内容,可以省略-如登陆时,会将用户名和密码内容作为请求数据
1 2 3 4 5 6 7 8 9 GET / HTTP/1.1 Host : xxx.xxx.xxx.xxx:23456Connection : keep-aliveCache-Control : max-age=0Upgrade-Insecure-Requests : 1User-Agent : Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7
请求类型 http协议有很多种请求类型,对我们来说常见的用的最多的是get和post请求。常见的请求类型如下:
Get 请求指定的页面信息,请求数据被包含到url中 ,并返回实体主体.在http协议中数据被包含在请求行的要访问资源中
由于get请求不存在请求体,所以他的请求头中没有Content-Length和Content-Type键值对.
Post 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体(请求数据)中 。POST请求可能会导致新的资源的建立和/或已有资源的修改。
Head 类似于get请求,但是响应消息没有内容,只是获得报头
Put 从客户端向浏览器传送的数据取代指定的文档内容
Delete 请求服务器删除指定的页面
Connect HTTP/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/json json数据
…
Content-Length :表示请求体(请求数据)的长度
Content-Type
Http协议规定POST提交的数据必须放在消息主题(entity-body)中,但协议并没有规定数据必须使用什么编码方式
开发者可以自己决定消息主题的格式
数据发送出去,还要服务端解析成功才有意义,服务端通常是根据请求头中的Content-Type字段来获知请求中的消息主体是用何种方式编码,再对主体进行解析
Content-Type类型盘点跳转
Content-Type四种常用方式
上面的 ..........具体的文件内容..........指代具体要传的文件内容
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 2 3 4 5 GET /example HTTP/1.1 Host: http://www.example.com User-Agent: Mozilla/5.0 Accept: text/html Cookie: key1=value1; key2=value2
在上面的示例中,Cookie字段包含了两个cookie键值对:key1=value1和key2=value2。这些cookie将会被发送到服务器,以便服务器可以识别和跟踪用户的会话。
以python为例:
1 2 3 4 5 6 7 8 9 10 import requestsresponse = requests.get('http://www.example.com' ) cookies = response.cookies.get_dict() response2 = requests.get('http://www.example.com' , cookies=cookies) print (response2.text)
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加密进化的动画解说
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开发的主体流程:
参考epoll模型跳转
基于select的web服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include "wrap.h" #include <sys/stat.h> #include <fcntl.h> #include <dirent.h> int hexit (char c) { if (c >= '0' && c <= '9' ) return c - '0' ; if (c >= 'a' && c <= 'f' ) return c - 'a' + 10 ; if (c >= 'A' && c <= 'F' ) return c - 'A' + 10 ; return 0 ; } #include <ctype.h> void strdecode (char * to,char * from) { for (;*from != '\0' ;++to,++from){ if (from[0 ]=='%' && isxdigit (from[1 ]) && isxdigit (from[2 ])) { *to = hexit(from[1 ])*16 +hexit(from[2 ]); from+=2 ; }else *to = *from; } *to = NULL ; } char *get_mime_type (char *name) { char * dot; dot = strrchr (name, '.' ); if (dot == (char *)0 ) return "text/plain; charset=utf-8" ; if (strcmp (dot, ".html" ) == 0 || strcmp (dot, ".htm" ) == 0 ) return "text/html; charset=utf-8" ; if (strcmp (dot, ".jpg" ) == 0 || strcmp (dot, ".jpeg" ) == 0 ) return "image/jpeg" ; if (strcmp (dot, ".gif" ) == 0 ) return "image/gif" ; if (strcmp (dot, ".png" ) == 0 ) return "image/png" ; if (strcmp (dot, ".css" ) == 0 ) return "text/css" ; if (strcmp (dot, ".au" ) == 0 ) return "audio/basic" ; if (strcmp ( dot, ".wav" ) == 0 ) return "audio/wav" ; if (strcmp (dot, ".avi" ) == 0 ) return "video/x-msvideo" ; if (strcmp (dot, ".mov" ) == 0 || strcmp (dot, ".qt" ) == 0 ) return "video/quicktime" ; if (strcmp (dot, ".mpeg" ) == 0 || strcmp (dot, ".mpe" ) == 0 ) return "video/mpeg" ; if (strcmp (dot, ".vrml" ) == 0 || strcmp (dot, ".wrl" ) == 0 ) return "model/vrml" ; if (strcmp (dot, ".midi" ) == 0 || strcmp (dot, ".mid" ) == 0 ) return "audio/midi" ; if (strcmp (dot, ".mp3" ) == 0 ) return "audio/mpeg" ; if (strcmp (dot, ".ogg" ) == 0 ) return "application/ogg" ; if (strcmp (dot, ".pac" ) == 0 ) return "application/x-ns-proxy-autoconfig" ; return "text/plain; charset=utf-8" ; } int sendHeader (int cfd,char * code,char *msg,char * fileType,int len) { char buf[1024 ]={0 }; sprintf (buf,"HTTP/1.1 %s %s\r\n" ,code,msg); sprintf (buf+strlen (buf),"Content-Type:%s\r\n" ,fileType); if (len>0 ) { sprintf (buf+strlen (buf),"Content-Length:%d\r\n" ,len); } strcat (buf,"\r\n" ); Write(cfd,buf,strlen (buf)); return 0 ; } int sendFile (int cfd,char * fileName) { int fd = open(fileName,O_RDONLY); if (fd<0 ) { perror("open error" ); return -1 ; } char buf[1024 ]={0 }; while (1 ) { int n = read(fd,buf,sizeof (buf)); if (n<=0 ) { break ; } else { Write(cfd,buf,n); } } close(fd); } int httpRequest (int cfd,fd_set* setReal) { char buf[1024 ]={0 }; int n = Readline(cfd,buf,sizeof (buf)); if (n<=0 ) { printf ("客户端已断开连接\n" ); close(cfd); FD_CLR(cfd,setReal); return -1 ; } char requestType[16 ]={0 }; char fileName[255 ]={0 }; char protocal[16 ]={0 }; sscanf (buf,"%[^ ] %[^ ] %[^ \r\n]" ,requestType,fileName+1 ,protocal); if (strlen (fileName+1 )<=1 ) strcpy (fileName,"./" ); else fileName[0 ] = '.' ; strdecode(fileName,fileName); printf ("requestType==[%s]\n" ,requestType); printf ("fileName==[%s]\n" ,fileName); printf ("protocal==[%s]\n" ,protocal); while ((n=Readline(cfd,buf,sizeof (buf))>0 )); struct stat st ; if (stat(fileName,&st)<0 ) { printf ("文件不存在\n" ); sendHeader(cfd,"404" ,"NOT FOUND" ,get_mime_type(".html" ),0 ); sendFile(cfd,"error.html" ); printf ("发送错误页成功\n" ); } else { if (S_ISREG(st.st_mode)) { printf ("文件存在\n" ); sendHeader(cfd, "200" , "OK" , get_mime_type(fileName), st.st_size); sendFile(cfd, fileName); } else if (S_ISDIR(st.st_mode)) { printf ("这是目录文件\n" ); sendHeader(cfd, "200" , "OK" , get_mime_type(".html" ), 0 ); sendFile(cfd, "head.html" ); struct dirent **namelist ; int num = scandir(fileName, &namelist, NULL , alphasort); if (num < 0 ) { perror("scandir" ); close(cfd); FD_CLR(cfd,setReal); return -1 ; } else { char buffer[1024 ] = {0 }; while (num--) { if (namelist[num]->d_type==DT_DIR) sprintf (buffer+strlen (buffer),"<li><a href=%s/> %s </a></li>" ,namelist[num]->d_name,namelist[num]->d_name); else sprintf (buffer+strlen (buffer),"<li><a href=%s> %s </a></li>" ,namelist[num]->d_name,namelist[num]->d_name); free (namelist[num]); } Write(cfd,buffer,strlen (buffer)); free (namelist); } sendFile(cfd, "tail.html" ); } } } int main (int argc,char ** argv) { struct sigaction act ; act.sa_handler = SIG_IGN; sigemptyset(&act.sa_mask); act.sa_flags = 0 ; sigaction(SIGPIPE,&act,NULL ); fd_set setReal; int lfd = Socket(AF_INET,SOCK_STREAM,0 ); struct sockaddr_in serv ; serv.sin_family = AF_INET; serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(23456 ); int opt = 1 ; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (int )); Bind(lfd,(struct sockaddr*)&serv,sizeof (serv)); Listen(lfd,2 ); int maxfd = lfd; FD_ZERO(&setReal); FD_SET(lfd,&setReal); printf ("服务器开启,等待连接\n" ); while (1 ) { fd_set setR = setReal; int fdChangeNum = select(maxfd+1 ,&setR,NULL ,NULL ,NULL ); if (fdChangeNum<0 ) { if (errno==EINTR) { continue ; } break ; } if (FD_ISSET(lfd,&setR)) { int cfd = accept(lfd,NULL ,NULL ); printf ("接受到新的连接,文件描述符为%d\n" ,cfd); int flag = fcntl(cfd,F_GETFL); flag |= O_NONBLOCK; fcntl(cfd,F_SETFL,flag); FD_SET(cfd,&setReal); if (maxfd<cfd) { maxfd = cfd ; } if (--fdChangeNum==0 ) { continue ; } } char str[99 ]={0 }; for (int i = lfd+1 ; i <= maxfd; i++) { if (FD_ISSET(i,&setR)) { printf ("数据通信存在,返回值为:%d\n" ,fdChangeNum); httpRequest(i,&setReal); printf ("请求完毕\n\n\n" ); } } } close(lfd); return 0 ; }
中文汉字编码问题 举例:浏览器请求的是localhost:8080/苦瓜.txt ,web服务器收到之后会显示: %E8%8B%A6%E7%93%9C.txt,需要进行转换,utf-8格式的一个中文汉字占3个字节,首先需要去掉文件名中的%,然后逐个将字符串形式的(例如E8)转换成数值,然后保存到数组当中.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int hexit (char c) { if (c >= '0' && c <= '9' ) return c - '0' ; if (c >= 'a' && c <= 'f' ) return c - 'a' + 10 ; if (c >= 'A' && c <= 'F' ) return c - 'A' + 10 ; return 0 ; } #include <ctype.h> void strdecode (char * to,char * from) { for (;*from != '\0' ;++to,++from){ if (from[0 ]=='%' && isxdigit (from[1 ]) && isxdigit (from[2 ])) { *to = hexit(from[1 ])*16 +hexit(from[2 ]); from+=2 ; }else *to = *from; } *to = NULL ; }
调用 strdecode 把代表unicode的字符串变成真正的unicode编码(其实就是16进制存储)
一些特殊符号 相关知识
html中的 空格 (space)是 
%20 URL编码中的 空格(space)
%21 — !
%22 — "
%23 — #
%24 — $
%25 — %
%26 — &
%27 — '
%28 — (
基于libevent的web服务器 参考libevent库讲解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> #include <event2/bufferevent.h> #include <event2/buffer.h> #include <event2/listener.h> #include <event2/util.h> #include <event2/event.h> #include <fcntl.h> #include <dirent.h> #include <sys/types.h> #include <sys/stat.h> int hexit (char c) { if (c >= '0' && c <= '9' ) return c - '0' ; if (c >= 'a' && c <= 'f' ) return c - 'a' + 10 ; if (c >= 'A' && c <= 'F' ) return c - 'A' + 10 ; return 0 ; } #include <ctype.h> void strdecode (char * to,char * from) { for (;*from != '\0' ;++to,++from){ if (from[0 ]=='%' && isxdigit (from[1 ]) && isxdigit (from[2 ])) { *to = hexit(from[1 ])*16 +hexit(from[2 ]); from+=2 ; }else *to = *from; } *to = NULL ; } char *get_mime_type (char *name) { char * dot; dot = strrchr (name, '.' ); if (dot == (char *)0 ) return "text/plain; charset=utf-8" ; if (strcmp (dot, ".html" ) == 0 || strcmp (dot, ".htm" ) == 0 ) return "text/html; charset=utf-8" ; if (strcmp (dot, ".jpg" ) == 0 || strcmp (dot, ".jpeg" ) == 0 ) return "image/jpeg" ; if (strcmp (dot, ".gif" ) == 0 ) return "image/gif" ; if (strcmp (dot, ".png" ) == 0 ) return "image/png" ; if (strcmp (dot, ".css" ) == 0 ) return "text/css" ; if (strcmp (dot, ".au" ) == 0 ) return "audio/basic" ; if (strcmp ( dot, ".wav" ) == 0 ) return "audio/wav" ; if (strcmp (dot, ".avi" ) == 0 ) return "video/x-msvideo" ; if (strcmp (dot, ".mov" ) == 0 || strcmp (dot, ".qt" ) == 0 ) return "video/quicktime" ; if (strcmp (dot, ".mpeg" ) == 0 || strcmp (dot, ".mpe" ) == 0 ) return "video/mpeg" ; if (strcmp (dot, ".vrml" ) == 0 || strcmp (dot, ".wrl" ) == 0 ) return "model/vrml" ; if (strcmp (dot, ".midi" ) == 0 || strcmp (dot, ".mid" ) == 0 ) return "audio/midi" ; if (strcmp (dot, ".mp3" ) == 0 ) return "audio/mpeg" ; if (strcmp (dot, ".ogg" ) == 0 ) return "application/ogg" ; if (strcmp (dot, ".pac" ) == 0 ) return "application/x-ns-proxy-autoconfig" ; return "text/plain; charset=utf-8" ; } int sendHeader (struct bufferevent * bev,char * code,char *msg,char * fileType,int len) { char buf[1024 ]={0 }; sprintf (buf,"HTTP/1.1 %s %s\r\n" ,code,msg); sprintf (buf+strlen (buf),"Content-Type:%s\r\n" ,fileType); if (len>0 ) { sprintf (buf+strlen (buf),"Content-Length:%d\r\n" ,len); } strcat (buf,"\r\n" ); bufferevent_write(bev,buf,strlen (buf)); return 0 ; } int sendFile (struct bufferevent * bev,char * fileName) { int fd = open(fileName,O_RDONLY); if (fd<0 ) { perror("open error" ); return -1 ; } char buf[1024 ]={0 }; while (1 ) { int n = read(fd,buf,sizeof (buf)); if (n<=0 ) { break ; } else { bufferevent_write(bev,buf,n); } } close(fd); } int httpRequest (struct bufferevent * bev) { char buf[1024 ]={0 }; int n =bufferevent_read(bev,buf,sizeof (buf)); char requestType[16 ]={0 }; char fileName[255 ]={0 }; char protocal[16 ]={0 }; sscanf (buf,"%[^ ] %[^ ] %[^ \r\n]" ,requestType,fileName+1 ,protocal); if (strlen (fileName+1 )<=1 ) strcpy (fileName,"./" ); else fileName[0 ] = '.' ; strdecode(fileName,fileName); printf ("requestType==[%s]\n" ,requestType); printf ("fileName==[%s]\n" ,fileName); printf ("protocal==[%s]\n" ,protocal); while ((n =bufferevent_read(bev,buf,sizeof (buf)))); struct stat st ; if (stat(fileName,&st)<0 ) { printf ("文件不存在\n" ); sendHeader(bev,"404" ,"NOT FOUND" ,get_mime_type(".html" ),0 ); sendFile(bev,"error.html" ); printf ("发送错误页成功\n" ); } else { if (S_ISREG(st.st_mode)) { printf ("文件存在\n" ); sendHeader(bev, "200" , "OK" , get_mime_type(fileName), st.st_size); sendFile(bev, fileName); } else if (S_ISDIR(st.st_mode)) { printf ("这是目录文件\n" ); sendHeader(bev, "200" , "OK" , get_mime_type(".html" ), 0 ); sendFile(bev, "head.html" ); struct dirent **namelist ; int num = scandir(fileName, &namelist, NULL , alphasort); if (num < 0 ) { perror("scandir" ); return -1 ; } else { char buffer[1024 ] = {0 }; while (num--) { if (namelist[num]->d_type==DT_DIR) sprintf (buffer+strlen (buffer),"<li><a href=%s/> %s </a></li>" ,namelist[num]->d_name,namelist[num]->d_name); else sprintf (buffer+strlen (buffer),"<li><a href=%s> %s </a></li>" ,namelist[num]->d_name,namelist[num]->d_name); free (namelist[num]); } bufferevent_write(bev,buffer,strlen (buffer)); free (namelist); } sendFile(bev, "tail.html" ); } } } void acceptClient2Str (struct sockaddr_in* client,char * str) { char sip[16 ]; memset (sip,0 ,sizeof (sip)); sprintf (str,"%s:%hu" ,inet_ntop(AF_INET,&client->sin_addr.s_addr,sip,sizeof (sip)),ntohs(client->sin_port)); } void read_cb (struct bufferevent *bev, void *ctx) { httpRequest(bev); } void event_cb (struct bufferevent *bev, short what, void *ctx) { if (what & BEV_EVENT_EOF) { printf ("有客户端断开连接\n" ); } } void listener_cb (struct evconnlistener *evl, evutil_socket_t fd, struct sockaddr *cliaddr, int socklen, void *ptr){ char str[16 ]={0 }; acceptClient2Str(cliaddr,str); printf ("客户端[%s]连接成功,占用%d描述符\n" ,str,fd); struct event_base *base = ptr; struct bufferevent *bev = bufferevent_socket_new(base,fd,BEV_OPT_CLOSE_ON_FREE); if (!bev) { printf ("Error constructing bufferevent!\n" ); event_base_loopbreak(base); return ; } bufferevent_setcb(bev,read_cb,NULL ,event_cb,NULL ); bufferevent_enable(bev,EV_READ); } int main () { struct sockaddr_in serv ; bzero(&serv, sizeof (serv)); serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(23456 ); serv.sin_family = AF_INET; struct event_base *base = event_base_new(); if (base==NULL ) { printf ("event_base_new error\n" ); return -1 ; } struct evconnlistener * listener = evconnlistener_new_bind(base,listener_cb,base, LEV_OPT_CLOSE_ON_FREE|LEV_OPT_REUSEABLE,-1 ,&serv,sizeof (serv)); printf ("等待连接中...\n" ); event_base_dispatch(base); printf ("跳出循环\n" ); evconnlistener_free(listener); event_base_free(base); return 0 ; }
与select,epoll的大同小异,对比select其实只是将read/write函数相对应换成bufferevent_read/bufferevent_write,并将httprequest的参数的文件描述符换成struct bufferevent *
web服务器框架
tomcat服务器
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。
包含了许多常用的 Boost.Asio 头文件。 -
包含了使用 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服务器,服务器回复客户端
安装与配置 需要安装两个
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 2 3 4 5 6 location /login{ fastcgi_pass 地址信息:端口; include fastcgi.conf; }
spawn-fcgi启动
1 2 3 4 5 # 前提条件,程序员已经写好fastCGI可执行程序 spawn-fcgi -a IP地址 -p 端口 -f fastcgi可执行程序 # 上面的IP地址和端口需要与前面nginx转发配置中的地址信息和端口对应 # nginx:localhost ->此处IP地址:127.0.0.1 # nginx:127.0.0.1 ->此处IP地址:127.0.0.1
如果返回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 2 #include "fcgi_config.h" #include "fcgi_stdio.h"
开发fastCGI需要使用到的库文件(二选一)
1 2 3 # 库文件 ./libfcgi/.libs/libfcgi++.so ./libfcgi/.libs/libfcgi.so
fastCGI如何处理数据
在FastCGI程序开发中,标准输入和标准输出被重定向到了socket连接 。
1 2 3 4 5 6 7 8 9 10 while (FCGI_Accept()>=0 ){ }
简单案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include "fcgi_config.h" #include <stdlib.h> #include <unistd.h> #include "fcgi_stdio.h" static void PrintEnv (char *label, char **envp) { printf ("%s:<br>\n<pre>\n" , label); for (; *envp != NULL ; envp++) { printf ("%s\n" , *envp); } printf ("</pre><p>\n" ); } int main () { int count = 0 ; while (FCGI_Accept() >= 0 ) { char *contentLength = getenv("CONTENT_LENGTH" ); char *text = getenv("QUERY_STRING" ); int len; printf ("Content-type: text/html\r\n" "\r\n" "<title>FastCGI 处理结果</title>" "<h1>FastCGI 结果</h1>\n" "Request number %d, Process ID: %d<p>\n" , ++count, getpid()); if (contentLength != NULL ) { len = strtol(contentLength, NULL , 10 ); } else { len = 0 ; } if (len <= 0 ) { printf ("No data from standard input.<p>\n" ); } else { int i, ch; printf ("Standard input:<br>\n<pre>\n" ); for (i = 0 ; i < len; i++) { if ((ch = getchar()) < 0 ) { printf ("Error: Not enough bytes received on standard input<p>\n" ); break ; } putchar (ch); } printf ("\n</pre><p>\n" ); } } return 0 ; }
分布式服务器
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
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.conf
1 2 3 4 5 6 7 bing_addr= port=22122 base_path=/home/yuqing/fastdfs
存储节点配置文件 /etc/fdfs/storage.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 group_name=group1 bing_addr= port=23000 base_path=/home/yuqing/fastdfs store_path_count = 1 store_path0 = /home/yuqing/fastdfs tracker_server = 192.168.209.121:22122
客户端配置文件 /etc/fdfs/client.conf
1 2 3 4 5 base_path = /home/yuqing/fastdfs tracker_server = 192.168.0.196:22122
FastDFS启动
启动追踪器
启动存储节点
启动客户端
格式为:程序 配置文件路径 [stop/restart]([stop/restart]省略表示第一次启动) 如:fdfs_storaged /etc/fdfs/storage.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # step 6. run the server programs # start the tracker server:启动追踪器 /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf # 关闭追踪器 /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf stop # 重启追踪器 /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart # start the storage server:启动存储节点 /usr/bin/fdfs_storaged /etc/fdfs/storage.conf # 重启和停止同追踪器格式 /sbin/service fdfs_trackerd restart /sbin/service fdfs_storaged restart # step 7. (optional) run monitor program#运行监控程序 # such as: /usr/bin/fdfs_monitor /etc/fdfs/client.conf # 用于检测fastDFS的状态 # step 8. (optional) run the test program # such as: /usr/bin/fdfs_test <client_conf_filename> <operation> /usr/bin/fdfs_test1 <client_conf_filename> <operation> # 客户端上传文件 /usr/bin/fdfs_upload_file /etc/fdfs/client.conf 要上传的文件路径 # 会返回一个字符串,表示文件id ,如: # group1/M00/00/00/CtM3A2SleRCAZ1YnAACK4SS3qzw622.jpg # 客户端下载文件 /usr/bin/fdfs_download_file /etc/fdfs/client.conf 要下载的文件的文件id
上传下载代码实现 使用多进程方式实现 exec函数组函数
父进程->创建子进程:执行execlp("fdfs_upload_file","xx",arg,NULL),有结果输出,用dup2管道重定向标准输出到管道的写端.父进程读管道读端
使用fastDFS API实现 余庆未提供api文档,只能通过看调用案例找API
安装文件夹中有test文件夹,该文件夹中的是调用案例
参考项目中自带的案例来实现
fastDFS配合fastCGI项目
nginx的fastdfs插件下载链接
在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 tracker_server=xxx.xxx.xxx.xxx:22122 storage_server_port=23000 group_name=group1 url_have_group_name=true bind_addr= store_path_count=1 store_path0=/home/xxx/fastdfs/storage
重新启动nginx后,执行ps aux | grep nginx发现没有worker进程,查看/usr/local/nginx/logs/error.log日志,报错如下:
1 2 3 4 5 6 7 ERROR - file: ini_file_reader.c, line: 1051 , include file "http.conf" not exists, line: "#include http.conf" sudo cp http.conf /etc/fdfs ERROR - file: shared_func.c, line: 1301 , file /etc/fdfs/mime.types not exist sudo cp mime.types /etc/fdfs
资源在存储节点的存储目录中,store_path0等中,需要告诉nginx服务器资源在哪,nginx.conf添加location
1 2 3 4 5 6 7 location /group1/M00/{ root /opt/fastdfs/data; ngx_fastdfs_module; }
添加了之后重启效果如下:ngx_http_fastdfs_set pid=699791
日志实现案例 源文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 #include "log.h" static int msgopt, wanopt;static char msgdatefmt[100 ], wandatefmt[100 ], ident_name[100 ];static struct timeval be_stime ;static FILE *msgfile = NULL , *wanfile = NULL ;int msgInit (char *pName) { if (msgLogOpen(pName, LOG_MESSAGE_FILE, LOG_POSTFIX_MESS,LOG_WARNING_FILE, LOG_POSTFIX_WARN) == 0 ) { msgLogFormat(LOG_PROCNAME|LOG_PID, LOG_MESSAGE_DFMT, LOG_PROCNAME|LOG_PID, LOG_WARNING_DFMT); } else { printf ("can not create log!\n" ); return -1 ; } return 0 ; } int msgLogOpen (char *ident, char *mpre, char *mdate, char *wpre, char *wdate) { time_t now_time; char openfilename[200 ], timestring[100 ]; now_time = time(NULL ); if ((!msgfile) && (*mpre)) { strcpy (openfilename, mpre); if (*mdate) { strftime(timestring, sizeof (timestring), mdate, localtime(&now_time)); strcat (openfilename, "." ); strcat (openfilename, timestring); } if ((msgfile = fopen(openfilename, "a+b" )) == NULL ) { printf ("openfilename=%s\n" , openfilename); return -1 ; } setlinebuf(msgfile); } if ((!wanfile) && (*wpre)) { strcpy (openfilename, wpre); if (*wdate) { strftime(timestring, sizeof (timestring), wdate, localtime(&now_time)); strcat (openfilename, "." ); strcat (openfilename, timestring); } if ((wanfile = fopen(openfilename, "a+b" )) == NULL ) { return -1 ; } setlinebuf(wanfile); } if ((msgfile) && (wanfile)) { if (*ident) { strcpy (ident_name, ident); } else { ident_name[0 ] = '\0' ; } msgopt = LOG_PROCNAME|LOG_PID; wanopt = LOG_PROCNAME|LOG_PID; strcpy (msgdatefmt, "%m-%d %H:%M:%S" ); strcpy (wandatefmt, "%m-%d %H:%M:%S" ); msglog(MSG_INFO,"File is msgfile=[%d],wanfile=[%d]." ,fileno(msgfile),fileno(wanfile)); return 0 ; } else { return -1 ; } } int msglog (int mtype, char *outfmt, ...) { time_t now_time; va_list ap; char logprefix[1024 ], tmpstring[1024 ]; time(&now_time); if (mtype & MSG_INFO) { strftime(logprefix, sizeof (logprefix), msgdatefmt, localtime(&now_time)); strcat (logprefix, " " ); if (msgopt&LOG_PROCNAME) { strcat (logprefix, ident_name); strcat (logprefix, " " ); } if (msgopt&LOG_PID) { sprintf (tmpstring, "[%6d]" , getpid()); strcat (logprefix, tmpstring); } fprintf (msgfile, "%s: " , logprefix); va_start(ap, outfmt); vfprintf (msgfile, outfmt, ap); va_end(ap); fprintf (msgfile, "\n" ); } if (mtype & MSG_WARN) { strftime(logprefix, sizeof (logprefix), wandatefmt, localtime(&now_time)); strcat (logprefix, " " ); if (wanopt & LOG_PROCNAME) { strcat (logprefix, ident_name); strcat (logprefix, " " ); } if (wanopt & LOG_PID) { sprintf (tmpstring, "[%6d]" , getpid()); strcat (logprefix, tmpstring); } fprintf (wanfile, "%s: " , logprefix); va_start(ap, outfmt); vfprintf (wanfile, outfmt, ap); va_end(ap); fprintf (wanfile, "\n" ); if (wanopt & LOG_PERROR) { fprintf (stderr , "%s: " , logprefix); va_start(ap, outfmt); vfprintf (stderr , outfmt, ap); va_end(ap); fprintf (stderr , "\n" ); } } return 0 ; } int msgLogFormat (int mopt, char *mdfmt, int wopt, char *wdfmt) { if (mopt >= 0 ) { msgopt = mopt; } else { msgopt = msgopt & mopt; } if (wopt >= 0 ) { wanopt = wopt; } else { wanopt = wanopt & wopt; } if (*mdfmt) strcpy (msgdatefmt, mdfmt); if (*wdfmt) strcpy (wandatefmt, wdfmt); return 0 ; } int msgLogClose (void ) { if (msgfile) fclose(msgfile); if (wanfile) fclose(wanfile); return 0 ; } long begusec_process (void ) { gettimeofday(&be_stime,NULL ); return 0 ; } long getusec_process (void ) { struct timeval ed_stime ; gettimeofday(&ed_stime,NULL ); return ((ed_stime.tv_sec-be_stime.tv_sec)*1000000 +ed_stime.tv_usec-be_stime.tv_usec); }
头文件log.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #ifndef LOG_H #define LOG_H #include <dirent.h> #include <errno.h> #include <fcntl.h> #include <math.h> #include <signal.h> #include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/stat.h> #include <sys/statfs.h> #include <sys/types.h> #include <time.h> #include <sys/time.h> #include <unistd.h> #define LOG_PROCNAME 0x00000001 #define LOG_PID 0x00000010 #define LOG_PERROR 0x00000100 #define NLO_PROCNAME 0x11111110 #define NLO_PID 0x11111101 #define NLO_PERROR 0x11111011 #define MSG_INFO 0x00000001 #define MSG_WARN 0x00000010 #define MSG_BOTH MSG_INFO|MSG_WARN #define LOG_MESSAGE_FILE "/home/itheima/log/tcpsvr" #define LOG_MESSAGE_DFMT "%m-%d %H:%M:%S" #define LOG_POSTFIX_MESS "%y%m" #define LOG_WARNING_FILE "/home/itheima/log/log.sys_warn" #define LOG_WARNING_DFMT "%m-%d %H:%M:%S" #define LOG_POSTFIX_WARN "" int msglog (int mtype, char *outfmt, ...) ;int msgLogFormat (int mopt, char *mdfmt, int wopt, char *wdfmt) ;int msgLogOpen (char *ident, char *mpre, char *mdate, char *wpre, char *wdate) ;int msgLogClose (void ) ;long begusec_process (void ) ; long getusec_process (void ) ; int msgInit (char *pName) ;#endif
使用方式
msgInit("日志文件名") 初始化日志文件
msglog([MSG_INFO或MSG_BOTH],"",...) 输出内容到日志文件
msgLogClose() 关闭日志文件
报文编解码
报文: 网络通信过程中接受发送的数据
编码: 将传输的数据转换为字节流的过程
ASN.1: 通用的一种编码格式
两台机器通信有如下要考虑
网络传输的时候需要用大端模式
字节序对齐不同,比如结构体
等等….
常用序列化方式 XML XML(Extensible Markup Language),类似于html
XML是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点.XML历史悠久,其1.0版本早在1998年就形成标准,并被广泛使用至今
XML最初产生的目标是对互联网文档进行标记,所以它的设计理念中就包含了对于人和机器都具备可读性.但是,当这种标记文档的设计被用来序列化对象的时候,就显得冗长而复杂
XML基本格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="utf-8" ?> <root > <part id = "01" name ="选项一" > <name > 我是徐茅山</name > <age > 今年20岁</age > <sex > 男</sex > </part > <part id ="02" name ="选项二" > <name > 我是李逍遥</name > <age > 今年22岁</age > <sex > 男</sex > </part > </root >
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 2 3 4 5 typedef struct _Persion { long age; char name[64 ]; }Person;
TLV节点对应的结构体
1 2 3 4 5 6 7 8 9 10 tupedef struct ITCAST_ANYBUF_ { unsigned char *pData; ITCAST_UINT32 dataLen; ITCAST_UINT32 unusedBits; iTCAST_UINT32 memoryType; iTCAST_UINT32 dataType; struct ITCAST_ANYBUF_ *next ; struct ITCAST_ANYBUF_ *prev ; }ITCAST_ANYBUF;
原理:
定义一个结点以TLV形式存储编码后的数据
被编码对象的每一个成员属性都会成为一个结点。从对象的一个成员开始到最后一个成员,所有新建的结点会连成一条双向链表,便于后续对象成员依次解码
整个对象也会被编码,即将链表的首结点封装成一个新的节点,作为传出参数传出,便于解码时以对象为单位进行解码
TLV原理图:
相关函数
编解码函数中包括对各种基本数据类型的编解码函数,用于对对象的成员属性进行分别编码;同时还包括对对象进行编解码的函数,即对成员属性编码后形成的链表首结点进行编码
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 2 3 ITCAST_ANYBUF p; ITCAST_UINT32 aa; DER_ItAsn1_ReadInteger(&p, &aa);
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 2 3 4 5 typedef struct _Temp { char name[64 ]; char *p; }Temp;
解码name时不可以使用DecodeChar来解码,p可以.原因如下:
1 2 3 4 5 6 7 8 9 int DecodeChar (char ** p) { *p = malloc (); } int main () { char name[10 ]; DecodeChar(...,&name,...); }
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 #include "teacher.h" #include "itcast_asn1_der.h" #include <stdio.h> #include <stdlib.h> #include <string.h> int encodeTeacher (Teacher * p, char ** outData, int * outlen) { ITCAST_ANYBUF *head = NULL ; ITCAST_ANYBUF* temp = NULL ; ITCAST_ANYBUF* next = NULL ; DER_ITCAST_String_To_AnyBuf(&temp,p->name,strlen (p->name)+1 ); DER_ItAsn1_WritePrintableString(temp,&head); DER_ITCAST_FreeQueue(temp); next = head; DER_ItAsn1_WriteInteger(p->age,&next->next); next=next->next; EncodeChar(p->p,strlen (p->p)+1 ,&next->next); next=next->next; DER_ItAsn1_WriteInteger(p->plen,&next->next); DER_ItAsn1_WriteSequence(head,&temp); *outData = temp->pData; *outlen = temp->dataLen; DER_ITCAST_FreeQueue(head); return 0 ; } int decodeTeacher (char * inData, int inLen, Teacher ** p) { ITCAST_ANYBUF *head = NULL ; ITCAST_ANYBUF* temp = NULL ; ITCAST_ANYBUF* next = NULL ; Teacher* pt = (Teacher *)malloc (sizeof (Teacher)); DER_ITCAST_String_To_AnyBuf(&temp,inData,inLen); DER_ItAsn1_ReadSequence(temp,&head); DER_ITCAST_FreeQueue(temp); next = head; DER_ItAsn1_ReadPrintableString(head,&temp); memcpy (pt->name,temp->pData,temp->dataLen); DER_ITCAST_FreeQueue(temp); next = next->next; DER_ItAsn1_ReadInteger(next,&pt->age); next = next->next; int len = 0 ; DecodeChar(next,&pt->p,&len); next=next->next; DER_ItAsn1_ReadInteger(next,&pt->plen); *p = pt; DER_ITCAST_FreeQueue(head); return 0 ; } void freeTeacher (Teacher ** p) { if (*p!=NULL ) { if ((*p)->p!= NULL ) { free ((*p)->p); } free (*p); } }
main函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include "teacher.h" #include "stdio.h" int main (int argc, char const *argv[]) { printf ("hello world!\n" ); Teacher t; memset (&t,0 ,sizeof (t)); strcpy (t.name,"路飞" ); t.age = 20 ; t.p = (char *)malloc (100 ); strcpy (t.p,"我是要成为海贼王的男人" ); t.plen = strlen (t.p); char *outData; int outLen; encodeTeacher(&t,&outData,&outLen); Teacher* pt; decodeTeacher(outData,outLen,&pt); printf ("name: %s\n" ,pt->name); printf ("age: %d\n" ,pt->age); printf ("p: %s\n" ,pt->p); printf ("plen: %d\n" ,pt->plen); freeTeacher(&pt); getchar(); return 0 ; }
执行结果
进一步封装 封装uml图如下:
其中的SequenceASN1部分:
SequenceASN1.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #ifndef ASN1DER_H #define ASN1DER_H #include "BaseASN1.h" #include <string> class SequenceASN1 : public BaseASN1{ public: SequenceASN1(); int writeHeadNode (int iValue) ; int writeHeadNode (char * sValue, int len) ; int writeNextNode (int iValue) ; int writeNextNode (char * sValue, int len) ; int readHeadNode (int &iValue) ; int readHeadNode (char * sValue) ; int readNextNode (int &iValue) ; int readNextNode (char * sValue) ; int packSequence (char ** outData, int &outLen) ; int unpackSequence (char * inData, int inLen) ; void freeSequence (ITCAST_ANYBUF* node = NULL ) ; private: ITCAST_ANYBUF* m_header = NULL ; ITCAST_ANYBUF* m_next = NULL ; ITCAST_ANYBUF* m_temp = NULL ; }; #endif
SequenceASN1.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 #include "SequenceASN1.h" SequenceASN1::SequenceASN1 () { } int SequenceASN1::writeHeadNode (int iValue) { DER_ItAsn1_WriteInteger (iValue,&m_header); m_next=m_header; return 0 ; } int SequenceASN1::writeHeadNode (char *sValue, int len) { EncodeChar (sValue,len,&m_header); m_next=m_header; return 0 ; } int SequenceASN1::writeNextNode (int iValue) { DER_ItAsn1_WriteInteger (iValue,&m_next->next); m_next=m_next->next; return 0 ; } int SequenceASN1::writeNextNode (char *sValue, int len) { EncodeChar (sValue,len,&m_next->next); m_next=m_next->next; return 0 ; } int SequenceASN1::readHeadNode (int &iValue) { DER_ItAsn1_ReadInteger (m_header,(ITCAST_UINT32*)&iValue); m_next = m_header->next; return 0 ; } int SequenceASN1::readHeadNode (char * sValue) { DER_ItAsn1_ReadPrintableString (m_header,&m_temp); memcpy (sValue,m_temp->pData,m_temp->dataLen); DER_ITCAST_FreeQueue (m_temp); m_next = m_header->next; return 0 ; } int SequenceASN1::readNextNode (int &iValue) { int ret =DER_ItAsn1_ReadInteger (m_next,(ITCAST_UINT32*)&iValue); if (ret!=0 ) return ret; m_next = m_next->next; return 0 ; } int SequenceASN1::readNextNode (char *sValue) { int ret =DER_ItAsn1_ReadPrintableString (m_next,&m_temp); if (ret!=0 ) return ret; memcpy (sValue,m_temp->pData,m_temp->dataLen); DER_ITCAST_FreeQueue (m_temp); m_next = m_next->next; return 0 ; } int SequenceASN1::packSequence (char **outData, int &outLen) { DER_ItAsn1_WriteSequence (m_header,&m_temp); outLen = m_temp->dataLen; *outData = (char *)m_temp->pData; DER_ITCAST_FreeQueue (m_header); return 0 ; } int SequenceASN1::unpackSequence (char *inData, int inLen) { DER_ITCAST_String_To_AnyBuf (&m_temp,(unsigned char *)inData,inLen); DER_ItAsn1_ReadSequence (m_temp,&m_header); DER_ITCAST_FreeQueue (m_temp); return 0 ; } void SequenceASN1::freeSequence (ITCAST_ANYBUF *node) { }
CSharp网络通信 学习参考链接
主要是下面几种方式,它们都位于System.Net.Sockets命名空间中
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 using System.Net;using System.Net.Sockets; namespace _Server { class Program { static void Main () { int port = 888 ; TcpClient tcpClient; IPAddress[] serverIP = Dns.GetHostAddresses("127.0.0.1" ); IPAddress localAddress = serverIP[0 ]; TcpListener tcpListener = new (localAddress, port); tcpListener.Start(); Console.WriteLine("服务器启动成功,等待用户接入…" ); while (true ) { try { tcpClient = tcpListener.AcceptTcpClient(); NetworkStream networkStream = tcpClient.GetStream(); BinaryReader reader = new (networkStream); BinaryWriter writer = new (networkStream); while (true ) { try { string strReader = reader.ReadString(); string [] strReaders = strReader.Split([' ' ]); Console.WriteLine("有客户端接入,客户IP:" + strReaders[0 ]); Console.WriteLine("来自客户端的消息:" + strReaders[1 ]); string strWriter = "我是服务器,欢迎光临" ; writer.Write(strWriter); } catch { break ; } } } catch { break ; } } } } }
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 using System;using System.IO;using System.Net; using System.Net.Sockets; namespace _Client { class Program { static void Main (string [] args ) { TcpClient tcpClient = new (); tcpClient.Connect("127.0.0.1" , 888 ); if (tcpClient != null ) { Console.WriteLine("连接服务器成功" ); NetworkStream networkStream = tcpClient.GetStream(); BinaryReader reader = new (networkStream); BinaryWriter writer = new (networkStream); string localip = "127.0.0.1" ; IPAddress[] ips = Dns.GetHostAddresses(Dns.GetHostName()); foreach (IPAddress ip in ips) { if (!ip.IsIPv6SiteLocal) localip = ip.ToString(); } writer.Write(localip + " 你好服务器,我是客户端" ); while (true ) { try { string strReader = reader.ReadString(); if (strReader != null ) { Console.WriteLine("来自服务器的消息:" + strReader); } } catch { break ; } } } Console.WriteLine("连接服务器失败" ); } } }
二进制读写 1 2 3 4 5 6 7 8 9 10 11 12 13 byte [] buffer = new byte [4 ]; int bytesRead = reader.Read(buffer, 0 , buffer.Length); if (bytesRead > 0 ){ int receivedNumber = BitConverter.ToInt32(buffer, 0 ); Console.WriteLine("来自服务器的消息:接收到的整数是 " + receivedNumber); } byte [] dataToSend = BitConverter.GetBytes(12345 ); writer.Write(dataToSend);
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 using Socket client = new ( ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); await client.ConnectAsync(ipEndPoint);while (true ){ var message = "Hi friends 👋!<|EOM|>" ; var messageBytes = Encoding.UTF8.GetBytes(message); _ = await client.SendAsync(messageBytes, SocketFlags.None); Console.WriteLine($"Socket client sent message: \"{message} \"" ); var buffer = new byte [1 _024]; var received = await client.ReceiveAsync(buffer, SocketFlags.None); var response = Encoding.UTF8.GetString(buffer, 0 , received); if (response == "<|ACK|>" ) { Console.WriteLine( $"Socket client received acknowledgment: \"{response} \"" ); break ; } } client.Shutdown(SocketShutdown.Both);
服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 using Socket listener = new ( ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); listener.Bind(ipEndPoint); listener.Listen(100 ); var handler = await listener.AcceptAsync();while (true ){ var buffer = new byte [1 _024]; var received = await handler.ReceiveAsync(buffer, SocketFlags.None); var response = Encoding.UTF8.GetString(buffer, 0 , received); var eom = "<|EOM|>" ; if (response.IndexOf(eom) > -1 ) { Console.WriteLine( $"Socket server received message: \"{response.Replace(eom, "" )} \"" ); var ackMessage = "<|ACK|>" ; var echoBytes = Encoding.UTF8.GetBytes(ackMessage); await handler.SendAsync(echoBytes, 0 ); Console.WriteLine( $"Socket server sent acknowledgment: \"{ackMessage} \"" ); break ; } }
常用网络相关命令 sudo lsof -i:端口号 查询端口占用
windows排查端口占用:
1 2 3 4 # 通过端口号查找占用的进程的pid netstat -ano | findstr :需要查的端口号 # 通过pid查进程名称,也可以直接打开任务管理器详细信息窗口查看 tasklist | findstr <PID>
windows用于ping测试ip地址的脚本
1 2 3 4 5 @echo off for /L %%i in (2,1,255) do ( ping -n 1 192.168.1.%%i | find "TTL=" >nul && echo 192.168.1.%%i 连通 || echo 192.168.1.%%i 不通 ) pause
UDP打洞 点击跳转
域名相关 托管域名 托管域名(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配置准确性,并提供多种网络诊断功能。以下是其核心功能与使用场景的详细解析
免费且跨平台
基于该工具的DNS记录查询网站