基于数据流的局域网点歌台

246 阅读9分钟

基于数据流的局域网点歌台

摘要:本文将介绍如何使用Python的socket库和多线程技术实现一个内网点歌台程序的后端。此外,还将介绍如何使用爬虫获取歌曲信息以及如何进行客户端和服务端的通信。

一、前言

关于我为什么要写这个。

在课间,老师为了活跃氛围,会叫班长让同学们进行点歌,但是很多人因为害羞或者有别的事情要忙,都不太愿意主动去说。

那么我今天分享的,就是一种可以让用户在局域网内进行歌曲点播的程序。通过该程序,同学可以方便地在局域网内向服务器提交想要听的歌曲,从而避免尴尬,提高了点歌的效率。

本文将大致介绍后端如何使用Python实现该功能,至于前端就不分享了,大家可以自行尝试。


本项目的思路还算明朗,总体就分为两个部分,服务器端和客户端。

在项目背景下,服务器端基本上就布置在教师电脑上即可,客户端分发给各位同学。

服务器端主要任务就是让各个客户端接入进来,等待各个客户端发送想听的歌曲,再通过selenium进行自动化操作(关于selenium本文暂不会介绍,感兴趣的朋友可以自行搜索。)服务器需要解决的问题有两个:①多用户接入②线程并发问题

客户端任务就简单多了,主要就是负责发送各位同学的听歌消息。为了缓解服务器的压力,本项目将一部分的爬虫内容放在客户端上,这样能有效降低服务器的带宽压力。

二、服务器端实现

服务器在整个项目中,是偏复杂的。主要分为两大要点:端口监听 和 多线程

①端口监听:是服务器实现数据传输的一个基础操作。数据监听,形象一点就是一个正在认真写作业的人,只有他愿意去听别人说话,他才会有反应,不然他还是在做自己的事情。

②多线程:通过创建多个线程,让本程序的服务器能够同时与多个客户端进行通信,而不是一个接一个地处理。当客户端请求到达时,服务器可以立即创建一个新的线程来处理该请求,而不是等待当前线程完成。

(零)导入包

目前服务器要实现功能,只需要四个包就够了。

socketsocket包是网络编程的基础,它提供了一种标准的方法来编写跨平台的网络应用程序。在Python中,socket库是内建的,可以直接使用,无需额外安装。

threading:多线程允许服务器在处理一个客户端请求的同时,继续监听新的连接请求。这避免了资源(如网络套接字)的闲置,提高了整体的资源利用率。

webbrowser:当本程序的服务器接收到客户端请求播放特定歌曲id时,便使用webbrowser包来打开歌曲的链接,这样用户就可以在浏览器中听到或看到这首歌曲。

queue:在程序中,queue包提供了一个线程安全的队列,同时可以有以下用处:①共享数据结构:多个线程可以安全地访问这个队列,将其用作共享的数据结构。 ②顺序处理:由于queue默认实现的是先进先出(FIFO)的数据结构,可以通过这个队列来保证按照客户端请求的顺序来播放歌曲。 ③ 解耦线程:通过使用队列,可以将多个线程解耦,使它们可以独立地工作,同时保持数据的一致性和顺序。

(一)端口监听

其实纵观所有的服务器端,他的主要任务就是接收客户端的连接请求,并与客户端进行通信。仅此而已。

那么从这一点出发,我们便可以简单的写出一个大概。

首先,我们需要使用socket库创建一个socket对象,并将其绑定在8888端口上。

# 设置服务器地址和端口
HOST = '0.0.0.0'
PORT = 8888

# 创建socket对象
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定地址和端口
socket_server.bind((HOST, PORT))

端口号随意,只要别用到常用的就行了。

我本人是比较喜欢8888,因为http默认的是8080。

然后,使用listen方法监听端口,等待客户端的连接。

# 设置最大连接数
socket_server.listen(5)  # 这里的5是backlog的值,建议不要设置过大

这里的 “ 5” 意味着服务器最多可以挂起5个未处理的连接请求。如果队列满了,新的连接请求可能会被拒绝。

通常情况下,这个值需要根据服务处理能力来设定。如果设置得太小,可能会拒绝一些合法的连接请求;如果设置得太大,可能会浪费资源。

等到有客户端接入进服务器,服务端直接循环接收信息即可。

其中,通过字符拼接,将网易云音乐的歌曲api与客户端传来的歌曲的ID进行拼接,至于ID从何而来,就要在后面篇幅说了。😘

    while True:
        try:
            # 接收消息
            data = client_socket.recv(1024).decode("UTF-8")
            if data == "exit":
                break
            else:
                # 处理消息,例如打开歌曲链接
                gequ = "https://music.163.com/#/song?id=" + song_id
                webbrowser.open(gequ)
                # 回复消息
                client_socket.send("成功".encode("UTF-8"))

                # 等待歌曲播放完毕(这里可以添加实际的等待逻辑)
                # ...

                # 从队列中取出下一首歌曲的ID
                song_id = song_queue.get()
        except Exception as e:
            print(f"处理客户端 {client_socket.getpeername()} 时发生错误:{e}")
            break

