
一应用层和应用程序直接相关虽然在应用层中有很多大佬研究出来了很多协议但是在实际开发的过程中主要的应用层还是需要“自定义协议”实例---开发外面软件首先实现获取商家列表的功能针对这样的功能需要约定一下“应用层协议”step1先确定功能中客户端和服务器需要交互哪些数据和功能需求直接相关比如需求为根据用户的位置获取到方圆5公里范围内所有的商家并且按照距离升序和降序每个商家的信息包含名称图片评分等等请求用户所在位置经纬度用户id响应商家列表商家相关的信息step2再确定数组组织的格式网络通信本质上是传输“字节流”或者“字符串”二进制而我们需要把结构化的数据能够组织成“字符串” -----------序列化并且还能把“字符串”解析回结构化的数据 ------------------反序列化结构化数据即结构体数据中包含了多个属性1直接使用纯文本的格式请求用户id经度纬度\n响应商家id商家名称商家图片商家评分商家距离 商家id商家名称商家图片商家评分商家距离 商家id商家名称商家图片商家评分商家距离\n这种直接使用纯文本的格式是基于简单的分隔符的定义方式有很多的缺点1不能直观看到每个属性是什么意思 2未来要扩展/修改属性会很麻烦2基于 XML 格式XML 格式是比较经典的格式通过成对的“标签”描述某个值的含义请求requestuserId111/userIdjingdu12/jingduweidu23/weidu/requestXML 和 HTML 的区别为XML 的标签是程序员自定义的HTML的标签都是标准的所以XML 是通用的一般的情况而HTML 是专用的特殊的情况XML的缺点非常消耗资源网络带宽格式上不优雅3JSONJSON相当于XML来说可读性更高网络带宽消耗更少请求{userId: 111,jingdu: 12,weidu: 23}由此可以看出是键值对的格式而且也支持嵌套其中的值也可以是另一个JSON注意JSON的每个{}都是独立的如果一次请求只是一个{}此时就不需要分隔符如果一次请求中需要有多个{}此时就可以使用数组的语法 [ ]响应[{accountId: 1111,name: 麻辣烫,image: xxxxx,distance: 2.5km},{accountId: 1111,name: 麻辣烫,image: xxxxx,distance: 2.5km},{accountId: 1111,name: 麻辣烫,image: xxxxx,distance: 2.5km}]4) protobufprotobuf是纯二进制的数据格式不可读无法使用肉眼进行解析虽然可读性很差但是传输效率很高1.2 用纯文本的格式实现算术运算器约定好协议1明确传递哪些数据请求运算符-*/操作数操作数1操作数2响应运算结果2组织传输数据的格式自定义文本格式请求运算符操作数1操作数2\n响应结果\n// 表示一个请求 public class Request { // 支持 - * / private String operator; private double num1; private double num2; public Request(String operator, double num1, double num2) { this.operator operator; this.num1 num1; this.num2 num2; } // 能够把这个对象转成字符串 public String convertToString() { // String.format 就类似于 printf , 把结果构造到 String 中. return String.format(%s,%f,%f\n, operator, num1, num2); } public static Request convertFromString(String request) { // 1. 获取参数 String[] split request.split(,); String operator split[0]; double num1 Double.parseDouble(split[1]); double num2 Double.parseDouble(split[2]); // 2. 创建对象 return new Request(operator, num1, num2); } public String getOperator() { return operator; } public double getNum1() { return num1; } public double getNum2() { return num2; } }// 表示一个响应数据 public class Response { private double result; public Response(double result) { this.result result; } public String convertToString() { return String.format(%f\n, result); } public static Response convertFromString(String response) { double result Double.parseDouble(response); return new Response(result); } public double getResult() { return result; } }import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; // 算术运算的客户端 public class TcpCalcClient { private Socket socket null; public TcpCalcClient(String serverIp, int serverPort) throws IOException { socket new Socket(serverIp, serverPort); } public void start() { System.out.println(客户端运行中...); try (InputStream inputStream socket.getInputStream(); OutputStream outputStream socket.getOutputStream()) { Scanner scannerConsole new Scanner(System.in); Scanner scannerNetwork new Scanner(inputStream); PrintWriter printWriter new PrintWriter(outputStream); while (true) { // 1. 从控制台读取用户输入的内容 System.out.println(请输入要进行的运算 ( - * /): ); String operator scannerConsole.next(); System.out.println(请输入第一个数字: ); double num1 scannerConsole.nextDouble(); System.out.println(请输入第二个数字: ); double num2 scannerConsole.nextDouble(); // 2. 构造请求并发送 Request request new Request(operator, num1, num2); String requestString request.convertToString(); printWriter.print(requestString); printWriter.flush(); // 3. 读取服务器的响应结果 if (!scannerNetwork.hasNext()) { break; } String responseString scannerNetwork.next(); Response response Response.convertFromString(responseString); // 4. 打印响应结果 System.out.println(response.getResult()); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpCalcClient client new TcpCalcClient(127.0.0.1, 9090); client.start(); } }import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; // 进行算术运算的服务器程序 public class TcpCalcServer { private ServerSocket serverSocket null; public TcpCalcServer(int port) throws IOException { serverSocket new ServerSocket(port); } public void start() throws IOException { System.out.println(服务器启动...); ExecutorService service Executors.newCachedThreadPool(); while (true) { Socket socket serverSocket.accept(); service.submit(() - { try { processConnection(socket); } catch (IOException e) { throw new RuntimeException(e); } }); } } // 处理一个连接 public void processConnection(Socket socket) throws IOException { System.out.printf([%s:%d] 客户端上线!, socket.getInetAddress().toString(), socket.getPort()); try (InputStream inputStream socket.getInputStream(); OutputStream outputStream socket.getOutputStream()) { Scanner scanner new Scanner(inputStream); PrintWriter printWriter new PrintWriter(outputStream); while (true) { // 1. 读取请求并解析 if (!scanner.hasNext()) { break; } String requestString scanner.next(); // 相比于前面的 EchoServer, 多了一步 反序列化 Request request Request.convertFromString(requestString); // 2. 根据请求计算响应 Response response process(request); // 3. 把响应写回到客户端 String responseString response.convertToString(); // 构造 String 的时候, 已经带了 \n, 直接 print 写入即可. printWriter.print(responseString); printWriter.flush(); } } catch (IOException e) { e.printStackTrace(); } finally { System.out.printf([%s:%d] 客户端下线!\n, socket.getInetAddress().toString(), socket.getPort()); socket.close(); } } private Response process(Request request) { // 根据当前参数和运算符完成计算, 并且构造响应 if (request.getOperator().equals()) { return new Response(request.getNum1() request.getNum2()); } else if (request.getOperator().equals(-)) { return new Response(request.getNum1() - request.getNum2()); } else if (request.getOperator().equals(*)) { return new Response(request.getNum1() * request.getNum2()); } else if (request.getOperator().equals(/)) { return new Response(request.getNum1() / request.getNum2()); } else { throw new RuntimeException(不支持的运算符: request.getOperator()); } } public static void main(String[] args) throws IOException { TcpCalcServer server new TcpCalcServer(9090); server.start(); } }二传输层传输层负责数据能够从发送端传输到接收端不关心中间过程只关心起点和终点在TCP/IP协议中,⽤源IP,源端⼝号,⽬的IP,⽬的端⼝号,协议号这样⼀个五元组来标识⼀个 通信(可以通过netstat-n查看);2.1 端口号整数用来区分同一个主机上不同的应用程序端口号是两个字节表示的无符号整数取值范围为0 ~ 65535但是0 ~ 1023之间的端口号数字一般会避开因为这些端口号是给知名的服务器预留的剩下的 1024-65535操作系统动态分配的端⼝号.客⼾端程序的端⼝号,就是由操作系统从这个范围分配的.比较重要的两个知名服务器180 ---- HTTP服务器2443 --- HTTPS服务器两个问题Q1一个进程是否可以bind多个端口号A可以。不是进程绑定端口号而是socket绑定端口号一个进程中完全可以创建多个socket 同时关联到多个端口号Q2一个端口号是否可以被多个进程bindA如果是同一种协议就不可以如果是不同的协议就可以2.2 UDP协议2.2.1 UDP协议端格式整个UDP数据报如下整个UDP报头为8个字节分为四个部分每个部分两个字节长度通过两个字节的数据描述了整个UDP数据报的长度报头固定8个字节整个长度-8就是载荷的长度其中两个字节的取值范围最大最65535约等于64KB但是放在现在是不够用的16位UDP长度,表示整个数据报(UDP首部UDP数据)的最大长度如果校验和出错就会直接丢弃校验和起到的作用是验证传输的数据是否出错了网络传输过程中为什么会出错A像10101这样的bit流本质上是电信号/光信号/电磁波电信号中高电平表示1低电平表示2但是在网络传输的过程中会受到外面环境的干扰如果网线中间经过了一些变化的磁场感应电流就可能影响到传输的数据内容所以一些高质量的网线外层通常都有“屏蔽层”本质上就是把UDP数据报每个字节除去校验和本身的这俩字节依次代入到一个数学公式进行一系列的计算》得到一个整数如果输入的这一串字节内容是固定的并且计算公式也是固定的此时得到的结果也就是一定的反之如果发现两次计算得到的结果不同此时输入的内容也一定是不同的计算校验和的方案本质上都是hash算法1CRC循环冗余方案简单相加计算起来比较简单快捷但是容易出现不同输入数据得到相同结果的情况2md5系列/sha1系列套入更加复杂的公式特点1定长无论输入的内容是多少个字节输出结果就是固定长度的有很多个版本163264128......2分散输入的内容哪怕只是改变一点点都会差别很大3不可逆根据原始数据计算出md5/sha1非常快的根据md5/sha1值反推出原始数据成本非常大几乎不可能