Java————网络编程

一 、网络编程基础

1. 为什么需要网络编程

用户在浏览器中,打开在线视频网站,
如优酷看视频,实质是通过网络,
获取到网络上的一个视频资源。

与本地打开视频文件类似,只是视频文件这个资源的来源是网络。
相比本地资源来说,网络提供了更为丰富的网络资源:

在这里插入图片描述

所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
而所有的网络资源,都是通过网络编程来进行数据传输的。

2. 什么是网络编程

网络编程,
指网络上的主机,通过不同的进程,
以编程的方式实现网络通信(或称为网络数据传输)。

即便是同一个主机,只要是不同进程,
基于网络来传输数据, 也属于网络编程。

在条件有限的情况下,
一般也都是在一个主机中运行多个进程来完成网络编 程。
在这里插入图片描述

二 、网络编程中的基本概念

1. 发送端和接收端

发送端和接收端只是相对的,
只是一次网络数据传输产生数据流向后的概念。
在这里插入图片描述

在一次网络数据传输时:

发送端:
数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。

接收端:
数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。

收发端:
发送端和接收端两端,也简称为收发端。

2. 请求和响应

在这里插入图片描述
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送
第二次:响应数据的发送。

好比在快餐店点一份炒饭:
先要发起请求:点一份炒饭,再有快餐店提供的对应响应:提供一份炒饭。

2. 客户端和服务端

服务端:
在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。

客户端:
获取服务的一方进程,称为客户端。

  • 客户端获取服务资源
    在这里插入图片描述
  • 客户端保存资源在服务器
    在这里插入图片描述
  • 常见的客户端服务端模型
    在这里插入图片描述
    最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
    1. 客户端先发送请求到服务端。
    2. 服务端根据请求数据,执行相应的业务处理。
    3. 服务端返回响应:发送业务处理结果。
    4. 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)。

三 、Socket套接字

Socket套接字,
是由系统提供用于网络通信的技术,
是基于TCP/IP协议的网络通信的基本操作单元。
基于Socket套接字的网络程序开发就是网络编程。

1. 分类

  • 流套接字:
    使用传输层TCP协议。
    TCP,即Transmission Control Protocol(传输控制协议),传输层协议。
    对于字节流来说,可以简单的理解为,传输数据是基于IO流,
    流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,
    可以多次发送,也可以分开多次接收。

  • 数据报套接字:
    使用传输层UDP协议。
    UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
    对于数据报来说,可以简单的理解为,传输数据是一块一块的,
    发送一块数据假如100个字节,必须一 次发送,
    接收也必须一次接收100个字节,
    而不能分100次,每次接收1个字节。

  • 原始套接字
    原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。

2. Java数据报套接字通信模型

对于UDP协议来说,
具有无连接,面向数据报的特征,
即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。

java中使用UDP协议通信,
主要基于 DatagramSocket 类来创建数据报套接字,
并使用 DatagramPacket 作为发送或接收的UDP数据报。

对于一次发送及接收UDP数据报的流程如下:
在这里插入图片描述

以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。
也就是只有请求,没有响应。
对于一个服务端来说,
重要的是提供多个客户端的请求处理及响应,流程如下:

在这里插入图片描述

3. Java流套接字通信模型

在这里插入图片描述

4. Socket编程注意事项

在这里插入图片描述

  • 客户端和服务端:
    开发时,经常是基于一个主机开启两个进程作为客户端和服务端,
    但真实的场景,一般都是不同主机。

  • 注意目的IP和目的端口号,
    标识了一次数据传输时要发送数据的终点主机和进程。

  • Socket编程是使用流套接字和数据报套接字,
    基于传输层的TCP或UDP协议,
    但应用层协议,也需要考虑。

  • 端口被占用的问题
    如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,
    这种情况也叫端口被占用。

    对于java进程来说,端口被占用的常见报错信息如下:
    在这里插入图片描述
    此时需要检查进程B绑定的是哪个端口,
    再查看该端口被哪个进程占用。
    以下为通过端口号查进程的方式:

    1. 在cmd输入 netstat -ano | findstr 端口号,则可以显示对应进程的pid。如以下命令显
      示了8888进程的pid在这里插入图片描述

    2. 在任务管理器中,通过pid查找进程。
      在这里插入图片描述
      如果占用端口的进程A不需要运行,
      就可以关闭A后,再启动需要绑定该端口的进程B。

      如果需要运行A进程,则可以修改进程B的绑定端口,
      换为其他没有使用的端口。

四 、 UDP数据报套接字编程

1. DatagramSocket API

DatagramSocket 是UDP Socket用于发送和接收UDP数据报。

  • DatagramSocket 构造方法:
    在这里插入图片描述
  • DatagramSocket 方法:

在这里插入图片描述

2. DatagramPacket API

DatagramPacket是UDP Socket发送和接收的数据报。

  • DatagramPacket 构造方法:
    在这里插入图片描述
  • DatagramPacket 方法:
    在这里插入图片描述

3. InetSocketAddress API

构造UDP发送的数据报时,需要传入 SocketAddress ,
该对象可以使用SocketAddress 的子类 InetSocketAddress 来创建。

在这里插入图片描述

4. 示例:一发一收(无响应)

以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),
即只有客户端请求,但没有服务端响应的示例:

