不定期推送关于后端开发、爬虫、算法题、数据结构方面的原创技术文章,以及生活中的逸闻趣事。 我目前是一名后端开发工程师。主要关注后端开发,数据安全,网络爬虫,物联网,边缘计算等方向。 原创博客主要内容
TIM截图20190218204943.png 前言本文快速回顾了计算机网络书本中常考的的知识点,用作面试复习,事半功倍。 主要内容有:计算机网络体系结构,TCP与UDP,UDP/TCP实现DEMO代码 -----正文开始-----基础计算机网络体系结构 在这里插入图片描述 1. 五层协议
数据报->分组->帧->比特流 2. 七层协议 其中表示层和会话层用途如下:
五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。 3. 数据在各层之间的传递过程 在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。 路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要运输层和应用层。 4. TCP/IP 它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。 现在的 TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层 TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中占用举足轻重的地位。 在这里插入图片描述 物理层/数据链路层/网络层知识点偏通信理论的多一些,可以放在后面复习 传输层TCP与UDP的特点 用户数据报协议 UDP(User Datagram Protocol):无连接的,尽最大可能交付,没有拥塞控制,面向报文 对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部,支持一对一、一对多、多对一和多对多的交互通信。 传输控制协议 TCP(Transmission Control Protocol)是有连接的,提供可靠交付,有流量控制,拥塞控制,面向字节流 把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块,每一条 TCP连接只能是点对点的(一对一)。 总结(TCP和UDP的区别): 1)TCP提供面向连接的传输;UDP提供无连接的传输 2)TCP提供可靠的传输(有序,无差错,不丢失,不重复);UDP提供不可靠的传输。 3)TCP面向字节流的传输,因此它能将信息分割成组,并在接收端将其重组;UDP是面向数据报的传输,没有分组开销。 4)TCP提供拥塞控制和流量控制机制;UDP不提供拥塞控制和流量控制机制。 5)TCP只能是点对点的(一对一)。UDP支持一对一、一对多、多对一和多对多的交互通信。 首部格式 UDP 在这里插入图片描述 首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。 TCP 在这里插入图片描述
TCP拆包粘包 如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况。 分包机制一般有两个通用的解决方法: 1,特殊字符控制 2,在包头首都添加数据包的长度 如果使用netty的话,就有专门的编码器和解码器解决拆包和粘包问题了。 tips: UDP没有粘包问题,但是有丢包和乱序。不完整的包是不会有的,收到的都是完全正确的包。传送的数据单位协议是UDP报文或用户数据报,发送的时候既不合并,也不拆分。 三次握手四次挥手: https://blog.csdn.net/qzcsu/article/details/72861891 三次握手 (1)第一步:源主机A的TCP向主机B发出连接请求报文段,其首部中的SYN(同步)标志位应置为1,表示想与目标主机B进行通信,**并发送一个同步序列号X(例:SEQ=100)进行同步,表明在后面传送数据时的第一个数据字节的序号是X+1(即101)**。SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号。(2)第二步:目标主机B的TCP收到连接请求报文段后,如同意,则发回确认。在确认报中应将ACK位和SYN位置1,表示客户端的请求被接受。确认号应为X+1(图中为101),同时也为自己选择一个序号Y。(3)第三步:源主机A的TCP收到目标主机B的确认后要向目标主机B给出确认,其ACK置1,确认号为Y+1,而自己的序号为X+1。**TCP的标准规定,SYN置1的报文段要消耗掉一个序号。** 运行客户进程的源主机A的TCP通知上层应用进程,连接已经建立。当源主机A向目标主机B发送第一个数据报文段时,**其序号仍为X+1,因为前一个确认报文段并不消耗序号。** 当运行服务进程的目标主机B的TCP收到源主机A的确认后,也通知其上层应用进程,连接已经建立。至此建立了一个全双工的连接。 在这里插入图片描述 三次握手的原因 如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。 如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。 四次挥手
在这里插入图片描述 等待 2MSL(最长报文段寿命) 的原因 书中解释: 在这里插入图片描述 TCP采用四次挥手关闭连接如图所示为什么建立连接协议是三次握手,而关闭连接却是四次握手呢? 建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。 而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。 如果已经建立了连接,但是客户端突然出现故障了怎么办? TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。 Time-wait状态好处和坏处 好处 上方所述两点 坏处 高并发下,端口都处在timewait很快就用完端口。 解决方法:
http://blog./xmlrpc.php?r=blog/article&uid=28541347&id=5748888
什么情况下会出现RST包 看这篇就够了:https://blog.csdn.net/eric0318/article/details/51113018 TCP 滑动窗口 在这里插入图片描述 窗口是缓存的一部分,用来暂时存放字节流。 发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {32, 33} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 TCP 可靠传输(超时重传) TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。 一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下: image 超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下: image 其中 RTTd 为偏差。 TCP 流量控制 流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。 TCP 拥塞控制 在这里插入图片描述 在这里插入图片描述
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 为了便于讨论,做如下假设:
1. 慢开始与拥塞避免 发送的最初执行慢开始,令 cwnd=1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 … 注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。 如果出现了超时,则令 ssthresh = cwnd/2,然后重新执行慢开始。 2. 快重传与快恢复 在接收方,要求每次接收到报文段都应该发送对已收到有序报文段的确认,例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。 快重传 在发送方,如果收到三个重复确认,那么可以确认下一个报文段丢失,例如收到三个 M2 ,则 M3 丢失。此时执行快重传,立即重传下一个报文段。 快恢复 在快重传情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。 慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。 UDP/TCP代码DEMO 经典:https://blog.csdn.net/column/details/socket.html TCP:https://blog.csdn.net/ns_code/article/details/14105457 UDP:https://blog.csdn.net/ns_code/article/details/14128987 TCP TCP连接的建立步骤 客户端向服务器端发送连接请求后,就被动地等待服务器的响应。典型的TCP客户端要经过下面三步操作:
服务端的工作是建立一个通信终端,并被动地等待客户端的连接。典型的TCP服务端执行如下两步操作:
Demo ServerDemo.java /** * sinture.com Inc. * Copyright (c) 2016-2018 All Rights Reserved. */package test.socketDemo.TCP;import java.io.IOException;import java.net.ServerSocket;import java.net.Socket;/** * @author yzd * @version Id: ServerDemo.java, v 0.1 2018年07月10日 12:36 yzd Exp $ */public class ServerDemo { public static void main(String[] args) throws IOException { // 服务端在20006端口监听客户端请求的TCP连接 ServerSocket server = new ServerSocket(20000); Socket client = null; boolean f = true; while(f){ // 等待客户端的连接,如果没有获取连接 client = server.accept(); System.out.println('与客户端连接成功!'); // 为每个客户端连接开启一个线程 new Thread(new ServerThread(client)).start(); } }} ServerThread.java /** * sinture.com Inc. * Copyright (c) 2016-2018 All Rights Reserved. */package test.socketDemo.TCP;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintStream;import java.net.Socket;/** * @author yzd * @version Id: ServerThread.java, v 0.1 2018年07月10日 13:41 yzd Exp $ */public class ServerThread implements Runnable { private Socket client = null; public ServerThread(Socket client){ this.client = client; } @Override public void run() { try{ //获取Socket的输出流,用来向客户端发送数据 PrintStream out = new PrintStream(client.getOutputStream()); //获取Socket的输入流,用来接收从客户端发送过来的数据 BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream())); boolean flag = true; while (flag){ //接收从客户端发送过来的数据 String str = buf.readLine(); if(str == null || ''.equals(str)){ flag = false; }else { if('bye'.equals(str)){ flag = false; }else{ //将接收到的字符串前面加上echo,发送到对应的客户端 out.println('echo:' + str); } } } out.close(); client.close(); } catch (IOException e) { e.printStackTrace(); } }} ClientDemo.java /** * sinture.com Inc. * Copyright (c) 2016-2018 All Rights Reserved. */package test.socketDemo.TCP;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintStream;import java.net.Socket;import java.net.SocketTimeoutException;/** * @author yzd * @version Id: ClientDemo.java, v 0.1 2018年07月10日 14:05 yzd Exp $ */public class ClientDemo { public static void main(String[] args) throws IOException { //客户端请求与本机在20006端口建立TCP连接 Socket client = new Socket('127.0.0.1', 20000); client.setSoTimeout(10000); //获取键盘输入 BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); //获取Socket的输出流,用来发送数据到服务端 PrintStream out = new PrintStream(client.getOutputStream()); //获取Socket的输入流,用来接收从服务端发送过来的数据 BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream())); boolean flag = true; while(flag){ System.out.print('输入信息:'); String str = input.readLine(); //发送数据到服务端 out.println(str); if('bye'.equals(str)){ flag = false; }else{ try{ //从服务器端接收数据有个时间限制(系统自设,也可以自己设置),超过了这个时间,便会抛出该异常 String echo = buf.readLine(); System.out.println(echo); }catch(SocketTimeoutException e){ System.out.println('Time out, No response'); } } } input.close(); if(client != null){ //如果构造函数建立起了连接,则关闭套接字,如果没有建立起连接,自然不用关闭 client.close(); //只关闭socket,其关联的输入输出流也会被关闭 } }} UDP UDP的通信建立的步骤 客户端要经过下面三步操作:
UDP服务端要经过下面三步操作:
Demo 这里有一点需要注意: UDP程序在receive()方法处阻塞,直到收到一个数据报文或等待超时。由于UDP协议是不可靠协议,如果数据报在传输过程中发生丢失,那么程序将会一直阻塞在receive()方法处,这样客户端将永远都接收不到服务器端发送回来的数据,但是又没有任何提示。为了避免这个问题,我们在客户端使用DatagramSocket类的setSoTimeout()方法来制定receive()方法的最长阻塞时间,并指定重发数据报的次数,如果每次阻塞都超时,并且重发次数达到了设置的上限,则关闭客户端。 ClientDemo.java /** * sinture.com Inc. * Copyright (c) 2016-2018 All Rights Reserved. */package test.socketDemo.UDP;import java.io.IOException;import java.io.InterruptedIOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetAddress;/** * @author yzd * @version Id: ClientDemo.java, v 0.1 2018年07月10日 14:46 yzd Exp $ */public class ClientDemo { public static final int TIMEOUT = 5000; public static final int MAXNUM = 5; public static void main(String[] args) throws IOException { String str_send = 'Hello UDPServer'; byte[] buf = new byte[1024]; //客户端在9000端口监听接收到的数据 DatagramSocket ds = new DatagramSocket(9000); InetAddress loc = InetAddress.getLocalHost(); //定义用来发送数据的DatagramPacket实例 DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),loc,3000); //定义用来接收数据的DatagramPacket实例 DatagramPacket dp_receive = new DatagramPacket(buf, 1024); //数据发向本地3000端口 ds.setSoTimeout(TIMEOUT); //设置接收数据时阻塞的最长时间 int tries = 0; //重发数据的次数 boolean receivedResponse = false; //是否接收到数据的标志位 //直到接收到数据,或者重发次数达到预定值,则退出循环 while(!receivedResponse && tries<MAXNUM){ //发送数据 ds.send(dp_send); System.out.println('Client send message succeed.'); try{ //接收从服务端发送回来的数据 ds.receive(dp_receive); //如果接收到的数据不是来自目标地址,则抛出异常 if(!dp_receive.getAddress().equals(loc)){ throw new IOException('Received packet from an unknown source'); } //如果接收到数据。则将receivedResponse标志位改为true,从而退出循环 receivedResponse = true; }catch(InterruptedIOException e){ //如果接收数据时阻塞超时,重发并减少一次重发的次数 tries += 1; System.out.println('Time out,' + (MAXNUM - tries) + ' more tries...' ); } } if(receivedResponse){ System.out.println('client received data from server:'); String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) + ' from ' + dp_receive.getAddress().getHostAddress() + ':' + dp_receive.getPort(); System.out.println(str_receive); //由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数, //所以这里要将dp_receive的内部消息长度重新置为1024 dp_receive.setLength(1024); }else{ //如果重发MAXNUM次数据后,仍未获得服务器发送回来的数据,则打印如下信息 System.out.println('No response -- give up.'); } ds.close(); }} ServerDemo.java /** * sinture.com Inc. * Copyright (c) 2016-2018 All Rights Reserved. */package test.socketDemo.UDP;import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;/** * @author yzd * @version Id: ServerDemo.java, v 0.1 2018年07月10日 15:12 yzd Exp $ */public class ServerDemo { public static void main(String[] args) throws IOException { String str_send = 'Hello UDPclient'; byte[] buf = new byte[1024]; //服务端在3000端口监听接收到的数据 DatagramSocket ds = new DatagramSocket(3000); //接收从客户端发送过来的数据 DatagramPacket dp_receive = new DatagramPacket(buf, 1024); System.out.println('Server is on,Waiting for client to send data......'); boolean f = true; while(f){ //服务器端接收来自客户端的数据 ds.receive(dp_receive); System.out.println('Server received data from client:'); String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) + ' from ' + dp_receive.getAddress().getHostAddress() + ':' + dp_receive.getPort(); System.out.println(str_receive); //数据发动到客户端的3000端口 DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),dp_receive.getAddress(),9000); ds.send(dp_send); System.out.println('Server send message succeed.'); //由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数, //所以这里要将dp_receive的内部消息长度重新置为1024 dp_receive.setLength(1024); } ds.close(); }} 应用层浏览器从输入URL地址到最终显示内容的过程 DNS查找对应ip过程 首先查找浏览器自身的DNS缓存,如果有这个域名映射且没过期(TTL)则直接向该IP发送HTTP请求,否则下一步 查找本地操作系统hosts缓存,如果有且没过期,拿出来使用完成DNS解析,否则下一步 查找本地DNS域名服务器, 如果不可以由该服务器解析,则把请求发至根域名服务器,解析该域名是由谁来授权管理,返回顶级域名服务器的IP地址 本地DNS服务器联系顶级域名服务器。 顶级域名服务器如果无法解析,则找下一级DNS服务器,并把IP发给本地DNS服务器。 以此类推,在DNS域名解析的过程中,使用UDP协议进行不可靠传输,不需要三次握手,传输需要的内容较少,使用UDP更快。 在网页开发过程中尽量减少对DNS域名的解析,天猫,淘宝等使用进行dns延迟缓存 https://www.cnblogs.com/sjm19910902/p/6423181.html HTTP请求过程
*7. Web服务器关闭TCP连接 一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码:Connection:keep-alive TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。 如果是第一次访问请求该网址 浏览器发送HTTP请求,请求头包括:
如果顺利访问:客户端返回200状态码 返回信息包括:
如果是第二次 浏览器则发出http请求时
但是如果前一次请求浏览器设置expires,则浏览器首先会检查缓存中的资源,如果在设置的expires时间之内则不会再次发送请求。 lastModified代表服务器最后修改时间,精确到秒。expires资源过期时间,精确到秒。Etag则代表资源的版本号,每次修改资源Etag就会变。不同资源的Etag不同。 如果正确访问 浏览器根据返回content-type,解析服务器返回的数据
js和css尽量使用外链形式,减少DOM结构的长度和复杂度,减少浏览器解析html文件的时间。 dom节点尽量别深度嵌套,css少使用多层选择器。 页面减少http请求的个数,多个图片使用图片dataURI编码或则图片精灵进行合并、css文件压缩合并、js文件压缩合并。配置localhost之后就不会走dns了 -----正文结束-----本文参考https://github.com/CyC2018/CS-Notes/ https://github.com/linw7/Skill-Tree/blob/master/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C.md https://www.jianshu.com/p/674fb7ec1e2c |
|