-
TCP是TCP协议中非常重要和常用的通信协议,可以实现可靠的网络通信
特点: 需要创建连接 需要三次握手 底层建立的连接流 数据包以流的方式传递 没有传输数据量大小的限制 传输过程中 可以保证数据一定不会丢 也不会多 也可以保证 顺序的一致 速度比较慢在可靠性要求比较高 速度要求比较低 的场景下优先使用
2. Java中实现TCP
在TCP通信中,通信的过程需要两端的参与,其中发起请求的端称之为客户端 被动等待请求的端称之为服务器端
案例:实现TPC通信
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* TCP通信的客户端
*/
public class Demo01Client {
public static void main(String[] args) throws Exception {
//1.创建Socket
Socket socket = new Socket();
//2.连接服务器
socket.connect(new InetSocketAddress("127.0.0.1", 44444));
//3.从客户端发送消息给服务端
OutputStream out = socket.getOutputStream();
//4.向服务端发送数据
String msg = "hello world~ ";
out.write(msg.getBytes());
out.flush();
//5.从服务端接受数据
InputStream in = socket.getInputStream();
byte [] data = new byte[1024];
int len = in.read(data);
String msg2 = new String(data,0,len);
System.out.println(msg2);
//6.关闭套接字
socket.close();
}
}
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* TCP通信的服务端
*/
public class Demo01Server {
public static void main(String[] args) throws Exception {
//1.创建服务端
ServerSocket ss = new ServerSocket();
//2.绑定指定端口
ss.bind(new InetSocketAddress(44444));
//3.等待客户端连接
Socket socket = ss.accept();
//4.接受客户端的数据
InputStream in = socket.getInputStream();
byte [] data = new byte[1024];
int len = in.read(data);
String str = new String(data,0,len);
System.out.println(str);
//5.向客户端返回数据
String msg = "hello net~";
OutputStream out = socket.getOutputStream();
out.write(msg.getBytes());
out.flush();
//6.关闭套接字
socket.close();
ss.close();
}
}
3. 在tcp网络通信中应用多线程技术实现一个服务端为多个客户端提供服务
案例:实现文件上传服务器 并且 通过多线程技术来实现同时处理多个客户端的效果
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
/**
* TCP案例:通过TCP实现文件上传 - 服务端代码
*/
public class Demo02UploadServer {
public static void main(String[] args) throws Exception {
//1.创建服务端
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(44444));
System.out.println("###千度网盘开始运行了###");
//2.不停接受客户端连接,一旦连接成功,交给线程处理
while(true){
Socket socket = ss.accept();
new Thread(new UploadRunnable(socket)).start();
}
}
}
class UploadRunnable implements Runnable{
private Socket socket = null;
public UploadRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
OutputStream out = null;
try {
//1.获取socket输入流
InputStream in = socket.getInputStream();
//2.创建文件输出流指向输出位置
String path = "upload/"+UUID.randomUUID().toString()+".data";
out = new FileOutputStream(path);
//3.对接流
byte [] data = new byte[1024];
int i = 0;
while((i = in.read(data))!=-1){
out.write(data,0,i);
}
System.out.println("接收到了来自["+socket.getInetAddress().getHostAddress()+"]的上传文件["+path+"]");
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭资源
if(out!=null){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
out = null;
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
}
}
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;
/**
* TCP案例:通过TCP实现文件上传 - 客户端代码
*/
public class Demo02UploadClient {
public static void main(String[] args) {
Scanner scanner = null;
InputStream in = null;
Socket socket = null;
try {
//1.要求用户输入文件路径
scanner = new Scanner(System.in);
System.out.println("--请输入要上传的文件的路径:");
String path = scanner.nextLine();
File file = new File(path);
//2.只有文件存在 且 是一个文件才上传
if(file.exists() && file.isFile()){
//2.创建连接文件的输入流
in = new FileInputStream(file);
//3.创建TCP客户端对象
socket = new Socket();
//4.连接TCP服务端
socket.connect(new InetSocketAddress("127.0.0.1",44444));
//5.获取到TCP服务端的输出流
OutputStream out = socket.getOutputStream();
//6.对接流 发送文件数据给服务端
byte [] data = new byte[1024];
int i = 0;
while((i = in.read(data))!=-1){
out.write(data,0,i);
}
}else{
throw new RuntimeException("文件不存在 或者是一个文件夹~~");
}
} catch (Exception e) {
e.printStackTrace();
} finally{
//7.关闭扫描器 关闭文件输入流 关闭套接字
if(scanner != null){
scanner.close();
}
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
in = null;
}
}
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
}
}
4. TCP通信中的粘包问题
a. 粘包问题概述
TCP通信中 如果连续传输多段数据 ,TCP在传输的过程中,会根据需要自动的拆分包合并包,造成数据边界信息丢失了,在接收端收到数据后无法判断数据的边界在哪里,这样的问题就称之为TCP通信中的粘包问题。
粘包问题的本质在于TCP协议是传输层的协议,而数据边界判断的问题本质上是会话层的问题,TCP协议并没有给予解决方案。
而socket编程是给予网络层 和 传输层的编程,没有会话层的功能提供,所以在socket编程中粘包问题需要程序开发人员自己想办法解决
b. 粘包问题解决方案
i. 只传输固定长度的数据
能解决粘包问题,但是程序的灵活性非常低,只能在每次传输的数据长度都一致的情况下使用,应用的场景比较少
ii. 约定分隔符
能解决粘包问题,但是如果数据本身包含分隔符,则需要进行转义。转义的过程比较麻烦,浪费时间,代码写起来也比较复杂
iii. 使用协议
在通信双发原定数据传输的格式 发送方严格按照格式发送 接收方严格按照格式接收 从而根据格式本身判断数据的边界
协议又分为公有协议 和 私有协议
使用公有协议:
利用会话层 传输层 应用层 的公有协议的规则 来传输数据 判断边界
在全世界范围内通用 但协议相对复杂
使用私有协议:
自己来约定传输双方使用的格式 从而来判断边界
协议可以根据需要定制 但只在有限的小范围内有效
案例:改造文件上传案例 通过自定义协议解决粘包问题 实现传输文件同时传输文件名
协议格式: [文件名长度]\r\n[文件名][文件内容长度]\r\n[文件内容]
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;
/**
* TCP案例:通过TCP实现文件上传 - 客户端代码
*/
public class Demo02UploadClient {
public static void main(String[] args) {
Scanner scanner = null;
InputStream in = null;
Socket socket = null;
try {
//1.要求用户输入文件路径
scanner = new Scanner(System.in);
System.out.println("--请输入要上传的文件的路径:");
String path = scanner.nextLine();
File file = new File(path);
//2.只有文件存在 且 是一个文件才上传
if(file.exists() && file.isFile()){
//2.创建连接文件的输入流
in = new FileInputStream(file);
//3.创建TCP客户端对象
socket = new Socket();
//4.连接TCP服务端
socket.connect(new InetSocketAddress("127.0.0.1",44444));
//5.获取到TCP服务端的输出流
OutputStream out = socket.getOutputStream();
//6.1向服务器发送[文件名字节长度\r\n]
out.write((file.getName().getBytes().length+"\r\n").getBytes());
//6.2向服务器发送[文件名字节]
out.write(file.getName().getBytes());
//6.3向服务器发送[文件内容字节长度\r\n]
out.write((file.length()+"\r\n").getBytes());
//6.4向服务器发送[文件内容字节]
byte [] data = new byte[1024];
int i = 0;
while((i = in.read(data))!=-1){
out.write(data,0,i);
}
}else{
throw new RuntimeException("文件不存在 或者是一个文件夹~~");
}
} catch (Exception e) {
e.printStackTrace();
} finally{
//7.关闭扫描器 关闭文件输入流 关闭套接字
if(scanner != null){
scanner.close();
}
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
in = null;
}
}
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
}
}
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* TCP案例:通过TCP实现文件上传 - 服务端代码
*/
public class Demo02UploadServer {
public static void main(String[] args) throws Exception {
//1.创建服务端
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(44444));
System.out.println("###千度网盘开始运行了###");
//2.不停接受客户端连接,一旦连接成功,交给线程处理
while(true){
Socket socket = ss.accept();
new Thread(new UploadRunnable(socket)).start();
}
}
}
class UploadRunnable implements Runnable{
private Socket socket = null;
public UploadRunnable(Socket socket) {
this.socket = socket;
}
/**
* 通过私有协议传输数据 协议的格式为 [文件名长度\r\n文件名 文件长度\r\n文件内容]
*/
@Override
public void run() {
OutputStream out = null;
try {
//1.获取socket输入流
InputStream in = socket.getInputStream();
//2.获取文件名 - 读到第一个回车换行之前 截取出文件名的长度 接着读取这个长度的字节 就是文件名
//--读取数据 直到遇到第一个回车换行
//----每次从流中读取一个字节 转成字符串 拼到line上 只要line还不是\r\n结尾 就重复这个过程
String line = "";
byte [] tmp = new byte[1];
while(!line.endsWith("\r\n")){
in.read(tmp);
line += new String(tmp);
}
//----读取到了 文件名长度\r\n 截掉\r\n 转成int 就是文件名的长度
int len = Integer.parseInt(line.substring(0, line.length()-2));
//----从流中接着读 len个字节 就是文件名
byte [] data = new byte[len];
in.read(data);
String fname = new String(data);
//3.读取文件内容 - 读到下一个回车换行之前 截取出文件内容的长度 接着读取这个长度的字节 就是文件内容
String line2 = "";
byte [] tmp2 = new byte[1];
while(!line2.endsWith("\r\n")){
in.read(tmp2);
line2 += new String(tmp2);
}
//----读取到了 文件长度\r\n 截掉\r\n 转成int 就是文件的长度
int len2 = Integer.parseInt(line2.substring(0, line2.length()-2));
//4.从流中读取 文件长度个字节 就是文件内容 输出到文件中
byte data2 [] = new byte[len2];
in.read(data2);
//5.创建文件输出流指向输出位置,将数据写出到流中
String path = "upload/"+fname;
out = new FileOutputStream(path);
out.write(data2);
System.out.println("接收到了来自["+socket.getInetAddress().getHostAddress()+"]的上传文件["+path+"]");
} catch (IOException e) {
e.printStackTrace();
} finally {
//6.关闭资源
if(out!=null){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
out = null;
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
}
}