// UDP服务端 package org.example.udp.demo1;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Arrays;public class UdpServer {//服务器socket要绑定固定的端口private static final int PORT = 8888;public static void main(String[] args) throws IOException {// 1.创建服务端DatagramSocket,指定端口,可以发送及接收UDP数据报DatagramSocket socket = new DatagramSocket(PORT);//不停的接收客户端udp数据报while (true){// 2.创建数据报,用于接收客户端发送的数据byte[] bytes = new byte[1024];//1m=1024kb, 1kb=1024byte, UDP最多
64k(包含UDP首部8byte)DatagramPacket packet = new DatagramPacket(bytes, bytes.length);System.out.println("---------------------------------------------------");System.out.println("等待接收UDP数据报...");// 3.等待接收客户端发送的UDP数据报,该方法在接收到数据报之前会一直阻塞,接收到数
据报以后,DatagramPacket对象,包含数据(bytes)和客户端ip、端口号socket.receive(packet);System.out.printf("客户端IP:%s%n", packet.getAddress().getHostAddress());System.out.printf("客户端端口号:%s%n", packet.getPort());System.out.printf("客户端发送的原生数据为:%s%n", Arrays.toString(packet.getData()));System.out.printf("客户端发送的文本数据为:%s%n", new String(packet.getData()));}}
}

运行后,服务端就启动了,控制台输出如下:

---------------------------------------------------
等待接收UDP数据报...// 此时代码是阻塞等待在 socket.receive(packet) 代码行,
// 直到接收到一个UDP数据报。
// UDP客户端 package org.example.udp.demo1;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;public class UdpClient {// 服务端socket地址,包含域名或IP,及端口号private static final SocketAddress ADDRESS = new 
InetSocketAddress("localhost", 8888);public static void main(String[] args) throws IOException {// 4.创建客户端DatagramSocket,开启随机端口就行,可以发送及接收UDP数据报DatagramSocket socket = new DatagramSocket();// 5-1.准备要发送的数据byte[] bytes = "hello world!".getBytes();// 5-2.组装要发送的UDP数据报,包含数据,及发送的服务端信息(服务器IP+端口号)DatagramPacket packet = new DatagramPacket(bytes, bytes.length, ADDRESS);// 6.发送UDP数据报socket.send(packet);}
}

客户端启动后会发送一个"hello world!" 的字符串到服务端,
在服务端接收后,控制台输出内容如下:

---------------------------------------------------
等待接收UDP数据报...
客户端IP:127.0.0.1
客户端端口号:57910
客户端发送的原生数据为:[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 
0, 0, 0, ...此处省略很多0]
客户端发送的文本数据为:hello world!                                                 ---------------------------------------------------
等待接收UDP数据报...

假设发送的数据字节数组长度为M,假设接收的数据字节数组长度为N,
从以上可以看出,发送的UDP数据报在接收到以后:

  1. 如果N>M,则接收的byte[]字节数组中会有很多初始化byte[]的初始值0,转换为字符串就是空白字符;

  2. 如果N<M,则会发生数据部分丢失(可以自己尝试,把接收的字节数组长度指定为比发送的字节数组长度更短)。

    要解决以上问题,就需要发送端和接收端双方约定好一致的协议,如规定好结束的标识或整个数据的长度。

5. 示例:请求响应

  • 以下是对应请求和响应的改造:
    (1)客户端先接收键盘输入,表示要展示的相对路径(相对BASE_PATH的路径)。

    (2)发送请求:将该相对路径作为数据报发送到服务端。

    (3)服务端接收并处理请求:根据该请求数据,作为本地目录的路径,列出下一级子文件及子文件夹。

    (4)服务端返回响应:遍历子文件和子文件夹,每个文件名一行,作为响应的数据报,返回给客户端。

    (5)客户端接收响应:简单的打印输出所有的响应内容,即文件列表。

  • 交互执行流程:

在这里插入图片描述

// UDP服务端 package org.example.udp.demo2;import java.io.File;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;public class UdpServer {//服务器socket要绑定固定的端口private static final int PORT = 8888;//本地文件目录要展示的根路径private static final String BASE_PATH = "E:/TMP";public static void main(String[] args) throws IOException {// 1.创建服务端DatagramSocket,指定端口,可以发送及接收UDP数据报DatagramSocket socket = new DatagramSocket(PORT);//不停的接收客户端udp数据报while (true){// 2.创建数据报,用于接收客户端发送的数据byte[] requestData = new byte[1024];//1m=1024kb, 1kb=1024byte, UDP最
多64k(包含UDP首部8byte)DatagramPacket requestPacket = new DatagramPacket(requestData, 
requestData.length);System.out.println("------------------------------------------------
---");System.out.println("等待接收UDP数据报...");// 3.等待接收客户端发送的UDP数据报,该方法在接收到数据报之前会一直阻塞,接收到数
据报以后,DatagramPacket对象,包含数据(bytes)和客户端ip、端口号socket.receive(requestPacket);System.out.printf("客户端IP:%s%n", 
requestPacket.getAddress().getHostAddress());System.out.printf("客户端端口号:%s%n", requestPacket.getPort());// 7.接收到的数据作为请求,根据请求数据执行业务,并返回响应for (int i = 0; i < requestData.length; i++) {byte b = requestData[i];if(b == '\3') {// 7-1.读取请求的数据:读取到约定好的结束符(\3),取结束符之前的内容String request = new String(requestData, 0, i);// 7-2.根据请求处理业务:本地目录根路径+请求路径,作为要展示的目录,列
出下一级子文件//请求的文件列表目录System.out.printf("客户端请求的文件列表路径为:%s%n", BASE_PATH + 
request);File dir = new File(BASE_PATH + request);//获取下一级子文件,子文件夹File[] children = dir.listFiles();// 7-3.构造要返回的响应内容:每个文件及目录名称为一行StringBuilder response = new StringBuilder();if(children != null){for (File child : children) {response.append(child.getName()+"\n");}}//响应也要约定结束符response.append("\3");byte[] responseData = 
response.toString().getBytes(StandardCharsets.UTF_8);// 7-4.构造返回响应的数据报DatagramPacket,注意接收的客户端数据报包
含IP和端口号,要设置到响应的数据报中DatagramPacket responsePacket = new 
DatagramPacket(responseData, responseData.length, 
requestPacket.getSocketAddress());// 7-5.发送返回响应的数据报socket.send(responsePacket);break;}}}}
}

