网络编程

网络基础

  • OSI七层,TCP/IP四层模型结构
  • 常见网络协议格式
  • 网络字节序和主机字节序之间的转换(大端法和小端法)
  • tcp服务器端/客户端通信流程

网络是如何通信的优质讲解视频

网络怎么变靠谱的优质视频

分层模型

网络分层OSI 7层模型

OSI是Open System Interconnection的缩写, 意为开放式系统互联. 国际标准化组织(ISO)制定了OSI模型, 该模型定义了不同计算机互联的标准, 是设计和描述计算机网络通信的基本框架.

image-20220729162142546

  1. 物理层—双绞线,光纤(传输介质),将模拟信号转换为数字信号(通过调制解调器modemn)
  2. 数据链路层数据校验,定义了网络传输的基本单位- ARP,RARP协议
  3. 网络层—定义网络,两台机器之间传输的路径选择点到点的传输 IP协议
  4. 传输层—传输数据 TCP,UDP,端到端的传输 (不需要考虑中间经过的点) TCP,UDP协议
  5. 会话层—通过传输层建立数据传输的通道.
  6. 表示层—编解码,翻译工作.
  7. 应用层—为客户提供各种应用服务,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)四层

image-20220729165744363image-20220729165801715

分层的含义如下图:

传输层及其以下的机制由内核提供,应用层由用户进程提供(后面将介绍如何使用socket API编写应用程序),应用程序对通讯数据的含义进行解释,而传输层及其以下处理通讯的细节,将数据从一台计算机通过一定的路径发送到另一台计算机。应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示:

image-20220801153019809

通信过程: 其实就是发送端层层打包, 接收方层层解包.

image-20220801152504753

不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做帧(frame)。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理。

TCP/IP协议分层通讯全过程

image-20220801162510989image-20220801162514898

