一步一步实现一个简单的web服务器

目录

一.实现一个tcp/ip服务器

二.实现一个web服务器,并且能够返回指定内容

三.实现一个web服务器,并且能够返回某个html页面

四.实现一个web服务器,并且能够返回多种内容

五.报错解决


一.实现一个tcp/ip服务器

tcp服务器端编程,分为七个步骤,直接上代码

# 导入socket模块中的所有方法
from socket import *

# 定义变量存储服务器的IP地址,端口号
# 这里我们设置IP地址为本机地址,端口号为8080
IP = '127.0.0.1'
PORT = 8080

# 1.实例化一个服务器套接字对象,使用socket方法,传入两个参数
# 第一个参数代表网络层基于IPv4,第二个参数代表传输层基于TCP协议
listen_socket = socket(AF_INET, SOCK_STREAM)

# 2.实例化的服务器套接字对象,调用bind()方法,绑定端口号和IP地址
# 一定注意,bind()方法的参数是一个元组,元组里面写有IP地址和端口号
listen_socket.bind((IP, PORT))

# 3.设置监听,并且设置最大监听数量为5
# 最大监听数量不超过128
listen_socket.listen(5)
# 到这里,服务器就启动成功了,所以我们可以输出一些提示信息如下
print(f'服务器已启动,在{PORT}端口等待连接')

# 4.服务器套接字对象调用accept()方法,等待客户端/浏览器的连接
# accept()方法没有参数,但是有两个返回值
# 返回一个元组,元组中第一个元素是一个新的套接字对象,这个套接字对象用来和客户端进行数据交换
# 元组中第二个元素是客户端的IP地址和端口号
# 这里定义两个变量,使用元组拆包的方式进行接收
# accept()方法,如果没有客户端连接,会一直停留在这一步,有点类似于input()函数
data_socket, addr = listen_socket.accept()
# 输出连接的客户端的信息如下
print(f'{addr}已连接')

# 5.新的套接字对象,调用recv()方法,接收客户端返回的数据
# 注意这里调用recv()方法的是data_socket对象,而不是listen_socket对象
# recv()方法有一个参数,1024代表一次接收的字节流长度最大为1024字节
# 把接收到的数据,使用decode()方法,由字节流转码成字符串,并打印控制台
recv_data = data_socket.recv(1024).decode('gbk')
print(recv_data)

# 6.新的套接字对象,调用send()方法,发送数据给客户端
# 同样注意这里调用send()方法的是data_socket对象,而不是listen_socket对象
# 把要发送的数据,先使用encode()方法,由字符串转码成字节流,再作为参数传入send()方法中
send_data = '服务器已收到'.encode('gbk')
data_socket.send(send_data)

# 7.关闭两个套接字对象
data_socket.close()
listen_socket.close()

程序运行以后,如图所示

>>>

服务器已启动,在8080端口等待连接

这里我们使用一个工具来模拟客户端:NetAssist

这个调试助手连接以后,pycharm运行窗口编程这样

>>>
服务器已启动,在8080端口等待连接
('127.0.0.1', 10869)已连接

此时,我们在调试助手窗口数据内容,然后发送给服务器,可以看到服务器自动回复了我们设置的内容

 此时,pycharm运行窗口中如下图,由于我们目前没有设置循环,所以连接一次服务器套接字对象就关闭了

>>>
服务器已启动,在8080端口等待连接
('127.0.0.1', 10869)已连接
你好

进程已结束,退出代码0

这样我们就实现了一个非常基础简单的tcp服务器端

二.实现一个web服务器,并且能够返回指定内容

第一步的代码中,我们使用的是cs模型,也就是client客户端-server服务器端模型

接下来我们编写一下bs模型,也就是browser浏览器-server服务器端模型的代码

这里和第一步代码不同的地方在于,回复给浏览器的内容,要符合http响应报文格式

完整代码如下