以上服务端运行结果和示例一是一样的:

---------------------------------------------------
等待接收UDP数据报...
// UDP客户端 package org.example.udp.demo2;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;public class UdpClient {// 服务端socket地址,包含域名或IP,及端口号private static final SocketAddress ADDRESS = new 
InetSocketAddress("localhost", 8888);public static void main(String[] args) throws IOException {// 4.创建客户端DatagramSocket,开启随机端口就行,可以发送及接收UDP数据报DatagramSocket socket = new DatagramSocket();// 5-1.准备要发送的数据:这里调整为键盘输入作为发送的内容Scanner scanner = new Scanner(System.in);while(true){System.out.println("---------------------------------------------------");System.out.println("请输入要展示的目录:");// 5-2.每输入新行(回车),就作为UDP发送的数据报,为了接收端获取有效的内容(去除空字符串),约定\3为结束String request = scanner.nextLine() + "\3";byte[] requestData = request.getBytes(StandardCharsets.UTF_8);// 5-3.组装要发送的UDP数据报,包含数据,及发送的服务端信息(服务器IP+端口号)DatagramPacket requestPacket = new DatagramPacket(requestData, requestData.length, ADDRESS);// 6.发送UDP数据报socket.send(requestPacket);// 8.接收服务端响应的数据报,并根据响应内容决定下个步骤(我们这里简单的打印即可)// 8-1.创建数据报,用于接收服务端返回(发送)的响应byte[] responseData = new byte[1024];DatagramPacket responsePacket = new DatagramPacket(responseData, responseData.length);// 8-2.接收响应数据报socket.receive(responsePacket);System.out.println("该目录下的文件列表为:");// byte[]下次解析的起始位置int next = 0;for (int i = 0; i < responseData.length; i++) {byte b = responseData[i];if(b == '\3')//结束符退出break;if(b == '\n'){//换行符时进行解析//起始位置到换行符前一个索引位置为要解析的内容String fileName = new String(responseData, next, i-next);System.out.println(fileName);//下次解析从换行符后一个索引开始next = i+1;}}}}
}

客户端启动后会等待输入要展示的路径:

---------------------------------------------------
请输入要展示的目录:

在输入想查看的目录路径后,会接收并打印服务端响应的文件列表数据:

---------------------------------------------------
请输入要展示的目录:
/
该目录下的文件列表为:
1
2
60441b1b8a74be3695ccc0d970693815
8f25103aa249707ee4ab17635142cd0e
---------------------------------------------------
请输入要展示的目录:

此时服务端也会打印接收到的客户端请求数据:

---------------------------------------------------
等待接收UDP数据报...
客户端IP:127.0.0.1
客户端端口号:57297
客户端请求的文件列表路径为:E:/TMP/
---------------------------------------------------
等待接收UDP数据报...

五 、 TCP流套接字编程

1. ServerSocket API

ServerSocket 是创建TCP服务端Socket的API。

  • ServerSocket 构造方法:
    在这里插入图片描述
  • ServerSocket 方法:
    在这里插入图片描述

2. Socket API

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务Socket。

不管是客户端还是服务端Socket,
都是双方建立连接以后,保存的对端信息,
及用来与对方收发数据的。

  • Socket 构造方法:
    在这里插入图片描述

  • Socket 方法:

在这里插入图片描述

3. TCP中的长短连接

TCP发送数据时,需要先建立连接,
什么时候关闭连接就决定是短连接还是长连接。

  • 短连接:
    每次接收到数据并返回响应后,都关闭连接,
    即是短连接。
    也就是说,短连接只能一次收发数据。

  • 长连接:
    不关闭连接,一直保持连接状态,双方不停的收发数据,
    即是长连接。
    也就是说,长连接可以多次收发数据。

  • 对比以上长短连接,两者区别如下:

    • 建立连接、关闭连接的耗时:
      短连接每次请求、响应都需要建立连接,关闭连接;
      而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。
      相对来说建立连接,关闭连接也是要耗时
      的,长连接效率更高。

    • 主动发送请求不同:
      短连接一般是客户端主动向服务端发送请求;
      而长连接可以是客户端主动发送请求,也可以是服务端主动发。

    • 两者的使用场景有不同:
      短连接适用于客户端请求频率不高的场景,如浏览网页等。
      长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等

基于BIO(同步阻塞IO)的长连接会一直占用系统资源。
对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。

由于每个连接都需要不停的阻塞等待接收数据,
所以每个连接都会在一个线程中运行。

一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:
一直不关闭连接,不停的处理请求。

实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。

4. 示例:一发一收(短连接)

以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),
即只有客户端请求,但没有服务端响应的示例:

// TCP服务端 package org.example.tcp.demo1;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;public class TcpServer {//服务器socket要绑定固定的端口private static final int PORT = 8888;public static void main(String[] args) throws IOException {// 1.创建一个服务端ServerSocket,用于收发TCP报文ServerSocket server = new ServerSocket(PORT);// 不停的等待客户端连接while(true) {System.out.println("---------------------------------------------------");System.out.println("等待客户端建立TCP连接...");// 2.等待客户端连接,注意该方法为阻塞方法Socket client = server.accept();System.out.printf("客户端IP:%s%n", client.getInetAddress().getHostAddress());System.out.printf("客户端端口号:%s%n", client.getPort());// 5.接收客户端的数据,需要从客户端Socket中的输入流获取System.out.println("接收到客户端请求:");InputStream is = client.getInputStream();// 为了方便获取字符串内容,可以将以上字节流包装为字符流BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));String line;// 一直读取到流结束:TCP是基于流的数据传输,一定要客户端关闭Socket输出流才表示服务端接收IO输入流结束while ((line = br.readLine()) != null) {System.out.println(line);}// 6.双方关闭连接:服务端是关闭客户端socket连接client.close();}}
}

