admin 管理员组

文章数量: 887018

注意:一些图是网上偷来的,以学习为主

网络简介介绍

IP地址+CIDR讲解
查看IP:

windows上:ipconfig

linux上:ifconfig

ip addr 命令:这个命令显示了这台机器上所有的网卡。大部分的网卡都会有一个IP地址。

IP地址是一个网卡在网络世界的通讯地址,相当于我们现实世界的门牌号码。

既然是门牌号码,不能大家都一样,不然就会起冲突。比方说,假如大家都叫六单元1001号,那快递就找不到地方了。所以,有时候咱们的电脑弹出网络地址冲突,出现上不去网的情况,多半是IP地址冲突了

本来32位的IP地址【IPv4】就不够,还被分成5类。现在想想,当时分配地址的时候,真的是太奢侈了。根据0的位数位置区别各类
下面这个表格,详细地展示了A、B、C、三类地址所能包含地主机数量

A类数量为2的24次方减2,这两个中的一个24位全为0的为网络号,另一个24全为1的为广播地址(前八位为0【0000 0000】~191【1111 1111】)

B类数量为2的16次方减2,这两个中的一个16位全为0的为网络号,另一个16全为1的为广播地址(前八位为128【1000 0000】~191【1011 1111】)

C类数量为2的8次方减2,这两个中的一个8位全为0的为网络号,另一个8全为1的为广播地址(前八位为128【1100 0000】~223【1101 1111】)

无类型域间选路

于是有了一个折中的方式叫做无类型域间选路,简称CIDR。这种方式打破了原来设计的几类地址的做法,将32位的IP地址一分为二,前面是网络号,后面是主机号。

从哪里分呢?你如果注意观察的话可以看到,192.168.0.169/24,这个IP地址中有一个斜杆,斜杆后面有一个数字24,这种地址表示形式,就是CIDR。后面24的意思是,32位中,前24位是网络号,后8位是主机号

伴随着CIDR存在的,一个是广播地址,brd 192.168.0.255。如果发送这个地址,所有192.168.0网络里面的机器都可以收到。另一个是子网掩码,netmask 255.255.255.0(子网掩码【子网掩码也是网络号最大值】和IP地址相 与& 得到网络号【192.168.0】)

思考题:

16.158.165.91/22这个CIDR。求一下这个网络的第一个地址、子网掩码、广播地址

私有IP和公有IP

表格最右列是私有IP地址段。平时我们看到的数据中心里,办公室、家里或学校的IP地址,一般都是私有IP地址段。因为这些地址允许组织内部的IT人员自己管理、自己分配,而且可以重复。因此,你家里的某个私有IP地址段和我家里的可以是一样的。

公有IP地址由组织【IP地址管理组织】统一分配【但世界上的IP地址已经卖完了,所以就有了IPv6】,你需要去买。如果你搭建一个网站,给家里的人使用,让路由器自动给你分配一个IP地址就行,但是假如你要做一个类似blibli这样的网站,就需要有公有IP地址,这样全世界的人才能访问

MAC地址
MAC地址和IP地址

link/ether fa:16:3e:fb:98:34 brd ff:ff:ff:ff:ff:ff

这个被称为MAC地址,是一个网卡的物理地址,用十六进制,6个byte表示。MAC地址全局唯一,不会有两个网卡相同的MAC地址,而且网卡自生产出来,就带着这个地址。

既然这样地话,整个互联网地通信,全部用MAC地址好了,只要知道对方的MAC地址,就可以把信息传过去。何必需要IP地址呢?

IP地址类似收货地址:广东省广州市增城区华商学院西八717宿舍

MAC地址类似身份证:(MAC地址是有一定定位功能的)身份证是xxx的人在哪儿?

网络设备的状态标识

<BROADCAST,MULTICAST,UP,LOWER_UP>

这个叫做net_device flags,网络设备的状态标识

UP:表示网卡处于启动的状态

BROADCAST:表示这个网卡有广播地址,可以发送广播包

MULTICAST:表示网卡可以发送多播包

LOWER_UP:表示L1是启动的,也即网线插着呢

MTUL1500:是指最大传输单元MTU为1500,这是以太网的默认值,以太网规定正文部分不允许超过1500个字节。正文里面有IP的头、TCP的头、HTTP的头。如果放不下,就需要分片来传输

qdisc pfifo_fast

qdisc 全称是queueing discipline,叫排队规则。内核如果需要通过某个网络接口发送数据包,它都需要按照qdisc(排队规则)把数据包加入队列。最简单的qdisc是pfifo,它不对进入的数据包做任何的处理,数据包采用先入先出的方式通过队列。pfifo_fast稍微复杂一些。

DHCP协议+wireshark抓包
DHCP(我要租房子)

新来的机器使用IP地址0.0.0.0发送了一个广播包,目的IP地址为255.255.255.255。广播包封装了UDP,UDP封装了BOOTP。其实DHCP是BOOTP的增强版

在这个广播包里面,新人大声喊:我是新来的(Boot request),我的MAC地址是这个,我还没有IP,谁能给我租个IP地址!
如果网络里面配置了DHCP Server【在路由器上的】的话,它就相当于这些IP的房东。它立刻能知道来了一个“新人”,需要租给它一个IP地址,这个过程我们称为DHCP Offer。DHCP Server为此客户留为它提供的IP地址,不会为其他DHCP客户分配IP地址。

在这个广播包里面,DHCP Server大声喊:我是房东,我有IP,快来租快来租!,这个房子给你怎样?

DHCP Server 仍然使用广播地址作为目的地址,因为,此时请求分配IP的新客户没有自己的IP。DHCP Server 回复说,我分配了一个可用的IP给你,你看如何,除此之外,服务器还发送了子网掩码、网关和IP地址租用期等信息。

新来的机器很开心,它的“喊”得到了回复,并且有人愿意租给它一个IP地址了,这意味着它可以在网络上立足了。

当然更令人开心的是,如果有多个DHCP Server,这台新机器会收到多个IP地址,简直受宠若惊。它会选择其中一个DHCP Offer,一般是最先到达的那个,并且会向网络发送一个DHCP Request广播数据包,包中包含客户端的MAC 地址、接收的租约中的IP地址、提供此租约DHCP服务器地址等,并告诉所有DHCP Server 它将接受哪一台服务器提供的IP地址,告诉其他DHCP服务器,谢谢你们的接纳,并请求撤销它们提供的IP地址,以便提供下一个IP租用请求者。

在这个广播包里面,新人大声喊:我就要这个IP地址,我们签合同吧!
当DHCP Server接收到客户机的DHCP request之后,会广播返回给客户机一个DHCP ACX消息包,表面已经接收客户机的选择,并将这一IP地址的合法租用信息和其他配置信息最终租约达成的时候,还是需要广播一下,让大家都知道。

在这个广播包里面,DHCP Server大声喊:合同在此,这个新人租的是我这个IP地址!

网络模型
协议

语法:就是这一段内容要符合一定的规则和格式。例如,这一部分写几个字节,下一个部分写几个字节。

语义:就一段内容要代表某种意义。例如这几个字节什么意思,那几个字节什么意思,代表什么。

顺序:就是先要干啥,后干啥。例如,先请求租房子,后提供offer,在请求签合同,最后确认合同成立并公示。

OSI七层模型

开放式系统互联通信参考模型
TCP/IP五层模型

网络底层协议介绍

物理层+数据链路层介绍
物理层:

如何在宿舍里自己组网玩联机游戏

两台电脑联机:一根网线,有两个头。一头插在一台电脑的网卡上,另一头插在另一台电脑的网卡上。水晶头要做交叉线,用的就是所谓的1-3、2-6交叉法。水晶头的第1、2和第3、6脚,它们分别起着收发信号的作用。将一端的1号和3号线、2号和6号线互换一下位置,就能够在物理层实现一段发送的信号,另一端能收到。

多台电脑联机:有一个叫做Hub的东西,也就是集线器。这种设备有多个口,可以将多台电脑连接起来。但是,和交换机不同,集线器没有大脑,它完全在物理层工作。它会收到的每一个字节,都复制到其他端口上去。【设m台机器发信息,一共有n台机器,集线器会产生m(n - 2)条垃圾信息】这是第一层物理层联通的方案。

数据链路层:

Hub采取的是广播的模式,如果每一台电脑发出的包,宿舍的每个电脑都能收到,那就麻烦了。

这就需要考虑这几个问题:

1、这个包是发给谁的?谁应该接收?

2、大家都在发,会不会产生混乱?有没有谁先发,谁后发的规则?

3、如果发送的时候出现错误,怎么办?

这几个问题,都是第二层,数据链路层,也即MAC层要解决的问题。

一、解决这一个问题就牵扯到第二层的网络包格式。对于以太网,第二层的最开始,就是目标的MAC地址和源的MAC地址。

二、MAC的全称Medium Acess Control,即媒体访问控制。控制什么呢?其实就是控制在往媒体发数据的时候,谁先发、谁后发的问题。防止发生混乱。这解决的是第二个问题。这个问题中的规则,学名叫多路访问。有很多算法可以解决这个问题。就像车管所管束马路上跑的车,能想的办法都想过了。比如接下来这三种方式:

方式一:分多个车道。每个车一个车道,你走你的,我走我的。这在计算机网络里叫作信道划分

方式二:抢令牌,轮着来,谁抢到谁先走。这在计算机网络里叫作轮流协议

方式三:不管三七二十一,有事儿先出门,发现特赌,就回去。错过高峰再出。我们叫作随机接入协议。著名的以太网,用的就是这个方式。

三、对于以太网,第二层的最后面是CRC,也就是循环冗余检测。通过XOR异或的算法,来计算整个包是否发送的过程出现错误,主要解决第三个问题。

以太网:

交换机:

Hub这种组网的办法,对一个宿舍来说没有问题,但是一旦机器数数码增多,问题就出现了。因为Hub是广播的,不管某个接口是否需要,所有的Bit都会被发送出去,然后让接收主机来判断是不是需要。这种方式路上的车少就没问题,车一多,产生冲突的概率就提高了。而且把不需要的包转发过去,纯属浪费。

