python TCP实现聊天程序

这是我Python课的课程设计,按理说这种聊天程序应该用UDP的,但是我的课程设计题目是TCP的,所以就用TCP写了

由于是课程设计,所以写的也没有那么复杂,后面有时间了再添加新功能,如果有什么建议可以联系我(邮箱cytcyt123@163.com),第一次发博客,请各位大神多多指教,有什么需要改进(包括但不限于程序,别的问题也可以指出来,我会虚心请教的),主要实现了多个客户端之间能互发消息,而且如果对方没在线,待其上线时服务器会自动发给他,所有的消息都会保存在数据库

使用了面对对象思想,服务器是数据(操纵数据库),UI,逻辑分离,因为客户端的数据都是从服务器来的,所以是数据和逻辑在一块,是UI和逻辑分离

总体说下程序,不明白的地方请评论,我再补充

百度云 链接:https://pan.baidu.com/s/1HievjAta3du6yclIWXMDYg 提取码:jpj7

Github: https://github.com/yougtao/PyChat

server.py运行一个, client.py可以运行多个, 要注意修改端口; python3环境,下载好相应的库, mysql数据库(后面有介绍)

下载不了联系我邮箱:cytcyt123@163.com

 

目录

先上运行界面:

说一下程序的一些要点:

服务器程序:

Database类,获取数据修改数据都是它的活了

ServerUI类,只负责显示界面,处理显示消息,刷新联系人列表等

Server逻辑类,关键了



先上运行界面:

一个服务器和两个客户端,模仿了Tim的界面,服务器绿背景是已经上线的,灰色为离线

聊天窗口内也是跟以前的QQ一样的,原谅我气泡消息形式做不出来,不能与时俱进

有时间了把头像啥的也弄出来,早日打败腾讯哈哈哈

橙色是这个人有新消息来了!

 

说一下程序的一些要点:

1. 客户端与服务器之间发送的所有数据格式都是字典形式:{"type":1, "from":12, "to":23, "data":"要发的东西"}  type是类型,to是接收人

2.type字段,服务端收到客户端发的数据后根据type字段来判断这是什么数据,偶数的都是对应的服务器返回的数据:

1是登录相关,例如要发送登录请求,判断登录账号密码,hello消息等;2是服务器的回应

3是请求数据;4是服务器返回的结果

11是给联系人发的消息,客户端发到服务器;4是服务器发到对应的客户端

13是给群联系人发的消息;14是服务器发给客户端

服务器有个接收socket,用来接受某个用户发来的消息,然后进行处理(该转发转发,该显示显示等)

每个用户也会建立一个接收的socket,用来接受服务器发来的消息,然后也是该显示显示等(这里不要把程序上的服务器客户端和socket层面上的服务器客户端混为一谈,也就是服务器和客户端互为 socket服务器socket客户端,要是还不明白的发评论提问)

每个用户在登录时会将自己的IP地址和接受socket的端口号发送给服务器保存起来,服务器在给客户端发送数据的时候就根据这个来和客户端(刚刚说了,客户端会建立一个接收socket的)建立连接(可怕的TCP,还要先三次握手建立好连接才能发送,要是用UDP多好,直接发,就怪我在抢题目的时候没抢到吧)

服务器程序:

服务端程序,一共三个类:数据类(Database),UI类(ServerUI),逻辑类(Server)

Database类,获取数据修改数据都是它的活了

用了pymysql库,我这里连接 host="127.0.0.1" user="root",password="",database="python" 

先看下数据库(这个数据库建的不是很好,因为后面代码重写了一次,所以有些字段没用了,懒得没有删)

数据库名:python,四个表:user用户表,group群表,group_member群成员表,message消息记录表

user表

 group表 group_member表 

message表

id(主键)int id(主键)int idint id(主键)int
nicknamevarchar(50) namevarchar(50) memberIDint 

source

int
statusint       targetint
ipint       timedatatime
portint       messagevarchar
         statusint

 

里面建了几个函数,供逻辑类(Server类)来调用,挑几个说一下

