这里写目录标题
- 前言
- 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
计算所请求网页的大小。
这里使用 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), ' 字节')
文章图片
文章图片
2. 系统时间查询 实现一个基于客户/服务器的网络文件传输程序。
传输层使用TCP。
交互过程
- 客户端向服务器端发送字符串”Time”。
- 服务器端收到该字符串后,返回当前系统时间。
- 客户端向服务器端发送字符串”Exit”。
- 服务器端返回”Bye”,然后结束TCP连接。
数据分为三种类型,如果收到了 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 端口为例,程序运行的结果如下:
文章图片
3. 网络文件传输 实现一个基于客户/服务器的网络文件传输程序。
传输层使用TCP。
交互过程
- 客户端从用户输入获得待请求的文件名。
- 客户端向服务器端发送文件名。
- 服务器端收到文件名后,传输文件。
- 客户端接收文件,重命名并存储在硬盘
分包传输每次传 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('接收完成')
文章图片
文章图片
文章图片
文章图片
文章图片
顺利打开 pdf,证明传输无误:
文章图片
4. 网络聊天室 实现一个基于客户/服务器的网络聊天程序。
要求实现多个用户的群聊。
传输层使用UDP。
通过 UDP 广播实现群聊。那么对于服务端来说,任意一个操作都要向所有的用户进行广播,比如谁进来,谁离开,谁发消息了。
服务端逻辑就是广播。于是服务端维护一个集合,称之为用户池,存储用户的 ip,并且在每收到一个连接时都判断:
- 如果当前 ip 不在用户池中,那么加入用户池,并且广播 “xxx 进入聊天室”
- 如果当前 ip 在用户池,那么表示该用户进行了发言,服务端广播该用户的消息
- 如果用户发送了关键字 quit,那么从用户池中删除用户,并且广播 “xxx 离开聊天室”
服务端的代码:
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 :
文章图片
下图左侧的终端是服务端,右侧上方是 StDiana,右侧下方是 AliceBob,如图,在 AliceBob 输入之后,服务端和 StDiana 都收到了它的信息,并且打印在各自的终端上:
文章图片
然后 StDiana 也给予了回复:
终端的顺序和上图一致。
文章图片
随后 AliceBob 直接表示我先溜了。然后发送了 quit 关键字以退出聊天室,于是它的退出的信息广播到了 StDiana,同时服务端也记录了下来这个信息:
文章图片
StDiana 也做出了回复,只是这一次除了服务端,没有人再听得到它的消息了:
文章图片
其他演示:
文章图片
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 网络编程)】
文章图片
然后键入信息:
文章图片
按下发送按钮,大家都收到了消息:
文章图片
同样,别的用户依次发言,大家都能收到:
文章图片