运行后,服务端就启动了,控制台输出如下:

---------------------------------------------------
等待客户端建立TCP连接...

可以看出,此时代码是阻塞等待在 server.accept() 代码行,
直到有新的客户端申请建立连接。

// TCP客户端 package org.example.tcp.demo1;import java.io.*;
import java.net.Socket;public class TcpClient {//服务端IP或域名private static final String SERVER_HOST = "localhost";//服务端Socket进程的端口号private static final int SERVER_PORT = 8888;public static void main(String[] args) throws IOException {// 3.创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接Socket client = new Socket(SERVER_HOST, SERVER_PORT);// 4.发送TCP数据,是通过socket中的输出流进行发送OutputStream os = client.getOutputStream();// 为了方便输出字符串作为发送的内容,可以将以上字节流包装为字符流PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));// 4-1.发送数据:pw.println("hello world!");// 4-2.有缓冲区的IO操作,真正传输数据,需要刷新缓冲区pw.flush();// 7.双方关闭连接:客户端关闭socket连接client.close();}
}

客户端启动后会发送一个"hello world!" 的字符串到服务端,
在服务端接收后,控制台输出内容如下:

---------------------------------------------------
等待客户端建立TCP连接...
客户端IP:127.0.0.1
客户端端口号:51118
接收到客户端请求:
hello world!
---------------------------------------------------
等待客户端建立TCP连接...

以上客户端与服务端建立的为短连接,每次客户端发送了TCP报文,及服务端接收了TCP报文后,双方
都会关闭连接。

5. 示例: 请求响应(短连接)

构造一个展示服务端本地某个目录(BASE_PATH)的下一级子文件列表的服务:

  1. 客户端先接收键盘输入,表示要展示的相对路径(相对BASE_PATH的路径)。
  2. 发送请求:
    使用客户端Socket的输出流发送TCP报文。即输入的相对路径。
  3. 服务端接收并处理请求:
    使用服务端Socket的输入流来接收请求报文,根据请求的路径,列出下一级子文件及子文件夹。
  4. 服务端返回响应:
    使用服务端Socket的输出流来发送响应报文。
    即遍历子文件和子文件夹,每个文件名一行,返回给客户端。
  5. 客户端接收响应:
    使用客户端Socket的输入流来接收响应报文。
    简单的打印输出所有的响应内容,即文件列表。
// TCP服务端 package org.example.tcp.demo2;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;public class TcpServer {//服务器socket要绑定固定的端口private static final int PORT = 8888;//本地文件目录要展示的根路径private static final String BASE_PATH = "E:/TMP";public static void main(String[] args) throws IOException {// 1.创建一个服务端ServerSocket,用于收发TCP报文ServerSocket server = new ServerSocket(PORT);// 不停的等待客户端连接while(true) {System.out.println("---------------------------------------------------");System.out.println("等待客户端建立TCP连接...");// 2.等待客户端连接,注意该方法为阻塞方法Socket socket = server.accept();System.out.printf("客户端IP:%s%n", socket.getInetAddress().getHostAddress());System.out.printf("客户端端口号:%s%n", socket.getPort());// 5.接收客户端的数据,需要从客户端Socket中的输入流获取InputStream is = socket.getInputStream();// 为了方便获取字符串内容,可以将以上字节流包装为字符流BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));// 客户端请求只发送一行数据,我们也只需要读取一行String request = br.readLine();// 6.根据请求处理业务:本地目录根路径+请求路径,作为要展示的目录,列出下一级子文件//请求的文件列表目录System.out.printf("客户端请求的文件列表路径为:%s%n", BASE_PATH + request);File dir = new File(BASE_PATH + request);//获取下一级子文件,子文件夹File[] children = dir.listFiles();// 7.返回响应给客户端:通过客户端socket中的输出流发送响应数据OutputStream os = socket.getOutputStream();// 为了方便输出字符串作为发送的内容,可以将以上字节流包装为字符流PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));// 7-1.返回的响应内容:每个文件及目录名称为一行if(children != null){for (File child : children) {pw.println(child.getName());}}// 7-2.有缓冲区的IO操作,真正传输数据,需要刷新缓冲区pw.flush();// 7-3.双方关闭连接:服务端是关闭客户端socket连接socket.close();}}
}

服务端运行结果:

---------------------------------------------------
等待客户端建立TCP连接...
// TCP客户端 package org.example.tcp.demo2;import java.io.*;
import java.net.Socket;
import java.util.Scanner;public class TcpClient {//服务端IP或域名private static final String SERVER_HOST = "localhost";//服务端Socket进程的端口号private static final int SERVER_PORT = 8888;public static void main(String[] args) throws IOException {// 准备要发送的数据:这里调整为键盘输入作为发送的内容Scanner scanner = new Scanner(System.in);while(true) {System.out.println("---------------------------------------------------");System.out.println("请输入要展示的目录:");// 每输入新行(回车),就作为发送的TCP请求报文String request = scanner.nextLine();// 3.创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接Socket socket = new Socket(SERVER_HOST, SERVER_PORT);// 4.发送TCP数据,是通过socket中的输出流进行发送OutputStream os = socket.getOutputStream();// 为了方便输出字符串作为发送的内容,可以将以上字节流包装为字符流PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));// 4-1.发送数据:pw.println(request);// 4-2.有缓冲区的IO操作,真正传输数据,需要刷新缓冲区pw.flush();// 8.接收返回的响应数据:通过socket中的输入流获取System.out.println("接收到服务端响应:");InputStream is = socket.getInputStream();// 为了方便获取字符串内容,可以将以上字节流包装为字符流BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));String line;// 一直读取到流结束:TCP是基于流的数据传输,一定要服务端关闭Socket输出流才表示客户端接收的IO输入流结束while ((line = br.readLine()) != null) {System.out.println(line);}// 9.双方关闭连接:客户端关闭socket连接socket.close();}}
}