# 导入socket模块中的所有方法
from socket import *

# 定义变量存储服务器的IP地址,端口号
# 这里我们设置IP地址为本机地址,端口号为8080
IP = '127.0.0.1'
PORT = 8080

# 1.实例化一个服务器套接字对象,使用socket方法,传入两个参数
# 第一个参数代表网络层基于IPv4,第二个参数代表传输层基于TCP协议
listen_socket = socket(AF_INET, SOCK_STREAM)

# 2.实例化的服务器套接字对象,调用bind()方法,绑定端口号和IP地址
# 一定注意,bind()方法的参数是一个元组,元组里面写有IP地址和端口号
listen_socket.bind((IP, PORT))

# 3.设置监听,并且设置最大监听数量为5
# 最大监听数量不超过128
listen_socket.listen(5)
# 到这里,服务器就启动成功了,所以我们可以输出一些提示信息如下
print(f'服务器已启动,在{PORT}端口等待连接')

# 4.服务器套接字对象调用accept()方法,等待客户端/浏览器的连接
# accept()方法没有参数,但是有两个返回值
# 返回一个元组,元组中第一个元素是一个新的套接字对象,这个套接字对象用来和客户端进行数据交换
# 元组中第二个元素是客户端的IP地址和端口号
# 这里定义两个变量,使用元组拆包的方式进行接收
# accept()方法,如果没有客户端连接,会一直停留在这一步,有点类似于input()函数
data_socket, addr = listen_socket.accept()
# 输出连接的客户端的信息如下
print(f'{addr}已连接')

# 5.新的套接字对象,调用recv()方法,接收客户端返回的数据
# 注意这里调用recv()方法的是data_socket对象,而不是listen_socket对象
# recv()方法有一个参数,1024代表一次接收的字节流长度最大为1024字节
# 把接收到的数据,使用decode()方法,由字节流转码成字符串,并打印控制台
recv_data = data_socket.recv(1024).decode('gbk')
print(recv_data)

# 6.新的套接字对象,调用send()方法,发送数据给客户端
# 同样注意这里调用send()方法的是data_socket对象,而不是listen_socket对象
# 把要发送的数据,先使用encode()方法,由字符串转码成字节流,再作为参数传入send()方法中

# 不同点在于发送内容的格式要符合http响应报文的格式
# http响应报文 = 响应行 + 响应头 + 空行 + 响应体
# 我们要回复的内容写在响应体中,空行则只书写\r\n
# 响应行
line = 'HTTP/1.1 200 ok\r\n'
# 响应头
head = 'Server:webserver\r\n'
# 响应体
body = '服务器已收到'
send_data = line + head + '\r\n' + body
# 转码
send_data = send_data.encode('gbk')
data_socket.send(send_data)

# 7.关闭两个套接字对象
data_socket.close()
listen_socket.close()

程序运行之后,我们打开浏览器,在地址栏输入,并回车

http://127.0.0.1:8080/

浏览器显示内容如下

 此时,pycharm运行栏显示内容如下

>>>
服务器已启动,在8080端口等待连接
('127.0.0.1', 12074)已连接
GET / HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

进程已结束,退出代码0
 

这样我们就实现了一个非常基础简单的web服务器

三.实现一个web服务器,并且能够返回某个html页面

针对上一步的代码,想要返回一个html页面,比如返回一个index.html的导航页面

只需要在body响应体那里,把内容换成相应的文件就可以了,涉及到文件操作三步骤,也可以使用with方法,完整代码如下

# 导入socket模块中的所有方法
from socket import *

# 定义变量存储服务器的IP地址,端口号
# 这里我们设置IP地址为本机地址,端口号为8080
IP = '127.0.0.1'
PORT = 8080

# 1.实例化一个服务器套接字对象,使用socket方法,传入两个参数
# 第一个参数代表网络层基于IPv4,第二个参数代表传输层基于TCP协议
listen_socket = socket(AF_INET, SOCK_STREAM)