看来Hub这种不管三七二十一都转发的设备是不行了,需要个智能的设备。因为每个口都只连接一台电脑,这台电脑又不怎么换IP和MAC设备,只要记住这台电脑的MAC地址,如果目标MAC地址不是这台电脑,这个口就不用转发了。

交换机怎么知道每个口的电脑MAC地址呢?

这需要交换机会学习。一台MAC1电脑将一个包发送给另一台MAC2电脑,当这个包到达交换机的时候,一开始交换机也不知道MAC2的电脑在哪个口,所以没办法,它只能将包转发给除了来的那个口之外的其他所有的口。但是,这个时候,交换机会干一件非常聪明的事情,就是交换机 会记住,MAC1是来自一个明确的口【交换机就知道一个口叫MAC1】。以后有包的目的地址是MAC1的,直接发送到这个口就可以。

当交换机作为一个关卡一样,过了一段时间之后,就有了整个网络的一个结构了,这个时候,基本上不用广播了,全部可以准确转发。当然,每个机器的IP地址会变,所在的口也会变,因而交换机上的学习的结果,我们称为转发表,是有一个过期时间的。

有了交换机,一般来说,你接个几十台,上百台机器打游戏,应该没啥问题。

网络层
IP:

当然电脑之间进行连接,还需要配置这两台电脑的IP地址、子网掩码和默认网关。要想电脑之间能够通信,这三项必须配置成为一个网络(同一个网络号,同一个CIDR),比如一个是192.168.0.1/24,另一个是192.168.0.2/24,否则是不通的。

这里还有一个没有解决的问题,当源机器知道目标机器的时候,可以将目标地址放入包里面,如果不知道呢?一个局域网里面接入了N台机器,我怎么知道每个MAC地址是谁呢?

ARP协议:

(由IP找MAC)

这就是ARP协议,也就是已知IP地址【在win和linux中可以用arp -a 查看所有跟本机有交集的IP地址】,求MAC地址的协议。

在一个局域网里面,当知道了IP地址,不知道MAC怎么办呢?全靠“喊”

ARP数据报格式:
为了避免每次都用ARP请求,机器本地也会进行ARP缓存。当然机器会不断地上线下线,IP也可能会变,所以ARP的MAC地址缓存过一段时间就会过期。

好了,至此我们宿舍四个电脑就组成了一个局域网。

用Hub连接起来,就可以玩游戏了。

ICMP协议:

(派兵侦察)

数据报表
IP数据报的首部长度和数据长度都是可变长的,但总是4字节的整数倍。

对于IPv4,4位版本字段是4.

4位首部长度的数值是以4字节为单位的,最小值为5,也就是说首部长度最小是4x5=20字节,也就是不带任何选项的IP首部,4位能表示的最大值是15,也就是说首部长度最大是60字节。

8位TOS字段有3个位用来指定IP数据报的优先级(目前已经废弃不用),还有4个位表示可选的服务类型(最小延迟、最大吞吐量、最大可靠性、最小成本),还有一个位总是0。

16位总长度是整个数据报(包括IP首部和IP层payload)的字节数。

每传一个IP数据报,16位的标识加1,可用于分片和重新组装数据报。

3位标志和13位片偏移用于分片。

TTL(Time to live)是这样用的:源主机为数据包设定一个生存时间,比如64,每过一个路由器就把该值减1,如果减到0就表示路由已经太长了仍然找不到目的主机的网络,就丢弃该包,因此这个生存时间的单位不是秒,而是跳(hop)。

协议字段指示上层协议TCP、UDP、ICMP还有IGMP

然后是校验和,只校验IP首部,数据的校验由更高层协议负责。

IPv4的IP地址长度为32位。

选项字段的解释从略。

无论是在宿舍,还是在办公室,或者运维一个数据中心,我们常常会遇到网络不通问题。那台机器明明就在那里,你甚至都可以通过机器的终端连上去看。它看着好好的,可是就是连不上去,究竟是哪里出了问题呢?

一般情况下,你会想到ping一下。那ping是如何工作的呢?

ping是基于ICMP协议工作的。ICMP全称Internet Control Message Protocol。就互联网控制报文协议。

网络包在异常复杂的网络环境中传输时,常常会遇到各种各样的问题。当遇到问题的时候,总不能“死个不明不白”,要传出消息来,报告情况,这样才可以调整传输策略。这就相当于我们经常看到电视剧里,古代行军的时候,为将为帅者需要通过侦察兵、哨探或传令兵等人肉的方式来掌握情况,控制整个战局。

ICMP报文是封装在IP包里面的。因为传输指令的时候,肯定需要源地址和目标地址。它本身非常简单。因为作为侦察兵,要轻装上阵,不能携带大量的包袱。
ICMP报文有很多类型,不同类型有不同的代码。
最常用的类型是主动请求为8,主动请求的应答为0.

查询报文类型我们经常在电视剧里听到这样的话:主帅说,来人哪!前方战事如何,快去派人打探,一有情况,立即通报!这种是主帅发起的,主动查看敌情,对应ICMP的查询报文类型。例如,常用的ping就是查询报文,是一种主动请求,并且获得主动应答的ICMP协议。所以,ping发的包也是符合ICMP协议格式的,只不过它在后面增加了自己的格式。

对ping的主动请求,进行网络抓包,称为ICMP ECHO ERQUEST。同理主动请求的回复,称为ICMP ECHO REPLY。比起原生的ICMP,这里面多了两个字段,一个是标识符。这个很好理解,你派出去两队侦察兵,一队是侦察战况的,一队是查找水源的,要有个标识才能区分。另一个是序号,你派出去的侦察兵,都要编个号。如果派出去10个,回来10个,就说明前方战况不错;如果派出去10个,回来2个,说明情况可能不妙。

另外一种就是差错报文:

终点不可达、源站抑制、时间超时、路由重定向

差错报文的结构相对复杂一些。除了前面还有IP,ICMP的前8个字节不变,后面则跟上出错的那个IP包的IP头和IP正文的前8个字节。而且这类侦察兵特别恪尽职守,不但自己返回来报信,还把一部分遗物也带回来。

抓包演示:


网络报文封装过程:

ping的发送和接收过程:

路由器

家庭路由器会有内网网口和外网网口。把外网网口的先插到光猫拉出的网线的网口上,将这个外网网口配置成和光猫的局域网一样。内网网口连上家里的所有电脑。

路由器将多个局域网相连,每个局域网的出口就叫网关

如果源IP和目标IP是同一个网段【可以把网段理解为一定范围的IP地址】,例如,你访问你旁边的手机,那就没网关什么事情,直接将源地址和目标地址放入IP头中,然后通过ARP获得MAC地址,将源MAC和目的MAC放入MAC头中,发出去就可以了。

如果不是同一个网段,该怎么办?这就需要发往默认网关Gateway。Gateway的地址一定是和源IP地址是一个网段。往往不是第一个,就是第二个。例如192.168.1.0/24这个网段,Gateway往往会是192.168.1.1/24或者192.168.1.2/24。如何发往默认网关呢?网关不是和源IP地址是一个网段的么?这个过程就和发往同一个网段的其他机器是一样的:将源地址和目标IP地址放入IP头中,通过ARP获得网关的MAC地址,将源MAC和网关MAC放入头中,发送出去。网关所在的端口,例如192.168.1.1/24将网络包收起来,然后接下来怎么做,就完全看网关的了。

网关往往是一个路由器,是一个三层转发的设备。

啥叫三层设备?就是把MAC头和IP头都取下来,然后根据里面的内容,看看接下来把包往哪里转发的设备。

很多情况下,人们把网关就叫做路由器。其实不完全准确,

准确来说:路由器是一台设备,它有多个网口或者网卡,分别连着多个局域网。每个网卡的IP地址都和局域网的IP地址相同的网段,每个网卡都是它握住的那个局域网的网关【网关就是局域网的出口】。任何一个想发往其他局域网的包,都会到达一个网关,被拿进来,拿下MAC头和IP头,看看,根据自己的路由算法,选择另一个网口,加上IP头和MAC头,然后扔出去。

路由表和转发网关

静态路由:

静态路由,其实就是在路由器上,配置一条一条规则。

每当要选择从哪个网口抛出去的时候,就是一条一条的匹配规则,找到符合的规则,就按规则中设置的那样,从某个1口抛出去。

windows查看路由表:route print

linux查看路由表:

IP routing

netstat -rn

效果图:(路由器告诉我们从哪里发和发到哪里)

第一行: 0.0.0.0 0.0.0.0 192.168.43.1 192.168.43.35 50

这表示发向任意网段的数据通过本机接口 192.168.43.35 被送往一个默认的网关:192.168.43.1 ,它的管理距离是50,这里对管理距离说说,管理距离指的是在路径选择的过程中信息可信度,管理距离越小的,可信度越高

第二行: 127.0.0.0 255.0.0.0 在链路上 127.0.0.1 331

A类地址127.0.0.0 留在本地调试使用,所以路由表中发向127.0.0.0 网络的数据通过本地回环127.0.0.1 发送给指定的网关:127.0.0.1,也就是从自己的回环接口发到自己的回环接口,这将不会占用局域网宽带。

第五行:192.168.43.0 255.255.255.0 在链路上 192.168.43.35 306

这里的目的网络与本机处于一个局域网,所以发向网络127.0.0.1(也就是发向局域网的数据)在链路上,这便不再需要路由器或不需要交换机交换,增加了传输效率

转发网关:
服务器A要访问服务器B。

首先,服务器A会思考,192.168.4.101和我不是一个网段的,因而需要先发给网关。那网关是谁呢?已经静态配置好了,网关是192.168.1.1。

网关的MAC地址是多少呢?发送ARP获取网关的MAC地址,然后发送包。包的内容是这样的:

源MAC:服务器A的MAC

目标MAC:192.168.1.1这个网口的MAC

