网络编程--多进程聊天室
功能介绍:
- 进入聊天室需要先输入姓名,姓名不能重复
- 有人进入聊天室,会向其他人发送通知(不给自己发),格式:某某进入聊天室
- 一个人发消息,其他人都会收到消息(自己不收),格式:某某说:啥啥啥
- 有人退出聊天室,会向其他亲人发送通知(不给自己发),格式:某某退出了聊天室
- 管理员说话:服务端发出消息,所有的客户端都接收消息,格式:管理员说:啥啥啥
拿到一个项目,我们不是着急着想代码该怎么写, 而是这个项目该怎么写,基本的思路是什么。
对于这个项目,必须的是有客户端和服务端
客户端主要用来发送消息和接收别人发的消息
服务端主要用来处理客户端的消息和发送管理员消息
那么具体的该怎么分析呢?
对功能模块的分析:消息的转发
对于消息的发送与接收需要的技术是:套接字、udp(主要是udp不需要连接,比较方便)
登录的用户如何存储:使用字典或列表(这使用字典,字典更方便)
消息收发的随意性:使用多进程fork(多进程还有Process,后边会讲到,在这先不说了)
这个项目的整体思路是什么或者说我们编写代码的流程是什么?
- 搭建网络连接(实现客户端和服务端正常连接)
- 创建多进程(为编写功能做铺垫,实现消息的收发不受父进程的影响)
- 每个进程的功能编写(父子进程要实现什么功能)
- 每个功能模块的代码实现(将父子进程的功能代码写完,完成测试)
开始进入正题了,首先我们实现网络的正常连接,保证后边的功能可以正常的进行
服务端代码
# chat_server.py
from socket import *
def main():
# 设置服务器地址
HOST = '0.0.0.0'
PORT = 8000
ADDR = (HOST, PORT)
# 创建udp套接字
sockfd = socket(AF_INET, SOCK_DGRAM)
# 设置端口释放
sockfd.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 绑定地址
sockfd.bind(ADDR)
while True:
# 接收客户端的消息
data, addr = sockfd.recvfrom(1024)
print(data.decode())
# 反馈给客户端的消息
data = sockfd.sendto('收到消息'.encode(), addr)
if __name__ == "__main__":
main()
对应的客户端代码
# chat_client.py
from socket import *
import sys
def main():
if len(sys.argv) < 3:
s = '''
输入有误
请按要求输入, 如:
python3 chat_client.py 127.0.0.1 8000
'''
print(s)
return
# 获取终端输入的地址
HOST = sys.argv[1]
PORT = int(sys.argv[2])
ADDR = (HOST, PORT)
# 创建udp套接字
sockfd = socket(AF_INET, SOCK_DGRAM)
# 消息的收发
while True:
data = input('>>')
# 发送消息
sockfd.sendto(data.encode(), ADDR)
# 接收服务端反馈的消息
data, addr = sockfd.recvfrom(1024)
print(data.decode())
if __name__ == "__main__":
main()
此时实现了客户端与服务端的连接,运行(先运行服务端)结果是:
连接没问题我们就开始创建多进程了
服务端:
父进程:处理客户端请求和转发消息
子进程:创建一个单独的进程用来发送管理员消息
客户端:
父进程:接收消息
子进程:发送消息
改写服务端代码
# chat_server.py
from socket import *
import os # 创建fork多进程先导入os模块
# 做管理员发送消息
def do_child():
pass
# 处理客户端的消息
def do_parent():
pass
def main():
# 设置服务器地址
HOST = '0.0.0.0'
PORT = 8000
ADDR = (HOST, PORT)
# 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM)
# 设置端口释放
sockfd.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 绑定地址
sockfd.bind(ADDR)
# 创建进程
p = os.fork()
if p < 0:
print('创建进程失败')
elif p == 0:
do_child()
else:
do_parent()
if __name__ == "__main__":
main()
改写客户端代码
# chat_client.py
from socket import *
import sys
import os
# 发送消息
def send_msg():
pass
# 接收消息
def recv_msg():
pass
def main():
if len(sys.argv) < 3:
s = '''
输入有误
请按要求输入, 如:
python3 chat_client.py 127.0.0.1 8000
'''
print(s)
return
# 设置连接服务器的地址
HOST = sys.argv[1]
PORT = int(sys.argv[2])
ADDR = (HOST, PORT)
# 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM)
# 创建多进程
p = os.fork()
if p < 0:
print('创建多进程失败')
elif p == 0:
send_msg()
else:
recv_msg()
if __name__ == "__main__":
main()
登录功能:
客户端:
- 输入姓名
- 将姓名发送给服务端
- 接收服务端的回复
- 根据回复内容判断是否登录成功
服务端:
- 接收客户端的请求
- 判断请求类型
- 判断姓名是否已经有人登录
- 有,则返回不能登录
- 没有,则返回可以登录,并将姓名保存
- 将登录消息转发给其他人
先写客户端:
while True:
name = input('输入登录名:')
msg = "L " + name
# 将用户名发送给服务端
sockfd.sendto(msg.encode(), ADDR)
# 接收服务端返回的消息
data, addr = sockfd.recvfrom(1024)
# 如果返回的是OK表示可以登录,否则重新登录
if data.decode() == 'OK':
print('您已进入聊天室')
break
else:
print('登录失败,请重新登录', data.decode())
小解:发送的消息为什么加了个"L "?
回答:因为我们有登录、消息发送、退出,那么怎么让服务端知道我们是在干嘛呢?我们做一个请求标志,如果服务端收到的是L…则表示登录,后边有类似的不会这么详解了,顺便说一下:C表示聊天,Q表示退出,这两个后边用到。
客户端写完了,写一下服务端的代码
do_parent(sockfd) # ① 见小解
....
# 处理客户端的消息
def do_parent(sockfd):
# 创建一个空字典,用来保存用户,保存格式:{name: addr}
user = {}
while True:
# 接收客户端的请求
msg, addr = sockfd.recvfrom(1024)
# 对请求做一个切割-->[请求类型,请求内容]
msgList = msg.decode().split(' ')
# 判断请求类型
if msgList[0] == "L":
do_login(sockfd, user, msgList[1], addr) # ② 见小解
# 登录处理
def do_login(sockfd, user, name, addr):
if name in user:
sockfd.sendto('用户名已存在'.encode(), addr)
return
else:
sockfd.sendto('OK'.encode(), addr)
msg = '欢迎{}进入聊天室'.format(name)
# 将消息转发给其他人(不包括自己)
for i in user:
if i != name:
sockfd.sendto(msg.encode(), user[i])
# 将用户名添加到字典中
user[name] = addr
print(user)
小解:① 这个是调用处理请求的方法,主要是传什么参数,传参根据需求来分析,do_parent方法只是用来处理客户端请求的,首先得接收这个请求,所以需要套接字sockfd,其次呢?这个方法就不需要了,就没有其次了,剩下的交给各自功能的模块
② 这个是调用登录的方法,还是参数的讲解,根据需求分析一下,首先要发送给客户端是否可以登录需要套接字sockfd和接收的姓名,判断姓名是否存在,在哪存在啊,在user中是否存在,需要user这个字典,如果不存在,我们还需要把姓名添加到user中,由于字典的格式是:{name: addr},所以还需要addr。
看一下运行结果(此时还没有转发功能)
补充一下客户端的接收消息,避免登录成功后直接就退出了
# 接收消息
def recv_msg(sockfd):
while True:
data, addr = sockfd.recvfrom(1024)
print(data.decode())
此时的结果是:
登录和消息的转发已经完成了,下边写一下聊天(注意消息的随意性)
客户端:
- 消息发送/消息接收
服务端:
- 接收客户端的请求
- 判断请求类型
- 将消息转发给其他用户
客户端代码:
# 子进程调用发送消息的方法
send_msg(sockfd, name, ADDR)
。。。