网络编程
为什么需要网络编程? ------ 丰富的网络资源
用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资
源。
与本地打开视频文件类似,只是视频文件这个资源的来源是网络。
相比本地资源来说,网络提供了更为丰富的网络资源:
网络资源
视频资源
图片资源
文本资源
文本资源
所谓的 网络资源 ,其实就是在网络中可以获取的各种数据资源。
而所有的网络资源,都是通过网络编程来进行数据传输的。
什么是网络编程
网络编程,指网络上的主机,通过 不同的进程 ,以编程的方式实现 网络通信(或称为网络数据传输) 。 进程 B
主机 2
视频资源
图片资源
文本资源
......
进程 A
主机 1
当然,我们只要满足 进程 不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,
也属于网络编程。
特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编
程。
但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:
进程 A :编程来获取网络资源
进程 B :编程来提供网络资源
网络编程中的基本概念
发送端和接收端
在一次网络数据传输时:
发送端 :数据的 发送方进程 ,称为发送端。发送端主机即网络通信中的源主机。
接收端 :数据的 接收方进程 ,称为接收端。接收端主机即网络通信中的目的主机。
收发端 :发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。 进程 B
主机 2
视频资源
图片资源
文本资源
......
进程 A
主机 1
发送端
发送端
接收端
接收端
请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送
第二次:响应数据的发送。
好比在快餐店点一份炒饭:
先要发起请求:点一份炒饭,再有快餐店提供的对应响应:提供一份炒饭
进程 A
主机 1
进程 B
主机 2
视频资源
图片资源
文本资源
......
发送端
发送端
接收端
接收端
请求
响应
客户端和服务端
服务端 :在常见的网络数据传输场景下,把 提供服务 的一方进程,称为服务端,可以提供对外服务。
客户端 : 获取服务 的一方进程,称为客户端。
对于服务来说,一般是提供:
客户端获取服务资源 客户端进程
客户端主机
服务端进程
服务端主机
视频资源
图片资源
文本资源
......
发送端
发送端
接收端
接收端
请求:获取服务资源
响应:返回服务资源
客户端保存资源在服务端
客户端进程
客户端主机
服务端进程
服务端主机
发送端
发送端
接收端
接收端
请求:保存用户资源
响应:返回处理结果
保
存
好比在银行办事:
银行提供存款服务:用户(客户端)保存资源(现金)在银行(服务端)
银行提供取款服务:用户(客户端)获取服务端资源(银行替用户保管的现金)
常见的客户端服务端模型
最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
- 客户端先发送请求到服务端
- 服务端根据请求数据,执行相应的业务处理
- 服务端返回响应:发送业务处理结果
- 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果) 客户端进程
客户端主机
服务端进程
服务端主机
发送端
发送端
接收端
接收端
请求
响应
处
理
业
务
展
示
Socket 套接字
概念
Socket 套接字,是由系统提供用于网络通信的技术,是基于 TCP/IP 协议的网络通信的基本操作单元。基
于 Socket 套接字的网络程序开发就是网络编程。
分类
Socket 套接字主要针对传输层协议划分为如下三类:
流套接字 :使用传输层 TCP 协议
TCP ,即 Transmission Control Protocol (传输控制协议),传输层协议。
以下为 TCP 的特点(细节后续再学习):
有连接
可靠传输
面向字节流
有接收缓冲区,也有发送缓冲区
大小不限
对于字节流来说,可以简单的理解为,传输数据是基于 IO 流,流式数据的特征就是在 IO 流没有关闭的情
况下,是无边界的数据,可以多次发送,也可以分开多次接收。
数据报套接字 :使用传输层 UDP 协议
UDP ,即 User Datagram Protocol (用户数据报协议),传输层协议。
以下为 UDP 的特点(细节后续再学习):
无连接
不可靠传输
面向数据报
有接收缓冲区,无发送缓冲区
大小受限:一次最多传输 64k 对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如 100 个字节,必须一
次发送,接收也必须一次接收 100 个字节,而不能分 100 次,每次接收 1 个字节。
原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的 IP 协议数据。
我们不学习原始套接字,简单了解即可。
Java 数据报套接字通信模型
对于 UDP 协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数
据报,一次接收全部的数据报。
java 中使用 UDP 协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用
DatagramPacket 作为发送或接收的 UDP 数据报。对于一次发送及接收 UDP 数据报的流程如下:
创建 DatagramSocket
发送端
构造发送的内容
( DatagramPacket )
发送的数据
( byte[] )
其他信息
( IP 地址,端口号)
②
发送 UDP 数据报
socket.send(packet)
创建 DatagramSocket
接收端
构造接收的内容
( DatagramPacket )
接收数据保存的地方
空的字节数组 byte[]
①
等待接收 UDP 数据报
(阻塞式等待)
socket.receive(packet)
③
更新接收的内容
( DatagramPacket )
接收的数据
( byte[] )
其他信息
( IP 地址,端口号)
接收到 UDP 数据报
( receive 向下执行)
以上只是一次发送端的 UDP 数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请
求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下: 创建 DatagramSocket
发送端
构造发送的内容
( DatagramPacket )
发送的数据
( byte[] )
其他信息
( IP 地址,端口号)
②
发送 UDP 数据报(请求)
socket.send(packet)
创建 DatagramSocket
接收端
构造接收的内容
( DatagramPacket )
接收数据保存的地方
空的字节数组 byte[]
①
等待接收 UDP 数据报
(阻塞式等待)
socket.receive(packet)
更新接收的内容
( DatagramPacket )
接收的数据
( byte[] )
其他信息
( IP 地址,端口号)
接收到 UDP 数据报
( receive 向下执行)
③
构造接收的内容
( DatagramPacket )
接收数据保存的地方
空的字节数组 byte[]
构造发送的内容
( DatagramPacket )
发送的数据
( byte[] )
其他信息
( IP 地址,端口号)
⑤
发送 UDP 数据报(响应)
socket.send(packet)
④
根据接收的数据,执
行业务
更新接收的内容
( DatagramPacket )
接收的数据
( byte[] )
其他信息
( IP 地址,端口号)
⑥
接收到响应的 UDP 数据报
( receive 向下执行)
根据响应的数据,
决定下一步应该如何做
(一般根据业务来定)
Java 流套接字通信模型 Socket 编程注意事项
源端口
进程 A
主机
1
进程 B
主机 2
③
客户端 服务端
①
源端口
目的端口
⑤
②
④
源 IP
目的 IP
应用层协议
应用层协议
一致
⑥
发送请求
返回响应 - 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场
景,一般都是不同主机。 - 注意目的 IP 和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程
- Socket 编程我们是使用流套接字和数据报套接字,基于传输层的 TCP 或 UDP 协议,但应用层协议,
也需要考虑,这块我们在后续来说明如何设计应用层协议。 - 关于端口被占用的问题 如果一个进程 A 已经绑定了一个端口,再启动一个进程 B 绑定该端口,就会报错,这种情况也叫端
口被占用。对于 java 进程来说,端口被占用的常见报错信息如下:
此时需要检查进程 B 绑定的是哪个端口,再查看该端口被哪个进程占用。以下为通过端口号查进程
的方式:
在 cmd 输入 netstat - ano | findstr 端口号 ,则可以显示对应进程的 pid 。如以下命令显
示了 8888 进程的 pid
在任务管理器中,通过 pid 查找进程
解决端口被占用的问题 :
如果占用端口的进程 A 不需要运行,就可以关闭 A 后,再启动需要绑定该端口的进程 B
如果需要运行 A 进程,则可以修改进程 B 的绑定端口,换为其他没有使用的端口。
UDP 数据报套接字编程
DatagramSocket API 方法签名
方法说明
DatagramSocket()
创建一个 UDP 数据报套接字的 Socket ,绑定到本机任意一个随机端口
(一般用于客户端)
DatagramSocket(int
port)
创建一个 UDP 数据报套接字的 Socket ,绑定到本机指定的端口(一般用
于服务端)
方法签名
方法说明
void
receive(DatagramPacket p)
从此套接字接收数据报(如果没有接收到数据报,该方法会阻
塞等待)
void send(DatagramPacket
p)
从此套接字发送数据报包(不会阻塞等待,直接发送)
void close()
关闭此数据报套接字
方法签名
方法说明
DatagramPacket(byte[]
buf, int length)
构造一个 DatagramPacket 以用来接收数据报,接收的数据保存在
字节数组(第一个参数 buf )中,接收指定长度(第二个参数
length )
DatagramPacket(byte[]
buf, int offset, int length,
SocketAddress address)
构造一个 DatagramPacket 以用来发送数据报,发送的数据为字节
数组(第一个参数 buf )中,从 0 到指定长度(第二个参数
length )。 address 指定目的主机的 IP 和端口号
方法签名
方法说明
InetAddress
getAddress()
从接收的数据报中,获取发送端主机 IP 地址;或从发送的数据报中,获取
接收端主机 IP 地址
int getPort()
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获
取接收端主机端口号
byte[] getData()
获取数据报中的数据
DatagramSocket 是 UDP Socket ,用于发送和接收 UDP 数据报。
DatagramSocket 构造方法:
DatagramSocket 方法:
DatagramPacket API
DatagramPacket 是 UDP Socket 发送和接收的数据报。
DatagramPacket 构造方法:
DatagramPacket 方法:
构造 UDP 发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创
建。 方法签名
方法说明
InetSocketAddress(InetAddress addr, int port)
创建一个 Socket 地址,包含 IP 地址和端口号
InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法:
示例一:一发一收(无响应)
以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),即只
有客户端请求,但没有服务端响应的示例:
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 ()));
}
}
} 可以看出,此时代码是阻塞等待在 socket.receive(packet) 代码行,直到接收到一个 UDP 数据报。
UDP 客户端
客户端启动后会发送一个 "hello world!" 的字符串到服务端,在服务端接收后,控制台输出内容如下:
等待接收 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 );
}
} 从以上可以看出,发送的 UDP 数据报(假设发送的数据字节数组长度为 M ),在接收到以后(假设接收
的数据字节数组长度为 N ):
- 如果 N>M ,则接收的 byte[] 字节数组中会有很多初始化 byte[] 的初始值 0 ,转换为字符串就是空白
字符; - 如果 N<M ,则会发生数据部分丢失(可以自己尝试,把接收的字节数组长度指定为比发送的字节
数组长度更短)。
要解决以上问题,就需要发送端和接收端双方约定好一致的协议,如规定好结束的标识或整个数据的长
度。
示例二:请求响应
示例一只是客户端请求和服务端接收,并没有包含服务端的返回响应。以下是对应请求和响应的改造:
构造一个展示服务端本地某个目录( BASE_PATH )的下一级子文件列表的服务
( 1 )客户端先接收键盘输入,表示要展示的相对路径(相对 BASE_PATH 的路径)
( 2 )发送请求:将该相对路径作为数据报发送到服务端
( 3 )服务端接收并处理请求:根据该请求数据,作为本地目录的路径,列出下一级子文件及子文件夹
( 4 )服务端返回响应:遍历子文件和子文件夹,每个文件名一行,作为响应的数据报,返回给客户端
( 5 )客户端接收响应:简单的打印输出所有的响应内容,即文件列表。
为了解决空字符或长度不足数据丢失的问题,客户端服务端约定好统一的协议:这里简单的设计为
ASCII 结束字符 \3 表示报文结束。
以下为整个客户端服务端的交互执行流程:
等待接收 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 数据报 ... 客户端
服务端
①服务端监听端口
Q W E R T Y U I O P
A S D F G H J K L
Z X C V B N M
space
.?123
return
②等待接收 UDP 数据报
③客户端输入要发送的内容
④发送数据报
⑥返回响应:发送响应的数据报
⑤接收到 UDP 数据报,解析处理
⑦接收到响应 UDP 数据报,解析
约定好统一的请求协议:
\3 作为结束符
客户端发送和服务端解析数据
约定好统一的响应协议:
\3 作为结束符
服务端发送和客户端解析数据
以下为服务端和客户端代码:
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 ); 以上服务端运行结果和示例一是一样的:
UDP 客户端
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 数据报 ...
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 ;
}
}
}
}
}
客户端启动后会等待输入要展示的路径: 方法签名
方法说明
ServerSocket(int port)
创建一个服务端流套接字 Socket ,并绑定到指定端口
方法签
名
方法说明
Socket
accept()
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端 Socket
对象,并基于该 Socket 建立与客户端的连接,否则阻塞等待
void
close()
关闭此套接字
在输入想查看的目录路径后,会接收并打印服务端响应的文件列表数据:
此时服务端也会打印接收到的客户端请求数据:
TCP 流套接字编程
和刚才 UDP 类似 . 实现一个简单的英译汉的功能
ServerSocket API
ServerSocket 是创建 TCP 服务端 Socket 的 API 。
ServerSocket 构造方法:
ServerSocket 方法:
请输入要展示的目录:
请输入要展示的目录:
/
该目录下的文件列表为:
1
2
60441 b1b8a74be3695ccc0d970693815
8f25103 aa249707ee4ab17635142cd0e
请输入要展示的目录:
等待接收 UDP 数据报 ...
客户端 IP : 127 . 0.0 . 1
客户端端口号: 57297
客户端请求的文件列表路径为: E : / TMP /
等待接收 UDP 数据报 ... 方法签名
方法说明
Socket(String host, int
port)
创建一个客户端流套接字 Socket ,并与对应 IP 的主机上,对应端口的
进程建立连接
方法签名
方法说明
InetAddress getInetAddress()
返回套接字所连接的地址
InputStream getInputStream()
返回此套接字的输入流
OutputStream getOutputStream()
返回此套接字的输出流
Socket API
Socket 是客户端 Socket ,或服务端中接收到客户端建立连接( accept 方法)的请求后,返回的服务端
Socket 。
不管是客户端还是服务端 Socket ,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据
的。
Socket 构造方法:
Socket 方法:
TCP 中的长短连接
TCP 发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数
据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以
多次收发数据。
对比以上长短连接,两者区别如下:
建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要
第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时
的,长连接效率更高。
主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送
请求,也可以是服务端主动发。
两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于
客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
扩展了解:
基于 BIO (同步阻塞 IO )的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的
消耗是不能承受的。
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的
处理请求。
实际应用时,服务端一般是基于 NIO (即同步非阻塞 IO )来实现长连接,性能可以极大的提升。
示例一:一发一收(短连接) 以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),即只
有客户端请求,但没有服务端响应的示例:
TCP 服务端
运行后,服务端就启动了,控制台输出如下:
可以看出,此时代码是阻塞等待在 server.accept() 代码行,直到有新的客户端申请建立连接。
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 连接 ... 客户端启动后会发送一个 "hello world!" 的字符串到服务端,在服务端接收后,控制台输出内容如下:
以上客户端与服务端建立的为短连接,每次客户端发送了 TCP 报文,及服务端接收了 TCP 报文后,双方
都会关闭连接。
示例二:请求响应(短连接)
示例一只是客户端请求和服务端接收,并没有包含服务端的返回响应。以下是对应请求和响应的改造:
构造一个展示服务端本地某个目录( BASE_PATH )的下一级子文件列表的服务
( 1 )客户端先接收键盘输入,表示要展示的相对路径(相对 BASE_PATH 的路径)
( 2 )发送请求:使用客户端 Socket 的输出流发送 TCP 报文。即输入的相对路径。
( 3 )服务端接收并处理请求:使用服务端 Socket 的输入流来接收请求报文,根据请求的路径,列出下
一级子文件及子文件夹。
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 ();
}
}
等待客户端建立 TCP 连接 ...
客户端 IP : 127 . 0.0 . 1
客户端端口号: 51118
接收到客户端请求:
hello world !
等待客户端建立 TCP 连接 ... ( 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. 返回的响应内容:每个文件及目录名称为一行 以上服务端运行结果和示例一是一样的:
TCP 客户端
if ( children != null ){
for ( File child : children ) {
pw . println ( child . getName ());
}
}
// 7-2. 有缓冲区的 IO 操作,真正传输数据,需要刷新缓冲区
pw . flush ();
// 7-3. 双方关闭连接:服务端是关闭客户端 socket 连接
socket . close ();
}
}
}
等待客户端建立 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
60441 b1b8a74be3695ccc0d970693815
8f25103 aa249707ee4ab17635142cd0e
请输入要展示的目录:
此时服务端也会打印接收到的客户端请求数据:
等待客户端建立 TCP 连接 ...
客户端 IP : 127 . 0.0 . 1
客户端端口号: 52493
客户端请求的文件列表路径为: E : / TMP /
等待客户端建立 TCP 连接 ...
目前 TCP 客户端和服务端实现的功能和 UDP 差不多,但都存在几个问题:
对于服务端来说,处理一次请求并返回响应后,才能再次处理下一次请求和响应,效率是比较低
的。这个问题比较好解决:可以使用多线程,每次的请求与响应都在线程中处理。这样多个客户端
请求的话,可以在多个线程中并发并行的执行。
服务端解析请求,是只读取了一行,而客户端解析响应,是一直读取到流结束。可以想想为什么解
析请求时,没有读取到流结束? 目前的业务,双方都已约定好业务是展示目录下的文件列表,且都只需要一种数据:请求传输的数
据代表要展示的目录;响应传输的数据代表文件列表:每一行为一个文件名。
如要提供更多的业务,如文件重命名,文件删除等操作时,就不能了。此时就需要提供更多的字段
来标识。一般我们需要设计更强大的协议。
再谈协议
回顾并理解为什么需要协议
以上我们实现的 UDP 和 TCP 数据传输,除了 UDP 和 TCP 协议外,程序还存在应用层自定义协议,可以想
想分别都是什么样的协议格式。
对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式:
客户端发送请求和服务端解析请求要使用相同的数据格式。
服务端返回响应和客户端解析响应也要使用相同的数据格式。
请求格式和响应格式可以相同,也可以不同。
约定相同的数据格式,主要目的是为了让接收端在解析的时候明确如何解析数据中的各个字段。
可以使用知名协议(广泛使用的协议格式),如果想自己约定数据格式,就属于自定义协议。
封装 / 分用 vs 序列化 / 反序列化
一般来说,在网络数据传输中,发送端应用程序,发送数据时的数据转换(如 java 一般就是将对象转换
为某种协议格式),即对 发送数据时的数据包装动作 来说:
如果是使用知名协议,这个动作也称为 封装
如果是使用小众协议(包括自定义协议),这个动作也称为 序列化 ,一般是将程序中的对象转换为
特定的数据格式。
接收端应用程序,接收数据时的数据转换,即对 接收数据时的数据解析动作 来说:
如果是使用知名协议,这个动作也称为 分用
如果是使用小众协议(包括自定义协议),这个动作也称为 反序列化 ,一般是基于接收数据特定的
格式,转换为程序中的对象
如何设计协议
对于协议来说,重点需要约定好如何解析,一般是根据字段的特点来设计协议:
对于定长的字段:
可以基于长度约定,如 int 字段,约定好 4 个字节即可
对于不定长的字段:
可以约定字段之间的间隔符,或最后一个字段的结束符,如换行符间隔, \3 符号结束等等
除了该字段 " 数据 " 本身,再加一个长度字段,用来标识该 " 数据 " 长度;即总共使用两个字段:
" 数据 " 字段本身,不定长,需要通过 " 长度 " 字段来解析;
" 长度 " 字段,标识该 " 数据 " 的长度,即用于辅助解析 " 数据 " 字段;
示例三:多线程 + 自定义协议
以下我们将示例二的业务做以下扩展:
提供多种操作:展示目录下文件列表,文件重命名,删除文件,上传文件,下载文件
在不同的操作中,需要抽象出请求和响应的字段,也即是说,要约定客户端服务端统一的请求协
议,同时也要约定服务端与客户端统一的响应协议 本示例中的自定义协议
以下为我们 TCP 请求数据的协议格式,这里简单起见,约定为换行符及结束符:
请求类型
操作的文件或目录路径
数据 \3
说明如下:
以上总共包含 3 个字段,前 2 个字段需要按换行符读取,最后一个字段需要按结束符读取
请求类型标识是什么操作:展示目录下文件列表,文件重命名,删除文件,上传文件,下载文件
重命名、上传文件操作,需要 " 数据 " 字段,其他操作可以置为空字符串
" 数据 " 字段为最后一个字段,使用 \3 结束符,这样在数据本身有换行符也能正确处理
以下为响应数据的协议格式:
状态码(标识是否操作成功)
数据(展示列表时,返回目录下的文件列表,或下载文件的数据) \3
以下为展示文件列表操作的自定义协议(请求、响应格式)
以下操作将展示服务端根目录下的子文件及子文件夹:
请求数据格式如下:
响应数据格式如下:
以下为上传文件操作的自定义协议(请求、响应格式)
需要先在客户端指定上传的服务端目录,及客户端要上传的文件路径,以下操作将会把客户端
Main.java 文件内容上传到服务端根目录 E:/TMP 下的 /1 目录下:
请求数据格式如下:
1
/
\3
200
\1
\2
\3
\1.txt
\2.txt\3 响应数据格式如下:
执行流程
约定好请求和响应的数据格式,也就是应用层协议,大家按照约定好的格式来发送和接收,以下为执行
流程
客户端
服务端
①服务端监听端口
Q W E R T Y U I O P
A S D F G H J K L
Z X C V B N M
space
.?123
return
②等待接收 TCP ,需要从输入流获取数据
③通过客户端输入,构造要发送的请求对象
④请求序列化:通过输出流,将请求对象输出为约定好的格式
⑥业务处理:根据请求内容,完成相应的业务操作,及构造响应对象
⑤请求反序列化:通过输入流将请求数据解析为 Request 对象
⑦响应序列化:通过输出流,将响应对象输出为约定好的格式
约定好统一的请求协议:
请求类型 + 换行符
操作的文件或目录路径 + 换行符
数据 +\3 结束符
客户端发送和服务端解析数据
约定好统一的响应协议:
状态码 + 换行符
数据 +\3 结束符
服务端发送和客户端解析数据
⑧响应反序列化:通过输入流将响应数据解析为 Response 对象
代码实现如下:
请求类
先按照约定的请求协议封装请求类:
每个字段为一个属性:操作类型,操作路径,数据
完成服务端解析请求封装:按约定的方式读,先按行读取前 2 个字段,再按结束符读第 3 个字段
完成客户端发送请求封装:按约定的方式写,前 2 个字段按行输出,第 3 个字段以 \3 结束
4
/1
package org.example;
public class Main {
...... 略
}\3
200
\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 ();
// 一直读取到结束符 \3
if ( 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 ();
}
@Override
public String toString () {
return "Request{" +
"type=" + type +
", url='" + url + '\'' +
", data='" + data + '\'' + 响应类
按照约定的响应协议封装响应类:
每个字段为一个属性:响应状态码
完成客户端解析响应封装:按约定的方式读,先按行读取第 1 个字段,再按结束符读第 2 个字段
完成服务端发送响应封装:按约定的方式写,第 1 个字段按行输出,第 2 个字段以 \3 结束
'}' ;
}
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 ;
}
}
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 <> (); TCP 服务端
以下完成服务端代码:
ServerSocket.accept() 为建立客户端服务端连接的方法,为提高效率,使用多线程
// 数据:循环读取
while ( true ){
// 一个字符一个字符的读
char c = ( char ) br . read ();
// 一直读取到结束符 \3
if ( 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 ();
}
@Override
public 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 ;
}
} 先要解析请求数据,即 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 () {
@Override
public 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 ()); TCP 客户端
以下为客户端代码:
先要建立和服务端的连接,连接服务端的 IP 和端口
根据输入来构建请求数据:
先接收操作类型和操作路径
重命名操作时,需要指定修改的文件名
文件上传操作时,需要指定上传的客户端本地文件路径
解析响应数据,并根据响应来执行相应的业务,我们这里暂时简单的解析为 Response 对象,并
打印即可
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 ;
}
}
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 ;
}
}