源IP:192.168.1.101

目标IP:192.168.4.101

包到达192.168.1.1这个网口,发现MAC一致,将包收起来,开始思考往哪里转发。在路由器A中配置了静态路由之后,要想访问192.168.56.2/24,要从192.168.56.1这个口出去,下一跳为192.168.56.2。

于是,路由器A思考的时候,匹配上了这条路由,要从192.168.56.1这个口发出去,发给192.168.56.2,那192.168.56.2的MAC地址是多少呢?路由器A发送ARP获取192.168.56.2的MAC地址,然后发送包。

包的内容是这样的:

源MAC:192.168.56.1的MAC地址

目标MAC:192.168.56.2的MAC地址

源IP:192.168.1.101

目标IP:192.168.4.101

包到达192.168.56.2这个网口,发现MAC一致,将包收进来,开始思考往哪里转发?

在路由器B中配置了静态路由,要想访问192.168.4.0/24,要从192.168.4.1这个口出去,没有下一跳了。因为我右手这个网卡,就是这个网段的,我是最后一跳了。于是,路由器B思考的时候,匹配上这条路由,要从192.168.4.1这个口发出去,发给192.168.4.101.那192.168.4.101的MAC地址是多少呢?路由器B发送ARP获取192.168.4.101的地址,然后发送包。包的内容是要的:

源MAC:192.168.4.1的MAC地址

目标MAC:192.168.4.101的MAC地址

源IP:192.168.1.101

目标IP:192.168.4.101

包到达服务器B,MAC地址匹配,将包收进来。

通过这个过程可以看出,每到一个新的局域网,MAC都要变,但是IP地址

都不变。在IP头里面,不会保存任何网关的IP地址。所谓的下一跳是,某个IP这个IP地址转换为MAC放入AMC头

欸?为什么上面要多个路由器呢?

NAT网关

这里遇见的第一个问题是,局域网之间没有商量过,各定各的网段,因而IP段冲突了。最左面家里的地址是192.168.1.101,最右面公司的地址192.168.1.101,如果单从IP地址上看,简直是自己访问自己,其实是家里的192.168.1.101要访问公司的192.168.1.101。

怎么解决这个问题呢?既然局域网之间没有商量过,那么各管各的,那到国际上。也即公网里面,就需要使用另外的地址。就像出国,不能用咱们自己的身份证,而要改用护照一样。

首先,目标服务器B在国际上要有一个国际身份(身份证号由公网决定),我们给它一个192.168.56.2。在网关B上,我们记下来,国际身份192.168.56.2。对应国内身份192.168.1.101。凡是要访问192.168.56.2,都转成192.168.1.101。于是,源服务器A要访问目标服务器B,要指定的目标地址为192.168.56.2。这是它的国际身份。

包到达192.168.1.1这个网口,发现MAC一致,将包收进来,开始思考往哪里转发。在路由器A中配置了静态路由:要想访问192.168.56.2/24,要从192.168.56.1这个口出去,没有下一跳了,因为我右手这个网卡,就是这个网段的,我是最后一跳了。于是,路由器A思考的时候,匹配上了这条路由,要从192.168.56.1这个口发出去,发给192.168.56.2。那192.168.56.2的MAC地址是多少呢?路由器A发送ARP获取192.168. 56.2的MAC地址。

当网络包发送到中间的局域网的时候,服务器A也需要有个国际身份,因而在国际上,源IP地址也不能用192.168.1.101,需要改成192.168.56.1。发送包内容是这样的:

源MAC:192.168.56.1的MAC地址

目标MAC:192.168.56.2的AMC地址

源IP:192.168.56.1(A的国际地址)

目标IP:192.168.56.2(B的国际地址)

包到达192.168.56.2这个网口,发现MAC一致,将包收起来,开始思考往哪里转发。路由器B是一个NAT网关,它上面配置了,要访问国际身份192.168.56.2对应国内身份192.168.1.101,于是改成访问192.168.1.101。在路由器B中配置静态路由:要想访问192.168.1.0/24,要从192.168.1.1这个口出去,没有下一跳了,因为我右手对这个网卡,就是这个网段的,我是最后一跳了。

于是,路由器B思考的时候,匹配上了这条路由,要从192.168.1.1这个口发出去,发给192.168.1.101。那192.168.1.101的MAC地址是多少呢?路由器B发送ARP获取192.168.1.101的MAC地址,然后发送包。内容是这样的:

源MAC:192.168.1.1的MAC地址

目标MAC:192.168.1.101的MAC地址

源IP:192.168.56.1(A的国际地址)

目标IP:192.168.1.101

包到达服务器B,MAC地址匹配,将包收进来。从服务器B接收的包可以看出,源IP为服务器A的国际身份,因而发送返回包的时候,也发给这个国际身份,由路由器A做NAT,转换为国内身份。从这个过程可以看出,IP地址也会变。这个过程用英文说就是Network Address Transletion,简称NAT。

可以通过https://www.whatismyip/查看自己的出口IP地址

linux抓包:tcpdump

动态路由算法

路由器就是一台网络设备,它有多张网卡。当一个入口的网络包送到路由器时,它会根据一个本地的转发信息库,来决定如何正确地转发流量。这个转发信息库通常被称为路由表。

网络管理员可以手工修改路由表中的路由协议,这种方式叫做静态路由协议。

一般来说网络环境简单的时候,还是可以的。但是有时候网络环境复杂并且多变,如果总是用静态路由,一旦网络结构发生变化,让网络管理员手工修改路由太复杂了,因而需要动态路由算法。

使用动态路由器,可以根据路由协议算法生成动态路由表,随网络运行状况的变化而变化,那路由算法是什么样的呢?

我们可以将复杂的路径,抽象为一种叫做图的数据结构。至于唐僧西行取经肯定是走的路越少越好,道路越短越好,因而这就转化称为如果在途中找到最短的路径的问题。

求最短路径常用的有两种办法,一种是Ballman-Ford算法,一种是Dijkstra算法。在计算机网络基本也是这两种办法计算。

第一大类的算法为距离矢量路径(distance vector routing)。它是基于Ballman-Ford算法的。

这种算法的基本思路是,每个路由器都保存一个路由表,包含多行,每行对应网络中的一个路由器,每一行包含两部分信息,一个是要到目标路由器,从那条线出去,另一个是到目标路由器的距离。由此可以看出,每个路由器都是知道全局信息的。

那这个信息如何更新呢?每个路由器都知道自己和邻居之间的距离,每过几秒,每个路由器都将自己所知的到达所有的路由器的距离告知邻居,每个路由器也能从邻居那里得到相似的信息。每个路由器根据新收集的信息,计算和其他路由器的距离。比如最近的一个邻居距离目标路由器的距离是M,而自己距离邻居是x,则自己到目标路由器的距离是M+x。

这个算法比较简单,但是还是有问题。

1、好消息传得快,坏消息传得慢。(有人来很快知道,有人走知道很慢)

(本来只有B、C两路由器,他们互相知道距离为1,有一天来了个A,B就知道AB距离,C也知道AC得距离)

(有一天A走了,B就不知道A在哪,也就不知道AB距离了,但是C很聪明,它知道A在哪,C知道自己距离A为2,这时候,B就觉自己距离C为3,这时候,C也蒙了,它觉得B距离A为3,它就距离A就为4)
(这时候B和C都蒙了,B觉得C距离A为4,那它就是5,唉,这时候,C觉得B距离A为5,那它为6吧)
依次类推,数越来越大,直到超过一个阈值,我们才能判定A真的挂了。

2、每次发送的时候,要发送整个全局路由表。

网络大了,谁也受不了,所以最早的路由协议RIP就是这个算法。它适应于小型网络(小于15跳)。当网络规模都小的时候,没有问题。现在一个数据中心内部路由器数目就很多,因而不适用了。所以上面的两个问题,限制了距离矢量路由的网络归模。

第二大类算法是链路状态路由(link state routing),基于Dijkstra算法。

这种算法的基本思路是:

​ 当一个路由器启动的时候,首先是发现邻居,向邻居say hello,邻居都回复。然后计算和邻居的距离,发送一个echo,要求马上返回,除以二的距离。

​ 然后将自己和邻居之间的链路状态包广播出去,发送到整个网络的每个路由器。这样每个路由器都能够收到它和邻居之间的关系的消息。因而,每个路由器都能在自己本地构建一个完整的图,然后针对整个图使用Dijkstra算法,找到两点之间的最短路径

不像距离距离矢量路由协议那样,更新时发送整个路由表。链路状态路由协议只广播更新的或改变的网络拓扑,这使得更新信息更小,节省了带宽和CPU利用率。而且一旦一个路由器挂了,它的邻居都会广播这个消息,可以使得坏消息迅速收敛。

1、基于链路状态链路由算法的OSPF:Open Shortest Path First,开放式最短路径优先就是这样一个基于链路状态路由协议,广泛应用在数据中心中的协议。由于主要用在数据中心内部,用于路由决策,因而称为内部网关协议(Interior Gateway Protocol,IGP)。

内部网关协议的重点就是找到最短的路径。在一个组织内部,路径最短往往最优。当然有时候OSPF可以发现多个最短的路径可以在多个路径中进行负载均衡,这常常被称为等价路由。

2、BGP协议

BGP协议使用的算法是路径矢量路由协议(path-vector protocol)。它是距离矢量路由协议的升级版,有全动态BGP,静态BGP。

BGP是自治系统间的路由协议,BGP交换的网络可达性信息提供了足够的信息来检测路由回路并根据性能优先和策略约束对路由进行决策。

网络编程基础

UDP协议和TCP协议基础

UDP协议:(User Datagram Protocol)【天真简单的熊孩子】

UDP段格式:


抓包图:
端口号的范围是0 - 2 ^ 16 - 1,其中0-1023被很多软件固定(well - known)占用

很多服务有well-known的端口号,然后客户端程序的端口号却不必是well-konwn的,往往是每次运行客户端程序时由系统自动分配一个空闲的端口号,用完就释放掉,称为ephemeral的端口号。

