Socket编程:一点点教你做个聊天工具——(三)可视化

 

emmmm,为什么突然就可视化了呢。不用担心,并不突兀,后面所有的问题都是在可视化的过程中出来的,我们一边写,一边解决。

 

上次基于TCP协议实现的C-S模式,不知道大家有注意到没有。如果你的server不启动,直接用client发送消息,程序会报错,告诉你找不到请求的主机。

 

其实这里就是TCP协议的不足之处,也不能叫做缺点,因为它提供的是可靠传输。什么是可靠传输呢?简单理解,就是只要你发送了消息,我一定给你传到。

 

想想看TCP是怎么做到的。

 

对!就是因为传数据开始前server和client之间有个建立链接的过程,只有建立好了链接,确定了server和client都没问题,才开始发送数据。

 

上篇文章的尾部,我们留下了UDP协议,这个协议和TCP有什么区别呢?其实就是上面说到的,发送数据的时候,不再建立链接,而是直接向目的主机进行发送,至于目的主机是否处于打开状态,甚至网络中是否真的存在这台主机,我都不管。

 

也因为UDP协议的这种属性,可以让我们在client发送消息的时候不会报错,即使server没有打开。这就是上篇文章里,我们为什么说道UDP协议比TCP协议更适合做这个小程序的原因。

 

相对应的,UDP就没有办法提供可靠的传输。也就是说,反正我发送了,server收到收不到我不管。我们简单探讨两种情况,如果server没有启动,数据在网络中找到了目标主机,但是没有运行server程序,也就是说server主机不接收。server主机就会把它丢掉。

 

还有一种情况,发送消息的目的主机在网络中不存在,数据会一直在网络中的路由器间传递,找目的主机。不过不用担心,网络中一个数据在路由器间传递到某个次数时,路由器就把它丢了,不会让他一直在网络中,占用网络资源。

 

以上就是理论的基础了。

 

下面说代码实现吧,上次的小例子中,我们创建socket对象没有给参数,直接用的默认值。默认的使用的传输协议是TCP。也因此,代码开始前,我们要从socket的参数开始说起。

 

socket参数:

family(地址簇,机器IP采用的IPv4还是IPv6):socket.AF_INET(IPv4,default),socket.AF_INET6(IPv6)。

type(采用的传输协议类型):socket.SOCK_STREAM(TCP流式,default),socket.DGRAM(UDP,数据包式),socket.SOCK_RDM(可靠的UDP模式),socket.SEQPACKET(可靠连续数据包),socket.SOCK_RAM(原始套接字)【我们只关注前两个就可以了】

 

还有proto协议号这个参数,默认为零,我们不关注了。

 

依旧采用上篇文章的方式,采用UDP,我们怎么写server:

import socket

sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#maybe? waiting test
ip_port = ('127.0.0.1', 55555)
#bind listen
sk.bind(ip_port)
#waring message
print('Running')
while True:
    print(sk.recv(1024).decode())
#close connection
conn.close()

首先,socket对象创建的时候,参数先给了。其次,我们看到,对象在bind完ip_port之后,直接进行了recv接受命令,没有前面的connect链接的步骤了。

 

client:

import socket

sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

ip_port = ('127.0.0.1', 55555)

while True:
    send = input('What :')
    sk.sendto(send.encode(), ip_port)

 

代码量已经不能忍受了,创建好了socket对象,定义了目标主机的ip_port,直接对目标主机进行发送。不过send方法变成了sendto方法。仅此而已了。

 

先别启动server,直接用client发送数据:

一点毛病没有。

 

打开server:

 

现在再用client发送:

看server输出:

棒呆!

 

得嘞,初始的发送消息的逻辑就这样了。

 

接下来一点点可视化吧。我是直接用的tkinter,默认大家有GUI的基础了,我们平衡代码逻辑和tkinter的实现。

 

首先,我们分析下,我们需要一个输入框写我们需要发送的数据,还要有一个文本框来展示发送以及收到的消息,再来一个发送按钮。

 

来个简单的菜单栏,用于编辑自己的IP和目的主机的IP。图片中顶部的Let us talk!不是艺术字,是一张图片,随便你们怎么设计吧,无关紧要。我用了canvas组件。

 

先来写主界面的布局:一个文本框,一个输入框,一个发送按钮:

import tkinter as tk

root = tk.Tk()
root.title('MyChat')
root.geometry('800x500')

t = tk.Text(root, height=10)
t.pack()

entry = tk.Entry(root)
entry.pack()
    
button = tk.Button(root, text='submit', command=insert_point)
button.pack()

root.mainloop()

基本的布局也就这样了,先来想发送数据时候的逻辑。我们点击按钮,需要做的操作有三个:

1.把输入框的数据显示在文本框里。

2.把输入框的数据发送给目标主机。

3.把输入框的数据清空方便下次输入。

 

一个个来,首先,按钮绑定事件用command参数,上面代码中我们绑定了inert_point事件,现在我们就来编写这个函数。函数的功能,需要完成上面的3个操作。

 

首先,获得输入框的数据,直接用相应对象的get方法。

 

文本框中插入从输入框获得的数据,用insert。

 

发送给目标主机,前面说了sendto。

 

输入框清空,用delete。从头到尾的delete,就是clear(皮~)。

 

