TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输。TCP/IP 协议采用4层结构,分别是应用层、传输层、网络层和链路层,每一层都呼叫它的下一层所提供的协议来完成自己的需求。由于我们大部分时间都工作在应用层,下层的事情不用我们操心;其次网络协议体系本身就很复杂庞大,入门门槛高,因此很难搞清楚TCP/IP的工作原理,通俗一点讲就是,一个主机的数据要经过哪些过程才能发送到对方的主机上。
TCP/IP协议的传输过程:
0、物理介质就是把电脑连接起来的物理手段,常见的有光纤、双绞线,以及无线电波,它决定了电信号(0和1)的传输方式,物理介质的不同决定了电信号的传输带宽、速率、传输距离以及抗干扰性等等。
TCP/IP协议栈分为四层,每一层都由特定的协议与对方进行通信,而协议之间的通信最终都要转化为 0 和 1 的电信号,通过物理介质进行传输才能到达对方的电脑,因此物理介质是网络通信的基石。
下面我们通过一张图先来大概了解一下TCP/IP协议的基本框架:
当通过http发起一个请求时,应用层、传输层、网络层和链路层的相关协议依次对该请求进行包装并携带对应的首部,最终在链路层生成以太网数据包,以太网数据包通过物理介质传输给对方主机,对方接收到数据包以后,然后再一层一层采用对应的协议进行拆包,最后把应用层数据交给应用程序处理。
网络通信就好比送快递,商品外面的一层层包裹就是各种协议,协议包含了商品信息、收货地址、收件人、联系方式等,然后还需要配送车、配送站、快递员,商品才能最终到达用户手中。
一般情况下,快递是不能直达的,需要先转发到对应的配送站,然后由配送站再进行派件。
配送车就是物理介质,配送站就是网关, 快递员就是路由器,收货地址就是IP地址,联系方式就是MAC地址。
快递员负责把包裹转发到各个配送站,配送站根据收获地址里的省市区,确认是否需要继续转发到其他配送站,当包裹到达了目标配送站以后,配送站再根据联系方式找到收件人进行派件。
1、ip数据包(网络层)
在网络层被包装的数据包就叫IP数据包,IPv4数据包的结构如下图所示:
IP数据包由首部和数据两部分组成,首部长度为20个字节,主要包含了目标IP地址和源IP地址,目标IP地址是网关路由的线索和依据;数据部分的最大长度为65515字节,理论上一个IP数据包的总长度可以达到65535个字节,而以太网数据包的最大长度是1500个字符,如果超过这个大小,就需要对IP数据包进行分割,分成多帧发送。
所以,网络层的主要工作是定义网络地址,区分网段,子网内MAC寻址,对于不同子网的数据包进行路由。
2、3次“握手”(传输层)
链路层定义了主机的身份,即MAC地址, 而网络层定义了IP地址,明确了主机所在的网段,有了这两个地址,数据包就从可以从一个主机发送到另一台主机。但实际上数据包是从一个主机的某个应用程序发出,然后由对方主机的应用程序接收。而每台电脑都有可能同时运行着很多个应用程序,所以当数据包被发送到主机上以后,是无法确定哪个应用程序要接收这个包。
因此传输层引入了UDP协议来解决这个问题,为了给每个应用程序标识身份,UDP协议定义了端口,同一个主机上的每个应用程序都需要指定唯一的端口号,并且规定网络中传输的数据包必须加上端口信息。 这样,当数据包到达主机以后,就可以根据端口号找到对应的应用程序了。UDP定义的数据包就叫做UDP数据包,结构如下所示:
UDP数据包由首部和数据两部分组成,首部长度为8个字节,主要包括源端口和目标端口;数据最大为65527个字节,整个数据包的长度最大可达到65535个字节。
UDP协议比较简单,实现容易,但它没有确认机制, 数据包一旦发出,无法知道对方是否收到,因此可靠性较差,为了解决这个问题,提高网络可靠性,TCP协议就诞生了,TCP即传输控制协议,是一种面向连接的、可靠的、基于字节流的通信协议。简单来说TCP就是有确认机制的UDP协议,每发出一个数据包都要求确认,如果有一个数据包丢失,就收不到确认,发送方就必须重发这个数据包。
为了保证传输的可靠性,TCP 协议在 UDP 基础之上建立了三次对话的确认机制,也就是说,在正式收发数据前,必须和对方建立可靠的连接。由于建立过程较为复杂,我们在这里做一个形象的描述:
主机A:我想发数据给你,可以么?
主机B:可以,你什么时候发?
主机A:我马上发,你接着!
经过三次对话之后,主机A才会向主机B发送正式数据,而UDP是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发过去了。所以 TCP 能够保证数据包在传输过程中不被丢失,但美好的事物必然是要付出代价的,相比 UDP,TCP 实现过程复杂,消耗连接资源多,传输速度慢。
总结一下,传输层的主要工作是定义端口,标识应用程序身份,实现端口到端口的通信,TCP协议可以保证数据传输的可靠性。
2、理论上讲,有了以上三层协议的支持,数据已经可以从一个主机上的应用程序传输到另一台主机的应用程序了,但此时传过来的数据是字节流,不能很好的被程序识别,操作性差。因此,应用层定义了各种各样的协议来规范数据格式,常见的有 HTTP、FTP、SMTP 等,HTTP 是一种比较常用的应用层协议,主要用于B/S架构之间的数据通信。应用层的主要工作就是定义数据格式并按照对应的格式解读数据。
3、全流程
首先我们梳理一下每层模型的职责:
- 链路层:对0和1进行分组,定义数据帧,确认主机的物理地址,传输数据;
- 网络层:定义IP地址,确认主机所在的网络位置,并通过IP进行MAC寻址,对外网数据包进行路由转发;
- 传输层:定义端口,确认主机上应用程序的身份,并将数据包交给对应的应用程序;
- 应用层:定义数据格式,并按照对应的格式解读数据。
然后再把每层模型的职责串联起来,用一句通俗易懂的话讲就是:
当你输入一个网址并按下回车键的时候,首先,应用层协议对该请求包做了格式定义;紧接着传输层协议加上了双方的端口号,确认了双方通信的应用程序;然后网络协议加上了双方的IP地址,确认了双方的网络位置;最后链路层协议加上了双方的MAC地址,确认了双方的物理位置,同时将数据进行分组,形成数据帧,采用广播方式,通过传输介质发送给对方主机。而对于不同网段,该数据包首先会转发给网关路由器,经过多次转发后,最终被发送到目标主机。目标机接收到数据包后,采用对应的协议,对帧数据进行组装,然后再通过一层一层的协议进行解析,最终被应用层的协议解析并交给服务器处理。
以下是简单的传输例子
ServerSocket s = null;
s = new ServerSocket(8888);//申请端口
System.out.println("等待连接");
Socket ss = s.accept();
System.out.println("已经有连接");
InputStream input = ss.getInputStream();
OutputStream output = ss.getOutputStream();
PrintWriter pw = new PrintWriter(output);
pw.println("你好!");
pw.flush();
服务器给众多客户端发送信息
package 网络;
import java.io.*;
import java.net.Socket;
import java.net.ServerSocket;
import 网络.Student;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
//服务器给众多客户端服务的时候要多次监听
class Service implements Runnable {
private ServerSocket server = null;
private Socket s = null;// 套接字
private InputStream input = null;
private OutputStream output = null;
private ObjectOutputStream objOut = null;
private PrintWriter pw = null;
public Service(Socket s) {
this.s = s;
}
@Override
public void run() {
try {
input = s.getInputStream();// 得到网络流的方法:从套接字中得到流,并保存返回的地址
output = s.getOutputStream();
objOut = new ObjectOutputStream(output);//实现流的类型转换
pw = new PrintWriter(output);
// 发一个整数
objOut.writeInt(200);
// 发一个浮点数
objOut.writeDouble(324.234);
// 发一个字符串
pw.println("Hello Socket!");
//发一个学生对象
Student ss = new Student(23, "张三");
objOut.writeObject(ss);
objOut.flush();
objOut.close();
} catch (Exception e) {
// TODO: handle exception
}
}
}
public class ObjectServer {
public static void main(String[] args) {
ServerSocket server = null;
Socket s = null;// 套接字
InputStream input = null;
OutputStream output = null;
ObjectOutputStream objOut = null;
PrintWriter pw = null;
// 服务器每个客户一个线程
try {
server = new ServerSocket(9999);// 创建端口号
while (true) {
System.out.println("监听客户连接。。。。");
s = server.accept();// 监听
System.out.println("已监听到客户连接[远程主机:" + s.getRemoteSocketAddress() + " : 端口:" + s.getPort() + "]");
new Thread(new Service(s)).start();
}
} catch (Exception e) {
// TODO: handle exception
}
}
}
package 网络;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ObjectClient {
public static void main(String[] args) {
Socket s = null;
InputStream input = null;
OutputStream output = null;
String ip = "192.168.50.69";
ObjectOutputStream objOut = null;
ObjectInputStream objIn=null;
BufferedReader br=null;
int port = 9999;
try {
s = new Socket(ip,port);//ip+端口号
input = s.getInputStream();//从套接字中得到输入输出流(字节流)
output = s.getOutputStream();//
objIn=new ObjectInputStream(input);//对象流用于实现类型转换
br=new BufferedReader(//因为字符流不能包装字节流
new InputStreamReader(input));
//将字节流转换为字符流,读的是字节,读后转换为字符
int a=objIn.readInt();
System.out.println(a);
double b=objIn.readDouble();
System.out.println(b);
String str=br.readLine();
System.out.println(str);
Student student=(Student)objIn.readObject();//强制类型转换
System.out.println(ip+student);
output.flush();//刷新
output.close();//释放流的资源
//网络流轻易不关闭,常连接
} catch (Exception e) {
e.printStackTrace();
}
}
}
package 网络;
import java.io.Serializable;
public class Student implements Serializable {
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + "]";
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
}
ps:本篇博文主要从编程的角度出发,目的是理解tcp/ip协议的传输过程,具体细节请参考