TCP长连接和短连接代码及其比较

前言: 最近又看到了关于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。问题主要出在在字节流中取出一个文件时发生的,我的注释里面写了详细情况,也可以看我推荐的博客。长连接的处理比较麻烦,我这里是一种简化的模拟,但是也要面对一些不好处理的问题!
在这里插入图片描述

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于TCP的程序设计过程代码: 客户端: ```python import socket # 创建TCP连接 client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_address = ('localhost', 8000) client_socket.connect(server_address) # 发送数据 message = 'Hello, server!' client_socket.sendall(message.encode()) # 接收数据 data = client_socket.recv(1024) print('Received:', data.decode()) # 关闭连接 client_socket.close() ``` 服务器端: ```python import socket # 创建TCP连接 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_address = ('localhost', 8000) server_socket.bind(server_address) server_socket.listen(1) while True: # 等待客户端连接 print('Waiting for connection...') client_socket, client_address = server_socket.accept() # 接收数据 data = client_socket.recv(1024) print('Received:', data.decode()) # 发送数据 message = 'Hello, client!' client_socket.sendall(message.encode()) # 关闭连接 client_socket.close() ``` 基于UDP的程序设计过程代码: 客户端: ```python import socket # 创建UDP连接 client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server_address = ('localhost', 8000) # 发送数据 message = 'Hello, server!' client_socket.sendto(message.encode(), server_address) # 接收数据 data, server = client_socket.recvfrom(1024) print('Received:', data.decode()) # 关闭连接 client_socket.close() ``` 服务器端: ```python import socket # 创建UDP连接 server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server_address = ('localhost', 8000) server_socket.bind(server_address) while True: # 接收数据 data, client = server_socket.recvfrom(1024) print('Received:', data.decode()) # 发送数据 message = 'Hello, client!' server_socket.sendto(message.encode(), client) ``` 以上代码仅为示例,实际应用程序中需要根据具体需求进行修改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值