​ 链路层有以太网、令牌环网等标准,链路层负责网卡设备的驱动、帧同步(即从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。交换机是工作在链路层的网络设备,可以在不同的链路层网络之间转发数据帧(比如十兆以太网和百兆以太网之间、以太网和令牌环网之间),由于不同链路层的帧格式不同,交换机要将进来的数据包拆掉链路层首部重新封装之后再转发。

​ 链路层有以太网、令牌环网等标准,链路层负责网卡设备的驱动、帧同步(即从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。交换机是工作在链路层的网络设备,可以在不同的链路层网络之间转发数据帧(比如十兆以太网和百兆以太网之间、以太网和令牌环网之间),由于不同链路层的帧格式不同,交换机要将进来的数据包拆掉链路层首部重新封装之后再转发。
​ 网络层的IP协议是构成Internet的基础。Internet上的主机通过IP地址来标识,Inter-net上有大量路由器负责根据IP地址选择合适的路径转发数据包,数据包从Internet上的源主机到目的主机往往要经过十多个路由器。路由器是工作在第三层的网络设备,同时兼有交换机的功能,可以在不同的链路层接口之间转发数据包,因此路由器需要将进来的数据包拆掉网络层和链路层两层首部并重新封装。IP协议不保证传输的可靠性,数据包在传输过程中可能丢失,可靠性可以在上层协议或应用程序中提供支持。
​ 网络层负责点到点(ptop,point-to-point)的传输(这里的“点”指主机或路由器),而传输层负责端到端(etoe,end-to-end)的传输(这里的“端”指源主机和目的主机)。传输层可选择TCP或UDP协议。
TCP是一种面向连接的、可靠的协议,有点像打电话,双方拿起电话互通身份之后就建立了连接,然后说话就行了,这边说的话那边保证听得到,并且是按说话的顺序听到的,说完话挂机断开连接。也就是说TCP传输的双方需要首先建立连接,之后由TCP协议保证数据收发的可靠性,丢失的数据包自动重发,上层应用程序收到的总是可靠的数据流,通讯之后关闭连接。
​ UDP是无连接的传输协议,不保证可靠性,有点像寄信,信写好放到邮筒里,既不能保证信件在邮递过程中不会丢失,也不能保证信件寄送顺序。使用UDP协议的应用程序需要自己完成丢包重发、消息排序等工作。
​ 目的主机收到数据包后,如何经过各层协议栈最后到达应用程序呢?其过程如下图所示:

image-20220801162904659

​ 以太网驱动程序首先根据以太网首部中的“上层协议”字段确定该数据帧的有效载荷(payload,指除去协议首部之外实际传输的数据)是IP、ARP还是RARP协议的数据报,然后交给相应的协议处理。假如是IP数据报,IP协议再根据IP首部中的“上层协议”字段确定该数据报的有效载荷是TCP、UDP、ICMP还是IGMP,然后交给相应的协议处理。假如是TCP段或UDP段,TCP或UDP协议再根据TCP首部或UDP首部的“端口号”字段确定应该将应用层数据交给哪个用户进程。IP地址是标识网络中不同主机的地址,而端口号就是同一台主机上标识不同进程的地址,IP地址和端口号合起来标识网络中唯一的进程。

​ 虽然IP、ARP和RARP数据报都需要以太网驱动程序来封装成帧,但是从功能上划分,ARP和RARP属于链路层,IP属于网络层。虽然ICMP、IGMP、TCP、UDP的数据都需要IP协议来封装成数据报,但是从功能上划分,ICMP、IGMP与IP同属于网络层,TCP和UDP属于传输层。

协议

协议事先约定好, 大家共同遵守的一组规则, 如交通信号灯.从应用程序的角度看, 协议可理解为数据传输和数据解释的规则;可以简单的理解为各个主机之间进行通信所使用的共同语言.

当原始协议经过不断增加完善改进, 最终形成了一个稳定的完整的传输协议, 被广泛应用于各种文件传输, 该协议逐渐就成了一个标准协议.

几种常见的标准协议

  • 传输层 常见协议有TCP/UDP协议。
  • 应用层 常见的协议有HTTP协议,FTP协议。
  • 网络层 常见协议有IP协议、ICMP协议、IGMP协议。
  • 网络接口层 常见协议有ARP协议、RARP协议。

TCP协议注重数据的传输。http协议着重于数据的解释。

具体协议含义

  • TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
  • UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
  • HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。
  • FTP文件传输协议(File Transfer Protocol)
  • IP协议是因特网互联协议(Internet Protocol)
  • ICMP协议是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
  • IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
  • ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。
  • RARP是反向地址转换协议,通过MAC地址确定IP地址。

网络相关名词

设备相关

  • 路由(名词)
    数据包从源地址到目的地址所经过的路径,由一系列路由节点组成。
  • 路由(动词)
    某个路由节点为数据包选择投递方向的选路过程。

路由节点

一个具有路由能力的主机或路由器,它维护一张路由表,通过查询路由表来决定向哪个接口发送数据包。

以太网交换机工作原理

以太网交换机是基于以太网传输数据的交换机,以太网采用共享总线型传输媒体方式的局域网。以太网交换机的结构是每个端口都直接与主机相连,并且一般都工作在全双工方式。交换机能同时连通许多对端口,使每一对相互通信的主机都能像独占通信媒体那样,进行无冲突地传输数据。
以太网交换机工作于OSI网络参考模型的第二层(即数据链路层),是一种基于MAC(Media Access Control,介质访问控制)地址识别、完成以太网数据帧转发的网络设备。

交换机刚启动时,MAC地址表中无表项。当接入PC的时候,交换机开始进行学习MAC地址.

交换机对数据帧的转发与过滤:

  • 单播帧的转发:

    • PCA发出目的到PCD的单播数据帧
    • 交换机根据帧中的目的地址,从相应的端口E1/0/4发送出去
    • 交换机不在其他端口上转发此单播数据帧
  • 广播、组播和未知单播帧的转发:

    img

    ​ 交换机会把广播、组播和未知单播帧从所有其他端口发送出去(除了接收到帧的端口)

交换机原理详解跳转

路由表(Routing Table)

在计算机网络中,路由表或称路由择域信息库(RIB)是一个存储在路由器或者联网计算机中的电子表格(文件)或类数据库。路由表存储着指向特定网络地址的路径。

路由条目

路由表中的一行,每个条目主要由**目的网络地址(Destination)、子网掩码(Genmask)、下一跳地址(GateWay)、发送接口(Iface)**四部分组成,如果要发送的数据包的目的网络地址匹配路由表中的某一行,就按规定的接口发送到下一跳地址。

缺省路由条目

路由表中的最后一行,主要由下一跳地址和发送接口两部分组成,当目的地址与路由表中其它行都不匹配时,就按缺省路由条目规定的接口发送到下一跳地址。

路由器工作原理

路由器(Router)是连接因特网中各局域网、广域网的设备,它会根据信道的情况自动选择和设定路由,以最佳路径,按前后顺序发送信号的设备。
传统地,路由器工作于OSI七层协议中的第三层,其主要任务是接收来自一个网络接口的数据包,根据其中所含的目的地址,决定转发到下一个目的地址。因此,路由器首先得在转发路由表中查找它的目的地址,若找到了目的地址,就在数据包的帧格前添加下一个MAC地址,同时IP数据包头的TTL(Time To Live)域也开始减数, 并重新计算校验和。当数据包被送到输出端口时,它需要按顺序等待,以便被传送到输出链路上。
路由器在工作时能够按照某种路由通信协议查找设备中的路由表。如果到某一特定节点有一条以上的路径,则基本预先确定的路由准则是选择最优(或最经济)的传输路径。由于各种网络段和其相互连接情况可能会因环境变化而变化,因此路由情况的信息一般也按所使用的路由信息协议的规定而定时更新。
网络中,每个路由器的基本功能都是按照一定的规则来动态地更新它所保持的路由表,以便保持路由信息的有效性。为了便于在网络间传送报文,路由器总是先按照预定的规则把较大的数据分解成适当大小的数据包,再将这些数据包分别通过相同或不同路径发送出去。当这些数据包按先后秩序到达目的地后,再把分解的数据包按照一定顺序包装成原有的报文形式。路由器的分层寻址功能是路由器的重要功能之一,该功能可以帮助具有很多节点站的网络来存储寻址信息,同时还能在网络间截获发送到远地网段的报文,起转发作用;选择最合理的路由,引导通信也是路由器基本功能;多协议路由器还可以连接使用不同通信协议的网络段,成为不同通信协议网络段之间的通信平台。
路由和交换之间的主要区别就是交换发生在OSI参考模型第二层(数据链路层),而路由发生在第三层,即网络层。这一区别决定了路由和交换在移动信息的过程 中需使用不同的控制信息,所以两者实现各自功能的方式是不同的。

img

如上图图所示:路由器A和B是经过配置的路由在他们的路由表中就保存了相应的网段和接口,如果主机1.1要发送数据包给主机3.1:

  1. 因为IP地址不在同一网段,主机就会将数据包发送给本网段的网关路由器A。
  2. 路由器A接收到数据包,查看数据包IP首部中的目标IP地址,在查找自己的路由表。数据包的目标IP地址是3.1.属于3.0网段路由器A在路由表中查到3.0网段转发的接口是S0接口。于是,路由器就将数据包从S0接口转发出去。
  3. 每个路由器但是按这个步骤去转发数据的,直到到达了路由器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,一种覆盖一座或几座大楼、一个校园或者一个厂区等地理区域的小范围的计算机网。

  1. 覆盖的地理范围较小,只在一个相对独立的局部范围内联,如一座或集中的建筑群内。
  2. 使用专门铺设的传输介质进行联网,数据传输速率高(10Mb/s~10Gb/s)
  3. 通信延迟时间短,可靠性较高
  4. 局域网可以支持多种传输介质

广域网(WAN)

wide area network,一种用来实现不同地区的局域网或城域网的互连,可提供不同地区、城市和国家之间的计算机通信的远程计算机网。
覆盖的范围比局域网(LAN)和城域网(MAN)都广。广域网的通信子网主要使用分组交换技术
广域网的通信子网可以利用公用分组交换网、卫星通信网和无线分组交换网,它将分布在不同地区的局域网或计算机系统互连起来,达到资源共享的目的。如互联网是世界范围内最大的广域网。

  1. 适应大容量与突发性通信的要求;
  2. 适应综合业务服务的要求;
  3. 开放的设备接口与规范化的协议;
  4. 完善的通信服务与网络管理。

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
截屏2022-08-15 15.02.37

查询经过的路由器信息

  • 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原理

三次握手四次挥手

建立连接需要三次握手,断开连接需要四次挥手

image-20220817131155533image-20220802125922013

段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本身也会占用一位.

建立三次握手的过程

  1. 客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的段1

    客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。

  2. 服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯

    服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。

  3. 客户必须再次回应服务器端一个ACK报文,这是报文段3

    客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为“三方握手(three-way-handshake)”。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。

在TCP通讯中,如果一方收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。例如,服务器并没有任何进程使用8080端口,我们却用telnet客户端去连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端的telnet程序收到RST段后报告错误Connection refused

数据传输的过程

  1. 客户端发出段4,包含从序号1001开始的20个字节数据
  2. 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback
  3. 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据

在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。

关闭连接(四次挥手)的过程

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

  1. 客户端发出段7,FIN位表示关闭连接的请求
  2. 服务器发出段8,应答客户端的关闭连接请求
  3. 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求
  4. 客户端发出段10,应答服务器的关闭连接请求

image-20220817151120112

建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。

当双方刚好同时关闭的时候(概率很低),是存在ACKFIN,ACK包合并为同一个发送的情况的,这种情况只需要三步挥手便结束了.

[重点的问题理解]

  1. 为什么四次挥手中,被动被请求关闭连接的一方要发送**一个确认包和一个请求关闭连接包,**而不能仅仅发一个请求连接包?

    因为第一个确认包仅仅表示收到了对方关闭连接的请求,但自身却还可能有未发送完的数据,第二个请求关闭连接包才真正表示自身的发送也完成可以关闭了.即表示第一个包和第二个包之间依然可以发送未发送完的数据包.

  2. 为什么最先请求关闭连接的一方在第四次挥手发送确认包后还要等待2倍MSL时间?

    1. 让四次挥手的过程更可靠, 确保最后一个发送给对方的ACK到达;

      若对方没有收到ACK应答, 对方会再次发送FIN请求关闭, 此时在2MS时间内被动关闭方仍然可以发送ACK给对方

      补充解释:发送的ACK有可能丢失,如果ACK丢失,另一方会重发关闭连接请求包,两个MSL时间可以大幅度增加接收到重发的关闭连接请求包的情况的可能.但是情况依旧不完美,因为重发的关闭请求包也存在丢失的可能,如果也丢失了,重发的时间又超过了2倍MSL时间,那么他将等不到发回来的确定包,即等不到关闭的时机.(脑补应该用心跳包解决)

    2. 为了保证在2MS时间内, 不能启动相同的SOCKET-PAIR.

      TIME_WAIT一定是出现在主动关闭的一方, 也就是说2MS是针对主动关闭一方来说的;由于TCP有可能存在丢包重传, 丢包重传若发给了已经断开连接之后相同的socket-pair(该连接是新建的, 与原来的socket-pair完全相同, 双方使用的是相同的IP和端口), 这样会对之后的连接造成困扰, 严重可能引起程序异常.因此很多操作系统实现的时候, 只要端口被占用, 服务就不能启动.通过这个方式尽可能解决该程序异常. (实际上服务端每次都是使用的同一个端口,而客户端一般设置为由内核分配随机端口,会避免这种问题发生.因此实际上很难发生这样的情况)

主动断开方,查看 netstat -anp | grep 端口号会发现状态为TIME_WAIT

截屏2022-09-25 16.12.34

tcp状态转换

socket-pair的概念: 客户端与服务端连接其实是一个连接对, 可以通过使用netstat -anp | grep 端口号进行查看(linux),他也可以查询是什么进程占用着端口

image-20220823115550841

说明: 上图中粗线表示主动方, 虚线表示被动方, 细线部分表示一些特殊情况

image-20220823115705716

三次握手过程

客户端–>服务端

  • 客户端: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)”机制解决这一问题。

image-20220817160708623

  1. 发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
  2. 发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
  3. 接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
  4. 接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
  5. 发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。
  6. 接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
  7. 接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
  8. 接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
  9. 接收端的应用程序在提走全部数据后,决定关闭连接,发出段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 类型

image-20220801160410607

其中的源地址和目的地址是指网卡的硬件地址(也叫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数据报的格式如下所示:

image-20220801161139071

源MAC地址、目的MAC地址在以太网首部和ARP请求中各出现一次,对于链路层为以太网的情况是多余的,但如果链路层是其它类型的网络则有可能是必要的。硬件类型指链路层网络类型,1为以太网,协议类型指要转换的地址类型,0x0800为IP地址,后面两个地址长度对于以太网地址和IP地址分别为6和4(字节),op字段为1表示ARP请求,op字段为2表示ARP应答。

目的端mac地址是通过发送端发送ARP广播, 接收到该ARP数据的主机先判断是否是自己的IP, 若是则应答一个ARP应答报文, 并将mac地址填入应答报文中; 若目的IP不是自己的主机IP, 则直接丢弃该ARP请求报文.

image-20220801170907341

如上图所示,ARP协议目的:解决同一个局域网上的主机或路由器的ip地址和硬件地址的映射问题

抓包命令

抓包命令: tcpdump -ntx

image-20220802154101644

思考题:如果源主机和目的主机不在同一网段,ARP请求的广播帧无法穿过路由器,源主机如何与目的主机通信?

那么就要通过ARP找到一个位于本局域网上的某个路由器的硬件地址,然后把分组发送个这个路由器,让这个路由器把分组转发给下一个网络,剩下的工作就由下一个网络来做

IP段格式

IP协议是TCP/IP协议族的基石,它为上层提供无状态、无连接、不可靠的服务

  • 无状态:指IP通信双方不同步传输数据的状态信息,因此所有IP数据报的发送,传输,接收都是相互独立的。这种服务最大缺点是无法处理乱序和重复的IP数据报。优点是简单高效,和UDP协议与HTTP协议相同,都是无状态协议。
  • 无连接:指IP通信双方都不长久的维持对方的任何信息。这表示上层协议每次发送数据,都需要明确指定对方的IP地址。
  • 不可靠:指IP协议不能IP数据报能准确到达接收端,只是会尽最大努力。一旦发送失败,就通知上层协议,而不会试图重发。

image-20220801173138936

  • 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路由的工作模式

image-20220802164931881

IP模块收到来自数据链路层的IP数据报,首先对数据报的头部做CRC校验,无误后开始分析头部具体信息。

如果IP数据报头部设置了源站选路选项,则IP模块调用数据报转发子模块来处理该数据报。

如果该数据报的头部目标IP地址是本机的某个IP地址,或者广播地址,则IP模块根据数据报协议字段来决定发送给哪个上层应用。如果不是本机,则掉用数据报转发子模块来处理该数据报。

数据报转发模块检查系统是否允许转发,不允许则丢弃。允许则将该数据报执行一些操作,就将它交给IP数据报输出模块。

IP数据报根据路由表计算下一跳路由。

IP输出队列存放所有等待发送的IP数据报。

路由机制

  1. 查找路由表中和数据报的目标IP完全匹配的主机IP地址,如果找到,就直接使用该项,没有就到第二步
  2. 查找路由表中和目标IP具有相同的网路ID的IP地址,如Gateway,有就使用,否则来到第三步
  3. 选择默认路由项,这一般为网关。

执行route命令可查看路由表

image-20220802165344800

  • Destination:目标网络或主机
  • Gateway:网关地址,*表示目标与本机在同一个网络上,不需要路由。
  • Genmask:网络掩码
  • Flags:路由标志,U:该路由活动,H:该路由目标是一个主机,G:该路由目标是网关,D:该路由是重定向产生的,M:该路由被重定向修改过
  • Metric:路由距离,达到目标网络所需的中转数
  • Ref:路由项被引用的次数
  • Use:该路由项被使用的次数
  • Iface:该路由对应的输出网卡接口

IPv6

IPv6由40个字节的固定头部和可变长的扩展头部组成。

image-20220802165506044

  • 4位版本号:IP协议版本,IPv6值为6
  • 8位通信类型:指示数据通信类型和优先级
  • 20位流标志:用于某些对连接服务质量有特殊要求的通信。
  • 16位净荷长度:指IPv6扩展头部和应用程序数据长度之和,不包含固定头部长度
  • 8位下一个包头:指紧跟IPv6固定头部的包头类型,如扩展头或上层协议。
  • 8位跳数限制:和IPv4的TTL含义相同
  • 后两项IP地址:IPv6地址一般用16进制字符串表示,用‘:’分割为8组,每组两个字节。

UDP数据报格式

无连接的,不安全,不可靠的

UDP(UserDatagramProtocol)是一个简单的面向消息的传输层协议,尽管UDP提供标头和有效负载的完整性验证(通过校验和),但它不保证向上层协议提供消息传递,并且UDP层在发送后不会保留UDP 消息的状态。因此,UDP有时被称为不可靠的数据报协议。如果需要传输可靠性,则必须在用户应用程序中实现。

UDP使用具有最小协议机制的简单无连接通信模型。UDP提供数据完整性的校验和,以及用于在数据报的源和目标寻址不同函数的端口号。它没有握手对话,因此将用户的程序暴露在底层网络的任何不可靠的方面。如果在网络接口级别需要纠错功能,应用程序可以使用为此目的设计的传输控制协议(TCP)。

  • UDP是基于IP的简单协议,不可靠的协议。

  • UDP的优点:简单,轻量化。

  • UDP的缺点:没有流控制,没有应答确认机制,不能解决丢包、重发、错序问题。

    并不是所有使用UDP协议的应用层都是不可靠的,应用程序可以自己实现可靠的数据传输,通过增加确认和重传机制,所以使用UDP 协议最大的特点就是速度快。

其他特点:

  • UDP 没有拥塞控制

    网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用是很重要的。

  • UDP 支持一对一、一对多、多对一和多对多的交互通信。

[UDP的应用] UDP协议一般作为流媒体应用、语音交流、视频会议所使用的传输层协议,还有许多基于互联网的电话服务使用的VOIP(基于IP的语音)也是基于UDP运行的.这些实时应用要求源主机以恒定的速率发送数据,并且允许在网络出现拥塞时丢失一部分数据,但却不允许数据有太大的时延。UDP 协议正好适合这种要求。

UDP头部格式

image-20220801175246809

  • 通过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
//基于UDP的TFTP协议帧
//以太网首部
00 05 5d 67 d0 b1 00 05 5d 61 58 a8 08 00
//IP首部
45 00
00 53 93 25 00 00 80 11 25 ec c0 a8 00 37 c0 a8
00 01
//UDP首部
05 d4 00 45 00 3f ac 40
//TFTP协议
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'
//TFTP是基于文本的协议,各字段之间用字节0分隔,开头的00 01表示请求读取一个文件,接下来的各字段是:

image-20220802170722364

wireshark抓包

image-20220802172344382

udp连接机制

image-20220802171337336

服务器在特定端口上收到UDP 数据包时,将通过以下两个步骤进行响应

  • 服务器首先检查是否有任何当前侦听指定端口请求的程序正在运行。
  • 如果该端口上没有程序正在接收数据包,则服务器将以 ICMP (ping) 数据包作为响应,以告知发送方目标不可达。

导致>>UDP洪水攻击 解决方案:大多数操作系统限制ICMP 数据包的响应速率

TCP数据流格式

稳定的, 安全的, 可靠的 TCP原理跳转

image-20220802171257567

  • TCP协议用于1对1,即不能用于基于广播和多播的应用程序
  • TCP连接双方的收发数据次数不一定相同,即发送多次的数据包,可能会被对方1次全部接收
  • TCP在发送数据报后,必须得到接收方的应答,才认为传输成功,所以是可靠的
  • TCP采用超时重传机制,超过时间没收到应答,就会重新发送。

tcp头部结构

image-20220802125922013

  • 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位紧急指针:正的偏移量,它和序号字段的值相加表示最后一个紧急数据的下一个字节序号

  • 选项:为可变长的可选信息

典型选项结构

image-20220802174222523

  • kind:说明选项的类型
  • length:选项的总长度
  • info:选项的具体信息

常见TCP的7种选项:

image-20220802174327208

kind的常见类型解释

  1. kind=0:选项表结束
  2. kind=1:空操作,一般用于将TCP选项的总长度填充为4字节的整数倍
  3. kind=2:最大报文段长度选项,初次连接,双方通过此选项协商最大报文长度,TCP通常设置此为MTU-40字节,避免被分片
  4. kind=3:窗口扩大因子选项。TCP头部通知窗口为N,扩大因子为M,则实际接收通知窗口为N*(2的M次方),且只能出现在同步报文段中,否则被忽略
  5. kind=4:选择性确定(Selective Acknowledge,SACK)。如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP报文段后续所有报文段。而该选项则可解决这种问题。
  6. kind=5:SACK实际工作选项。告诉发送端本端已经收到的数据块,从而让发送端只发送丢失的数据
  7. 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);
//inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);

参数说明:
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_atoninet_ntoa只支持ipv4类型的地址转换,而inet_ptoninet_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);
//函数名的h表示主机host, n表示网络network, s表示short, l表示long

上述的几个函数, 如果本来不需要转换函数内部就不会做转换.

同步与异步

  • 同步 : 发送一个请求,等待返回,然后再发送下一个请求
  • 异步 : 发送一个请求,不等待返回,随时可以再发送下一个请求

同步可以**避免出现死锁**,读脏数据的发生,一般共享某一资源的时候用,如果每个人都有修改权限,同时修改一个文件,有可能使一个人读取另一个人已经删除的内容,就会出错,同步就会按顺序来修改。

异步则是可以提高效率了,现在cpu都是双核,四核,异步处理的话可以同时做多项工作,当然必须保证是可以并发处理的。

实际项目开发中会优先选择异步交互模型

四种情况

  • 同步非阻塞: 若客户端发送数据之后, read函数不阻塞(文件描述符设置为非阻塞,但是一直循环read空转)
  • 同步阻塞:客户端发送数据之后, read数据, 若对方不发送应答数据, 就一直阻塞.
  • 异步阻塞: 比如: select poll epoll, 若没有事件发生, select 或者epoll可以一直阻塞
  • 异步非阻塞: 比如: 将epoll设置非阻塞, 不管有没有事件发生都会立刻返回

异步需要有第三方参与通知才能实现

阻塞与非阻塞

阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:

  • 阻塞是指IO操作需要彻底完成后才返回到用户空间
  • 非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成

DNS解析过程

