在前面网络编程基础中,我们简单解释了TCPIP五层协议,并且通过QQ收发消息的示例来展示五层协议中每一层的作用.在此我先简单回顾一下TCPIP五层协议
- TCPIP五层协议
- 应用层:网络上传输的数据,应用程序如何使用(要寄出的物品)
- 传输层:负责端到端的传输,通行双方只关心起点和终点(快递公司及收发件人具体的门牌号,确保交到正确的人手里)
- 网络层:负责主机到主机的传输,着重规划传输路线(负责计算快递从武汉发往广州走哪条路最快)
- 数据链路层:负责两个相邻节点的数据转发(快递车在两个相邻驿站或集运中心的转运过程)
- 物理层:负责将数据转化成能在物理介质中传播的信号(快递车走的高速公路)
应用层协议
协议是什么?
协议就是计算机之间约定好的沟通规则
-
为什么要使用协议?
如果两个不同国家的人,一个说的是法语,一个说的是中文.他们不管如何说自己的语言对方都听不懂,但如何他们都遵守一种协议(说英语).那么这两个人就可以通过英语来交流和理解对方的意思了
协议作为统一规范.只有使用相同的协议电脑的各种硬件,操作系统,软件才能互相交流,理解对方的意思. -
常见的传输协议
- http协议
Web程序中最常用的协议,通过浏览器看视频或新闻都是通过http协议通信的.下面是一个http请求的简单示例
httpGET /index.html HTTP/1.1 # 【请求行】 # GET:获取数据 # /index.html:要访问的页面 # HTTP/1.1:使用的协议版本 Host: www.example.com # 【请求头】要访问的网站地址 User-Agent: Chrome/120.0 # 【请求头】告诉服务器我是浏览器 Accept: text/html # 【请求头】我想要网页内容 # 【空行】分隔请求头和请求体 # GET 请求没有请求体,到这里就结束- xml协议
xml协议属于比较老旧的格式比例,现在更多用来作为配置文件
xml<?xml version="1.0" encoding="UTF-8"?> <bookstore> <book id="1"> <name>XML入门教程</name> <author>编程新手</author> <price>39.9</price> <publish_date>2025-01-01</publish_date> </book> <book id="2"> <name>Python零基础</name> <author>代码达人</author> <price>49.9</price> <publish_date>2025-02-01</publish_date> </book> </bookstore>- 缺点:
- 结构复杂
- 不易读
- 冗余字符过多
- json协议
json是目前java开发中的绝对主流
json{ "姓名": "张三", "年龄": 20, "是否学生": true, "爱好": ["看书", "编程", "打球"], "地址": { "省份": "广东省", "城市": "广州市" }, "余额": null }- 优点:
- 简洁:同样的信息json占用的字符更少\
- 缺点:
- 不支持注释
- http协议
以上只是部分协议,还有protobuf,IBM等各种各样的协议
- 自定义协议
在日常开发过程中,自定义协议是很常见的.下面是一个基于自定义协议的在线计算器程序
java
package OnlineCalculator;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpCalServer {
ServerSocket serverSocket = null;
public TcpCalServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
Socket socket = serverSocket.accept();
service.submit(() -> {
try {
connect(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
private void connect(Socket socket) throws IOException {
System.out.printf("%s[%s:%d]\n","客户端上线",socket.getInetAddress().toString(),socket.getPort());
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerConnect = new Scanner(inputStream);
while(scannerConnect.hasNext()) {
//接收请求
String requestString = scannerConnect.next();
Request request = Request.requestFromString(requestString);
//构造响应
Response response = calculate(request);
//返回响应
outputStream.write(response.responseToString().getBytes());
outputStream.flush();
}
}
catch (IOException e) {
e.printStackTrace();
}
finally {
System.out.printf("%s[%s:%d]\n","客户端下线",socket.getInetAddress().toString(),socket.getPort());
socket.close();
}
}
private Response calculate(Request request) {
double num;
if(request.getOperator().equals("+")) {
num = request.getNum1() + request.getNum2();
} else if (request.getOperator().equals("-")) {
num = request.getNum1() - request.getNum2();
} else if (request.getOperator().equals("*")) {
num = request.getNum1() * request.getNum2();
} else if (request.getOperator().equals("/")) {
num = request.getNum1() / request.getNum2();
}else {
throw new RuntimeException("错误的运算符" + request.getOperator());
}
return new Response(num);
}
public static void main(String[] args) throws IOException {
TcpCalServer tcpCalServer = new TcpCalServer(9090);
tcpCalServer.start();
}
}
package OnlineCalculator;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class TcpCalClient {
Socket socket = null;
private String ip;
private int port;
public TcpCalClient(String ip, int port) throws IOException {
this.ip = ip;
this.port = port;
socket = new Socket(ip, port);
}
public void start() {
try(OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream()) {
Scanner scannerClient = new Scanner(inputStream);
Scanner scanner = new Scanner(System.in);
while (true) {
//输入内容
System.out.println("输入运算符(+,-,*,/):");
String operator = scanner.next();
System.out.println("输入第一个数字");
double num1 = scanner.nextDouble();
System.out.println("输入第二个数字");
double num2 = scanner.nextDouble();
//构造请求
Request request = new Request(operator,num1,num2);
//发送请求
outputStream.write(request.requestToString().getBytes());
outputStream.flush();
//接收请求
String responseString = scannerClient.next();
Response response = Response.responseFromString(responseString);
//打印结果
System.out.printf("%s[%s:%d]%f\n","服务端返回",socket.getInetAddress().toString(),socket.getPort(),response.getNum());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpCalClient tcpCalClient = new TcpCalClient("127.0.0.1",9090);
tcpCalClient.start();
}
}
package OnlineCalculator;
public class Request {
private String operator;
private double num1;
private double num2;
public String getOperator() {
return operator;
}
public double getNum1() {
return num1;
}
public double getNum2() {
return num2;
}
public Request(String operator, double num1, double num2) {
this.operator = operator;
this.num1 = num1;
this.num2 = num2;
}
//构造协议内容
public String requestToString() {
return String.format("%s,%f,%f\n",operator,num1,num2);
}
//解析协议内容
public static Request requestFromString(String request) {
String[] split = request.split(",");
String operator = split[0];
double num1 = Double.parseDouble(split[1]);
double num2 = Double.parseDouble(split[2]);
return new Request(operator,num1,num2);
}
}
package OnlineCalculator;
public class Response {
private double num;
public Response(double num) {
this.num = num;
}
public String responseToString() {
return String.format("%f\n",num);
}
public static Response responseFromString(String response) {
return new Response(Double.parseDouble(response));
}
public double getNum() {
return num;
}
}
在这个在线计算器程序中,Request和Response就属于是自定义协议.通过自己定义的规则来进行客户端和服务端通信
端口号
端口号是由两个字节表示的无符号整数,因此端口号的范围在0~65535
而端口号的作用就是用来区分不同的应用程序
就拿前面在线计算机为例.9090端口就代表着我们编写的这个计算机程序
在每一个服务创建cocket时,都必须指定对应的端口号.一个进程可以创建多个socket,所以一个程序可以具有多个端口号.但一个socket只能绑定一个端口号
一个端口并不是只能被一个进程绑定.当使用的协议不同时,也是可以正常绑定重复的端口的.比如前文写过的TCP回显服务器和UDP回显服务器,因为基于的协议不同,因此都能绑定同一个9090端口
通常情况下:服务器都是由程序员自定义的端口,客户端则是系统自动分配端口
- "知名"端口号
对于0~1023端口.这些都被"知名"服务器给默认使用了.所以我们在自定义端口时应尽量避免知名端口
为什么"知名"端口号的知名打了引号?因为现在所说的知名端口号是20~30年前计算机刚发展时的知名服务器使用的.而现在大部分"知名"服务器都已经消失了
现在我们还是能接触到很多知名端口的.例如
ftp服务器:使用21端口
ssh服务器:使用22端口
telnet服务器:使用23端口
http服务器:使用80端口
https服务器:使用443端口
UDP原理
首先我们先简单回顾一下UDP的特点
- UDP特点
- 无连接
直接通过IP和端口号进行传输数据,无需使用accpet()来进行连接 - 不可靠传输
UDP协议发完数据报后不会再做任何处理,无法得知数据是否到达且完整 - 面向数据报
UDP报文既不会拆分,也不会合并 - 全双工
可以同时进行收发数据 - 大小受限
UDP能传输的最大长度是64KB
- 无连接
知道这些后,我们就能接着学习下UDP的协议格式了
-
协议格式

-
UDP报头
- 源端口号
- 目的端口号
- UDP长度:UDP报头长度加上UDP载荷长度.由于16位最大仅能表示65535.因此一个UDP数据报最大只能那个传输约64KB(实际上总是小于64KB)
- UDP效验和:用来数学方法检测在网络传输中数据有无损坏.在网络传输中容易受到电磁干扰,内存错误等问题.所以在处理报文的过程中就把UDP数据报中除了效验和的内容都进行16位反码加法来获得唯一效验值.收到数据报后会优先计算效验值是否相同,如果不同则数据损坏
-
UDP载荷
- 应用层数据:最大传输数据长度仅约64KB.当传输大于64KB的数据时,要么在应用层手动分包拼装,要么就换成TCP协议
-
UDP协议的主要应用场景
对效率要求高,可以接收少量丢包的场景
如:DNS查询,在线语音等等