4、TCP网络传输的基本流程
二、网络编程套接字(socket)
5、cookie和session的用法
6、基本实现http协议的代码
四、传输层协议TCP和UDP
4、TCP和UDP之间的对比
六、数据链路层和应用层(以太网协议和DNS协议)
计算机网络是通过 传输介质、通信设施和网络通信协议 ,把分散在不同地点的计算机设备互连起来的,实现资源共享和数据传输的系统。网络编程就是编写程序使互联网的两个(或多个)设备(如计算机)之间进行数据传输。JAVA语言对网络编程提供了良好的支持。通过其提供的接口我们可以很方便地进行网络编程。
网络通信协议是网络编程中的关键,通信双方达成的一种共识,当通信双方都遵守这样的约定,才能正确传输信息。
1、分层可以避免一个协议太过于庞大和复杂。
2、分层之后协议之间 解耦合 ,上层协议不需要理解下层协议的细节
3、任意层次的协议的可以进行灵活的替换
在这里我们对于TCP/IP模型进行详细的描述:
面试常见:
1、对于一台主机来说,它的操作系统内核实现了从应用层到物理层
2、对于一台路由器来说,它实现了从网络层到物理层。
3、对于一台交换机来说,它实现了从数据链路层到物理层。
4、对于集线器来说,它实现的物理层。
在这些基本操作中涉及到 封装 和 分用 两个操作流程。
封装的意思就是给基于数据的基础上,在数据前加上协议报头,当数据由应用层传输到传输层时,传输层会在得到的数据基础上加上传输层协议报头。 当数据由传输层到达网络层时,会继续添加网络层协议报头,也就是我们常说的IP地址。当数据由网络层传输到数据链路层时会在数据前加上数据链路层协议报头,也就是常数的mac地址。数据链路层将数据发送给物理层时,物理层就会将这个数据转化成光电信号,通过一些硬件设备,例如网线,光纤,电磁波等传输出去。
分用的过程正好与封装相反,当物理层接收到对方发送过来的光电信号时,会将它解析成二进制的bit流,进一步得到数据链路层数据帧。 数据链路层解析数据帧,玻璃针头和针尾取出,其中的IP数据报交给网络层。网络层接收到刚才的网络层数据报,通过解析去掉网络层的协议报头。把数据交给传输层,传输层拿到传输层,数据报在进行解析,去掉传输层报头,最后将数据交给应用层,这时应用层就可以解析数据,分析出发送者是谁,接收者是谁显示到桌面上。
当发送者通过自己的主机将数据封装完成之后,会通过路由器广域网等发送到服务器。当服务器通过分用把数据拆开之后看到接收方的地址,然后服务器将数据重新封装之后,通过路由器广域网等途径发送到接收者的主机,然后接收者的主机一步一步的进行分佣,最终得到数据并且显示出来。
套接字(socket)是一组API,用来实现网络编程,一般是通过客户端(server)和服务器(cilent)实现的。
IP地址:用来识别互联网上一台主机的位置。
端口号:用来区分是主机上的那个应用。
在一次通信过程中涉及到五元组: 源IP , 目的IP , 源端口 , 目的端口 , 协议类型 。
JAVA中提供了两种风格:
UDP(DatagramSocket面向数据报,发送和接受数据要以数据包为单位)
TCP(ServerSocket面向字节流)
//UDP的服务器
import java.io.IOException;
import java.NET.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class Test01 {
//进行实例化操作
private DatagramSocket socket=null;
//端口号
public Test01(int port) throws SocketException {
socket=new DatagramSocket(port);
}
//开启服务器
public void start() throws IOException {
System.out.println("开启服务器");
while (true){
//1、读取请求并分析
DatagramPacket requstPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requstPacket);
String reques=new String(requstPacket.getData(),0,
requstPacket.getLength()).trim();
//2、请求数据相应
String response=process(reques);
//3、把响应返回给服务器
DatagramPacket responsePacket=new DatagramPacket(response.getBytes()
,response.getBytes().length,requstPacket.getSocketAddress());
socket.send(responsePacket);
//日志
System.out.printf("[%s:%d] req: %s; resp:%s n",requstPacket.getAddress().toString(),
requstPacket.getPort(),reques,response);
}
}
private String process(String reques) {
//我们写最简单的服务器回溯
return reques;
}
public static void main(String[] args) throws IOException {
Test01 test01=new Test01(9090);
test01.start();
}
}
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class Test02 {
// 客户端的主要流程分成四步.
// 1. 从用户这里读取输入的数据.
// 2. 构造请求发送给服务器
// 3. 从服务器读取响应
// 4. 把响应写回给客户端.
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
// 需要在启动客户端的时候来指定需要连接哪个服务器
public Test02(String serverIp, int serverPort) throws SocketException {
this.serverIp = serverIp;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 读取用户输入的数据
System.out.print("-> ");
String request = scanner.nextLine();
if (request.equals("exit")) {
break;
}
// 2. 构造请求发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3. 从服务器读取响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength()).trim();
// 4. 显示响应数据
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
Test02 client = new Test02("127.0.0.1", 9090);
// UdpEchoClient client = new UdpEchoClient("47.98.116.42", 9090);
client.start();
}
}
客户端发送的数据是按行读取(n)
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test05 {
// 1. 初始化服务器
// 2. 进入主循环
// 1) 先去从内核中获取到一个 TCP 的连接
// 2) 处理这个 TCP 的连接
// a) 读取请求并解析
// b) 根据请求计算响应
// c) 把响应写回给客户端
private ServerSocket serverSocket=null;
public Test05(int port) throws IOException {
//绑定端口号
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//创建一个线程池
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
//先从内核中获取到TCP连接
Socket clientSocket=serverSocket.accept();
//处理这个连接
//我们可以在这个地方加上一个线程池
executorService.execute(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
private void processConnection(Socket clientSocket) {
//获取地址和端口
System.out.printf("[%s:%d] 客户端上线 n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
//长连接版本服务器
while(true){
//读取
String request=bufferedReader.readLine();
//处理数据
String response=process(request);
//将这个返回给客户端
bufferedWriter.write(response+"n");
bufferedWriter.flush();
//写一个日志
System.out.printf("[%s:%d] req:%s,resq:%sn",clientSocket.getInetAddress().toString()
,clientSocket.getPort(),request,response);
}
} catch (IOException e) {
//e.printStackTrace();
System.out.printf("[%s:%d] 客户端下线n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
Test05 test05=new Test05(9090);
test05.start();
}
}
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Test04 {
// 1. 启动客户端(一定不要绑定端口号) 和服务器建立连接
// 2. 进入主循环
// a) 读取用户输入内容
// b) 构造一个请求发送给服务器
// c) 读取服务器的响应数据
// d) 把响应数据显示到界面上.
private Socket socket=null;
public Test04(String socketIP,int socketPort) throws IOException {
socket=new Socket(socketIP,socketPort);
}
public void start(){
System.out.println("启动客户端");
Scanner scanner=new Scanner(System.in);
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
while(true){
System.out.print("-> ");
String request=scanner.nextLine();
if(request.equals("exit")){
break;
}
//发送给服务器
bufferedWriter.write(request+"n");
//刷新 因为写入到缓冲区中并没有写入到内核中
bufferedWriter.flush();
//读取服务器中的内容
String response=bufferedReader.readLine();
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
Test04 test04=new Test04("127.0.0.1",9090);
test04.start();
}
}
hppt和hppts都是应用层协议,应用层协议大多都需要手动指定,hppt协议是基于TCP来实现的。
当你打开一个网页的时候,会出现一个网址这个网址就是URL。
url中的服务器的ip来确定是哪一个服务器,
url中的端口号来确定是哪个进程。
url中的path来确定是哪个进程所管理的具体文件。
https://www.baidu.com/s? cl=3 &tn=baidutop10 &fr=top1000 &wd=%E6%95%B0%E8%AF%BB%E5%8D%81%E4%B9%9D%E5%B1%8A%E5%85%AD%E4%B8%AD%E5%85%A8%E4%BC%9A%E7%B2%BE%E7%A5%9E &rsv_idx=2 &rsv_dl=fyb_n_homepage &sa=fyb_n_homepage &hisfilter=1
这种键值对是特殊约定的事物。
什么是抓包?我用一张图来描述
fiddler不会对传输数据进行加工和修改,只会讲数据截取下来让用户看到。
1)首行: 方法(GET) URL 版本号
2)协议头(header):若干个键值对用(:)来分割
3)空行:header的结束标记
4)正文(body):对于get来说为空,对于post来说不为空
1)首行: 版本号 状态码 状态码对应的描述信息
2)协议头(header):若干个键值对用(:)来分割
3)空行:header的结束标记
4)正文(body):常见的格式为html格式,部分可能存在加密的情况
GET一般把数据放到url中去
Post一般把数据放到body中去
cookie可以将登录认证后的用户信息保存在本地浏览器上,当后面每次发送http请求时,都会附带上这个信息,就不需要每次都重新验证用户。
但是这样明文传输cookie可能会泄密在这个前提下我们引入session来保存数据
在服务器登录成功时,我们把用户保存到一个hash表中,同时生成一个key(sessionid),把sessionid写回到这个cookie中去,当后续访问时,只需要在通过sessionid访问服务器中就行了。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class HttpRequset {
private String method;//方法
private String url;//url
private String version;//版本号
//协议头
private Map<String,String> headers=new HashMap<>();
//url中和body中的内容放在这里
private Map<String,String> parameters=new HashMap<>();
//cookie中的类容解析
private Map<String,String> cookies=new HashMap<>();
private String body;//body
//用工厂模式来写
public static HttpRequset build(InputStream inputStream) throws IOException {
HttpRequset requset=new HttpRequset();
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
//1、解析首行
String firstline=bufferedReader.readLine();
String[] fiestlines=firstline.split(" "); //第一行是通过空格来区分的
requset.method=fiestlines[0];
requset.url=fiestlines[1];
requset.version=fiestlines[2];
//2、将首行中的url中的元素提取出来
int pre=requset.url.indexOf("?");//如果没有就返回-1
if(pre!=-1){
String qureyString=requset.url.substring(pre+1);
parseKV(qureyString,requset.parameters);
}
//3、将headers中的内容解析出来
String line="";
while((line=bufferedReader.readLine())!=null && line.getBytes().length!=0){
String[] lines=line.split(": ");
requset.headers.put(lines[0],lines[1]);
}
//4、解析cookie中的内容
String cookie=requset.headers.get("Cookie");
if(cookie!=null){
parseCookie(cookie,requset.cookies);
}
//5、body中的内容
if("POST".equalsIgnoreCase(requset.method) || "PUT".equalsIgnoreCase(requset.method)){
//我们首先要知道body的长度
int length=Integer.parseInt(requset.headers.get("Content-Length"));
//建立一个缓冲区
char[] buffer=new char[length];
int len=bufferedReader.read(buffer);
requset.body=new String(buffer,0,len);
parseKV(requset.body,requset.parameters);
}
return requset;
}
private static void parseCookie(String cookie, Map<String, String> cookies) {
String[] line=cookie.split("; ");
for(String s:line){
String[] lines=s.split("=");
cookies.put(lines[0],lines[1]);
}
}
private static void parseKV(String qureyString, Map<String, String> parameters) {
String[] line=qureyString.split("&");
for (String s:line){
String[] lines=s.split("=");
parameters.put(lines[0],lines[1]);
}
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getVersion() {
return version;
}
public String getBody() {
return body;
}
public String getHeaders(String key){
return headers.get(key);
}
public String getParameters(String key){
return parameters.get(key);
}
public String getCooies(String key){
return cookies.get(key);
}
}
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;
public class HttpResponse {
private String version="HTTP/1.1";//版本号
private int status;//验证信息
private String message;//状态信息
//header中的内容
private Map<String,String> headers=new HashMap<>();
//body中的内容
private StringBuffer body=new StringBuffer();
private OutputStream outputStream=null;
//工厂模式
public static HttpResponse build(OutputStream outputStream){
HttpResponse response=new HttpResponse();
response.outputStream=outputStream;
return response;
}
public void setVersion(String version) {
this.version = version;
}
public void setStatus(int status) {
this.status = status;
}
public void setMessage(String message) {
this.message = message;
}
public void setHeaders(String key,String value){
this.headers.put(key,value);
}
public void setBody(String val){
this.body.Append(val);
}
public void fush() throws IOException {
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(outputStream));
//写第一行
bufferedWriter.write(version+" "+status+" "+message+"n");
//将文件信息放入header中写入
headers.put("Content-Length",body.toString().getBytes().length+"");
//将header中的类容写入
for(Map.Entry<String,String> entry:headers.entrySet()){
bufferedWriter.write(entry.getKey()+": "+entry.getValue()+"n");
}
//
bufferedWriter.write("n");
//将body中的数据放入
bufferedWriter.write(body.toString());
//将数据刷新
bufferedWriter.flush();
}
}
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<form method="post" action="/login">
<div style="margin-bottom: 5px">
<input type="text" name="username" placeholder="请输入姓名">
</div>
<div style="margin-bottom: 5px">
<input type="text" name="password" placeholder="请输入密码">
</div>
<div>
<input type="submit" value="登录">
</div>
</form>
</body>
</html>
import Test05.HttpServerV3;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpServerV4 {
static class User{
//保存的相关信息
public String userName;
public int age;
public String school;
}
private ServerSocket socket=null;
//我们通过session会话
private Map<String,User> session=new HashMap<>();
public HttpServerV4(int port) throws IOException {
this.socket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("系统启动");
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
Socket cliensocket=socket.accept();
executorService.execute(new Runnable() {
@Override
public void run() {
process(cliensocket);
}
});
}
}
private void process(Socket cliensocket) {
try {
//读取请求
HttpResponse response=HttpResponse.build(cliensocket.getOutputStream());
HttpRequset requset=HttpRequset.build(cliensocket.getInputStream());
//response.setHeaders("Set-Cookie","YYX=qwe");
//计算相应
if("GET".equalsIgnoreCase(requset.getMethod())){
doGet(requset,response);
}else if("Post".equalsIgnoreCase(requset.getMethod())){
doPost(requset,response);
}else{
response.setStatus(405);
response.setMessage("Method Not Allowed");
}
//写入
response.fush();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
cliensocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void doPost(HttpRequset requset, HttpResponse response) {
//2
if(requset.getUrl().startsWith("/login")){
String user=requset.getParameters("username");
String pass=requset.getParameters("password");
/*System.out.println(user+":"+pass);*/
if("yyx".equals(user) && "2001".equals(pass)){
//通过
response.setStatus(403);
response.setMessage("Forbidden");
//要写不然返回的不是utf-8的编码格式
response.setHeaders("Content-Type","text/html; charset=utf-8");
//response.setHeaders("Set-Cookie","userName=YYX");
String sessionId= UUID.randomUUID().toString();
User user01=new User();
user01.userName="杨亚轩";
user01.age=18;
user01.school="武昌工学院";
session.put(sessionId,user01);
response.setHeaders("Set-Cookie","sessionId="+sessionId);
response.setBody("<html>");
response.setBody("验证成功!");
response.setBody("</html>");
}else {
//没有通过
response.setStatus(403);
response.setMessage("Forbidden");
//要写不然返回的不是utf-8的编码格式
response.setHeaders("Content-Type","text/html; charset=utf-8");
response.setBody("<html>");
response.setBody("验证失败!");
response.setBody("</html>");
}
}
}
private void doGet(HttpRequset requset, HttpResponse response) throws IOException {
//1
if(requset.getUrl().startsWith("/index.html")){
String sessionId=requset.getCooies("sessionId");
User user=session.get(sessionId);
if (sessionId==null || user==null) {
//当前情况下用户没有登录
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("Content-Type","text/html; charset=utf-8");
InputStream inputStream=HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
String str="";
while((str=bufferedReader.readLine())!=null){
response.setBody(str+"n");
}
bufferedReader.close();
}else{
//已经登录就不需要再登录的
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("Content-Type","text/html; charset=utf-8");
response.setBody("<html>");
response.setBody("该用户已经登录了");
response.setBody("</html>");
}
}
}
public static void main(String[] args) throws IOException {
HttpServerV4 httpServerV4=new HttpServerV4(9090);
httpServerV4.start();
}
}
对于UDP来说它有三个非常重要的特性
1)无连接(不需要建立连接就可以通信)
2)不可靠(接收方是否接受到消息发送方不知道)
3)面向数据报(以DatagramPacket为单位进行传输)
对于TCP来说它有三个非常重要的特性
1)有连接(需要建立连接就可以通信)
2)可靠(接收方是否接受到消息发送方知道)
3)面向字节流
TCP的报头是变长(报头长度在4的基础上*4)
当主机a向主机B发送数据的时候,主机B会向主机a发送一种应答报文ack,当主机a接收到由主机B发来的ack时,那么就能确定主机a发送过去的数据被主机B接收。
当主机a发送的数据为1到100时,那么主机B返回的确认应答为101,接下来数据就要从101开始发送。
确认应答是比较理想的情况,数据在传输过程中有很大的概率丢包。
当出现丢包状况时有两种可能,第1种就是主机a发送的请求丢失,第2种情况就是主机B发送的ack丢失,当发送一条数据后,TCP内部就会自动生成一个定时器,当一定的时间没有收到ack,那么就会触发定时器触发重传。如果是ack丢了,那么TCP会在内部进行数据去重。
三次握手涉及到SYN同步报文段当主机a发送SYN同步报文段,尝试与主机B发生连接,当主机B收到,来自主机a的SYN后,会向主机a发送SYN和ACK。当主机A收到,来自主机B的数据后,会向主机B发送一个ACK。
注意:
如果三次握手仅只握两次,那么有一方接收不到信号就不能正常工作。如果三次握手握手四次,可以但是会降低传输效率。
几个重要的状态:
LISTEN:表示服务器已经准备好了,可以接受客户端的消息了。
SYN-SEND/SYN-RCVD: 建立连接的中间过程,如果建立连接顺利这两个状态一瞬间就会消失。
ESTABLISHED:连接建立完成验证的双方都有发送和接收能力,这时就可以开始传输正文了。
四次挥手主机A向主机B发送FIN,主机B收到由主机A发来的FIN后就会立即发送ACK,当应用程序代码处理完积压的数据后,主机B才会发送FIN给主机A,主机A接收到由主机B发来的信息返回ACK。
当出现延时应答时,四次挥手就会变成三次挥手。
CLOSE_WAIT:四次挥手挥到一半就不挥了,主要原因是因为接受方没有调用close方法,会导致4次挥手只回了两次而没有正确的关闭连接。
TIME_WAIT:谁主动断开连接,谁就会进入这个状态。这时主机已经完成了4次挥手的过程,但是不能立即释放连接要等待一段时间之后。再彻底释放连接。
滑动窗口的本质是节省了时间,当窗口为n时,n份数据传输的时间变成了一份时间,n份应答的时间也重叠成一份时间。
当滑动窗口出现丢包问题时间:
1)ACK丢了
出现这种情况不需要进行处理,当接收方接受到6001时,就会自动默认前面的数据发送过来,这时窗口直接滑动。
2)数据丢失
这种情况下,接下来发送的ACK都会是丢失的那个数据的开始,当收到三个后开始重传,成功后ACK就变成相应的,窗口向后滑动。
窗口不能无线大,传输速率过快会让接收方处理不来,当处理方没有空间时,就会停止发送,这时主机a就会发送一个窗口探测,通过主机B的不断更新,从而得知主机a是否继续发送数据。
流量控制本质上就是根据接收方的处理能力来反向横置发送能力。根据接受缓冲区的剩余空间大小,来约制发送方的滑动窗口大小。
可以根据网络状态进行及时的调整。
ACK和Resp本来不能同时相应,但是由于捎带应答就可以把两个操作合并。
当数据进行读取的时候,读取的内容可能和想象的内容有差异,面向字节流传输都会出现这个问题。
解决方法:
我们通过应用层协议本身来区分出包和包之间的边界。(使用分割符、明确包的长度)
1、进程崩溃时,TCP连接就会进行正常的四次挥手。
2、主机关机时,关机会强制杀死进程,杀死进程就是四次挥手。
3、主机断电:
a)接收方断电,对端尝试发送消息ACK没有接受(超时重传,到达一定次数重置连接,放弃连接)
b)发送方断电,对端尝试接受消息(在空闲时间会传输心跳包,当心跳包没有相应就会自动放弃)
1、TCP适用于要求可靠性的场景。(外网通信网络环境复杂的地方,udp丢包概率大,可以考虑TCP)。
2、Udp适用于对于可靠性要求没有那么高,但是需要很高的传输效率的场景,如机房等地方。
3、Udp能够实现广播,但是TCP只能1对1进行传输。
4、对于游戏这种领域,udp和TCP都不能传输。、
网络层解决两个问题:(地址管理和路由选择)
报头长度是在改变的。(不需要手动分包)
4位版本号(version): 指定IP协议的版本, 对于IPv4来说, 就是4.
4位头部长度(header length): IP头部的长度是多少个32bit, 也就是 length * 4 的字节数. 4bit表示最大的数字是15, 因此IP头部最大长度是60字节.
8位服务类型(Type Of Service): 3位优先权字段(已经弃用), 4位TOS字段, 和1位保留字段(必须置为0). 4位TOS分别表示: 最小延时, 最大吞吐量, 最高可靠性, 最小成本 . 这四者相互冲突, 只能选择一个. 对于ssh/telnet这样的应用程序, 最小延时比较重要; 对于ftp这样的程序, 最大吞吐量比较重要.
16位总长度(total length): IP数据报整体占多少个字节.
16位标识(id): 唯一的标识主机发送的报文. 如果IP报文在数据链路层被分片了, 那么每一个片里面的这个id都是相同的.
3位标志字段: 第一位保留(保留的意思是现在不用, 但是还没想好说不定以后要用到). 第二位置为1表示禁止分片, 这时候如果报文长度超过MTU, IP模块就会丢弃报文. 第三位表示"更多分片", 如果分片了的话, 最后一个分片置为1, 其他是0. 类似于一个结束标记.
13位分片偏移(framegament offset): 是分片相对于原始IP报文开始处的偏移. 其实就是在表示当前分片在原报文中处在哪个位置. 实际偏移的字节数是这个值 * 8 得到的. 因此, 除了最后一个报文之外, 其他报文的长度必须是8的整数倍(否则报文就不连续了).
8位生存时间(Time To Live, TTL): 数据报到达目的地的最大报文跳数. 一般是64. 每次经过一个路由, TTL -=1, 一直减到0还没到达, 那么就丢弃了. 这个字段主要是用来防止出现路由循环
8位协议: 表示上层协议的类型
16位头部校验和: 使用CRC进行校验, 来鉴别头部是否损坏.
32位源地址和32位目标地址: 表示发送端和接收端
地址管理就是给每个主机一个单独的身份标识。
我们有两种方法:
1)动态分配IP地址:联网就分配不联网就不分配。
2)NAT机制,网络转换机制,允许局域网中的IP地址可以重复,使用一个外网IP来代表同一批局域网内部设备,一般由路由器负责。
网段划分就是将一个IP地址划分成 网络号和主机号 两个部分。
1、同一个局域网内部的设备,网络号相同主机号不同
2、两个相邻的局域网,网络号不能相同。
例如:
IPV4地址:192.168.0.16 子网掩码:255.255.255.0
网络号:192.168.0.0
相邻两个局域网之间的网络号是不一样的
主机号为.1的叫做网关,也就是当前局域网的路由器
当一个包从我的电脑出发,他会首先查看我的电脑认不认识这个目的IP,如果不认识就会发给我电脑所连接的路由器,有我电脑连接的路由器,认不认识这个目的IP如果认识就会传输过去,如果不认识再把数据交给光猫,如果光猫也不认识,那么就会继续交给上级路由器。
路由表:(和上面所说的差不多)
当有一个IP数据达到路由器的时候,路由器就会检查,看目的IP所属的网站在路由表中有哪些对应的选项,如果没有就会走默认选项。
目的IP & 子网掩码再路由表中查找。
数据链路层负责两个相邻设备之间的传输
面试常见:
已经有IP的情况下为什么还有MAC地址?
1目录
4、TCP网络传输的基本流程
二、网络编程套接字(socket)
5、cookie和session的用法
6、基本实现http协议的代码
四、传输层协议TCP和UDP
4、TCP和UDP之间的对比
六、数据链路层和应用层(以太网协议和DNS协议)
计算机网络是通过 传输介质、通信设施和网络通信协议 ,把分散在不同地点的计算机设备互连起来的,实现资源共享和数据传输的系统。网络编程就是编写程序使互联网的两个(或多个)设备(如计算机)之间进行数据传输。Java语言对网络编程提供了良好的支持。通过其提供的接口我们可以很方便地进行网络编程。
网络通信协议是网络编程中的关键,通信双方达成的一种共识,当通信双方都遵守这样的约定,才能正确传输信息。
1、分层可以避免一个协议太过于庞大和复杂。
2、分层之后协议之间 解耦合 ,上层协议不需要理解下层协议的细节
3、任意层次的协议的可以进行灵活的替换
在这里我们对于TCP/IP模型进行详细的描述:
面试常见:
1、对于一台主机来说,它的操作系统内核实现了从应用层到物理层
2、对于一台路由器来说,它实现了从网络层到物理层。
3、对于一台交换机来说,它实现了从数据链路层到物理层。
4、对于集线器来说,它实现的物理层。
在这些基本操作中涉及到 封装 和 分用 两个操作流程。
封装的意思就是给基于数据的基础上,在数据前加上协议报头,当数据由应用层传输到传输层时,传输层会在得到的数据基础上加上传输层协议报头。 当数据由传输层到达网络层时,会继续添加网络层协议报头,也就是我们常说的IP地址。当数据由网络层传输到数据链路层时会在数据前加上数据链路层协议报头,也就是常数的MAC地址。数据链路层将数据发送给物理层时,物理层就会将这个数据转化成光电信号,通过一些硬件设备,例如网线,光纤,电磁波等传输出去。
分用的过程正好与封装相反,当物理层接收到对方发送过来的光电信号时,会将它解析成二进制的bit流,进一步得到数据链路层数据帧。 数据链路层解析数据帧,玻璃针头和针尾取出,其中的IP数据报交给网络层。网络层接收到刚才的网络层数据报,通过解析去掉网络层的协议报头。把数据交给传输层,传输层拿到传输层,数据报在进行解析,去掉传输层报头,最后将数据交给应用层,这时应用层就可以解析数据,分析出发送者是谁,接收者是谁显示到桌面上。
当发送者通过自己的主机将数据封装完成之后,会通过路由器广域网等发送到服务器。当服务器通过分用把数据拆开之后看到接收方的地址,然后服务器将数据重新封装之后,通过路由器广域网等途径发送到接收者的主机,然后接收者的主机一步一步的进行分佣,最终得到数据并且显示出来。
套接字(socket)是一组API,用来实现网络编程,一般是通过客户端(server)和服务器(cilent)实现的。
IP地址:用来识别互联网上一台主机的位置。
端口号:用来区分是主机上的那个应用。
在一次通信过程中涉及到五元组: 源IP , 目的IP , 源端口 , 目的端口 , 协议类型 。
JAVA中提供了两种风格:
UDP(DatagramSocket面向数据报,发送和接受数据要以数据包为单位)
TCP(ServerSocket面向字节流)
//UDP的服务器
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class Test01 {
//进行实例化操作
private DatagramSocket socket=null;
//端口号
public Test01(int port) throws SocketException {
socket=new DatagramSocket(port);
}
//开启服务器
public void start() throws IOException {
System.out.println("开启服务器");
while (true){
//1、读取请求并分析
DatagramPacket requstPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requstPacket);
String reques=new String(requstPacket.getData(),0,
requstPacket.getLength()).trim();
//2、请求数据相应
String response=process(reques);
//3、把响应返回给服务器
DatagramPacket responsePacket=new DatagramPacket(response.getBytes()
,response.getBytes().length,requstPacket.getSocketAddress());
socket.send(responsePacket);
//日志
System.out.printf("[%s:%d] req: %s; resp:%s n",requstPacket.getAddress().toString(),
requstPacket.getPort(),reques,response);
}
}
private String process(String reques) {
//我们写最简单的服务器回溯
return reques;
}
public static void main(String[] args) throws IOException {
Test01 test01=new Test01(9090);
test01.start();
}
}
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class Test02 {
// 客户端的主要流程分成四步.
// 1. 从用户这里读取输入的数据.
// 2. 构造请求发送给服务器
// 3. 从服务器读取响应
// 4. 把响应写回给客户端.
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
// 需要在启动客户端的时候来指定需要连接哪个服务器
public Test02(String serverIp, int serverPort) throws SocketException {
this.serverIp = serverIp;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 读取用户输入的数据
System.out.print("-> ");
String request = scanner.nextLine();
if (request.equals("exit")) {
break;
}
// 2. 构造请求发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3. 从服务器读取响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength()).trim();
// 4. 显示响应数据
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
Test02 client = new Test02("127.0.0.1", 9090);
// UdpEchoClient client = new UdpEchoClient("47.98.116.42", 9090);
client.start();
}
}
客户端发送的数据是按行读取(n)
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test05 {
// 1. 初始化服务器
// 2. 进入主循环
// 1) 先去从内核中获取到一个 TCP 的连接
// 2) 处理这个 TCP 的连接
// a) 读取请求并解析
// b) 根据请求计算响应
// c) 把响应写回给客户端
private ServerSocket serverSocket=null;
public Test05(int port) throws IOException {
//绑定端口号
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//创建一个线程池
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
//先从内核中获取到TCP连接
Socket clientSocket=serverSocket.accept();
//处理这个连接
//我们可以在这个地方加上一个线程池
executorService.execute(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
private void processConnection(Socket clientSocket) {
//获取地址和端口
System.out.printf("[%s:%d] 客户端上线 n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
//长连接版本服务器
while(true){
//读取
String request=bufferedReader.readLine();
//处理数据
String response=process(request);
//将这个返回给客户端
bufferedWriter.write(response+"n");
bufferedWriter.flush();
//写一个日志
System.out.printf("[%s:%d] req:%s,resq:%sn",clientSocket.getInetAddress().toString()
,clientSocket.getPort(),request,response);
}
} catch (IOException e) {
//e.printStackTrace();
System.out.printf("[%s:%d] 客户端下线n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
Test05 test05=new Test05(9090);
test05.start();
}
}
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Test04 {
// 1. 启动客户端(一定不要绑定端口号) 和服务器建立连接
// 2. 进入主循环
// a) 读取用户输入内容
// b) 构造一个请求发送给服务器
// c) 读取服务器的响应数据
// d) 把响应数据显示到界面上.
private Socket socket=null;
public Test04(String socketIP,int socketPort) throws IOException {
socket=new Socket(socketIP,socketPort);
}
public void start(){
System.out.println("启动客户端");
Scanner scanner=new Scanner(System.in);
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
while(true){
System.out.print("-> ");
String request=scanner.nextLine();
if(request.equals("exit")){
break;
}
//发送给服务器
bufferedWriter.write(request+"n");
//刷新 因为写入到缓冲区中并没有写入到内核中
bufferedWriter.flush();
//读取服务器中的内容
String response=bufferedReader.readLine();
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
Test04 test04=new Test04("127.0.0.1",9090);
test04.start();
}
}
hppt和hppts都是应用层协议,应用层协议大多都需要手动指定,hppt协议是基于TCP来实现的。
当你打开一个网页的时候,会出现一个网址这个网址就是URL。
url中的服务器的ip来确定是哪一个服务器,
url中的端口号来确定是哪个进程。
url中的path来确定是哪个进程所管理的具体文件。
https://www.baidu.com/s? cl=3 &tn=baidutop10 &fr=top1000 &wd=%E6%95%B0%E8%AF%BB%E5%8D%81%E4%B9%9D%E5%B1%8A%E5%85%AD%E4%B8%AD%E5%85%A8%E4%BC%9A%E7%B2%BE%E7%A5%9E &rsv_idx=2 &rsv_dl=fyb_n_homepage &sa=fyb_n_homepage &hisfilter=1
这种键值对是特殊约定的事物。
什么是抓包?我用一张图来描述
fiddler不会对传输数据进行加工和修改,只会讲数据截取下来让用户看到。
1)首行: 方法(GET) URL 版本号
2)协议头(header):若干个键值对用(:)来分割
3)空行:header的结束标记
4)正文(body):对于get来说为空,对于post来说不为空
1)首行: 版本号 状态码 状态码对应的描述信息
2)协议头(header):若干个键值对用(:)来分割
3)空行:header的结束标记
4)正文(body):常见的格式为html格式,部分可能存在加密的情况
GET一般把数据放到url中去
Post一般把数据放到body中去
cookie可以将登录认证后的用户信息保存在本地浏览器上,当后面每次发送http请求时,都会附带上这个信息,就不需要每次都重新验证用户。
但是这样明文传输cookie可能会泄密在这个前提下我们引入session来保存数据
在服务器登录成功时,我们把用户保存到一个hash表中,同时生成一个key(sessionid),把sessionid写回到这个cookie中去,当后续访问时,只需要在通过sessionid访问服务器中就行了。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class HttpRequset {
private String method;//方法
private String url;//url
private String version;//版本号
//协议头
private Map<String,String> headers=new HashMap<>();
//url中和body中的内容放在这里
private Map<String,String> parameters=new HashMap<>();
//cookie中的类容解析
private Map<String,String> cookies=new HashMap<>();
private String body;//body
//用工厂模式来写
public static HttpRequset build(InputStream inputStream) throws IOException {
HttpRequset requset=new HttpRequset();
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
//1、解析首行
String firstline=bufferedReader.readLine();
String[] fiestlines=firstline.split(" "); //第一行是通过空格来区分的
requset.method=fiestlines[0];
requset.url=fiestlines[1];
requset.version=fiestlines[2];
//2、将首行中的url中的元素提取出来
int pre=requset.url.indexOf("?");//如果没有就返回-1
if(pre!=-1){
String qureyString=requset.url.substring(pre+1);
parseKV(qureyString,requset.parameters);
}
//3、将headers中的内容解析出来
String line="";
while((line=bufferedReader.readLine())!=null && line.getBytes().length!=0){
String[] lines=line.split(": ");
requset.headers.put(lines[0],lines[1]);
}
//4、解析cookie中的内容
String cookie=requset.headers.get("Cookie");
if(cookie!=null){
parseCookie(cookie,requset.cookies);
}
//5、body中的内容
if("POST".equalsIgnoreCase(requset.method) || "PUT".equalsIgnoreCase(requset.method)){
//我们首先要知道body的长度
int length=Integer.parseInt(requset.headers.get("Content-Length"));
//建立一个缓冲区
char[] buffer=new char[length];
int len=bufferedReader.read(buffer);
requset.body=new String(buffer,0,len);
parseKV(requset.body,requset.parameters);
}
return requset;
}
private static void parseCookie(String cookie, Map<String, String> cookies) {
String[] line=cookie.split("; ");
for(String s:line){
String[] lines=s.split("=");
cookies.put(lines[0],lines[1]);
}
}
private static void parseKV(String qureyString, Map<String, String> parameters) {
String[] line=qureyString.split("&");
for (String s:line){
String[] lines=s.split("=");
parameters.put(lines[0],lines[1]);
}
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getVersion() {
return version;
}
public String getBody() {
return body;
}
public String getHeaders(String key){
return headers.get(key);
}
public String getParameters(String key){
return parameters.get(key);
}
public String getCooies(String key){
return cookies.get(key);
}
}
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;
public class HttpResponse {
private String version="HTTP/1.1";//版本号
private int status;//验证信息
private String message;//状态信息
//header中的内容
private Map<String,String> headers=new HashMap<>();
//body中的内容
private StringBuffer body=new StringBuffer();
private OutputStream outputStream=null;
//工厂模式
public static HttpResponse build(OutputStream outputStream){
HttpResponse response=new HttpResponse();
response.outputStream=outputStream;
return response;
}
public void setVersion(String version) {
this.version = version;
}
public void setStatus(int status) {
this.status = status;
}
public void setMessage(String message) {
this.message = message;
}
public void setHeaders(String key,String value){
this.headers.put(key,value);
}
public void setBody(String val){
this.body.append(val);
}
public void fush() throws IOException {
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(outputStream));
//写第一行
bufferedWriter.write(version+" "+status+" "+message+"n");
//将文件信息放入header中写入
headers.put("Content-Length",body.toString().getBytes().length+"");
//将header中的类容写入
for(Map.Entry<String,String> entry:headers.entrySet()){
bufferedWriter.write(entry.getKey()+": "+entry.getValue()+"n");
}
//
bufferedWriter.write("n");
//将body中的数据放入
bufferedWriter.write(body.toString());
//将数据刷新
bufferedWriter.flush();
}
}
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<form method="post" action="/login">
<div style="margin-bottom: 5px">
<input type="text" name="username" placeholder="请输入姓名">
</div>
<div style="margin-bottom: 5px">
<input type="text" name="password" placeholder="请输入密码">
</div>
<div>
<input type="submit" value="登录">
</div>
</form>
</body>
</html>
import Test05.HttpServerV3;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpServerV4 {
static class User{
//保存的相关信息
public String userName;
public int age;
public String school;
}
private ServerSocket socket=null;
//我们通过session会话
private Map<String,User> session=new HashMap<>();
public HttpServerV4(int port) throws IOException {
this.socket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("系统启动");
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
Socket cliensocket=socket.accept();
executorService.execute(new Runnable() {
@Override
public void run() {
process(cliensocket);
}
});
}
}
private void process(Socket cliensocket) {
try {
//读取请求
HttpResponse response=HttpResponse.build(cliensocket.getOutputStream());
HttpRequset requset=HttpRequset.build(cliensocket.getInputStream());
//response.setHeaders("Set-Cookie","YYX=qwe");
//计算相应
if("GET".equalsIgnoreCase(requset.getMethod())){
doGet(requset,response);
}else if("Post".equalsIgnoreCase(requset.getMethod())){
doPost(requset,response);
}else{
response.setStatus(405);
response.setMessage("Method Not Allowed");
}
//写入
response.fush();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
cliensocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void doPost(HttpRequset requset, HttpResponse response) {
//2
if(requset.getUrl().startsWith("/login")){
String user=requset.getParameters("username");
String pass=requset.getParameters("password");
/*System.out.println(user+":"+pass);*/
if("yyx".equals(user) && "2001".equals(pass)){
//通过
response.setStatus(403);
response.setMessage("Forbidden");
//要写不然返回的不是utf-8的编码格式
response.setHeaders("Content-Type","text/html; charset=utf-8");
//response.setHeaders("Set-Cookie","userName=YYX");
String sessionId= UUID.randomUUID().toString();
User user01=new User();
user01.userName="杨亚轩";
user01.age=18;
user01.school="武昌工学院";
session.put(sessionId,user01);
response.setHeaders("Set-Cookie","sessionId="+sessionId);
response.setBody("<html>");
response.setBody("验证成功!");
response.setBody("</html>");
}else {
//没有通过
response.setStatus(403);
response.setMessage("Forbidden");
//要写不然返回的不是utf-8的编码格式
response.setHeaders("Content-Type","text/html; charset=utf-8");
response.setBody("<html>");
response.setBody("验证失败!");
response.setBody("</html>");
}
}
}
private void doGet(HttpRequset requset, HttpResponse response) throws IOException {
//1
if(requset.getUrl().startsWith("/index.html")){
String sessionId=requset.getCooies("sessionId");
User user=session.get(sessionId);
if (sessionId==null || user==null) {
//当前情况下用户没有登录
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("Content-Type","text/html; charset=utf-8");
InputStream inputStream=HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
String str="";
while((str=bufferedReader.readLine())!=null){
response.setBody(str+"n");
}
bufferedReader.close();
}else{
//已经登录就不需要再登录的
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("Content-Type","text/html; charset=utf-8");
response.setBody("<html>");
response.setBody("该用户已经登录了");
response.setBody("</html>");
}
}
}
public static void main(String[] args) throws IOException {
HttpServerV4 httpServerV4=new HttpServerV4(9090);
httpServerV4.start();
}
}
对于UDP来说它有三个非常重要的特性
1)无连接(不需要建立连接就可以通信)
2)不可靠(接收方是否接受到消息发送方不知道)
3)面向数据报(以DatagramPacket为单位进行传输)
对于TCP来说它有三个非常重要的特性
1)有连接(需要建立连接就可以通信)
2)可靠(接收方是否接受到消息发送方知道)
3)面向字节流
TCP的报头是变长(报头长度在4的基础上*4)
当主机a向主机B发送数据的时候,主机B会向主机a发送一种应答报文ack,当主机a接收到由主机B发来的ack时,那么就能确定主机a发送过去的数据被主机B接收。
当主机a发送的数据为1到100时,那么主机B返回的确认应答为101,接下来数据就要从101开始发送。
确认应答是比较理想的情况,数据在传输过程中有很大的概率丢包。
当出现丢包状况时有两种可能,第1种就是主机a发送的请求丢失,第2种情况就是主机B发送的ack丢失,当发送一条数据后,TCP内部就会自动生成一个定时器,当一定的时间没有收到ack,那么就会触发定时器触发重传。如果是ack丢了,那么TCP会在内部进行数据去重。
三次握手涉及到SYN同步报文段当主机a发送SYN同步报文段,尝试与主机B发生连接,当主机B收到,来自主机a的SYN后,会向主机a发送SYN和ACK。当主机A收到,来自主机B的数据后,会向主机B发送一个ACK。
注意:
如果三次握手仅只握两次,那么有一方接收不到信号就不能正常工作。如果三次握手握手四次,可以但是会降低传输效率。
几个重要的状态:
LISTEN:表示服务器已经准备好了,可以接受客户端的消息了。
SYN-SEND/SYN-RCVD: 建立连接的中间过程,如果建立连接顺利这两个状态一瞬间就会消失。
ESTABLISHED:连接建立完成验证的双方都有发送和接收能力,这时就可以开始传输正文了。
四次挥手主机A向主机B发送FIN,主机B收到由主机A发来的FIN后就会立即发送ACK,当应用程序代码处理完积压的数据后,主机B才会发送FIN给主机A,主机A接收到由主机B发来的信息返回ACK。
当出现延时应答时,四次挥手就会变成三次挥手。
CLOSE_WAIT:四次挥手挥到一半就不挥了,主要原因是因为接受方没有调用close方法,会导致4次挥手只回了两次而没有正确的关闭连接。
TIME_WAIT:谁主动断开连接,谁就会进入这个状态。这时主机已经完成了4次挥手的过程,但是不能立即释放连接要等待一段时间之后。再彻底释放连接。
滑动窗口的本质是节省了时间,当窗口为n时,n份数据传输的时间变成了一份时间,n份应答的时间也重叠成一份时间。
当滑动窗口出现丢包问题时间:
1)ACK丢了
出现这种情况不需要进行处理,当接收方接受到6001时,就会自动默认前面的数据发送过来,这时窗口直接滑动。
2)数据丢失
这种情况下,接下来发送的ACK都会是丢失的那个数据的开始,当收到三个后开始重传,成功后ACK就变成相应的,窗口向后滑动。
窗口不能无线大,传输速率过快会让接收方处理不来,当处理方没有空间时,就会停止发送,这时主机a就会发送一个窗口探测,通过主机B的不断更新,从而得知主机a是否继续发送数据。
流量控制本质上就是根据接收方的处理能力来反向横置发送能力。根据接受缓冲区的剩余空间大小,来约制发送方的滑动窗口大小。
可以根据网络状态进行及时的调整。
ACK和Resp本来不能同时相应,但是由于捎带应答就可以把两个操作合并。
当数据进行读取的时候,读取的内容可能和想象的内容有差异,面向字节流传输都会出现这个问题。
解决方法:
我们通过应用层协议本身来区分出包和包之间的边界。(使用分割符、明确包的长度)
1、进程崩溃时,TCP连接就会进行正常的四次挥手。
2、主机关机时,关机会强制杀死进程,杀死进程就是四次挥手。
3、主机断电:
a)接收方断电,对端尝试发送消息ACK没有接受(超时重传,到达一定次数重置连接,放弃连接)
b)发送方断电,对端尝试接受消息(在空闲时间会传输心跳包,当心跳包没有相应就会自动放弃)
1、TCP适用于要求可靠性的场景。(外网通信网络环境复杂的地方,udp丢包概率大,可以考虑TCP)。
2、Udp适用于对于可靠性要求没有那么高,但是需要很高的传输效率的场景,如机房等地方。
3、Udp能够实现广播,但是TCP只能1对1进行传输。
4、对于游戏这种领域,udp和TCP都不能传输。、
网络层解决两个问题:(地址管理和路由选择)
报头长度是在改变的。(不需要手动分包)
4位版本号(version): 指定IP协议的版本, 对于IPv4来说, 就是4.
4位头部长度(header length): IP头部的长度是多少个32bit, 也就是 length * 4 的字节数. 4bit表示最大的数字是15, 因此IP头部最大长度是60字节.
8位服务类型(Type Of Service): 3位优先权字段(已经弃用), 4位TOS字段, 和1位保留字段(必须置为0). 4位TOS分别表示: 最小延时, 最大吞吐量, 最高可靠性, 最小成本 . 这四者相互冲突, 只能选择一个. 对于ssh/telnet这样的应用程序, 最小延时比较重要; 对于ftp这样的程序, 最大吞吐量比较重要.
16位总长度(total length): IP数据报整体占多少个字节.
16位标识(id): 唯一的标识主机发送的报文. 如果IP报文在数据链路层被分片了, 那么每一个片里面的这个id都是相同的.
3位标志字段: 第一位保留(保留的意思是现在不用, 但是还没想好说不定以后要用到). 第二位置为1表示禁止分片, 这时候如果报文长度超过MTU, IP模块就会丢弃报文. 第三位表示"更多分片", 如果分片了的话, 最后一个分片置为1, 其他是0. 类似于一个结束标记.
13位分片偏移(framegament offset): 是分片相对于原始IP报文开始处的偏移. 其实就是在表示当前分片在原报文中处在哪个位置. 实际偏移的字节数是这个值 * 8 得到的. 因此, 除了最后一个报文之外, 其他报文的长度必须是8的整数倍(否则报文就不连续了).
8位生存时间(Time To Live, TTL): 数据报到达目的地的最大报文跳数. 一般是64. 每次经过一个路由, TTL -=1, 一直减到0还没到达, 那么就丢弃了. 这个字段主要是用来防止出现路由循环
8位协议: 表示上层协议的类型
16位头部校验和: 使用CRC进行校验, 来鉴别头部是否损坏.
32位源地址和32位目标地址: 表示发送端和接收端
地址管理就是给每个主机一个单独的身份标识。
我们有两种方法:
1)动态分配IP地址:联网就分配不联网就不分配。
2)NAT机制,网络转换机制,允许局域网中的IP地址可以重复,使用一个外网IP来代表同一批局域网内部设备,一般由路由器负责。
网段划分就是将一个IP地址划分成 网络号和主机号 两个部分。
1、同一个局域网内部的设备,网络号相同主机号不同
2、两个相邻的局域网,网络号不能相同。
例如:
IPV4地址:192.168.0.16 子网掩码:255.255.255.0
网络号:192.168.0.0
相邻两个局域网之间的网络号是不一样的
主机号为.1的叫做网关,也就是当前局域网的路由器
当一个包从我的电脑出发,他会首先查看我的电脑认不认识这个目的IP,如果不认识就会发给我电脑所连接的路由器,有我电脑连接的路由器,认不认识这个目的IP如果认识就会传输过去,如果不认识再把数据交给光猫,如果光猫也不认识,那么就会继续交给上级路由器。
路由表:(和上面所说的差不多)
当有一个IP数据达到路由器的时候,路由器就会检查,看目的IP所属的网站在路由表中有哪些对应的选项,如果没有就会走默认选项。
目的IP & 子网掩码再路由表中查找。
数据链路层负责两个相邻设备之间的传输
面试常见:
已经有IP的情况下为什么还有MAC地址?
1、源mac会随着设备的不同而改变而源IP不会变
2、mac地址和IP地址是独立发明从来的
DNS协议是一套系统(由于IP地址不好记,DNS系统会将域名自动翻译成IP地址)
浏览器的请求可能直接达到cdn服务器就直接返回了。当cdn服务器上没有时,就会在反向代理服务器缓存中查找,如果反向代理服务器中也没有,就需要访问百度的应用服务器。、源mac会随着设备的不同而改变而源IP不会变
2、mac地址和IP地址是独立发明从来的
DNS协议是一套系统(由于IP地址不好记,DNS系统会将域名自动翻译成IP地址)
浏览器的请求可能直接达到cdn服务器就直接返回了。当cdn服务器上没有时,就会在反向代理服务器缓存中查找,如果反向代理服务器中也没有,就需要访问百度的应用服务器。