image-20230606112624290
  1. 在浏览器中输入www.magedu.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。

  2. 如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。

    Windows和Linux系统都会在本地缓存dns解析的记录,提高速度。

  3. 如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找 TCP/IP 参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。

  4. 如果要查询的域名,不由本地DNS服务器区域解析,但该DNS服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。

  5. 如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(没有设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至13台根DNS,根DNS服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责 .com域的这台服务器。这台负责 .com域的服务器收到请求后,如果自己无法解析,它就会找一个管理 .com域的下一级DNS服务器地址(magedu.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找magedu.com域服务器,重复上面的动作进行查询,直至找到www.magedu.com主机。

  6. 如果用的是转发模式(设置转发器),此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

    image-20230606115008324
  • 字符串长度来说,URL包含URI

    image-20230606120007734

    红色部分+绿色部分 = URL

    绿色部分 = URI

URL的静态请求: http://localhost/login.html

URL的动态请求: http://localhost/login?user=zhang&age=12

  • http:协议
  • localhost:域名
  • /login:服务器端要处理的指令(包括这里往下的部分都存在于http协议中的请求行的第二部分请求资源中)
  • ?:连接符,后面的内容是客户端给服务端提交的数据
  • &:分隔符

正向/反向代理

  • 正向代理是为客户端服务的
  • 反向代理是为服务端服务的

正向代理

image-20230605155633306

正向代理是位于客户端和原始服务器之间的服务器,为了能够从原始服务器获取请求的内容,客户端需要将请求发送给代理服务器,然后再由代理服务器将请求转发给原始服务器,原始服务器接受到代理服务器的请求并处理,然后将处理好的数据转发给代理服务器,之后再由代理服务器转发发给客户端,完成整个请求过程。

正向代理的典型用途就是为在防火墙内的局域网客户端提供访问Internet的途径 , 比如:

  • 学校的局域网
  • 单位局域网访问外部资源

反向代理

反向代理方式是指代理原始服务器来接受来自Internet的链接请求,然后将请求转发给内部网络上的原始服务器,并将从原始服务器上得到的结果转发给Internet上请求数据的客户端。那么顾名思义,反向代理就是位于Internet和原始服务器之间的服务器,对于客户端来说就表现为一台服务器,客户端所发送的请求都是直接发送给反向代理服务器,然后由反向代理服务器统一调配

image-20230605170249203

防盗链

盗链 是指服务提供商自己不提供服务的内容,通过技术手段绕过其它有利益的最终用户界面(如广告),直接在自己的网站上向最终用户提供其它服务提供商的服务内容,骗取最终用户的浏览和点击率。受益者不提供资源或提供很少的资源,而真正的服务提供商却得不到任何的收益。

socket编程

传统的进程间通信借助内核提供的IPC机制进行, 但是只能限于本机通信, 若要跨机通信, 就必须使用网络通信.( 本质上借助内核-内核提供了socket伪文件的机制实现通信—-实际上是使用文件描述符), 这就需要用到内核提供给用户的socket API函数库.因为socket的伪文件机制, 所以可以使用文件描述符相关的函数read write

如下图, 一个文件描述符操作两个缓冲区, 这点跟管道是不同的, 管道是两个文件描述符操作一个内核缓冲区.

image-20220803110150586

相关结构体

socket编程用到的重要的结构体:struct sockaddr

image-20220803114648128

sockaddr结果参数使用sockaddr_in结构体变量来填充就可以了,内部划分得更细致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//struct sockaddr结构说明:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
//struct sockaddr_in结构:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};

/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
}; //网络字节序IP--大端模式

//通过man 7 ip可以查看相关说明

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

image-20220803145641364

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);
//serv.sin_addr.s_addr = htonl(INADDR_ANY);
//INADDR_ANY: 表示使用本机任意有效的可用IP(服务器可以插狠多张网卡)
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:地址不可用。通常出现在尝试连接不存在的本地地址或远程地址时。
  • EAGAINEWOULDBLOCK:非阻塞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);
//对应recv和send这两个函数flags直接填0就可以了.
//阻塞状态下,read返回0表示客户端断开连接,读取完毕
//flags也可以填写MSG_PEEK,MSG_PEEK标志可以用来读取套接字接收队列中可读的数据,一些情况会用到它,比如为了避免不阻塞而先检查套接字接收队列中可读的数据长度,再采取相应操作。

注意: 如果写缓冲区已满, 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
/*
* writen - 发送固定字节数数据完全
* @buf: 发送缓冲区
* @count: 要读取的字节数
* 成功返回count,失败返回-1
* 类中变量 : m_fd
*/
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;
}

/*
* readn - 读取固定字节数完全
* @fd: 文件描述符
* @buf: 接收缓冲区
* @count: 要读取的字节数
* 成功返回count,失败返回-1,读到EOF返回<count
* 类中变量 : m_fd
*/
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 端口号来查看该端口对应的连接情况

截屏2022-08-05 15.51.04

netstat -tunlp命令查看网络连接情况,仅服务器

截屏2022-08-05 15.49.14

ESTABLISHED表示链接建立了

mac下netstat命令为简化版,用lsof命令取而代之

截屏2022-08-05 15.54.43

案例

服务器开发流程

image-20220803145641364

  1. 创建socket,返回一个文件描述符lfd – socket()

    ​ – 该文件描述符用于监听客户端连接

  2. 将lfd和IP PORT进行绑定 – bind()

  3. 将lfd由主动变为被动监听 – listen()

  4. 接受一个新的连接,得到一个文件描述符 – accept()

    ​ – 该文件描述符是用于和客户端进行通信的

  5. 读写

  6. 关闭文件描述符 – close(lfd) close(cfd)

客户端开发流程

  1. 创建socket,返回一个文件描述符cfd – socket()

    ​ —该文件描述符是用于和服务端通信

  2. 连接服务器 –connect()

  3. 读写

  4. 关闭文件描述符 – 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);//表示使用本地任意可用iP
bind(lfd,(sockaddr*)&serv,sizeof(serv));

listen(lfd,2);
std::cout<<"等待客户端链接..."<<std::endl;
int fd =accept(lfd,NULL,NULL);//不关心连接的对方客户端信息
std::cout<<"客户端已链接"<<std::endl;
//开始读写
//char str[]="你好,世界!";
//send(lfd,str,strlen(str)+1,0);
//读数据
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> /* See NOTES */
#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;
}

windows代码

代码上和linux有一定差异,但差异基本不大

服务器

  1. 加载套接字库,创建套接字(WSAStartup()/socket())
  2. 绑定套接字到一个IP地址和一个端口上(bind())
  3. 将套接字设置为监听模式等待连接请求(listen())
  4. 请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
  5. 用返回的套接字和客户端进行通信(send()/recv())
  6. 返回,等待另一个连接请求;
  7. 关闭套接字,关闭加载的套接字库(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[])
{
//初始化WSA
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;
}

//绑定IP和端口
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;
}

客户端

  1. 加载套接字库,创建套接字(WSAStartup()/socket())
  2. 向服务器发出连接请求(connect())
  3. 和服务器进行通信(send()/recv())
  4. 关闭套接字,关闭加载的套接字库(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(); //string转const char*
//char * sendData = "你好,TCP服务端,我是客户端\n";
send(sclient, sendData, strlen(sendData), 0);
//send()用来将数据由指定的socket传给对方主机
//int send(int s, const void * msg, int len, unsigned int flags)
//s为已建立好连接的socket,msg指向数据内容,len则为数据长度,参数flags一般设0
//成功则返回实际传送出去的字符数,失败返回-1,错误原因存于error
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,这个错误我们要忽略.

防止阻塞被信号打断

acceptread, 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; //usigned int 剩余未读取的字节数
ssize_t nread; //int 实际读到的字节数
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')//结束标识为\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){
//如果这样使用 0.0.0.0,任意ip将可以连接
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);//表示使用本地任意可用iP
bind(lfd,(sockaddr*)&serv,sizeof(serv));

listen(lfd,2);
std::cout<<"等待客户端链接..."<<std::endl;
again:
int fd =accept(lfd,NULL,NULL);//不关心连接的对方客户端信息
if(fd==-1){
//std::cout<<"获取链接失败"<<fd<<std::endl;
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;
}
//开始读写
//char str[]="你好,世界!";
//send(lfd,str,strlen(str)+1,0);
//读数据
std::cout<<fds.size()<<std::endl;
for (size_t i = 0; i < fds.size(); i++)
{
std::cout<<fds[i]<<std::endl;
/* code */
}
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方法,可以解决上述问题

多进程实现

不设置非阻塞状态的话都要考虑防止阻塞被信号打断

  1. 注册子进程回收
  2. 创建socket,得到一个监听的文件描述符lfd — socket()
  3. 将lfd和IP和端口port进行绑定 — bind()
  4. 设置监听 — listen()
  5. 进入while循环
    1. 接受新客户端到来 — accept()
    2. fork子进程收发数据
      1. 子进程关闭监听文件描述符后收发数据,最后要设置关闭描述符退出子进程exit(0);read()/write()
      2. 父进程关闭通信文件描述符

下面代码结合了防止阻塞被信号打断的代码

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)//全部子进程回收完毕
{
//std::cout<<"全部子进程回收完毕"<<std::endl;
}
}

int main2()
{
while (1)
{
char buf[99];
gets(buf);
cout<<buf<<endl;
cout<<kmpSearch(buf,"kill")<<endl;
}
return 0;
}

//解析sockaddr_in结构提取[ip:端口]字符串
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));
//std::cout<<str<<std::endl;
}

//服务器端
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);//表示使用本地任意可用iP
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;
}
截屏2022-08-21 16.49.03

多线程实现

不设置非阻塞状态的话都要考虑防止阻塞被信号打断

  1. 注册子进程回收

  2. 创建socket,得到一个监听的文件描述符lfd — socket()

  3. 将lfd和IP和端口port进行绑定 — bind()

  4. 设置监听 — listen()

  5. 进入while循环

    1. 接受新客户端到来 — accept()

    2. pthread_create创建子线程收发数据

      1. 收发数据 —read()/write()
      2. 关闭传入的描述符
    3. 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>

//解析sockaddr_in结构提取[ip:端口]字符串
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));
//std::cout<<str<<std::endl;
}
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;
//pthread_exit()该函数也可以
}
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,//socket返回的套接字
SOL_SOCKET,//通用的套接字选项
SO_REUSEADDR,//如果*optval为0,重用bind中的地址;SO_REUSEPORT这个重用端口的也可以,但用得比较少
&opt,//根据optname的不同指向一个数据结构或一个整数,比如说一些选项是[int的非0/0]对应[选项启用/禁用]开关.
sizeof(int));//optval指向的对象的大小
//比如说设置端口复用如下:
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))];//1024对应1024个文件描述符
//long int __fds_bits[16];
}

