杂记|深大计网实验 4(Socket 网络编程)


这里写目录标题

  • 前言
  • 1. URL 请求程序
  • 2. 系统时间查询
  • 3. 网络文件传输
  • 4. 网络聊天室
  • sp. 聊天室 GUI

前言 互联网编程 2.0(大雾
好吧本来没想写的,但是老师给的实验指导实在是有些【苗条】,于是还是打算记录一下,毕竟这个实验也挺有意思的。。。
用 python,大多数库都是自带的,整挺好。这次终于体验到 python 之 “禅” 了,毕竟不到 70 行就写了一个超级 primary 的 QQ 聊天室,太香了!这不禁让我的 c 孝子成分降低了 0.000013%,我急了!
实验报告重复会被锤,我就不放了。。。简单记录下代码和思路
省流版:
  • 第一题 url 下载,request 直接笋干缪啥
  • 第二题 TCP 问时间,开个 socket 搞之
  • 第三题 TCP 传文件,也是 socket + 分包传输,搞定
  • 第四题 UDP 聊天室,服务端嗯广播就好了,客户端两线程,一个侦听一个发送,图形界面用 Qt
1. URL 请求程序 请求一个网页,并存储为html文件。
计算所请求网页的大小。
这里使用 python 的 request 库请求对应的 url,并且保存到一个名为 a.html 的文件中。python 的代码如下:
import requests import sys import osurl = sys.argv[1] res = requests.get(url) filename = 'a.html'with open(filename, 'wb') as fd: for chunk in res.iter_content(chunk_size=128): fd.write(chunk)print('目标 URL: ', res.url) print('文件名: ', filename) print('文件大小: ', os.path.getsize(filename), ' 字节')

杂记|深大计网实验 4(Socket 网络编程)
文章图片

杂记|深大计网实验 4(Socket 网络编程)
文章图片

2. 系统时间查询 实现一个基于客户/服务器的网络文件传输程序。
传输层使用TCP。
交互过程
  1. 客户端向服务器端发送字符串”Time”。
  2. 服务器端收到该字符串后,返回当前系统时间。
  3. 客户端向服务器端发送字符串”Exit”。
  4. 服务器端返回”Bye”,然后结束TCP连接。
服务端代码从命令行获取要侦听的端口号,然后侦听对应端口。一旦有连接就尝试从 socket 中获取客户端发送的数据。
数据分为三种类型,如果收到了 time 那么我们响应一个当前的系统时间,如果收到了 exit 那我们直接退出,如果收到了其他的字符串那么我们返回一个 “无效命令” 提示客户端输入正确的命令。
服务端的代码如下:
from socket import * import sys import timeport = int(sys.argv[1]) serverSocket = socket(AF_INET, SOCK_STREAM) serverSocket.bind(('', port)) serverSocket.listen(114514)while True: conn, addr = serverSocket.accept() print('-----------------------------------') print('接收到新连接: ') print('客户端地址: ', addr, ':') while True: recvmsg = conn.recv(1024).decode() if recvmsg=='': continue print('服务端收到信息: ', recvmsg) if recvmsg=='time': ret = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) print('服务端发送信息: ', ret) conn.send(ret.encode()) elif recvmsg=='exit': print('服务端发送信息: ', 'bye') conn.send('bye'.encode()) break else: print('服务端发送信息: ', '无效命令') conn.send('无效命令'.encode()) conn.close()

再来看客户端的代码,也是同样的逻辑,首先从命令行接收参数,这次接收两个参数,第一个参数是主机名,我们一般填 localhost,第二个参数是连接的端口号,要和服务端侦听的端口号相一致就行了
客户端的代码如下:
from socket import * import syshost = sys.argv[1] port = int(sys.argv[2])clientSocket = socket(AF_INET, SOCK_STREAM) clientSocket.connect((host, port)) print('连接到 ', host, ':', port) print('-----------------------------------')while True: sendmsg = input('请输入发送的信息:') clientSocket.send(sendmsg.encode()) recvmsg = clientSocket.recv(1024).decode() print('来自服务端的信息: ', recvmsg) if recvmsg=='bye': break clientSocket.close()

以 1145 端口为例,程序运行的结果如下:
杂记|深大计网实验 4(Socket 网络编程)
文章图片

3. 网络文件传输 实现一个基于客户/服务器的网络文件传输程序。
传输层使用TCP。
交互过程
  1. 客户端从用户输入获得待请求的文件名。
  2. 客户端向服务器端发送文件名。
  3. 服务器端收到文件名后,传输文件。
  4. 客户端接收文件,重命名并存储在硬盘
