一个简单的聊天工具

 

 

  

 

一、系统需求分析····································3

二、系统功能········································3

三、系统模块划分····································4

四、系统流程图······································6

五、详细设计及实现··································7

六、总结体会········································19

一. 
系统需求分析

网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的!中间最主要的就是数据包的组装,数据包的过滤,数据包的捕获,数据包的分析,当然最后再做一些处理

计算机网络技术与通信技术结合形成的网络通信技术,网络是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。通信是人与人之间通过某种媒体进行的信息交流与传递。网络通信是通过网络将各个孤立的设备进行连接,通过信息交换实现人与人,人与计算机,计算机与计算机之间的通信。

在网络越来越发达的今天,人们对网络的依赖度越来越大,由此应运而生的即时通信也在蓬勃的发展。即时通信是指能够即时发送和接收互联网消息等的业务。近几年来,即时通信的功能日益丰富,逐渐集成了电子邮件、博客、音乐、电视、游戏和搜索等多种功能。即时通信不再是一个单纯的聊天工具,它已经发展成集交流、资讯、娱乐、搜索、电子商务、办公协作和企业客户服务等为一体的综合化信息平台。

这次网络编程的课程设计,我选择网络即时通信程序,实现一个简单的聊天工具。通过制作该程序,我能更好地学习网络编程知识,提高自己的动手能力。本程序选择使用TCP协议,从而保证信息传输的可靠性。本程序实现功能类似QQ,既可以一对一聊天,也可以发送文件。

编程语言使用python3.6,其中图形化界面的实现使用Tkinter包;

开发平台使用PyCharm

二. 系统功能

    程序没有对服务器端和客户端的界面进行区分,实现的是客户端与服务器端之间的多进程通信以及文件传输。服务端与客户端功能上的划分是以开始监听为依据,开始监听的作为服务端,等待连接,客户端登陆之后,对服务端的IP与端口号进行搜索,建立连接。如果没有填写或填写出错,默认端口为8000,默认IP为0.0.0.0。对于IP地址无要求,端口要求不小于8000.

服务器端功能的实现

在特定端口上进行侦听,等待客户端连接。用户可以配置服务端的侦听端口,默认端口为8000。可以由用户发送消息。当停止服务时,断开所有的用户连接。

客户端功能的实现

客户端界面打开后,通过搜索IP与端口连接到已经开启监听服务器端。用户可以随时登录和退出,用户首先发送消息。

  

三. 系统模块划分

系统分为服务器端与客户端

服务器端:

在互联网的进程通信中,全局标识一个进程需要一个被称为“半相关”的三元组(协议,本地主机地址和本地端口号)来描述;而一个完整的进程通信实例,则需要一个被称为“相关”的五元组(协议,本地主机地址,本地端口号,远端主机地址和远端端口号)来描述。

虽然在利用socket套接字建立连接时需要考虑主机与客户端采取不同的动作,但是考虑到实用角度,在发送信息以及传输文件时不区分主机与客户端。

客户端:

客户端聊天工具实现的核心分为两个部分,包括发送消息和接收消息,在代码的实现上分别通过调用函数send_message_presend()receive_message()实现,其中receive_message()通常使用申请新的线程来调用。为了把整个文件的传输嵌套在同一个页面中,同时不影响消息的发送,所以整个文件的发送以及接收过程申请了一个线程。

四. 系统流程图

 1、系统工作原理图如下:

 

系统工作原理图

图中反映了两种优选时序,即:

1、服务器socket()----服务器bind()----服务器listen()----客户端socket()----服务器accept()----客户端connect()----客户端send ()----客户端recv ()----关闭close()

2、服务器socket()----客户端socket()----服务器bind()----服务器listen()----客户端connect()----服务器accept()----客户端send()----服务器recv ()----服务器send()----客户端recv ()----关闭close()

因为connect()寻求连接和accept()接收连接等函数可能出现阻塞,即在此函数执行过程中,由于操作系统本身原因或通信信道被其他进程长时间独占等,导致函数无法返回,服务端程序在等待连接时可能处于无响应状态,等待客户端搜索并连接之后,程序可以正常运行。

  发送文件时输入 文件路径以及文件名,程序在发送文件时首先自动发送一个固定字符作为识别发送的是文件还是消息的依据,文件保存在程序所在文件夹。

2、 系统流程图如下:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

系统流程图

五. 详细设计及实现

 

(〇)多线程技术

多线程技术在这次实验中有很重要的地位,应用在以下几个方面:

实现两方可以同时发送消息,换句话说就是不规定一问一答,同一方可以连续发送消息。虽然在利用socket套接字建立连接时需要考虑主机与客户端采取不同的动作,但是考虑到实用角度,在发送信息以及传输文件时不区分主机与客户端。建立连接的任何一方主动发送消息(包括文字、文件的分片等)时,申请一个新的线程,因此不影响自己在发送消息的时候接收消息,实现了上述要求。

为了把整个文件的传输嵌套在同一个页面中,同时不影响消息的发送,所以整个文件的发送以及接收过程申请了一个线程。

多线程的类定义如下:


class MyThread(threading.Thread):
    def __init__(self, func, *args):
        super().__init__()

        self.func = func
        self.args = args

        self.setDaemon(True)
        self.start()  # 在这里开始

    def run(self):
        self.func(*self.args)

 

(一)建立聊天工具

1. socket(套接字)编程技术

首先在建立连接时,由一方根据要连接的套接字发出请求,具体实现调用connect_listen()函数。

 

def connect_listen():  # 监听部分
    ip_in = var_ip_init.get()
    port_in = int(var_port_init.get())
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((ip_in, port_in)) 
    server.listen()
    sock, addr = server.accept()
    MyThread(receive_message, sock)

 

另一方通过调用connect_find()进入监听状态,等待建立连接。

 

def connect_find():  # 搜索部分
    var_ip_connent.set(e_ip_main.get())
    var_port_connent.set(e_port_main.get())
    ip_in = var_ip_connent.get()
    port_in = int(var_port_connent.get())
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((ip_in, port_in))

    MyThread(receive_message, client)

 

2. 聊天工具的实现

聊天工具实现的核心分为两个部分,包括发送消息和接收消息,在代码的实现上分别通过调用函数send_message_presend()receive_message()实现,其中receive_message()通常使用申请新的线程来调用,具体原因上文中已经讲到。

 

def receive_message(sock):
    while True:
        data = sock.recv(1024)
        # print(data.decode('utf-8'))
        if data.decode('utf-8') != '#':
            str_info = "\nRecieve:\n    " + data.decode('utf-8')
            print(str_info)
            t_record.insert('insert', str_info)
            t_record.update()

            str_info = "[NOTE]收到消息,请回信!\n"
            t_log.insert('insert', str_info)
            print(str_info)
            t_log.update()
        else:
            name = sock.recv(1024).decode('utf-8')
            filesize_byte = int(sock.recv(1024).decode('utf-8'))

            str_info = "[NOTE]准备接收文件!\n"
            t_log.insert('insert', str_info)
            print(str_info)
            t_log.update()

            f = open(name, 'wb')
            recv_mesg =sock.recv(filesize_byte)
            print(recv_mesg)
            f.write(recv_mesg)
            f.close()
            # sock.send('success'.encode('utf-8'))

            str_info = "[NOTE]文件-" + name + "-接收完成!\n"
            t_log.insert('insert', str_info)
            print(str_info)
            t_log.update()

        time.sleep(2)

 

def send_message_presend(sock):  # 发送功能
    str = e_message.get()
    str_info = "\nSend:\n    " + str
    var_entry_message.set(str)
    t_record.insert('insert', str_info)
    t_record.update()

    sock.send(str.encode('utf-8'))

    str_info = "[NOTE]已经成功发送,请等待回信!\n"
    t_log.insert('insert', str_info)
    t_log.update()

    time.sleep(2)

 

整个聊天工具实现的另一个重要部分就是页面的设计,在输入框中输入要发送的消息,点击发送即可,消息记录(接收到或者发送的消息)会显示在特定部分。界面如下图所示:

 

 

 

 

 

 

 

 

3. 关于多人聊天实现的探索

因为使用了图形化界面,在创建新界面以及在同一界面中容易产生线程冲突,没有实现多人聊天的功能,这里阐述多人聊天功能实现的思想。

 

 

 

 

 

 

 

 

如上图所示,主机用户相当于群主的身份,主机用户利用多线程技术与多个一般用户建立socket连接,形成一个群组,主机用户发送消息可以直接广播给多个一般用户;一般用户先将要发送的消息发送给主机用户,主机用户监听(接收)到消息时,向除消息的发送方以外的用户广播此消息,进而达到聊天室(多人聊天)的目的。

 

(二)传输文件

1. 文件的本地处理(分片)

图形化界面中文件的发送部分如下图所示:

 

 