像这个addMessage()函数,将消息添加到数据库,source:发送人的id,target:接受人的id,time:发送时间(这里记录的是服务器接受到的时候的时间),message:消息内容,status:1代表已经发送给接收人了,0代表接收人还没上线(上线时会自动发给他

    # 添加消息
    def addMessage(self,source,target,time,message,status)

像这个getUserList()函数,因为服务器会显示用户列表,所以这里建了这么个函数,逻辑类只需要调用这个函数就能获取到用户列表,然后交给UI类去显示,返回的格式是字典数组(不知道python字典是啥的去百度)

像这样 [{"id"}:36,"name":"昵称","status":-1,"ip":0,"port":0}, {"id"}:36,"name":"昵称","status":-1,"ip":0,"port":0}]

    # 获取用户列表
    def getUserList(self)
    # 获取群列表
    def getGroupList(self)

getContacts()函数:这个是取出来发给客户端的,因为里面的字段不一样所以和上面的分开了

    # 获取contacts列表
    def getContacts(self)

其他的像 findIndex() findName() findSock() isOnline()等都有注释,而且看名字就明白

 

ServerUI类,只负责显示界面,处理显示消息,刷新联系人列表等

一共有4个函数,具体的实现等后面补充的时候再具体分析,先说说各个函数的作用

__init__()函数:初始化函数主要的界面都在这里创建,具体的看代码

setUserList(self,users)函数:设置左边的用户列表,逻辑类获取到所有的用户信息,然后调用这个函数来显示

setUserStatus(self,index,status)函数:某个用户在线离线时逻辑类会调用这个函数,改变用户列表的项背景色(status=0灰 1绿),注意index不是用户的id,二是用户列表的索引(用户列表的顺序会和逻辑类里保存用户数组的顺序一致)

showLog(self, text, type = 0)函数:显示日志消息,也是逻辑类来调用,将消息显示在右边窗口上,text要显示的文本,type设置颜色的(0=默认黑色,1=绿色,2=橙色,3=红色)

 

Server逻辑类,关键了

会开两个线程,一个是用来刷新用户状态的,每个用户上线时都会给其设置一个time=10,每秒会减1,当小于0时就将其设置为离线了,当然肯定不能每个用户只能登录10秒啊,每个客户端在登录成功后每隔5秒会发送一个hello信息,这时候就会将time重新设置为10秒,这样就可以一直在线了,这样当某个客户端意外退出时,服务器就会自动将其设置为离线了

另一个线程是接受客户端数据的线程,接受好后会根据发送的数据的type字段进行判断处理,具体的后面说

refreshUserStatus() 函数:这是第一个线程所调用的函数,这个刷新用户状态的线程具体怎么处理的都在这个函数里,待会详细说

 

receiveThread()函数:这是第二个线程,sock,addr = self.receiveSock.accept(),在accept()客户端的连接后会再开一个线程来处理,为什么还要再开呢,主要是这样就会立即继续循环等待accept()接受其他客户端的请求,而不必等处理完这个客户端后再处理下一个客户端,如果处理的比较快还好,要是时间久了那个客户端会等不及呀;

    # 接受消息线程
    def receiveThread(self):
        try:
            self.receiveSock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
            self.receiveSock.bind((self.HostIP, self.HostPort))
            self.receiveSock.listen(32) #允许最大连接数为16
            self.buffersize = 1024
            serverUI.showLog("Server准备就绪,IP=" +self.HostIP + " Port=" +            str(self.HostPort) +" listen=32 maxsize=1024", 2)
        except:
            serverUI.showLog('创建接收socket失败!', 3)
            return False
        
        while True:
            try:
                sock,addr = self.receiveSock.accept()
                t = _thread.start_new_thread(self.receive,(sock,addr))
            except:
                serverUI.showLog('接收数据失败!', 3)
                sock.close()

 

reveive()函数:注意是指的我自己定义的这个函数,不是socket的那个recv()函数,不要混了

从Python3开始要求sock收发消息的时候要发送字节流不能直接发字符串,就因为这个Debug了好久就是不知道哪里的事