# 2.实例化的服务器套接字对象,调用bind()方法,绑定端口号和IP地址
# 一定注意,bind()方法的参数是一个元组,元组里面写有IP地址和端口号
listen_socket.bind((IP, PORT))

# 3.设置监听,并且设置最大监听数量为5
# 最大监听数量不超过128
listen_socket.listen(5)
# 到这里,服务器就启动成功了,所以我们可以输出一些提示信息如下
print(f'服务器已启动,在{PORT}端口等待连接')

# 4.服务器套接字对象调用accept()方法,等待客户端/浏览器的连接
# accept()方法没有参数,但是有两个返回值
# 返回一个元组,元组中第一个元素是一个新的套接字对象,这个套接字对象用来和客户端进行数据交换
# 元组中第二个元素是客户端的IP地址和端口号
# 这里定义两个变量,使用元组拆包的方式进行接收
# accept()方法,如果没有客户端连接,会一直停留在这一步,有点类似于input()函数
data_socket, addr = listen_socket.accept()
# 输出连接的客户端的信息如下
print(f'{addr}已连接')

# 5.新的套接字对象,调用recv()方法,接收客户端返回的数据
# 注意这里调用recv()方法的是data_socket对象,而不是listen_socket对象
# recv()方法有一个参数,1024代表一次接收的字节流长度最大为1024字节
# 把接收到的数据,使用decode()方法,由字节流转码成字符串,并打印控制台
recv_data = data_socket.recv(1024).decode()
print(recv_data)

# 6.新的套接字对象,调用send()方法,发送数据给客户端
# 同样注意这里调用send()方法的是data_socket对象,而不是listen_socket对象
# 把要发送的数据,先使用encode()方法,由字符串转码成字节流,再作为参数传入send()方法中

# 不同点在于发送内容的格式要符合http响应报文的格式
# http响应报文 = 响应行 + 响应头 + 空行 + 响应体
# 我们要回复的内容写在响应体中,空行则只书写\r\n
# 响应行
line = 'HTTP/1.1 200 ok\r\n'
# 响应头
head = 'Server:webserver\r\n'
# 响应体
# 使用with方法操作文件
# 这里还要注意打开文件的编码,使用utf8编码(因为这个html文件就是使用utf8编码)
# 同时,encode和decode方法转码,编码就不用指定了,不然可能会产生乱码问题
with open('./html/index.html', 'r', encoding='utf8') as f:
    body = f.read()
send_data = line + head + '\r\n' + body
# 转码
send_data = send_data.encode()
data_socket.send(send_data)

# 7.关闭两个套接字对象
data_socket.close()
listen_socket.close()

在浏览器输入地址,能够正确链接到指定html文件

这样,这个web服务器就能够返回指定的html页面(主页)

四.实现一个web服务器,并且能够返回多种内容

这个代码还不够完善,我们想在主页点击连接,就能跳转到指定的html页面或者跳转到指定的超文本内容,还需要做如下的修改

我们先观察一下浏览器发送的http请求报文结构

服务器已启动,在8080端口等待连接
('127.0.0.1', 14070)已连接

# 请求行
GET /html1.html HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

在请求行中,出现了我们想要连接到的页面的资源路径,即/html1.html 这个路径,那么我们就可以在浏览器发送了http请求报文以后,把这个资源路径的地址提取出来,存储到变量中,并把这个变量作为文件打开的地址,然后把文件返回给浏览器,

最后再添加循环,就能够实现上述效果

完整代码如下

# 导入socket模块中的所有方法
from socket import *

# 定义变量存储服务器的IP地址,端口号
# 这里我们设置IP地址为本机地址,端口号为8080
IP = '127.0.0.1'
PORT = 8080

# 1.实例化一个服务器套接字对象,使用socket方法,传入两个参数
# 第一个参数代表网络层基于IPv4,第二个参数代表传输层基于TCP协议
listen_socket = socket(AF_INET, SOCK_STREAM)

