ServerSocket API
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
ServerSocket 方法:
Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的.
Socket 构造方法:
Socket 方法:
TCP中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
对比以上长短连接,两者区别如下:
- 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
- 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
- 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
扩展了解:
基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求。
实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。
示例一:一发一收(短连接)
以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),即只有客户端请求,但没有服务端响应的示例:
TCP服务端
package net1;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpServer {
private static final int port = 8888;
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(port);
while (true) {
System.out.println("------------------------------------");
System.out.println("等待客户端建立TCP连接...");
Socket client = server.accept();
System.out.println("客户端ip :" + client.getInetAddress().getHostAddress());
System.out.println("客户端port :" + client.getPort());
System.out.println("接收到客户端请求:>>");
InputStream inputStream = client.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
client.close();
}
}
}
TCP客户端
package net1;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
public class TcpClient {
private static final String host = "127.0.0.1";
private static final int port = 8888;
public static void main(String[] args) throws IOException {
Socket client = new Socket(host, port);
OutputStream outputStream = client.getOutputStream();
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream, "utf-8"));
printWriter.println("Hello I am client");
printWriter.flush();
client.close();
}
}
以上客户端与服务端建立的为短连接,每次客户端发送了TCP报文,及服务端接收了TCP报文后,双方都会关闭连接。
示例二:请求响应(短连接)
构造一个展示服务端本地某个目录的下一级子文件列表的服务
- 客户端先接收键盘输入,表示要展示的相对路径(相对BASE_PATH的路径)
- 发送请求:使用客户端Socket的输出流发送TCP报文。即输入的相对路径。
- 服务端接收并处理请求:使用服务端Socket的输入流来接收请求报文,根据请求的路径,列出下一级子文件及子文件夹。
- 服务端返回响应:使用服务端Socket的输出流来发送响应报文。即遍历子文件和子文件夹,每个文件名一行,返回给客户端。
- 客户端接收响应:使用客户端Socket的输入流来接收响应报文。简单的打印输出所有的响应内容,即文件列表。
TCP服务端
package net1;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpEchoServer {
private static final int port = 8888;
private static final String path = "";
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(port);
while (true) {
System.out.println("-------------------------------");
System.out.println("等待客户端建立TCP连接...");
Socket socket = server.accept();
System.out.println("客户端ip :" + socket.getInetAddress().getHostAddress());
System.out.println("客户端port :" + socket.getPort());
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String request = bufferedReader.readLine();
System.out.println("客户端请求的文件路径为:" + path + request);
File dir = new File(path + request);
File[] files = dir.listFiles();
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, "utf-8"));
if (files != null) {
for (File f : files) {
writer.println(f.getName());
}
}
writer.flush();
socket.close();
}
}
}
TCP客户端
package net1;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private static final String host = "127.0.0.1";
private static final int port = 8888;
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("----------------------------------");
System.out.println("请输入要展示的目录:");
String request = scanner.nextLine();
Socket socket = new Socket(host, port);
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, "utf-8"));
writer.println(request);
writer.flush();
System.out.println("接收到服务端响应:");
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
socket.close();
}
}
}
回顾并理解为什么需要协议
以上我们实现的UDP和TCP数据传输,除了UDP和TCP协议外,程序还存在应用层自定义协议,可以想想分别都是什么样的协议格式。
对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式:
- 客户端发送请求和服务端解析请求要使用相同的数据格式。
- 服务端返回响应和客户端解析响应也要使用相同的数据格式。
- 请求格式和响应格式可以相同,也可以不同。
- 约定相同的数据格式,主要目的是为了让接收端在解析的时候明确如何解析数据中的各个字段。
- 可以使用知名协议(广泛使用的协议格式),如果想自己约定数据格式,就属于自定义协议。
封装/分用 vs 序列化/反序列化
一般来说,在网络数据传输中,发送端应用程序,发送数据时的数据转换(如java一般就是将对象转换为某种协议格式),即对发送数据时的数据包装动作来说:
- 如果是使用知名协议,这个动作也称为封装
- 如果是使用小众协议(包括自定义协议),这个动作也称为序列化,一般是将程序中的对象转换为特定的数据格式。
接收端应用程序,接收数据时的数据转换,即对接收数据时的数据解析动作来说:
- 如果是使用知名协议,这个动作也称为分用
- 如果是使用小众协议(包括自定义协议),这个动作也称为反序列化,一般是基于接收数据特定的格式,转换为程序中的对象
如何设计协议
对于协议来说,重点需要约定好如何解析,一般是根据字段的特点来设计协议:
对于定长的字段:
- 可以基于长度约定,如int字段,约定好4个字节即可
对于不定长的字段:
- 可以约定字段之间的间隔符,或最后一个字段的结束符,如换行符间隔,\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结束
请求类
- 每个字段为一个属性:操作类型,操作路径,数据
- 完成服务端解析请求封装:按约定的方式读,先按行读取前2个字段,再按结束符读第3个字段
- 完成客户端发送请求封装:按约定的方式写,前2个字段按行输出,第3个字段以\3结束
package net1;
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 inputStream) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
Request request = new Request();
request.type = Integer.parseInt(bufferedReader.readLine());
request.url = bufferedReader.readLine();
List<Character> list = new ArrayList<>();
while (true) {
char c = (char) bufferedReader.read();
if (c == '\3') {
break;
}
list.add(c);
}
StringBuilder stringBuilder = new StringBuilder();
for (char c : list) {
stringBuilder.append(c);
}
request.data = stringBuilder.toString();
return request;
}
//客户端发送请求到服务器
public void clientWrite(OutputStream outputStream) {
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(type);
printWriter.println(url);
printWriter.write(data + "\3");
printWriter.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;
}
}
响应类
- 每个字段为一个属性:响应状态码
- 完成客户端解析响应封装:按约定的方式读,先按行读取第1个字段,再按结束符读第2个字段
- 完成服务端发送响应封装:按约定的方式写,第1个字段按行输出,第2个字段以\3结束
package net1;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class Response {
private int status;
private String data;
//客户端解析服务端返回的响应数据
public static Response clientParse(InputStream inputStream) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
Response response = new Response();
response.status = Integer.parseInt(bufferedReader.readLine());
List<Character> list = new ArrayList<>();
while (true) {
char c = (char) bufferedReader.read();
if (c == '\3') {
break;
}
list.add(c);
}
StringBuilder stringBuilder = new StringBuilder();
for (char c : list) {
stringBuilder.append(c);
}
response.data = stringBuilder.toString();
return response;
}
//服务端返回响应给客户端
public void serverWrite(OutputStream outputStream) throws UnsupportedEncodingException {
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream, "utf-8"));
printWriter.println(status);
printWriter.write(data + "\3");
printWriter.flush();
}
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;
}
@Override
public String toString() {
return "Response{" +
"status=" + status +
", data='" + data + '\'' +
'}';
}
}
TCP服务端
- ServerSocket.accept() 为建立客户端服务端连接的方法,为提高效率,使用多线程
- 先要解析请求数据,即 Request 已封装好的服务端解析请求,返回 Request 对象
- 返回响应数据,需要根据不同的请求字段,做不同的业务处理,并返回对应的响应内容如果操作的url路径再服务端根目录 E:/TMP 下找不到,则返回响应状态码404正常执行完,返回200响应状态码;要注意根据不同操作类型来执行不同的业务
package net1;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Files;
import java.util.UUID;
public class TcpFileServer {
private static final int port = 8888;
private static final String path = "";
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(port);
while (true) {
Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("------------------------------");
System.out.println("客户端建立TCP连接...");
System.out.println("客户端IP: " + socket.getInetAddress().getHostAddress());
InputStream inputStream = socket.getInputStream();
Request request = Request.serverParse(inputStream);
System.out.println("服务器收到请求: " + request);
Response response = build(request);
OutputStream outputStream = socket.getOutputStream();
System.out.println("服务器返回响应: " + response);
response.serverWrite(outputStream);
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
public static Response build(Request request) throws IOException {
Response response = new Response();
response.setStatus(200);
File url = new File(path + request.getUrl());
if (!url.exists()) {
response.setStatus(404);
response.setData("");
return response;
}
try {
switch (request.getType()) {
case 1: {
File[] childer = url.listFiles();
if (childer == null) {
response.setData("");
} else {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < childer.length; i++) {
File child = childer[i];
stringBuilder.append(child.getAbsolutePath().substring(path.length()) + "\n");
}
response.setData(stringBuilder.toString());
}
break;
}
case 2: {
url.renameTo(new File(url.getParent() + File.separator + request.getData()));
break;
}
case 3: {
url.delete();
break;
}
case 4: {
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 net1;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;
public class TcpFileClient {
private static final String host = "127.0.0.1";
private static final int port = 8888;
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
Request request = build(scanner);
Socket socket = new Socket(host, port);
OutputStream outputStream = socket.getOutputStream();
System.out.println("客户端发送请求:" + request);
request.clientWrite(outputStream);
InputStream inputStream = socket.getInputStream();
Response response = Response.clientParse(inputStream);
System.out.println("客户端收到响应: " + response);
socket.close();
}
}
private 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;
}
}