UDP协议不面向连接,也不保证传输的可靠性:
1、发送端的UDP协议层只管把应用层传来的数据封装成段交给IP协议层就算完成

2、接收端的UDP协议层只管把收到的数据根据端口号交给相应的应用程序就算完成任务了,如果发送端发来多个数据包并且在网络上经过不同的路由,到达接收端时顺序已经错乱了,UDP协议层也不保证按发送时的顺序交给应用层。

3、通常接收端的UDP协议层将收到的数据放在一个固定大小的缓冲区等待应用程序来提取和处理如果应用程序提取和处理的速度很,而发送端发送的速度很快就会丢失数据包,UDP协议层并不报告这种错误。

因此,使用UDP协议的应用程序必须考虑到这些可能的问题且实现适当的解决办法,例如等待应答、超时重发、为数据包编号、流量控制等。一般使用UDP协议的应用程序实现都比较简单,只是发送一些对可靠性要求不高的消息,而不发送大量的数据。

特点:沟通简单、轻信他人、愣头青、做事不懂权变

适合场景:

​ 第一,需要资源少、在网络情况比较内网,或则对于丢包不敏感的应用

​ 第二,不需要一对一沟通,建立连接,而是可以广播的应用

​ 第三、需要处理速度快,延时低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的时候

商业案例:

1、流媒体的协议:直播带货

2、实时游戏:瞬间秒杀

3、loT物联网:嵌入式比较呆

4、移动通信领域:移动流量上网的数据面对的协议 GTP-U是基于UDP的

TCP协议:(Transmission Control Protocol)【使命必达】

TCP数据报表:
抓包图:
源端口号和目标端口号是不可少的,这一点和UDP是一样的。如果没有这两个端口号。数据就不知道应该发给哪个应用。

接下来是包的序号。为什么要给包编号呢?

当然是为了解决乱序的问题。不编好号怎么确认哪个应该先来,哪个应该后到呢。编号是为了解决乱序问题。既然是社会老司机,做事当然要稳重,一件件来,面临在复杂的情况,也临危不乱。

还应该有的就是确认序号。发出去的包应该有确认,要不然我怎么找到对方有没有收到呢?如果没有收到就应该重新发送,直到送达。这个可以解决不丢包的问题。作为老司机,做事当然要靠谱,答应了就要做到,暂时做不到也要有个回复

接下来一些状态位。例如SYN是发起一个连接,ACK是回复,RST是重新连接FIN是结束连接等。TCP是面向连接的,因而双方要维护连接的状态,这些状态位的包的发送,会引起双方的状态变更。

还有一个重要的就是窗口大小。TCP要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力,别发送太快,撑死我,也别发的太慢,饿死我。

通过对TCP头的解析,我们知道要掌握TCP协议,重点应该关注以下几个问题:

顺序问题,稳重不乱

丢包问题,承诺靠谱

连接维护,有始有终

流量控制,把握分寸

拥塞控制,知进知退

TCP协议三次握手原理

首先要建立一个连接,所以我们先来看连接维护问题。(连接客户端和服务端)

TCP的连接建立,我们常常称为三次握手。

A:您好,我是A。

B:您好A,我是B。(这时B回复A的话,A确定B听到了)

A:您好B。(这时A回复B的话,B确定A听到了)

首先,为什么要三次,而不是两次?按说两个人打招呼,一来一回就可以了啊?为了可靠,为什么不是四次?

三次握手除了双方建立连接外,主要还是为了沟通一件事情,就是TCP包的序号的问题

A要告诉B,我这面发起的包的序号起始是从哪个号开始的,B同样也要告诉A,B发起的包的序号起始是从哪个号开始的。

每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个32位的计数器,每4微秒加一,如果计算一下,如果到重复,需要4个多小时,那个绕路的包早就死翘翘了,因为我们都知道IP包头里面有个TTL,也即生存时间。

好了,双方终于建立了信任,建立了连接。前面也说过,为了维护这个连接,双发都要维护一个状态机,在连接建立的过程中,双方的状态变化时序图就像这样
一开始,客户端和服务端都处于CLOSED状态。先是服务器主动监听某个端口,处于LISTEN状态。

然后客户端主动发起连接SYN(请求),之后处于SYN-RCVD状态。(A:您好,我是A)【服务器和客户端可以根据IP不同区别】【注意包的序号和确认序号】
客户端收到服务端发送的SYN和ACK(应答)(B:您好A,我是B)
之后发送ACK的ACK,之后处于ESTABLISHED状态,因为它的一发一收成功了(A:您好B)

服务端收到ACK的ACK之后,处于ESTABLISHED状态,因为它也一发一收了。

抓包演示:

TCP协议四次挥手原理

好了,说完连接,接下来说一说“拜拜”,说散就散。这常被称为四次挥手。

A:B,我不爱你了,我们分手吧。

B:我知道了。(这时B回复A的话,A确定B听到)

B:A,我也不爱你了,我们分手吧。

A:好的,我们分手(这时A回复B的话,B确定A听到了,B和A断开)
断开的时候,我们可以看到,当A说“不玩了”,就进入FIN_WAIT_1的状态(A:B,我不爱你了,我们分手吧。)【服务器和客户端可以根据IP不同区别】【重点注意包的序号和确认序号】
B收到“A不玩”的消息后,发送知道了,就进入CLOSE_WAIT的状态。(B:我知道了。)
A收到“B说真的了”,就进入FIN_WAIT_2的状态,如果这个时候B直接跑路,则A永远在这个状态。TCP协议里面并没有对这个状态的处理,但是Linux有,可以调整tcp_fin_timeout这个参数。设置一个超时时间。

如果B没有跑路,发送“B也不玩了”的请求到达A时(B:A,我也不爱你了,我们分手吧。)
A发送“知道B也不玩了”的ACK后,从FIN_WAIT_2状态结束(A:好的,我们分手)
按说A可以跑路了,但是最后的这个ACK万一B收不到呢?则B会重新发一个“B不玩了”,这个时候A已经跑路了的话,B就再也收不到ACK了,因而TCP协议要求A最后等待一段时间TIME_WAIT,这个时间要足够长,长到如果B没收到ACK的话,“B说不玩了”会重发的,A会重新发一个ACK并且足够时间到达B。

A直接跑路还有一个问题是,A的端口就直接空出来了,但是B不知道,B原来发过的很多包很可能还在路上,如果A的端口被一个新的应用占用了,这个新的应用会收到上个连接中B发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来B发送的所有的包都死翘翘,再空出端口来。

等待的时间设为2MSL,MSL是Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上生存的最长时间,超过足够时间报文将被丢弃。因为TCP报文基于是IP协议的,而IP头中有一个TTL域,是IP数据报可以经过的最大路由数,每经过一个处理它的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。协议规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。

还有一个异常情况就是,B超过2MSL的时间,依然没有收到它发的FIN的ACK,怎么办呢?按照TCP的原理,B当然还会重发FIN,这个时候A再收到这个包之后,A就表示,我已经在这里等了这么长的时间了,已经仁至义尽了,之后的我就都不认了,于是就直接发送RST,B就知道A早就跑了。

TCP协议解决顺序问题和丢包问题

如何实现一个靠谱的协议

客户端每发送一个包,服务器都应该有个回复,如果服务器端超过一定的时间没有回复,客户端就会重新发送这个包,知道有回复。

上一个收到了应答,再发送下一个。这种模式有点像两个人直接打电话,你一句,我一句。但是这种方式的缺点是效率比较低。如果一方在电话那头处理的时间比较长,这一头就要干等着,双方都没有办法干其他事情。

咱们在日常工作中也不是这样的,不能你交代你的下属办一件事情,就一直看着他做,而是应该按照你的安排,先将事情记录下来,办完一件回复一件。在他办事情的过程中,你还可以同时交代新的事情,这样双方就并行了。如果使用这种模式,其实需要你和你的下属就不能靠脑子了,而是要都准备一个本子,你每交代下属一个事情,双方的本子都要记录一下。

TCP协议使用的也是同样的模式。为了保证顺序性,每一个包都有一个ID。在建立连接的时候,会商定起始的ID是什么,然后按照ID一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某几个之前ID,表示都收到了,这种模式称为累计确认或者累计应答。(比如发了三个包,序号分别是 1、 5 、 10,而回复只需要回复11就表示三个包都收到了)

下面分别为发件人仓库和收件人仓库:
收件人仓库这的4、5号位置的包已经接收也已经返回确认序号了,但是这个序号还没递达。6、7、8、9、11、12、13、14都是空的,都还没收到包。为什么在发货人仓库里有未发送可发送呢?就是看收件人仓库的空仓库大小,如果发件人的货物量大于收件人仓库能容的大小就会放在未发送不可发送。

接下来我们结合一个例子来看,发送端和接收端当前的状态如下:

1、2、3没有问题,双方达成一致。

4、5接收方说ACK了,但是发送方还没收到,有可能丢了,有可能在路上。

6、7、8、9肯定都发了,但是8、9已经到了,但是6、7没到,出现了乱序,缓存着但是没有办法ACK

假设4的确到了,不幸的是,5的ACK丢了,6、7的数据包丢了,这该怎么办呢?

超时重试传:也即对每一个发送了,但是没有ACK的包,都有设一个定时器,超过了一定的时间,就重新尝试。但是这个超时的时间如何评估呢?这个时间不宜过短,时间必须大于往返时间RTT否则会一起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。

估计忘返时间,需要TCP通过采样RTT的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样RTT,还要采样RTT地波动范围,计算出一个估计地超时时间。由于重传时间是不断变化地,我们称为自适应重传算法(Adaptive Retransmission Algorithm)

如果过一段时间,5、6、7都超时了,就会重新发送。接收方发现5原来接收过,于是丢弃5;6收到了,发送ACK,要求下一个是7,7不幸又丢了。当7再次超时的时候,有需要重传的时候,都会将下一次超时时间间隔设为先前值得两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

