之前在学习廖雪峰老师的python3教程,看到socket的那一章时,有一个想法突然冒出来,是不是可以通过socket做一个简单的聊天室软件,在构思了一段时间后,废话不多说,开始实施:

一、思维导图


按照自定的报文格式构造和解析消息在客户端完成,服务端只负责接受消息和分发消息。

二、所需库与模块

  • socket
  • threading
  • tkinter
  • datetime
  • time
  • os

三、报文

了解过web的同学应该知道http协议,http协议是人们规定的一个超文本传输协议,一般是用作web的,而在协议中又分别有请求报文与响应报文,我们今天的聊天室也需要一个报文,但不需要http协议的报文那么复杂,我们自己制定一个简单的报文。

方法有n(normal) , e(enter) , q(quit)
enter方法:e-$-timestamp-$-name ,不需要提供内容,需要时间戳和用户名。
quit方法:q-$-name ,不需要提供时间戳和内容,只需提供用户名。

四、客户端

chatroom_client.py

import tkinter as tk
import tkinter.messagebox
import socket
import datetime
import os
import threading
import time

def analyze_and_build(raw):
    rawlist=raw.decode('utf-8').split('-$-')
    method=rawlist[0]
    if method=='n':
        time=datetime.datetime.fromtimestamp(float(rawlist[1])).strftime('%Y-%m-%d %H:%M:%S')
        name=rawlist[2]
        content=rawlist[3]
        return '%s\n%s : %s\n\n' % (time,name,content)
    elif method=='e':
        time=datetime.datetime.fromtimestamp(float(rawlist[1])).strftime('%Y-%m-%d %H:%M:%S')
        name=rawlist[2]
        return '%s\n%s加入了聊天\n\n' % (time,name)
    elif method=='q':
        name=rawlist[1]
        return '%s退出了聊天\n\n' % name

def mainview():
    name=nameEntry.get()
    host=hostEntry.get()
    port=portEntry.get()
    if name=='' or host=='' or port=='':
        tkinter.messagebox.showinfo(title='login failed',message='Value cannot be empty!')
    else:
        s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        s.connect((host,int(port)))
        loginWindow.destroy()
        mainWindow=tk.Tk()
        mainWindow.title('Chatroom(%s)' % name)
        mainWindow.geometry('440x390')

        showText=tk.Text(mainWindow,height=20,width=60)
        emptyLabel=tk.Label(mainWindow,height=1)
        writeText=tk.Text(mainWindow,height=4,width=60)
        showText.tag_config('red',foreground='red')


        showText.pack()
        emptyLabel.pack()
        writeText.pack()


        s.send(('e-$-%s-$-%s' % (datetime.datetime.now().timestamp(),name)).encode('utf-8'))

        def fresh():
            while True:
                data=s.recv(1024)
                rawlist=data.decode('utf-8').split('-$-')
                if (rawlist[0]=='n' or rawlist[0]=='e') and rawlist[2]==name:
                    showText.insert('end',analyze_and_build(data),'red')
                else:
                    showText.insert('end',analyze_and_build(data))
                showText.see('end')

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

        def send_message():
            data=writeText.get('0.0','end')
            if data=='\n':
                pass
            else:
                s.send(('n-$-%s-$-%s-$-%s' % (datetime.datetime.now().timestamp(),name,data)).encode('utf-8'))
                writeText.delete('0.0','end')

        sendButton=tk.Button(mainWindow,text='send',width=5,height=1,command=send_message)
        sendButton.pack()

        def quit():
            s.send(('q-$-%s' % name).encode('utf-8'))
            time.sleep(0.5)
            s.send('_quitchatroom'.encode('utf-8'))
            mainWindow.destroy()
            os._exit(0)

        mainWindow.protocol('WM_DELETE_WINDOW',quit)
        mainWindow.mainloop()





loginWindow=tk.Tk()
loginWindow.title('login')
loginWindow.geometry('200x250')

loginLabel=tk.Label(loginWindow,
    text='Login',
    font=('Arial',15),
    width=10,height=2)

