Python语言实现两台计算机用TCP协议跨局域网通信

写过一篇  金牌会员 | 2024-2-27 13:53:14 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 832|帖子 832|积分 2496

成果展示:


(这张图是在我本地电脑上用pycharm运行两个程序测试,实际可以在两台电脑上分别运行。)
设备要求和实现的功能:

实现的功能:
跨局域网通信(仅支持两台计算机)
跨局域网收发小文件,支持缓存在服务器,再一键接收(仅支持两台计算机)
使用方法:

在服务器上运行server.py程序,在两台客户机上分别运行client.py程序,就会弹出图形化界面,就可以开始愉快使用啦。需要修改的地方是client.py里HOST要设置为服务器的ip地址,以及两处toFile要换成对应的服务器路径和本地存储路径。服务器的代码使用了python的tk库,但我用的是华为云的ECS服务器,是linux内核,并不支持跳出图形化界面,要跳出图形化界面的话需要下载X11,把消息转发到本地,在本地才能弹出窗口。具体的配置链接请看[https://www.cnblogs.com/yyiiing/p/17912650.html#5240133]
通信的基本原理:

因为临近考试了,虽然感觉很好玩,但没有花太多时间折腾,就搓了两天hh。原理很简单,用服务器作为中转,两台客户机和服务器建立起TCP连接,客户机A发消息给服务器,服务器再转发给客户机B。发文件的原理其实也是一样的,只不过是客户机A先把文件传输到服务器上,然后客户机再从服务器上读取。
UI界面介绍:

右边蓝色框仅显示聊天的文本信息,左边黄色框显示文件传输信息。
send Text按钮是用来发送文本的
select File按钮是用来选择文件的,点击后会出现如下界面:

send File按钮是用来发送文件的
receive按钮是用来接收所有未接收的文件
程序实现的基本原理

客户端:

通信部分由于py库已经封装得很好了,所以直接调用socket库去建立和服务器的连接即可。
  1.    HOST = '你的服务器IP地址'
  2.     PORT = 21567
  3.     ADDR = (HOST, PORT)
  4.     BUFSIZ = 65536
  5.     tcpCli = socket(AF_INET, SOCK_STREAM)
  6.     tcpCli.connect(ADDR)
  7.     root = tk.Tk()
  8.     chat_window = ChatWindow(root)
  9.     threading.Thread(target=recv, args=(tcpCli, BUFSIZ, chat_window)).start()  # 创建接收消息的线程并启动
  10.     root.mainloop()
  11.     tcpCli.close()
复制代码
比较困难的地方在于收发消息(广义的消息,包括文件和文本)
我建了一个message_queue用来存放所有要放出去的消息

  • 出队时检查一下标识符,如果是以sendFile开头的话,那它的构成就是:#sendFile#filepath(准备要发送的文件在本地的路径)。根据路径读取文件,加上#fileData#告诉服务器我这是要发送文件了,打包发过去即可。
  • 如果是文本消息的话不需要加标识符,直接发送。
是的,标识符是我为了区别文件和文本自己设计的,我猜测真实的场景里发送文件和发送消息应该是直接分开的,而不是都塞在一个消息队列里。
  1. def send_messages(self):
  2.         while True:
  3.             message = self.message_queue.get()  # 从队列中获取消息
  4.             if message.startswith("#sendFile#"):
  5.                 _, __, filepath = message.split("#")
  6.                 path_parts = filepath.split("/")
  7.                 # 获取最后一个部分
  8.                 last_part = path_parts[-1]
  9.                 toFile = "/home/liyishui/" #发送到服务器上的存储路径
  10.                 toFile+=last_part
  11.                 with open(filepath, "rb") as f:
  12.                     data = f.read()
  13.                     tcpCli.sendall(("#fileData#%s#" % toFile).encode() + data)
  14.                 toFile= "#sendFile#"+last_part
  15.                 tcpCli.send(toFile.encode())
  16.             else:
  17.                 tcpCli.send(message.encode())
复制代码
对于接收的消息:

  • 如果是以#fileData#开头,表明这是服务器在向你转发文件,你的客户端将会读取文件名(filepath)和数据(filedata),进行写入操作,并在左侧消息栏显示[Download] File download complete
  • 如果是以#sendFile#开头,表明这是对方在向你的客户机发送文件,发送完成之后对方朝服务器发送了#sendFile#filename,表示发送成功。服务器再转告你,你的左侧消息栏就会弹出一条[Newfile] fileName
  • 否则就是常规的文本消息操作,直接打印到图形化界面即可
  1. def recv(sock, BUFSIZ, chat_window):
  2.     try:
  3.         while True:
  4.             data = sock.recv(BUFSIZ)
  5.             print(data)
  6.             print(data.decode())
  7.             if not data:
  8.                 break
  9.             if data.startswith(b"#fileData#"):
  10.                 _, filename, filepath, filedata = data.decode().split("#", 3)
  11.                 toFile = "C:\\Users\\33245\\Desktop\\ttssmm\\animal\\backend\\user2\" + filepath.strip() #本地文件存储路径
  12.                 with open(toFile, "wb") as f:
  13.                     f.write(filedata.encode())
  14.                 time_tag = '[%s] ' % ctime()
  15.                 tag_config = ('sent_time_tag', 'green')
  16.                 chat_window.info_text.config(state='normal')
  17.                 chat_window.info_text.insert(tk.END, time_tag, tag_config)
  18.                 chat_window.info_text.insert(tk.END, '[Download] File download complete: {}\n'.format(filepath))
  19.                 chat_window.info_text.config(state='disabled')
  20.                 chat_window.info_text.see(tk.END)
  21.                 continue
  22.             elif data.startswith(b"#sendFile#"):
  23.                 _, __, filename = data.split(b"#", 2)
  24.                 chat_window.info_text.config(state='normal')
  25.                 filename=filename.decode()
  26.                 chat_window.info_text.insert(tk.END, '[Newfile]: %s\n' % filename)
  27.                 chat_window.info_text.config(state='disabled')
  28.                 chat_window.info_text.see(tk.END)
  29.                 continue
  30.             elif data.decode() == '[CHAT]BEGIN':
  31.                 chat_window.add_log('[对方] ', data.decode())
  32.                 print(data.decode())  # 打印到控制台
  33.             elif data.decode() == '[CHAT]END':
  34.                 sock.close()
  35.                 break
  36.             else:
  37.                 chat_window.add_log('对方 ', data.decode())
  38.                 print(data.decode())  # 打印到控制台
  39.     except OSError:
  40.         pass
复制代码
客户端的其他代码部分就是和图形化界面有关,具体的用法可以自行百度(而且也不是我写的啦,俺的舍友们搓的)
服务器端:

服务器端通信部分比较多,但整体思路还是一样的。
新建一个socket,监听PORT(这里是21567,换成其他的理论上也可以)
  1. HOST = ''
  2.   PORT = 21567
  3.   ADDR = (HOST, PORT)
  4.   tcp = socket(AF_INET, SOCK_STREAM)
  5.   tcp.bind(ADDR)
  6.   tcp.listen(3)
  7.   Users = []
  8.   Addrs = []
复制代码
开两个队列,这是用来存放客户机尚未接收的文件名,客户机点击receive时,把队列里的元素全部出队。
开放连接,等到加入的用户数达到两个以后中断循环,客户机1和客户机2都新建用来连接的线程。
  1. def accept_connections():
  2.   user1_files_queue = queue.Queue()  # 新建用户1的文件队列
  3.   user2_files_queue = queue.Queue()  # 新建用户2的文件队列
  4.   
  5.   while len(Users) != 2:
  6.     tcpCli, addr = tcp.accept()
  7.     Users.append(tcpCli)
  8.     Addrs.append(addr)
  9.     user_box.insert(tk.END, "User" + str(len(Users)) + ": " + str(addr) + "\n")
复制代码
启动线程,target函数是下面的trans,trans1的参数是user[0],然后才是user[1],因为对于trans1来说user[0]才是自己,user[1]是对方,trans2则反过来。
  1.   trans1 = threading.Thread(target=trans, args=(Users[0], Users[1], 1024, user1_files_queue, user2_files_queue))
  2.   trans1.start()
  3.   trans2 = threading.Thread(target=trans, args=(Users[1], Users[0], 1024, user2_files_queue, user1_files_queue))
  4.   trans2.start()
复制代码
trans函数里的sock1并不一定是user[0],取决于谁调用了trans。这个函数的功能是不断从sock1中获取消息,判断是否为文本或者文件,是文本则转发给sock2,是文件则存储并进入sock2的待接收队列。
可能有朋友对于涉及到线程的部分有点晕我们可以这么理解:
对于0号客户机,我们开了一个线程,这个线程会一直持续工作,工作的内容是从1号客户机接收消息,再转发给自己。对于1号客户机,我们也开了一个线程,同样的从对方接受消息然后转发给自己。
这个部分可以整合成一个函数trans,并用线程并行地去调用。不能写两个函数直接分别调用是因为这样就会有先后顺序,会导致一方一直在等待发消息,另一方的函数一直运行不了,所以要用线程库保证能并行。
  1. def trans(sock1, sock2, BUFSIZ, user1_files_queue, user2_files_queue):
  2.   while True:
  3.     try:
  4.       data = sock1.recv(BUFSIZ)
  5.     except OSError:
  6.       break
  7.     if not data:
  8.       sock1.close()
  9.     else:
  10.       # 判断是否是文件传输标识
  11.       if data.startswith(b"#fileData#"):
  12.         _, filename, filepath, filedata = data.split(b"#", 3)
  13.         save_file(filepath.decode(), filedata)
  14.         message_display.insert(tk.END, f"File received: {filepath.decode()}\n")
  15.         user2_files_queue.put(filepath.decode())
  16.       elif data == b"#receiveAll#":
  17.         send_all_files(sock1, user1_files_queue)
  18.       else:
  19.         try:
  20.           sock2.send(data)
  21.         except OSError:
  22.           sock1.close()
  23.           break
复制代码
待优化的地方:


  • 只能实现小文件传输,因为原理是一次性把数据全部发过去,TCP连接的上限是64KB,对于更大的文件要分片多次发送,还没有实现这个逻辑。
  • 没有离线传输,要实现的话就要用数据库去缓存待发出的消息,以及登记用户的名字、IP等等,相当于多搞一个注册账户的功能,再加一个数据库
  • 只能两个人之间传输,要优化的话其实比较好处理,在原来的基础上加一个选择聊天室的模块,把可供连接的用户数增加,不再用trans函数来sock1和sock2这样对话,而是直接服务器给所有客户机广播。
多说几句:

第一次拿ptyhon写非人工智能领域的东西,感慨python真好用。
写博客的时候突然好奇真实的聊天场景应该是什么,就去搜了一下QQ,大概原理分发消息和发视频、发文件

  • 打视频直接P2P,因为要经过服务器中转的话不仅速度变慢,也给服务器造成很大压力。所以我猜测一定程度上视频比文本更不容易被监管。
  • 发文本消息分在线和离线。在线直接P2P传输,速度快,非常及时。如果对方不在线的话,就缓存到数据库,等上线了再发送。你可能会想那P2P不经过服务器的话,腾讯是怎么知道我们聊天内容的?本地会存储聊天消息,我们一边聊天,后台一边向服务器同步消息。这样既能保证聊天很及时,而同步消息没有那么着急,也可以慢慢来。
  • 发文件的原理和我上面代码实现的其实很像,也是经由服务器,先储存在服务器,用户接收时再下载。所以qq发文件会有一个七天缓存,服务器只负责保存七天,七天后直接清空。
  • 发送的消息可以拿md5或者sha256加密一下,一点都不加的话随便一抓包就知道我们在聊什么了哈哈哈
源码:

客户端
  1. import os.path
  2. from socket import *
  3. import threading
  4. from time import ctime
  5. from queue import Queue
  6. import tkinter as tk
  7. from tkinter.filedialog import askopenfilename
  8. class ChatWindow:
  9.     def __init__(self, master):
  10.         self.master = master
  11.         master.title('501密聊')
  12.         master.geometry('620x400')  # 设置窗口大小
  13.         self.paned_window = tk.PanedWindow(master, orient=tk.HORIZONTAL, sashwidth=4, sashrelief=tk.RAISED)
  14.         self.paned_window.pack(expand=True, fill='both')
  15.         # 左侧面板
  16.         self.left_panel = tk.Frame(self.paned_window, width=200, height=400)
  17.         self.left_panel.grid_propagate(False)  # 禁止自动调整大小
  18.         self.paned_window.add(self.left_panel)
  19.         # 在左侧面板中添加显示信息的Text组件
  20.         self.info_text = tk.Text(self.left_panel, state='disabled', width=25, height=20, bg='light yellow')
  21.         self.info_text.pack(expand=True, fill='both')
  22.         self.info_text.tag_configure('tag_me', foreground='green')
  23.         self.info_text.tag_configure('tag_user', foreground='blue')
  24.         #在左侧面板中添加"reiceive"按钮
  25.         self.receive_button = tk.Button(self.left_panel, text="Receive", command=self.receive_all_files)
  26.         self.receive_button.pack(side=tk.BOTTOM, pady=5)
  27.         # 右侧面板
  28.         self.right_panel = tk.Frame(self.paned_window)
  29.         self.right_panel.grid_propagate(False)  # 禁止自动调整大小
  30.         self.paned_window.add(self.right_panel)
  31.         # 在右侧面板中添加聊天窗口的组件
  32.         self.log_text = tk.Text(self.right_panel, state='disabled', width=50, height=20, bg='light blue')
  33.         self.log_text.grid(row=0, column=0, columnspan=2)
  34.         self.send_entry = tk.Text(self.right_panel, width=40, height=5)
  35.         self.send_entry.grid(row=1, column=0)
  36.         self.send_entry.bind('<KeyPress-Return>', self.send_message_on_enter)  # 绑定回车键事件
  37.         self.send_button = tk.Button(self.right_panel, text='Send Text [or Enter]', command=self.send_message)
  38.         self.send_button.grid(row=1, column=1,columnspan=2)
  39.         self.scrollbar = tk.Scrollbar(self.right_panel, orient='vertical', command=self.log_text.yview)
  40.         self.scrollbar.grid(row=0, column=2, sticky='ns')
  41.         self.log_text.config(yscrollcommand=self.scrollbar.set)
  42.         self.message_queue = Queue()  # 创建队列用于存储待发送的消息
  43.         self.send_thread = threading.Thread(target=self.send_messages)  # 创建发送消息的线程
  44.         self.send_thread.start()  # 启动发送消息的线程
  45.         # 设置标签样式
  46.         self.log_text.tag_configure('sent_time_tag', foreground='green')
  47.         self.log_text.tag_configure('recv_time_tag', foreground='blue')
  48.         self.log_text.tag_configure('sent_message', foreground='green')
  49.         self.log_text.tag_configure('recv_message', foreground='blue')
  50.         self.file_entry = tk.Entry(self.right_panel, width=40)
  51.         self.file_entry.grid(row=2, column=0)
  52.         self.select_file_button = tk.Button(self.right_panel, text='Select File', command=self.select_file)
  53.         self.select_file_button.grid(row=2, column=1)
  54.         self.send_file_button = tk.Button(self.right_panel, text='Send File', command=self.send_file)
  55.         self.send_file_button.grid(row=2, column=2)
  56.     def receive_all_files(self):
  57.         self.message_queue.put("#receiveAll#")
  58.     def save_file(filename, filedata):
  59.         with open(filename, "wb") as f:
  60.             f.write(filedata)
  61.     # 在客户端的 process_received_messages 方法中
  62.     def select_file(self):
  63.         filepath = askopenfilename()
  64.         if filepath:
  65.             self.file_entry.delete(0, tk.END)
  66.             self.file_entry.insert(tk.END, filepath)
  67.     def send_file(self):
  68.         filepath = self.file_entry.get()
  69.         if filepath:
  70.             self.message_queue.put("#sendFile#" + filepath)
  71.             self.info_text.config(state='normal')
  72.             self.info_text.insert(tk.END, 'File sent complete: %s\n' % os.path.basename(filepath))
  73.             self.info_text.config(state='disabled')
  74.             self.info_text.see(tk.END)
  75.             self.file_entry.delete(0, tk.END)
  76.         else:
  77.             tk.messagebox.showinfo('Warning', 'Please enter a file path.')
  78.     def send_message(self):
  79.         message = self.send_entry.get('1.0', tk.END)
  80.         self.send_entry.delete('1.0', tk.END)
  81.         if message.strip():
  82.             self.message_queue.put(message.strip())  # 将消息放入队列中
  83.             self.add_log('[Me]', message.strip(), is_sent=True)  # 将消息显示在聊天消息列表框中
  84.             print('[Me]', message.strip())  # 打印到控制台
  85.     def send_message_on_enter(self, event):  # 回车键事件处理函数
  86.         self.send_message()
  87.     def add_log(self, user, message, is_sent=False):
  88.         self.log_text.config(state='normal')
  89.         if is_sent:
  90.             time_tag = '[%s] ' % ctime()
  91.             tag_config = ('sent_time_tag', 'green')
  92.             message_tag = 'sent_message'
  93.         else:
  94.             time_tag = '[%s] ' % ctime()
  95.             tag_config = ('recv_time_tag', 'blue')
  96.             message_tag = 'recv_message'
  97.         self.log_text.insert(tk.END, time_tag, tag_config)
  98.         self.log_text.insert(tk.END, '\n')  # 插入换行符
  99.         if is_sent:
  100.             self.log_text.insert(tk.END, user, message_tag)
  101.         else:
  102.             self.log_text.insert(tk.END, user, message_tag)
  103.         self.log_text.insert(tk.END, '%s\n' % message)
  104.         self.log_text.config(state='disabled')
  105.         self.log_text.see(tk.END)
  106.         print(message)  # 打印到控制台
  107.     def send_messages(self):
  108.         while True:
  109.             message = self.message_queue.get()  # 从队列中获取消息
  110.             if message.startswith("#sendFile#"):
  111.                 _, __, filepath = message.split("#")
  112.                 path_parts = filepath.split("/")
  113.                 # 获取最后一个部分
  114.                 last_part = path_parts[-1]
  115.                 toFile = "你的服务器上的存储路径" #
  116.                 toFile+=last_part
  117.                 with open(filepath, "rb") as f:
  118.                     data = f.read()
  119.                     tcpCli.sendall(("#fileData#%s#" % toFile).encode() + data)
  120.                 toFile= "#sendFile#"+last_part
  121.                 tcpCli.send(toFile.encode())
  122.             else:
  123.                 tcpCli.send(message.encode())
  124. def recv(sock, BUFSIZ, chat_window):
  125.     try:
  126.         while True:
  127.             data = sock.recv(BUFSIZ)
  128.             print(data)
  129.             print(data.decode())
  130.             if not data:
  131.                 break
  132.             if data.startswith(b"#fileData#"):
  133.                 _, filename, filepath, filedata = data.decode().split("#", 3)
  134.                 toFile = "你的本地文件存储路径" + filepath.strip()
  135.                 with open(toFile, "wb") as f:
  136.                     f.write(filedata.encode())
  137.                 time_tag = '[%s] ' % ctime()
  138.                 tag_config = ('sent_time_tag', 'green')
  139.                 chat_window.info_text.config(state='normal')
  140.                 chat_window.info_text.insert(tk.END, time_tag, tag_config)
  141.                 chat_window.info_text.insert(tk.END, '[Download] File download complete: {}\n'.format(filepath))
  142.                 chat_window.info_text.config(state='disabled')
  143.                 chat_window.info_text.see(tk.END)
  144.                 continue
  145.             elif data.startswith(b"#sendFile#"):
  146.                 _, __, filename = data.split(b"#", 2)
  147.                 chat_window.info_text.config(state='normal')
  148.                 filename=filename.decode()
  149.                 chat_window.info_text.insert(tk.END, '[Newfile]: %s\n' % filename)
  150.                 chat_window.info_text.config(state='disabled')
  151.                 chat_window.info_text.see(tk.END)
  152.                 continue
  153.             elif data.decode() == '[CHAT]BEGIN':
  154.                 chat_window.add_log('[对方] ', data.decode())
  155.                 print(data.decode())  # 打印到控制台
  156.             elif data.decode() == '[CHAT]END':
  157.                 sock.close()
  158.                 break
  159.             else:
  160.                 chat_window.add_log('对方 ', data.decode())
  161.                 print(data.decode())  # 打印到控制台
  162.     except OSError:
  163.         pass
  164. if __name__ == '__main__':
  165.     HOST = '你的ip地址'
  166.     PORT = 21567
  167.     ADDR = (HOST, PORT)
  168.     BUFSIZ = 65536
  169.     tcpCli = socket(AF_INET, SOCK_STREAM)
  170.     tcpCli.connect(ADDR)
  171.     root = tk.Tk()
  172.     chat_window = ChatWindow(root)
  173.     threading.Thread(target=recv, args=(tcpCli, BUFSIZ, chat_window)).start()  # 创建接收消息的线程并启动
  174.     root.mainloop()
  175.     tcpCli.close()
复制代码
服务器端
  1. from socket import *import tkinter as tkimport threadingimport osimport queue  # 添加队列模块def trans(sock1, sock2, BUFSIZ, user1_files_queue, user2_files_queue):
  2.   while True:
  3.     try:
  4.       data = sock1.recv(BUFSIZ)
  5.     except OSError:
  6.       break
  7.     if not data:
  8.       sock1.close()
  9.     else:
  10.       # 判断是否是文件传输标识
  11.       if data.startswith(b"#fileData#"):
  12.         _, filename, filepath, filedata = data.split(b"#", 3)
  13.         save_file(filepath.decode(), filedata)
  14.         message_display.insert(tk.END, f"File received: {filepath.decode()}\n")
  15.         user2_files_queue.put(filepath.decode())
  16.       elif data == b"#receiveAll#":
  17.         send_all_files(sock1, user1_files_queue)
  18.       else:
  19.         try:
  20.           sock2.send(data)
  21.         except OSError:
  22.           sock1.close()
  23.           breakdef save_file(filename, filedata):  with open(filename, "wb") as f:    f.write(filedata)def send_all_files(sock, files_queue):  while not files_queue.empty():    filepath = files_queue.get()    filename = os.path.basename(filepath)    print(filename)    with open(filepath, "rb") as f:      data = f.read()    sock.sendall(("#fileData#%s#" % filename).encode() + data)def accept_connections():
  24.   user1_files_queue = queue.Queue()  # 新建用户1的文件队列
  25.   user2_files_queue = queue.Queue()  # 新建用户2的文件队列
  26.   
  27.   while len(Users) != 2:
  28.     tcpCli, addr = tcp.accept()
  29.     Users.append(tcpCli)
  30.     Addrs.append(addr)
  31.     user_box.insert(tk.END, "User" + str(len(Users)) + ": " + str(addr) + "\n")  trans1 = threading.Thread(target=trans, args=(Users[0], Users[1], 1024, user1_files_queue, user2_files_queue))
  32.   trans1.start()
  33.   trans2 = threading.Thread(target=trans, args=(Users[1], Users[0], 1024, user2_files_queue, user1_files_queue))
  34.   trans2.start()if __name__ == '__main__':  HOST = ''
  35.   PORT = 21567
  36.   ADDR = (HOST, PORT)
  37.   tcp = socket(AF_INET, SOCK_STREAM)
  38.   tcp.bind(ADDR)
  39.   tcp.listen(3)
  40.   Users = []
  41.   Addrs = []  root = tk.Tk()  root.title("Chat Server")  root.geometry("400x300")    # 创建用户框  user_box = tk.Listbox(root, width=20)  user_box.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)    # 创建用于显示消息的文本框  message_display = tk.Text(root)  message_display.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)    # 创建一个线程用于接受连接  accept_thread = threading.Thread(target=accept_connections)  accept_thread.start()    root.mainloop()
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

写过一篇

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表