从上面的文件中可以看出, 这几个宏本质上还是位操作.(与信号集操作类似)

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);//阻塞监控变化,不需要考虑返回0的情况,因为返回>0和<0才会解除堵塞(如果select最后一个参数填写了超时时间设置的话,需要考虑=0的情况)
if(fdChangeNum<0)
{
if(errno==EINTR)//信号被中断
{
continue;
}
break;//出问题返回(并未做什么完善的处理)
}
//有客户端连接请求到来
if(FD_ISSET(lfd,&setR))//如果lfd有响应
{
std::cout<<"客户端连接存在,返回:"<<fdChangeNum<<std::endl;
int cfd = accept(lfd,NULL,NULL);
std::cout<<"接受到新的连接,文件描述符为"<<cfd<<std::endl;
//将cfd加入到委托内核监控中
FD_SET(cfd,&setReal);
//调整监控文件描述符范围,将新加入的accept接受到的描述符添加入委托内核监控的范围
if(maxfd<cfd)
{
maxfd = cfd ;
}
//如果只有一个描述符变化了,而且是在有客户端连接请求到来的上下文执行中
//则此处说明只有这一个连接请求导致的描述符变化,因此这样情况下后面代码可以略过
if(--fdChangeNum==0)//如果只剩最后一个就无需在执行后面了
{
continue;
}
}
char str[99]={0};
//有客户端发数据过来
for (int i = lfd+1; i <= maxfd; i++)//i表示accept接受的fd
{
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个的限制

  1. poll函数
  2. epoll多路IO模型
  3. 了解poll ET/LT触发模式并实现
  4. 理解epoll边缘非阻塞模式并实现
  5. 了解epoll反应堆模型设计思想
  6. 能看懂epoll反应堆模型的实现代码

linux下常用epoll unix下常用select

效率来看: epoll>poll>select

多路IO-poll

跟select类似, 监控内核监控事件,实现多路IO, 但poll不能跨平台

1
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明

  • fds: 传入传出参数, 实际上是一个结构体数组

    1
    2
    3
    4
    5
    6
    struct pollfd 
    {
    int fd; /* file descriptor */ //监控的文件描述符
    short events; /* requested events */ //要监控的事件---不会被修改
    short revents; /* returned events */ //返回发生变化的事件 ---由内核返回
    };
    • fds.fd: 要监控的文件描述符

    • fds.events:(多个之间用或相连)

      POLLIN—->读事件

      POLLOUT—->写事件

      POLLERR—->异常事件

      POLLIN|POLLHUP —->管道断开连接

      其他更多宏参考 man 2 poll

    • fds.revents: 返回的事件

  • nfds: 数组实际有效内容的个数,就是fds数组下标的最大值+1

  • timeout: 超时时间, 单位是毫秒.

    • -1:永久阻塞, 直到监控的事件发生
    • 0: 不管是否有事件发生, 立刻返回
    • >0: 直到监控的事件发生或者超时

返回值

  • 成功:返回就绪事件的个数
  • 失败: 返回-1

timeout=0, poll函数不阻塞,且没有事件发生, 此时返回-1, 并且errno=EAGAIN, 这种情况不应视为错误.

说明

  1. 当poll函数返回的时候, 结构体当中的fdevents没有发生变化, 究竟有没有事件发生由revents来判断, 所以poll是请求和返回分离.
  2. struct pollfd结构体中的fd成员若赋值为-1, 则poll不会监控.
  3. 相对于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
//修改的话,直接修改1024处的数字.
//hard表示可以修改的最大限制

代码

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;
//将新文件描述符加入poll监控
//寻找数组中空出来的位置
size_t i;
for (i = 0; i < 1024; i++)
{
if (client[i].fd == -1)
{
client[i].fd = cfd;
client[i].events = POLLIN;
//修改client数组下标最大值
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))//检测到连接断开的方式之一
{
//std::cout<<"断开了解"<<std::endl;
}
if (client[j].revents & POLLIN)//这里必须是&,如果是==无法检测到其中的连接断开的情况
{
int iReadRet = Read(client[j].fd, str, sizeof(str));
std::cout<<iReadRet<<std::endl;
if (iReadRet <= 0)//检测到连接断开的方式之二
{
//std::cout<<client[j].revents<<std::endl;
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 events */
//常用如下:(多选项通过|(按位或)结合)
//EPOLLIN:监听可读事件
//EPOLLOUT:监听可写事件
//EPOLLERR:监听异常事件
//EPOLLET: 边缘触发
epoll_data_t data; /* User data variable 就是上面那个epoll_data结构体*/
};

p.s. 客户端关闭既会触发服务器的EPOLLIN事件,也会触发EPOLLOUT事件

EPOLLOUT的理解:传输大量数据的时候,没有办法一次将数据全部发送出去就需要将剩下的数据缓存起来,等内核通知缓冲区可写的时候再继续发送(EPOLLOUT事件表示fd的发送缓冲区可写,在一次发送大量数据(超过发送缓冲区大小)的情况下很有用)

你需要将一个10G大小的文件返回给用户,那么你简单send这个文件是不会成功的。

这个场景下,你send 10G的数据,send返回值不会是10G,而是大约256k,表示你只成功写入了256k的数据。接着调用send,send就会返回EAGAIN,告诉你socket的缓冲区已经满了,此时无法继续send。

此时异步程序的正确处理流程是调用epoll_wait,当socket缓冲区中的数据被对方接收之后,缓冲区就会有空闲空间可以继续接收数据,此时epoll_wait就会返回这个socket的EPOLLOUT事件,获得这个事件时,你就可以继续往socket中写出数据。

epoll_wait函数

等待内核返回事件发生

1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数说明:

  • epfd: epoll树根
  • events: 传出参数, 其实是一个事件结构体数组(epoll_event数组结构体)
  • maxevents: epoll_event数组的项最大数量
  • timeout
    • -1: 表示永久阻塞
    • 0: 立即返回
    • >0: 表示超时等待事件

返回值:

  • 成功: 返回发生事件的个数
  • 失败: 若timeout=0, 没有事件发生则返回; 返回-1, 设置errno值

**[注意] **epoll_waitevents是一个传出参数, 调用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, 且errnoEAGAIN, 则表示当前资源不可用, 也就是说缓冲区无数据(缓冲区的数据已经读完了); 或者当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;//设置为ET模式
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
//将cfd设置为非阻塞
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);//测试按照两个两个读,用于测试ET模式
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指针指向的结构体中有回调函数, 最终可以调用这个回调函数.

image-20220916141825428

其实就是自己定义结构记录回调等信息放入ptr指向中(原本用于存放fd的联合体),在main中根据epoll通知,取ptr信息来自己调用回调以此实现了一个架构体系

核心代码如下:

image-20220917161147604

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
// epoll基于非阻塞I/O事件驱动
#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; //是否在监听:1->在红黑树上(监听), 0->不在(不监听)
char buf[BUFLEN];
int len;
long last_active; //记录每次加入红黑树 g_efd 的时间值
};

int g_efd; //全局变量, 保存epoll_create返回的文件描述符
int g_lfd; //全局变量, 保存监听的文件描述符
struct myevent_s g_events[MAX_EVENTS+1]; //自定义结构体类型数组. +1-->listen fd


/*将结构体 myevent_s 成员变量 初始化*/
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;
//memset(ev->buf, 0, sizeof(ev->buf));
//ev->len = 0;
ev->last_active = time(NULL); //调用eventset函数的时间 unix时间戳

return;
}

/* 向 epoll监听的红黑树 添加一个 文件描述符 */
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; //EPOLLIN 或 EPOLLOUT

if(ev->status == 1)
{ //已经在红黑树 g_efd 里
op = EPOLL_CTL_MOD; //修改其属性
}
else
{ //不在红黑树里
op = EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1
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 ;
}

/* 从epoll 监听的 红黑树中删除一个 文件描述符*/

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); //从红黑树 efd 上将 ev->fd 摘除

return ;
}

/* 当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */
// 回调函数 - 监听的文件描述符发送读事件时被调用
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 while(0)的目的是为了避免使用goto语句
do
{
for (i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素
{
if (g_events[i].status == 0) //类似于select中找值为-1的元素
{
break; //找到第一个能用的 //跳出 for
}
}

if (i == MAX_EVENTS)
{
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break; //避免goto, 跳出do while(0) 不执行后续代码
}

//将cfd设置为非阻塞
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;//避免goto
}

/* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */
eventset(&g_events[i], cfd, recvdata, &g_events[i]);

//将cfd添加到红黑树g_efd中,监听读事件
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));//读文件描述符, 数据存入myevent_s成员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); //设置该 fd 对应的回调函数为 senddata
eventadd(g_efd, EPOLLOUT, ev); //将fd加入红黑树g_efd中,监听其写事件
}
else if (len == 0)
{
Close(ev->fd);
/* ev-g_events 地址相减得到偏移元素位置 */
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); //从红黑树g_efd中移除
eventset(ev, fd, recvdata, ev); //将该fd的 回调函数改为 recvdata
eventadd(g_efd, EPOLLIN, ev); //从新添加到红黑树上, 设为监听读事件
}
else
{
Close(ev->fd); //关闭链接
eventdel(g_efd, ev); //从红黑树g_efd中移除
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}

return;
}

/*创建 socket, 初始化lfd */

void initlistensocket()
{
//创建socket
g_lfd = Socket(AF_INET, SOCK_STREAM, 0);

//对事件结构体赋值
/* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */
eventset(&g_events[MAX_EVENTS], g_lfd, acceptconn, &g_events[MAX_EVENTS]);//仅仅是对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); //创建红黑树,返回给全局 g_efd
if(g_efd<0)
{
perror("create epoll error");
return -1;
}

//socket-bind-listen-初始化socket并将监听文件描述符上树
initlistensocket();

struct epoll_event events[MAX_EVENTS+1]; //保存已经满足就绪事件的文件描述符数组

int checkpos = 0, i;
while (1)
{
/* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */
long now = time(NULL); //当前时间
//一次循环检测100个。 使用checkpos控制检测对象
for (i = 0; i < 100; i++, checkpos++)
{
if (checkpos == MAX_EVENTS)
{
checkpos = 0;
}

if (g_events[checkpos].status != 1) //不在红黑树 g_efd 上
{
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]); //将该客户端 从红黑树 g_efd移除
}
}

/*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/
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++)
{
/*使用自定义结构体myevent_s类型指针,接收联合体data的void *ptr成员*/
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->arg);
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->arg);
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; //usigned int 剩余未读取的字节数
ssize_t nread; //int 实际读到的字节数
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){
//如果这样使用 0.0.0.0,任意ip将可以连接
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;//是否关闭线程池的标识 0表示不摧毁线程池,1表示摧毁线程池
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)//此处还要再判断任务数量的原因是:会有多个线程堵塞在上面pthread_cond_wait处,直接会有多个线程同时过了上一个任务数量检测,因此此处才是真正的确保有任务才执行
{
//为什么要拷贝?避免任务被修改,生产者会添加任务
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);
//printf("arg:%d",(int)theTask->arg);
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;
//printf("%s %d,%d\n",__FUNCTION__,pool->maxJobNum,pool->jobNum);
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++)
{
/* code */
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);//通知所有子线程解除阻塞诱杀
//由于已经设置了线程分离,因此下面代码不需要了
// for (int i = 0; i < pool->threadNum; i++)
// {
// pthread_join(pool->threads[i],NULL);
// }
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);//先加锁
//printf("%s 任务编号为:%d\n",__FUNCTION__,(int)arg);
//printf("%s %d<=%d\n",__FUNCTION__,pool->maxJobNum,pool->jobNum);

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 /*10s检测一次线程空闲情况*/
#define MIN_WAIT_TASK_NUM 10 /*如果queue_size > MIN_WAIT_TASK_NUM 添加新的线程到线程池*/
#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; /* 记录忙状态线程个数的锁 锁的目标为 - busyThrNum */

pthread_cond_t queueNotFull; /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
pthread_cond_t queueNotEmpty; /* 任务队列里不为空时,通知等待任务的线程 */

pthread_t *threads; /* 存放线程池中每个线程的tid。数组 */
pthread_t adjustTid; /* 存管理线程tid 管理者线程工作:根据任务数多少动态调节子线程数量 */
threadpool_task *taskQueue; /* 任务队列(数组首地址) */

int minThrNum; /* 线程池最小线程数 */
int maxThrNum; /* 线程池最大线程数 */
int liveThrNum; /* 当前存活线程个数 */
int busyThrNum; /* 忙状态线程个数 */
int waitExitThrNum; /* 要销毁的线程个数 */

int queueFront; /* taskQueue队头下标(执行任务的下标) */
int queueRear; /* taskQueue队尾下标(插入任务的下标) */
int queueSize; /* taskQueue队中实际任务数 */
int queueMaxSize; /* taskQueue队列可容纳任务数上限 */

int shutdown; /* 标志位,线程池使用状态,true或false */
}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);
//准备执行任务,忙碌数+1
pthread_mutex_lock(&pool->busyThrNum);
pool->busyThrNum++;
pthread_mutex_unlock(&pool->busyThrNum);
(*task.function)(task.arg);
//执行完任务,忙碌数-1
pthread_mutex_lock(&pool->busyThrNum);
pool->busyThrNum--;
pthread_mutex_unlock(&pool->busyThrNum);
}
}

//判断线程是否还存活
int isThreadAlive(pthread_t tid)
{
int kill_rc = pthread_kill(tid,0);//发0号信号,测试线程是否存活

if(kill_rc!=0)
{
if(kill_rc == ESRCH)//线程id不存在,表示线程未存活
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);
//printf("[%s():%d] manager get info...\n",__FUNCTION__,__LINE__);
//创建新线程算法 (任务数大于设置的最小等待任务个数,且存活的线程数少于最大线程数)
if(queueSize>=MIN_WAIT_TASK_NUM && liveThrNum<pool->maxThrNum)
{
pthread_mutex_lock(&pool->lock);
int add = 0;
//在线程数组中找空位或者已经死亡的线程位置插入DEFAULT_THREAD_VARY个新线程
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);
}
//销毁多余线程算法 忙碌线程小于存活线程的一半并且存活线程大于最小线程,就清理一部分线程(EFAULT_THREAD_VARY<liveThrNum为自行添加,感觉需要)
if ((busyThrNum * 2) < liveThrNum && liveThrNum > pool->minThrNum&&DEFAULT_THREAD_VARY<liveThrNum)
{
/* 一次销毁DEFAULT_THREAD个线程, 隨機10個即可 */
pthread_mutex_lock(&(pool->lock));
pool->waitExitThrNum = DEFAULT_THREAD_VARY; /* 要销毁的线程数 设置为10 */
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;
}

//创建线程池 queueMaxSize-任务最大个数
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;
//为线程id数组开辟空间
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);//此处的do while结构仅仅是避免使用goto(无需循环作用),使break代替goto的作用
threadpool_free(pool);//前面代码调用失败,释放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;
}

截取一点点的效果图(效果过长)

截屏2022-09-20 15.54.01

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

image-20220920172923105

UDP服务器编码流程:

  1. 创建套接字 type=SOCK_DGRAM (datagrams数据报) – socket
  2. 绑定ip和端口 – bind
  3. 收消息–recvfrom 发消息–sendto
  4. 关闭套接字–close

UDP客户端编码流程:

  1. 创建套接字 type=SOCK_DGRAM (datagrams数据报) – socket
  2. 收消息–recvfrom 发消息–sendto
  3. 关闭套接字–close

测试命令

1
nc -u ip号 端口号

案例

下面是服务端收,客户端发简单案例

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>
//解析sockaddr_in结构提取[ip:端口]字符串
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));
//std::cout<<str<<std::endl;
}

