前言:这次实验用的是python语言,但是本人的python也不是很强,所以代码大部分也是借鉴别人的。实验本身还是很有趣的
一、实验目的
通过本实验,学习采用Socket(套接字)设计简单的网络数据收发程序,理解应用数据包是如何通过传输层进行传送的
二、实验内容
Socket(套接字)是一种抽象层,应用程序通过它来发送和接收数据,就像应用程序打开一个文件句柄,将数据读写到稳定的存储器上一样。一个socket允许应用程序添加到网络中,并与处于同一个网络中的其他应用程序进行通信。一台计算机上的应用程序向socket写入的信息能够被另一台计算机上的另一个应用程序读取,反之亦然。
不同类型的socket与不同类型的底层协议族以及同一协议族中的不同协议栈相关联。现在TCP/IP协议族中的主要socket类型为流套接字(sockets sockets)和数据报套接字(datagram sockets)。流套接字将TCP作为其端对端协议(底层使用IP协议),提供了一个可信赖的字节流服务。一个TCP/IP流套接字代表了TCP连接的一端。数据报套接字使用UDP协议(底层同样使用IP协议),提供了一个"尽力而为"(best-effort)的数据报服务,应用程序可以通过它发送最长65500字节的个人信息。一个TCP/IP套接字由一个互联网地址,一个端对端协议(TCP或UDP协议)以及一个端口号唯一确定。
2.1、采用TCP进行数据发送的简单程序(java/python3.5)
2.2、采用UDP进行数据发送的简单程序(java/python3.5)
2.3、多线程\线程池对比(java/python3.5)
当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时,虽然其在连接建立后即可向服务器端发送数据,服务器端在处理完已有客户端的请求前,却不会对新的客户端作出响应。
并行服务器:可以单独处理没一个连接,且不会产生干扰。并行服务器分为两种:一客户一线程和线程池。
每个新线程都会消耗系统资源:创建一个线程将占用CPU周期,而且每个线程都自己的数据结构(如,栈)也要消耗系统内存。另外,当一个线程阻塞(block)时,JVM将保存其状态,选择另外一个线程运行,并在上下文转换(context switch)时恢复阻塞线程的状态。随着线程数的增加,线程将消耗越来越多的系统资源。这将最终导致系统花费更多的时间来处理上下文转换和线程管理,更少的时间来对连接进行服务。那种情况下,加入一个额外的线程实际上可能增加客户端总服务时间。
我们可以通过限制总线程数并重复使用线程来避免这个问题。与为每个连接创建一个新的线程不同,服务器在启动时创建一个由固定数量线程组成的线程池(thread pool)。当一个新的客户端连接请求传入服务器,它将交给线程池中的一个线程处理。当该线程处理完这个客户端后,又返回线程池,并为下一次请求处理做好准备。如果连接请求到达服务器时,线程池中的所有线程都已经被占用,它们则在一个队列中等待,直到有空闲的线程可用。
2.4写一个简单的chat程序,并能互传文件,编程语言不限。
三、设计结构
由于是双向通信,所以Socket的工作流程需要一对套接字连接进行使用,一个是作为服务端(Server),一个是作为客户端(Client)。程序的主要设计框架和流程主要如上图所示。
服务端使用 socket()创建套接字之后,通过bind()方法绑定端口,然后使用listen()对端口进行阻塞式地监听,等待客户端发来建立连接的请求。当接收到建立连接的请求时,使用accept()方法接受客户端的连接请求,此后进入recv()和send()不断进行接收数据和发送数据的操作。最后,使用close()关闭套接字终止程序,不过服务端程序一般不会主动进行关闭。
客户端相对来说比较简单,同样使用socket()和close()来创建和关闭套接字。客户端使用connect()向目标的地址和端口发出建立连接的请求,建立连接成功之后就会进入recv()和send()中不断进行接收数据和发送数据的操作。
四、实验过程
1.采用TCP进行数据发送的简单程序
TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
TCP通信需要经过创建连接、数据传送、终止连接三个步骤。
TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中打电话。
1.客户端
建设客户端的步骤如下:
1)导入socket模块
2)创建TCP套接字
3)建立连接(与服务端)
4)发数据
5)关闭套接字
1.使用TCP的网络连接,需要用到python中的socket模块,所以首先需要在代码中,将它导入进来:
import socket
2.初始化套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3.连接服务器,使用本地的ip地址和随机一个端口号即可,这里我设置为6666
tcp_socket.connect(('10.63.235.139', 6666))
4.发送数据,这里发送“hello”
tcp_socket.send('hello'.encode())
5.断开连接
tcp_socket.close()
2.服务端
建设服务端的步骤如下:
1.导入socket模块
2.创建TCP套接字
3.绑定ip和port
4.设置为被动监听模式,最大并发接收的数量是128(设置监听模式才能是服务器)
5.等待接收链接请求,接收到的是一个元组 (客户端的socket对象包含了:(客户端的地址,端口))
6.关闭套接字
1.导入socket模块
import socket
2.初始化套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3.服务器绑定ip和port
tcp_socket.bind(('10.63.235.139', 6666))
4.设置为被动监听模式 最大并发接收的数量是128
tcp_socket.listen(128)
5.等待接收链接请求 接收到一个元组
client, addr = tcp_socket.accept()
接收要用客户端的socket对象接收,因为发的时候就是用他的对象发的
data = client.recv(1024)
发送的格式是字节,要解码,decode()默认gbk
print(data.decode())
6.断开连接
tcp_socket.close()
这样客户端和服务端就可以实现简单的通信了,先运行服务端再运行客户端,可以在服务端收到“hello”字符串,运行结果如下:
可以看到,服务端只接受了一次数据就关闭连接了,现在对服务端进行进一步完善。设置一个循环,不断地等待接受链接请求,从而实现接收多组数据。同时对于客户端,其可以发送数据,也可以使用socket接受数据,最终的服务端和客户端代码如下:
客户端:
import socket
addr = ('10.63.39.151', 6666)
while True:
# 创建tcp客户端套接字
# 1. AF_INET:表示ipv4
# 2. SOCK_STREAM: tcp传输协议
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 和服务端应用程序建立连接
tcp_socket.connect(addr)
# 输入发送的数据
data = input('客户端发送:')
# 若输入'exit'则断开连接
if data=='exit':
print('断开连接……')
break
# 发送数据
tcp_socket.sendall(str('客户端发送:' + data).encode())
print('客户端等待中……')
# recv(buffersize) 表示接收数据, buffersize是每次接收数据的长度,这里接受1024字节数据
server_recv = tcp_socket.recv(1024)
# 编码格式默认为gbk
print(server_recv.decode())
# 关闭套接字
tcp_socket.close()
服务端:
import socket
addr = ('10.63.39.151', 6666)
# 创建tcp服务端套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立即释放
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 给程序绑定端口号
tcp_socket.bind(addr)
# 设置监听
# 128:最大等待建立连接的个数, 提示: 目前是单任务的服务端,同一时刻只能服务与一个客户端,后续使用多任务能够让服务端同时服务与多个客户端,
# 不需要让客户端进行等待建立连接
# listen后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字来完成
tcp_socket.listen(128)
while True:
print('服务端等待中……')
# 等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
# 1. 专门和客户端通信的套接字: client_socket
# 2. 客户端的ip地址和端口号: client_addr
client_socket, client_addr = tcp_socket.accept()
# 接收客户端发送的数据, 这次接收数据的最大字节数是1024
client_data = client_socket.recv(1024)
print(client_data.decode())
message = input('服务端发送:')
client_socket.sendall(str('服务端发送:' + message).encode())
# 关闭服务端与客户端的套接字, 终止和客户端通信的服务
client_socket.close()
# 关闭服务端的套接字, 终止和客户端提供建立连接请求的服务
tcp_socket.close()
运行结果如下:
2.采用UDP进行数据发送的简单程序(python3.5)
UDP——用户数据报协议(User Datagram Protocol),是一个无连接的简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
使用UDP进行数据的发送和使用TCP进行数据的发送很多地方是相同的,这里列举一些不同的地方:
1.创建套接字
套接字创建的格式为:
udp_socket = socket.socket(参数1, 参数2)
- 参数1:family(给定的套接族)一般有两种重要参数
- socket.AF_INET(用于服务器与服务器之间的网络通信)
- socket.AF_INET6 (基于IPV6方式的服务器与服务器之间的网络通信)
- 参数2:type(套接字类型),也是一般两个类型
- socket.SOCK_STREAM(基于TCP的流式socket通信)
- socket.SOCK_DGRAM(基于UDP的数据报式socket通信)
由于创建的是UDP套接字,所以选择的参数为:
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
2.发送数据
UDP发送数据使用的函数为sendto,其格式为:
udp_socket.sendto(参数1).encode(参数2,('参数3',参数4))
- 参数1:表示发送数据的内容
- 参数2:表示编码格式
- 参数3:表示目的地ip
- 参数4:表示目的地端口
3.接受数据
UDP接收数据采用的函数是recvfrom,而TCP采用的是recv,不过函数里面的参数都是要接受的字符的字节长度。recvfrom接收的不仅是数据,也包括发送方的ip地址和端口号信息,可以用于回复信息。
4.UDP的服务端中不需要设置监听模式,也不需要建立连接
其余数据传输内容,与基于TCP的传输一致。代码如下:
服务端:
import socket
addr = ('10.63.39.151', 6666)
# 创建udp服务端套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置端口号复用,让程序退出端口号立即释放
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 给程序绑定端口号
udp_socket.bind(addr)
while True:
print('服务端等待中……')
client_data, client_addr = udp_socket.recvfrom(1024)
# 接收客户端发送的数据, 这次接收数据的最大字节数是1024
print(client_data.decode())
message = input('服务端发送:')
udp_socket.sendto(str('服务端发送:' + message).encode(),client_addr)
# 关闭服务端的套接字, 终止和客户端提供建立连接请求的服务
udp_socket.close()
客户端:
import socket
addr = ('10.63.39.151', 6666)
while True:
# 创建udp客户端套接字
# 1. AF_INET:表示ipv4
# 2. SOCK_DGRAM: udp传输协议
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 输入发送的数据
data = input('客户端发送:')
# 若输入'exit'则断开连接
if data=='exit':
print('断开连接……')
break
# 发送数据
udp_socket.sendto(str('客户端发送:' + data).encode(),addr)
print('客户端等待中……')
# recv(buffersize) 表示接收数据, buffersize是每次接收数据的长度,这里接受1024字节数据
server_data,server_addr = udp_socket.recvfrom(1024)
# 编码格式默认为gbk
print(server_data.decode())
# 关闭套接字
udp_socket.close()
运行结果如下:
客户端页面:
服务端页面:
对TCP和UDP的总结:
- tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器。
- tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip、port等信息就好,本地客户端可以随机。
- tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的。
- 当客户端需要链接服务器时,就需要使用connect进行链接,udp是不需要链接而是直接发送,但是tcp必须先链接,只有链接成功才能通信。
- 当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务。
- listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的。
- 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信。
- 关闭accept返回的套接字意味着这个客户端已经服务完毕。
- 当客户端的套接字调用close后,服务器端会recv解堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线。
3.多线程\线程池对比
多线程的服务端设计流程如下所示:
与单线程不同之处在于当每次建立起一个新的连接时,就使用threading模块创建一个新的线程,向新的线程中传入该客户端套接字的信息,并保持通信,同时该线程需要通过thread.setDaemon(True)设置为守护主线程。
代码如下:
import socket
import threading
addr=('10.72.22.194',6666)
def fun(client_socket, client_addr):
sentence = client_socket.recv(1024).decode()
print("服务端收到来自 " + str(client_addr)+" 的信息")
print("客户端 " + str(client_addr) + " 的内容是:" + sentence)
client_socket.send('hi'.encode())
print("服务端已向 " + str(client_addr)+" 回复信息")
client_socket.close()
print("服务端与客户端 " + str(client_addr) + " 的连接已关闭")
if __name__ == '__main__':
server_socket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
server_socket.bind(addr)
server_socket.listen(128)
print("服务端等待连接中……")
while True:
client_socket,client_addr = server_socket.accept()
print(f'客户端{client_addr}连接成功\n')
thread = threading.Thread(target=fun, args=(client_socket, client_addr))
thread.setDaemon(True) # 设置守护主线程
thread.start()
客户端的实现基于多线程。Fun为线程的工作函数,其核心功能为创建客户端套接字,使该套接字与服务器建立TCP连接,向服务器的套接字发送数据,接受服务端套接字传输的数据。客户端基于多线程实现数据发送可以有效地模拟真实生产环境下,不同PC主机向服务器发送数据的状态,从而对多线程/线程池的工作效果加以分析。
代码如下:
import socket
import threading
addr=('10.72.22.194',6666)
def fun(number):
clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientSocket.connect(addr)
print("客户端 " + str(number) + " 与服务端成功建立连接")
clientSocket.send("hello".encode())
print("客户端 " + str(number) + " 已经成功发送信息")
data = clientSocket.recv(1024).decode()
print("客户端 " + str(number) + " 收到回复信息")
clientSocket.close()
print("客户端 " + str(number) + " 与服务端的连接断开")
for number in range(10):
thread = threading.Thread(target=fun, args=(number,))
thread.start()
运行结果如下:
线程池:
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
在python中我们可以使用 ThreadPoolExecutor 来实例化线程池对象。传入max_workers参数来设置线程池中最多能同时运行的线程数目。我们可以使用 submit 函数来提交线程需要执行的任务(函数名和参数)到线程池中,并返回该任务的抽象对象,注意 submit() 不是阻塞的,而是立即返回。通过 submit 函数返回的任务抽象对象,能够使用其 done() 方法判断任务是否结束。我们通过ThreadPoolExecutor()创建了一个最大工作线程数为10的线程池。
若将服务器端线程池最大工作线程数设置为比客户端请求线程总数小,由于线程池只会维护最大工作线程数的线程进行工作,因此,当线程池的线程已满时,后到的任务需要排队等待线程池对其进行工作调度。
线程池客户端代码:
由于和多线程的客户端完成相同的功能,所以线程池的客户端代码与多线程的相同。
线程池服务端代码:
import socket
from concurrent.futures import ThreadPoolExecutor
addr=('10.72.22.194',6666)
def fun(client_socket, client_addr):
sentence = client_socket.recv(1024).decode()
print("服务端收到来自 " + str(client_addr)+" 的信息")
print("客户端 " + str(client_addr) + " 的内容是:" + sentence)
client_socket.send('hi'.encode())
print("服务端已向 " + str(client_addr)+" 回复信息")
client_socket.close()
print("服务端与客户端 " + str(client_addr) + " 的连接已关闭\n")
if __name__ == '__main__':
server_socket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
server_socket.bind(addr)
server_socket.listen(128)
print("服务端等待连接中……")
pool = ThreadPoolExecutor(max_workers=10)
while True:
client_socket,client_addr = server_socket.accept()
pool.submit(fun, client_socket, client_addr)
print(f'客户端{client_addr}连接成功\n')
运结果如下:
客户端:
服务端:
多线程/线程池的比较:
- 线程池是在程序运行开始,创建好的n个线程,并且这n个线程挂起等待任务的到来。而多线程是在任务到来得时候进行创建,然后执行任务。
- 线程池中的线程执行完之后不会回收线程,会继续将线程放在等待队列中;多线程程序在每次任务完成之后会回收该线程。
- 由于线程池中线程是创建好的,所以在效率上相对于多线程会高很多。
- 线程池也在高并发的情况下有着较好的性能;不容易挂掉。多线程在创建线程数较多的情况下,很容易挂掉。
4.写一个简单的chat程序,并能互传文件,编程语言不限。
由题目可知,设置的核心点在与三个功能:发送和接受信息,发送文件,接受文件。发送和接受信息上面三题已经实现了,主要要实现的模块就是发送文件和接受文件这部分。
客户端设计步骤:
1.聊天功能chat
参照第一部分tcp通信的客户端即可
if FLAG=='1': # 聊天模式
client_socket.sendall('1'.encode('utf-8'))
print('--------开始聊天--------')
while True:
data = input("> ")
if data == 'exit':
client_socket.send('exit'.encode('utf-8')) # 客户端发送停止消息
print('--------结束聊天--------')
break
elif not data:
continue
client_socket.send(data.encode('utf-8')) # 客户端发送消息
rev_data = client_socket.recv(1024) # 客户端接受消息
if not rev_data:
break
print('服务器:', rev_data.decode('utf-8'))
2.发送文件
首先客户端先向服务端发送信号,让服务端端进入接受状态
client_socket.sendall('2'.encode('utf-8'))
接着在客户端输入想要发送的文件路径
path = input('文件路径:')
根据用户输入的路径查找文件名,查找不到会报错退出
file_name = os.path.basename(path)
文件的大小是不确定的,而实际传输中往往使用的是缓冲区,如果不能够事先知道文件的大小,会导致传输的数据出错,因此,需要先获取文件名和大小,并打包起来。
file_size = os.stat(path).st_size
Informf = (file_name + '|' + str(file_size))
向服务端发送文件名和文件大小
client_socket.send(Informf.encode())
为了防止粘包,将文件名和大小发送过去之后,等待服务端收到,直到从服务端接受一个信号(说明服务端已经收到),这里我设置的回送消息是‘服务器收到文件’
msg = client_socket.recv(1024)
print(msg.decode())
接着以读取二进制文件的形式打开路径下的文件
f = open(path, 'rb')
通过语句msg=client_socket.recv(1024),指定了缓冲区的大小为1024字节,因此需要对于文件循环读取,直到文件的数据填满了一个缓冲区,此时将缓冲区数据发送出去,继续读取下一部分文件;或是当缓冲区未填满,而文件读取完毕,此时应当将这个未满的缓冲区发送给服务器。
while Flag:
if send_size + 1024 > file_size:
data = f.read(file_size - send_size)
Flag = False
else:
data = f.read(1024)
send_size += 1024
client_socket.send(data)
最后,服务器指定一个路径,将该文件传输到该路径下。
3.接收文件
接收文件部分和发送文件部分大部分是相同的。主要都包括了一下几个步骤
- 获取请求方法、文件名、文件大小
- 防止粘包,给客户端发送一个信号。
- 已经接收文件的大小
- 接收的文件路径拼接
- 上传文件,如果文件大小大于缓冲区的大小,则分段上传写入文件,上传完成退出循环
- 关闭文件
代码如下:
print('--------接收文件--------')
client_socket.sendall('3'.encode('utf-8'))
message = client_socket.recv(1024)
print(message.decode(encoding='utf8'))
message = input('path: ')
message = str(message)
# 发送数据 加码
client_socket.sendall(message.encode('utf-8'))
print('客户端等待...')
pre_data = client_socket.recv(1024).decode()
# 获取请求方法、文件名、文件大小
file_name, file_size = pre_data.split('|') # 服务器的解包
# 防止粘包,给客户端发送一个信号。
client_socket.sendall('客户端收到文件 '.encode())
# 已经接收文件的大小
recv_size = 0
file_dir = os.path.join('D:\\', message) # 下载文件路径拼接
print('收到文件', file_name)
f = open(file_dir, 'wb')
Flag = True
while Flag:
# 未上传完毕,
if int(file_size) > recv_size:
# 最多接收1024,可能接收的小于1024
data = client_socket.recv(1024)
recv_size += len(data)
# 写入文件
f.write(data)
# 上传完毕,则退出循环
else:
recv_size = 0
Flag = False
client_socket.sendall('下载完成'.encode()) # 新增发送
print('接收成功')
f.close()
print('--------结束接收--------')
对于服务端的设计,其实就是客户端的功能的设计的反过来,客户端发送文件时服务端接收文件,客户端接收文件时服务端发送文件,因此两者的设计步骤和思路一样。
完整代码如下:
客户端:
import socket
import os
addr = ('10.72.22.194', 6666)
container = {'key': '', 'data': ''} # 文件路径的剪切
while True:
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(addr)
print("1.聊天 2.发送文件 3.接收文件")
FLAG=input('选择功能:')
if FLAG=='1': # 聊天模式
client_socket.sendall('1'.encode('utf-8'))
print('--------开始聊天--------')
while True:
data = input("> ")
if data == 'exit':
client_socket.send('exit'.encode('utf-8')) # 客户端发送停止消息
print('--------结束聊天--------')
break
elif not data:
continue
client_socket.send(data.encode('utf-8')) # 客户端发送消息
rev_data = client_socket.recv(1024) # 客户端接受消息
if not rev_data:
break
print('服务器:', rev_data.decode('utf-8'))
elif FLAG == '2': # 发送文件,即原本的功能
client_socket.sendall('2'.encode('utf-8'))
print('--------发送文件--------')
path = input('文件路径:') # 客户端输入要上传文件的路径
file_name = os.path.basename(path) # 根据路径获取文件名
file_size = os.stat(path).st_size # 获取文件大小
Informf = (file_name + '|' + str(file_size)) # 客户端的打包
client_socket.send(Informf.encode()) # 发送文件名 和 文件大小
# 为了防止粘包,将文件名和大小发送过去之后,等待服务端收到,直到从服务端接受一个信号(说明服务端已经收到)
msg = client_socket.recv(1024)
print(msg.decode())
send_size = 0
f = open(path, 'rb')
Flag = True
while Flag:
if send_size + 1024 > file_size:
data = f.read(file_size - send_size)
Flag = False
else:
data = f.read(1024)
send_size += 1024
client_socket.send(data)
data1 = client_socket.recv(1024) # 新增接收
print(data1.decode())
print('--------结束发送--------')
f.close()
elif FLAG == '3': # 接收
print('--------接收文件--------')
client_socket.sendall('3'.encode('utf-8'))
message = client_socket.recv(1024)
print(message.decode(encoding='utf8'))
message = input('path: ')
message = str(message)
# 发送数据 加码
client_socket.sendall(message.encode('utf-8'))
print('客户端等待...')
# 之前的Srv
pre_data = client_socket.recv(1024).decode()
# 获取请求方法、文件名、文件大小
file_name, file_size = pre_data.split('|') # 服务器的解包
# 防止粘包,给客户端发送一个信号。
client_socket.sendall('客户端收到文件 '.encode())
# 已经接收文件的大小
recv_size = 0
file_dir = os.path.join('D:\\', message) # 下载文件路径拼接
print('收到文件', file_name)
f = open(file_dir, 'wb')
Flag = True
while Flag:
# 未上传完毕,
if int(file_size) > recv_size:
# 最多接收1024,可能接收的小于1024
data = client_socket.recv(1024)
recv_size += len(data)
# 写入文件
f.write(data)
# 上传完毕,则退出循环
else:
recv_size = 0
Flag = False
client_socket.sendall('下载完成'.encode()) # 新增发送
print('接收成功')
f.close()
print('--------结束接收--------')
else:
print("输入错误,请重新输入!")
print('---------------------------')
sk.close()
服务端:
import socketserver
import os
class MyServer(socketserver.BaseRequestHandler):
def handle(self):
conn = self.request
print('connected...')
while True:
order = conn.recv(1024).decode(encoding='utf8') # 增加读取
if order == '1':
print('--------开始聊天--------')
Flag = True
while Flag:
data = conn.recv(1024) # 接受客户端发送的消息
print('客户端:', data.decode())
if not data:
continue
elif data.decode() == 'exit':
Flag = False
print('--------结束聊天--------')
message = input('> ')
conn.send(message.encode()) # 客户端发送消息
if order == '2': # 接收文件,即原本的任务
print('--------接收文件--------')
pre_data = conn.recv(1024).decode()
# 获取请求方法、文件名、文件大小
file_name, file_size = pre_data.split('|') # 服务器的解包
# 防止粘包,给客户端发送一个信号。
conn.sendall('服务器收到文件'.encode())
# 已经接收文件的大小
recv_size = 0
file_dir = os.path.join('D:\桌面', file_name) # 接收的文件路径拼接
print("文件路径:", file_dir)
f = open(file_dir, 'wb')
Flag = True
while Flag:
# 未上传完毕,
if int(file_size) > recv_size:
# 最多接收1024,可能接收的小于1024
data = conn.recv(1024)
recv_size += len(data)
# 写入文件
f.write(data)
# 上传完毕,则退出循环
else:
recv_size = 0
Flag = False
conn.sendall('服务器下载完成'.encode()) # 添加发送
print('上传成功')
f.close()
print('--------结束接收--------')
if order == '3': # 发送文件
print('--------发送文件--------')
conn.sendall('服务器准备发送文件'.encode('utf-8'))
# 原来的Clt
# path = input('path:') # 客户端输入要上传文件的路径
path = conn.recv(1024).decode() # 这里路径是从客户端读取的
# file_name = os.path.basename(path) # 根据路径获取文件名
file_name = os.path.join('D:\\\\', path) # 拼接
print('文件路径:', file_name)
file_size = os.stat(file_name).st_size # 获取文件大小
Informf = (file_name + '|' + str(file_size)) # 包装文件名 和 文件大小
conn.send(Informf.encode()) # 发送
# 为了防止粘包,将文件名和大小发送过去之后,等待服务端收到,直到从服务端接受一个信号(说明服务端已经收到)
bbb = conn.recv(1024).decode()
print(bbb)
send_size = 0
f = open(file_name, 'rb')
Flag = True
while Flag:
if send_size + 1024 > file_size:
data = f.read(file_size - send_size)
Flag = False
else:
data = f.read(1024)
send_size += 1024
conn.send(data)
conn.recv(1024) # 新增接收
f.close()
print('--------结束发送--------')
instance = socketserver.ThreadingTCPServer(('10.72.22.194', 6666), MyServer)
instance.serve_forever()
运行结果如下:
1.聊天
客户端:
服务端:
2.发送文件
客户端:
服务端:
3.接收文件
客户端:
服务端:
可以看到,程序成功实现了服务端和客户端相互传输文件和聊天的功能
五、实验总结
通过这次实验,我学会了学习采用 Socket(套接字)设计简单的网络数据收发程序,理解应用数据 包是如何通过传输层进行传送的。能够实现设计python程序设计出基于tcp和udp的客户端和服务端之间的通信和传输文件的功能。
在这次实验也遇到了很多问题,比如对于python环境配置的不熟悉,python编程能力的不足和对TCP的理解不够深等,通过后面的查询资料和询问老师同学得以