做一个循环来接受用户发的数据,sock.recv(1024).decode(encoding = "utf8") ,接受数据(字节流),用decode解码成字符串,因为发送的时候就是json字符串(json不知道的百度一下,很简单)形式,然后json.load()还原成python数组形式;

根据type字段判断这是客户端发来的什么消息,type=1,登录相关的信息,交给loginHandle()函数处理;type=3,客户端请求数据,这里只写了一个联系人列表,本来还想加个请求历史消息记录的;type=11or13,客户端给好友或者群发送消息,交给handleMessage()函数去处理

    # 接受sock处理
    def receive(self,sock,addr):
        try:
            while True:
                data = sock.recv(1024).decode(encoding = "utf8")
                if not data:
                    break
                msg = json.loads(data)

                if msg['type'] == 1: # 登录
                    self.loginHandle(msg,sock)
                    
                elif msg['type'] == 3: #请求信息
                    if msg['data'] == "contacts": # 请求联系人列表
                        contacts = self.database.getContacts()
                        if self.sendData(4,msg['from'],contacts,{"code":"contacts"}):
                            serverUI.showLog('发送联系人列表失败!', 3)

                elif msg['type'] == 11 or msg['type'] == 13:
                    self.handleMessage(msg) #消息处理
        except:
            serverUI.showLog('客户端关闭了连接!', 3)
            pass
        finally:
            sock.close()
            return

 

loginHandle(msg,sock)函数:这里牵扯到一个是login,一个是hello,客户端先发送login登录,成功后每隔5秒会发送一个hello消息,前面已经说了服务会给每个客户端设置一个超时值,收到客户端发来的hello后就会给其time重新设置为10,如果隔了10秒还未收到客户端的hello消息就讲其强制下线

具体分析下这个函数,先找用户是不是存在,isLogin代表是否登录,time大于0就是登录

如果发送的事hello并且服务器记录的确实是已经在线了,调用online()函数,这个函数就是将其time重新设置为10,并将数据库中对应的用户status字段设置为1;接下来是查找此用户的name(接受的时候只接收了该用户的id,就是有QQ号了,现在要找他的昵称),找到name后调用UI类的showLog()函数,在服务器的日志窗口中显示,接下来是将用户的time重新设置为10,然后接收他发来的ip和port,以便服务器给该客户端发送消息时使用(因为服务器的ip和port是固定的,客户端有很多个,服务器并不知道他的ip和port信息);之后就将联系人列表和未读的消息发送给该客户端(自行脑补QQ),time.sleep(1)是程序暂停1秒,要让客户端消化消化啊,不要发的这么快嘛(不要删掉,因为客户端这个时候正在刷新联系人列表,联系人还没出来怎么接受消息啊)

    # 处理登录
    def loginHandle(self,msg,sock):  
        id = msg ['from']
        index = self.database.findIndex(id)
        if index < 0 :
            return False

        isLogin = self.database.users[index]['time'] > 0
        
        
        if msg['data'] == "hello" and isLogin:
            self.database.online(id) #修改数据状态
            return

        name = self.database.findName(id)
        if isLogin:
            serverUI.showLog('用户('+name+')重新上线!', 1)
        else:
            serverUI.showLog('用户('+name+')已经上线!', 1)
        serverUI.setUserStatus(index,10) # 更新用户状态
        self.database.users[index]['ip'] = msg['ip']
        self.database.users[index]['port'] = msg['port']
        self.database.online(id) #修改数据状态

        # 发送联系人列表
        contacts = self.database.getContacts()
        if not self.sendData(4,msg['from'],contacts,{"code":"contacts"}):
            serverUI.showLog('发送联系人列表失败!', 3)

        time.sleep(1)
        if msg['data'] == "login":
            msgs = self.database.getUnreadMessage(id)
            self.sendData(12, id, msgs)

 

sendMessage()函数:说处理消息之前先说说这个函数,服务器给客户端发数据只需要调用这个函数就行了

