闲着无聊,写了一个基于java的socket文件传输。
是这样设计的:
1、Server
提供文件传输的server服务器端,接收client发送过来的文件。
提供多线程并发处理,能同时处理多个client的文件传输请求。
2、Client
根据提供的参数指定的server以及本地文件的路径,进行文件传输
client的代码
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
public class FileClient {
private static final Logger LOGGER = Logger.getLogger(FileClient.class);
private Socket socket;
public void sendFile(File file, String host, int port) throws IOException {
if (!file.exists() || !file.isFile()) {
throw new IllegalArgumentException("file : " + file
+ " is not a valid file!");
}
connect(host, port);
sendFile(file);
close();
}
private void sendFile(File file) throws IOException {
BufferedOutputStream fileOutput = new BufferedOutputStream(
socket.getOutputStream());
BufferedInputStream input = new BufferedInputStream(
new FileInputStream(file));
IOUtils.copy(input, fileOutput);
fileOutput.flush();
IOUtils.closeQuietly(input);
IOUtils.closeQuietly(fileOutput);
}
private void close() throws IOException {
if (isConnected()) {
socket.close();
}
}
private boolean isConnected() {
return null != socket && socket.isConnected() && !socket.isClosed();
}
private void connect(String host, int port) throws IOException {
if (isConnected()) {
return;
}
socket = new Socket();
socket.setKeepAlive(true);
socket.setReuseAddress(true);
InetAddress addr = InetAddress.getByName(host);
SocketAddress endpoint = new InetSocketAddress(addr, port);
socket.connect(endpoint);
}
/**
* @param args
*/
public static void main(String[] args) {
String filePath = System.getProperty("file");
if (StringUtils.isEmpty(filePath)) {
LOGGER.error("Error: JVM argumengs -Dfile is null !");
return;
}
String host = System.getProperty("host");
if (StringUtils.isEmpty(host)) {
LOGGER.error("Error: JVM argumengs -Dhost is null !");
return;
}
String portString = System.getProperty("port");
if (StringUtils.isEmpty(portString)) {
LOGGER.error("Error: JVM argumengs -Dport is null !");
return;
}
int port = Integer.valueOf(portString).intValue();
File file = new File(filePath);
FileClient client = new FileClient();
try {
LOGGER.info("start to transfer file : " + file);
long before = System.currentTimeMillis();
client.sendFile(file, host, port);
LOGGER.info("transfer file : " + file
+ " successfully! It consumes "
+ (System.currentTimeMillis() - before) + " ms.");
} catch (IOException e) {
LOGGER.error("Error:" + e.getMessage(), e);
}
}
}
server的代码:
import java.io.File;
import java.net.Socket;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
public class Bootstrap {
/**
* @param args
*/
public static void main(String[] args) throws Exception{
int cpuTimes = 4;
int port = 7777;
BlockingQueue<Socket> queue = new LinkedBlockingQueue<Socket>();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new FileEchoServer("server-1", port, queue));
File path = new File("E:/temp");
for (int i = 0, count = Runtime.getRuntime().availableProcessors()
* cpuTimes; i < count; i++) {
exec.execute(new FileConsumer("socket-" + i, queue,path));
}
exec.shutdown();
}
}
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.BlockingQueue;
import org.apache.log4j.Logger;
/**
* @author kanpiaoxue
*
*/
public class FileEchoServer implements Runnable {
protected static final Logger LOGGER = Logger.getLogger(FileEchoServer.class);
protected final ServerSocket serverSocket;
protected final BlockingQueue<Socket> queue;
protected final String name;
public FileEchoServer(String name, int port, BlockingQueue<Socket> queue)
throws IOException {
serverSocket = new ServerSocket(port);
serverSocket.setReuseAddress(true);
LOGGER.info(serverSocket);
this.queue = queue;
this.name = name;
}
@Override
public void run() {
setName(name);
while (true) {
try {
queue.put(serverSocket.accept());
} catch (Exception e) {
LOGGER.error("Error:" + e.getMessage(), e);
}
}
}
private void setName(String name) {
Thread.currentThread().setName(name);
LOGGER.info(name + " start to work.");
}
}
import java.net.Socket;
import java.util.concurrent.BlockingQueue;
import org.apache.log4j.Logger;
/**
* @author kanpiaoxue
*
*/
public abstract class AbstractSocketConsumer implements Runnable {
protected static final Logger LOGGER = Logger
.getLogger(AbstractSocketConsumer.class);
protected final String name;
protected final BlockingQueue<Socket> queue;
public AbstractSocketConsumer(String name, BlockingQueue<Socket> queue) {
super();
this.name = name;
this.queue = queue;
}
private void setName(String name) {
Thread.currentThread().setName(name);
LOGGER.info(name + " start to work.");
}
@Override
public void run() {
setName(name);
while (true) {
try {
consume(queue.take());
} catch (Exception e) {
LOGGER.error("Error:" + e.getMessage(), e);
}
}
}
protected abstract void consume(Socket socket) throws Exception;
}
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.io.IOUtils;
import com.wanmei.net.slef.file.AbstractSocketConsumer;
public class FileConsumer extends AbstractSocketConsumer {
private File filePath;
private static final AtomicLong FILE_COUNT = new AtomicLong();
public FileConsumer(String name, BlockingQueue<Socket> queue, File path) {
super(name, queue);
this.filePath = path;
}
@Override
protected void consume(Socket socket) throws Exception {
LOGGER.info("start to receive file from "
+ socket.getInetAddress().getHostName() + ":"
+ socket.getPort());
long before = System.currentTimeMillis();
File file = new File(filePath, "file-" + FILE_COUNT.getAndIncrement());
if (file.exists()) {
file.delete();
}
BufferedOutputStream fileOutput = new BufferedOutputStream(
new FileOutputStream(file));
BufferedInputStream input = new BufferedInputStream(
socket.getInputStream());
IOUtils.copy(input, fileOutput);
fileOutput.flush();
PrintWriter response = new PrintWriter(socket.getOutputStream());
String responseString = echo(socket);
response.println(responseString);
response.flush();
if (null != socket) {
IOUtils.closeQuietly(input);
IOUtils.closeQuietly(fileOutput);
IOUtils.closeQuietly(response);
socket.close();
}
LOGGER.info("transfer file : " + file + " successfully! It consumes "
+ (System.currentTimeMillis() - before) + " ms.");
}
private String echo(Socket socket) {
return "Transfer file " + socket.getLocalAddress().getHostName() + ":"
+ socket.getLocalPort() + " OK!";
}
}
这个进行过测试,代码是可以运行的。这里的代码有其他的的依赖的jar包,就是apache下面的commons下面的一些常用包:
org.apache.commons.io.IOUtils
org.apache.commons.lang.StringUtils
org.apache.log4j.Logger
后记:这里只是简单的写了一个socket的文件传输。其实这里的代码的实际应用意义不是很大。
一般我们进行文件传输的时候,还需要进行一些必要的工作,比如:文件大小的校验,或者文件的MD5的校验,用来保证文件传输之前和传输之后的完整性,正确性。
这里给出一个提示:
client:可以在发送文件的client里面写2个socket,第一个socket用来告诉服务器“我要给你传输一个文件,这个文件的名字是:file-test.txt,它的MD5是:XXXXXXXXXX,文件的大小是xxxxxbytes”。当这个socket从服务器得到允许“yes”的消息,以及服务器创建的临时ServerSocket的host、port之后,开始用第二个socket进行文件传输。
server:在server中,当一个socket接到client发送的“我要给你传输一个文件,这个文件的名字是:file-test.txt,它的MD5是:XXXXXXXXXX,文件的大小是xxxxxbytes”的消息之后,判断是否需要client进行文件发送(例如根据文件的大小,判断当前的服务器有足够的磁盘空间存放客户端发送来的文件。如果文件大小够存放client传输过来的文件,那么进行下一步。如果不够存放client传输过来的文件,那么告知client,当前的服务器磁盘空间已满,不能进行文件传输。)。如果需要,则把该文件的名称,该文件的MD5记录到本地的内存中,然后建立一个临时的ServerSocket对象(指定任意端口),再通过上面的socket回复给client的第一个socket,告诉他可以(yes),并告知client的第一个socket这个临时的ServerSocket的对象的host,port。好允许client利用第二个socket进行文件传输,将文件流写给这里的ServerSocket。
当Server端判断出文件接收完毕,马上对该接收到的文件生成MD5校验码,将该校验码与之前client第一个socket传送来的MD5校验码进行校对。发现一致,继续等待下一个文件的传输任务的到来;如果不一致,可以告诉client,该文件需要进行重传。
这里需要注意的一个地方是:当server端启动多个线程进行文件接收的时候,最好不要用文件大小来判断磁盘空间是否可以存放client传输过来的文件。为什么?因为server接收文件是并行的。当其中一个线程接收到文件的磁盘检查的时候,该服务器的磁盘空间确实够存放文件,就会告诉client进行文件传输。这个时候很有可能,server的另一个线程也接到文件大小的检验的任务,开始检查磁盘空间是否够存放client传输的文件。发现空间是够用的,也告诉了当前的client可以进行文件传输。这个时候,会产生问题的:当server的磁盘空间就剩下1G的时候,一个client传输的文件是600M,另一个client传输的文件是700M,就会造成2个client的文件都无法完成传输而报错。因为它们占用服务器空间的大小,超过了服务器现在的空间大小。
那么该如何处理服务器磁盘空间大小检查的问题呢?没有更好的办法,服务器只能采用单线程的服务器文件传输。另一个方法,服务器程序内含一个文件大小检查的线程,定时(间隔10秒)检查服务器的空间是否达到预警阀值(这个阀值最好设置的大一点,比如1T的大小,可以存放半天的数据传输)。当到达预警阀值,可以发送预警信息(电子邮件、手机短信)给管理员,要求他进行磁盘空间的扩展。
另一个文件传输的方法:采用一个socket进行文件传输,而不是像上面那样采用2个socket,一个用来发送文件的具体信息,一个用来传输文件。如果需要采用一个socket,就需要自己写一个协议。其实这样的协议是存在的,如 http 协议。我们也可以自己写一个传输文件的协议,该协议分为2个部分。第一部分,header,用来存放文件的必要信息,比如:文件大小,文件名称,文件的MD5等等;第二部分,body,用来存放文件的流内容。这样,client可以在按照协议发送给服务器一个封装好的内容,server呢按照协议进行解析出header、body,然后存放文件。这样做就会复杂一点,要想简化,可以采用http协议来传输文件。http具有“协议、header、body”的完整结构,可以满足文件传输的需要。
上面给出的思路,大致能实现文件的安全传输,里面包含了文件传输,文件完整性/准确性校验,文件传输发生错误之后的重传机制。这个思路,和FTP的文件传输相似。可以进行参考。