TCP / IP 协议
应用层
传输层
互联网层
数据链路层
物理层
一、应用层
应用层是我们网络编程主要针对的地方,也是程序员进行主要交互的一层。我们来看一些应用协议有哪些,以及应用层协议的两种形式。
1. 常用的现有应用层协议
DNS(域名系统)
HTTP(超文本传送协议)
FTP(文件传输协议)
Telnet(远程终端协议)
2. 自定义协议
开发之前,建立约定。开发过程中就会让客户端和服务器之间严格遵守协议约定好的格式。
自定义协议大体分成两类:
(1) 文本格式
文本格式即把请求响应当成字符串来处理,处理的基本单位是字符。
常见的文本格式有两类:xml,json
因为文本格式较为简单,当然自己也可以约定文本格式。
① xml
xml 是一种格式化组织数据的方式,它在 Java 标准库中有其对应的实现,这种格式不仅可以用于自定义协议,也可以用于网络传输。
格式如下:
整个 xml 是由 " 标签 " 构成的,此外,标签是成对出现的。
比方说: <num1> 10 </num1> 开始标签: <num1> 结束标签: </num1> 中间值: 10
请求: 响应: <request> <response> <num1> 10 </num1> <result> 30 </result> <num2> 20 </num2> </response> <operator> + </operator> </request>
② json
json 是一种键值对的方式,同样地,它在 Java 标准库中有其对应的实现。
格式如下:
键和值之间使用 [冒号] 分割,键值对之间使用 [逗号] 分割。
请求: 响应: { { num1: 10, result: 30 num2: 20, } operator: "+" }
2) 二进制格式
把请求响应当成二进制数据处理,处理的基本单位是字节
protobuffer,thift…
3. 理解自定义协议
我们先来看一下以 【文本 + 分隔符】的方式,来自己设计一个协议。
UdpCalServer 用来描述服务器 UdpCalClient 用来描述客户端
(1) 基于 UDP 的 socket 写一个计算器运算
① UdpCalServer
package demo1; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; public class UdpCalServer { private DatagramSocket socket = null; public UdpCalServer(int port) throws SocketException { socket = new DatagramSocket(port); } public void start() throws IOException { System.out.println("服务器启动!"); while (true) { //2. 读取请求并解析 DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); socket.receive(requestPacket); String request = new String(requestPacket.getData(), 0, requestPacket.getLength()); //3. 根据请求构造响应 String response = process(request); //4. 构造响应并返回 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress()); socket.send(responsePacket); //顺便打印日志 String log = String.format("[%s : %d], req: %s; resp: %s", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response); System.out.println(log); } } private String process(String request) { String[] strings = request.split(";"); if (strings.length != 3) { System.out.println("你所输入的格式不正确!"); return null; } int num1 = Integer.parseInt(strings[0]); int num2 = Integer.parseInt(strings[1]); String key = strings[2]; int result = 0; if (key.equals("+")){ result = num1 + num2; } else if (key.equals("-")) { result = num1 - num2; } else if (key.equals("*")) { result = num1 * num2; } else { result = num1 / num2; } return result + ""; } public static void main(String[] args) throws IOException { UdpCalServer server = new UdpCalServer(9090); server.start(); } }
② UdpCalClient
package demo1; import java.io.IOException; import java.net.*; import java.util.Scanner; public class UdpCalClient { private String serverIp; private int serverPort; private DatagramSocket socket = null; public UdpCalClient(String serverIp, int serverPort) throws SocketException { this.serverIp = serverIp; this.serverPort = serverPort; this.socket = new DatagramSocket(); } public void start() throws IOException { Scanner scanner = new Scanner(System.in); System.out.println("输入格式:num1; num2; +-*/ "); while (true) { System.out.println("输入计算的数和符号 ->"); String request = scanner.nextLine(); if (request.equals("exit")) { System.out.println("Exit!"); return; } //1. 构造请求并发送 DatagramPacket requestPacket = new DatagramPacket( request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), serverPort ); socket.send(requestPacket); //2. 尝试获取响应 DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096); socket.receive(responsePacket); String response = new String(responsePacket.getData(), 0, responsePacket.getLength()); //3,显示响应结果 String log = String.format("req: %s resp: %s", request, response); System.out.println(log); System.out.println(); } } public static void main(String[] args) throws IOException { UdpCalClient client = new UdpCalClient("127.0.0.1", 9090); client.start(); } }
通信结果:
在上面的例子中,我们可以看到:当计算 15+3 = 18 这个逻辑的时候,我们在服务器与客户端通信之前,就两边约定好了,将操作数放在前两位,操作符放在后一位,它们之间通过 ; 来分割。也就说,格式为:【15;3;+】
所以说,自定义协议本质上也是一种提前的约定。
这个和我们开头说的例子是一样的:网友A 和 网友B 见面,他们提前就要告知对方今天穿什么颜色的上衣和裤子,否则就算到达同一地点,也很难找到找到彼此。
4. DNS
DNS:Domain Name System ( 域名系统 )
什么是域名?
类似于
www.baidu.com www.sogou.com
可以看到,域名就是通过一串单词通过点来进行分割的,在我们传统的叫法,域名实际上就是某某网站…
在传输数据的时候,先根据域名,再转换成对应的 IP地址,最后根据 IP地址进行传输。因为,IP地址是通过点分十进制来表示的,对于日常使用的网络用户来说,相对不好理解,而使用域名,就相当于为 IP地址 套上了一层外套,域名也就更加形象了。
二、传输层
传输层只负责端对端的数据传输,即只考虑起点和终点,不考虑中间过程。
传输层是操作系统内核实现的,因此谈到的传输层协议,一般都是指现成的一些协议,很少会涉及 " 自定制 "。
TCP / UDP 协议就是传输层中的主要协议。
1. 深入理解端口号
之前的博客提到:端口号其实是用来标识一个进程的。
一个主机中的所有进程都是通过同一个网卡来传输数据的,让每个进程分别绑定到不同的端口号,此时收到的网络数据报中就会包含一个 " 目的端口 ",它就会根据目的端口找到对应端口号的进程,从而把数据交给对应的进程。
2. 端口号和 PID 的区别
在学习线程的时候,我们了解过 PID 的概念,PID( Process Identification ),即进程标识符,操作系统里每打开一个程序都会创建一个进程 ID,即 PID. 但我们必须明确:程序在运行的时候,PID 是不会改变标识符的,但进程终止后,PID 标识符就会被系统回收,就可能会被继续分配给新运行的程序,所以说 PID 是临时的一个标志。
但是,端口号是固定不变的。
所以说:在网络编程中,我们指定一个程序 / 进程 与服务器通信,需要知道 主机的 IP地址 和 进程的端口号。而不能用 PID 来代替端口号!
举个例子:你打中国移动 10086,准备办理流量套餐,那么对面就会有人工客服接听电话,当你办理好了套餐之后,就把电话挂了。但你发现,电话套餐忘记办理了,于是就再次打电话过去,这个时候,你会发现,系统为你分配的客服和上一个客服就不再是同一个人了。那么,端口号就相当于 10086 这个固定的号码,而不同的客服就表示不同的 PID。
3. 注意
(1)
通常情况下,一个进程对应一个端口号,两个进程无法绑定同一个端口。
而比较常见的情况是:一个进程能够对应多个端口。这是通过 socket 文件和端口绑定来实现的,因为一个进程可以有多个文件,那么就可以做到让多个 socket 文件绑定到不同的端口。
让一个进程对应多个端口,实际上是为了解决【调试问题】,通常情况下,服务器会提供客户端一个【业务端口】,这个端口为客户端提供服务,如果在提供服务的时候,出现了数据问题怎么办?这时候,就可以事先让当前进程再绑定另一个【调试端口】,而这个端口就是用来定位一些问题。
(2)
端口号是一个两个字节整数,那么最多也就是 16 位,所以它的范围为 0- 65535.
知名端口号: 0 - 1023 注册端口号: 1024 - 49151 剩下的端口号叫动态端口号或私有端口号: 49152 - 65535