快速重传的机制:当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是他就会发送冗余的ACK,仍然ACK的是期望接收的报文段。而当客户端收到三个冗余的ACK后,就会在定时器过期之前,重传丢失的报文段。

例如,接收方发现6收到了,8也收到了,但是7还没来,那肯定是丢了,于是接收方发送6的ACK,要求下一个是7.接下来,收到后续的包,仍然发送6的ACK,要求下一个7.当客户端收到3个重复ACK,就会发现7的确丢了,不等超时,马上重发。

还有一种方式称为Selective Ackonwledgment(SACK)。这种方式需要在TCP头里加一个SACK的东西,可以将缓存的地图发送给发送方。例如可以发送ACK6、SACK8等 有了地图,发送方一下子就能看出来是7丢了

TCP协议的流量控制和拥塞控制

流量控制:

我们再来看流量控制机制,在对于包的确认中,同时会携带一个窗口的大小。我们先假设窗口不变的情况,窗口始终为9。4的确认来的时候,会右移一个,这个时候第13个包也可以发送了。

这个时候,假设发送端发送过猛,会将第三部分的10、11、12、13全部发送完毕,之后就停止发送了,未发送可发送部分为0。
当对于包5的确认到达的时候,在客户端相当于窗口再滑动了一格,这个时候,才可以有更多的包可以发送了,例如第14个包才可以发送。
如果接收方实在处理的太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为0,则发送方将暂时停止发送。

我们假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包6确认后,窗口大小就不能再是9了,就要缩小一个变为8.
这个新的窗口8通过6的确认消息到达发送端的时候,你会发现窗口没有平行右移,而是仅仅左面的边右移了,窗口的大小从9改成了8。

如果接收端还是一直不处理数据,则随着确认的包越来越多,窗口越来越小,直到为0.

当这个窗口通过包14的确认到达发送端的时候,发送端的窗口1也调整为0,停止发送。
如果这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。

当接收方比较慢的时候,要防止低能窗口综合征:

​ 只要空出一个字节来就赶快告诉发送方,然后马上又填满了。

​ 可以当窗口太小的时候,不是更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。

整个TCP协议发送收到报文:
拥塞控制:

拥塞控制的问题,也是通过拥塞窗口cwnd的大小来控制的,怕把网络塞满。

水管有粗细,网络有带宽,也即每秒钟能够发送多少数据;水管有长度,端到端有时延。在理想状态下,水管里面水的量 = 水管粗细 x 水管长度。对于到了网络上,通道的容量 = 宽带 x 往返延迟。如果我们设置发送窗口,使得发送但未确认的包为通道的容量,就能够撑满整个管道。
如果我们在这个基础上调大窗口,使得单位时间内更多的包可以发送,会出现什么现象呢?

我们来想,原来发送一个包,从一段到达另一端,假设一共经过四个设备,每个设备处理一共包消耗费1s,所以到达另一端需要耗费4s,如果发送的更加快速,则单位时间内,会有更多的包到达这些之间设备,这些设备还是只能每秒处理一个包的话,多出来的包就会被丢弃,这是我们不想看到的。

我们可以想其他的办法,例如这个四个设备本来每秒处理一个包,但是我们在这些设备上加缓存,处理不过来的在队列里面排着,这样包就不会丢失,但是缺点是会增加延迟,这个缓存的包,4s肯定到达不了接收端了,如果时延达到一定程度,就会超时重传,也是我们不想看到的。

于是TCP的拥塞控制主要来避免两种现象,包丢失和超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。但是一开始我怎么知道速度多块呢,我怎么知道应该把窗口调整到多大呢?

慢启动:如果我们通过漏斗往瓶子里灌水,我们就知道,不能一桶水一下子倒进去,肯定会溅出来,要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。

拥塞控制:有一个阈值,当超过这个值的时候就要小心一点了,不能倒这么快了,可能快满了,再慢下来。

快速恢复:当接收端发现了一个中间包的时候,发送三次前一个包的ACK,于是发送端就会快速的重传,不必等待超时重传。TCP认为这种情况不严重,因为大部分没丢,只丢了一小部分。

状态机:
坐标图:

网路编程预备知识和网络系统调用时序介绍

预备知识:网络字节序

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#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网络,l表示32位长整数,s表示16位短整数。

**socket API是一层抽象的网络编程接口(可以理解为一个IP地址),适用于各种底层网络协议,如IPv4、IPv6,以及UNIX Domain Socket。**然而,各种网络协议的地址格式并不相同,如下图所示:

IPv4、IPv6和UNIX Domain Socket 是地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX,这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr,就可以根据地址类型字段确定结构体中的内容。

sock API的实现早ANSI C标准化,那是还没有void*类型,因此*这些函数的参数都用struct sockaddr 类型表示,在传递参数之前要强制类型转换一下(转换为下面图中的第一个结构体)
基于TCP协议的网络程序:

建立连接后,TCP协议提供双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到从read()返回,发送下一条请求,如此循环下去。

如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样的服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown则连接处于半关闭状态,仍可接收对方发来的数据。
服务端:

socket()创建一个终端节点来交流,返回一个文件描述符。

#include<sys/types.h>
#include<sys/socket.h>

int socket(int domain, int type, int protocol);
//domain为域名(网络编程接口;地址协议族),如IPv4、IPv6,以及UNIX Domain Socket为地址类型,地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX,最常用的是AF_INET,    type为套接字类型,TCP就是用SOCK_STREAM,     protocol为协议,默认为0


//IPv4协议实现(AF_INRT)
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
//udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
//raw_socket = socket(AF_INET, SOCK_RAW, protocol);

//可以存放IP地址‘AF_INET’结构体
struct sockaddr_in{
    sa_family_t sin_family;//16位地址类型
    in_port_t sin_port;//端口号(用网络字节序)
    struct in_addr sin_addr;//(IP地址)
};
struct in_addr {
    uint32_t s_addr;
}

socket()创建的一个socket处于一个命名空间里,但是没有一个真实的地址,为了得到这样一个地址,bind()可以把一个指定的地址绑定到socket这个文件描述符上,如果不绑定,服务器就会随机分配一个地址,这样的话,客户端连接时就会混乱。bind()成功返回0,失败返回-1且设置errno

#include<sys/types.h>
#include<sys/socket.h>

int bind(int sockfd, const strcut sockaddr *addr, socklen_t addrlen);//sockfd为套接字的文件描述符,const struct sockaddr *addr是(网络编程接口)强制类型转换成通用的结构体,每次使用都被强制类型转换成一个通用结构体,而addr这个结构体需要自己赋值,常用的就是IPv4(AF_INET)的结构体,addrlen为结构体长度【sizeof(addr)】

//通用结构体,这个结构体的作用就是给人家强制类型转换的
struct sockaddr {
    sa_family sa_family;
    char sa_data[14];
}

listen()标记一个可用的socket套接字,这个socket套接字就可以拿去接收连接了,这些套接字都处于监听状态,在监听队列中;listen()成功返回0,失败返回-1且设置errno

#include<sys/types.h>
#include<sys/socket.h>

int listen(int sockfd, int backlog);//sockfd为套接字文件描述符,且这个socket套接字的类型是SOCK_STREAM或者SOCK_SEQPACKET,  backlog参数定义了队列的最大长度,这个队列就是处理(标记)套接字的,如果队满了,会返回一个ECONNREFUSED,请求会被扔了,下次再发

listen()觉得某个套接字可以去连接了,则accept()是等待一个具有连接能力套接字过来连接;把等待队列的第一个人(套接字)找出来,创建一个新的套接字【这个套接字才有用,后面的操作都是基于这个套接字】和返回一个新的文件描述符(注意:新创建的这个套接字可不是再监听状态中的,原来的套接字还是原来的套接字,原来的套接字还是可以监听的)

#include<sys/types.h>
#include<sys/socket.h>

socklen_t addrlen;
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);//sockfd为具有连接能力的套接字的文件描述符且这个socket套接字的类型是SOCK_STREAM或者SOCK_SEQPACKET;addr为域名的结构体,这里需要强制类型转换为通用结构体,addr这个结构体必须是传进来套接字的结构体,如果对传进来的IP不感兴趣也可以传NULL;addrlen为结构体长度,注意是传地址,所以先前要定义好,addrlen是一个值-结果参数,即可传入也可传出

#define _GNU_SOURCE
#include<sys/socket.h>

int accept4(int sockfd, struct sockaddr * addr, socklen_t *addrlen, int flags);//其他参数一样,就是flags可以设置成SOCK_NONBLOCK或SOCK_CLOEXEC

客户端:

connect()连接上服务器的IP地址,一般都会成功。基于连接的协议(比如TCP)只能连接一次,其他的协议可能可以连接多次。connet()成功返回0,失败返回-1且设置errno

#include<sys/types.h>
#include<sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);//sockfd为套接字的文件描述符,const struct sockaddr *addr是(网络编程接口)强制类型转换成通用的结构体,每次使用都被强制类型转换成一个通用结构体,而addr这个结构体需要自己赋值,常用的就是IPv4(AF_INET)的结构体,addrlen为结构体长度   【此处的addr是服务器的addr(里面有IP地址和端口),套接字是客户端自己定义的】

拓展:

inet_ntop()把网络地址(IP地址【纯数字】)转为字符串

#include<arpa/inet.h>

const char* inet_ntop(int af, cosnt void *src, char *dst, socklen_t size);//af为地址协议族,把(客户端)网络地址src【IP】转为字符串存在dst中,size为字符串长度

inet_pton()把字符串转为网络地址

#include<arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);//af为地址协议族(我们一般用AF_INET【IPv4】),src为某个IP地址字符串赋到struct sockaddr_in结构体的sin_addr中

服务器代码演示:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<strings.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<ctype.h>


