网络编程--多进程聊天室

功能介绍:

  1. 进入聊天室需要先输入姓名,姓名不能重复
  2. 有人进入聊天室,会向其他人发送通知(不给自己发),格式:某某进入聊天室
  3. 一个人发消息,其他人都会收到消息(自己不收),格式:某某说:啥啥啥
  4. 有人退出聊天室,会向其他亲人发送通知(不给自己发),格式:某某退出了聊天室
  5. 管理员说话:服务端发出消息,所有的客户端都接收消息,格式:管理员说:啥啥啥

拿到一个项目,我们不是着急着想代码该怎么写, 而是这个项目该怎么写,基本的思路是什么。

对于这个项目,必须的是有客户端和服务端
客户端主要用来发送消息和接收别人发的消息
服务端主要用来处理客户端的消息和发送管理员消息
那么具体的该怎么分析呢?

对功能模块的分析:消息的转发
对于消息的发送与接收需要的技术是:套接字、udp(主要是udp不需要连接,比较方便)
登录的用户如何存储:使用字典或列表(这使用字典,字典更方便)
消息收发的随意性:使用多进程fork(多进程还有Process,后边会讲到,在这先不说了)

这个项目的整体思路是什么或者说我们编写代码的流程是什么?

  1. 搭建网络连接(实现客户端和服务端正常连接)
  2. 创建多进程(为编写功能做铺垫,实现消息的收发不受父进程的影响)
  3. 每个进程的功能编写(父子进程要实现什么功能)
  4. 每个功能模块的代码实现(将父子进程的功能代码写完,完成测试)

开始进入正题了,首先我们实现网络的正常连接,保证后边的功能可以正常的进行

服务端代码

# 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)

。。。