上次写了个OIO的的Sokcet编程,现在把最近学习的NIO补上
客户端:Client
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
/**
* 使用SocketChannel实现TCP协议的文件传输
* NIO中的SocketChannel和OIO中的Socket对应
* NIO中的ServerSocketChannel和OIO中的ServerSocket对应
*/
public class Client{
public static void main(String[] args) {
//先启动服务端,再启动客户端
Client client = new Client();
//发送文件
String filepath = System.getProperty("user.dir")+"\\resource\\client\\";
client.upload(filepath,"client1.jpg");
}
private final Charset charset = Charset.forName("UTF-8");//java默认编码为Unicode,有的操作系统不支持,统一编码格式为UTF-8
private final String serverAddress = "localhost";
private final int serverPort = 9111;
/**
* 传递到服务端的应该有文件路径和文件名
* @param filepath
* @param filename
*/
public void upload(String filepath,String filename) {
try{
File sendfile = new File(filepath+filename);
FileChannel fileChannel = new FileInputStream(sendfile).getChannel();//获取该文件的输入流的通道
SocketChannel socketChannel = SocketChannel.open();//打开Socket通道
socketChannel.connect(new InetSocketAddress(serverAddress,serverPort));//绑定服务器的链接地址和端口号
socketChannel.configureBlocking(false);//设置为非阻塞式
//由于是非阻塞式连接,所以socketChannel.connect()方法不论是否真正的连接成功,都会立即返回,
while(!socketChannel.finishConnect()){
//socket没有真正连接前,不断的自旋、等待,或者做一些其他的事情
System.out.println("等待连接中,做其他事....");
}
System.out.println("成功连接到服务器...");
//将存储在服务器的文件名编码为UTF-8格式的二进制字节序列
ByteBuffer fileNamebuffer = charset.encode(filename);
socketChannel.write(fileNamebuffer);//将文件名传过去
System.out.println("开始传输文件");
ByteBuffer filebuffer = ByteBuffer.allocate(1024);//开启缓冲内存区域,用于存储文件内容
int len = 0;
while((len = fileChannel.read(filebuffer)) != -1){//将文件数据从fileChannel读取并存储到filebuffer缓冲区中
filebuffer.flip();//将内存缓冲区翻转为读模式
socketChannel.write(filebuffer);//读取本次缓冲区所有文件并写入通道
filebuffer.clear();//清空缓冲区
}
//单向关闭,表示客户端数据写完了,如果需要服务端响应数据,就要用这种方式
// socketChannel.shutdownOutput();
//读完文件通道关闭
fileChannel.close();
socketChannel.close();
System.out.println("传输完成...");
}catch (IOException e){
e.printStackTrace();
}
}
}
服务端Server,对每个客户端的请求视为一个单独的对象,所以加了一个静态内部类作为当前处理的客户端对象
服务端涉及了Selector选择器,通过监听不同的通道来实现IO的多路复用。这样既能在非阻塞式的监听客户端请求,还可以只使用一个线程来处理多个客户端请求,比起以前一个线程对应一个客户端而言,节约了线程上下文切换的开销,效率要高很多。
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 使用SocketChannel实现TCP协议的文件传输
* NIO中的SocketChannel和OIO中的Socket对应
* NIO中的ServerSocketChannel和OIO中的ServerSocket对应
*/
public class Server{
public static void main(String[] args) {
//先启动服务端
Server server = new Server();
server.startServer();
}
private final Charset charset = Charset.forName("UTF-8");
private final int serverPort = 9111;
//将客户端传递的数据封装为一个对象
static class FileData{
String clientAddress;
String filename; //客户端上传的文件名称
FileChannel fileOutChannel;//输出的文件通道
}
//使用Map保存每个客户端传输,当OP_READ通道可读时,根据channel找到对应的对象
Map<SelectableChannel, FileData> map = new ConcurrentHashMap<>();
/**
* 启动服务器
*/
public void startServer(){
try{
// 1、获取Selector选择器
Selector selector = Selector.open();
// 2、创建一个通道,用于获取客户端的请求连接
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverChannel.socket();
// 3.设置为非阻塞
serverChannel.configureBlocking(false);
// 4、绑定连接
// InetSocketAddress只传入端口号,则自动绑定当前本机IP
serverSocket.bind(new InetSocketAddress(serverPort));//即服务端开放99端口
// 5、将该通道注册到选择器上,并注册的IO事件为:“接收新连接”
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端已开启,监听新连接。。。");
// 6、遍历选择器,轮询感兴趣的I/O就绪事件(选择键集合)
while (selector.select() > 0) {
// 7、获取选择键集合
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
// 8、获取单个的选择键,并处理
SelectionKey key = it.next();
// 9、判断key是具体的什么事件,是否为新连接事件
if (key.isAcceptable()) {
// 10、若接受的事件是“新连接”事件,就获取客户端新连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
if (socketChannel == null) continue;
// 11、客户端新连接,切换为非阻塞模式
socketChannel.configureBlocking(false);
// 12、将获取到的客户端socket通道再次注册到选择器,并注册为可读事件
// 这样下次遍历选择器时,就进入可读事件
socketChannel.register(selector, SelectionKey.OP_READ);
// 业务处理 - 每次客户端上传一个文件就创建一个对象存到map中
// 一个客户端对象对应一个socket通道
FileData fileData = new FileData();
fileData.clientAddress = socketChannel.getRemoteAddress().toString();
map.put(socketChannel, fileData);
System.out.println("与客户端"+fileData.clientAddress+ "连接成功...");
} else if (key.isReadable()) {
receiveFile(key);
}
// NIO的特点只会累加,已选择的键的集合不会删除
// 如果不删除,下一次又会被select函数选中
it.remove();
}
}
}catch (IOException e){
e.printStackTrace();
}
}
/**
* 接收文件
*/
private void receiveFile(SelectionKey key){
ByteBuffer buffer = ByteBuffer.allocate(1024);//开启内存缓冲区域
FileData fileData = map.get(key.channel());
SocketChannel socketChannel = (SocketChannel) key.channel();
String directory = System.getProperty("user.dir")+"\\resource\\server\\";//服务端收到文件的存储路径
long start = System.currentTimeMillis();
try {
int len = 0;
while ((len = socketChannel.read(buffer)) != -1) {//将客户端写入通道的数据读取并存储到buffer中
buffer.flip();//将缓冲区翻转为读模式
//客户端发送过来的,首先是文件名
if (null == fileData.filename) {
// 文件名 decode解码为UTF-8格式,并赋值给client对象的filename属性
fileData.filename = (System.currentTimeMillis()+"_"+charset.decode(buffer).toString()).substring(5);
//先检查存储的目录是否存在
File dir = new File(directory);
if(!dir.exists()) dir.mkdir();
//再检查文件是否存在,不存在就创建文件,然后通过FikeChanel写入数据
File file = new File(directory + fileData.filename);
if(!file.exists()) file.createNewFile();
//将设定要存放的文件路径+文件名创建一个输出流通道
FileChannel fileChannel = new FileOutputStream(file).getChannel();
fileData.fileOutChannel = fileChannel;//赋值给client对象
}
//客户端发送过来的,最后是文件内容
else{
// 通过已经创建的文件输出流通道向文件中写入数据
fileData.fileOutChannel.write(buffer);
}
buffer.clear();//清除本次缓存区内容
}
fileData.fileOutChannel.close();
key.cancel();
System.out.println("上传完毕,费时:"+Long.valueOf(System.currentTimeMillis()-start)+"毫秒");
System.out.println("文件在服务端的存储路径:" + directory + fileData.filename);
System.out.println("");
} catch (IOException e) {
key.cancel();
e.printStackTrace();
return;
}
}
}
最后留个小问题,希望后面自己能来解决,或者有大佬看到了能够指点一二
上面的代码我在windows本地环境测试是完全没问题的,但在linux的远程云服务器上测试就有个小问题,就是客户端的ByteBuffer缓冲区大小会影响服务端接收数据的完整性。
我测试文件是一个约84kb的图片文件
当客户端的ByteBuffer设为1024时,linux云服务器就只能收到64kb的数据
当客户端的ByteBuffer设为10240时,linux云服务器就只能收到70kb的数据
当客户端的ByteBuffer设为102400时,linux云服务器才能收到84kb的完整数据
我寻思缓冲区太小就多循环读取几次不就行了吗?为什么会造成数据得丢失。
如果有大佬能帮忙指教一下,不胜感激,谢谢!!!