nameLabel=tk.Label(loginWindow,
    text='name:',
    font=('Arial',10),
    width=6,height=1)

hostLabel=tk.Label(loginWindow,
    text='host:',
    font=('Arial',10),
    width=6,height=1)

portLabel=tk.Label(loginWindow,
    text='port:',
    font=('Arial',10),
    width=6,height=1)

nameEntry=tk.Entry(loginWindow)
hostEntry=tk.Entry(loginWindow)
portEntry=tk.Entry(loginWindow)

loginButton=tk.Button(loginWindow,
    text='login',
    width=10,
    height=1,
    command=mainview)

loginLabel.pack()
nameLabel.pack()
nameEntry.pack()
hostLabel.pack()
hostEntry.pack()
portLabel.pack()
portEntry.pack()
loginButton.pack()

loginWindow.mainloop()

五、服务器

chatroom_server.py

import socket,threading,os
host=os.environ.get('HOST') or '127.0.0.1'
port=os.environ.get('PORT') or 9999
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind((host,int(port)))
s.listen(5)
print('Waiting for connection...')
socks={}

def tcplink(sock,addr):
    global socks
    print('Accept new connection from %s:%s...' % addr)
    while True:
        data=sock.recv(1024)
        if data.decode('utf-8')=='_quitchatroom':
            socks.pop(sock)
            break
        for eachsock in socks:
            eachsock.send(data)
    sock.close()
    print('Connection from %s:%s closed.' % addr)

while True:
    sock,addr=s.accept()
    socks[sock]=addr
    t=threading.Thread(target=tcplink,args=(sock,addr))
    t.start()

六、演示

1.打开聊天室服务器

2.打开聊天室客户端

3.填写信息

4.成功登陆,出现聊天室窗口

5.输入不同人名,相同地址与端口,再打开一个客户端。

6.聊天与退出

七、回顾与思考

至此,这个聊天室就基本完成了,使用时间戳的原因是因为时间戳不包含时区,然后再在本地转换,但是这个聊天室还是有很多缺点的,比如很多地方没有验证,可能会出现错误、窗口没有美化等等,如果有人有兴趣完善的话,那真的要给你点一个大大的赞,如果有需要,还可以用pyinstaller打包成exe文件,那样使用就会很方便。

如果你有什么意见和建议,欢迎在评论区留言。

评论

  • 最新随笔

  • 对我来说,写东西很好的一点是,自己写的东西,后面基本都会再看,一遍或者很多遍。这样就可以很好地发现自己之前的漏洞,并看看现在有什么变化。
  • 前段时间应该是百度更新了页面结构,那个黑帽泛目录网站我也没有去更新代码,就导致了文章页没有相关推荐,没想到这时候谷歌开始收录了,而且效果还不错。其实之前在谷歌一直不收录的时候就有怀疑过没有尽头的相关推荐很可能是导致不收录的原因,不过也一直没有去管它,而这次误打误撞的收录也恰恰证实了这个想法。(btw,其实这个网站从出现固定链接开始叫泛目录应该不太准确)
  • 最近吉他谱网站iloveguitar上线了,这几天在填充一些内容花了一些时间,这个网站也是使用flask作为后端框架的,前端UI框架使用的是bootstrap4,喜欢吉他的小伙伴欢迎来这里逛一逛哟,有什么建议欢迎在留言板提一下哟。
  • 那个泛目录网站发布了几个星期了,百度没怎么来爬,收了一个首页,谷歌直接就不收了,倒是一个查外链的网站天天来爬(笑哭),还有建议一点就是泛目录还是用在有建站历史的域名比较好(因为已经过了审核期),新域名就尽量不要用了。
  • 刚刚又发了一篇,最近文章百度都不太收录,orz,哈哈
  • 之前那个用python做的静态资源服务器,我又发现了一大用处,现在我都用来在局域网里的不同电脑之间传输文件,真的好用哈哈
  • 今天交换到了博客有史以来第一条友情链接
  • 刚刚更新了一下博客,修复了一些bug,添加了一个项目页