网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)
当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。
特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程
但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源
在一次网络数据传输时:
发送端:数据的发送方进程,称为发送方.发送端主机即网络通信中的源主机
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机
收发端:发送端和接收端两端,也简称为收发端
一般来说,获取一个网络资源,涉及到两次网络数据传输:
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
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协议数据。
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都没有建立连接,并且一次发送全部数据报,一次接受全部的数据报
java中使用UDP协议通信,主要基于DatagramSocket类来创建数据报套接字,并使用DatagramPacket作为发送或者接受的UDP数据报.对于一次发送及接收UDP数据报的流程如下:
以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:
1. 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场
景,一般都是不同主机。
2. 注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程
3. Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议,也需要考虑,这块我们在后续来说明如何设计应用层协议。
4. 关于端口被占用的问题
如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫 端口被占用。对于java进程来说,端口被占用的常见报错信息如下:
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
方法名 | 方法说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端) |
DatagramSocket(int port) |
创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用 于服务端) |
DatagramSocket 方法:
方法名 | 方法说明 |
void receive(DatagramPacket p) |
从此套接字接收数据报(如果没有接收到数据报,该方法会阻 塞等待) |
void send(DatagramPacket p) |
从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
DatagramPacket是UDP Socket发送和接收的数据报
DatagramPacket 构造方法:
方法名 | 方法说明 |
DatagramPacket(byte[] buf, int length) |
构造一个DatagramPacket以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf)中,接收指定长度(第二个参数 length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) |
构造一个DatagramPacket以用来发送数据报,发送的数据为字节 数组(第一个参数buf)中,从0到指定长度(第二个参数 length)。address指定目的主机的IP和端口号 |
DatagramPacket 方法:
方法名 | 方法说明 |
InetAddress getAddress() |
从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取 接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获 取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创
建。
InetSocketAddress(SocketAddress的子类)构造方法:
方法名 | 方法说明 |
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),即只有客户端请求,但没有服务端响应的示例:
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 ("等待接受数据报...");
//3.等待客户端发送的UDP数据报,该方法在接受到数据之前会一直阻塞,接受到数据报以后,DatagramPacket对象,包含数据(bytes)和客户端ip、端口号
socket.receive ( packet );
System.out.printf ("客户端IP:%s\n",packet.getAddress().getHostAddress());
System.out.printf ("客户端端口号:%d\n",packet.getPort ());
System.out.printf ("客户端发送的原生数据为:%s\n",new String(packet.getData ()));
}
}
}
可以看出,此时代码是阻塞等待在 socket.receive(packet) 代码行,直到接收到一个UDP数据报
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数据报,包含数据,及发送的服务器信息
DatagramPacket packet = new DatagramPacket ( bytes,bytes.length,ADDRESS );
//6.发送UDp数据报
socket.send ( packet );
}
}
客户端启动后会发送一个"hello world!" 的字符串到服务端,在服务端接收后,控制台输出内容如下:
从以上可以看出,发送的UDP数据报(假设发送的数据报字节数组长度为M) ,在接收到以后(假设接收的数据字节数组长度为N):
1.如果N>M,则接收的byte[]字节数组中会有很多初始化byte[]的初始值0,转换为字符串就是空白
字符;
2. 如果N
示例一只是客户端请求和服务端接收,并没有包含服务端的返回响应。以下是对应请求和响应的改造:
构造一个展示服务端本地某个目录(BASE_PATH)的下一级子文件列表的服务
(1)客户端先接收键盘输入,表示要展示的相对路径(相对BASE_PATH的路径)
(2)发送请求:将该相对路径作为数据报发送到服务端
(3)服务端接收并处理请求:根据该请求数据,作为本地目录的路径,列出下一级子文件及子文件夹
(4)服务端返回响应:遍历子文件和子文件夹,每个文件名一行,作为响应的数据报,返回给客户端
(5)客户端接收响应:简单的打印输出所有的响应内容,即文件列表。
为了解决空字符或长度不足数据丢失的问题,客户端服务端约定好统一的协议:这里简单的设计为
ASCII结束字符 \3 表示报文结束。
以下为整个客户端服务端的交互执行流程:
以下为服务端和客户端代码:
public class UdpServer {
//服务器socket要绑定固定的端口
private static int PORT = 8888;
//本地文件目录要展示的路径
private static final String BASE_PATH = "D:/tmp";
public static void main(String[] args) throws IOException {
// 1.创建服务端DatagramSocket,指定端口,可以发送及接收UDP数据报
DatagramSocket socket = new DatagramSocket ( PORT );
while (true) {
// 2.创建数据报,用于接收客户端发送的数据
byte[] requestData = new byte[1024];
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 ( "客户端端口号:%d\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;
}
}
}
}
}
public class UdpClient {
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 ("该目录下的文件列表为:");
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;
}
}
}
}
}
和刚才UDP类似. 实现一个简单的英译汉的功能
ServerSocket 是创建TCP服务端Socket的API
ServerSocket 构造方法:
方法名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
方法 名 |
方法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的
Socket 构造方法:
方法签名 | 方法说明 |
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的 进程建立连接 |
Socket 方法:
方法名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据
基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求。
实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。
以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),即只有客户端请求,但没有服务端响应的示例:
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 ();
}
}
}
可以看出.此时代码是阻塞在server.accept()代码行,直到有新的客户端申请建立连接
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报文,及服务端接收了TCP报文后,双方都会关闭连接
示例一只是客户端请求和服务端接收,并没有包含服务端的返回响应。以下是对应请求和响应的改造:
构造一个展示服务端本地某个目录(BASE_PATH)的下一级子文件列表的服务
(1)客户端先接收键盘输入,表示要展示的相对路径(相对BASE_PATH的路径)
(2)发送请求:使用客户端Socket的输出流发送TCP报文。即输入的相对路径。
(3)服务端接收并处理请求:使用服务端Socket的输入流来接收请求报文,根据请求的路径,列出下一级子文件及子文件夹。
(4)服务端返回响应:使用服务端Socket的输出流来发送响应报文。即遍历子文件和子文件夹,每个文件名一行,返回给客户端。
(5)客户端接收响应:使用客户端Socket的输入流来接收响应报文。简单的打印输出所有的响应内容,即文件列表。
以下为服务端和客户端代码:
public class TcpServer {
//服务器socket要绑定固定的端口
private static final int PORT = 8888;
//本地文件目录要展示的根路径
private static final String BASE_PATH = "D:/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 file:children) {
pw.println (file.getName ());
}
}
//7-2.有缓冲区的IO操作,真正传输数据,需要刷新缓冲区
pw.flush ();
socket.close ();
}
}
}
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 ) );
//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 ();
}
}
}
在输入想查看的目录路径后,会接收并打印服务端响应的文件列表数据:
目前客户端和服务器实现的功能和UDP差不多,但都存在几个问题:
对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式:
一般来说,在网络数据传输中,发送端应用程序,发送数据时的数据转换(如java一般就是将对象转换为某种协议格式),即对发送数据时的数据包装动作来说:
接收端应用程序,接收数据时的数据转换,即对接收数据时的数据解析动作来说:
以下我们将示例二的业务做以下扩展:
以下为我们TCP请求数据的协议格式,这里简单起见,约定为换行符及结束符:
请求类型
操作的文件或目录路径
数据\3
说明如下:
以下为响应数据的协议格式:
状态码(标识是否操作成功)
数据(展示列表时,返回目录下的文件列表,或下载文件的数据)\3
先按照约定的请求协议封装请求类:
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 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 clientWriter(OutputStream os) throws IOException{
PrintWriter pw = new PrintWriter ( new OutputStreamWriter ( os ,"UTF-8") );
pw.println (type);
pw.println (url);
//pw.print ( data+"\3" );
pw.write (data+"\3" );
//4.2有缓冲区的IO操作,真正传输数据,需要刷新缓冲区
pw.flush ();
}
@Override
public 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;
}
}
按照约定的响应协议封装响应类:
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 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);
}
response.data = sb.toString ();
return response;
}
//服务器返回响应给客户端
public void severWriter(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;
}
}
以下完成服务端代码:
public class TcpServer {
//服务器socket要绑定固定的端口
private static final int PORT = 8888;
//本地文件目录要展示的根路径
private static final String BASE_PATH = "D:/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.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.6.根据请求处理业务:处理完成返回响应对象
Response response = build ( request );
} catch (IOException e) {
e.printStackTrace ();
}
}
} ).start ();
}
}
private 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;
}
//4上传文件
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;
}
}
以下为客户端代码:
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.clientWriter(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;
}
}