(二)线程异步

为了能够同时处理多个客户端的请求,我在这使用多线程技术。每当有一个客户端连接进来,我们就创建一个新的线程来处理与该客户端的通信。

但是会有问题,因为当多个客户端同时连接进来,或者同时进行点歌操作会导致堵塞,那么就需要通过线程锁来控制对共享资源的访问,具体可以查看进程同步和互斥

# 创建一个队列来暂存歌曲ID
song_queue = queue.Queue()

# 创建一个锁,用于控制对共享资源的访问
thread_lock = threading.Lock()

def client_handler(client_socket, song_id):
    # 处理客户端发送的消息
    with thread_lock:
        song_queue.put(song_id)  # 将歌曲ID放入队列

    #中间就是服务器处理歌曲ID的代码块,大家可以根据需要自行修改
    
    # 关闭连接
    client_socket.close()

def start_server():
    print(f"服务端已开始监听,正在等待客户端连接...")

    # 创建一个线程来监听新的客户端连接
    new_thread = threading.Thread(target=wait_for_connection)
    new_thread.start()

def wait_for_connection():
    while True:
        # 监听客户端连接
        client_socket, addr = socket_server.accept()
        print(f"接收到了客户端的连接,客户端的信息:{addr}")

        # 创建一个新的线程来处理客户端连接
        song_id = client_socket.recv(1024).decode("UTF-8")  # 歌曲ID直接通过连接发送,所以直接使用这个方式
        client_thread = threading.Thread(target=client_handler, args=(client_socket, song_id))
        client_thread.start()

if __name__ == '__main__':
    start_server()

以上是本程序,线程操作的大致模型,有具体需求可以更改,但是最重要的就是通过进程锁来限制用户的接入。

服务器端的代码就是这样了。

三、客户端实现

客户端的难点,就是通过爬虫,将客户需要的歌曲的ID从网易云爬取下来。

当然本文不会介绍如何进行爬虫,担心被封哈哈哈😜,如果有需求可以私聊我。但是本文还是会将api放在代码里。

(一)连接之前的操作

客户端的主要任务是连接到服务器,并向服务器发送歌曲ID。

首先,我们需要使用socket库创建一个socket对象,并连接到服务器。使用requests库发送HTTP请求获取歌曲信息,并将歌曲信息发送给服务器。

为了避免总是要输入ip地址,所以写了一个保存输入地址的逻辑。

import socket
import json
import requests
# 创建socket对象
socket_client = socket.socket()
# 连接到服务器
ip_address = ""
try:
    f = open("./save_address","r")
    if f.read() == "":
        address = input("请输入服务器ip地址")
        f = open("./save_address", "w")
        f.write(address)
        ip_address = address
    else:
        ip_address = f.read()
except FileNotFoundError:
    address = input("请输入服务器ip地址")
    f = open("./save_address","w")
    f.write(address)
    ip_address = address
except socket.gaierror:
    address = input("服务器的ip地址错误,请重新输入服务器ip地址")
    f = open("./save_address", "w")
    f.write(address)
    ip_address = address
socket_client.connect((ip_address, 8888))

(二)通信

在本例中,我们使用requests库向一个api发送HTTP请求,获取与输入歌名相关的歌曲信息。该api不做详细解释。

请求返回值使用“gbk2312”编码,以便传输中文。

song_info = {}
    qingqiu = requests.get(
        "https://ting.yeyulingfeng.com/yeyudata.php?types=search&count=20&source=netease&pages=1&name=" + input(
            "请输入要听的歌名"))
    qingqiu.encoding = 'gbk2312'
    qingqiu_chuli = json.loads(qingqiu.text)
    for i in range(len(qingqiu_chuli)):
        a = {"id": qingqiu_chuli[i]['id'], "name": qingqiu_chuli[i]['name'], "artist": qingqiu_chuli[i]['artist'],
             "album": qingqiu_chuli[i]['album']}
        song_info[i] = a
    for i in song_info:
        print(f"第{i+1}个",song_info[i])

客户端将获取到的歌曲信息展示给用户,包括但不限于歌曲名、歌手名、专辑名称等,并提示用户输入想要听的歌曲是第几个(方便用户交互)。

客户端将歌曲索引发送给服务器。服务器接收到请求后,打开对应的歌曲链接,并将“成功”发送给客户端。

message = input("请输入你想听第几个:")
if message == "exit":
    break
song_id = str(song_info[int(message) - 1]['id'])
socket_client.send(song_id.encode("UTF-8"))
recv_data = socket_client.recv(1024).decode("UTF-8")
print(f"服务器说:{recv_data}")

注意:发送的时候编码模式推荐使用“UTF-8”,编码范围较广。

四、总结

这是我第一次写博客,大家轻喷。如果有意见或者有更好的方式,还请帮忙斧正╰(°▽°)╯