1. 网络编程重要概念
1.1. IP
IP:设备在网络中的地址,是唯一的标识
IPv4是目前的主流方案,最多只能有2^32个IP,目前已经使用完了,为了解决这个问题而出现的IPv6最多有2^128个IP,
特殊IP地址:
127.0.0.1,也可以是localhost:是回送地址也称本地回环地址,也称本机IP,永远只会寻找当前所在本机
1.2. 端口号
端口号:应用程序在设备中唯一的标识,一个端口号只能被一个应用程序使用
由两个字节表示的整数,取值范围:0~65535,其中0~1023之间的端口号用于一些知名的网络服务或者应用
1.3. 协议
协议是指数据在网络传输中的规则,由于网络通信是非常复杂的事,如果使用一个协议来约定所有的网络通信细节,就会导致这个协议非常庞大且复杂,所以把这个大的协议拆分成多个小的协议,让每一个小的协议专注于解决一个问题,再让这些协议之间相互配合。协议分层就是把功能定位类似的协议,放到同一层中,并且约定好层与层之间的交互关系,上层协议调用下层协议,下层协议给上层提供服务
协议分层的优点:
- 降低了使用成本,使用某个协议的时候不必关注其他协议的实现细节
- 降低整个体系的耦合性,灵活的变更某个层次的协议
1.3.1. TCP/IP 五层协议模型
应用层:是最接近用户的一层,为用户提供各种网络应用服务
传输层:提供端到端的通信服务,确保数据可靠、有序地从源端传输到目的端,只关注网络通信中的“起点和终点”
网络层:主要功能是进行寻址和路由选择
数据链路层:针对上述规划好的路径进行具体的实施
物理层:描述的是硬件设备需要满足的条件
上述过程就相当于公司中董事长(传输层)指定公司的目标,高管(网络层)对目标进行规划,基层员工(数据链路层)按照规划具体实施,办公设备(物理层)
1.3.2. OSI 七层网络模型
相较于 TCP/IP 五层协议多出了表示层和会话层,TCP/IP 五层协议是把表示层和会话层和应用层融合到一起了
网络设备所在分层:
- 对于一台主机,它的操作系统内核实现了从传输层到物理层的内容,也就是 TCP/IP 五层协议模型的下四层
- 对于一台路由器,它实现了从网络层到物理层,也就是 TCP/IP 五层协议模型的下三层
- 对于一台交换机,它实现了从数据链路层到物理层,也就是 TCP/IP 五层协议模型的下两层
- 对于集线器,它实现了物理层
2. 数据在网络通信过程中的整体流程
假如需要通过 QQ 来发送 Hello 给另一个人,在发送方这里需要经过:
- 用户输入 Hello 点击发送,程序会把发送的内容读取到,构造成一个“应用层数据包”,应用层的网络协议就描述了这个数据包的构造,此处的协议往往是程序员自己制定的
- 程序调用操作系统的 api(传输层给应用层提供的 api),把上述组织好的数据包作为参数传入进来,此时传输层就会把上述的应用层数据再进一步封装(这里的封装类似于字符串拼接,因为发送的数据都是二进制的字符串)成一个传输层数据包,传输层用到的主要协议 TCP/ UDP 协议
- 传输层构造好数据之后,就会继续调用网络层给传输层提供的 api ,把数据继续传入网络层,网络层也有许多协议,主要的就是 IPv4 协议,IP协议就会把拿到的传输层数据包构造成网络层数据包
- 网络层继续调用数据链路层的 api 把数据交给数据链路层处理,数据链路层常见的协议(以太网)在 IP 数据包的基础上再一步进行包装
- 上述得到的数据需要进一步交给物理层(硬件设备),针对以上二进制文件进行真正的传输工作,把二进制序列转化为光电信号传输
接收方视角:
- 接收方物理层接收到光电信号还原成二进制序列
- 物理层转回来的数据交给数据链路层,以太网对数据包进行解析,拿出这里的报头和载荷,根据报头信息决定是转发,丢弃还是保留(向上进行解析)然后继续向上传输网络层
- 网络层拿到数据之后,通过 IP 协议对数据包进行解析,同样的根据报头的信息来判断保留
- 传输层这里根据 UDP/ TCP 协议也是执行上述操作,进一步传输给应用层
- 到应用层这里就是针对上面的数据进行反序列化操作
传输层只关注起点和终点,中间过程的操作可能存在很多的交换机和路由器来完成数据转发的过程
交换机:封装分用到数据链用层,就可以决定数据是转发还是丢弃了(不再分用)(二层转发)
路由器:封装分用到网络层(三层转发)
3. 面试题
TCP 和 UDP 的区别:
- 连接方式:TCP 是有链接的协议(通信双方保存了通信对端的信息),UDP 是无连接的协议(没有保存)
- 可靠性:TCP 提供可靠的数据传输,通过确认机制,超时重传等机制来确保数据的完整性和准确性,如果说发送方发送的数据没有被接收方正确接收,发送方就会重新发送数据。UDP 则不提供可靠的数据传输,不会关心发送的数据是否被正确接收
- 传输效率:TCP 相对与 UDP 来说效率较低
- 传输的大小:TCP 传输是面向字节流的,UDP 传输是面向数据报的,传输的单位就不是字节了,一次发送 / 接收完整的数据报
- TCP 支持全双工(一个通信链路可以发送数据,也可以接收数据),UDP 支持半双工(一个通信链路只能发送/接收)
4. UDP 协议的简单示例
回显服务器:客户端发送什么请求,服务器就返回什么相应(不包含任何逻辑处理)
对于服务器端来说,需要在 socket 对象创建的时候,就指定一个端口号,作为构造方法的参数,后续服务器运行之后,操作系统就会把端口号和该进程关联起来
对于同一个系统来说,同一时刻,一个端口号只能被一个进程绑定,但是一个进程可以绑定多个端口号(创建多个 socket 对象),端口号就是为了区分进程
创建 socket 对象之后开始接收客户端的请求:
//获取用户请求并解析 DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096); //接收请求 socket.receive(requestPacket); String request = new String(requestPacket.getData(),0,requestPacket.getLength()); String response = process(request);
接收到用户的请求之后,服务器端需要作出响应,发送到客户端
//把响应写回客户端 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length); socket.send(responsePacket);
这里把相应的内容写回客户端时,由于原来是一个字符串,直接获取长度获取的是字符的个数,所以需要获取字节数组之后再获取字节数组的长度
还有就是,由于 UDP 是无连接的(没有保存通信双方的 ip 和端口),DatagramSocket 这个类中不持有对方的信息,进行 send 的时候就需要在 send 的数据包里,把要发给谁这样的信息写进去
//把响应写回客户端 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress()); socket.send(responsePacket);
服务端整体逻辑:
public class UdpEchoServer { private DatagramSocket socket = null; public UdpEchoServer(int port) throws SocketException { //对于服务器端来说,需要在 socket 对象创建的时候,就指定一个端口号,作为构造方法的参数 //后续服务器运行之后,操作系统就会把端口号和该进程关联起来 socket = new DatagramSocket(port); } public void start() throws IOException { System.out.println("服务器启动..."); while (true){ //获取用户请求并解析 DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096); //接收请求 socket.receive(requestPacket); String request = new String(requestPacket.getData(),0,requestPacket.getLength()); String response = process(request); //把响应写回客户端 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress()); socket.send(responsePacket); //打印信息 System.out.printf("[%s:%d] req=%s, resp=%s\n",requestPacket.getAddress(),requestPacket.getPort(), request,response); } } //根据请求确定响应 private String process(String request){ return request; } public static void main(String[] args) throws IOException { UdpEchoServer udpEchoServer = new UdpEchoServer(8090); udpEchoServer.start(); } }
接下来看客户端:
由于是客户端主动发送的请求,所以需要知道服务端的端口号才能找到服务器,所以客户端不需要指定端口号,但不是客户端不需要端口号,而是系统随机分配了一个端口给客户端
此外,如果客户端指定了端口之后,由于客户端是在用户的电脑上运行的,所指定的端口就可能和现有的端口冲突
构造方法里需要拿到服务端的 IP 和端口号
public UdpEchoClient(String serverIP,int serverPort) throws SocketException { socket = new DatagramSocket(); this.serverIP = serverIP; this.serverPort = serverPort; }
接下来创建请求数据报时,传入请求内容的字节数组和长度,再传入服务器的 IP 和端口号,但是发现报错了
因为原来定义的 IP 是字符串类型的,所以需要转化一下:
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(this.serverIP),this.serverPort);
public class UdpEchoClient { private DatagramSocket socket = null; private String serverIP; private int serverPort; public UdpEchoClient(String serverIP,int serverPort) throws SocketException { socket = new DatagramSocket(); this.serverIP = serverIP; this.serverPort = serverPort; } public void start() throws IOException { System.out.println("客户端开始运行..."); Scanner scanner = new Scanner(System.in); while (true){ System.out.print("->"); String request = scanner.next(); //构造出 UDP 请求 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(this.serverIP),this.serverPort); socket.send(requestPacket); //从服务端读取到响应 DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096); socket.receive(responsePacket); //打印响应到的内容 String response = new String(responsePacket.getData(),0, responsePacket.getLength()); System.out.println(response); } } public static void main(String[] args) throws IOException { UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",8090); udpEchoClient.start(); } }