文件的发送方在提供本地文件位置后,程序会将整个文件加载,并进行编码。如果文件超过分片的大小限制,则将文章进行分片,在每个分片前拼接当前分片在原文件中的位置(占用固定长度)。分片如下图所示:

 

 

 

 

 

 

 

2. 分片的传输

在发送文件的分片前,由发送方先发送一条消息,其中包括特定字符表示,接收方收到此消息后意味着要开始准备接收文件。

接下来的一个轮次由发送方发送文件的大小、分片的个数等确认信息,接收方收到后,返回确认信息。

发送方收到确认后正式开始发送文件的分片,接收方每次回答确认,发送方收到确认后继续发送下一个文件分片,直至整个文件传输完成。

 

3. 文件的整合

接收方逐一接收分片,全部接收完成后,对每个分片进行处理,读出每个分片前端固定长度的位置信息,进行拼接,进行解码,并将文件保存至当前程序所在的文件夹,从而完成整个文件的传输过程。

 

 

 

 

 

 

软件的运行方式如下各图所示,相应关键信息在图上标出,在此不做赘述。

注明:为了截图方便,以下过程在一台主机上模拟进行,所以套接字的设置较随意,但这个软件已经经过验证,可以在不同主机之间通信。

 

 

     

 

     

 

 

 

六. 总结体会

这次课程设计选择了两个方向去实现,包括聊天工具的实现以及文件的传输。

整个实验的主要重难点集中在两部分:socket编程以及图形化界面的实现。

先来看socket编程,这是整个实验的核心部分,也是和课程关联最密切的。其核心思想就是利用套接字socket在两台计算机之间建立连接,在传输层的协议中,本次实验选择能够提供可靠传输的TCP连接。整个socket编程中并没有特别需要注意的问题,因为用到的也仅仅是最基础的一部分,我们要做的就是调用封装好的函数,循规蹈矩就好。消息传输也好,文件(分片)传输也罢,都是发送方把要传输的内容编码交给下层传输,接收方收到信息解码,再根据需要提取、组合完成内容的重现。其中文件的分片思想很好的体现了在真实计算机网络中的相关协议设计思想。

再来看图形化界面的实现,在这一部分中出现了较多的问题,主要是图形化界面和多线程的冲突问题,在创建新窗口时经常会出现线程的冲突,当然这个问题从根本上来自于对线程的管理不合理,也因此考虑到时间关系,并没有很好的解决,从而也选择放弃了在聊天工具的实现部分实现多人聊天的功能,但具体设计思想体现在上述操作方法与实验步骤部分。在设计图形化界面的初期会发现其实设计一个真正方便使用的软件并不简单,想要达到一个良好的交互是需要不断调整的。

回顾整个项目,在代码的程式上仍然存在很大的问题,很大一部分原因也是来自于tkinter(图形化界面实现)的使用,Button按钮的响应、函数的调用很复杂,整个代码的可读性较低。Python语言的类编程思想也没有很好的体现,是以后要加强改进的部分。

这次的课设集中于socket技术,深究的大部分是计算机网络体系应用层以及网络层的部分,没有涉及较底层的技术。收获主要在于对于聊天工具以及文件传输原理的了解,虽然真正有实用价值的软件需要考虑更多的因素,但是只有真正了解原理才能更好的研究改进已存的技术。

当然,限于自己的知识水平和经验的不足,在程序设计的过程中,我也遇到了不少问题。通过自己的不断钻研和查询资料使问题都得以解决。不过,虽然完成了这次开发,但我依旧要清醒地认识到自己所编的内容中还有许多不完善的地方,而且,由于时间有限,有些在实际应用中可能需要的功能并没有完全实现,比如说以下功能还有待完善:用户登录、发送表情、屏蔽发言、群聊的功能等等。

这次之所以选择使用python来完成设计,一是python和其他语言的函数的使用及其意义大致相同,易于上手,二是希望通过这次的课设能够为以后的科研训练等等内容打下基础,当然,无论使用什么语言什么开发工具,能够学到知识并且能够运用自己所学解决问题才是最重要的。在此感谢老师对我的指导,不仅指出程序上的不足也指出了学习方法上的欠缺,谢谢老师!

 

 

 

 

;课设所有代码如下

import os
from mttkinter import mtTkinter as tk
import time
import socket
import threading


class MyThread(threading.Thread):
    def __init__(self, func, *args):
        super().__init__()

        self.func = func
        self.args = args

        self.setDaemon(True)
        self.start()  # 在这里开始

    def run(self):
        self.func(*self.args)