#define SERV_PORT 8000
#define MAXLINE 80
int main() {

    struct sockaddr_in serveraddr, cliaddr;
    int listenfd, connfd;
    socklen_t cliaddr_len;

    char buf[MAXLINE];
    int n;
    char str[INET_ADDRSTRLEN];

    listenfd =  socket(AF_INET, SOCK_STREAM, 0);//创建一个IPv4(AF_INET)协议,类型为SOCK_STREAM的套接字。0为protocol,常常置为0;
    //服务器 ip地址;端口初始化
    bzero(&serveraddr, sizeof(serveraddr));//清空结构体内容
    //对可以存放IP地址和端口的结构体赋值
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(SERV_PORT);//注意用网络字节序
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY其实就是0,对应的IP地址为。0.0.0.0
    
    //给定义好的套接字赋值一个结构体,里面有IP地址和端口。也就是给套接字绑定上IP地址和端口
    bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    //监听,标记套接字可用了,可以拿去连接了,且有个可以长度为3的队列 
    listen(listenfd, 3);

    printf("Accepting connetions...\n");
    //注意每次连接accept()得到一个新的描述符,这个描述符就是用来和客户端沟通的,可以理解为创建了一个文件,接下来的所有操作就是对这个文件操作,而这个文件里面的东西会被客户端收到
    while (1) {
        cliaddr_len = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);//接收套接字且返回一个新的套接字
        printf("receive from %s: %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), htons(cliaddr.sin_port));//
        //把文件描述符中的东西写入缓冲区
        n = read(connfd, buf, MAXLINE);
        
//服务器队客户端的内容进行处理
        for (int i = 0; i < n; i++) {
            buf[i] = toupper(buf[i]);
        }
        
        //把缓存区的内容写进文件描述符connfd中
        write(connfd, buf, n);

        close(connfd);
    }
    return 0;
}

客户端:

#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include<stdlib.h>

#define SEVE_PORT 8000
#define MAXLINE 80

int main() {
    struct sockaddr_in servaddr;
    char buf[MAXLINE] = {"hello tcp"};
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SEVE_PORT);
    inet_pton(AF_INET, "172.17.51.115", &servaddr.sin_addr);

    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
   //connect()发送SYN请求连接服务端,收到基于TCP服务端的连接后就返回,返回的结果可以说明成功或失败。连接成功后套接字sockfd就成了一个客户端和服务端沟通的桥梁,客户端可以通过这个桥梁给服务端送数据,服务端对数据操作了也可以通过此桥梁把数据送会给客户端 
    int n;
    printf("ready connect, Please input :\n");
    while (n = read(0, buf, MAXLINE)) {
        write(sockfd, buf, n);//把数据写到服务器可以访问的文件里,因为这个文件已经连接了服务器所以服务器可以访问
        if (!strncmp(buf, "quit", 4)) 
            break;
        n = read(sockfd, buf, MAXLINE);//把经过服务器访问过的文件的数据写到缓冲区
        printf("response from server:\n");
        write(1, buf, n);//把缓存区数据输出到终端
    }
    
    close(sockfd);

    return 0;
}

目前实现的client每次运行只能从命令行读取一个字符串发给服务器,再从服务器收回来,现在我们吧他改成交互的,不断从终端接收用户输入并和server交互。

这时server仍在运行,但是client的运行结果并不正确。原因是什么呢?仔细查看server.c可以发现,server对每个请求只处理一次,应答后将关闭连接,client不能继续使用这个连接发送数据。但是client下次循环时又调用write发送数据给server,write调用只负责把数据交给TCP发送缓冲区就可以成功返回了,所以不会出错,而server收到数据后应答一个RST段,client收到RST段后无法立刻通知应用层,只把这个状态保存在TCP协议层。client下次循环又调用write发数据给server,由于TCP协议层已经处于RST状态,因此不会将数据发出,而是发一个SIGPIPE信号给应用层,SIGPIPE信号的缺省处理动作是终止程序,所以看到上面的现象。

TCP程序增多进程并发服务器功能

使用多进程并发处理多个client的请求

网络服务器通常fork来同时服务多个客户端,父进程专门负责监听端口,每次accept一个新的客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程。

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<strings.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<pthread.h>

#define SERV_PORT 8000
#define MAXLINE 80

#define prrexit(msg) {\
    perror(msg);\
    exit(1);\
}
int main() {

    struct sockaddr_in serveraddr, cliaddr;
    int listenfd, connfd;
    socklen_t cliaddr_len;

    char buf[MAXLINE];
    int n;
    char str[INET_ADDRSTRLEN];

    listenfd =  socket(AF_INET, SOCK_STREAM, 0);//创建一个IPv4(AF_INET)协议,类型为SOCK_STREAM的套接字。0为protocol,常常置为0;
    if (listenfd < 0) {
        prrexit("socket");
    }
    //服务器 ip地址;端口初始化
    bzero(&serveraddr, sizeof(serveraddr));//清空结构体内容
    //对可以存放IP地址和端口的结构体赋值
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(SERV_PORT);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY其实就是0,对应的IP地址为0.0.0.0
    
    //给定义好的套接字赋值一个结构体,里面有IP地址和端口。也就是给套接字绑定上IP地址和端口
    if (bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
        prrexit("bind");
    }

    //监听,标记套接字可用了,可以拿去连接了,且有个可以长度为3的队列 
    if (listen(listenfd, 3) < 0) {
        prrexit("listen");
    }
    
    printf("Accepting connetions...\n");

    while (1) {
        cliaddr_len = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);//接收套接字且返回一个新的套接字
        if (connfd < 0) {
            prrexit("accept");
        }
        printf("receive from %s: %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), htons(cliaddr.sin_port));
        
        
        pid_t pid = fork();
        if (pid < 0) {
            prrexit("fork");
        }
        //父进程:等待 创建连接
        if (pid > 0) {
            close(connfd);

            while (waitpid(-1, NULL, WNOHANG) > 0) {}

            continue;
        } 
        close(listenfd);
        while (1) {
            n = read(connfd, buf, MAXLINE);
            if (!strncmp(buf, "quit", 4)) break;
            write(1, buf, n);//注意这里输出到终端的字符串长度是输出读到缓冲区的长度,过长会输出乱码
            for (int i = 0; i < n; i++) {
                buf[i] = toupper(buf[i]);
            }
            write(connfd, buf, n);
        }
        
        close(connfd);
    }
    return 0;
}

TCP程序改为多线程并发服务

上面这种方式你应该也能发现问题,如果每次连上一个客户端,都创建一个新进程,然后操作完了,就结束子进程,实在是太麻烦了,而且系统公开销很大。因此,我们可以使用线程。相比于进程来讲,这样要轻量级的多。

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<strings.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<pthread.h>

#define SERV_PORT 8000
#define MAXLINE 80

#define prrexit(msg) {\
    perror(msg);\
    exit(1);\
}

void *up_server(void *arg) {
    pthread_detach(pthread_self());//关闭当前线程并且自动释放内存给另一个需要的线程。也就是释放创建线程的线程
    int connfd = (int)arg;
    char buf[MAXLINE];
    int n;
    while (1) {
        n = read(connfd, buf, MAXLINE);
        if (!strncmp(buf, "quit", 4)) break;
        write(1, buf, n);//注意这里输出到终端的字符串长度是输出读到缓冲区的长度,过长会输出乱码
        for (int i = 0; i < n; i++) {
            buf[i] = toupper(buf[i]);
        }
        write(connfd, buf, n);
    }
    close(connfd);
    return NULL;
}

int main() {

    struct sockaddr_in serveraddr, cliaddr;
    int listenfd, connfd;
    socklen_t cliaddr_len;

    char buf[MAXLINE];
    int n;
    char str[INET_ADDRSTRLEN];

    listenfd =  socket(AF_INET, SOCK_STREAM, 0);//创建一个IPv4(AF_INET)协议,类型为SOCK_STREAM的套接字。0为protocol,常常置为0;
    if (listenfd < 0) {
        prrexit("socket");
    }
    //服务器 ip地址;端口初始化
    bzero(&serveraddr, sizeof(serveraddr));//清空结构体内容
    //对可以存放IP地址和端口的结构体赋值
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(SERV_PORT);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY其实就是0,对应的IP地址为0.0.0.0
    
    //给定义好的套接字赋值一个结构体,里面有IP地址和端口。也就是给套接字绑定上IP地址和端口
    if (bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
        prrexit("bind");
    }

    //监听,标记套接字可用了,可以拿去连接了,且有个可以长度为3的队列 
    if (listen(listenfd, 3) < 0) {
        prrexit("listen");
    }
    
    printf("Accepting connetions...\n");

    while (1) {
        cliaddr_len = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);//接收套接字且返回一个新的套接字
        if (connfd < 0) {
            prrexit("accept");
        }
        printf("receive from %s: %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), htons(cliaddr.sin_port));//
//多进程
        /*
        pid_t pid = fork();
        if (pid < 0) {
            prrexit("fork");
        }
        //父进程:等待 创建连接
        if (pid > 0) {
            close(connfd);

            while (waitpid(-1, NULL, WNOHANG) > 0) {}

            continue;
        } 
        close(listenfd);
*/
//多线程
        pthread_t tid;
        pthread_create(&tid, NULL, up_server, (void *)connfd);
    }
    return 0;
}

基于UDP协议的网络协议

下图是典型的UDP客户端/服务端通讯过程
由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上有很多保证通讯可靠性的机制需要在应用层实现。

编译运行server,在两个终端里各开一个client与server交互,看看server是否具有并发服务的能力。用Ctrl+C关闭server,然后再运行server,看此时client还能否和server联系上。和前面TCP程序的运行结果相比较,体会无连接的含义。

send()、sendto()、sendmsg()传送一个消息到另一个套接字上;如果套接字里面们没有消息且为阻塞状态就会等待消息递达,如果为非阻塞状态则返回-1和标记错误

