TCP 通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端
通信步骤
- 服务器先启动,服务器不会主动的请求客户端
- 客户端请求服务器,必须使用客户端请求服务器端,客户端与服务端就会建立一个逻辑连接
- 这个连接中包含一个对象,
IO
对象 - 客户端与服务端通信,客户端与服务端使用
IO
对象进行通信,通信的数据可以是字符,音频,视频 - 所以这个对象是一个
字节
流对象
客户端与服务端通信程序
TCP 通信
-
java.net
包中包含的类和接口,它们提供低层次的通信细节 - 我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节
- 向服务器发送连接请求,给服务器发送数据,读取服务器回写的数据
通信类
客户端 Socket
作用
- 此类实现客户端套接字(也可以就叫
“套接字”
) - 套接字是两台机器间通信的端点
- 套接字中包含了
IP地址
和端口号
的网络单位
构造器
-
Socket(String host, int port)
创建一个流套接字并将其连接到指定主机上的指定端口号
????参数
- String host:服务器主机的名称 / 服务器的IP地址
- int port:服务器的端口号
成员方法
- OutputStream getOutputStream():返回此套接字的输出流
- InputStream getInputStream():返回此套接字的输入流
- void close():关闭此套接字
注意事项
- 客户端和服务器端进行交互,必须使用
Socket
中提供的网络流,不能使用自己创建的流对象 - 当我们创建客户端对象
Socket
的时候,就会去请求服务器和服务器经过3
次握手建立连接通路 - 如果服务器没有启动,那么就会抛出异常
ConnectException: Connection refused: connect
- 如果服务器已经启动,那么就可以进行交互了
ServerSocket
作用
- 此类实现服务器套接字
构造器
-
ServerSocket(int port)
成员方法
- Socket accept():监听并接受到此套接字的连接
客户端与服务器通信 Demo
客户端
/**
* @author BNTang
**/
public class Client {
public static void main(String[] args) throws Exception {
// 1.创建一个客户端对象 Socket,绑定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8088);
// 2.获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 3.给服务器发送数据
os.write("Hello Server this is Client".getBytes());
// 4.获取InputStream对象
InputStream is = socket.getInputStream();
// 5.读取服务器回写的数据
byte[] bytes = new byte[1024];
int read = is.read(bytes);
System.out.println(new String(bytes, 0, read));
// 6.释放资源(Socket)
socket.close();
}
}
服务端
/**
* @author BNTang
**/
public class Server {
public static void main(String[] args) throws Exception {
// 1.创建服务器ServerSocket对象和系统要指定的端口号
ServerSocket server = new ServerSocket(8088);
// 2.获取到请求的客户端对象Socket
Socket socket = server.accept();
// 3.获取当前socket输入流InputStream对象
InputStream is = socket.getInputStream();
// 4.读取客户端发送的数据
byte[] bytes = new byte[1024];
int read = is.read(bytes);
System.out.println(new String(bytes, 0, read));
// 5.获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 6.给客户端回写数据
os.write("server Response".getBytes());
// 7.释放资源(Socket,ServerSocket)
socket.close();
server.close();
}
}
测试方式为,先启动服务端直接运行即可,然后紧接着运行客户端即可开始测试,效果如下图所示
文件上传
整体思路
基本实现
客户端
/**
* @author BNTang
**/
public class Client {
public static void main(String[] args) throws Exception {
// 1.创建一个客户端Socket对象
try (Socket socket = new Socket("127.0.0.1", 8088)) {
// 2.获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 3.创建一个本地字节输入流FileInputStream对象
FileInputStream fis = new FileInputStream("D:/1.png");
// 4.读取本地文件
int readLength;
byte[] bytes = new byte[1024];
while ((readLength = fis.read(bytes)) != -1) {
// 5.把读取到的文件上传到服务器
os.write(bytes, 0, readLength);
}
// 6.获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 7.读取服务回写的数据
while ((readLength = is.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, readLength));
}
// 8.释放资源(FileInputStream,Socket)
fis.close();
}
}
}
服务端
/**
* @author BNTang
**/
public class Server {
public static void main(String[] args) throws Exception {
// 1.创建一个服务器ServerSocket对象
try (ServerSocket server = new ServerSocket(8088)) {
// 2.获取到请求的客户端Socket对象
Socket socket = server.accept();
// 3.获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 4.判断d:\\upload文件夹是否存在,不存在则创建
File file = new File("D:/upload");
if (!file.exists()) {
boolean mkdirs = file.mkdirs();
System.out.println("不存在upload文件夹,创建 upload文件夹" + mkdirs);
}
// 自定义一个文件的命名规则: 防止同名的文件被覆盖 规则:域名+毫秒值+随机数
String fileName = "www.it6666.top_" + System.currentTimeMillis() + new Random().nextInt(999999) + ".png";
// 5.创建一个本地字节输出流FileOutputStream对象
FileOutputStream fos = new FileOutputStream(file + "/" + fileName);
// 6.读取客户端上传的文件
int readLength;
byte[] bytes = new byte[1024];
// 7.从网络字节输入流当中读取数据
while ((readLength = is.read(bytes)) != -1) {
// 8.把读取到的文件保存到服务器的硬盘上
fos.write(bytes, 0, readLength);
}
// 9.给客户端回写"上传成功"
socket.getOutputStream().write("上传成功".getBytes());
// 10.释放资源(FileOutputStream,Socket,ServerSocket)
fos.close();
socket.close();
}
}
}
如上的客户端和服务端存在阻塞问题,测试方式如:先启动服务端,在启动客户端,你可以发现,上传的文件样式如下图阻塞了
这个时候你将客户端或者服务端关闭其中一个发现图片已经上传完毕了如下图所示
再次查看图片发现已经是一张完整的图片了如下图所示
造成阻塞的问题就是客户端在上传文件的时候没有将 -1
写给服务端,如下图,因为在循环的条件为 != -1
等于 -1
就直接结束了,所以服务端没有结束标志 -1
就造成了死循环了一直阻塞在那里,解决这个问题很简单,在客户端写完之后自己手动的添加一个 -1
但是还有其它的方式,如下介绍
解决阻塞问题
- 上传完文件,给服务端写一个结束标记
- 在 Socket 中为我们提供了一个void shutdownOutput()禁用此套接字的输出流
- 对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列
改造客户端代码如下
/**
* @author BNTang
**/
public class Client {
public static void main(String[] args) throws Exception {
// 1.创建一个客户端Socket对象
try (Socket socket = new Socket("127.0.0.1", 8088)) {
// 2.获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 3.创建一个本地字节输入流FileInputStream对象
FileInputStream fis = new FileInputStream("D:/1.png");
// 4.读取本地文件
int readLength;
byte[] bytes = new byte[1024];
while ((readLength = fis.read(bytes)) != -1) {
// 5.把读取到的文件上传到服务器
os.write(bytes, 0, readLength);
}
socket.shutdownOutput();
// 6.获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 7.读取服务回写的数据
while ((readLength = is.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, readLength));
}
// 8.释放资源(FileInputStream,Socket)
fis.close();
}
}
}
测试方式同上,发现已经没有了之前的阻塞问题了效果如下图
服务端让其一直运行
/**
* @author BNTang
**/
public class Server {
public static void main(String[] args) throws Exception {
// 1.创建一个服务器ServerSocket对象
ServerSocket server = new ServerSocket(8088);
while (true) {
// 2.获取到请求的客户端Socket对象
Socket socket = server.accept();
// 3.获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 4.判断d:\\upload文件夹是否存在,不存在则创建
File file = new File("D:/upload");
if (!file.exists()) {
boolean mkdirs = file.mkdirs();
System.out.println("不存在upload文件夹,创建 upload文件夹" + mkdirs);
}
// 自定义一个文件的命名规则: 防止同名的文件被覆盖 规则:域名+毫秒值+随机数
String fileName = "www.it6666.top_" + System.currentTimeMillis() + new Random().nextInt(999999) + ".png";
// 5.创建一个本地字节输出流FileOutputStream对象
FileOutputStream fos = new FileOutputStream(file + "/" + fileName);
// 6.读取客户端上传的文件
int readLength;
byte[] bytes = new byte[1024];
// 7.从网络字节输入流当中读取数据
while ((readLength = is.read(bytes)) != -1) {
// 8.把读取到的文件保存到服务器的硬盘上
fos.write(bytes, 0, readLength);
}
// 9.给客户端回写"上传成功"
socket.getOutputStream().write("上传成功".getBytes());
// 10.释放资源(FileOutputStream,Socket,ServerSocket)
fos.close();
socket.close();
// 让服务端一直开着不用关了
// server.close();
}
}
}
使用多线程提高效率
/**
* @author BNTang
**/
public class Server {
public static void main(String[] args) throws Exception {
// 1.创建一个服务器ServerSocket对象
ServerSocket server = new ServerSocket(8088);
while (true) {
// 2.获取到请求的客户端Socket对象
Socket socket = server.accept();
// TODO 可以使用线程池
new Thread(() -> {
try {
// 3.获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 4.判断d:\\upload文件夹是否存在,不存在则创建
File file = new File("D:/upload");
if (!file.exists()) {
boolean mkdirs = file.mkdirs();
System.out.println("不存在upload文件夹,创建 upload文件夹" + mkdirs);
}
// 自定义一个文件的命名规则: 防止同名的文件被覆盖 规则:域名+毫秒值+随机数
String fileName =
"www.it6666.top_" + System.currentTimeMillis() + new Random().nextInt(999999) + ".png";
// 5.创建一个本地字节输出流FileOutputStream对象
FileOutputStream fos = new FileOutputStream(file + "/" + fileName);
// 6.读取客户端上传的文件
int readLength;
byte[] bytes = new byte[1024];
// 7.从网络字节输入流当中读取数据
while ((readLength = is.read(bytes)) != -1) {
// 8.把读取到的文件保存到服务器的硬盘上
fos.write(bytes, 0, readLength);
}
// 9.给客户端回写"上传成功"
socket.getOutputStream().write("上传成功".getBytes());
// 10.释放资源(FileOutputStream,Socket,ServerSocket)
fos.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
模拟 BS 服务器
服务器处理
/**
* @author BNTang
**/
public class Server {
public static void main(String[] args) throws Exception {
// 创建一个服务器ServerSocket
ServerSocket server = new ServerSocket(8080);
// 获取到请求的客户端对象(浏览器)
Socket socket = server.accept();
// 获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 读取客户端的请求信息
byte[] bytes = new byte[1024];
int readLength;
while ((readLength = is.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, readLength));
}
socket.close();
server.close();
}
}
- 在浏览器当中输入地址:http://127.0.0.1:8080/TestCode/web/index.html
读取浏览器发来的信息
/**
* @author BNTang
**/
public class Server {
public static void main(String[] args) throws Exception {
// 创建一个服务器ServerSocket
ServerSocket server = new ServerSocket(8080);
// 获取到请求的客户端对象(浏览器)
Socket socket = server.accept();
// 获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 把is网络字节输入流对象,转换为字符缓冲输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 把客户端请求信息的第一行读取出来 GET /web/index.html HTTP/1.1
String line = br.readLine();
// 把读取的信息进行切割 GET /web/index.html HTTP/1.1
String[] arr = line.split(" ");
System.out.println(Arrays.toString(arr));
// 把路径前边的/去掉,进行截取 /web/index.html HTTP/1.1
String htmlPath = arr[1].substring(1);
System.out.println(htmlPath);
// 创建一个本地字节输入流,根据路径读取本地文件
FileInputStream fis = new FileInputStream(htmlPath);
// 获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 写入HTTP协议响应头,固定写法
os.write("HTTP/1.1 200 OK\r\n".getBytes());
os.write("Content-Type:text/html\r\n".getBytes());
// 必须要写入空行,否则浏览器不解析
os.write("\r\n".getBytes());
// 一读一写复制文件,把服务读取的html文件回写到客户端
int readLength;
byte[] bytes = new byte[1024];
while ((readLength = fis.read(bytes)) != -1) {
os.write(bytes, 0, readLength);
}
socket.close();
server.close();
}
}
测试方式,在当前工程当中创建两个文件夹如下图所示
在 web
文件夹中创建一个 index.html 的文件,然后启动服务端,在浏览器当中输入:http://127.0.0.1:8080/TestCode/web/index.html 最终效果图如下图所示
多线程处理
/**
* @author BNTang
**/
public class Server {
public static void main(String[] args) throws Exception {
// 创建一个服务器ServerSocket
ServerSocket server = new ServerSocket(8080);
while (true) {
// 获取到请求的客户端对象(浏览器)
Socket socket = server.accept();
// TODO 可使用线程池
new Thread(() -> {
try {
// 获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 把is网络字节输入流对象,转换为字符缓冲输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 把客户端请求信息的第一行读取出来 GET /web/index.html HTTP/1.1
String line = br.readLine();
// 把读取的信息进行切割 GET /web/index.html HTTP/1.1
String[] arr = line.split(" ");
System.out.println(Arrays.toString(arr));
// 把路径前边的/去掉,进行截取 /web/index.html HTTP/1.1
String htmlPath = arr[1].substring(1);
System.out.println(htmlPath);
// 创建一个本地字节输入流,根据路径读取本地文件
FileInputStream fis = new FileInputStream(htmlPath);
// 获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 写入HTTP协议响应头,固定写法
os.write("HTTP/1.1 200 OK\r\n".getBytes());
os.write("Content-Type:text/html\r\n".getBytes());
// 必须要写入空行,否则浏览器不解析
os.write("\r\n".getBytes());
// 一读一写复制文件,把服务读取的html文件回写到客户端
int readLength;
byte[] bytes = new byte[1024];
while ((readLength = fis.read(bytes)) != -1) {
os.write(bytes, 0, readLength);
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// 服务器不关闭
// server.close();
}
}
}
- ????测试方式同上