1. TCP通信
1.1. 简介
-
TCP指的是传输控制协议 (Transmission Control Protocol)
-
TCP协议是
面向连接的通信协议
,即传输数据之前,在发送端和接收端建立逻辑连接
,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”
1.2. 三次握手简述
-
三次握手
:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠第一次握手,客户端向服务器端发出连接请求,等待服务器确认
第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求
第三次握手,客户端再次向服务器端发送确认信息,确认连接
-
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等
1.3. Java使用TCP协议
1.3.1. ServerSocket和Socket类
-
Java中的TCP通信
-
Java对基于TCP协议的的网络提供了良好的封装,使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信。
-
Java为客户端提供了Socket类,为服务器端提供了ServerSocket类
-
1.3.2. TCP客户端发送数据
java中,客户端通过Socket类
向服务端发起TCP请求
-
构造方法
方法名 说明 Socket(InetAddress address,int port) 创建流套接字并将其连接到指定IP指定端口号 Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号 -
相关方法
方法名 说明 InputStream getInputStream() 返回此套接字的输入流 OutputStream getOutputStream() 返回此套接字的输出流
- 步骤
-
创建客户端的 Socket对象
-
获取输出流(默认是字节流) 用来发送数据
-
获取输入流 用来接收服务器的数据
-
关闭流
- 代码实现
package link.xiaomo.test7;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Demo02 {
public static void main(String[] args) throws IOException, InterruptedException {
//1创建客户端的 Socket对象
//参1和参2 表示当前要连接的服务器的ip和端口
Socket socket = new Socket("127.0.0.1", 15000);
//2 获取输出流(默认是字节流) 用来发送数据
OutputStream os = socket.getOutputStream();
//发送 文字 注意要转为字节数组
os.write("你好".getBytes("gbk"));
//3 获取输入流 用来接收服务器的数据
InputStream is = socket.getInputStream();
byte[] bytes = new byte[1024];
//这里会等待接收数据
int len = is.read(bytes);
//打印数据
System.out.println(new String(bytes, 0, len));
//4 关闭流
os.close();
is.close();
socket.close();
}
}
1.3.3. TCP服务器接收数据
java中,服务端通过ServerSocket类
接收客户端的请求并响应。
-
构造方法
方法名 说明 ServerSocket(int port) 创建绑定到指定端口的服务器套接字 -
相关方法
方法名 说明 Socket accept() 监听要连接到此的套接字并接受它 -
注意事项
- accept方法是阻塞的,作用就是等待客户端连接
- 客户端创建对象并连接服务器,此时是通过三次握手协议,保证跟服务器之间的连接
- 针对客户端来讲,一般是先往外写的,所以是输出流
针对服务器来讲,一般是先往里读的,所以是输入流 - read方法也是阻塞的
- 客户端在关流的时候,还多了一个往服务器写结束标记的动作
- 最后一步断开连接,通过四次挥手协议保证连接终止
- 步骤
- 创建serversocket对象
- 调用 accept等待 连接
- 获取输入流 用来接收客户端发来的数据
- 获取输出流 给客户端回复数据
- 关闭流
- 代码实现
package link.xiaomo.test7;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Demo03 {
public static void main(String[] args) throws IOException {
//1 创建serversocket对象
ServerSocket ss = new ServerSocket(20000);
//2 调用 accept等待 连接
//一旦有连接过来,accept方法就会建立连接 然后返回一个socket对象 用来处理此次连接请求
System.out.println("等待连接");
Socket accept = ss.accept();
System.out.println("连接成功, 来自于:" + accept.getRemoteSocketAddress());
//3 获取输入流 用来接收客户端发来的数据
InputStream is = accept.getInputStream();
byte[] bytes = new byte[1024];
int len = is.read(bytes);//等待接收数据
System.out.println("接收到数据" + new String(bytes, 0, len));
//4 获取输出流 给客户端回复数据
OutputStream os = accept.getOutputStream();
os.write("hello".getBytes());
System.out.println("回复了数据");
//5 关闭流
os.close(); //关闭的输出流
is.close(); //关闭的是输入流
accept.close(); //关闭的是处理本次连接请求的 socket对象
ss.close(); // 关闭是服务器
}
}
1.4. 传输结束标记
当我们接收数据时,不知道有多少数据,所以使用循环的方式接收。那么我们要怎么知道对方的数据已经传输完毕了呢?
由于接收方数循环接收,发送方一定要把数据发送完后,再发送一个结束标记,使用socket对象调用shutdownOutput方法
1.4.1. 客户端代码
- 注意socket.shutdownOutput();
package link.xiaomo.test8;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException, InterruptedException {
//1创建客户端的 Socket对象
//参1和参2 表示当前要连接的服务器的ip和端口
Socket socket = new Socket("127.0.0.1", 20000);
//2 获取输出流(默认是字节流) 用来发送数据
OutputStream os = socket.getOutputStream();
//发送 文字 注意要转为字节数组
os.write("你好".getBytes());
//shutdownOutput只会关闭当前的输出流
//这样 对方接收的代码就会得到一个-1的结束标记
socket.shutdownOutput();
//3 获取输入流 用来接收服务器的数据
InputStream is = socket.getInputStream();
byte[] bytes = new byte[1024];
//这里会等待接收数据
int len;
while ((len = is.read(bytes)) != -1) {
//打印数据
System.out.println(new String(bytes, 0, len));
}
//4 关闭流
os.close();
is.close();
socket.close();
}
}
1.4.2. 服务端代码
- 注意 accept.shutdownOutput();
package link.xiaomo.test8;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
//1 创建serversocket对象
ServerSocket ss = new ServerSocket(20000);
//2 调用 accept等待 连接
System.out.println("等待连接");
Socket accept = ss.accept();
System.out.println("连接成功, 来自于:" + accept.getRemoteSocketAddress());
//3 获取输入流 用来接收客户端发来的数据
InputStream is = accept.getInputStream();
byte[] bytes = new byte[1024];
int len;//等待接收数据
while ((len = is.read(bytes)) != -1) {
System.out.println("接收到数据" + new String(bytes, 0, len));
}
//4 获取输出流 给客户端回复数据
OutputStream os = accept.getOutputStream();
os.write("hello".getBytes());
//关闭输出流 对方才能读取到-1 这个结束的标记
accept.shutdownOutput();
System.out.println("回复了数据");
//5 关闭流
os.close(); //关闭的输出流
is.close(); //关闭的是输入流
accept.close(); //关闭的是处理本次连接请求的 socket对象
ss.close(); // 关闭是服务器
}
}
1.5. TCP文件上传案例
-
案例需求
客户端:连接服务器, 然后给服务器传输一个文件,然后接受服务器回复信息.
服务器:接收客户端发来的文件数据写入本地文件,给出反馈
-
案例分析
- 创建客户端对象,创建输入流对象指向文件,每读一次数据就给服务器输出一次数据,输出结束后使用shutdownOutput()方法告知服务端传输结束,会给服务器一个结束标记
- 创建服务器对象,创建输出流对象指向文件,每接受一次数据就使用输出流输出到文件中,传输结束后。使用输出流给客户端反馈信息
- 客户端接受服务端的回馈信息
-
相关方法
名字 描述 void shutdownOutput() 禁止用此套接字的输出流 -
代码实现
package link.xiaomo.test9;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
//服务器 接收客户端传来的文件. 保存到本地
public class Server {
public static void main(String[] args) {
try ( //1 创建服务器的socket对象
ServerSocket ss = new ServerSocket(20000);
//2 接收客户端的连接
Socket accept = ss.accept();
//4 创建一个文件的字节输出流 用来把接收的数据 保存到本地 这里保存到了桌面
FileOutputStream fos = new FileOutputStream("C:\\Users\\halon\\Desktop\\111.png");
) {
//3 获取网络输入流 用来接收客户端传来的文件数据
InputStream is = accept.getInputStream();
byte[] bytes = new byte[1024 * 8];
int len;
//读取数据 保存到本地
while ((len = is.read(bytes)) != -1) {
//把接收的数据 保存到本地
fos.write(bytes, 0, len);
}
//5 获取网络输出流 给客户端回复一个 上传成功
OutputStream os = accept.getOutputStream();
os.write("上传成功".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
package link.xiaomo.test9;
import java.io.*;
import java.net.Socket;
//客户端 把mymodule里的111.png上传到服务器
public class Client {
public static void main(String[] args) {
try ( //1创建socket对象 连接服务器
Socket socket = new Socket("127.0.0.1", 20000);
//2 创建字节输入流读取111.png
FileInputStream fis = new FileInputStream("mymodule/111.png");) {
//3 读取文件内容 要发送出去
byte[] bytes = new byte[1024 * 8];
int len;
//4获取网络输出流
OutputStream os = socket.getOutputStream();
while ((len = fis.read(bytes)) != -1) {
//5 把读取的数据 通过输出流发送出去
os.write(bytes, 0, len);
}
//5结束输出流 让对方得到结束标记
socket.shutdownOutput();
//6 获取网络输入流 用来得到服务器的回复数据
InputStream is = socket.getInputStream();
byte[] bytesReceive = new byte[1024];
int len1 = is.read(bytesReceive);
System.out.println(new String(bytesReceive, 0, len1));
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.5.1. 优化一:随机文件名
-
问题:
第二次上传文件的时候,会把第一次的文件给覆盖。
-
解决方案
UUID. randomUUID()方法生成随机的文件名
-
代码实现
package link.xiaomo.test9;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
//服务器 接收客户端传来的文件. 保存到本地
public class Server {
public static void main(String[] args) {
// 利用了UUID给保存的文件 起一个不重复的名字
String uuid = UUID.randomUUID().toString().replace("-", "");
System.out.println(uuid);
try ( //1 创建服务器的socket对象
ServerSocket ss = new ServerSocket(20000);
//2 接收客户端的连接
Socket accept = ss.accept();
//4 创建一个文件的字节输出流 用来把接收的数据 保存到本地 这里保存到了桌面
FileOutputStream fos = new FileOutputStream("C:\\Users\\halon\\Desktop\\" + uuid);
) {
//3 获取网络输入流 用来接收客户端传来的文件数据
InputStream is = accept.getInputStream();
byte[] bytes = new byte[1024 * 8];
int len;
//读取数据 保存到本地
while ((len = is.read(bytes)) != -1) {
//把接收的数据 保存到本地
fos.write(bytes, 0, len);
}
//5 获取网络输出流 给客户端回复一个 上传成功
OutputStream os = accept.getOutputStream();
os.write("上传成功".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.5.2. 优化二:服务端处理后不停止
-
问题:
- 服务器目前只能处理一个客户端请求,接收完一个文件之后,服务器就关闭了。
-
解决方案
- 使用循环, 注意哪些代码写在循环里面
-
代码实现
package link.xiaomo.test10;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
//服务器 接收客户端传来的文件. 保存到本地
public class Server {
public static void main(String[] args) {
//1 创建服务器的socket对象
ServerSocket ss = null;
try {
ss = new ServerSocket(20000);
} catch (IOException e) {
e.printStackTrace();
}
FileOutputStream fos = null;
Socket accept = null;
while (true) {
try {
//2 接收客户端的连接
accept = ss.accept();
System.out.println("接收到连接");
//3 获取网络输入流 用来接收客户端传来的文件数据
InputStream is = accept.getInputStream();
byte[] bytes = new byte[1024 * 8];
int len;
//4 创建一个文件的字节输出流 用来把接收的数据 保存到本地 这里保存到了桌面
// 利用了UUID给保存的文件 起一个不重复的名字dd
String uuid = UUID.randomUUID().toString().replace("-", "");
fos = new FileOutputStream("C:\\Users\\halon\\Desktop\\" + uuid);
//读取数据 保存到本地
System.out.println("开始接收文件");
while ((len = is.read(bytes)) != -1) {
//把接收的数据 保存到本地
fos.write(bytes, 0, len);
}
System.out.println("接收文件完毕");
//5 获取网络输出流 给客户端回复一个 上传成功
OutputStream os = accept.getOutputStream();
os.write("上传成功".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (accept != null) {
try {
accept.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
1.5.3. 优化三:结合多线程
-
问题
使用循环虽然可以让服务器处理多个客户端请求。但是还是无法同时跟多个客户端进行通信。
-
解决方案
开启多线程处理
-
代码实现
package link.xiaomo.test11;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
class ProcessAccept implements Runnable {
public Socket accept;
public ProcessAccept(Socket accept) {
this.accept = accept;
}
@Override
public void run() {
FileOutputStream fos = null;
try {
//3 获取网络输入流 用来接收客户端传来的文件数据
InputStream is = accept.getInputStream();
byte[] bytes = new byte[1024 * 8];
int len;
//4 创建一个文件的字节输出流 用来把接收的数据 保存到本地 这里保存到了桌面
// 利用了UUID给保存的文件 起一个不重复的名字dd
String uuid = UUID.randomUUID().toString().replace("-", "");
fos = new FileOutputStream("C:\\Users\\halon\\Desktop\\" + uuid);
//读取数据 保存到本地
System.out.println("开始接收文件");
while ((len = is.read(bytes)) != -1) {
//把接收的数据 保存到本地
fos.write(bytes, 0, len);
}
System.out.println("接收文件完毕");
//5 获取网络输出流 给客户端回复一个 上传成功
OutputStream os = accept.getOutputStream();
os.write("上传成功".getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (accept != null) {
try {
accept.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
//服务器 接收客户端传来的文件. 保存到本地
public class Server {
public static void main(String[] args) {
//1 创建服务器的socket对象
ServerSocket ss = null;
try {
ss = new ServerSocket(20000);
} catch (IOException e) {
e.printStackTrace();
}
Socket accept = null;
while (true) {
try {
//2 接收客户端的连接
System.out.println("等待连接");
accept = ss.accept();
System.out.println("接收到连接");
//把当前处理客户端连接的accept对象 传入到一个子线程任务中去运行
new Thread(new ProcessAccept(accept)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1.5.4. 优化四:结合线程池
-
需求
使用多线程虽然可以让服务器同时处理多个客户端请求。但是资源消耗太大。
-
解决方案
加入线程池
-
代码实现
package link.xiaomo.test12;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
class ProcessAccept implements Runnable {
public Socket accept;
public ProcessAccept(Socket accept) {
this.accept = accept;
}
@Override
public void run() {
FileOutputStream fos = null;
try {
//3 获取网络输入流 用来接收客户端传来的文件数据
InputStream is = accept.getInputStream();
byte[] bytes = new byte[1024 * 8];
int len;
//4 创建一个文件的字节输出流 用来把接收的数据 保存到本地 这里保存到了桌面
// 利用了UUID给保存的文件 起一个不重复的名字dd
String uuid = UUID.randomUUID().toString().replace("-", "");
fos = new FileOutputStream("C:\\Users\\halon\\Desktop\\" + uuid);
//读取数据 保存到本地
System.out.println("开始接收文件");
while ((len = is.read(bytes)) != -1) {
//把接收的数据 保存到本地
fos.write(bytes, 0, len);
}
System.out.println("接收文件完毕");
//5 获取网络输出流 给客户端回复一个 上传成功
OutputStream os = accept.getOutputStream();
os.write("上传成功".getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (accept != null) {
try {
accept.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
//服务器 接收客户端传来的文件. 保存到本地
public class Server {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量
10, //线程池的总数量
60, //临时线程空闲时间
TimeUnit.SECONDS, //临时线程空闲时间的单位
new ArrayBlockingQueue<>(5),//阻塞队列
Executors.defaultThreadFactory(),//创建线程的方式
new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略
);
//1 创建服务器的socket对象
ServerSocket ss = null;
try {
ss = new ServerSocket(20000);
} catch (IOException e) {
e.printStackTrace();
}
Socket accept = null;
while (true) {
try {
//2 接收客户端的连接
System.out.println("等待连接");
accept = ss.accept();
System.out.println("接收到连接");
//把当前处理客户端连接的accept对象的任务 交给线程池
pool.execute(new ProcessAccept(accept));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1.6. 使用字符流完成TCP
- 通过转换流 转为缓冲字符流
- BufferedReader br = new BufferedReader(new InputStreamReader(accept.getInputStream(), "gbk"));
- BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream(), "gbk"));
- 因为是缓冲流 所以写完数据要flush
1.6.1. 服务端
package link.xiaomo.test3;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
//tcp服务器代码
//1创建ServerSocket对象 指定服务器运行的端口
ServerSocket ss = new ServerSocket(20000);
//2 accept方法等待客户端连接 这个方法是阻塞的 如果连接成功会返回一个socket对象
//这个socket对象用来专门处理此次客户的连接
System.out.println("等待客户端连接 ");
Socket accept = ss.accept();
System.out.println("收到一个客户端的连接,来自于:" +
accept.getRemoteSocketAddress());
//3 先收一条数据
BufferedReader br = new BufferedReader(new InputStreamReader(accept.getInputStream(), "gbk"));
char[] cs = new char[10];
int len;
while ((len = br.read(cs)) != -1) {
System.out.println("收到数据" + new String(cs, 0,
len));
}
System.out.println("服务器给客户端回复数据:");
//4 发一条数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream(), "gbk"));
bw.write("你好fsdfdsjFJdsljfL发到上交垮落法sdfsdklfjkSLSF");
bw.flush();
accept.shutdownOutput();
br.close();
bw.close();
accept.close();//仅此次服务结束
ss.close();//表示服务器关闭
}
}
1.6.2. 客户端
package link.xiaomo.test3;
import java.io.*;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
//1 创建socket对象 参1 是要连接的服务器的ip 参2 是要连接的服务器的端口
Socket socket = new Socket("127.0.0.1", 20000);
//2 获取输出流 用来发送数据 获取的是字节流对象 然后通过转换流 最后转为BufferedWriter对象
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "gbk"));
//3 发送数据
bw.write("你好fdsfds京东方思考理解分离式的fsdjflksjlkfs");
bw.flush();
//发送完数据 要给接收方再发一个结束标记 表示此次发送结束了 shutdownOutput
socket.shutdownOutput();
//4 接收服务器返回的数据
//获取输入流 通过转换流InputStreamReader 最后转为BufferedReader对象
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "gbk"));
char[] cs = new char[10];
int len;
System.out.println("接收服务器返回的数据:");
while ((len = br.read(cs)) != -1) {
System.out.println(new String(cs, 0, len));
}
bw.close();
br.close();
socket.close();
}
}
1.7. 多线程服务器 同时接受多个客户端连接
- 有于命令窗口只有一个 ,会导致客户多的时候,只能回复一个
//同时可以接收多个客户端连接
//服务器的发送功能
class ServerSend implements Runnable {
//处理此次服务的socket对象 通过构造方法传入
private Socket accept;
Scanner sc = new Scanner(System.in);
public ServerSend(Socket accept) {
this.accept = accept;
}
@Override
public void run() {
while (true) {
System.out.println("服务器给客户端回复数据:");
try {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream(), "gbk"));
String text = sc.nextLine();//输入服务器回复的话
//4 发一条数据
bw.write(text);
bw.flush();
//accept.shutdownOutput(); 循环发送不能关闭
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//服务器的接收功能
class ServerReceive implements Runnable {
//处理此次服务的socket对象 通过构造方法传入
private Socket accept;
public ServerReceive(Socket accept) {
this.accept = accept;
}
@Override
public void run() {
//3 先收一条数据
try {
BufferedReader br = new BufferedReader(new InputStreamReader(accept.getInputStream(), "gbk"));
char[] cs = new char[10];
int len;
while ((len = br.read(cs)) != -1) {
System.out.println("收到数据" + new String(cs, 0,
len));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Server {
public static void main(String[] args) throws IOException {
//tcp服务器代码
//1创建ServerSocket对象 指定服务器运行的端口
ServerSocket ss = new ServerSocket(20000);
while (true) {
//2 accept方法等待客户端连接 这个方法是阻塞的 如果连接成功会返回一个socket对象
//这个socket对象用来专门处理此次客户的连接
System.out.println("等待客户端连接 ");
Socket accept = ss.accept();
System.out.println("收到一个客户端的连接,来自于:" +
accept.getRemoteSocketAddress());
new Thread(new ServerReceive(accept)).start();
new Thread(new ServerSend(accept)).start();
//accept.close();//仅此次服务结束
//ss.close();//这里服务器不能关闭 因为要循环回去 接收下一个客户
}
}
}
Comments NOTHING