#include<sys/types.h>
#include<sys/socket.h>
//send()比write()多了个flags,如果flags置0就等价于write;如果send()中的套接字的缓冲区没有充满数据,则还处于阻塞状态;如果send()在非阻塞状态, 将会调用失败且返回EAGAIN 或 EWOULDBLOCK,如果想传送更多数据可以调用select()决定
ssize_t send(int sockfd, const void *bud, size_t len, int flags);
//sendto()用于套接字类型为SOCK_STREAM, SOCK_SEQPACKET的套接字。当连接失败时,sendto()参数的dest_addr和addrlen是可忽略的,分别置为NULL和0且返回错误ENOTCONN;当dest_addr和addrlen分别置为NULL或0时,可能会返回EISCONN 
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const srtuct sockaddr *dest_addr, socklen_t addrlen);
//对应send()和sendto()的消息是通过buf和len建立的,而sendmsg()是通过msg.msg_iov建立的,而sendmsg()还可以传送控制消息
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);


msghdr结构体
struct msghdr {
               void         *msg_name;       /* optional address */
               socklen_t     msg_namelen;    /* size of address */
               struct iovec *msg_iov;        /* scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* ancillary data, see below */
               size_t        msg_controllen; /* ancillary data buffer len */
               int           msg_flags;      /* flags (unused) */
           };

recv()、recvfrom()、recvmsg()回复来自套接字连接的消息,他们对有无的连接的套接字都要回复;他们如果被连接成功会返回接收到的消息的长度

#include<sys/types.h>
#include<sys/socket.h>
//recv()也是比read()多了个flags,如果flags置为0就等价于read();recv(sockfd, buf, len, flags)等价于 recvfrom(sockfd, buf, len, flags, NULL, NULL);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

服务端代码演示:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<strings.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>

#define SERV_PORT 8000
#define MAX_LEN 80

#define prrexit(msg) {\
    perror("msg");\
    exit(1);\
}

int main() {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;
    char buf[MAX_LEN];
    socklen_t cliaddr_len;
    char str[INET_ADDRSTRLEN];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);//创建服务端套接字
    
    bzero(&servaddr, sizeof(servaddr));//清空套接字结构体
    //对套接字进行赋值
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htonl(SERV_PORT);

    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));//把结构体中内容绑定到套接字上
    printf("udpserver ready~\n");
    
    while (1) {
        int n = recvfrom(sockfd, buf, MAX_LEN, 0, (struct sockaddr *)&cliaddr, &cliaddr_len);//接收客户端cliaddr套接字中的消息到服务端sockfd的MAX_LEN长度的buf中
        if (n < 0) {
            prrexit("recvfrom");
        }
        /*
    对收到的数据进行操作
        printf("received form %s : %d\n", inet_ntop(AF_INET
, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
        for (int i = 0; i < n; i++) {
            buf[i] = toupper(buf[i]);
        } 
        */
        //操作完就送回客户端
        sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));

    }


    return 0;
}

客户端代码演示:

#include<stdio.h> 
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<strings.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>

#define SERV_PORT 8000
#define MAX_LEN 80


int main() {
    struct sockaddr_in servaddr;
    int sockfd;
    char buf[MAX_LEN];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);//创建套接字
    //对结构体进行服务器关系操作
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, "172.17.51.115", &servaddr.sin_addr);
    int n;

    while (n = read(0, buf, MAX_LEN)) {
        //sockfd把buf中的数据送到服务端
        n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
        //接收服务器返回的消息
        n = recvfrom(sockfd, buf, MAX_LEN, 0, NULL, 0);
        write(1, buf, n);
    }
    close(sockfd);
    return 0;
}

TCP服务器增加线程池结构
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<strings.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<pthread.h>

#define SERV_PORT 8000
#define MAXLINE 80

#define prrexit(msg) {\
    perror(msg);\
    exit(1);\
}

typedef struct Task {
    int fd;
    struct Task *next;
}Task;

typedef struct Task_pool {
    Task *head;
    Task *tail;
    pthread_mutex_t lock;
    pthread_cond_t havetask;
} Task_pool;

Task_pool *Task_pool_init() {
    Task_pool *tp = (Task_pool *) malloc(sizeof(Task_pool));
    tp->head = NULL;
    tp->tail = NULL;
    pthread_mutex_init(&tp->lock, NULL);//初始化一个线程锁
    pthread_cond_init(&tp->havetask, NULL);//初始化一个条件变量

    return tp;
}

void task_pool_push(Task_pool *tp, int fd) {

    pthread_mutex_lock(&tp->lock);//锁住插入操作,抢到锁

    Task *t = (Task *) malloc(sizeof(Task));
    t->fd = fd;
    t->next = NULL;
    
    if (!tp->tail) {
        tp->head = tp->tail = t;
    } else {
        tp->tail->next = t;
        tp->tail = t;
    }

    pthread_cond_broadcast(&tp->havetask);//每当插入一个套接字就对所有线程广播,唤醒所有线程

    pthread_mutex_unlock(&tp->lock);//解锁
    return ;
}


Task task_pool_pop(Task_pool *tp) {

    pthread_mutex_lock(&tp->lock);//锁住出队操作,抢锁

    while (tp->head == NULL) {//如果队列中没有套接字就阻塞等待,等广播有节点了就被唤醒
        pthread_cond_wait(&tp->havetask, &tp->lock);
    }

    Task tmp, *k;
    k = tp->head;
    tmp = *k;
    tp->head = tp->head->next;

    if (!tp->head) {
        tp->tail = NULL;
    }
    free(k);//释放出队那块内存,释放后那块内存就没有数据了,而前面已经把节点中数据存到tmp中了,注意这里不是地址,因为地址指的是那块内存,而为出队同时释放内存,所以要存数据而不是存地址
    pthread_mutex_unlock(&tp->lock);
    return tmp;//返回出队节点,注意这里不是返回节点地址,因为前面已经释放了
}

void task_pool_free(Task_pool *tp) {
    pthread_mutex_lock(&tp->lock);
    Task *p = tp->head, *k;
    
    while (p) {
        k = p;
        p = p->next;
        free(k);
    }
    tp->head = NULL;

    pthread_mutex_unlock(&tp->lock);
    pthread_mutex_destroy(&tp->lock);
    pthread_cond_destroy(&tp->havetask);
    free(tp);
    return ;
}

void *up_server(void *arg) {
    pthread_detach(pthread_self());//结束创建这个进程的进程

    char buf[MAXLINE];
    int n;
    
    Task_pool *tp = arg;
    
    while (1) {//死循环持续弹出队列中的套接字进行操作
        Task tmp = task_pool_pop(tp);
        int connfd = tmp.fd;
        printf("get task fd = %d\n", connfd);
        while (1) {
            n = read(connfd, buf, MAXLINE);
            if (!strncmp(buf, "quit", 4)) break;
            write(1, buf, n);
            for (int i = 0; i < n; i++) {
                buf[i] = toupper(buf[i]);
            }
            write(connfd, buf, n);
        }
        printf("bye task fd = %d\n", connfd);
        close(connfd);
    }
    return (void *)0;
}

int main() {

    struct sockaddr_in serveraddr, cliaddr;
    int listenfd, connfd;
    socklen_t cliaddr_len;

    //char buf[MAXLINE];
    int n;
    char str[INET_ADDRSTRLEN];

    Task_pool *tp = Task_pool_init();
    
//多线程
    pthread_t tid;
    for (int i = 0; i < 4; i++) {//创建4个线程,线程数量最好根据CPU数量来创建,也就是看几核的
        pthread_create(&tid, NULL, up_server, (void *)tp);
        printf("new thread is %lx\n", tid);
    }

    listenfd =  socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        prrexit("socket");
    }
    bzero(&serveraddr, sizeof(serveraddr));//清空结构体内容
    //对可以存放IP地址和端口的结构体赋值
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(SERV_PORT);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY其实就是0,对应的IP地址为0.0.0.0

    //给定义好的套接字赋值一个结构体,里面有IP地址和端口。也就是给套接字绑定上IP地址和端口
    if (bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
        prrexit("bind");
    }

    //监听,标记套接字可用了,可以拿去连接了,且有个可以长度为3的队列 
    if (listen(listenfd, 2) < 0) {
        prrexit("listen");
    }
    
    printf("Accepting connetions...\n");

    while (1) {
        cliaddr_len = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);//一调用accept()时accept()就进入阻塞状态,只有当客户端请求连接后,然后基于TCP的客户端返回ACK同意连接消息后,然后客户端发送ACK给服务端告诉服务端已经连接了,客户端收到后accept()就接收客户端送来的包且生成一个套接字,这个套接字就是一个文件描述符,可以理解这个文件描述符指的是某个文件,而这个文件里面包含了客户端送来的数据,而接下来服务端对这个文件里的东西进行操作,服务端一旦完成某操作时,客户端传的数据也会被操作。这就好像两个人共享一个房子,无论谁对里面的同时属于两个人的东西进行动作(客户端传传数据,服务端操作数据),都会改变里面的东西。
        if (connfd < 0) {
            prrexit("accept");
        }
        printf("receive from %s: %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), htons(cliaddr.sin_port));

        task_pool_push(tp, connfd);//一旦连接客户端就把生存的套接字存到队列中(线程池)

    }
        task_pool_free(tp);
    return 0;
}

使用epoll+线程池并发处理多个

(如果连接的线程数超过创建的线程数,多余的线程只能在队列中阻塞等待,等前面的线程结束才能连接。而select或epoll就会把所有的文件描述符放在一个空间里监听着。某一线程可能执行一会就不执行了,而等待队列中的文件描述符还在阻塞等待,所以为了提高效率,服务器应该先去处理其他套接字)

【epoll不是一个线程处理一个文件描述符,而是一个线程处理一个文件描述符的一个需求动作,当完成这个需求就结束当前线程,让空出来的线程去连接其他文件描述符】

【怎么知道某线程有需求动作】(select的解决方式:轮询。把连接进来的文件描述符放到一个集合里面,一个一个地问,从第一个开始问是否有需要,如果没有就问下一个,如果有就为你服务,无限循环询问)(epoll的解决方式:把连接进来的文件描述符放到一个红黑树里面,然后阻塞在那里,如果某个套接字有需求动作,就会触发某个条件调用callback函数回调,通知服务器让把一个线程给这个需求){这就好比,select是服务员一个一个地去问客人要不要服务,而epoll是客户如果要服务就自己叫一个服务员}

