前言: 最近又看到了关于TCP长连接和短连接的概念,以前也看过Http长连接和短连接的概念,因为Http是建立在TCP协议之上的,所以它其实是依赖TCP的长连接和短连接。所以,我就萌生了一个想法,看看这两种方式的传输效率上到底有多大差别——实践出真知!或者 Takl is cheap, show me your code!
TCP长连接和短连接
长连接是指可以实现服务端和客户端之间连续的传输数据,在传输的过程中,连接保持开启,并不中断,数据传输完成后连接不关闭。
短连接是指当服务器端和客户端连接成功后开始传输数据,传输完成即关闭连接。如果需要再次传输数据,则需要在创建新的连接进行数据传输。
如果你没有系统的学习过计算机网络的话,可能感觉这样似乎没有什么区别,这里我简单说明一下:
传输时间 = 建立连接的时间 + 传输数据的时间
TCP 建立连接需要进行三次握手,如果频繁的传输数据并且采用短连接的形式,则会频繁的建立连接,降低了传输效率。但是如果采用长连接的形式,则只需要建立一次连接,就可以传输多次数据了。当然了,好处有,坏处也是少不了的。短连接传输完成即关闭,服务器不需要保存连接对象,对于服务器的压力较小;长连接则需要保持多个连接对象(很多客户一起使用,需要保持他们的连接),对于服务器的压力较大,并且长连接如何处理数据也是一个问题了——传输数据本质上就是字节流,如果连续的传输字节流,你如何知道哪一部分是属于哪一个文件的呢?
如果你对这个问题,感兴趣的可以参考我这篇博客,应该会对你有一些启发,这是我个人的一种解决方案。
单个TCP(Socket)连接,发送多个文件
测试用例
我这里使用六个图片文件作为测试,对于短连接就是连续发送六次数据;对于长连接是发送一次数据。
测试图片的大小
TCP 短连接测试
TCP短连接客户端
package short_tcp;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ShortClient {
private static final String host = "127.0.0.1";
private static final int port = 10000;
public static void main(String[] args) {
// 存放需要传输文件的目录
Path path = Paths.get("D:", "DragonFile", "tcp", "targetFile");
// Files.find() 和 walk() 方法似乎使用起来更加简单,但是它传递的参数,
// 我暂时还不清楚,那就不用了,免得引入额外的麻烦。
File dir = path.toFile();
// 获取该目录下的所有文件
File[] files = dir.listFiles();
// 创建短连接客户端对象
ShortClient shortClient = new ShortClient();
// 连续发送6个文件
System.out.println("Start data transfer...");
long start = System.currentTimeMillis();
for (File file : files) {
shortClient.send(file);
}
System.out.println("6个文件使用tcp短连接的方式发送结束,总耗时:" + (System.currentTimeMillis()-start) + " ms。");
System.out.println("End of data transmission!");
}
public void send(File file) {
// 创建一个 Socket 客户端
try (Socket client = new Socket(host, port)) {
// 获取输出流,用于发送数据
OutputStream output = new BufferedOutputStream(client.getOutputStream());
// 获取输入流,用于接收服务器的响应数据
InputStream input = new BufferedInputStream(client.getInputStream());
// 每次实际读取到的字节数
int len = 0;
// 每次读取1024字节的数据
byte[] b = new byte[1024];
// 创建一个文件输入流,用于获取文件数据,并使用输出流发送到服务器
try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
while ((len = in.read(b)) != -1) {
output.write(b, 0, len);
}
// 强制刷新输出流,防止阻塞
output.flush();
}
// 关闭输出流,注意可不能使用 output.close()!它会把整个套接字关闭的!
// 关于它会不会刷新输出流,我没有找到资料,当我感觉应该是会的,但是感觉不靠谱,
// 上面我仍然手动刷新输出流吧!
client.shutdownOutput();
// 读取服务器的响应信息
len = input.read(b);
// 注意如果 len 为 -1,会导致 RunTimeException
System.out.println(new String(b, 0, len, StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
TCP短连接服务端
package short_tcp;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
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.nio.charset.StandardCharsets;
import java.util.UUID;
public class ShortServer {
private static final int port = 10000;
public static void main(String[] args) {
new ShortServer().start();
}
public void start() {
try (ServerSocket server = new ServerSocket(port)) {
// 这里不使用多线程,因为我主要关注点在于创建socket连接的开销
while (true) {
System.out.println("Waitting for client to connect...");
try (Socket client = server.accept()) {
InputStream input = new BufferedInputStream(client.getInputStream());
OutputStream output = new BufferedOutputStream(client.getOutputStream());
/* 统一使用 .jpg 扩展名了,这里要指明一点,由于传输的只是文件的数据部分,
* 所以我是无法知道该使用什么扩展名的,当然了,是可以把其它信息传输过来,
* 但是那样处理回麻烦一点。
* */
// 使用 UUID 生成随机的文件名
String filename = UUID.randomUUID().toString() + ".jpg";
// 实际读取到的字节数
int len = 0;
// 每次读取1024字节
byte[] b = new byte[1024];
// 创建一个输出流,保存接收到的文件数据
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(
new File("D:/DragonFile/tcp/short_tcp", filename)))) {
// 我在客户端使用了关闭了输出流,这里是可以使用 -1 来判断的!
while ((len = input.read(b)) != -1) {
out.write(b, 0, len);
}
// 这里不需要手动刷新,因为 try-with-resource
// 语句会自动关闭流,而关闭流的时候,会调用flush方法的。
// 注意这些细节问题。
}
output.write("The data has been received successfully".getBytes(StandardCharsets.UTF_8));
output.flush(); // 如果这里不进行手动刷新,会导致客户端读取到 -1 的异常!
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
说明:关于这部分的代码,就是很普通的socket传输文件,只是调用多次,传输多个文件而已。只要是学习过Socket网络编程的,基本上都没有问题了。
测试结果
服务器端
客户端
说明
第一次建立Socket连接的过程不算,因为它耗时太多了,但主要并不是传输的时间。我们看后面三次,分别是25ms、25ms、25ms,平均时间也就是25ms了。我这里不是精确计算,只能做一个大致的估计。
传输文件结果
传输文件的大小
这里只看总的大小了(虽然应该看每一个的大小或者md5值,但是应该还是没有问题的!)
TCP 长连接测试
这里长连接,就是一次性把六个文件的数据,通过一次连接传输过去,多个文件一起传输,如果不携带额外的控制信息的话,那么文件传输是没有意义的,因为谁也无法从字节流里面把文件分出来,这是一个数学上的问题了。不过它和我以前这篇博客倒是有些相似了,如果只有你自己知道,就可以去做一些有趣的事情了。文件合并(图片+视频),修改md5值,隐藏文件
所以,需要除了文件本身的字节数据以外的控制信息,这里我定义一个简单的协议。协议包含一个头部和数据部分,头部是8个字节的长度信息,数据部分就是相应长度的数据信息了。它很简单,不过在这里也足够用了。 具体可以参考我最上面推荐的博客,并且我的注释也写得很详细了。
TCP长连接客户端
package long_tcp;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
public class LongClient {
private static final String host = "127.0.0.1";
private static final int port = 10000;
public static void main(String[] args) {
// 存放需要传输文件的目录
Path path = Paths.get("D:", "DragonFile", "tcp", "targetFile");
File dir = path.toFile();
// 获取该目录下的所有文件
File[] files = dir.listFiles();
// 创建长连接客户端对象
LongClient longClient = new LongClient();
System.out.println("Start data transfer...");
long start = System.currentTimeMillis();
longClient.send(files);
System.out.println("6个文件使用tcp长连接的方式发送结束,总耗时:" + (System.currentTimeMillis()-start) + " ms。");
System.out.println("End of data transmission!");
}
public void send(File[] files) {
try (Socket client = new Socket(host, port)) {
// 获取输出流,用于发送数据
OutputStream output = new BufferedOutputStream(client.getOutputStream());
// 获取输入流,用于接收服务器的响应数据
InputStream input = new BufferedInputStream(client.getInputStream());
// 每次实际读取到的字节数
int len = 0;
// 每次读取1024字节的数据
byte[] b = new byte[1024];
for (File file : files) {
// 获取文件的长度
long length = file.length();
// 先将文件的长度写入socket
output.write(this.long2Byte(length));
try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
// 然后写入文件数据
while ((len = in.read(b)) != -1) {
output.write(b, 0, len);
}
}
// 手动刷新输出流
output.flush();
// 读取服务器响应
len = input.read(b);
// 打印服务器响应数据
System.out.println(new String(b, 0, len, StandardCharsets.UTF_8));
}
// 关闭输出流
client.shutdownOutput();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 使用长度为8个字节的数组保存long的值
public byte[] long2Byte(long length) {
byte[] b = new byte[8];
b[0] = (byte) (length >>> 56);
b[1] = (byte) (length >>> 48);
b[2] = (byte) (length >>> 40);
b[3] = (byte) (length >>> 32);
b[4] = (byte) (length >>> 24);
b[5] = (byte) (length >>> 16);
b[6] = (byte) (length >>> 8);
b[7] = (byte) (length >>> 0);
return b;
}
}
TCP长连接服务端
package long_tcp;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
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.nio.charset.StandardCharsets;
import java.util.UUID;
public class LongServer {
private static final int port = 10000;
public static void main(String[] args) {
new LongServer().start();
}
public void start() {
try (ServerSocket server = new ServerSocket(port)) {
// 这里因为只有一次连接,我其实是不需要使用死循环的,但是为了保证统一,还是使用吧
while (true) {
System.out.println("Waitting for client to connect...");
try (Socket client = server.accept()) {
InputStream input = new BufferedInputStream(client.getInputStream());
OutputStream output = new BufferedOutputStream(client.getOutputStream());
// 读取8个字节的长度,这里因为情况比较特殊,它是一定能读取到8个头的,
// 至于其它情况,暂时不考虑,就认为它一定能读取完整的8个字节,但是下面的1024就不一定了。
byte[] head = new byte[8];
// 读取数据部分
byte[] data = new byte[1024];
// 这里使用一个死循环来读取,记得要正确退出!
while (true) {
// 我这里认为它一定可以读取满8个字节的,因为我是本机测试,基本上没有问题的,
// 对于特殊的情况,我们不去考虑它,注意我们的关注点是传输的效率问题!
if (input.read(head) == -1) {
break; //如果为 -1,说明流已经关闭了
}
// 我这里转成int,因为我测试使用的图片不是太大,这样处理也方便一些
int length = (int) byte2Long(head);
System.out.println("length:" + length);
String filename = UUID.randomUUID().toString() + ".jpg";
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(
new File("D:/DragonFile/tcp/long_tcp", filename)))) {
int len = 0;
int hasRead = 0;
while ((len = input.read(data)) != -1) {
out.write(data, 0, len);
hasRead += len;
// 当文件最后一部分不足1024时,直接读取此部分,然后结束,文件已经读取完成了。
int remind = length - hasRead;
if (remind < 1024 && remind > 0) {
byte[] b = new byte[remind];
len = input.read(b); // 这里没有特殊原因,它也是可以读取remind长度的。
out.write(data, 0, len);
System.out.println("remind:" + remind + " len:" + len);
break; // 一个文件的数据已经读取完成了,及时退出该次读取
} else if (remind == 0) {
// 这里我有点疏忽了,没有考虑,文件小于1024字节时的读取
break;
}
}
// 响应客户端接收文件成功的数据
output.write("The data has been received successfully".getBytes(StandardCharsets.UTF_8));
output.flush(); // 刷新输出流
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public long byte2Long(byte[] b) {
return (((long)b[0] << 56) +
((long)(b[1] & 255) << 48) +
((long)(b[2] & 255) << 40) +
((long)(b[3] & 255) << 32) +
((long)(b[4] & 255) << 24) +
((b[5] & 255) << 16) +
((b[6] & 255) << 8) +
((b[7] & 255) << 0));
}
}
测试结果
客户端
服务端
注:这里我额外打印了获取到的文件的长度,因为长连接的处理比较麻烦,稍微有些问题没考虑到就不行了。
传输的文件
注意:不要看到文件就认为成功了,文件的大小也必须是一样的,即传输前和传输后的大小是一样的,更为严格的要求是文件的md5值是一样的,我这里为了简单期间就只看大小了。
传输的文件
传输文件的大小
说明
第一次建立Socket连接的过程不算,因为它耗时太多了,但主要并不是传输的时间。我们看后面三次,分别是23ms、22ms、25ms,平均时间也就是23.33ms了。我这里不是精确计算,只能做一个大致的估计。
结果分析
对于第一次建立连接发送数据,时间都是非常长的。但是对于这里到底是哪里耗时这么多,我暂时也不太清楚,它可能属于底层连接的问题了。我只比较后面三次的时间,但是感觉似乎差距也不是很大,顶多也就快了1ms多,但是它实在是太小了,让人觉得似乎长连接的效率也就是那样了!按照我一开始的理解,它应该会有一定的效率提升的。 所以,一定是哪里设计的不够合理,让我们来分析一下,对于整个传输过程来说,耗时主要是建立连接+传输数据了。但是如果数据量特别大,那它所占据的耗时百分比就越大,所以我这里传输的文件似乎太大了。反而掩盖了长连接所带来的减少建立连接的时间。比如传输10m文件,无论是长连接还是短连接效率上应该都是没有什么区别了,特别是网络特别慢的情况下,整个耗时都可以看做是传输耗时了,对于建立连接的耗时可以忽略不计了! 而且长连接其实还是比短连接多传输了一些控制信息,不然它是无法处理一个数据流为多个文件的。
新的测试文件
文件内容:
看我引经据典来写几句话吧,哈哈。
文件大小:
这次文件是很小了,保证了一次即可传输完成,这样应该就可以突出建立连接的时间了。
复制一下,还是传输六个文件
TCP短连接测试结果
这次我除了第一次外,再测试五次,然后统计这五次的平均时间。
总时间:17ms + 18ms + 19ms + 20ms + 18ms = 92ms
平均时间:92ms/5 = 18.4ms
TCP长连接测试结果
总时间:14ms + 16ms + 17ms + 17ms + 16ms = 80ms
平均时间:80ms/5 = 16ms
总结
这里虽然使用了小文件,但是似乎效果也不是很理想,因为我可能没有考虑到在本机进行环回测试,其实网速是很快的,所以其实我上面的图片也不算是大文件了。不过,我使用大文件效果是更不理想的,这样也算是可以了。
通过上面的比较,可以发现:平均每次传输可以减少2ms的时延,大概提高了 2ms/18ms = 11%。当然了,随着文件的增大,整个效率是会逐渐减少的,甚至于没什么效果了。但是,我们这里是应该可以认为,长连接确实是可以提高一定的效率的,特别是在含有大量的连接,但是传输的数据量特别少的情况下,它是很有效的,特别是某些应用广泛的协议——HTTP协议。 通常使用HTTP的客户端,都是属于数据量很小的传输,我想效果应该是有的,虽然长连接的处理也比较麻烦了。
PS:
比如,当我传输小文件时,代码无法正常工作了,直接卡死了。所以我就把服务器关闭了,最后传输花费了20357ms。问题主要出在在字节流中取出一个文件时发生的,我的注释里面写了详细情况,也可以看我推荐的博客。长连接的处理比较麻烦,我这里是一种简化的模拟,但是也要面对一些不好处理的问题!