int main(int argc,char* argv[])
{
//创建socket
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的流程

image-20220922114207752

  • 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));//lfd为socket函数返回

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[])
{
//创建socket
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"//要指向服务端创建的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[])
{
//创建socket
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文件.(供服务端标识发给谁)

截屏2022-09-23 11.21.57

长度固定为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. 1.4.x 系列, 较为早期版本, 适合源码学习
  2. 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:

  1. 解压: tar -zxvf libevent-2.1.12-stable.tar.gz
  2. cd到libevent-2.1.12-stable目录下, 查看README文件, 该文件里描述了安装的详细步骤, 可参照这个文件进行安装.

进入源码目录:

  1. 执行配置./configure, 检测安装环境, 生成makefile.

    执行./configure的时候也可以指定路径, ./configure --prefix=/usr/xxxxx, 这样就可以安装到指定的目录下, 但是这样在进行源代码编译的时候需要指定用-I头文件的路径和用-L库文件的路径. 若默认安装不指定–prefix, 则会安装到系统默认的路径下, 编译的时候可以不指定头文件和库文件所在的路径.

  2. 执行make命令编译整个项目文件.

    通过执行make命令, 会生成一些库文件(动态库和静态库)和可执行文件.

  3. 执行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.h的L:337 可以找到
返回值:
成功返回event_base结构体指针;
失败返回NULL;
event_base_free函数

释放event_base指针

1
void event_base_free(struct event_base *);   //event.h的L:561
event_reinit函数

如果有子进程, 且子进程也要使用base, 则子进程需要对event_base重新初始化, 此时需要调用event_reinit函数.

1
2
3
4
5
6
int event_reinit(struct event_base *base); //event.h的L:349
函数参数:
由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方法和当前所使用的方法:

image-20220923154028865

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.h的L:660

参数说明:

  • base: 由event_base_new函数返回的指向event_base结构的指针

  • flags的取值:

    • 默认不设置时等同于event_base_dispatch

    • #define EVLOOP_ONCE 0x01

      只触发一次, 如果事件没有被触发, 阻塞等待

    • #define EVLOOP_NONBLOCK 0x02

      非阻塞方式检测事件是否被触发, 不管事件触发与否, 都会立即返回.

event_base_dispatch函数

进入循环等待事件(阻塞)

1
2
3
int event_base_dispatch(struct event_base *base);   //event.h的L:364
函数参数:
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库的流程

  1. 创建根节点–event_base_new

  2. 设置监听事件和数据可读可写的事件的回调函数

    设置了事件对应的回调函数以后, 当事件产生的时候会自动调用回调函数

  3. 事件循环–event_base_dispatch

    相当于while(1), 在循环内部等待事件的发生, 若有事件发生则会触发事件对应的回调函数。

  4. 释放根节点–event_base_free

    释放由event_base_new和event_new创建的资源, 分别调用event_base_free和event_free函数.

事件驱动event

事件驱动实际上是libevent的核心思想

image-20220923181138553

主要几个状态:

  • 无效的指针: 此时仅仅是定义了 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 //边缘触发, 如果底层模型支持设置则有效, 若不支持则无效.
    //若要想设置持续的读事件则: EV_READ | EV_PERSIST
  • 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服务器

  1. 创建socket — socket

  2. 设置端口复用 — setsockopt

  3. 绑定 — bind

  4. 设置监听 — listen

  5. 创建地基 — event_base_new

  6. 创建lfd对应的事件 — event_new

    该事件编写回调函数为accept接受新描述符,构造新事件(处理读写的回调函数)并上树

  7. 第六步事件上树(就是上地基) — event_add

  8. 进入事件循环 — event_base_dispatch

  9. 退出循环(可以写到回调里面) — event_base_free

  10. 释放资源 — event_free

image-20220926162016068

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
//编写libevent服务端
#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 };//用于记录要下树的对应event

//typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
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);
//将通信文件描述符对应的事件从base地基上删除
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");
//创建通信文件描述符对应的事件并设置回调函数为readcb
connevs[cfd] = event_new(base, cfd, EV_READ|EV_PERSIST, readcb, NULL);
if(connevs[cfd]==NULL)
{
//退出循环
event_base_loopexit(base, NULL);
}
//将通信文件描述符对应的事件上event_base地基
event_add(connevs[cfd], NULL);
}
}

int main()
{
//创建socket
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 *event_new(struct event_base *base, evutil_socket_t fd, short events, event_callback_fn cb, void *arg);
struct event *ev = event_new(base, lfd, EV_READ|EV_PERSIST, conncb, base);
if(ev==NULL)
{
printf("event_new error\n");
return -1;
}
//将新的事件节点上base地基
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事件驱动的核心回调函数, 那么四个缓冲区以及触发回调的关系如下:

image-20220925164248163

从图中可以得知, 一个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函数

该函数客户端使用,封装了底层的socketconnect接口, 通过调用此函数, 可以将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);
//bev bufferevent指针
//ctx 上下文指针(context)

//事件回调
typedef void(*bufferevent_event_cb)(struct bufferevent *bev, short what, void*ctx);
/*
What 代表 对应的事件
BEV_EVENT_EOF--遇到文件结束指示
BEV_EVENT_ERROR--发生错误,比如说客户端连接一个未开启的socket
BEV_EVENT_TIMEOUT--发生超时
BEV_EVENT_CONNECTED--请求的过程中连接已经完成,比如说客户端连接服务器成功
*/
//客户端断开连接时what为 BEV_EVENT_READING | BEV_EVENT_EOF
bufferevent_write函数

将data的数据写到bufferevent的写缓冲区

1
int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);

size 为写多长

bufferevent_write_buffer函数

将data的数据写到bufferevent的写缓冲区的另外一个写法

1
int bufferevent_write_buffer(struct bufferevent *bufev, struct evbuffer *buf);

实际上 ,bufferevent的内部的两个缓冲区结构就是struct evbuffer

bufferevent_read函数

将bufferevent的读缓冲区数据读到data中, 同时将读到的数据从bufferevent的读缓冲清除。

1
size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);

size为要读的最大长度

返回实际读到的字节数

bufferevent_read_buffer函数

将bufferevent读缓冲数据读到buf中, 接口的另外一种。

1
int bufferevent_read_buffer(struct bufferevent *bufev, struct evbuffer *buf);

size为要读的最大长度

返回实际读到的字节数

bufferevent_enable函数

设置事件生效,使回调会被触发

1
int bufferevent_enable(struct bufferevent *bufev, short event);

​ event参数参考event_new函数的events参数

bufferevent_disable函数

设置事件不生效,使回调不会被触发

1
int bufferevent_disable(struct bufferevent *bufev, short event);

同上

bufferevent_get_output函数

获取bufferevent里的写缓冲区指针

1
struct evbuffer* bufferevent_get_output(struct bufferevent* bufev);
bufferevent_get_input函数

获取bufferevent里的读缓冲区指针

1
struct evbuffer* bufferevent_get_input(struct bufferevent* bufev);
evbuffer_get_length函数

获取bufferevent里的缓冲区数据长度

1
int evbuffer_get_length(struct evbuffer* buf)
链接监听器

链接监听器封装了底层的socket通信相关函数, 比如socket, bind, listen, accept这几个函数。链接监听器创建后实际上相当于调用了socket, bind, listen, 此时等待新的客户端连接到来, 如果有新的客户端连接, 那么内部先进行调用accept处理, 然后调用用户指定的回调函数。可以先看看函数原型, 了解一下它是怎么运作的:

所在头文件: event2/listener.h

链接监听器相关函数
evconnlistener_new_bind函数

当前没有套接字的情况下对链接监听器进行初始化

1
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_newoptions参数(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和链接器服务器流程
主流程
  1. 创建地基 — event_base_new
  2. 创建socket,设置复用,listen,监听描述符上树实现监听调用accept(有客户端链接会调用回调函数<客户端连接回调流程>) — evconnlistener_new_bind
  3. 进入事件循环,等待事件发生 — event_base_dispatch
  4. 跳出循环,释放资源 — evconnlistener_free;event_base_free
客户端连接回调流程
  1. 创建bufferevent(将通信文件描述符和bufferevent绑定) — bufferevent_socket_new
  2. 设置回调函数:可以是读回调;写回调;信号事件回调 — bufferevent_setcb
  3. 使回调函数生效(不写也行,默认生效) — bufferevent_enable
读回调流程
  1. 读bufferevent的读缓冲区数据 — bufferevent_read
  2. 其他业务流程

image-20220926162128957

最终代码案例
服务器代码
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
//编写libevent服务端
#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>

//解析sockaddr_in结构提取[ip:端口]字符串
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));
//std::cout<<str<<std::endl;
}

//typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
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");
}
}

//通过ptr指针将struct event_base指针传入进来
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);//此处的参数位无法将cliaddr传过去read_cb解析ip地址(应该已经被释放了)
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;
}
//创建事件连接监听器,如果有客户端连接会调用listener_cb回调函数
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
//编写libevent客户端
#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>

//解析sockaddr_in结构提取[ip:端口]字符串
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));
//std::cout<<str<<std::endl;
}

//监控cmd输入回调
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;//传入buffereventt指针,目的是为了写到bufferevent的写缓冲区
//发给服务端
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)
{
//printf("事件回调:%hu\n",what);//连接成功为128,服务器断开为17
if(what & BEV_EVENT_EOF)
printf("连接断开\n");
else if(what & BEV_EVENT_ERROR)
printf("发生错误\n");
else if (what & BEV_EVENT_CONNECTED)
{
printf("连接成功\n");
return;
}
//试图做清理,其实此处应该是将base传进来跳出循环才对
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);
//新建监控标准输入事件 (往回调中传入bufferevent的指针,目的是为了写缓冲区)
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复用技术:既不使用多线程,也不使用多进程,在一个进程或一个线程中让多个客户端同时请求服务(都是委托内核进行监控,若有事件发生则通知应用程序)

客户端优化

客户端的效率优化

  1. 单进程只处理一个连接

  2. 多线程使用同一个连接

  3. 多线程使用多个连接 (使用到连接池)

    缺点:如果频繁创建连接和销毁连接会有时间消耗

  4. 连接池+线程池(避免上述缺点)

连接池只用于客户端

连接池不用于服务端,因为连接是只有客户端发起连接请求之后才会有的.

连接池思想

  1. 一个数据结构保存连接信息
  2. 创建连接池操作—poolInit()
  3. 获取连接池操作 (如队头取)
  4. 使用完将连接存放的操作 (如存到队尾)
  5. 可以根据实际需要动态调整连接的数量
  6. 销毁连接池

在网络编程中,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:
// 设置I/O为非阻塞模式
int blockIO(int fd);
// 设置I/O为阻塞模式
int noBlockIO(int fd);
// 读超时检测函数,不含读操作
int readTimeout(unsigned int wait_seconds);
// 写超时检测函数, 不包含写操作
int writeTimeout(unsigned int wait_seconds);
// 带连接超时的connect函数
int connectTimeout(struct sockaddr_in *addr, unsigned int wait_seconds);
// 每次从缓冲区中读取n个字符
int readn(void *buf, int count);
// 每次往缓冲区写入n个字符
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) // 表示可以正常写了
{
// 开辟空间容纳待发送的数据(头4个字节用来存发送的数据长度)
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); // 头4个空间放传送数据的长度
memcpy(netdata + 4, sendData, dataLen);
writen(netdata, dataLen + 4);
if (netdata != NULL)
{
free(netdata);
netdata = NULL;
}
}
if (ret < 0)
{
// 失败返回-1,超时返回-1并且errno = ETIMEDOUT
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); // 读包头 4个字节
if (ret == -1)
{
// printf("func readn() err:%d \n", ret);
return ret;
}
else if (ret < 4)
{
ret = PeerCloseError;
// printf("func readn() err peer closed:%d \n", ret);
return ret;
}
int n = ntohl(netdatalen);
char *tmpBuf = (char *)malloc(n + 1); // 多分配一个字节内容,兼容可见字符串 字符串的真实长度仍然为n
if (tmpBuf == NULL)
{
ret = MallocError;
return ret;
}
ret = readn(tmpBuf, n); // 根据长度读数据
if (ret == -1)
{
// printf("func readn() err:%d \n", ret);
return ret;
}
else if (ret < n)
{
ret = PeerCloseError;
// printf("func readn() err peer closed:%d \n", ret);
return ret;
}

*recvData = tmpBuf;
recvLen = n;
tmpBuf[n] = '\0'; // 多分配一个字节内容,兼容可见字符串 字符串的真实长度仍然为n
return 0;
}

void TcpClient::disConnect()
{
if (m_fd >= 0)
{
close(m_fd);
}
}

void TcpClient::freeMemory(char **buf) // 释放内存,并将指针置空
{
if (*buf != NULL)
{
free(*buf);
*buf = NULL;
}
}

/*
* writeTimeout - 写超时检测函数,不含写操作
* @wait_seconds: 等待超时秒数,如果为0表示不检测超时
* 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
*/
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;
}

/*
* connectTimeout - connect
* @addr: 要连接的对方地址
* @wait_seconds: 等待超时秒数,如果为0表示正常模式
* 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
*/
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)); // 真正的连接connect函数
if (ret < 0 && errno == EINPROGRESS)
{
fd_set connect_fdset;
struct timeval timeout; // select函数需要用的控制阻塞时间的结构体
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)
{
/* ret返回为1(表示套接字可写),可能有两种情况,
一种是连接建立成功,一种是套接字产生错误,*/
/* 此时错误信息不会保存至errno变量中,
因此,需要调用getsockopt来获取。 */
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;
}

/*
* readn - 读取固定字节数
* @fd: 文件描述符
* @buf: 接收缓冲区
* @count: 要读取的字节数
* 成功返回count,失败返回-1,读到EOF返回<count
*/
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;
}