def insert_point():
    var = entry.get()
    t.insert('insert', var + '\n')
    sk_send.sendto(var.encode(), ip_port_send)
    entry.delete(0, tk.END)

 

我们的函数就这样写。文本框对象insert方法的第一个参数是插入方式,insert是在末尾插入。我们把插入的数据最后加上个换行符,好看些。

 

输入框的delete方法,两个参数分别指明删除的起点和终点。我们从头删到尾,就和清空一样了。

 

最终的代码是这样的:

import tkinter as tk

sk_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

ip_port_send = ('127.0.0.1', 55556)

root = tk.Tk()
root.title('My QQ')
root.geometry('800x500')
    
t = tk.Text(root, height=10)
t.pack()

entry = tk.Entry(root)
entry.pack()

def insert_point():
    var = entry.get()
    t.insert('insert', var + '\n')
    sk_send.sendto(var.encode(), ip_port_send)
    entry.delete(0, tk.END)
    
button = tk.Button(root, text='submit', command=insert_point)
button.pack()

root.mainloop()

ip_port_send写成你要发送到的ip和port。我们看到,这样一个具有client功能的可视化工具就完成了,server那边的接收呢?

 

一样的,基本的布局,然后是逻辑功能。

 

继续分析,接受端需要做什么?循环等待接收,只要一有数据,就把数据写进文本框里。嗯,比发送要做的事情少多了。

 

import tkinter as tk
import socket

sk_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

ip_port_recv = ('192.168.1.117', 55555)

sk_recv.bind(ip_port_recv)


while True:
    var = sk_recv.recv(1024).decode()
    t.insert('insert', var + '\n')

root = tk.Tk()
root.title('My QQ')
root.geometry('800x500')
    
t = tk.Text(root, height=10)
t.pack()

entry = tk.Entry(root)
entry.pack()

def insert_point():
    var = entry.get()
    t.insert('insert', var + '\n')
    sk_send.sendto(var.encode(), ip_port_send)
    entry.delete(0, tk.END)
    
button = tk.Button(root, text='submit', command=insert_point)
button.pack()

root.mainloop()

没什么特殊的,while循环接收数据,收到了数据就插入到文本框里。

 

ok了,现在你可以玩了,有窗口的聊天工具。

 

 

 

 

 

 

 

 

 

 

 

 

我在等你回来,因为server端根本打不开,可视化界面根本出不来是不是。知道原因在哪吗?

 

看这里:

 

while是个循环,程序会挂起在sk_recv.recv()处,直到接收到了数据,才会往下走,可是insert执行完毕后,就进入到了下一次循环中等待下一次的数据接收。

 

所以后面的tkinter代码都不能执行。

 

那......把while放到tk代码之间呢?

 

很遗憾,也不行!

 

 

因为界面的显示也是一个循环,其实因为界面在一遍遍的刷新,我们才看到了好似是可操作性的窗口。这和写游戏差不多,让小人动起来,其实就是一次次的循环输出刷新页面而已。

 

那问题来了,在一个“死”循环里有一个“死”循环,会怎样?

 

对啊~,程序会卡在内层循环里,而外层循环循环不了完整地一次。界面还是出不来。

 

绝望~,没办法解决了吗?有啊,不然我就不会写这个系列了。其实很简单,我们就是想让接收数据的循环和界面显示的循环互相不要影响,你干你的,他干他的,必要时把你们的数据分享下就可以。

 

想到办法了吗?对啊,前面有说道嘛!多线程。

 

简单说下,照顾非科班基础知识不那么充足的读者。

 

我们知道我们的程序运行起来是一个进程。而线程是操作系统够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

 

大概懂了吧。

 

多线程怎么用呢?别担心,也很简单。

 

threading,这也是个内置库,不用安装。

 

首先我们把接收数据的那个循环包装一下,写成一个函数,起名叫做loop:

def loop():
    while True:
        var = sk_recv.recv(1024).decode()
        t.insert('insert', var + '\n')

 

创建线程对象,并用loop初始化,之后调用start方法,就可以运行起一个指定的线程了:

t1 = threading.Thread(target=loop)
t1.start()

到这里loop就成了我们进程中的一个线程,去自己运行它的了,我们也就不管他了。

 

弄到这里,我们遇到的难题基本已经处理完毕了。不过你需要把server的功能和client合并起来,因为我们的聊天工具,既要能接收数据,又要能发送数据,好在接收的循环已经在一个线程中了。而发送的功能在界面显示的循环中,已经不会起冲突了,大胆合并吧。

 

还有一点点问题,就是我想让用户可以自己设置他的IP和目的主机的IP,而不是我的代码一开始就写好的,固定了的IP。这里就是config菜单的作用了。

 

我们放在下篇文章说吧。

 

不出意外,那应该是这个系列的最后一篇了。末尾我会把完整代码贴出来。

 

为什么现在不贴?因为文章还没写完嘛。怕你学习热情散尽,直接copy。

 

还有一点,我写代码的习惯不好,没注释,也给我点时间把注释写上。(笑)

 

好,结束吧。我们遗留下来的问题——用户自定义本地与目标IP。

 

准确点,菜单栏的配置功能及界面提示。(加上顶部图片的canvas显示,我们也捎带提下。)

发布了53 篇原创文章 · 获赞 80 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_41500251/article/details/90216451