如果文件存在,服务端返回一个 ok 字符串,然后双方开始通过 socket 进行文件的传输。如果不存在,那么服务端返回一个错误提示,然后终止。
分包传输每次传 128 kb 就好。不足的部分直接发。下面是服务端的代码:
from socket import * import sys import timeport = int(sys.argv[1]) serverSocket = socket(AF_INET, SOCK_STREAM) serverSocket.bind(('', port)) serverSocket.listen(114514)conn, addr = serverSocket.accept() print('-----------------------------------') print('接收到新连接: ') print('客户端地址: ', addr, ':')filename = conn.recv(1024).decode()try: file = open(filename, 'rb+') print('客户端请求文件: ', filename) print('服务端发送信息: ', 'ok') conn.send('ok'.encode()) except: print('服务端发送信息: ', '目标文件不存在') conn.send('目标文件不存在'.encode()) conn.close() exit()while True: r = file.read(1024*128) if len(r)==0: break print('发送 ', len(r), ' 字节的数据') conn.send(r)conn.close() print('发送完成')

下面是客户端的代码:
from socket import * import syshost = sys.argv[1] port = int(sys.argv[2])clientSocket = socket(AF_INET, SOCK_STREAM) clientSocket.connect((host, port)) print('连接到 ', host, ':', port) print('-----------------------------------')filename = input('请输入目标文件名: ') clientSocket.send(filename.encode()) recvmsg = clientSocket.recv(1024).decode() print('来自服务端的信息: ', recvmsg)if recvmsg=='ok': file = open(filename, 'wb') while True: r = clientSocket.recv(1024*128) if len(r)==0: break print('接收到 ', len(r), ' 字节的数据') file.write(r)clientSocket.close() print('接收完成')

杂记|深大计网实验 4(Socket 网络编程)
文章图片

杂记|深大计网实验 4(Socket 网络编程)
文章图片

杂记|深大计网实验 4(Socket 网络编程)
文章图片

杂记|深大计网实验 4(Socket 网络编程)
文章图片

杂记|深大计网实验 4(Socket 网络编程)
文章图片

顺利打开 pdf,证明传输无误:
杂记|深大计网实验 4(Socket 网络编程)
文章图片

4. 网络聊天室 实现一个基于客户/服务器的网络聊天程序。
要求实现多个用户的群聊。
传输层使用UDP。
通过 UDP 广播实现群聊。那么对于服务端来说,任意一个操作都要向所有的用户进行广播,比如谁进来,谁离开,谁发消息了。
服务端逻辑就是广播。于是服务端维护一个集合,称之为用户池,存储用户的 ip,并且在每收到一个连接时都判断:
  1. 如果当前 ip 不在用户池中,那么加入用户池,并且广播 “xxx 进入聊天室”
  2. 如果当前 ip 在用户池,那么表示该用户进行了发言,服务端广播该用户的消息
  3. 如果用户发送了关键字 quit,那么从用户池中删除用户,并且广播 “xxx 离开聊天室”
此外,为了维护良好的用户体验,服务端需要知道每一个用户的昵称。在客户端第一次连接服务端的时候,就发送一串字符串,格式是 “hello,xxx” 其中 xxxx 就是用户的昵称。
服务端的代码:
from socket import * import sysport = int(sys.argv[1]) serverSocket = socket(AF_INET, SOCK_DGRAM) serverSocket.bind(('', port)) print('UDP 服务端开始侦听')user = {}# map(ip, 昵称)def sendToAll(user, msg): for addr in user: serverSocket.sendto(msg.encode(), addr)while True: msg, addr = serverSocket.recvfrom(2048) msg = msg.decode() response = '' if msg == 'quit':# 退出 response = user[addr] + ' 退出了聊天室' del user[addr] elif not addr in user:# 新加入 user[addr] = msg.split('hello,')[1] response = user[addr] + ' 进入了聊天室' else:# 正常消息 response = user[addr] + ': ' + msg # 响应 sendToAll(user, response) print(response)

客户端稍微麻烦一些,要创建两个线程,一个线程 listener 负责侦听服务端的广播,因为在打字的时候,别人可能也在说话。而另一个线程 sender 则负责将用户的输入打包发送给服务端。
这里就面临一个小问题。两个线程都 while 1 在循环读取。sender 可以通过用户输入 quit 来结束循环,而 listener 怎么办呢?可以使用进程间通信的方法,但是这里使用了一个 trick,通过 try 语句捕获【试图在关闭的 socket 管道上读取数据】的异常,然后中断掉就可以了。
于是有客户端的代码:
from socket import * import sys import threading import time# 监听线程 class listener(threading.Thread): def __init__(self, conn): threading.Thread.__init__(self) self.conn = conn def run(self): while True: try:# 利用 conn.close 作为退出标志 msg, addr = self.conn.recvfrom(2048) print(msg.decode()) except: break# 发送线程 class sender(threading.Thread): def __init__(self, conn, addr): threading.Thread.__init__(self) self.conn = conn self.addr = addr def run(self): while True: msg = input() self.conn.sendto(msg.encode(), self.addr) if msg == 'quit': time.sleep(0.5)# 防止管道过早关闭 self.conn.close() breaknickname = sys.argv[3] addr = (sys.argv[1], int(sys.argv[2])) conn = socket(AF_INET, SOCK_DGRAM) conn.sendto(('hello,'+nickname).encode(), addr)l = listener(conn) s = sender(conn, addr)l.start() s.start()l.join() s.join()

