python socket-tcp多人网络聊天室服务端(35行代码实现)
关键词:多进程、TCP、共享内存
目前已运行在ali云服务器上,不出意外在服务器到期前都可以正常使用;大概8月到期;
ip 47.108.60.37 端口 2333
使用网络调试助手即可连接
实现难点
进程间通信,如何在单个进程中给其他socket发信息;
1.进程间通信
进程间通信有管道,消息队列,queue,数据库等方式,但是这几种方式不便于实现;
1.管道的局限性在于两个进程间,和控制数据流向,对多进程的数据传输就不是很方便;
2.关于消息队列其实并不是很不清楚,我查阅linux c上的关于消息队列的编程,linux c是用链表实现的,意思是对每个节点的数据都开可以直接进行操作,也算是一种方便。但是python的多进程库queue不太一样,只有put和get,意思是只能逐个放入和逐个取出,找了很多资料也没发现queue有更方便的用法,至少在多进程通信上没有更方便的用法;
3.数据库,目前觉得比较方便一点的是redis开源数据库,但是图快速开发的话,只实现特定功能确实有点大柴小用,虽然说python通过redis库直接进行操作也确实方便,但是需要运行一个redis服务端;
4.共享内存在目前看来是最优解,很方便的可以实现进程间内存共享,在数据量小的情况下至少是最合适的。
2.进程内给所以客户端发信息
最开始使用的共享内存创建的列表来添加不同客户端的socket,但是在删除socket的时候却出现了问题,删除列表内元素使用的remove方法,可能是socket创建后自己本身结果原因,传入的值被remove方法误读了,提示没有该值;如下,此为使用列表作为socket连接记录;
import socket as sk import multiprocessing as mp import time as t import os def c_s_c(sock_in, addr,sock_list): #进程出来函数 for sock_c in sock_list.items(): #遍历字典元组 sock_c[1].send(("user"+str(addr)+"---in---\n").encode()) #遍历通知所有在聊天室的用户有新用户进入 while True: readdata = sock_in.recv(1024) #recv函数有阻塞左作用,若连接断开或者sock被关闭,返回空 if readdata: #print(readdata) #服务端看信息调试用 for sock_c in sock_list.items(): sock_c[1].send(str(addr).encode()+readdata) #转发给所用用户 else: for sock_c in sock_list.items(): sock_c[1].send(("user"+str(addr)+"---out---").encode()) #encode编码为字节序 del sock_list[addr] #将退出的用户从字典中删除 sock_in.close() os._exit(0) #终止进程 break #摆设用 def main(): s = sk.socket(sk.AF_INET ,sk.SOCK_STREAM) s.bind(('',2333)) s.listen() sock_list=mp.Manager().dict() #共享内存字典-------------------------------- while True: sock,addr = s.accept() sock_list[addr]=sock #将地址作为索引-------------------------------- t1 = mp.Process(target=c_s_c, args=(sock, addr,sock_list)) #多进程------ t1.start() if __name__ == '__main__': main()
连接单个客户端后打印如下;
退出客户端并不能移除掉连接信息;
如下是使用字典,以ip addr 作为索引,问题才得以解决
import socket as sk import multiprocessing as mp import time as t import os def c_s_c(sock_in, addr,sock_list): #进程出来函数 for sock_c in sock_list.items(): #遍历字典元组 sock_c[1].send(("user"+str(addr)+"---in---\n").encode()) #遍历通知所有在聊天室的用户有新用户进入 while True: readdata = sock_in.recv(1024) #recv函数有阻塞左作用,若连接断开或者sock被关闭,返回空 if readdata: #print(readdata) #服务端看信息调试用 for sock_c in sock_list.items(): sock_c[1].send(str(addr).encode()+readdata) #转发给所用用户 else: for sock_c in sock_list.items(): sock_c[1].send(("user"+str(addr)+"---out---").encode()) #encode编码为字节序 del sock_list[addr] #将退出的用户从字典中删除 sock_in.close() os._exit(0) #终止进程 break #摆设用 def main(): s = sk.socket(sk.AF_INET ,sk.SOCK_STREAM) s.bind(('',2333)) s.listen() sock_list=mp.Manager().dict() #共享内存字典-------------------------------- while True: sock,addr = s.accept() sock_list[addr]=sock #将地址作为索引-------------------------------- t1 = mp.Process(target=c_s_c, args=(sock, addr,sock_list)) #多进程------ t1.start() if __name__ == '__main__': main()
优化版本1
共享内存在应对大量数据处理时,因每个进程都对同一个变量进行操作,在for循环处易发生错误,但作为一个聊天室demo作演示已绰绰有余,大数据处理还得上数据库,;
import socket as sk import multiprocessing as mp import time as t import os def c_s_c(sock_in, addr,sock_list): #进程出来函数 for sock_c in sock_list.items():#遍历字典元组 sock_c[1].send(("user"+str(addr)+"---in---\n").encode())#遍历通知所有在聊天室的用户有新用户进入 while True: try: readdata = sock_in.recv(1024)#recv函数有阻塞左作用,若连接断开或者sock被关闭,返回空 if readdata: print(readdata) #服务端看信息调试用 for sock_c in sock_list.items(): try: sock_c[1].send(str(addr).encode()+readdata)#转发给所用用户 except: del sock_list[sock_c[0]] else: for sock_c in sock_list.items(): if sock_c[0]!=addr: #如果链接中断了,就不再发给该sock,继续操作该sock会引发异常; sock_c[1].send(("user"+str(addr)+"---out---\n").encode())#encode编码为字节序 del sock_list[addr] #将退出的用户从字典中删除 print("del success") os._exit(0) #终止进程 break#摆设用 except: for sock_c in sock_list.items(): if sock_c[0]!=addr: #如果链接中断了,就不再发给该sock,继续操作该sock会引发异常; sock_c[1].send(("user"+str(addr)+"---out---\n").encode())#encode编码为字节序 del sock_list[addr] #将退出的用户从字典中删除 print("del success") os._exit(0) #终止进程 break#摆设用 def main(): s = sk.socket(sk.AF_INET ,sk.SOCK_STREAM) s.setsockopt(sk.SOL_SOCKET, sk.SO_KEEPALIVE, 1) s.setsockopt(sk.SOL_TCP, sk.TCP_KEEPIDLE, 1) s.setsockopt(sk.SOL_TCP, sk.TCP_KEEPINTVL, 1) s.setsockopt(sk.SOL_TCP, sk.TCP_KEEPCNT, 1) s.bind(('',2333)) s.listen() sock_list=mp.Manager().dict()#共享内存字典-------------------------------- while True: sock,addr = s.accept() sock_list[addr]=sock#将地址作为索引-------------------------------- t1 = mp.Process(target=c_s_c, args=(sock, addr,sock_list)) #多进程------ t1.start() if __name__ == '__main__': main()
理论上支持任意多的用户接入~至少几十个接入没有问题;如果需要开发个客户端的话,用单终端实现收和发这个暂时不知道怎么解决,如果用一些gui库的话,在两个窗口上,一个作接收,一个作发送还是有个大概思路
小记2
目前这35行代码运行仍然有需要改善的地方,在使用中发现,如果客户端没有按照3次或者4次握手正常断开,服务端会继续维持这个链接720s,这个结果就影响到了进程的终止处理,因此这里需要加入客户端保活检测机制,后续在尝试解决;
优化版本2
目前这个版本已解决断开造成的异常问题,
import socket as sk import threading as mp #https://www.cnblogs.com/hardfood/p/14263412.html def c_s_c(sock_in, addr,sock_list:dict): #线程处理函数 print(sock_list) for sock_c in list(sock_list.keys()):#遍历字典 try: sock_list[sock_c].send(("user"+str(addr)+"---in---\n").encode())#遍历通知所有在聊天室的用户有新用户进入 except: pass while True: readdata = sock_in.recv(1024)#recv函数有阻塞作用,若连接断开或者sock被关闭,返回空 if readdata: print(readdata) #服务端看信息调试用 for sock_c in list(sock_list.keys()): try: sock_list[sock_c].send(str(addr).encode()+readdata)#转发给所用用户 except: del sock_list[sock_c] #异常则删除此对象 print("del:{} success".format(sock_c)) else: for sock_c in list(sock_list.keys()): try: sock_list[sock_c].send(("user"+str(addr)+"---out---\n").encode())#encode编码为字节序 except: pass break#退出线程 def main(): s = sk.socket(sk.AF_INET ,sk.SOCK_STREAM) s.bind(('',2333)) s.listen() sock_list={}#字典-------------------------------- while True: sock,addr = s.accept() sock_list[addr]=sock#将地址作为索引-------------------------------- t1 = mp.Thread(target=c_s_c, args=(sock, addr,sock_list)) #多进程------ t1.start() if __name__ == '__main__': main()
优化版本3
import socket as sk import threading as mp #https://www.cnblogs.com/hardfood/p/14263412.html def c_s_c(sock_in, addr,sock_list:dict): #线程处理函数 print(sock_list) for sock_c in list(sock_list.keys()):#遍历字典 try: sock_list[sock_c].send(("user"+str(addr)+"---in---\n").encode())#遍历通知所有在聊天室的用户有新用户进入 except: pass while True: readdata = sock_in.recv(1024)#recv函数有阻塞作用,若连接断开或者sock被关闭,返回空 if readdata: print(readdata) #服务端看信息调试用 for sock_c in list(sock_list.keys()): try: sock_list[sock_c].send(str(addr).encode()+readdata)#转发给所用用户 except: pass else: break#退出循环 for sock_c in list(sock_list.keys()): try: sock_list[sock_c].send(("user"+str(addr)+"---out---\n").encode())#encode编码为字节序 except: del sock_list[sock_c] #异常则删除此对象 print("del:{} success".format(sock_c)) def main(): s = sk.socket(sk.AF_INET ,sk.SOCK_STREAM) s.bind(('',2333)) s.listen() sock_list={}#字典-------------------------------- while True: sock,addr = s.accept() sock_list[addr]=sock#将地址作为索引-------------------------------- t1 = mp.Thread(target=c_s_c, args=(sock, addr,sock_list)) #多线程------ t1.start() if __name__ == '__main__': main()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!