(验收完实验了,发篇文章记录一下),建议大家如果记着做实验的话就不要看原理部分了
计算机实验2-网络基础编程
目的与要求:
通过编写网络通信应用程序,理解套接字编程的基本方法,掌握不同类型套接字通信的实现机制,具备初步设计应用层协议的能力。
重点:
TCP、UDP套接字通信的实现机制与不同特性。
难点:
多线程套接字编程
内容:
采用Java、C++等任一语言开发一个多线程网络应用程序,包括客户端、服务器端和应用层协议,完成教材习题的第31和32题(原书第6版)。
实验原理
本次实验主要基于 TCP实现,介绍TCP是必要的,另一个程序是在UDP上实现
TCP原理
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输协议。它是互联网协议套件中重要的传输层协议之一,负责在网络上可靠地传输数据。
TCP协议的主要特点包括:
- 面向连接:在通信前,发送方和接收方需要建立一个连接,并保持连接状态直到通信结束。这种连接方式可以保证数据的可靠性和顺序性。
- 可靠性:TCP使用确认机制来确保数据的可靠传输。接收方会向发送方发送确认信息,告知已经成功接收到数据。如果发送方在一定时间内未收到确认信息,将进行超时重传。
- 流式传输:TCP采用字节流方式进行数据传输,将数据划分为以字节为单位的流,而不是固定大小的数据块。这样可以更灵活地处理数据的传输和接收。
- 拥塞控制:TCP具有拥塞控制机制,以避免网络拥塞情况下的数据丢失和性能下降。通过动态调整发送速率和接收窗口大小,TCP可以根据网络的负载情况进行自适应调整。
- 有序性:TCP保证数据的有序传输,即按照发送的顺序进行接收。通过对数据包进行编号和排序,接收方可以按照正确的顺序恢复原始数据。
- 可全双工通信:TCP允许同时进行双向的数据传输,发送方和接收方可以在同一时间内发送和接收数据。
TCP协议提供了可靠的、有序的数据传输,并具有拥塞控制机制,使得它成为广泛应用于互联网和局域网中的重要传输协议。
TCP通信机制(C-S模式)
客户具有向服务器发起接触的任务。服务器为了能够对客户的初始接触做出反应,服务器必须已经准备好。这意味着两件事。第一,与在UDP中的情况一样,TCP服务器在客户试图发起接触前必须作为进程运行起来。第二,服务器程序必须具有一扇特殊的门,更精确地说是一个特殊的套接字,该门欢迎来自运行在任意主机上的客户进程的某种初始接触。使用房子与门来比喻进程与套接字,有时我们将客户的初始接触称为“敲欢迎之门”。
随着服务器进程的运行,客户进程能够向服务器发起一个TCP连接。这是由客户程序通过创建一个TCP套接字完成的。当该客户生成其TCP套接字时,它指定了服务器中的欢迎套接字的地址,即服务器主机的IP地址及其套接字的端口号。生成其套接字后,该客户发起了一个三次握手并创建与服务器的一个TCP连接。发生在运输层的三次握手,对于客户和服务器程序是完全透明的。
在三次握手期间,客户进程敲服务器进程的欢迎之门。当该服务器“听”到敲门声时,它将生成一扇新门(更精确地讲是一个新套接字),它专门用于特定的客户。在我们下面的例子中,欢迎之门是一个我们称为serverSocket的TCP套接字对象;它是专门对客户进行连接的新生成的套接字,称为连接套接字(connectionSocket)。初次遇到TCP套接字的学生有时会混淆欢迎套接字(这是所有要与服务器通信的客户的起始接触点)和每个新生成的服务器侧的连接套接字(这是随后为与每个客户通信而生成的套接字)。
从应用程序的观点来看,客户套接字和服务器连接套接字直接通过一根管道连接。客户进程可以向它的套接字发送任意字节,并且TCP保证服务器进程能够按发送的顺序接收(通过连接套接字)每个字节。TCP因此在客户和服务器进程之间提供了可靠服务。此外,就像人们可以从同一扇门进和出一样,客户进程不仅能向它的套接字发送字节,也能从中接收字节;类似地,服务器进程不仅从它的连接套接字接收字节,也能向其发送字节。
大致的流程如下:
在python中为了实现客户服务器之间的通信可通过如下示例代码来实现
服务器端
from socket import *
port = 8888
serversocket = socket(AF_INET, SOCK_STREAM) #指定为TCP套接字创建“欢迎之门”,AF_INET表示IPV4
serversocket.bind(('', port))#将该套接字与端口进行绑定
serversocket.listen(5)#监听端口,其中的数字表示最多能够聆听5个客户敲门
print("ready to receive")
while True:
connSocket, addr = serversocket.accept()#serversocket相当于“欢迎之门”,connsocket为连接套接字
print(f"新连接建立:主机为:{addr[0]},端口号:{addr[1]}")
sentence = connSocket.recv(1024).decode()#从缓冲区中接收要处理数据
#以下代码可根据功能进行改动
modifiSentence = sentence.upper()
connSocket.send(modifiSentence.encode())#发送处理完的数据
connSocket.close()
代码中较为特殊的函数为 connSocket, addr = serversocket.accept()
该函数为阻塞函数, 当客户敲该门时,程序为serverSocket 调用accept()方法,这在服务器中创建了一个称为connSocket的新套接字,由这个特定的客户专用。客户和服务器则完成了握手,在客户clientSocket 和服务器的serverSocket 之间创建了一个 TCP 连接。借助于创建的TCP连接,客户与服务器现在能够通过该连接相互发送字节。使用TCP,从一侧发送的所有字节不仅确保到达另一侧,而且确保按序到达。
因为TCP通信机制中是对字节进行传输因此需要编码传输,解码读取。
客户端
from socket import *
port = 8888
servername = 'localhost'#指定服务器地址
clientsocket = socket(AF_INET, SOCK_STREAM)#指定为TCP服务,AF_INET表示IPV4
clientsocket.connect((servername,port))#与该服务器以及相应端口进行连接
sentence = input("请输入:")
clientsocket.send(sentence.encode())#发送要处理数据
modified = clientsocket.recv(1024)#接收处理过的数据
print (modified.decode())
clientsocket.close()
用户端代码较为简单不过多解释
示例代码中实现的功能是实现用户输入一串字母服务器返回一个大写的字母并发送给用户
代码所实现的流程大致如下:
UDP原理
UDP(User Datagram Protocol,用户数据报协议)是一种网络传输协议,它是在IP协议的基础上进行了简化而产生的。UDP是一种无连接的协议,不需要建立和维护连接,因此传输效率较高。
UDP的特点是传输速度快、延迟低,适用于对实时性要求较高的应用,如音频、视频等。它不保证数据的可靠性,传输过程中可能会发生丢包、重复、乱序等问题,因此不适用于对数据完整性要求较高的应用,如文件传输。
UDP的数据包称为用户数据报(User Datagram),每个数据报包含源端口号、目标端口号、数据长度和校验和等信息。UDP不提供流量控制、拥塞控制和错误恢复等机制,而是将这些功能交给应用程序来处理。因此,UDP的实现较为简单、轻量,占用的资源较少。
总之,UDP是一种快速、简单的网络传输协议,适用于对实时性要求较高、对数据完整性要求较低的应用场景。
UDP通信机制(C-S模式)
现在仔细观察使用UDP套接字的两个通信进程之间的交互。在发送进程能够将数据分组推出套接字之门之前,当使用UDP时,必须先将目的地址附在该分组之上。在该分组传过发送方的套接字之后,因特网将使用该目的地址通过因特网为该分组选路到接收进程的套接字。当分组到达接收套接字时,接收进程将通过该套接字取回分组,然后检查分组的内容并采取适当的动作。
目的主机的IP地址是目的地址的一部分。通过在分组中包括目的地的I地址,因特网中的路由器将能够通过因特网将分组选路到目的主机。但是因为一台主机可能运行许多网络应用进程,每个进程具有一个或多个套接字,所以在目的主机指定特定的套接字也是必要的。当生成一个套接字时,就为它分配一个称为端口号(port number)的标识符。因此,分组的目的地址也包括该套接字的端口号。总的来说,发送进程为分组附上目的地址,该目的地址是由目的主机的IP地址和目的地套接字的端口号组成的。发送方的源地址也是由源主机的IP地址和源套接字的端口号组成,该源地址也要附在分组之上。然而,将源地址附在分组之上通常并不是由UDP应用程序代码所为,而是由底层操作系统自动完成的。
UDP通信示例代码:
服务器端:
from socket import *
port = 12000
serversocket = socket(AF_INET,SOCK_DGRAM)#创建了UDP套接字
serversocket.bind(('',port))#将套接字与端口绑定
print("ready to receive")
while True:
message,clientAddress = serversocket.recvfrom(2048)#获取客户的地址和所要处理的数据
#功能部分(可改)
modifiedmessage = message.decode().upper()
serversocket.sendto(modifiedmessage.encode(),clientAddress)#返回处理过的数据
因为UDP为无连接服务因此需要从套接字中获取客户端的地址及相应端口,以便将处理过后的数据返回,相比TCP服务端代码UDP并不需要监听端口,以及不用再生成连接套接字
客户端:
from socket import *
server_name = "localhost"#指定服务器地址
serverport = 12000#指定要发向的服务器端口
clientsocket = socket(AF_INET, SOCK_DGRAM)#创建UDP套接字
while True:
message = input("请输入:")
if message == "exit":
break
clientsocket.sendto(message.encode(), (server_name, serverport))#向服务器的响应地址发送
modifiedMessage, serverAddress = clientsocket.recvfrom(2048)#接收处理过后的数据
print(modifiedMessage.decode())
clientsocket.close()
UDP客户端示例代码和TCP示例代码差不多,TCP的客户端不需要在sendto函数中加入服务器地址参数
该示例代码执行流程如下:
功能概述
本次实验所用的代码为python,开发的程序为实现一个聊天室,并具有文件传输功能(基于TCP),另一个程序将输入的句子变为大写输出(基于UDP)
实验内容
带有文件传输功能的聊天室
本次聊天室程序的大致思想是利用客户-服务器体系结构实现信息的传输,其中一个客户端发消息后,将消息传输至服务器,由服务器广播到所有客户端,因为文件和消息传输需要可靠的数据传输服务因此选择TCP
服务器端
首先定义了服务器类
class Server:
def __init__(self,buffer_size=1024):
self.host = '127.0.0.1'#服务器地址
self.port = 12001#服务器端口
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)#"欢迎之门"套接字
self.conns = []#不止一个连接,需要链表来存,以便将消息向所有客户端发送
self.buffer_size = buffer_size#接收缓冲区的大小
接下来在类中实现方法
首先需要开启服务器
def start(self):
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)
print('开始服务 {}:{}'.format(self.host, self.port))
while True:
connection_socket, address = self.server_socket.accept()
client_thread = threading.Thread(target=self.handle_client, args=(connection_socket,))
client_thread.start()
client_thread = threading.Thread(target=self.handle_client, args=(connection_socket,))
为每个客户端创建了一个线程专门用来所发的请求,实现了多线程,可并行处理用户的请求
接下来是实现handle_client程序
def handle_client(self, connection_socket):
self.conns.append(connection_socket)
print('新客户端连接:', connection_socket.getpeername())#打印出是那台主机和端口与本服务器发生了连接
while True:
try:
message = connection_socket.recv(self.buffer_size).decode()
if message.split(":")[0] == "FILE":#客户想要发送文件
client_name = message.split(":")[1]
filename = message.split(":")[2]
file_size = int(message.split(":")[3])
# threading.Thread(target=self.receive_file, args=(connection_socket, filename,file_size)).start()
self.receive_file(connection_socket, filename, file_size)#接收文件
threading.Thread(target=self.send_file, args=(connection_socket, filename, client_name,file_size)).start()#创建一个新线程来发送文件
else:
self.broadcast(connection_socket, message)#广播该信息
except Exception as e:
print(e)
print('该客户端断开连接:', connection_socket.getpeername())
self.conns.remove(connection_socket)
break
为了实现这里,需要自定义一些应用层协议在本次实验中中,客户端向服务器发送的报文的格式为name:message
,而当客户想发送文件时报文为 FILE:client_name:filename:file_size
(由此可见用户的昵称name不能为FILE), 在正常发消息的情况下会直接调用 self.broadcast(connection_socket, message)
广播该信息,在客户端有文件传输的需求时,首先会根据自定义报文格式,从报文中取出相关信息随后通过这些信息作为参数调用receive_file
函数,接收到文件的服务器就需要去向各个客户端分发文件,这里创建了一个新线程来发送文件(考虑到会有些大文件导致时间过长,为了不影响聊天),很显然一个问题为何不创建一个线程去接收文件呢,因为在多线程情况下如果send_file比receive_file先执行那么就会导致分发一个不存在的文件,明显不合理,因此必须先执行完receive_file函数后才能继续执行后续操作,由于连接有时会不稳定所以必须加入异常处理
接下来就是实现三个函数,receive_file,send_file,broadcast
receive_file:
def receive_file(self, connection_socket, filename,file_size):
data = b''
if file_size<self.buffer_size:
data = connection_socket.recv(self.buffer_size)
f = open(f"D:/{filename}", "wb")
f.write(data)
f.close()
else:
received_size = 0
while received_size < file_size:
size = 0 # 准确接收数据大小,解决粘包
if file_size - received_size > self.buffer_size: # 多次接收
size = self.buffer_size
else: # 最后一次接收完毕
size = file_size - received_size
filedata = connection_socket.recv(size) # 多次接收内容,接收大数据
data+=filedata
filedata_len = len(filedata)
received_size += filedata_len
print('已接收:', int(received_size / file_size * 100), "%")
f = open(f"D:/{filename}", "wb")
f.write(data)
f.close()
print(f"服务器已接受来自{connection_socket.getpeername()}的文件")
这个函数中存在两个坑,一个是recv函数是阻塞的,这就会导致如果使用while True
:方法 不断取数据那么当无数据可取且没有新数据到来时就会一直阻塞无法退出循环 ,还有当获取文件描述符在调用recv函数之前调用那么就会导致无法收到数据(讲真的,我也不知道为什么,已经被搞崩溃了),后面想到了这种办法,可以解决那两个问题(其实代码可以缩减只留一个循环但是由于之前不断的尝试就忘记删除前面的代码)思路很简单:当文件大小小于缓冲区时只需接收一次,大于缓冲区时多次接收
send_file:
def send_file(self, connection_socket, filename, client_name,file_size):
for conn in self.conns:
if connection_socket.getpeername() == conn.getpeername():#上传文件的客户不需要接收
continue
conn.send(f"FILE:{client_name}:{filename}:{file_size}".encode())#自定义的报文格式
with open(f"D:/{filename}", "rb") as f: #开始发送文件
while True:
data = f.read(1024)
if not data:
break
conn.sendall(data)
f.close()
print("服务器已完成分发")
这个实现起来还比较简单,通过遍历连接列表将文件分发出去,要注意一点就是上传该文件的客户不需要接收到该分发的文件
broadcast:
def broadcast(self, connection_socket, message):
for conn in self.conns:
if connection_socket.getpeername() == conn.getpeername():
continue
conn.send(message.encode())
和send_files函数思路一样,这不过该函数发的是消息
服务器端完整代码:
import socket
import threading
class Server:
def __init__(self,buffer_size=1024):
self.host = '127.0.0.1'
self.port = 12001
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.conns = []
self.buffer_size = buffer_size
def start(self):
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)
print('开始服务 {}:{}'.format(self.host, self.port))
while True:
connection_socket, address = self.server_socket.accept()
client_thread = threading.Thread(target=self.handle_client, args=(connection_socket,))
client_thread.start()
def handle_client(self, connection_socket):
self.conns.append(connection_socket)
print('新客户端连接:', connection_socket.getpeername())
while True:
try:
message = connection_socket.recv(self.buffer_size).decode()
if message.split(":")[0] == "FILE":
client_name = message.split(":")[1]
filename = message.split(":")[2]
file_size = int(message.split(":")[3])
# threading.Thread(target=self.receive_file, args=(connection_socket, filename,file_size)).start()
self.receive_file(connection_socket, filename, file_size)
threading.Thread(target=self.send_file, args=(connection_socket, filename, client_name,file_size)).start()
else:
self.broadcast(connection_socket, message)
except Exception as e:
print(e)
print('该客户端断开连接:', connection_socket.getpeername())
self.conns.remove(connection_socket)
break
def receive_file(self, connection_socket, filename,file_size):
data = b''
if file_size<self.buffer_size:
data = connection_socket.recv(self.buffer_size)
f = open(f"D:/{filename}", "wb")
f.write(data)
f.close()
else:
received_size = 0
while received_size < file_size:
size = 0 # 准确接收数据大小,解决粘包
if file_size - received_size > self.buffer_size: # 多次接收
size = self.buffer_size
else: # 最后一次接收完毕
size = file_size - received_size
filedata = connection_socket.recv(size) # 多次接收内容,接收大数据
data+=filedata
filedata_len = len(filedata)
received_size += filedata_len
print('已接收:', int(received_size / file_size * 100), "%")
f = open(f"D:/{filename}", "wb")
f.write(data)
f.close()
print(f"服务器已接受来自{connection_socket.getpeername()}的文件")
def send_file(self, connection_socket, filename, client_name,file_size):
for conn in self.conns:
if connection_socket.getpeername() == conn.getpeername():
continue
conn.send(f"FILE:{client_name}:{filename}:{file_size}".encode())
with open(f"D:/{filename}", "rb") as f:
while True:
data = f.read(self.buffer_size)
if not data:
break
conn.sendall(data)
f.close()
print("服务器已完成分发")
def broadcast(self, connection_socket, message):
for conn in self.conns:
if connection_socket.getpeername() == conn.getpeername():
continue
conn.send(message.encode())
if __name__ == '__main__':
server = Server()
server.start()
客户端
首先定义客户类
class Client:
def __init__(self,name,buffer_size=1024):
self.serverhost='localhost'#要连接的服务器地址
self.serverport=12001#要连接的服务器端口
self.client_socket=socket(AF_INET, SOCK_STREAM)
self.name=name#用户昵称
self.buffer_size=buffer_size#设置缓冲区大小
类中要实现的方法,作为一名用户,有聊天和发送接收文件的需求
因此需要最少实现三个方法chat(),send_file,receive_file()
chat():
def chat(self):
self.client_socket.connect((self.serverhost, self.serverport))
print("已连接")
threading.Thread(target=self.send_message).start()
threading.Thread(target=self.receive_message).start()
chat函数主要作用为判断用户是否连接到服务器以便执行后续操作,这里因为发送消息和接收消息可以并行执行因此创建了两个线程
因此需要实现send_message
函数和receive_message
函数
send_message:
def send_message(self):
while True:
sentence = input()
if sentence == "exit":
self.client_socket.close()
break
if sentence == "FILE":#用户有发文件需求,必须先输入FILE
try:
file_path = input("请输入文件路径:")
file_size = os.stat(file_path).st_size
threading.Thread(target=self.send_file, args=(file_path, file_size)).start()
except Exception as e:
print(e)
finally:
continue
self.client_socket.send(f"{self.name}:{sentence}".encode())
之前介绍服务端时说过该程序自定义的应用层协议以及发送报文格式,那么需要在客户端实现并规范,正常发送消息时会直接向服务器发送 name:sentence
格式的报文,随后由服务器广播至每一个客户端,需要传送文件时必须输入FILE转至文件处理处 获取参数并调用send_file
, 这里因为发送文件和发消息并不冲突因此可以创建另一个线程去执行,以免文件不存在需加入异常处理
实现send_file函数
send_file:
def send_file(self, file_path, file_size):
filename = file_path.split("/")[-1]
self.client_socket.send(f"FILE:{self.name}:{filename}:{file_size}".encode())#自定义报文格式
print(f"共{file_size}字节")
with open(file_path, "rb") as f:
while True:
data = f.read(self.buffer_size)
if not data:
break
self.client_socket.sendall(data)
f.close()
print("文件上传完毕")
这个方法没有什么坑比较简单直接实现就行,但要记得之前自定义的报文格式FILE:name:filename:file_size
与之相对应的还有receive_message
和receive_file
receive_message:
def receive_message(self):
try:
while True:
mess = self.client_socket.recv(self.buffer_size).decode()
if mess.split(":")[0] == "FILE":#判断是否为服务器发来的文件
other_client_name = mess.split(":")[1]#发来文件的用户名
filename = mess.split(":")[2]
file_size = int(mess.split(":")[3])
print(f"接收来自{other_client_name}的{filename}文件,大小为{file_size}字节")
self.receive_file(filename, file_size, other_client_name)
# threading.Thread(target=self.receive_file,args=(filename,file_size,other_client_name)).start()
else:
print(mess)
except Exception as e:
print("与服务器断开连接")
self.client_socket.close()
首先需要判断服务器发来的是否为文件,如为正常消息那么直接打印出来,如果是文件那么需要从报文获取相应参数之后调用receive_file
这里不创建线程的原因是为了能够准确无误的接收文件,如果一旦创建了新线程执行就会导致主线程和新线程都在不断调用recv
函数,主线程会将本来要存在文件中的内容读取出来打印造成程序错误。最后由于连接的不稳定需要异常处理
receive_file:
def receive_file(self, filename, file_size, other_client_name):
print("开始接收文件---------")
data = b''
if file_size < self.buffer_size:
data = self.client_socket.recv(self.buffer_size)
f = open(f"D:/learn/{filename}", "wb")#改成自己想要存储文件的路径
f.write(data)
f.close()
else:
received_size = 0
while received_size < file_size:
size = 0 # 准确接收数据大小,解决粘包
if file_size - received_size > 1024: # 多次接收
size = 1024
else: # 最后一次接收完毕
size = file_size - received_size
filedata = self.client_socket.recv(size) # 多次接收内容,接收大数据
data += filedata
filedata_len = len(filedata)
received_size += filedata_len
print('已接收:', int(received_size / file_size * 100), "%")
f = open(f"D:/learn/{filename}", "wb")
f.write(data)
f.close()
print(f"已接收{other_client_name}的文件")
和服务器端的代码差不多,将服务器端代码复制过来改改打印消息和路径就可以了
客户端完整代码:
from socket import *
import threading
import os
class Client:
def __init__(self, name, buffer_size=1024):
self.serverhost = 'localhost'
self.serverport = 12001
self.client_socket = socket(AF_INET, SOCK_STREAM)
self.name = name
self.buffer_size = buffer_size
def chat(self):
self.client_socket.connect((self.serverhost, self.serverport))
print("已连接")
print("---------欢迎进入聊天室--------")
threading.Thread(target=self.send_message).start()
threading.Thread(target=self.receive_message).start()
def send_message(self):
while True:
sentence = input()
if sentence == "exit":
self.client_socket.close()
break
if sentence == "FILE":
try:
file_path = input("请输入文件路径:")
file_size = os.stat(file_path).st_size
threading.Thread(target=self.send_file, args=(file_path, file_size)).start()
except Exception as e:
print(e)
finally:
continue
self.client_socket.send(f"{self.name}:{sentence}".encode())
def receive_message(self):
try:
while True:
mess = self.client_socket.recv(self.buffer_size).decode()
if mess.split(":")[0] == "FILE":
other_client_name = mess.split(":")[1]
filename = mess.split(":")[2]
file_size = int(mess.split(":")[3])
print(f"接收来自{other_client_name}的{filename}文件,大小为{file_size}字节")
self.receive_file(filename, file_size, other_client_name)
# threading.Thread(target=self.receive_file,args=(filename,file_size,other_client_name)).start()
else:
print(mess)
except Exception as e:
print("与服务器断开连接")
self.client_socket.close()
def send_file(self, file_path, file_size):
filename = file_path.split("/")[-1]
self.client_socket.send(f"FILE:{self.name}:{filename}:{file_size}".encode())
print(f"共{file_size}字节")
with open(file_path, "rb") as f:
while True:
data = f.read(self.buffer_size)
if not data:
break
self.client_socket.sendall(data)
f.close()
print("文件上传完毕")
def receive_file(self, filename, file_size, other_client_name):
print("开始接收文件---------")
data = b''
if file_size < self.buffer_size:
data = self.client_socket.recv(self.buffer_size)
f = open(f"D:/learn/{filename}", "wb")
f.write(data)
f.close()
else:
received_size = 0
while received_size < file_size:
size = 0 # 准确接收数据大小,解决粘包
if file_size - received_size > 1024: # 多次接收
size = 1024
else: # 最后一次接收完毕
size = file_size - received_size
filedata = self.client_socket.recv(size) # 多次接收内容,接收大数据
data += filedata
filedata_len = len(filedata)
received_size += filedata_len
print('已接收:', int(received_size / file_size * 100), "%")
f = open(f"D:/learn/{filename}", "wb")
f.write(data)
f.close()
print(f"已接收{other_client_name}的文件")
if __name__ == "__main__":
name = input("请输入昵称:")
client = Client(name)
client.chat()
至此该聊天室代码完成
运行结果
正常消息的发送和接收;
张三发消息:
李四发消息:
王五发消息:
服务器端:
文件传输演示:
由王五发文件:
先输入FILE然后再输入文件路径
张三:
李四:
服务器端:
根据自己设定的文件路径:
服务器端存储
客户端存储
验证是否还能发消息:
王五:
仍能正常接收消息
张三:
李四:
输入exit退出聊天室
服务器端
将字符串中的字符转为大写字母
这个程序基于 UDP实现
服务器端
from socket import *
port = 12000
serversocket = socket(AF_INET,SOCK_DGRAM)
serversocket.bind(('',port))
print("ready to receive")
while True:
message,clientAddress = serversocket.recvfrom(2048)
modifiedmessage = message.decode().upper()
serversocket.sendto(modifiedmessage.encode(),clientAddress)
客户端
from socket import *
server_name = "localhost"
serverport = 12000
clientsocket = socket(AF_INET, SOCK_DGRAM)
while True:
message = input("请输入:")
if message == "exit":
break
clientsocket.sendto(message.encode(), (server_name, serverport))
modifiedMessage, serverAddress = clientsocket.recvfrom(2048)
print(modifiedMessage.decode())
clientsocket.close()
和示例代码相同在实验原理部分已做过较为详细的讲解
运行结果
客户端:
服务器端:
输入一串字符串交给服务器处理后返回给客户一个将英文字母全部变为大写的数据
教材习题
P31
a.
如果先运行TCPClient,那么客户机将尝试与不存在的服务器进程建立TCP连接。将不会建立TCP连接。
b
UDPClient不与服务器建立TCP连接。因此,如果首先运行UDPClient,然后运行UDPServer,然后在键盘上键入一些输入,那么一切都可以正常工作。
c.
如果使用不同的端口号,那么客户端将试图建立一个TCP连接与错误的进程或不存在的进程。会发生错误。
P32.
在原始程序中,UDPClient在创建套接字时不指定端口号。在本例中,代码允许底层操作系统选择端口号。在另一行中,当UDPClient执行时,将创建一个端口号为5432的UDP套接字。
UDPServer 需要知道客户机端口号,以便能够将数据包发送回正确的客户机套接字。DPServer 通过分解从客户机接收的数据报来确定客户机端口号。因此,UDP 服务器可以使用任何客户机端口号,包括 5432。因此,UDPServer 不需要修改。
实验总结
对于聊天室的程序其实还有很多功能有待完善,比如说在发文件时,接收方要确定是否接收,对于这一功能最开始有实现过但是总是失败确认过后无法继续执行,后面通过分析代码大致知道了问题,因为客户端在多线程下input函数的阻塞导致程序无法继续下去,send_message
函数中有sentence = input()
,之前在receive_message
中也有一个ack = input(f"是否要接收来自{other_client_name},大小为{file_size}的文件,\n 请输入y/n")
判断ack是否等于y ,想起来容易,做起来难,由于send_message
和receive_message
处于两个不同的线程中,send_message
在没有输入的时候会一直被input阻塞,此时如果receive_message
输入了y那么就会被认为是sentence
发送并打印,receive_message
无法得到ack
真实的值,这方面原因应该是由于线程之间的资源共享,初步考虑的解决方案是通过建立不同进程,但本人并未实现(不一定正确,仍然需要不断调试) 代码还有一个小bug就是在发消息时如果A客户在编辑消息时B发来消息就会导致A客户的消息丢失必须重新编辑,出现这个问题可能还是由于多线程,一个线程用来发送,一个线程用来接收,线程间的资源共享(更确切原因有待进一步挖掘)
对于第二个程序直接将书上的例子写进去,还是很有代表性的一个程序可以更加深入的理解UDP通信的一些机制,发送信息时需要附加地址信息,因为它是无连接服务
如果觉得有帮助,拜托点个赞吧😃😃,哪里有错误望指正