开始两个客户端分别以 StDiana 和 AliceBob 的昵称进入聊天室。其中 StDiana 先进,AliceBob 后进,那么前者可以看到后者的进入记录,而后者无法看到前者的进入信息。
下图中终端顺序是服务端,StDiana,和 AliceBob :
杂记|深大计网实验 4(Socket 网络编程)
文章图片

下图左侧的终端是服务端,右侧上方是 StDiana,右侧下方是 AliceBob,如图,在 AliceBob 输入之后,服务端和 StDiana 都收到了它的信息,并且打印在各自的终端上:
杂记|深大计网实验 4(Socket 网络编程)
文章图片

然后 StDiana 也给予了回复:
终端的顺序和上图一致。
杂记|深大计网实验 4(Socket 网络编程)
文章图片

随后 AliceBob 直接表示我先溜了。然后发送了 quit 关键字以退出聊天室,于是它的退出的信息广播到了 StDiana,同时服务端也记录了下来这个信息:
杂记|深大计网实验 4(Socket 网络编程)
文章图片

StDiana 也做出了回复,只是这一次除了服务端,没有人再听得到它的消息了:
杂记|深大计网实验 4(Socket 网络编程)
文章图片

其他演示:
杂记|深大计网实验 4(Socket 网络编程)
文章图片

sp. 聊天室 GUI 本着不要内卷的原则,撸了个超级超级简陋的版本。。。你们卷吧,我先 run 了
唔… 用的是 PySide2 不是 qt,因为 qt 多线程访问 UI 有点麻烦。。。
然后这里和上面不同,主线程也算一个进程,它读取 GUI 输入,子线程 listener 监听线程负责监听并且回显。严格来说只有两个执行流,而非上面代码的三个。
使用 PyQt 下面开源的 PySide2 编写,以规避多线程访问 UI 的麻烦。思路和上文的命令行客户端一致。下面是 GUI 客户端的代码:
from socket import * import sys import threading import time from PySide2.QtWidgets import *class QTLineEditExample(QMainWindow): def __init__(self, conn, addr): super().__init__() self.initUI() self.conn = conn self.addr = addrdef initUI(self): self.tb = QTextBrowser(self)# 消息 self.tb.resize(400, 350) self.tb.move(50, 50)self.input = QLineEdit(self)# 输入 self.input.resize(275, 40) self.input.move(50, 425)self.sendBtn = QPushButton('发送',self) # 发送 self.sendBtn.resize(100, 40) self.sendBtn.move(350, 425) self.sendBtn.clicked.connect(self.send)self.setGeometry(300, 300, 500, 500)# 窗体 self.setWindowTitle('聊天室 -- 李若龙 2018171028') self.show()def send(self): msg = self.input.text() self.conn.sendto(msg.encode(), self.addr) if msg == 'quit': time.sleep(0.5)# 防止管道过早关闭 self.conn.close() self.close() self.input.setText('')# 监听线程 class listener(threading.Thread): def __init__(self, conn, qt): threading.Thread.__init__(self) self.conn = conn self.qt = qt def run(self): while True: try:# 利用 conn.close 作为退出标志 msg, addr = self.conn.recvfrom(2048) qt.tb.append(msg.decode()) except: breaknickname = sys.argv[3] addr = (sys.argv[1], int(sys.argv[2])) conn = socket(AF_INET, SOCK_DGRAM) conn.sendto(('hello,'+nickname).encode(), addr)app = QApplication(sys.argv) qt = QTLineEditExample(conn, addr)l = listener(conn, qt) l.start()sys.exit(app.exec_())

首先是三个客户端依次进入聊天室
【杂记|深大计网实验 4(Socket 网络编程)】杂记|深大计网实验 4(Socket 网络编程)
文章图片

然后键入信息:
杂记|深大计网实验 4(Socket 网络编程)
文章图片

按下发送按钮,大家都收到了消息:
杂记|深大计网实验 4(Socket 网络编程)
文章图片

同样,别的用户依次发言,大家都能收到:
杂记|深大计网实验 4(Socket 网络编程)
文章图片

    推荐阅读