一 . 实验目的
熟悉基于 Python 进行 TCP 套接字编程的基础知识,理解 HTTP 报文格式,能基于 Python 编写一个可以一次响应一个 HTTP 请求,并返回静态文件的简单 Web 服务器。
二 . 实验内容
利用 Python 开发一个可以一次处理一个 HTTP 请求的 Web 服务器,该服务器可以接受并解析HTTP 请求,然后从服务器的文件系统中读取被 HTTP 请求的文件,并根据该文件是否存在而向客户端发送正确的响应消息。
三 . 实验原理
基于 TCP 的面向客户端/服务器在 Python 实现中的的工作流程是:
1. 首先在服务器端通过调用 s o c k e t ( ) 创建套接字来启动一个服务器;
2. 服务器调用 b i n d ( ) 绑定指定服务器的套接字地址(IP 地址 + 端口号);
3. 服务器调用 l i s t e n ( ) 做好侦听准备,同时规定好请求队列的长度;
4. 服务器进入阻塞状态,等待客户的连接请求;
5. 服务器通过 a c c e p t ( ) 来接收连接请求,并获得客户的 socket 地址。
6. 在客户端通过调用 s o c k e t ( ) 创建套接字;
7. 客户端调用 c o n n e c t ( ) 和服务器建立连接。
8. 连接建立成功后,客户端和服务器之间通过调用 r e a d ( ) 和 w r i t e ( ) 来接收和发送数据。
9. 数据传输结束后,服务器和客户各自通过调用 c l o s e ( ) 关闭套接字。
基于 Python的 TCP 客户端/服务器具体工作流程如图1所示。
四 . 实验条件
• 装有 python 环境的电脑一台(使用两个终端模拟两台电脑/进程);
• 局域网环境;
• 部分代码;
• Python 语言参考手册 – TCP 部分;
• HTTP 协议参考手册。
五 . 实验步骤
逐步填充代码中不完善的部分,并完成一个具有以下功能的简单 Web 服务器:
1. 服务器收到请求时能创建一个 TCP 套接字;
2. 可以通过这个 TCP 套接字接收 HTTP 请求;
3. 解析 HTTP 请求并在操作系统中确定客户端所请求的特定文件;
4. 从服务器的文件系统读取客户端请求的文件;
5. 当被请求文件存在时,创建一个由被请求的文件组成的“请求成功”HTTP 响应报文;
6. 当被请求文件不存在时,创建“请求目标不存在”HTTP 响应报文;
7. 通过 TCP 连接将响应报文发回客户端。
六 . 进阶任务
本实验中的 Web 服务器一次只能处理一个 HTTP 请求,请自行查阅线程知识,修改代码,实现一个能够同时处理多个请求的多线程服务器。
七 . 注意事项及说明
客户端发来的 HTTP 请求中,URL 都是相对根“/”的相对路径,因此需要将服务器文件系统中的某个地址映射为这个“根”。为了简化工作,建议将服务器程序存放的路径作为这个根,这样运行服务器程序时,程序会自动从当前运行的路径开始查询文件。
例如:假设将“HelloWorld.html”这个 html 文件放置在服务器程序文件存放目录中,服务器运行主机的 IP 地址为“123.234.12.34”,6789 为服务器监听的端口号。
则从 URL:http;//123.234.12.34:6789/HelloWorld.html 出发可以获取到“HelloWorld.html” 这个文件。
八 . 单一请求
代码说明在代码注释中给出,具体流程如 三、实验原理
服务器 web_server.py代码
from socket import *
import sys
def web_server(ip, port):
# 服务器端通过调用socket()创建套接字来启动一个服务器
serverSocket = socket(AF_INET,SOCK_STREAM)
server_address = (ip, port)#接收传入的ip地址与端口号
#服务器调用bind()绑定指定服务器的套接字地址(IP 地址 + 端口号)
serverSocket.bind(server_address)
# 服务器调用listen()做好侦听准备,同时规定好请求队列的长度
Length=1024
try:
serverSocket.listen(1)
except socket.error:
print("fail to listen on port %s" % error)
sys.exit(1)
while True:
print("Ready to server...")
#服务器进入阻塞状态,等待客户的连接请求
#服务器通过accept来接收连接请求,并获得客户的 socket 地址
connectionSocket, addr = serverSocket.accept()
break
while True:
try:
#通过TCP 套接字接收 HTTP 请求
message = connectionSocket.recv(Length)
msg_de = message.decode('utf-8')
print('file msg from client: '+msg_de)
#收到客户端断开连接消息
if msg_de == 'disconnect':break
#从服务器的文件系统读取客户端请求的文件
filename=message
f=open(filename[0:])
#被请求文件存在,创建一个由被请求的文件组成的“请求成功”HTTP 响应报文
output=(msg_de+' has been successfully received.').encode('utf-8')
#通过 TCP 连接将响应报文发回客户端
connectionSocket.send(output)
except IOError:
#被请求文件不存在,创建“请求目标不存在”HTTP 响应报文
output=('The request has failed.').encode('utf-8')
#通过 TCP 连接将响应报文发回客户端
connectionSocket.send(output)
print("finish test, close connect")
#通过close()关闭套接字
connectionSocket.close()
serverSocket.close()
if __name__=='__main__':
web_server('127.0.0.1',6000)
客户端 web_client.py代码
from socket import *
def web_client(ip,port):
#在客户端通过调用socket()创建套接字
clientSocket = socket(AF_INET,SOCK_STREAM)
failed_count = 0
while True:
try:
print("start connect to server ")
#客户端调用connect()和服务器建立连接
clientSocket.connect((ip,port))
break
except socket.error:
failed_count += 1
print("fail to connect to server %d times" % failed_count)
if failed_count == 100: return
#连接成功后发送消息
while True:
print("connect success")
receive_count = 0
while True:
if receive_count%2==0:
msg = 'iloveu.txt'#存在的文档消息请求
else:
msg='1.txt'#不存在的文档消息请求
#使用send()发送消息
clientSocket.send(msg.encode('utf-8'))
msg = clientSocket.recv(1024)
print('msg from server :'+msg.decode('utf-8'))
receive_count+= 1
#一共发送14次消息,随后告知服务器将要断开连接
if receive_count==14:
msg = 'disconnect'
clientSocket.send(msg.encode('utf-8'))
break
break
#通过close关闭套接字
clientSocket.close()
if __name__=='__main__':
web_client('127.0.0.1',6000)
九 . 多请求
在单一HTTP请求的代码基础上,创建多线程服务器,可以同一时间处理多个HTTP请求,并在客户端使用多线程同时向服务器发送HTTP请求。
客户端在请求时将自己的线程号与请求文件一同发出,服务器在收到客户端的请求后,若请求成功,将自身的线程号一并返回给客户端。
多线程服务器 web_mul_server.py代码
from socket import *
import sys,threading
def web_mul_server(ip, port,num):
# 服务器端通过调用socket()创建套接字来启动一个服务器
serverSocket = socket(AF_INET,SOCK_STREAM)
server_address = (ip, port)#接收传入的ip地址与端口号
#服务器调用bind()绑定指定服务器的套接字地址(IP 地址 + 端口号)
serverSocket.bind(server_address)
# 服务器调用listen()做好侦听准备,同时规定好请求队列的长度
Length=1024
try:
serverSocket.listen(1)
except socket.error:
print("fail to listen on port %s" % error)
sys.exit(1)
while True:
print("server "+str(num)+" ready to server...")
#服务器进入阻塞状态,等待客户的连接请求
#服务器通过accept来接收连接请求,并获得客户的 socket 地址
connectionSocket, addr = serverSocket.accept()
break
while True:
try:
#通过TCP 套接字接收 HTTP 请求
message = connectionSocket.recv(Length)
msg_de = message.decode('utf-8')
print('file msg from '+msg_de)
#收到客户端断开连接消息
if msg_de == 'disconnect':break
#从服务器的文件系统读取客户端请求的文件
client_num=message.split()[0]
client_num=client_num.decode('utf-8')
filename=message.split()[1]
f=open(filename[0:])
#被请求文件存在,创建一个由被请求的文件组成的“请求成功”HTTP 响应报文
output=(msg_de+' from '+client_num+' has been successfully received.--server '+str(num)).encode('utf-8')
#通过 TCP 连接将响应报文发回客户端
connectionSocket.send(output)
except IOError:
#被请求文件不存在,创建“请求目标不存在”HTTP 响应报文
output=('The request has failed.').encode('utf-8')
#通过 TCP 连接将响应报文发回客户端
connectionSocket.send(output)
print("finish test, close connect")
#通过close()关闭套接字
connectionSocket.close()
serverSocket.close()
if __name__=='__main__':
t1=threading.Thread(target=web_mul_server,args=('127.0.0.1',6000,1))
t2=threading.Thread(target=web_mul_server,args=('127.0.0.1',7000,2))
t3=threading.Thread(target=web_mul_server,args=('127.0.0.1',8000,3))
t1.start()
t2.start()
t3.start()
多线程客户端 web_mul_client.py代码
from socket import *
import threading
def web_mul_client(ip,port,num):
#在客户端通过调用socket()创建套接字
clientSocket = socket(AF_INET,SOCK_STREAM)
failed_count = 0
while True:
try:
print('client '+str(num)+" start connect to server ")
#客户端调用connect()和服务器建立连接
clientSocket.connect((ip,port))
break
except socket.error:
failed_count += 1
print('client '+str(num)+" fail to connect to server %d times" % failed_count)
if failed_count == 100: return
#连接成功后发送消息
while True:
print('client '+str(num)+" connect success")
receive_count = 0
while True:
if receive_count%2==0:
msg ='client'+str(num)+' iloveu.txt'#存在的文档消息请求
else:
msg='client'+str(num)+' 1.txt'#不存在的文档消息请求
#使用send()发送消息
clientSocket.send(msg.encode('utf-8'))
msg = clientSocket.recv(1024)
print('msg from server :'+msg.decode('utf-8'))
receive_count+= 1
#一共发送14次消息,随后告知服务器将要断开连接
if receive_count==14:
msg = 'disconnect'
clientSocket.send(msg.encode('utf-8'))
break
break
#通过close关闭套接字
clientSocket.close()
if __name__=='__main__':
t1=threading.Thread(target=web_mul_client,args=('127.0.0.1',6000,1))
t2=threading.Thread(target=web_mul_client,args=('127.0.0.1',7000,2))
t3=threading.Thread(target=web_mul_client,args=('127.0.0.1',8000,3))
t1.start()
t2.start()
t3.start()
十 . HTTP请求
在设计好tcp的基础上,在报文中加上相应的http头部即可从url发送文件需求消息。需要注意的是由于tcp的http请求报文存在一定的格式,在server进行接收时需要截取其中有用的消息片段访问文件,在发送消息时也要按照固定的格式进行发送,否则将会出错。
通过url向web发送报文,发送方式为127.0.0.1:4900/iiii.html 即ip:port/文件名。
若从url输入内容127.0.0.1:4040/iloveu.html,在server中收到的信息如
需要用split()函数截取出有用的文件名信息进行下一步操作。
在向url返回信息时,需要在发送前发送http头信息
单一http请求web.py代码
from socket import *
import sys
def web_server(ip, port):
# 服务器端通过调用socket()创建套接字来启动一个服务器
serverSocket = socket(AF_INET,SOCK_STREAM)
serverSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server_address = ('', port)#接收传入的ip地址与端口号
#服务器调用bind()绑定指定服务器的套接字地址(IP 地址 + 端口号)
serverSocket.bind(server_address)
# 服务器调用listen()做好侦听准备,同时规定好请求队列的长度
Length=1024
try:
serverSocket.listen(5)
except socket.error:
print("fail to listen on port %s" % error)
sys.exit(1)
while True:
print("Ready to server...")
connectionSocket, addr = serverSocket.accept()
print('accepted!'+str(addr))
try:
#通过TCP 套接字接收 HTTP 请求
message = connectionSocket.recv(Length)
message=message.split()[1]
msg_de = message.decode('utf-8')
print('file msg from client: '+msg_de)
#收到客户端断开连接消息
if msg_de == '\disconnect':break
#从服务器的文件系统读取客户端请求的文件
filename=message
f=open(filename[1:])
text=f.read()
#被请求文件存在,创建一个由被请求的文件组成的“请求成功”HTTP 响应报文
output= "HTTP/1.1 200 OK\r\n"
output+= "\r\n"
output+='\r\n'
output+=msg_de+' has been successfully received.\n'
output=output.encode('utf-8')
print('output: '+str(output))
#通过 TCP 连接将响应报文发回客户端
connectionSocket.send(output)
for i in range(0,len(text)):
connectionSocket.send(text[i].encode('utf-8'))
print('send!')
except IOError:
#被请求文件不存在,创建“请求目标不存在”HTTP 响应报文
output= "HTTP/1.1 200 OK\r\n"
output+= "\r\n"
output+='\r\n'
output+='The request has failed.\n'
output=output.encode('utf-8')
print('output: '+str(output))
#通过 TCP 连接将响应报文发回客户端
connectionSocket.send(output)
print("finish test, close connect")
#通过close()关闭套接字
connectionSocket.close()
serverSocket.close()
if __name__=='__main__':
web_server('127.0.0.1',8888)
单一http请求实验结果
多http请求web_mul.py代码
from socket import *
import sys,threading
def web_mul(ip, port):
# 服务器端通过调用socket()创建套接字来启动一个服务器
serverSocket = socket(AF_INET,SOCK_STREAM)
serverSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server_address = ('', port)#接收传入的ip地址与端口号
#服务器调用bind()绑定指定服务器的套接字地址(IP 地址 + 端口号)
serverSocket.bind(server_address)
# 服务器调用listen()做好侦听准备,同时规定好请求队列的长度
Length=1024
try:
serverSocket.listen(5)
except socket.error:
print("fail to listen on port %s" % error)
sys.exit(1)
while True:
print("Ready to server...")
connectionSocket, addr = serverSocket.accept()
print('accepted!'+str(addr))
try:
#通过TCP 套接字接收 HTTP 请求
message = connectionSocket.recv(Length)
message=message.split()[1]
msg_de = message.decode('utf-8')
print('file msg from client: '+msg_de)
#收到客户端断开连接消息
if msg_de == '\disconnect':break
#从服务器的文件系统读取客户端请求的文件
filename=message
f=open(filename[1:])
text=f.read()
#被请求文件存在,创建一个由被请求的文件组成的“请求成功”HTTP 响应报文
output= "HTTP/1.1 200 OK\r\n"
output+= "\r\n"
output+='\r\n'
output+=msg_de+' has been successfully received.\n'
output=output.encode('utf-8')
print('output: '+str(output))
#通过 TCP 连接将响应报文发回客户端
connectionSocket.send(output)
for i in range(0,len(text)):
connectionSocket.send(text[i].encode('utf-8'))
print('send!')
except IOError:
#被请求文件不存在,创建“请求目标不存在”HTTP 响应报文
output= "HTTP/1.1 200 OK\r\n"
output+= "\r\n"
output+='\r\n'
output+='The request has failed.\n'
output=output.encode('utf-8')
print('output: '+str(output))
#通过 TCP 连接将响应报文发回客户端
connectionSocket.send(output)
print("finish test, close connect")
#通过close()关闭套接字
connectionSocket.close()
serverSocket.close()
if __name__=='__main__':
t1=threading.Thread(target=web_mul,args=('127.0.0.1',4700))
t2=threading.Thread(target=web_mul,args=('127.0.0.1',4800))
t3=threading.Thread(target=web_mul,args=('127.0.0.1',4900))
t1.start()
t2.start()
t3.start()
多http请求实验结果
十一 . 实验分析
实验过程
在单一HTTP请求中打开两个终端分别运行服务器端web_wever.py与客户端web_client.py。其中需要先打开服务器端再运行客户端。
在单一HTTP请求的基础上进行多HTTP请求的编写。其中对于client向server请求的方式可以有多种,可以开多个进程对server发送请求,此处使用较为方便同时也是server端使用的多线程的方法发送请求。
由于TCP是可靠的连接,在一方连接断开时需要通知另一方自己将断开连接,在实验中在client发送了一定次数请求后将关闭连接并通知server使其也关闭的功能。若client关闭了连接却没有通知server导致server在循环中继续通过tcp发送消息将导致程序报错,同理若server断开连接并进行下一次的等待操作,若没有通知client,client将无法通过tcp传输而导致报错,也因为没有断开连接将无法正常进行下一次的连接操作。
还有一种较为常见可行的方法是client接收用户键盘输入的文件请求,关闭操作也可以由接收的键盘输入控制,而后将文件请求通过tcp发送给服务器端,之后的方法大致相同。
在使用url向server发送请求时要注意端口号,以及在处理报文和发送报文时需要按照固定的格式,接收时需要截取特定的报文内容,发送时需要添加固定的格式。
需要注意传输的内容格式需要是byte,发送时需要使用encode()转码,接收时需要使用decode()转成str。
结果分析
在单一HTTP请求与多HTTP请求中都做到了client向server发送文件请求,server在本地目录下查询文件后返回的操作。在执行完后正常而言不应该进行close操作,但为了代码与执行的整洁性与逻辑性,我们仍然选择在完成特定功能后关闭连接。
tcp是可靠的连接,我们在多HTTP请求中可以观察到,当连接确立后,对应的server线程会接收来自对应client发送的消息,随后将消息准确地发回对应的client。
可以通过url向web发送报文,发送方式为127.0.0.1:4900/iiii.html 即ip:port/文件名。
遇到问题
在server进行accept操作时若不使用循环,当没有client时会报错,所以使用循环进行等待。
在server和client断开连接时若操作不当,server或client单方面断开连接很容易导致报错,在这里client断开连接时会向server发送断开连接消息告知server,server随机断开不再进行传输操作。
当连接异常时,较多为server或client单方面中断,另一方若继续通过tcp发送消息,电脑将中断该操作。
在使用url向server发送请求时,在直接使用之前的方式会出现应答出错,需要将单独的while循环的accpet放进整体中才能正常运行。