/*
* writen - 确保数据发送完全
* @buf: 发送缓冲区
* @count: 要读取的字节数
* 成功返回count,失败返回-1
*/
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;

// select返回值三态
// 1 若timeout时间到(超时),没有检测到读事件 ret返回=0
// 2 若ret返回<0 && errno == EINTR 说明select的过程中被别的信号中断(可中断睡眠原理)
// 2-1 若返回-1,select出错
// 3 若ret返回值>0 表示有read事件发生,返回事件发生的个数

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>

// 超时的时间
// static const int TIMEOUT = 10000;

class TcpServer
{
public:
TcpServer();
~TcpServer();

// 服务器设置监听
int setListen(unsigned short port);
// 等待并接受客户端连接请求, 默认连接超时时间为10000s
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;
// 设置端口复用选项为1,即打开
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;
}
}
// 一但检测出 有select事件发生,表示对方完成了三次握手,客户端有新连接建立
// 此时再调用accept将不会堵塞
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收到的数据如下:

image-20220929172726558

这个消息看起来很乱很复杂,对应的就是我们说的请求消息.
请求消息分为四部分内容:

  1. 请求行 说明请求类型,要访问的资源(请求指令或请求文件等资源,如果是get类型,问号后还会带数据),以及使用的http版本
  2. 请求头 说明服务器使用的附加信息,都是键值对,比如表明浏览器类型
  3. 空行 不能省略-而且是\r\n,包括请求行和请求头中每一行都是以\r\n结尾(\r是移动到末尾,\n是换行)
  4. 请求体(请求数据) 表明请求的特定数据内容,可以省略-如登陆时,会将用户名和密码内容作为请求数据

image-20220930134506074

1
2
3
4
5
6
7
8
9
GET / HTTP/1.1
Host: xxx.xxx.xxx.xxx:23456
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-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.36
Accept: 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.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7

请求类型

http协议有很多种请求类型,对我们来说常见的用的最多的是get和post请求。常见的请求类型如下:

  1. Get 请求指定的页面信息,请求数据被包含到url中,并返回实体主体.在http协议中数据被包含在请求行的要访问资源中

    由于get请求不存在请求体,所以他的请求头中没有Content-LengthContent-Type键值对.

  2. Post 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体(请求数据)中。POST请求可能会导致新的资源的建立和/或已有资源的修改。

  3. Head 类似于get请求,但是响应消息没有内容,只是获得报头

  4. Put 从客户端向浏览器传送的数据取代指定的文档内容

  5. Delete 请求服务器删除指定的页面

  6. Connect HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器

  7. Options 允许客户端查看浏览器的性能

  8. 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四种常用方式

  • application/x-www-form-urlencoded

    1
    2
    3
    4
    5
    6
    7
    #请求行
    POST http://www.example.com HTTP/1.1
    #请求头
    Content-Type: application/x-www-form-urlencoded;charset=utf-8
    #空行
    #请求数据(向服务器提交的数据)
    title=test&user=kevin&passwd=32222
  • application/json

    1
    2
    3
    4
    POST / HTTP/1.1
    Content-Type: application/json;charset=utf-8

    {"title":"test","sub":[1,2,3]}
  • text/xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    POST / HTTP/1.1
    Content_Type: text/xml

    <?xml version="1.0" encoding="utf8"?>
    <methodcall>
    <methodname color="red">examples.getStateName</methodname>
    <params>
    <value><i4>41</i4></value>
    </params>
    </methodcall>
  • multipart/form-data

    传输大文件(大数据块)常用的格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    POST / HTTP/1.1
    Content-Type: multipart/form-data

    #发送的数据(两张图片)
    ------WebKitFormBoundaryPpL3BfPQ4cHShsBz \r\n
    Content-Disposition: from-data; name="file";filename="qw.png"
    Content-Type: image/png\r\n; md5="xxxxxxxxx"\r\n
    ..........具体的文件内容..........
    ..........具体的文件内容..........
    ------WebKitFormBoundaryPpL3BfPQ4cHShsBz--
    Content-Disposition: from-data; name="file";filename="qw2.png"
    Content-Type: image/png\r\n; md5="xxxxxxxxx"\r\n
    ..........具体的文件内容..........
    ..........具体的文件内容..........
    ------WebKitFormBoundaryPpL3BfPQ4cHShsBz--

上面的 ..........具体的文件内容..........指代具体要传的文件内容

http响应消息

响应消息是代表服务器收到请求消息后,给浏览器做的反馈,所以响应消息是服务器发送给浏览器的,响应消息也分为四部分:

  1. 状态行 包括http版本号,状态码,状态信息
  2. 消息报头 说明客户端要使用的一些附加信息,也是键值对(Content-Type是必填的)
  3. 空行 \r\n 同样不能省略
  4. 响应正文 服务器返回给客户端的文本信息

image-20220930140603665

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
#使用requests库发送第一次HTTP请求,并获取返回的cookie信息。然后在第二次HTTP请求中,通过设置cookies参数为第一次请求获取到的cookie信息,即可在下一次请求中带上cookie信息
import requests
# 发起第一次HTTP请求获取cookie
response = requests.get('http://www.example.com')
# 获取返回的cookie信息
cookies = response.cookies.get_dict()
# 发起第二次HTTP请求时带上cookie信息
response2 = requests.get('http://www.example.com', cookies=cookies)
# 输出第二次请求的响应内容
print(response2.text)

get_dict() 是requests库中Response对象的一个方法,用于获取响应中的cookie信息并以字典形式返回。具体来说, get_dict() 方法会将响应中的所有cookie信息转换为一个字典,其中键为cookie的名称,值为cookie的值。这样可以方便地获取和处理响应中的cookie信息,并在需要时将其用于下一次请求中。

  1. cookies = response.cookies.get_dict(): 这句代码将response对象中的所有Cookie信息转换为字典格式,并将其赋值给变量cookies。字典格式的Cookie信息以键值对的形式存储,其中键是Cookie的名称,值是Cookie的值。这种方式可以方便地对Cookie进行访问和操作。

  2. cookies = response.cookies: 这句代码直接将response对象中的所有Cookie信息赋值给变量cookies,但是此时cookies并不是字典格式,而是一个RequestsCookieJar对象,它是requests库中用于存储Cookie信息的数据结构。虽然RequestsCookieJar对象提供了一些方法来操作Cookie,但直接访问和操作Cookie可能相对繁琐。

https协议

参考HTTPS加密进化的动画解说

image-20241221200512263

https = http + SSL/TLS

SSL是TlS的前身

现在绝大部分浏览器都不支持SSL,而是支持TLS,但SSL名声很大,所以很多人把名字混用)

SSL证书: 保存在服务器上的数据文件,表明域名属于谁,日期,还包含了特定的公钥和私钥,要让他生效需要向CA(Certificate Authority)申请.

CA 签发证书的过程:

  1. ⾸先 CA 会把持有者的公钥、⽤途、颁发者、有效时间等信息打成⼀个包,然后对这些信息进行 Hash 计算,得到⼀个 Hash 值;
  2. 然后 CA 会使⽤自己的私钥将该 Hash 值加密,⽣成 Certificate Signature,也就是 CA 对证书做了签名;
  3. 最后将 Certificate Signature 添加在⽂件证书上,形成数字证书;

客户端校验服务端的数字证书的过程:

  1. ⾸先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1(数字摘要);

  2. 通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使⽤ CA 的公钥解密 Certificate

    Signature 内容,得到⼀个 Hash 值 H2(数字摘要)。

  3. 最后⽐较 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 urlcurl 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协议,如下图:

image-20220930143719918

为了支持并发服务器,我们可以有多个选择,比如多进程服务器,多线程服务器,select,poll,epoll等多路IO工具都可以,甚至如果读者觉得libevent非常熟练的话,也可以使用libevent进行开发.

读取到内容后的流程

socket读取到内容后的流程如下

image-20220930151516803

[注意点]

  • 浏览器中的每一次访问都是一个独立的访问,遇上一次的访问没有关系,即浏览器的任何链接跳转,都是关闭连接后建立新连接

  • 在浏览器中访问的资源是一个中文的名称的话,需要进行编码的转换

  • web服务端资源目录取决于web服务器程序的工作目录,可以通过 chdir函数切换工作目录

  • 若浏览器关闭读端,会导致服务器进程退出,原因是当浏览器关闭读端后,而服务端还继续往一个关闭读端的链接写数据会收到到 SIGPIPE信号,这个信号的默认处理动作是使进程终止

    解决方案: 忽略该信号signal(SIGPIPE,SIG_IGN);

基于epoll的web服务器

由于我们知道epoll在大量并发少量活跃的情况下效率很高,所以本文以epoll为例,介绍epoll开发的主体流程:

image-20220930143938195

参考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>

//16进制单字符转化为10进制单字符, return 0不会出现
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>
//to和from可以指向同一个内存
void strdecode(char* to,char* from)
{
for(;*from != '\0';++to,++from){
if(from[0]=='%' && isxdigit(from[1]) && isxdigit(from[2]))//isxdigit用于判断是否是16进制数字
{
//一个%E8%8ba6的中文字符的字符串占6个字节,转换成16进制的中文字符后占3个字节
*to = hexit(from[1])*16+hexit(from[2]);//字符串%E8变成16进制的E8数值
from+=2;//结合上for中的++,总共是+3
}else
*to = *from;
}
*to = NULL;
}

//通过文件名字获得文件类型
char *get_mime_type(char *name)
{
char* dot;

dot = strrchr(name, '.'); //自右向左查找‘.’字符;如不存在返回NULL
/*
*charset=iso-8859-1 西欧的编码,说明网站采用的编码是英文;
*charset=gb2312 说明网站采用的编码是简体中文;
*charset=utf-8 代表世界通用的语言编码;
* 可以用到中文、韩文、日文等世界上所有语言编码上
*charset=euc-kr 说明网站采用的编码是韩文;
*charset=big5 说明网站采用的编码是繁体中文;
*
*以下是依据传递进来的文件名,使用后缀判断是何种文件类型
*将对应的文件类型按照http定义的关键字发送回去
*/
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";
}

//发送响应头 code=200 msg=OK fileType=Content-Type len=Content-Length
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");
//printf("发送出去的内容为:====下面====\n");
//printf("%s",buf);
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{
//printf("%s\n",buf);
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)//=0为客户端断开链接,<0为异常
{
printf("客户端已断开连接\n");
close(cfd);
FD_CLR(cfd,setReal);
return -1;
}
//printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
//printf("读到的第一行为%s\n",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=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);
//自己拼接html文件
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);
//printf("%s\n", namelist[num]->d_name);
free(namelist[num]); //由于scandir内部是malloc申请的空间,因此记得要释放
}
Write(cfd,buffer,strlen(buffer));
free(namelist);
}

sendFile(cfd, "tail.html");

}
}
//若文件存在
//判断文件类型
//普通文件
//目录文件
}

int main(int argc,char** argv)
{
//忽略SIGPIPE信号
struct sigaction act;
act.sa_handler = SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGPIPE,&act,NULL);
fd_set setReal;
//改变当前进程的工作目录
//char path[255]={0};
//sprintf("",getenv("HOME"),"")
//chdir()
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);//阻塞监控变化,不需要考虑返回0的情况,因为返回>0和<0才会解除堵塞
if(fdChangeNum<0)
{
if(errno==EINTR)//信号被中断
{
continue;
}
break;//出问题返回(并未做什么完善的处理)
}
//有客户端连接请求到来
if(FD_ISSET(lfd,&setR))//如果lfd有响应
{
//printf("客户端连接存在,返回:%d\n",fdChangeNum);
int cfd = accept(lfd,NULL,NULL);
printf("接受到新的连接,文件描述符为%d\n",cfd);
//设置cfd为非阻塞
int flag = fcntl(cfd,F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd,F_SETFL,flag);
//将cfd加入到委托内核监控中
FD_SET(cfd,&setReal);
//调整监控文件描述符范围,将新加入的accept接受到的描述符添加入委托内核监控的范围
if(maxfd<cfd)
{
maxfd = cfd ;
}
//如果只有一个描述符变化了,而且是在有客户端连接请求到来的上下文执行中
//则此处说明只有这一个连接请求导致的描述符变化,因此这样情况下后面代码可以略过
if(--fdChangeNum==0)//如果只剩最后一个就无需在执行后面了
{
continue;
}
}
char str[99]={0};
//有客户端发数据过来
for (int i = lfd+1; i <= maxfd; i++)//i表示accept接受的fd
{
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
//16进制单字符转化为10进制单字符, return 0不会出现
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>
//to和from可以指向同一个内存
void strdecode(char* to,char* from)
{
for(;*from != '\0';++to,++from){
if(from[0]=='%' && isxdigit(from[1]) && isxdigit(from[2]))//isxdigit用于判断是否是16进制数字
{
//一个%E8%8b%a6的中文字符的字符串占9个字节,转换成16进制的中文字符后占3个字节
*to = hexit(from[1])*16+hexit(from[2]);//字符串%E8(3字节)变成16进制的E8数值(1字节)
from+=2;//结合上for中的++,总共是+3
}else
*to = *from;
}
*to = NULL;
}

调用 strdecode 把代表unicode的字符串变成真正的unicode编码(其实就是16进制存储)

一些特殊符号

相关知识

html中的 空格 (space)是&nbsp

  • %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
//编写libevent服务端
#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>

//16进制单字符转化为10进制单字符, return 0不会出现
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>
//to和from可以指向同一个内存
void strdecode(char* to,char* from)
{
for(;*from != '\0';++to,++from){
if(from[0]=='%' && isxdigit(from[1]) && isxdigit(from[2]))//isxdigit用于判断是否是16进制数字
{
//一个%E8%8ba6的中文字符的字符串占6个字节,转换成16进制的中文字符后占3个字节
*to = hexit(from[1])*16+hexit(from[2]);//字符串%E8变成16进制的E8数值
from+=2;//结合上for中的++,总共是+3
}else
*to = *from;
}
*to = NULL;
}

//通过文件名字获得文件类型
char *get_mime_type(char *name)
{
char* dot;
dot = strrchr(name, '.'); //自右向左查找‘.’字符;如不存在返回NULL
/*
*charset=iso-8859-1 西欧的编码,说明网站采用的编码是英文;
*charset=gb2312 说明网站采用的编码是简体中文;
*charset=utf-8 代表世界通用的语言编码;
* 可以用到中文、韩文、日文等世界上所有语言编码上
*charset=euc-kr 说明网站采用的编码是韩文;
*charset=big5 说明网站采用的编码是繁体中文;
*
*以下是依据传递进来的文件名,使用后缀判断是何种文件类型
*将对应的文件类型按照http定义的关键字发送回去
*/
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";
}

//发送响应头 code=200 msg=OK fileType=Content-Type len=Content-Length
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");
//printf("发送出去的内容为:====下面====\n");
//printf("%s",buf);
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{
//printf("%s\n",buf);
bufferevent_write(bev,buf,n);
}
}
close(fd);
}