# 2.实例化的服务器套接字对象,调用bind()方法,绑定端口号和IP地址
# 一定注意,bind()方法的参数是一个元组,元组里面写有IP地址和端口号
listen_socket.bind((IP, PORT))

# 3.设置监听,并且设置最大监听数量为5
# 最大监听数量不超过128
listen_socket.listen(5)
# 到这里,服务器就启动成功了,所以我们可以输出一些提示信息如下
print(f'服务器已启动,在{PORT}端口等待连接')

# 添加循环
while True:

    # 4.服务器套接字对象调用accept()方法,等待客户端/浏览器的连接
    # accept()方法没有参数,但是有两个返回值
    # 返回一个元组,元组中第一个元素是一个新的套接字对象,这个套接字对象用来和客户端进行数据交换
    # 元组中第二个元素是客户端的IP地址和端口号
    # 这里定义两个变量,使用元组拆包的方式进行接收
    # accept()方法,如果没有客户端连接,会一直停留在这一步,有点类似于input()函数
    data_socket, addr = listen_socket.accept()
    # 输出连接的客户端的信息如下
    print(f'{addr}已连接')

    # 5.新的套接字对象,调用recv()方法,接收客户端返回的数据
    # 注意这里调用recv()方法的是data_socket对象,而不是listen_socket对象
    # recv()方法有一个参数,1024代表一次接收的字节流长度最大为1024字节
    # 接收的数据我们先转码成字符串
    recv_data = data_socket.recv(1024).decode()
    print(recv_data)

    # 添加是否断开连接的判断
    if not recv_data:
        print(f'{addr}已断开')
        break
    else:

        # 然后我们使用字符串的split()方法,按照空格进行切割,返回一个列表
        recv_data = recv_data.split(' ')
        # 通过http请求报文结构,不难发现,这个列表的第二个元素,就是要访问的资源路径地址
        # 把这个地址赋值给变量path
        path = recv_data[1]

        # 6.新的套接字对象,调用send()方法,发送数据给客户端
        # 同样注意这里调用send()方法的是data_socket对象,而不是listen_socket对象
        # 把要发送的数据,先使用encode()方法,由字符串转码成字节流,再作为参数传入send()方法中

        # 不同点在于发送内容的格式要符合http响应报文的格式
        # http响应报文 = 响应行 + 响应头 + 空行 + 响应体
        # 我们要回复的内容写在响应体中,空行则只书写\r\n
        # 响应行
        line = 'HTTP/1.1 200 ok\r\n'
        # 响应头
        head = 'Server:webserver\r\n'
        # 响应体

        # 这里我们添加一个if判断,如果直接输入网址,跳转到主页
        # 因为直接输入网址,http请求报文的资源路径是 / 左斜杠
        if path == '/':
            path = '/index.html'

        # 使用with方法操作文件
        # 这里拼接path和./html,组成一个完整的资源路径
        # 同时注意打开方式应该为rb二进制读,编码不指定
        with open('./html' + path, 'rb') as f:
            body = f.read()
        # 这里应该注意,body存储的内容本身就是字节流(二进制流)的格式了,所以不需要再转码
        send_data = line + head + '\r\n'
        # 转码
        send_data = send_data.encode()
        data_socket.send(send_data + body)

# 7.关闭套接字对象,监听套接字对象可以不关闭
data_socket.close()

然后在浏览器直接输入资源路径,能够实现直接跳转到指定页面

或者只输入网址,不输入资源路径,也能够跳转到主页,同时在主页点击链接,也能够链接到其他页面或者内容中

这样,就搭建了一个简单版本的web服务器 

五.报错解决

FileNotFoundError: [Errno 2] No such file or directory: './html/favicon.ico'

解决办法:在html文件夹中添加一个favicon.ico的图标

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值