客户端启动后会等待输入要展示的路径:

---------------------------------------------------
请输入要展示的目录:

在输入想查看的目录路径后,会接收并打印服务端响应的文件列表数据:

---------------------------------------------------
请输入要展示的目录:
/
接收到服务端响应:
1
2
60441b1b8a74be3695ccc0d970693815
8f25103aa249707ee4ab17635142cd0e
---------------------------------------------------
请输入要展示的目录:

此时服务端也会打印接收到的客户端请求数据:

---------------------------------------------------
等待客户端建立TCP连接...
客户端IP:127.0.0.1
客户端端口号:52493
客户端请求的文件列表路径为:E:/TMP/
---------------------------------------------------
等待客户端建立TCP连接...

目前TCP客户端和服务端实现的功能和UDP差不多,但都存在几个问题:
对于服务端来说,处理一次请求并返回响应后,才能再次处理下一次请求和响应,效率是比较低
的。
这个问题比较好解决:可以使用多线程,每次的请求与响应都在线程中处理。
这样多个客户端请求的话,可以在多个线程中并发并行的执行。
服务端解析请求,是只读取了一行,而客户端解析响应,是一直读取到流结束。

目前的业务,双方都已约定好业务是展示目录下的文件列表,且都只需要一种数据:
请求传输的数据代表要展示的目录;响应传输的数据代表文件列表:每一行为一个文件名。

如要提供更多的业务,如文件重命名,文件删除等操作时,就不能了。此时就需要提供更多的字段来标识。一般我们需要设计更强大的协议。

六 、自定义协议

除了UDP和TCP协议外,程序还存在应用层自定义协议。

对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式:

客户端发送请求和服务端解析请求要使用相同的数据格式。

服务端返回响应和客户端解析响应也要使用相同的数据格式。

请求格式和响应格式可以相同,也可以不同。

约定相同的数据格式,主要目的是为了让接收端在解析的时候明确如何解析数据中的各个字段。

可以使用知名协议(广泛使用的协议格式),
如果想自己约定数据格式,就属于自定义协议。

1. 封装/分用 & 序列化/反序列化

一般来说,在网络数据传输中,发送端应用程序,
发送数据时的数据转换(如java一般就是将对象转换为某种协议格式),
即对发送数据时的数据包装动作来说:

如果是使用知名协议,这个动作也称为封装
如果是使用小众协议(包括自定义协议),这个动作也称为序列化,一般是将程序中的对象转换为特定的数据格式。

接收端应用程序,接收数据时的数据转换,
即对接收数据时的数据解析动作来说:

如果是使用知名协议,这个动作也称为分用。

如果是使用小众协议(包括自定义协议),这个动作也称为反序列化,一般是基于接收数据特定的格式,转换为程序中的对象

2. 如何设计协议

对于协议来说,重点需要约定好如何解析,
一般是根据字段的特点来设计协议:

  • 对于定长的字段:
    可以基于长度约定,如int字段,约定好4个字节即可。

  • 对于不定长的字段:
    可以约定字段之间的间隔符,或最后一个字段的结束符,如换行符间隔,\3符号结束等等。

除了该字段“数据”本身,再加一个长度字段,用来标识该“数据”长度;即总共使用两个字段:

  1. “数据”字段本身,不定长,需要通过“长度”字段来解析;
  2. “长度”字段,标识该“数据”的长度,即用于辅助解析“数据”字段。

3. 多线程+自定义协议

  • 以下我们将上个的业务做以下扩展:

    提供多种操作:
    展示目录下文件列表,文件重命名,删除文件,上传文件,下载文件。

在不同的操作中,需要抽象出请求和响应的字段,也即是说,要约定客户端服务端统一的请求协议,同时也要约定服务端与客户端统一的响应协议。

  • 本示例中的自定义协议

    以下为我们TCP请求数据的协议格式,这里简单起见,约定为换行符及结束符:

    请求类型
    操作的文件或目录路径
    数据\3

    说明如下:

    以上总共包含3个字段,前2个字段需要按换行符读取,最后一个字段需要按结束符读取。

    请求类型标识是什么操作:展示目录下文件列表,文件重命名,删除文件,上传文件,下载文件。

    重命名、上传文件操作,需要“数据”字段,其他操作可以置为空字符串。

    “数据”字段为最后一个字段,使用\3结束符,这样在数据本身有换行符也能正确处理。

  • 以下为响应数据的协议格式:
    状态码(标识是否操作成功)
    数据(展示列表时,返回目录下的文件列表,或下载文件的数据)\3

    以下为展示文件列表操作的自定义协议(请求、响应格式)在这里插入图片描述

  • 请求数据格式如下:

1
/
\3
  • 响应数据格式如下:
200
\1
\2
\3
\1.txt
\2.txt\3
  • 以下为上传文件操作的自定义协议(请求、响应格式)
    需要先在客户端指定上传的服务端目录,
    及客户端要上传的文件路径,以下操作将会把客户端 Main.java 文件内容上传到服务端根目录 E:/TMP 下的 /1 目录下:在这里插入图片描述

  • 请求数据格式如下:

4
/1
package org.example;public class Main {
……略
}\3
  • 响应数据格式如下:
200
\3
  • 执行流程
    约定好请求和响应的数据格式,也就是应用层协议,
    大家按照约定好的格式来发送和接收,以下为执行
    流程。
    在这里插入图片描述