int httpRequest(struct bufferevent * bev)
{
char buf[1024]={0};
//读取请求行数据,分析出要请求的资源文件名
int n =bufferevent_read(bev,buf,sizeof(buf));
//printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
//printf("读到的第一行为%s\n",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);
//自己拼接html文件
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);
//printf("%s\n", namelist[num]->d_name);
free(namelist[num]); //由于scandir内部是malloc申请的空间,因此记得要释放
}
bufferevent_write(bev,buffer,strlen(buffer));
free(namelist);
}

sendFile(bev, "tail.html");

}
}
//若文件存在
//判断文件类型
//普通文件
//目录文件
}


//解析sockaddr_in结构提取[ip:端口]字符串
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));
//std::cout<<str<<std::endl;
}

//typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
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");
}
}

//通过ptr指针将struct event_base指针传入进来
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);//此处的参数位无法将cliaddr传过去read_cb解析ip地址(应该已经被释放了)
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;
}
//创建事件连接监听器,如果有客户端连接会调用listener_cb回调函数
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服务器
    • apache组织产品,开源的免费服务器
  • weblogic服务器
    • bea公司,收费的服务器
    • 不交费的话访问量受到限制
  • IIS服务器
    • Internet Information Server
    • 微软公司主推的服务器
  • nginx
    • 小巧且高效的HTTP服务器
    • 也可以做一个高效的负载均衡反向代理
    • 邮件服务器

其他的还有:(下面的都可以实现路由到函数的功能)

  • cppcms:一个轻量级、高性能的 C++ Web 框架,具有类似 Flask 的路由和模板引擎功能。
  • Pistache:一个高性能、非阻塞的 C++ Web 框架,具有类似 Flask 的路由和中间件功能。
  • oatpp:一个现代、高性能的 C++ Web 框架,具有类似 Flask 的路由和依赖注入功能。
  • Casablanca:一个跨平台、高性能的 C++ Web 框架,具有类似 Flask 的路由和异步 I/O 功能。

Flask是python用的web框架

Nginx

[[nginx#基本介绍|nginx详解跳转]]

REST API

Boost.Asio 是一个用于建立异步 I/O 应用程序的 C++ 库,它提供了网络编程、串口编程、进程间通信等功能。在这个示例中,我们使用了 Boost.Asio 库提供的异步 I/O 操作来处理请求和响应,从而构建一个简单的 REST API。

1
boost/asio.hpp 

包含了许多常用的 Boost.Asio 头文件。 -

1
boost/asio/ssl.hpp 

包含了使用 Boost.Asio 进行 SSL 通信的头文件。

CGI

通用网关接口(Common Gateway Interface)描述了客户端和服务器程序之间传输数据的一种标准,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据.CGI独立于任何语言,CGI程序可以用任何脚本语言或者是完全独立编程语言实现,只要这个语言可以在这个系统上运行

image-20230613102754891 image-20230613103012085
  1. 用户通过浏览器访问服务器,发送了一个请求
  2. 服务器接受数据并解析
  3. nginx对于一些登录数据不知道如何处理,nginx将这些数据转发给了cgi程序
    • 服务器端会创建一个cgi进程(每一个客户端请求,都会创建一个cgi进程)
  4. CGI进程执行后返回结果,返还nginx,自身销毁,nginx回复客户端.

因此CGI会频繁创建与销毁,效率低,由此fastCGI诞生.

fastCGI

快速通用网关接口(Fast Common Gateway Interface)是通用网关接口(CGI)的改进,描述了客户端和服务器程序之间传输数据的一种标准.FastCGI致力于减少Web服务器与CGI程序之间互动的开销,从而使服务器可以同时处理更多的Web请求.与为每个请求创建一个新的进程不同,FastCGI使用持续的进程来处理一连串的请求.这些进程由FastCGI进程管理器管理,而不是web服务器

与CGI的区别:

  • CGI是短生存期应用程序
  • FastCGI是长生存期应用程序,像一个常驻(long-live)型的CGI,可以一直执行,不需要每次都创建与销毁
image-20230613105633722
  1. 用户通过浏览器访问服务器,发送了一个请求
  2. 服务器接受数据并解析
  3. nginx对于一些登录数据不知道如何处理,nginx将这些数据通过套接字转发给了fastCGI进程管理器
  4. fastCGI程序启动(不是由web服务器直接启动,而是通过fastCGI进程管理器启动)
  5. fastCGI程序执行完毕后,通过套接字将处理结果返回给web服务器,服务器回复客户端
安装与配置

需要安装两个

  • fastCGI框架

    根目录/libfcgi/.libs/libfcgi++.so
    根目录/libfcgi/.libs/libfcgi.so

  • spawn-fcgi (fastCGI进程管理器)

nginx不能像apache那样直接执行外部可执行程序,但nginx可以作为代理服务器,将请求转发给后端服务器,这也是nginx的主要作用之一.其中nginx就支持fastCGI代理,接受客户端的请求,然后将请求转发给后端fastcgi进程.下面介绍如何使用C/C++编写cgi/fastcgi,并部署到nginx中.

**fastCGI**进程由fastCGI进程管理器管理,而不是nginx.这样就需要一个FastCGI管理器,管理我们编写的fastcgi程序.我们使用spawn-fcgi作为FastCGI进程管理器

**spawn-fcgi**是一个通用的FastCGI进程管理器,简单小巧,原先是属于lighttpd的一部分,后来由于使用比较广泛,所以就迁移出来作为独立新项目了.spawn-fcgi使用pre-fork模型,功能主要是打开监听端口,绑定地址,然后fork-and-exec创建我们编写的fastcgi应用程序进程,退出完成工作.fastcgi应用程序初始化,然后进入死循环侦听socket的连接请求

image-20230614121207887
  1. 客户端访问,发送请求
  2. nginx web服务器,无法处理用户提交的数据,转发数据
  3. spawn-fcgi 通信过程中的服务器角色
    • 被动接受数据
    • 在spawn-fcgi启动的时候给其绑定ip和端口
  4. fastCGI程序
    • 程序员写的login.c -> 可执行程序login
    • 使用spawn-fcgi进程管理器启动login程序得到一个进程

nginx的数据转发配置

1
2
3
4
5
6
location /login
{
#转发这个数据,fastCGI进程
fastcgi_pass 地址信息:端口;#数据转发到spawn-fcgi,地址信息可以为ip或域名
include fastcgi.conf;#导入[与nginx.conf同级目录下的fastcgi.conf文件],其中定义了一些http通信的时候用到的环境变量
}

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)
{
//接收到数据后如何处理
//根据REQUEST_METHOD
//get读请求行的第二部分,QUERY_STRING
//post读CONTENT-LENGTH
//根据CONTENT-TYPE知道长度
//从标准输入进来的就是请求体(请求数据)的内容
//打印到标准输出的就是要返回给请求方的内容
}

简单案例

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)
{ // 可能是get方式
printf("No data from standard input.<p>\n");
}
else
{ // post方式提交的数据
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");
}

} /* while */

return 0;
}

分布式服务器

image-20230511145005482

  • nginx
    • 能处理静态请求 -> html,jpg
    • 动态请求无法处理
    • 服务器集群之后,每台服务器上部署的内容必须相同
  • FastCGI
    • 帮助服务器处理动态请求
  • 反向代理服务器
    • 客户端并不能直接访问到web服务器,直接访问到的是反向代理服务器
    • 客户端将请求发送给反向代理服务器,反向代理服务器将客户端请求转发给服务器
  • 关系型[[数据库]]
    • 存储文件属性信息
    • 用户属性信息
  • redis - 非关系型[数据库]
    • 提高程序效率
    • 存储的是服务器经常要从关系型数据库中读取的数据
  • FASTDFS - 分布式文件系统
    • 存储文件内容
    • 供用户下载

分布式文件系统

文件系统的全部, 不在同一台主机上,而是在很多台主机上,多个分散的文件系统组合在一起,形成了一个完整的文件系统。

image-20230511152805626

分布式文件系统:

  1. 需要有网络
  2. 多台主机
    不需要在同一地点
  3. 需要管理者
  4. 编写应用层的管理程序
    不需要编写

常用分布式文件系统

分布式文件系统 github star os支持
minio 25.1k win/linux
fastdfs 7k win
ceph 8.6k win/linux
GlusterFS 2.9k win/linux

FastDFS

  • 是用c语言编写的一款开源的分布式文件系统。

    开发者:余庆 - 淘宝的架构师

  • 为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,注重高可用、高性能等指标

    • 冗余备份: 纵向扩容
    • 线性扩容: 横向扩容
  • 可以很容易搭建一套高性能的文件服务器集群提供文件 上传、下载 等服务。E.g.图床,网盘

fastDFS框架中的三个角色(进程)

  • 追踪器(Tracker) - 管理存储节点的管理者 - 守护进程
  • 存储节点(storage) -守护进程
  • 客户端 - 通过连接追踪器找到目标存储节点,通过连接存储节点进行上传与下载操作
    • 文件上传
    • 文件下载

三者的关系

image-20230511154527564 image-202305111547958

追踪器

最先启动追踪器

存储节点

第二个启动的角色

存储节点启动之后, 会单独开一个线程向追踪器汇报信息

  • 汇报当前存储节点的容量, 和剩余容量
  • 汇报数据的同步情况
  • 汇报数据被下载的次数

客户端

最后启动

上传

  • 连接追踪器, 询问存储节点的信息
  • 我要上传1G的文件, 询问那个存储节点有足够的容量
  • 追踪器查询, 得到结果
  • 追踪器将查到的存储节点的IP+端口发送给客户端
  • 通过得到IP和端口连接存储节点
  • 将文件内容发送给存储节点

下载

  • 连接追踪器, 询问存储节点的信息
  • 问一下, 要下载的文件在哪一个存储节点
  • 追踪器查询, 得到结果
  • 追踪器将查到的存储节点的IP+端口发送给客户端
  • 通过得到IP和端口连接存储节点
  • 下载文件