说说服务器给客户端发的数据的格式,还是这个样子 {"type":12, "to":23, "data":"要发的东西"} ,只不过data有点不同,客户端给数据库发的data就是消息文本本身,但是服务器给客户端发还要加上time,然后可能还一次发多条,所以data就用了一个数组;

变成了 {"type":1, "to":23, "data":data是个数组}

data数组格式 ["from":谁的,"fromName":"具体谁发的,名字", time:"发送时间", "msg":"这才是消息内容"]

注意from和fromName不一定是对应的关系,给好友的时候是对应的;给群发的时候,from是指的哪个好友或者群(好友 消息就是QQ号,群消息就是群号);而fromName是发送人的名字,不管是他发的是好友消息还是群消息(如果是群里发的那也是在群里发消息的这个人的名字而不是群名字,可以打开QQ群仔细想想)

讲讲这个函数,findSock()是查找该人的ip和port,因为客户端会建立一个接收的套接字,所以这里用connect()连接就行了,json.dumps()先将字典msg转为json字符串,bytes()函数将字符串转为字节流,dict(msg,**extra)是将这两个字典拼接在一起

    def sendData(self,type,to,data,extra={}):
        if not data:
            return False
          
        ip,port = self.database.findSock(to)
        try:
            sendSock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
            sendSock.connect((ip, port))
            #格式化数据并发送
            msg = {"type":type, "to":to, "data":data}
            json_string = json.dumps(dict(msg,**extra))
            sendSock.send(bytes(json_string, encoding = "utf8"))
            sendSock.close()
            return True
        except:
            sendSock.close()
            return False

 

handleMessage()函数:这个是处理客户端发送的好友或者群消息,这两个处理稍微有点不同,所以用分开判断

theTime记录下当前时间,fromName找到该发送人的名字,这里就没有判断是否已经存在了,因为已经登录了嘛

如果type=11:找到接收人的名字()因为要在服务器的日志窗口中显示嘛),然后组装好发给接收人的数据,判断该人是否在线,如果在线给他发过去,sendData()函数发送成功会返回True,然后将该消息加入数据库,isSendFlag是代表该消息是否已经成功发给了接收人

如果type=13:跟type=11一样先在日志窗口中显示(不过这里查找的是群名了),然后要找到该群的所有成员并将接收人除去(这个不难理解吧),作个循环给群成员除了自己都发过去

    # 处理消息
    def handleMessage(self,recvMsg):
        msgType = recvMsg['type']
        theTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        fromName = self.database.findName(recvMsg['from'])

        if msgType == 11: # 单发消息
            toName = self.database.findName(recvMsg['to'])
            headInfo = fromName + ' => ' + toName + '  at '+theTime+' : ';
            serverUI.showLog(headInfo + recvMsg['data'])
            # 如果在线直接发送,否则添加未读消息
            data = [{"from":recvMsg['from'], "fromName":fromName, "time":theTime, "msg":recvMsg['data']}]
            isSendFlag = 0
            if self.database.isOnline(recvMsg['to']):
                if self.sendData(12,recvMsg['to'],data):
                    isSendFlag = 1
            # 加入数据库
            self.database.addMessage(recvMsg['from'],recvMsg['to'],theTime,recvMsg['data'],isSendFlag)
        
        elif msgType == 13: # 群发消息
            toName = self.database.findGroupName(recvMsg['to'])
            headInfo = fromName + ' =>> ' + toName + '  at '+theTime+' : ';
            serverUI.showLog(headInfo+recvMsg['data'])
            # 循环处理
            users = self.database.getGroupMember(recvMsg['to'])
            users.remove(recvMsg['from'])
            for u in users:
                data = [{"from":recvMsg['to'], "fromName":fromName, "time":theTime, "msg":recvMsg['data']}] # 此处to是目的地
                isSendFlag = 0
                if self.database.isOnline(u):
                    if self.sendData(14,u,data):
                        isSendFlag = 1
                # 群发消息不加入数据库
                #self.database.addMessage(recvMsg['from'],u,theTime,recvMsg['data'],isSendFlag)

 

 

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值