epoll:通过注册callback函数的方式,当某个文件描述符发送变化的时候,就主动通知。(epoll会监听多个文件描述符是否有I/O操作)
epoll_create()创建一个集合存放多个套接字

#include<sys/epoll.h>

int epoll_create(int size);
int epoll_createl(int falgs);

epoll_ctl()不仅要监听着看否有新套接字连接进来(如果有新套接字进来就把这个套接字放到集合中)而且看某个套接字是否有需求动作

#include<sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//epfd为创建地集合,op参数可有EPOLL_CTL_ADD,EPOLL_CTL_MOD, EPOLL_CTL_DEL,就是对集合地操作,可增加、减少,修改集合中的套接字,fd为监听的套接字,看这个套接字有没有需求,event就是事件,一个结构体,对fd的需求事件结构体进行赋值

event结构体
strcut epoll_event {
    unit32_t events;//事件,什么需求
    epoll_data_t data;//文件描述符,就哪个文件描述符有需求【需求宏:EPOLLIN\EPOLLOUT\EPOLLRDHUP\EPOLLPRI\EPOLLERR\EPOLLET\EPOLLONESHOT,要触发某个宏要 或 上EPOLLET】
};

typedef union epoll_data {
    void *ptr;
    int fd;//需要监听的套接字,一般会用这个
    uint32_t u32;
    unit64_t u64;
} epoll_data_t;


epoll_wait()阻塞等待套接字进来

#include<sys.epoll.h>

int nfds = epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);

服务器代码演示:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<strings.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<pthread.h>
#include<sys/epoll.h>

#define SERV_PORT 8000
#define MAXLINE 80

#define prrexit(msg) {\
    perror(msg);\
    exit(1);\
}

typedef struct Task {
    int fd;
    struct Task *next;
}Task;

typedef struct Task_pool {
    Task *head;
    Task *tail;
    pthread_mutex_t lock;
    pthread_cond_t havetask;
} Task_pool;

Task_pool *Task_pool_init() {
    Task_pool *tp = (Task_pool *) malloc(sizeof(Task_pool));
    tp->head = NULL;
    tp->tail = NULL;
    pthread_mutex_init(&tp->lock, NULL);
    pthread_cond_init(&tp->havetask, NULL);

    return tp;
}

void task_pool_push(Task_pool *tp, int fd) {

    pthread_mutex_lock(&tp->lock);

    Task *t = (Task *) malloc(sizeof(Task));
    t->fd = fd;
    t->next = NULL;
    
    if (!tp->tail) {
        tp->head = tp->tail = t;
    } else {
        tp->tail->next = t;
        tp->tail = t;
    }

    pthread_cond_broadcast(&tp->havetask);

    pthread_mutex_unlock(&tp->lock);
    return ;
}


Task task_pool_pop(Task_pool *tp) {

    pthread_mutex_lock(&tp->lock);

    while (tp->head == NULL) {
        pthread_cond_wait(&tp->havetask, &tp->lock);
    }

    Task tmp, *k;
    k = tp->head;
    tmp = *k;
    tp->head = tp->head->next;

    if (!tp->head) {
        tp->tail = NULL;
    }
    pthread_mutex_unlock(&tp->lock);
    return tmp;
}

void task_pool_free(Task_pool *tp) {
    pthread_mutex_lock(&tp->lock);
    Task *p = tp->head, *k;
    
    while (p) {
        k = p;
        p = p->next;
        free(k);
    }
    tp->head = NULL;

    pthread_mutex_unlock(&tp->lock);
    pthread_mutex_destroy(&tp->lock);
    pthread_cond_destroy(&tp->havetask);
    free(tp);
    return ;
}

void *up_server(void *arg) {
    pthread_detach(pthread_self());

    char buf[MAXLINE];
    int n;
    
    Task_pool *tp = arg;
    
    if (1) {
        Task tmp = task_pool_pop(tp);
        int connfd = tmp.fd;
        printf("get task fd = %d\n", connfd);
        while (1) {
            n = read(connfd, buf, MAXLINE);
            write(1, buf, n);
            for (int i = 0; i < n; i++) {
                buf[i] = toupper(buf[i]);
            }
            write(connfd, buf, n);
        }
        printf("bye task fd = %d\n", connfd);
        if (!strncmp(buf, "QUIT", 4)) {
            close(connfd);
        }
        //close(connfd);不能close,因为还有可能其他需求
    }
    return (void *)0;
}

int main() {

    struct sockaddr_in serveraddr, cliaddr;
    int listenfd, connfd;
    socklen_t cliaddr_len;

    //char buf[MAXLINE];
    int n;
    char str[INET_ADDRSTRLEN];

    Task_pool *tp = Task_pool_init();
    
//多线程
    pthread_t tid;
    for (int i = 0; i < 4; i++) {
        pthread_create(&tid, NULL, up_server, (void *)tp);
        printf("new thread is %lx\n", tid);
    }

    listenfd =  socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        prrexit("socket");
    }
    
    
    
//创建一个集合红黑树
    int epfd = epoll_create(256);

    struct epoll_event event, events[256];//这里可以这样理解这个数组,数组中每个元素都可以存套接字和需求,所以可以把数组为一定数量套接字的需求的集合
    event.events = EPOLLIN | EPOLLET;
    event.data.fd = listenfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event);//上面创建的套接字和这个套接字所有可能的需求放到epfd这个集合中
    
    
    
    bzero(&serveraddr, sizeof(serveraddr));//清空结构体内容
    //对可以存放IP地址和端口的结构体赋值
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(SERV_PORT);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY其实就是0,对应的IP地址为0.0.0.0

    //给定义好的套接字赋值一个结构体,里面有IP地址和端口。也就是给套接字绑定上IP地址和端口
    if (bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
        prrexit("bind");
    }

    //监听,标记套接字可用了,可以拿去连接了,且有个可以长度为3的队列 
    if (listen(listenfd, 2) < 0) {
        prrexit("listen");
    }
    
    printf("Accepting connetions...\n");

    while (1) {
    
        int nfds = epoll_wait(epfd, events, 256, -1);//阻塞等待需求到来
        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == listenfd) {//如果此索引的需求套接字为监听的套接字就能进入这判断;能进入这里的都是之前没有加需求动作的套接字(新客人到),就进行调用accep()等待连接,记住accpe()返回的套接字为从客户端带有需求动作的套接字;可以理解这判断语句的这部分为把没有需求的套接字连接后成具有需求的套接字,然后放到红黑树中

                
            cliaddr_len = sizeof(cliaddr);
            connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);//接收套接字且返回一个新的套接字
            if (connfd < 0) {
                prrexit("accept");
            }
            printf("receive from %s: %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), htons(cliaddr.sin_port));
                
                
            event.events = EPOLLIN | EPOLLET;
            event.data.fd = connfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);//把带有需求动作的套接字connfd放到集合epfd中,且对connfd进行监听,看是否有新需求动作。
         
                

            } else if (events[i].events & EPOLLIN){//如果数组当前索引的套接字不是监听的套接字(监听的套接字为标记的套接字,标记的套接字为可连接的套接字;到来这里的套接字为具有需求动作的套接字)
                int clifd = events[i].data.fd;//就用一个新的变量保存数组中的这个套接字。为什么要保存,因为数组存的是当前监听的套接字,如果不是了说明数组中还有其他的套接字,就要把这个套接字放入线程池中
                if (clifd < 3) {//可能异常中断
                    continue;
                }
                task_pool_push(tp, clifd);
            }
        }
    }
    task_pool_free(tp);
    return 0;
}

web服务器实现

超文本传输协议(英文:HyperText Transfer Protocol, 缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。

1、HTTP协议采用了请求/响应模型
2、无状态保存

HTTP是一种不保存状态,即无状态(stateless)协议。HTTP协议自身不对请求和响应之间的通信状态进行保存。也就是说HTTP这个级别,协议对于发送过的请求或响应都不做持久化处理。
3、无连接。

无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间,并且可以提高高并发性能,不能和每个用户建立长久的连接,请求一次相应一次,服务端和客户端就中断了。

HTTP请求办法:

HTTP/1.1协议中共定义了八种办法(也叫“动作”)来以不同方式操作指定的资源:

GET:向指定的资源发出“显示”请求

POST:向指定资源提交数据,请求服务器进行**处理(**例如提交表单或者上传文件)。数据被包含在请求本文中。

HEAD:于GET一样,都是向服务器发出指定资源的请求。只不过服务器将不传回资源的本文部分。

PUT:向指定资源位置上传其最新内容。

TRACE:回显服务器收到的请求,主要用于测试或诊断。

OPTIONS:这个办法可使服务器传回该资源所支持的所有HTTP请求办法。

CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。通常用于HTTP代理服务器的链接(经由非加密的HTTP代理服务器)。

所有的HTTP响应的第一行都是状态行,依次是当前HTTP版本号,3位数字组成的状态代码,以及描述状态的短语,彼此由空格分隔。
URL(路径)

超文本传输协议(HTTP)的统一资源定位符将从因特网获取信息的五个基本元素包括在一个简单的地址中:

传送协议。

层级URL标记符号(为[//],固定不变)

访问资源需要的凭证信息(可省略)

服务器(通常为域名,有时为IP地址)

端口号(以数字方式表示,若为HTTP的默认值“:80”可省略)

路径(以“/”字符区别路径中的每一个目录名称)

查询(GET模式的窗体参数,以“?”字符为起点,每个参数以“&”隔开,再以“=“分开参数名称与数据,通常以UTF8的URL编码,避开字符冲突的问题)

片段 以”#“字符为起点

HTTP请求格式(请求协议)
HTTP响应格式(响应协议)
报文:

本文标签: 存了 网络编程 笔记