FastDFS集群
image-20230517113645120
  • 集群的优点:避免单点故障(主要是针对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
    #将追踪器和部署主机的IP地址进行绑定,也可以不写
    #如果不写会自动绑定当前主机IP,如果为云服务器建议不写
    bing_addr=
    #追踪器监听的端口(默认22122)
    port=22122
    #追踪器存储日志信息的目录,xxx.pid文件,必须是一个存在的目录,并且用户需要对该路径中的文件有读写权限
    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
    #当前存储节点和部署主机的IP进行绑定,如果为云服务器建议不写
    bing_addr=
    #存储节点监听的端口(默认23000),监听客户端的连接
    port=23000
    #存储节点存储日志信息的目录,xxx.pid文件,必须是一个存在的目录,并且用户需要对该路径中的文件有读写权限
    base_path=/home/yuqing/fastdfs
    #存储节点提供的存储文件的路径个数
    store_path_count = 1
    #具体的存储路径,上面项为2的话,存储路径还需要填写store_path1
    store_path0 = /home/yuqing/fastdfs
    #设置追踪器的ip地址与端口 (该项可以多次,即轮询多个追踪器,用于多个追踪服务器。追踪服务器的值格式为“HOST:PORT”,HOST可以是主机名或IP地址,并且HOST可以是用逗号分隔的双IP或主机名,双IP必须是内部(内网)IP和外部(外网)IP,或两种不同类型的内部(内网)IP。例如:192.168.2.100,122.244.141.46:22122 另一个例子:192.168.1.10,172.17.4.21:22122)
    tracker_server = 192.168.209.121:22122
  • 客户端配置文件 /etc/fdfs/client.conf

    1
    2
    3
    4
    5
    #客户端存储日志信息的目录,xxx.pid文件,必须是一个存在的目录,并且用户需要对该路径中的文件有读写权限
    base_path = /home/yuqing/fastdfs

    #设置追踪器的ip地址与端口 (填写规则同上存储节点配置文件)
    tracker_server = 192.168.0.196:22122
FastDFS启动
  1. 启动追踪器
  2. 启动存储节点
  3. 启动客户端

格式为:程序 配置文件路径 [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函数组函数

  • execl
  • execlp

父进程->创建子进程:执行execlp("fdfs_upload_file","xx",arg,NULL),有结果输出,用dup2管道重定向标准输出到管道的写端.父进程读管道读端

image-20230525163610657
使用fastDFS API实现

余庆未提供api文档,只能通过看调用案例找API

安装文件夹中有test文件夹,该文件夹中的是调用案例

参考项目中自带的案例来实现

fastDFS配合fastCGI项目

image-20230625113716573

nginx的fastdfs插件下载链接

  1. 在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
  2. 从fastDFS插件的源码目录的src目录中找mod_fastdfs.conf执行sudo cp mod_fastdfs.conf /etc/fdfs,修改mod_fastdfs.conf(参考storage.conf来修改)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #日志位置
    base_path=/home/xxx/fastdfs/storage
    #配置追踪器的ip和端口
    tracker_server=xxx.xxx.xxx.xxx:22122
    #存储节点绑定的端口
    storage_server_port=23000
    #当前存储节点所属组
    group_name=group1
    #客户端下载文件时候,这个下载的url中是否包含组的名字(仅在只有同一个组的时候,这个选项可以设为false)
    url_have_group_name=true
    #存储节点绑定的IP地址
    bind_addr=
    #存储节点上存储路径的个数
    store_path_count=1
    #存储路径的详细位置(可以多个)
    store_path0=/home/xxx/fastdfs/storage
    #store_path1=...
  3. 重新启动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"
    #解决方式如下:从fastDFS源码安装目录找./conf,执行下面命令:
    sudo cp http.conf /etc/fdfs

    ERROR - file: shared_func.c, line: 1301, file /etc/fdfs/mime.types not exist
    #解决方式如下:从fastDFS源码安装目录找./conf,执行下面命令:
    sudo cp mime.types /etc/fdfs
  4. 资源在存储节点的存储目录中,store_path0等中,需要告诉nginx服务器资源在哪,nginx.conf添加location

    1
    2
    3
    4
    5
    6
    7
    #location /group1/M00/00/00/
    #由于/00/00经常变化并不是固定目录,因此:
    location /group1/M00/{
    #告诉服务器资源的位置
    root /opt/fastdfs/data;
    ngx_fastdfs_module;#标记需要通过fastdfs插件处理
    }

    添加了之后重启效果如下:ngx_http_fastdfs_set pid=699791

日志实现案例

源文件

1
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;
/* ************************************************************************************ */
/* ************************************************************************************ */
/* ************************************************************************************ */
//日志文件初始化,也可以通过msgLogOpen进行初始化
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"); /* 默认信息输出时间格式 MM-DD HH24:MI:SS */
strcpy(wandatefmt, "%m-%d %H:%M:%S"); /* 默认告警输出时间格式 MM-DD HH24:MI:SS */

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会将localtime(&now_time)按照msgdatefmt格式,输出到logprefix.*/
strftime(logprefix, sizeof(logprefix), msgdatefmt, localtime(&now_time));
strcat(logprefix, " ");
/*static int msgopt,wanopt;*/
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, " ");
/*#define LOG_PROCNAME 0x00000001*/ /* msglog 输出日志时打印程序名 */
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) /* 设置开始时间 0=ok */
{
gettimeofday(&be_stime,NULL);

return 0;
}
/* ************************************************************************************ */
long getusec_process(void) /* 返回usecond 从 begusec_process历时 */
{
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 /* msglog 输出日志时打印程序名 */
#define LOG_PID 0x00000010 /* msglog 输出日志时打印进程 PID */
#define LOG_PERROR 0x00000100 /* msglog 是否把告警内容输出到stderr */
#define NLO_PROCNAME 0x11111110 /* msglog 不输出程序名 */
#define NLO_PID 0x11111101 /* msglog 不输出进程 PID */
#define NLO_PERROR 0x11111011 /* msglog 不输出告警到stderr */

#define MSG_INFO 0x00000001 /* msglog 输出到告警日志文件中 */
#define MSG_WARN 0x00000010 /* msglog 输出到普通日志文件中 */
#define MSG_BOTH MSG_INFO|MSG_WARN /* msglog 输出到普通和告警日志文件中 */

#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); /* 设置开始时间 0=ok */
long getusec_process(void); /* 返回usecond 从 begusec_process历时 */

int msgInit(char *pName);
#endif
/* ************************************************************************************ */
/* ************************************************************************************ */
/* ************************************************************************************ */
/* ************************************************************************************ */

使用方式

  1. msgInit("日志文件名") 初始化日志文件
  2. msglog([MSG_INFO或MSG_BOTH],"",...) 输出内容到日志文件
  3. msgLogClose() 关闭日志文件

报文编解码

  • 报文: 网络通信过程中接受发送的数据
  • 编码: 将传输的数据转换为字节流的过程
  • ASN.1: 通用的一种编码格式

两台机器通信有如下要考虑

  1. 网络传输的时候需要用大端模式
  2. 字节序对齐不同,比如结构体
  3. 等等….

常用序列化方式

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数据交换格式

  1. 高效的序列化:protobuf使用二进制编码,相比于文本格式如JSON和XML,它的序列化后的数据体积更小,传输效率更高
  2. 跨平台和语言无关:protobuf定义了一种独立于语言和平台的数据描述语言,可以在不同的编程语言之间进行数据的序列化和反序列化,使得不同系统之间的数据交换更加方便。
  3. 可扩展性:protobuf支持向后兼容和向前兼容的数据格式演化,可以在不破坏现有数据结构的情况下进行数据模型的更新和扩展。
    • 向后兼容性(Backward compatibility):新版本的数据模型可以解析旧版本的数据,即新模型可以正确地处理旧数据,不会导致解析错误或数据丢失。这意味着旧版本的数据可以与新版本的代码一起使用。
    1. 向前兼容性(Forward compatibility):旧版本的数据可以被新版本的数据模型解析,即旧模型可以正确地处理新数据,不会导致解析错误或数据丢失。这意味着新版本的数据可以与旧版本的代码一起使用。
  4. 代码生成:protobuf提供了代码生成工具(protoc命令行工具),可以根据定义的数据结构自动生成对应的类代码,简化了数据的编码和解码过程,提高了开发效率。
  5. 可读性: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有两部分

  1. 一部分描述信息内数据,数据类型及序列格式(相当于属性)
  2. 一部分描述如何将各部分组成消息(相当于方法)

标准的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;

image-20230207141410914

TLV节点对应的结构体

1
2
3
4
5
6
7
8
9
10
tupedef struct ITCAST_ANYBUF_
{
unsigned char *pData; //数据的值V 真正的数据在这里
ITCAST_UINT32 dataLen; //数据长度L
ITCAST_UINT32 unusedBits;
iTCAST_UINT32 memoryType;
iTCAST_UINT32 dataType; //数据类型T
struct ITCAST_ANYBUF_ *next;//指向下一个成员属性编码形成的结点
struct ITCAST_ANYBUF_ *prev;//指向上一个成员属性编码形成的结点
}ITCAST_ANYBUF;

原理:

  • 定义一个结点以TLV形式存储编码后的数据
  • 被编码对象的每一个成员属性都会成为一个结点。从对象的一个成员开始到最后一个成员,所有新建的结点会连成一条双向链表,便于后续对象成员依次解码
  • 整个对象也会被编码,即将链表的首结点封装成一个新的节点,作为传出参数传出,便于解码时以对象为单位进行解码

TLV原理图:

image-20230210143611378

相关函数

编解码函数中包括对各种基本数据类型的编解码函数,用于对对象的成员属性进行分别编码;同时还包括对对象进行编解码的函数,即对成员属性编码后形成的链表首结点进行编码

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_AnyBufDER_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();//DecodeChar中包含对二级指针的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>
/*
typedef struct _Teacher
{
char name[64];
int age;
char *p;
long plen;
}Teacher;
//在teacher.h中定义*/
//编码Teacher结构体
int encodeTeacher(Teacher * p, char ** outData, int * outlen)
{
//编码name
ITCAST_ANYBUF *head = NULL;//头结点
ITCAST_ANYBUF* temp = NULL;
ITCAST_ANYBUF* next = NULL;
DER_ITCAST_String_To_AnyBuf(&temp,p->name,strlen(p->name)+1);//只要传temp指针就可以,内部会malloc
DER_ItAsn1_WritePrintableString(temp,&head);
DER_ITCAST_FreeQueue(temp);
next = head;
//编码age
DER_ItAsn1_WriteInteger(p->age,&next->next);
next=next->next;
//编码p
EncodeChar(p->p,strlen(p->p)+1,&next->next);
next=next->next;
//编码plen
DER_ItAsn1_WriteInteger(p->plen,&next->next);
//序列化
DER_ItAsn1_WriteSequence(head,&temp);
//输出参数赋值
*outData = temp->pData;
*outlen = temp->dataLen;
//释放内存
DER_ITCAST_FreeQueue(head);
return 0;
}

//解码Teacher结构体
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));
//将inData反序列化成链表
//将char* --> ITCAST_ANYBUF
DER_ITCAST_String_To_AnyBuf(&temp,inData,inLen);
DER_ItAsn1_ReadSequence(temp,&head);//有疑问!!!!!!!!!!!! 此时得到head指向的链表
DER_ITCAST_FreeQueue(temp);
next = head;
//解码name
DER_ItAsn1_ReadPrintableString(head,&temp);
memcpy(pt->name,temp->pData,temp->dataLen);
DER_ITCAST_FreeQueue(temp);
next = next->next;
//解码age
DER_ItAsn1_ReadInteger(next,&pt->age);
next = next->next;
//解码p
int len = 0;
DecodeChar(next,&pt->p,&len);//此处的pt中的p也要记得在freeTeacher中释放内存!
next=next->next;
//解码plen
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");
/* code */
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;
}

执行结果

image-20230210173019307

进一步封装

封装uml图如下:

ASN.1 编解码- 类图jpg_Page1

其中的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 // ASN1DER_H
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_ITCAST_String_To_AnyBuf(&m_temp,(unsigned char*)sValue,strlen(sValue)+1);
// DER_ItAsn1_ReadPrintableString(m_temp,&m_header);
// DER_ITCAST_FreeQueue(m_temp);
// m_next = m_header;
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)
{
// DER_ITCAST_String_To_AnyBuf(&m_temp,(unsigned char*)sValue,strlen(sValue)+1);
// DER_ItAsn1_ReadPrintableString(m_temp,&m_next);
// DER_ITCAST_FreeQueue(m_temp);
// m_next = m_next->next;
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);//释放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
// Server
// .NET 8.0控制台应用
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"); //定义IP地址
IPAddress localAddress = serverIP[0]; //IP地址
TcpListener tcpListener = new(localAddress, port); //监听套接字
tcpListener.Start();
//开始监听
Console.WriteLine("服务器启动成功,等待用户接入…"); //输出消息
while (true)
{
try
{
tcpClient = tcpListener.AcceptTcpClient(); //每接收一个客户端则生成一个TcpClient
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]); //输出接收的客户端IP地址
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
// Client
// .NET 8.0控制台应用
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对象,自动分配主机IP地址和端口号
tcpClient.Connect("127.0.0.1", 888); //连接服务器,其IP和端口号为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"; //存储本机IP,默认值为127.0.0.1
IPAddress[] ips = Dns.GetHostAddresses(Dns.GetHostName());//获取所有IP地址
foreach (IPAddress ip in ips)
{
if (!ip.IsIPv6SiteLocal) //如果不是IPV6地址
localip = ip.ToString(); //获取本机IP地址
}
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
//reader和writer与上面定义一致
// 读取二进制数据
byte[] buffer = new byte[4]; // 假设我们期望接收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)
{
// Send message.
var message = "Hi friends 👋!<|EOM|>";
var messageBytes = Encoding.UTF8.GetBytes(message);
_ = await client.SendAsync(messageBytes, SocketFlags.None);
Console.WriteLine($"Socket client sent message: \"{message}\"");

// Receive ack.
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;
}
// Sample output:
// Socket client sent message: "Hi friends 👋!<|EOM|>"
// Socket client received acknowledgment: "<|ACK|>"
}

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)
{
// Receive message.
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 /* is end of message */)
{
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;
}
// Sample output:
// Socket server received message: "Hi friends 👋!"
// Socket server sent acknowledgment: "<|ACK|>"
}

常用网络相关命令

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服务器 ,其核心功能包括:

  1. 授权解析权:明确域名由哪些DNS服务器管理(如ns1.cloudflare.com),指导用户请求的查询路径
  2. 冗余与负载均衡:支持配置多个NS记录,当主服务器故障时,备用服务器可接管,保障解析不中断
  3. 子域名管理:若子域名需独立解析(如blog.example.com),可通过NS记录指向其他DNS服务商

示例:若域名使用Cloudflare的DNS服务,则需在注册商处设置NS记录为 ns1.cloudflare.comns2.cloudflare.com,此后所有DNS查询由Cloudflare处理

DNSChecker

全球DNS解析检测与诊断工具,主要用于验证域名解析记录的全球传播状态、检查DNS配置准确性,并提供多种网络诊断功能。以下是其核心功能与使用场景的详细解析

免费且跨平台

基于该工具的DNS记录查询网站