C/S, TCP/IP协议妙趣横生、惟妙惟肖谈
一、客户端与服务器
浏览器就是一个可执行程序(客户端),淘宝网nginx服务器返回数据包,来回很多次才完全发完,最后发一个特殊的包结束。
【客户端服务器角色规律总结】
a)数据通讯总在两端进行,其中一端叫客户端,另一端叫服务器端;
b)总有一方先泛起第一个数据包,这发起第一个数据包的这一端,就叫客户端【浏览器】;被动收到第一个数据包这端,叫服务器端【淘宝服务器】;
c)连接建立起来,数据双向流动(双工)
d)既然服务器端是被动接收连接,那么客户端必须得能够找到服务器在哪里;
浏览器要访问淘宝网,需要知道淘宝服务器的地址(IP地址),以及淘宝服务器的姓名(端口号,一个0-65535的无符号数字)
淘宝网服务器(nginx服务器)会调用listen()函数来监听80端口。
在编写网络通讯程序时,只需要指定淘宝服务器的ip地址和淘宝服务器的端口号,就能够跟淘宝服务器进行通讯。
e)epoll:单台服务器高并发
二、网络模型
1、OSI七层网络模型
物【物理层】 链【数据链路层】 网【网络层】 传【传输层】 会【会话层】 表【表示层】 应【应用层】
把一个要发送出去的数据包从里到外裹了7层发送到网络上去。
2、TCP/IP协议四层模型
tcp/ip实际是一组协议的代名词,而不仅仅是一个协议;每一层都对应着一些协议。
3、TCP/IP协议的解释和比喻
上层数据先加TCP头,再加IP头,再加LLC头,再加MAC头。
三、最简单的客户端和服务器程序实现代码
1、套接字socket概念
套接字(socket):就是个数字,通过调用socket()函数来生成;这个数字具有唯一性;一直用直到调用close()函数把这个数字关闭;一切皆文件,咱们就把socket也看成是文件描述符,我们可以用socket来收发数据;send(),recv()。
2、一个简单的服务器端通讯程序范例
3、IP地址
写服务器程序,不用考虑ipv4、ipv6的问题,遵照ipv4规则写就行;
写客户端程序,只演示ipv4版本的客户端范例。
4、一个简单的客户端通讯程序范例
c/s建立连接时双方彼此都要有ip地址/端口号;连接一旦建立起来,那么双方的通讯【双工收发】就只需要用双方彼此对应的套接字即可。
5、客户端服务器程序综合演示和调用流程图
服务器端程序要先运行
四、TCP和UDP的区别
TCP(Transfer Control Protocol):传输控制协议
UDP(User Datagram Protocol):用户数据报协议
TCP协议:可靠的面向连接的协议,数据包丢失的话操作系统底层会感知并且帮助你重新发送数据包;
UDP协议:不可靠的,无连接的协议。
【优缺点】
a)tcp:可靠协议,耗费更多的系统资源确保数据传输的可靠;得到好处就是只要不断线,传输给对方的数据,一定正确的,不丢失,不重复,按顺序到达对端;
b)udp:不可靠协议,发送速度特别快;但无法确保数据可靠性。
【各自的用途】
a)tcp:文件传输,收发邮件需要准确率高,但效率可以相对差;一般TCP比UDP用的范围和场合更广。
b)udp:qq聊天信息,DNS解析等,估计随着网络的发展,网络性能更好,丢包率更低,那么udp应用范围更广。
TCP三次握手详析、telnet,wireshark示范
一、TCP连接的三次握手
只有TCP有三次握手(UDP没有)
1、最大传输单元MTU
MTU:每个数据包包含的数据最多可以有多少个字节,1.5K左右;
要发送100K,操作系统内部会把这100K数据拆分成若干个数据包(分片),每个数据包大概1.5K之内(大概拆解成68个),对端重组;我们只需要知道有拆包,组包,这68个包各自传送的路径可能不同,每一个包可能因为路由器,交换机原因可能被再次分片;最终TCP/IP协议保证了我们收发数据的顺序性和可靠性。
2、TCP包头结构(往左90度)
a)源端口,目标端口
b)关注syn位和ack位(开/关)
c)一个tcp数据包,是可能没有包体,此时,总会设置一些标志位来达到传输控制信息的目的,起控制作用
3、TCP数据包收发之前的准备工作
TCP数据的收发是双工的:每端既可以收数据,又可以发数据。
【TCP数据包的收发三大步骤】
a)建立TCP连接[connect:客户端],三次握手
b)多次反复的数据收发[read/wirte]
c)关闭TCP连接[close]
UDP不存在三次握手来建立连接的问题。UDP数据包是直接发送出去,不用建立所谓的连接。
4、TCP三次握手建立连接的过程([syn] [syn/ack] [ack])
1)客户端给服务器发送 了一个SYN标志位置位的无包体TCP数据包,SYN被置位,就表示发起TCP链接;
2)服务器收到了这个SYN标志位置位的数据包,服务器给客户端返回一个SYN和ACK标志位都被置位的无包体TCP数据包;
3)客户端收到服务器发送回来的数据包之后,再次发送ACK置位的数据包,服务器端收到这个数据包之后,客户端和服务器端的TCP链接就正式建立。
5、为什么TCP握手是三次握手而不是二次
三次握手很大程度上是为了防止恶意的人坑害别人而引入的一种TCP连接验证机制。
为了确保数据稳定可靠的收发 ,尽量减少伪造数据包对服务器的攻击。
源ip,源端口 ---------------- 目的ip,目的端口
syn------------->
<--------syn/ack(验证源ip和源端口真实存在)
ack-------------->
二、telnet工具使用介绍
telnet是一款命令行方式运行的客户端TCP通讯工具,可以连接到服务器端,往服务器端发送数据,也可以接收从服务器端发送过来的信息;该工具能够方便的测试服务器端的某个TCP端口是否通,是否能够正常收发数据,所以是一个非常实用,重要,常用的工具。
telnet ip地址 端口号
telnet在windows下输入一个字符就往server发,在linux下输入字符后要回车才往server发。
三、wireshark监控数据包
wireshark是个软件,分析网络数据包,规则:host 192.168.1.126 and port 9000。
【TCP断开的四次挥手】
a)FIN,ACK 服务器->客户端
b)ACK 客户端->服务器
c)FIN,ACK 客户端->服务器
d)ACK 服务器->客户端
TCP状态转换,TIME_WAIT详解,SO_REUSEADDR
一、TCP状态转换
同一个IP(INADDR_ANY),同一个端口SERV_PORT,只能被成功的bind()一次,若再次bind()就会失败,并且会显示:Address already in use。
介绍命令netstat:显示网络相关信息
-a:显示所有选项
-n:能显示成数字的内容全部显示成数字
-p:显示段落这对应程序名
netstat -anp | grep -E 'State|9000'
【测试】用两个客户端连接到服务器,服务器给每个客户端发送一串字符"I sent sth to client!\n",并关闭客户端;我们用netstat观察,原来那个监听端口一直在监听,但是当来了两个连接之后(连接到服务器的9000端口),虽然这两个连接被close掉了,但是产生了两条TIME_WAIT状态的信息。
客户端连接到服务器,并且服务器把客户端关闭,服务器端就会产生一条针对9000监听端口的状态为 TIME_WAIT 的连接;只要用netstat看到 TIME_WAIT状态的连接,那么此时杀掉服务器程序再重新启动,就会启动失败,bind()函数返回失败。
TCP状态转换图(11种状态)是针对一个TCP连接来说的。
客户端: CLOSED ->SYN_SENT->ESTABLISHED【连接建立,可以进行数据收发】
服务端: CLOSED ->LISTEN->【客户端来握手】SYN_RCVD->ESTABLISHED【连接建立,可以进行数据收发】
谁主动close连接,谁就会给对方发送一个FIN标志置位的一个数据包给对方,假设是服务器端先关闭:
服务器主动关闭连接:ESTABLISHED->FIN_WAIT1->FIN_WAIT2->TIME_WAIT(等一段时间回到close状态)
客户端被动关闭:ESTABLISHED->CLOSE_WAIT->LAST_ACK
二、TIME_WAIT状态
【何时产生】四次挥手主动关闭的一方就会产生!
具有TIME_WAIT状态的TCP连接,就好像一种残留的信息一样,服务器程序退出并重新执行会失败;连接处于TIME_WAIT状态是有时间限制的(1-4分钟之间) = 2 MSL(最长数据包生命周期)。
【为什么要引入TIME_WAIT】:
(1)可靠的实现TCP全双工的终止
如果服务器最后发送的ACK包因为某种原因丢失了,那么客户端一定会重新发送FIN,这样因为服务器端有TIME_WAIT的存在,服务器会重新发送ACK包给客户端,但是如果没有TIME_WAIT这个状态,服务器都已经关闭连接了,此时客户端重新发送FIN,服务器给回的就不是ACK包,而是RST(连接复位)包,从而使客户端没有完成正常的4次挥手,可能出现错误;
RST标志:当我们close一个TCP连接时,如果发送缓冲区有数据,那么操作系统会很优雅的把发送缓冲区里的数据发送完毕,然后再发fin包表示连接关闭;出现这个RST标志的包一般都表示异常关闭,如果发生了异常,一般都会导致丢失一些发送缓冲区的数据包,主动关闭一方也不会进入TIME_WAIT。
(2)允许老的重复的TCP数据包在网络中消逝
如果最后一个ack还没到客户端,不用TIME_WAIT来阻止新server启动,这时启动一个server,再来一个老数据包,就不知道这个包是给老的还是给新的了,容易引起混乱。所以维持TIME_WAIT一段时间是为了在这段时间内彻底丢弃老的数据包,等待最后一个ack到达客户端,再全部结束。
三、SO_REUSEADDR选项
setsockopt(SO_REUSEADDR)用在服务器端,socket()创建之后,bind()之前。
所有TCP服务器都应该指定本套接字选项,以防止当套接字处于TIME_WAIT时bind()失败的情形出现。
【设置后测试】两个进程,绑定同一个IP和端口:bind()失败;TIME_WAIT状态时的bind绑定:bind()成功。
SO_REUSEADDR:主要解决TIME_WAIT状态导致bind()失败的问题。
listen()队列剖析、阻塞非阻塞、同步异步
一、listen()队列剖析
listen():监听端口,用在TCP连接中的服务器端角色。
listen()函数调用格式:int listen(int sockfd, int backlog);
要理解好backlog这个参数,要了解 “监听套接字队列”!
1、监听套接字的队列
对于一个调用listen()进行监听的套接字,操作系统会给这个套接字维护两个队列:
a)未完成连接队列(保存连接用的)
当客户端发送tcp连接三次握手的第一次(syn包)给服务器的时候,服务器就会在未完成队列中创建一个跟这个syn包对应的一项,我们可以把这项看成是一个半连接(因为连接还没建立),这个半连接的状态会从LISTEN变成SYN_RCVD状态,同时给客户端返回第二次握手包(syn,ack包),这个时候,其实服务器是在等待完成第三次握手。
b)已完成连接队列(保存连接用的)
当第三次握手完成了,这个连接就变成了ESTABLISHED状态,每个已经完成三次握手的客户端都放在这个队列中作为一项。
backlog曾经的含义:已完成队列和未完成队列里边条目之和不能超过backlog。
【总结】
(1)客户端这个connect()什么时候返回?收到三次握手的第二次握手包(也就是收到服务器发回来的syn/ack)之后返回。
(2)RTT是未完成队列中任意一项在未完成队列中留存的时间,这个时间取决于客户端和服务器。
对于客户端,这个RTT时间是第一次和第二次握手加起来的时间;对于服务器,这个RTT时间实际上是第二次和第三次握手加起来的时间。如果这三次握手包传递速度特别快的话,大概187毫秒能够建立起来这个连接,这个时间不快,所以建立TCP连接的成本很高。
(3)如果一个恶意客户,迟迟不发送三次握手的第三个包。那么这个连接就建立不起来,那么这个处于SYN_RCVD的这一项,就会一致停留在服务器的未完成队列中,这个停留时间大概是75秒,如果超过这个时间,这一项会被操作系统干掉。
2、accept()函数
accept()函数:就是用来从已完成连接队列中的队首位置取出来一项(每一项都是一个已经完成三路握手的TCP连接)返回给进程。如果已完成连接队列是空,那么accept()一般会一直卡在这里(休眠)等待,一直到已完成队列中有一项时才会被唤醒。因为客户端connet返回就可以收发数据了,所以服务器端要尽快地用accept()把已完成队列中的数据(TCP连接)取走。
accept()返回的是个套接字,这个套接字就代表那个已经用三次握手建立起来的那个tcp连接,因为accept()是从已完成队列中取的数据,所以一定要区别连个服务器端的两个套接字:
a)监听9000端口这个套接字——“监听套接字”(listenfd),只要服务器程序在运行,这个套接字就应该一直存在;
b)当客户端连接进来,操作系统会为每个成功建立三次握手的客端再创建一个套接字(connfd),accept()返回的就是这种套接字,也就是从已完成连接队列中取得的一项。随后,服务器是使用这个accept()返回的套接字和客户端通信的。
【思考题】
(1)如果两个队列之和(已完成连接队列和未完成连接队列)达到了listen()所指定的第二参数,也就是说队列满了,此时,再有一个客户发送syn请求,服务器怎么反应?
实际上服务器会忽略这个syn,不给回应;客户端这边,发现syn没回应,过一会会重发syn包。
(2)从连接被扔到已经完成队列中去,到accept()从已完成队列中把这个连接取出之间是有个时间差的,如果还没等accept()从已完成队列中把这个连接取走的时候,客户端如果发送来数据,这个数据就会被保存在已经连接的套接字的接收缓冲区里,这个缓冲区有多大,最大就能接收多少数据量。
3、syn攻击(syn flood):典型的利用TCP/IP协议涉及弱点进行坑爹的一种行为。
拒绝服务攻击(DOS/DDOS):不停发第一次握手的syn包,服务器未完成队列中就越来越多,总会超过backlog;
所以backlog被进一步明确和规定了:已完成连接队列中最大条目数。虽然这样即使未完成连接队列被填满后还是无法处理新的连接,但是系统会自动处理未完成连接队列的内容,75s后删除。
所以在写代码时尽快用accept()把已完成队列里边的连接取走,尽快留出空闲位置给后续的已完成三次握手的条目用,这个已完成队列就一般不会满,一般这个backlog值给300左右。
二、阻塞与非阻塞I/O
阻塞和非阻塞主要是指调用某个系统函数时,这个函数是否会导致我们的进程进入sleep()(卡住休眠)状态而言的。
1、阻塞I/O
调用一个函数,这个函数卡在这里等待一个事情发生,只有这个事情发生了,这个函数才会往下走,如阻塞函数accept()。
这种阻塞并不好,效率很低,所以一般不会用阻塞方式来写服务器程序。
2、非阻塞I/O:不会卡住,充分利用时间片,执行效率更高。
【非阻塞模式的两个鲜明特点】
(1)不断地调用accept()、recvfrom()函数来检查有没有数据到来,如果没有,函数会返回一个特殊的错误标记来标识,这种标记可能是EWULDBLOCK,也可能是EAGAIN;如果数据没到来,那么这里有机会执行其他函数,但是也得不停的再次调用accept(),recvfrom()来检查数据是否到来,非常累;
(2)如果数据到来,那么就得卡在这里把数据从内核缓冲区复制到用户缓冲区,所以复制这个阶段是卡着完成的。
三、同步与异步I/O:这两个概念容易和阻塞/非阻塞混淆
1、异步I/O:
调用一个异步I/O函数时,要给这个函数指定一个接收缓冲区,我还要给定一个回调函数;
调用完一个异步I/O函数后,该函数会立即返回,其余判断交给操作系统,操作系统会判断数据是否到来,如果数据到来了,操作系统会把数据拷贝到你所提供的缓冲区里,然后调用你所指定的这个回调函数来通知你。
【非阻塞和异步I/O区别】
a)非阻塞I/O要不停的调用I/O函数来检查数据是否来,如果数据来了,就得卡在I/O函数这里把数据从内核缓冲区复制到用户缓冲区,然后这个函数才能返回;
b)异步I/O根本不需要不停的调用I/O函数来检查数据是否到来,只需要调用一次,然后就可以干别的事情去了;内核判断数据到来,拷贝数据到你提供的缓冲区,调用你的回调函数来通知你,并没有被卡在那里的情况。
2、同步I/O:select/poll和epoll
步骤一:调用select()判断有没有数据,有数据,走下来,没数据卡在那里;
步骤二:select()返回之后,用recvfrom()去取数据,当然取数据的时候(内核到用户)也会卡那么一下。
同步I/O感觉更麻烦,要调用两个函数才能把数据拿到手;但是同步I/O和阻塞式I/O比,有 I/O复用(2个函数的)能力。所谓I/O复用,就是多个socket(多个TCP连接)可以弄成一捆,我可以用select等数据,因为select()的能力是等多条TCP连接上的任意一条有数据来都可以感知到,然后哪条TCP有数据来,我再用具体的比如recvfrom()去收。
所以,这种调用一个函数能够判断一堆TCP连接是否来数据的这种能力,叫I/O复用,英文I/O multiplexing。
很多资料把阻塞I/O,非阻塞I/O,同步I/O归结为一类 ,因为他们多多少少的都有阻塞的行为发生;也可以把阻塞I/O,非阻塞I/O 都归结为同步I/O模型,而把异步I/O单独归结为一类,因为异步I/O是真正的没有阻塞行为发生的。
【通俗理解】参考:https://www.cnblogs.com/wangzhaobo/articles/9596623.html
阻塞与非阻塞关注的是在等待数据来的时候是不是在干别的事(有没有一直傻等),而同步与异步关注的是如何获取到数据来了这件事,是自己看到的还是别人告诉的,所以阻塞和非阻塞都算有一点同步。现实中非阻塞异步是最符合我们常理的,好比我们去买煎饼,我跟摊煎饼的阿姨说,你摊好了给我打电话(异步),然后我就去聊天、玩手机(非阻塞),然后摊好了通知我(回调函数),我取走煎饼。最蠢的其实就是阻塞异步地做事,就好比你跟阿姨说数据说你来了通知我一下,然后你还在哪里一直等着数据来而不干别的事(阻塞),那你让阿姨通知你干嘛?同理,阻塞同步就是一直站在那里等煎饼出来,然后看到煎饼就拿走,因为是自己看而不是等通知所以是同步。非阻塞同步就是去聊天玩手机,然后每过一会儿就过来看一下煎饼摊好没有,摊好了就拿走。