socket实现文件的传输以及底层的原理
之前,自己浅显地了解了socket的基本的知识点,为了更加透彻的了解socket的原理以及为了以后更加灵活地使用这方面的知识,自己还是做了一个文件传输的例子,看看内部究竟是怎样进行通信的,怎样做到了三次握手等等。
首先我们先建立服务端:
/*
* 文 件 名: MyServer.java
* 版 权: . Copyright 2008-2016, All rights reserved Beijing Kinchany Science and Technology Co.,Ltd.
* 描 述: <描述>
* 修 改 人: zuo_paopao
* 修改时间: 2018年3月26日
*/
package com.zuo.bb;
/**
* <一句话功能简述>
* <功能详细描述>
*
* @author zuoxianyong
* @version [版本号, 2018年3月26日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class MyServer
{
ServerSocket server=null;
Socket client=null;
boolean flag=true;
DataInputStream dis;
DataOutputStream dos;
FileOutputStream fos;
public static void main(String[] args)
{
new MyServer().ServerStart();
}
public void ServerStart()
{
try
{
server=new ServerSocket(8888);
System.out.println("端口号:"+server.getLocalPort());
client=server.accept();
if (client !=null){
System.out.println("连接完毕");
}
System.out.println ("输入目标地址");
String fileStr = (new Scanner(System.in)).next().toString();
dis=new DataInputStream(client.getInputStream());
dos=new DataOutputStream(client.getOutputStream());
String answer="g";
byte ans[]=answer.getBytes();
byte b[]=new byte[1024 * 4];
int ti;
new File(fileStr).mkdirs();
while(flag)
{
ti=dis.read(b);
dos.write(ans);
String select=new String(b,0,ti);
if(select.contains("/]0f"))
{
File f=new File(fileStr+(select.replace("/]0f","")));
System.out.println("creat directory");
f.mkdirs();
}
else if(select.contains("/]0c"))
{
fos=new FileOutputStream(fileStr+(select.replace("/]0c","")));
String cs;
boolean cflag=true;
int tip=dis.readInt();
dos.write(ans);
while(tip>0)
{
ti=dis.read(b,0,(tip>1024*4 ? 1024*4:tip));
tip=tip-ti;
cs=new String(b,0,4);
fos.write(b,0,ti);
}
fos.flush();
fos.close();
dos.write(ans);
}
else if(select.contains("/]00"))
{
flag=false;
}
}
dis.close();
client.close();
server.close();
}
catch(IOException e)
{
System.out.println("MyServer Error");
}
}
}
其次建立客户端:
/*
* 文 件 名: MyClient.java
* 版 权: . Copyright 2008-2016, All rights reserved Beijing Kinchany Science and Technology Co.,Ltd.
* 描 述: <描述>
* 修 改 人: zuoxianyong
* 修改时间: 2018年3月26日
*/
package com.zuo.bb;
/**
* <一句话功能简述>
* <功能详细描述>
*
* @author zuoxianyong
* @version [版本号, 2018年3月26日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class MyClient
{
Socket client;
boolean flag = true;
FileInputStream fis;// 此输入流负责读取本机上要传输的文件
DataOutputStream dos;// 此输出流负责向另一台电脑(服务器端)传输数据
DataInputStream dis;// 此输入流负责读取另一台电脑的回应信息
String fileStr;
public static void main(String[] args)
{
new MyClient().ClientStart();
}
public void ClientStart()
{
try
{
client = new Socket("127.0.0.1", 8888);
// 服务器端的IP,(这个只是在局域网内的)我的是这个,你的根据实际而定
System.out.println("已连接");
dos = new DataOutputStream(client.getOutputStream());
dis = new DataInputStream(client.getInputStream());
System.out.println("输入源文件夹路径");
fileStr = (new Scanner(System.in)).next().toString();
transmit(new File(fileStr));
String s = "/]00";// 提示传输完毕的标记
byte b[] = s.getBytes();
dos.write(b, 0, s.length());
dos.flush();
}
catch (IOException e)
{
System.out.println("MyClient Error");
}
}
public void transmit(File f)
throws IOException// 这是传输的核心,而且将被递归
{
byte b[];
String ts;
int ti;
for (File f1 : f.listFiles())
{ // 首先通过if语句判断f1是文件还是文件夹
if (f1.isDirectory()) // fi是文件夹,则向服务器端传送一条信息
{
ts = "/]0f" + (f1.getPath().replace(fileStr, ""));
// "/]0f"用于表示这条信息的内容是文件夹名称
b = ts.getBytes();
dos.write(b);
dos.flush();
dis.read();
transmit(f1);
// 由于f1是文件夹(即目录),所以它里面很有可能还有文件或者文件夹,所以进行递归
}
else
{
fis = new FileInputStream(f1);
ts = "/]0c" + (f1.getPath().replace(fileStr, ""));// 同上,表示这是一个文件的名称
b = ts.getBytes();
dos.write(b);
dos.flush();
dis.read();
dos.writeInt(fis.available());// 传输一个整型值,指明将要传输的文件的大小
dos.flush();
dis.read();
b = new byte[1024 * 4];
while (fis.available() > 0)// 开始传送文件
{
ti = fis.read(b);
dos.write(b, 0, ti);
dos.flush();
}
dos.flush();
fis.close();
dis.read();
}
}
}
}
三次握手建立连接
主机A | 主机B | |
---|---|---|
[主动打开] | [被动打开] | |
连接请求 | ———————SYN, SEQ = x—————→ | |
←——SYN, ACK, SEQ = y, ACK= x + 1—— | 确认 | |
确认 | ———ACK, SEQ = x + 1, ACK = y + 1——→ |
A的TCP向B发出连接请求报文段,其首部中的同步比特SYN应置为1,并选择序号x,表明传送数据时的第一个数据字节的序号是x。B的TCP收到连接请求报文段后,如同意则发回确认。B在确认报文段中应将SYN置为1,其确认号应为x+1,同时也为自己选择序号y。A收到此报文段后,向B给出确认,其确认号应为y+1。A的TCP通知上层应用进程,连接已经建立。当运行服务器进程的主机B的TCP收到主机A的确认后,也通知其上层应用进程,连接已经建立。
TCP 连接释放的过程(四次挥手)
主机A | 主机B | |
---|---|---|
应用进程释放连接 | ——————FIN, SEQ = x—————→ | 通知主机应用进程 |
←——ACK, SEQ= y, ACK = x + 1——— | 确认 | |
确认 | ——————SEQ=Y+1——————→ |
提示⚠️:TCP是一种可信赖的字节流服务,任何写入socket输出流的数据副本必须保留(保留到本地缓冲区),直到另一端成功的接收。向输出流写入信息并不意味着数据实际上已经被发送,他们只是被复制到了本地缓冲区。就算调用flush()也不会保证能立即发送到信道。
缓冲与数据传输
不能假设从一端写入输出流的数据和在另一端从输入流读出数据之间有任何的一致性。尤其是在发送端由单个输出流的write()方法传输数据,可能要经过另一端的多个read()方法获取,而一个read()方法可以返回多个write()写入的内容。为了展示这种情况,给出如下程序:
byte[] buffer1 = new byte[1000];
byte[] buffer2 = new byte[2000];
byte[] buffer3 = new byte[5000];
...
Socket socket = new Socket("127.0.0.1","8888");
OutputStream out = socket.getOutputStream;
...
out.write(buffer1);
...
out.write(buffer2);
...
out.write(buffer3);
...
socket.close();
这个TCP连接向接收端传输8000字节,在接收端这些字节的分组方式,取决于read和write调用的时间差,以及提供给in.read的缓冲区大小。我们可以认为TCP连接上发送的字节序列在某一个瞬间分成了3个FIFO(先入先出)序列:
- sendQ:在发送端底层实现中缓存的字节,这些字节已经写入网络流,还没有被接收端收到
- RecvQ:在接收端底层实现中缓存的字节,等待分配到应用程序-即从输入流中读取数据。
Delivered:接收着从输入流中已经读取到的字节。
sendQ中的字节既然存在就表明 对方还没有收到,为了防止网络异常重发,out.write()是向sendQ追加字节。然后又sendQ向RecvQ发送的过程不能由用户程序看到和观察,并且以块(chunk)的形式传输,chunk的大小与write()写入的字节大小无关。
从RecvQ读取数据时,字节从RecvQ发送到delivered中 ,转移的chunk的大小与RecVQ中的字节和read(BUF)中缓冲区的大小有关。