window_init = tk.Tk()
window_init.title("InUs")
window_init.geometry('300x120')

var_record = tk.StringVar()
var_ip_init = tk.StringVar()
var_port_init = tk.StringVar()
var_ip_connent = tk.StringVar()
var_port_connent = tk.StringVar()
var_entry_message = tk.StringVar()
var_log = tk.StringVar()

l_ip_init = tk.Label(window_init, text='IP:', width=6, height=1)  # 第一个窗口
l_ip_init.place(x=60, y=8)

e_ip_init = tk.Entry(window_init)
e_ip_init.place(x=100, y=10)

l_port_init = tk.Label(window_init, text='端口:', width=6, height=1)
l_port_init.place(x=54, y=38)

e_port_init = tk.Entry(window_init)
e_port_init.place(x=100, y=40)


def start_socket():  # 第二个窗口
    window_main = tk.Tk()
    window_main.title("InUs")
    window_main.geometry('900x650')

    menubar = tk.Menu(window_main)  # 文件部分
    filemenu = tk.Menu(menubar, tearoff=0)
    menubar.add_cascade(label='文件', menu=filemenu)
    # filemenu.add_cascade(label='功能一')  # 功能未加
    # filemenu.add_cascade(label='功能二')  # 功能未加

    operatemenu = tk.Menu(menubar, tearoff=0)  # 操作部分
    menubar.add_cascade(label='操作', menu=operatemenu)

    def send_file(sock):
        address = e_file_main.get()
        name = e_filename_main.get()
        filesize_byte = os.path.getsize(address)
        sock.send('#'.encode('utf-8'))
        time.sleep(0.5)
        sock.send(name.encode('utf-8'))
        time.sleep(0.5)
        sock.send(str(filesize_byte).encode('utf-8'))
        time.sleep(0.5)
        with open(address, 'rb') as f:
            data = f.read()
            sock.send(data)

        """
        time.sleep(3)
        str_reve = sock.recv(1024).decode('utf-8')
        print('dsf ' + str_reve)
        if str_reve == 'success':
            str_info = "[NOTE]文件传输成功\n"
            t_log.insert('insert', str_info)
            print(str_info)
            t_log.update()
        """

    # operatemenu.add_cascade(label='功能一', command=send_file)  # 功能未加
    # operatemenu.add_cascade(label='功能二')  # 功能未加

    helpmenu = tk.Menu(menubar, tearoff=0)  # 帮助部分
    menubar.add_cascade(label='帮助', menu=helpmenu)
    # helpmenu.add_cascade(label='功能一')  # 功能未加

    t_record = tk.Text(window_main, width=80, height=15)
    t_record.place(x=50, y=30)

    var_socket = '根据初始化输入,当前套接字信息如下\nIP: ' + var_ip_init.get() + '\n端口: ' + var_port_init.get()

    l_show_socket = tk.Label(window_main, text=var_socket, width=30, height=3)
    l_show_socket.place(x=660, y=60)

    l_ip_main = tk.Label(window_main, text='IP:', width=6, height=1)
    l_ip_main.place(x=660, y=200)

    e_ip_main = tk.Entry(window_main)
    e_ip_main.place(x=700, y=200)

    l_port_main = tk.Label(window_main, text='端口:', width=6, height=1)
    l_port_main.place(x=654, y=240)

    e_port_main = tk.Entry(window_main)
    e_port_main.place(x=700, y=240)

    l_file_main = tk.Label(window_main, text='文件位置:', width=8, height=1)
    l_file_main.place(x=635, y=330)

    e_file_main = tk.Entry(window_main)
    e_file_main.place(x=700, y=330)

    l_filename_main = tk.Label(window_main, text='文件名字:', width=8, height=1)
    l_filename_main.place(x=635, y=370)

    e_filename_main = tk.Entry(window_main)
    e_filename_main.place(x=700, y=370)

    e_message = tk.Entry(window_main, width=60)
    e_message.place(x=50, y=250)

    window_main.config(menu=menubar)

    def receive_message(sock):
        while True:
            data = sock.recv(1024)
            # print(data.decode('utf-8'))
            if data.decode('utf-8') != '#':
                str_info = "\nRecieve:\n    " + data.decode('utf-8')
                print(str_info)
                t_record.insert('insert', str_info)
                t_record.update()

                str_info = "[NOTE]收到消息,请回信!\n"
                t_log.insert('insert', str_info)
                print(str_info)
                t_log.update()
            else:
                name = sock.recv(1024).decode('utf-8')
                filesize_byte = int(sock.recv(1024).decode('utf-8'))

                str_info = "[NOTE]准备接收文件!\n"
                t_log.insert('insert', str_info)
                print(str_info)
                t_log.update()

                f = open(name, 'wb')
                recv_mesg =sock.recv(filesize_byte)
                print(recv_mesg)                                            # fsdfsfds
                f.write(recv_mesg)
                f.close()
                # sock.send('success'.encode('utf-8'))

                str_info = "[NOTE]文件-" + name + "-接收完成!\n"
                t_log.insert('insert', str_info)
                print(str_info)
                t_log.update()

            time.sleep(2)

    def send_message_presend(sock):  # 发送功能
        str = e_message.get()
        str_info = "\nSend:\n    " + str
        var_entry_message.set(str)
        t_record.insert('insert', str_info)
        t_record.update()

        sock.send(str.encode('utf-8'))

        str_info = "[NOTE]已经成功发送,请等待回信!\n"
        t_log.insert('insert', str_info)
        t_log.update()

        time.sleep(2)

    def connect_listen():  # 监听部分
        ip_in = var_ip_init.get()
        port_in = int(var_port_init.get())

        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        str_info = "[NOTE]开始监听,等待连接...\n"
        t_log.insert('insert', str_info)
        str_info = "[CAUTION]监听过程中,程序可能出现无响应状态,请不要关闭程序,建立连接后程序恢复正常!\n"
        t_log.insert('insert', str_info)
        t_log.update()

        server.bind((ip_in, port_in))
        # server.bind(('0.0.0.0', 8000))
        server.listen()

        sock, addr = server.accept()

        str_info = "[NOTE]已经成功连接,请等待对方首先发送消息!\n"
        t_log.insert('insert', str_info)
        t_log.update()

        b_send = tk.Button(window_main, text='发送', width=5, height=1, command=lambda: send_message_presend(sock))
        b_send.place(x=600, y=250)

        b_file = tk.Button(window_main, text='发送文件', width=10, height=1, command=lambda: send_file(sock))
        b_file.place(x=720, y=410)

        data = sock.recv(1024)
        # print(data.decode('utf-8'))

        str_info = "\nRecieve:\n    " + data.decode('utf-8')
        t_record.insert('insert', str_info)
        t_record.update()

        MyThread(receive_message, sock)

    b_listen = tk.Button(window_main, text='开始监听', width=10, height=1, command=connect_listen)
    b_listen.place(x=720, y=130)

    def connect_find():  # 搜索部分
        var_ip_connent.set(e_ip_main.get())
        var_port_connent.set(e_port_main.get())

        ip_in = var_ip_connent.get()
        port_in = int(var_port_connent.get())

        client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        str_info = "[NOTE]开始寻找" + ip_in + ",等待连接...\n"
        t_log.insert('insert', str_info)
        t_log.update()

        client.connect((ip_in, port_in))
        # client.connect(('127.0.0.1', 8000))

        str_info = "[NOTE]已经成功连接,请首先发送消息!\n"
        t_log.insert('insert', str_info)
        t_log.update()

        b_send = tk.Button(window_main, text='发送', width=5, height=1, command=lambda: send_message_presend(client))
        b_send.place(x=500, y=250)

        b_file = tk.Button(window_main, text='发送文件', width=10, height=1, command=lambda: send_file(client))
        b_file.place(x=720, y=410)

        MyThread(receive_message, client)

    b_find = tk.Button(window_main, text='尝试寻找', width=10, height=1, command=connect_find)
    b_find.place(x=720, y=280)

    t_log = tk.Text(window_main, width=110, height=10)
    t_log.place(x=50, y=470)


def init_sure():
    if var_ip_init.get() == '':
        var_ip_init.set('0.0.0.0')
    else:
        var_ip_init.set(e_ip_init.get())
        print(var_ip_init.get() + '[TEST]')
    if var_port_init.get() == '':
        var_port_init.set('8000')
    else:
        var_port_init.set(e_port_init.get())

    start_socket()


b_init_reset = tk.Button(window_init, text='重置', width=5, height=1, command=init_sure)  # 第一个窗口的按钮
b_init_reset.place(x=100, y=70)

b_init_sure = tk.Button(window_init, text='确定', width=5, height=1, command=init_sure)  # 第一个窗口的按钮
b_init_sure.place(x=160, y=70)
window_init.mainloop()

 

posted @ 2020-05-25 16:41  xuquanyi  阅读(523)  评论(0编辑  收藏  举报