//  请求类 
先按照约定的请求协议封装请求类:
每个字段为一个属性:操作类型,操作路径,数据
完成服务端解析请求封装:按约定的方式读,先按行读取前2个字段,再按结束符读第3个字段
完成客户端发送请求封装:按约定的方式写,前2个字段按行输出,第3个字段以\3结束package org.example.tcp.demo3;import java.io.*;
import java.util.ArrayList;
import java.util.List;public class Request {//操作类型:1(展示目录文件列表),2(文件重命名),3(删除文件),4(上传文件),5(下载
文件)private Integer type;//操作的目录路径private String url;//数据private String data;//服务端解析请求时:根据约定好的格式来解析public static Request serverParse(InputStream is) throws IOException {BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-
8"));Request request = new Request();//前2行分别为操作类型和操作路径request.type = Integer.parseInt(br.readLine());request.url = br.readLine();//使用list保存字符List<Character> list = new ArrayList<>();//数据:循环读取while(true){//一个字符一个字符的读char c = (char) br.read();//一直读取到结束符\3if(c == '\3')break;list.add(c);}//拼接数据StringBuilder sb = new StringBuilder();for (char c  : list){sb.append(c);}request.data = sb.toString();return request;}//客户端发送请求到服务端public void clientWrite(OutputStream os) throws IOException {PrintWriter pw = new PrintWriter(os);pw.println(type);pw.println(url);pw.write(data+"\3");// 4-2.有缓冲区的IO操作,真正传输数据,需要刷新缓冲区pw.flush();}@Overridepublic String toString() {return "Request{" +"type=" + type +", url='" + url + '\'' +", data='" + data + '\'' +'}';}public Integer getType() {return type;}public void setType(Integer type) {this.type = type;}public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public String getData() {return data;}public void setData(String data) {this.data = data;}
}
// 响应类 
按照约定的响应协议封装响应类:
每个字段为一个属性:响应状态码
完成客户端解析响应封装:按约定的方式读,先按行读取第1个字段,再按结束符读第2个字段
完成服务端发送响应封装:按约定的方式写,第1个字段按行输出,第2个字段以\3结束package org.example.tcp.demo3;import java.io.*;
import java.util.ArrayList;
import java.util.List;public class Response {//响应的状态码,200表示操作成功,404表示没有找到该路径的文件或目录private int status;//数据private String data;//客户端解析服务端返回的响应数据public static Response clientParse(InputStream is) throws IOException {BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-
8"));Response response = new Response();response.status = Integer.parseInt(br.readLine());//使用list保存字符List<Character> list = new ArrayList<>();//数据:循环读取while(true){//一个字符一个字符的读char c = (char) br.read();//一直读取到结束符\3if(c == '\3')break;list.add(c);}//拼接数据StringBuilder sb = new StringBuilder();for (char c  : list){sb.append(c);}response.data = sb.toString();return response;}//服务端返回响应给客户端public void serverWrite(OutputStream os) throws IOException {PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));pw.println(status);pw.write(data+"\3");// 4-2.有缓冲区的IO操作,真正传输数据,需要刷新缓冲区pw.flush();}@Overridepublic String toString() {return "Response{" +"status=" + status +", data='" + data + '\'' +'}';}public int getStatus() {return status;}public void setStatus(int status) {this.status = status;}public String getData() {return data;}public void setData(String data) {this.data = data;}
}
//  TCP服务端 以下完成服务端代码:
ServerSocket.accept() 为建立客户端服务端连接的方法,为提高效率,使用多线程。先要解析请求数据,即 Request 已封装好的服务端解析请求,返回 Request 对象。
返回响应数据,需要根据不同的请求字段,做不同的业务处理,并返回对应的响应内容。
如果操作的url路径再服务端根目录 E:/TMP 下找不到,则返回响应状态码404。
正常执行完,返回200响应状态码;要注意根据不同操作类型来执行不同的业务。package org.example.tcp.demo3;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Files;
import java.util.UUID;public class TcpServer {//服务器socket要绑定固定的端口private static final int PORT = 8888;//本地文件目录要展示的根路径private static final String BASE_PATH = "E:/TMP";public static void main(String[] args) throws IOException {// 1.创建一个服务端ServerSocket,用于收发TCP报文ServerSocket server = new ServerSocket(PORT);// 不停的等待客户端连接while(true) {// 2.等待客户端连接,注意该方法为阻塞方法Socket socket = server.accept();new Thread(new Runnable() {@Overridepublic void run() {try {System.out.println("------------------------------------
---------------");System.out.println("客户端建立TCP连接...");System.out.printf("客户端IP:%s%n", 
socket.getInetAddress().getHostAddress());System.out.printf("客户端端口号:%s%n", socket.getPort());// 5.接收客户端的数据,需要从客户端Socket中的输入流获取InputStream is = socket.getInputStream();// 解析为请求对象Request request = Request.serverParse(is);System.out.println("服务端收到请求:"+request);// 6.根据请求处理业务:处理完成返回响应对象Response response = build(request);// 7.返回响应给客户端:通过客户端socket中的输出流发送响应数据OutputStream os = socket.getOutputStream();// 7-1.返回的响应内容:按照约定格式输出响应对象中的内容System.out.println("服务端返回响应:"+response);response.serverWrite(os);// 7-3.双方关闭连接:服务端是关闭客户端socket连接socket.close();} catch (IOException e) {e.printStackTrace();}}}).start();}}//根据请求处理业务,返回响应对象public static Response build(Request request){Response response = new Response();response.setStatus(200);File url = new File(BASE_PATH+request.getUrl());//该路径的文件或目录不存在if(!url.exists()){response.setStatus(404);response.setData("");return response;}try {switch (request.getType()){//1展示目录文件列表case 1: {File[] children = url.listFiles();if(children == null){response.setData("");}else{//拼接要返回的数据:文件列表StringBuilder sb = new StringBuilder();for (int i = 0; i < children.length; i++) {File child = children[i];//文件路径截取掉服务端本地路径前缀sb.append(child.getAbsolutePath().substring(BASE_PATH.length())+"\n");}response.setData(sb.toString());}break;}//2文件重命名case 2: {url.renameTo(new 
File(url.getParent()+File.separator+request.getData()));break;}//3删除文件case 3: {url.delete();break;}//上传文件case 4: {//上传到请求的操作路径目录下,保存的文件名简单的以随机字符串uuid生成即
可FileWriter upload = new 
FileWriter(url.getAbsolutePath()+File.separator+ UUID.randomUUID());upload.write(request.getData());upload.flush();upload.close();break;}//下载文件case 5: {String data = new String(Files.readAllBytes(url.toPath()));response.setData(data);break;}}} catch (IOException e) {e.printStackTrace();}return response;}
}
// TCP客户端 以下为客户端代码:
先要建立和服务端的连接,连接服务端的IP和端口
根据输入来构建请求数据:
先接收操作类型和操作路径
重命名操作时,需要指定修改的文件名
文件上传操作时,需要指定上传的客户端本地文件路径
解析响应数据,并根据响应来执行相应的业务,我们这里暂时简单的解析为 Response 对象,并打印即可。package org.example.tcp.demo3;import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Scanner;public class TcpClient {//服务端IP或域名private static final String SERVER_HOST = "localhost";//服务端Socket进程的端口号private static final int SERVER_PORT = 8888;public static void main(String[] args) throws IOException {// 准备要发送的数据:这里调整为键盘输入作为发送的内容Scanner scanner = new Scanner(System.in);while(true) {//根据键盘输入构造一个请求对象,包含操作类型,操作路径,长度和数据Request request = build(scanner);// 3.创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接Socket socket = new Socket(SERVER_HOST, SERVER_PORT);// 4.发送TCP数据,是通过socket中的输出流进行发送OutputStream os = socket.getOutputStream();// 4-1.发送请求数据:按照约定的格式输出请求对象中的内容System.out.println("客户端发送请求:"+request);request.clientWrite(os);// 8.接收返回的响应数据:通过socket中的输入流获取InputStream is = socket.getInputStream();// 根据约定的格式获取响应数据Response response = Response.clientParse(is);System.out.println("客户端收到响应:"+response);// 9.双方关闭连接:客户端关闭socket连接socket.close();}}//客户端发送请求时,根据键盘输入构造一个请求对象public static Request build(Scanner scanner) throws IOException {System.out.println("---------------------------------------------------
");System.out.println("请输入要操作的类型:1(展示目录文件列表),2(文件重命名),
3(删除文件),4(上传文件),5(下载文件)");Request request = new Request();int type = Integer.parseInt(scanner.nextLine());System.out.println("请输入要操作的路径:");String url = scanner.nextLine();String data = "";//只需要操作类型和操作路径的请求,长度和数据构造为空的if(type == 2){//重命名操作,需要输入重命名的名称System.out.println("请输入要重命名的名称:");data = scanner.nextLine();}else if(type == 4){//文件上传,需要提供上传的文件路径System.out.println("请输入要上传的文件路径:");String upload = scanner.nextLine();data = new String(Files.readAllBytes(Paths.get(upload)));}else if(type != 1 && type !=3 && type!=5){System.out.println("只能输入1-5的数字,请重新输入");return build(scanner);}request.setType(type);request.setUrl(url);request.setData(data);return request;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/140770.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

汽车电子——产品标准规范汇总和梳理(车载网络)

文章目录 前言 一、菊花链 二、K Line 三、L Line 四、RS485 五、LIN 六、CAN 七、FlexRay 八、MOST 九、Bluetooth 十、LAN 十一、移动网络 十二、实施和测试 总结 前言 见《汽车电子——产品标准规范汇总和梳理》 一、菊花链 暂无统一的正式标准。 菊花链通信&…

Linux查看系统信息

# 查看操作系统的详细信息 uname -a# 查看已安装的Linux发行版信息 cat /etc/os-release# 查看Linux Standard Base (LSB)的信息 lsb_release -a# 查看主机的信息 hostnamectl# 查看文件系统的磁盘空间使用情况 df -h# 查看系统内存的使用情况 free -h# 查看网络接口的信息 ifc…

[React] react-hooks如何使用

react-hooks思想和初衷&#xff0c;也是把组件&#xff0c;颗粒化&#xff0c;单元化&#xff0c;形成独立的渲染环境&#xff0c;减少渲染次数&#xff0c;优化性能。 文章目录 1.为什么要使用hooks2.如何使用hooks2.1 useState2.2 useEffect2.3 useLayoutEffect2.4 useRef2.5…

【网络编程】TCP Socket编程

TCP Socket编程 1. ServerSocket2. Socket3. TCP的长短连接4. Socket 通信模型5. 代码示例&#xff1a;TCP 回显服务器 流套接字&#xff1a; 使用传输层TCP协议 TCP: 即Transmission Control Protocol&#xff08;传输控制协议&#xff09;&#xff0c;传输层协议。 TCP的特点…

【计算机网络】IP协议(下)

文章目录 1. 特殊的IP地址2. IP地址的数量限制3. 私有IP地址和公网IP地址私有IP为什么不能出现在公网上&#xff1f;解决方案——NAT技术的使用 4. 路由5. IP分片问题为什么要进行切片&#xff1f;如何做的分片和组装&#xff1f;16位标识3位标志13位片偏移例子 细节问题如何区…

基于springboot地方废物回收机构管理系统springboot11

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

智慧农业农场小程序源码 智慧农场系统源码

智慧农业农场小程序源码 智慧农场系统源码 一、 智慧农场系统的组成 智慧农场系统一般包括传感器、控制器、数据采集与处理平台、应用软件等组成部分。其中, 传感器主要用于采集土壤温度、湿度、光照强度等环境参数,以及作物生长状态、水肥情况等生产信息。控制器则根据传感器…

GLTF编辑器的另一个作用

1、GLB模型介绍 GLB&#xff08;GLTF Binary&#xff09;是一种用于表示三维模型和场景的文件格式。GLTF是"GL Transmission Format"的缩写&#xff0c;是一种开放的、跨平台的标准&#xff0c;旨在在各种3D图形应用程序和引擎之间进行交换和共享。 GLB文件是GLTF文件…

PyCharm 手动下载插件

插件模块一直加载失败&#xff0c;报错信息&#xff1a; Marketplace plugins are not loaded. Check the internet connection and refresh. 尝试了以下方法&#xff0c;均告失败&#xff1a; pip 换源Manage Plugin Repositories...HTTP 代理设置...关闭三个防火墙 最后选…

RK3568平台开发系列讲解(工具命令篇)ADB的安装

🚀返回专栏总目录 文章目录 一、ADB介绍二、Windows 下安装 adb 工具沉淀、分享、成长,让自己和他人都能有所收获!😄 一、ADB介绍 adb 全称 Android Debug Bridge,直译过来就是 Android 调试桥,它是一个通用的命令行工具。adb 做为 Android 设备与 PC 端连接的一个桥梁…

MissionPlanner编译过程

环境 windows 10 mission planner 1.3.80 visual studio 2022 git 2.22.0 下载源码 (已配置git和ssh) 从github上克隆源码 git clone gitgithub.com:ArduPilot/MissionPlanner.git进入根目录 cd MissionPlanner在根目录下的ExtLibs文件下是链接的其它github源码&#xff0…

MySQL 高级(进阶) SQL 语句(二) -----存储过程

目录 1 存储过程 1.1 创建存储过程​ 1.2 调用存储过程 1.3 查看存储过程 1.4 存储过程的参数 1.5 修改存储过程 1.6 删除存储过程 2 条件语句 3 循环语句 1 存储过程 存储过程是一组为了完成特定功能的SQL语句集合。 存储过程在使用过程中是将常用或者复杂的工作预…

ClickHouse分布式集群部署

目录 ​编辑 一、环境说明 二、安装部署 2.1 RPM方式安装 2.1.1 安装yum-utils 2.1.2 配置yum repo源 2.1.3 yum install 下载安装clickhouse 2.2 信息配置 2.2.1 配置外网可访问地址 2.2.2 修改存储路径 2.2.2.1 新建存储目录 2.2.2.2 授权 2.2.2.3 修改配置 2.…

单片机第三季-第三课:STM32开发板原理图、配置、浮点运算单元

目录 1&#xff0c;开发板原理图 2&#xff0c;浮点运算单元&#xff08;FPU&#xff09; 1&#xff0c;开发板原理图 课程视频比较早&#xff0c;介绍了三款开发板。观看视频时用的开发板说和51单片机共板的STM32核心板&#xff0c;将51单片机从底座拆下来后&#xff0c;安…

【从0学习Solidity】35. 荷兰拍卖

【从0学习Solidity】35. 荷兰拍卖 博主简介&#xff1a;不写代码没饭吃&#xff0c;一名全栈领域的创作者&#xff0c;专注于研究互联网产品的解决方案和技术。熟悉云原生、微服务架构&#xff0c;分享一些项目实战经验以及前沿技术的见解。关注我们的主页&#xff0c;探索全栈…

黑马JVM总结(十四)

&#xff08;1&#xff09;分代回收_1 Java虚拟机都是结合前面几种算法&#xff0c;让他们协同工作&#xff0c;具体实现是虚拟机里面一个叫做分代的垃圾回收机制&#xff0c;把我们堆内存大的区域划分为两块新生代、老年代 新生代有划分为伊甸园、幸存区Form、幸存区To 为什…

ARMv8 cache的包含策略inclusive 和 exclusive之间的区别以及Cortex-A55示例详解

Inclusive 和 Exclusive 一&#xff0c; 什么是cache的inclusive 和 exclusive二&#xff0c;Inclusive 和 Exclusive cache示例2.1 Inclusive cache2.2 Exclusive cache 三&#xff0c; inclusive cache和 exclusive cache的比较3.1 cache coherency3.2 miss rate3.3 cache ca…

使用 Docker 安装 Elasticsearch (本地环境 M1 Mac)

Elasticsearchkibana下载安装 docker pull elasticsearch:7.16.2docker run --name es -d -e ES_JAVA_OPTS“-Xms512m -Xmx512m” -e “discovery.typesingle-node” -p 9200:9200 -p 9300:9300 elasticsearch:7.16.2docker pull kibana:7.16.2docker run --name kibana -e EL…

最频繁被问到的SQL面试题

面试感叹失败的原因可能有很多&#xff0c;而做成的道路只有⼀条&#xff0c;那就是不断积累。纯手工的8291字的SQL面试题总结分享给初学者&#xff0c;俗称八股文&#xff0c;期待对新手有所帮助。 窗口函数题 窗口函数其实就是根据当前数据, 计算其在所在的组中的统计数据。…

网工基础知识——以太网

1972年Bob Metcalfe“以太网之父”被Xerox雇佣为网络专家&#xff0c;Bob Metcalfe 来到Xerox公司的Palo Alto研究中心&#xff08;PARC&#xff09;的第一个任务是把Palo Alto的计算机连接到ARPANET&#xff08;Internet的前身&#xff09;上。1972年底Bob Metcalfe以ALOHA系统…