一、网络基础(IP,端口,TCP等)
IP地址:标识网络中设备的一个地址
ping可以检查网络,ping www.baidu.com
可以查看百度网址的IP,网页搜索ping出来的这个IP也同样可以进入百度网页,网页搜索网址,实际上是搜索IP,只不过使用了URL网页网址(如www.baidu.com)封装,方便记忆搜寻,www.baidu.com相比一串数字(112.80.248.76,随着时间不同,该IP可能会变化),肯定是网址好记,如下图
端口:端口是传输数据的通道,每个端口都有对应的端口号,每个电脑存在一个IP地址,拥有多个端口,每个程序对应一个端口,以保证将该电脑接收到数据传输给对应的程序,比如发送微信,根据IP可以知道发送到哪台电脑,根据端口可以知道发送到该电脑的微信,而不是QQ或其他程序
端口号
分为知名端口号和动态端口号
知名端口号
范围从1到1023
,这些端口号一般固定分配给一些服务
,比如21端口分配给FTP(文件传输协议)服务,25端口分配给SMTP(简单邮件传输协议) 服务,80端口分配给HTTP服务(如输入www.baidu.com:80搜索还是百度首页)。
动态端口号
范围从1024到65535
,一般用于程序员开发应用程序使用
的端口,如果程序员开发的程序没有设置端口号,操作系统会在动态端口号这个范围内随机生成一个给开发的应用程序使用。当运行一个程序默认会有一个端口号,当这个程序退出时,所占用的这个端口号就会被释放。
TCP:通过IP地址和端口号可以进行数据传输,在数据传输之前,需要选择一个对应的传输协议,保证程序之间按照指定的传输规则进行数据的通信,这个传输协议就是TCP。TCP简称传输监控协议
,是一种面向连接的、可靠的、基于字节流的传输层通讯协议
,TCP 是一个稳定、可靠的传输协议,常用于对数据进行准确无误的传输
,比如: 文件下载,浏览器上网。
TCP通讯步骤:创建连接(TCP三次握手)、传输数据、关闭连接(四次挥手)
TCP 的特点
面向连接
:通信双方必须先建立好连接才能进行数据的传输,数据传输完成后,双方必须断开此连接,以释放系统资源。
可靠传输
:TCP 采用发送应答机制;超时重传;错误校验;流量控制和阻塞管理
socket:通讯数据的传输是通过socket来实现,socket
(简称 套接字
)是进程之间的通信工具
,负责进程之间的网络数据传输
,好比数据的搬运工,进程之间想要进行网络通信需要基于这个 socket。只要跟网络相关的应用程序或者软件都使用到了socket。
二、TCP网络应用可开发流程
TCP网络应用开发分为TCP客户端
程序开发和TCP服务端
程序开发(客户端程序是指运行在用户设备上的程序 服务端程序是指运行在服务器设备上的程序,专门为客户端提供数据服务)
步骤
:创建服务端端套接字对象;绑定端口号;设置监听;等待接受客户端的连接请求;接收数据;发送数据;关闭套接字
TCP客户端程序开发
步骤:创建客户端套接字对象(socket());和服务端套接字建立连接(connect());发送数据(send());接收数据(recv());关闭客户端套接字(close())
socket.socket(AddressFamily, Type)
:创建客户端对象,参数说明:AddressFamily
表示IP地址类型,分别为IPv4(socket.AF_INET)和IPv6(socket.AF_INET6),Type
表示连接类型(socket.SOCK_STREAM表示以TCP形式创建socket,socket.SOCK_DGRAM表示以UDP形式创建socket)
connect((host,port))
:表示和服务端套接字建立连接,host
是服务器ip地址,port
是应用程序的端口号
send(data)
:表示发送数据,data
是二进制数据(使用"".encode(“gbk”)转化)接收服务端在win系统上编码使用gbk
编码,如果是mac、linux默认使用utf-8
recv(buffersize)
:表示接收数据,buffersize
是每次接收数据的长度
TCP服务端程序开发
步骤
:创建服务端套接字对象;绑定端口号;设置监听;等待接受客户端的连接请求;接收数据;发送数据;关闭套接字;
socket.socket(AddressFamily, Type)
:创建客户端对象,参数说明:AddressFamily
表示IP地址类型,分别为IPv4(socket.AF_INET)和IPv6(socket.AF_INET6),Type
表示连接类型(socket.SOCK_STREAM表示以TCP形式创建socket,socket.SOCK_DGRAM表示以UDP形式创建socket)
setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
:端口复用,让程序退出端口号立马释放,因为服务器是一次性的,如果马上重启服务器时,会报错,原因是地址和端口没有被释放
bind(host, dort))
:表示绑定端口号, host 是ip地址﹐port 是端口号﹐ip地址一般不指定﹐表示本机的任何一个ip地址都可以。
listen(backlog)
:表示设置监听﹐backlog参数表示最大等待建立连接的个数
accept()
:表示等待接受客户端的连接请求
send(data)
:表示发送数据,data
是二进制数据(使用"".encode(“gbk”)转化)
recv(buffersize)
:表示接收数据,buffersize
是每次接收数据的长度
下面两段代码,分别为客户端和服务端,先运行服务端,再运行客户端,两者之间会有数据传输
客户端
import socket
if __name__ == '__main__':
# 创建TCP客户端套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 和服务端建立连接
tcp_socket.connect(('127.0.0.1', 8888))
# 发送数据
send_data = "发送数据内容".encode("gbk")
tcp_socket.send(send_data)
# 接收数据(设置最大字节数为1024),接收的是二进制数据,需要解码
recv_data = tcp_socket.recv(1024).decode("gbk")
print(recv_data)
# 关闭嵌套字
tcp_socket.close()
服务端
import socket
if __name__ == '__main__':
# 创建TCP服务端套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立马释放
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口号 绑定没有给定IP时默认本地IP地址
server_socket.bind(('', 8888))
# 设置监听
server_socket.listen(10)
# 等待客户端 客户端连接后,该函数会返回两个数据,一个是客户端Socket对象,一个是客户端的地址信息
client_socket, ip_port = server_socket.accept()
print(f'客户端 {ip_port[0]} 使用端口 {ip_port[1]} 连接成功...')
# 接收客户端数据
data = client_socket.recv(1024).decode('gbk')
print(data)
# 给客户端发送数据
data = '数据内容'.encode('gbk')
client_socket.send(data)
# 关闭客户端
client_socket.close()
# 关闭服务端
server_socket.close()
TCP网络应用程序的注意点介绍
当TCP客户端程序想要和TCP服务端程序进行通信的时候必须要先建立连接;TCP客户端程序一般不需要绑定端口号,因为客户端是主动发起建立连接的;TCP服务端程序必须绑定端口号,否则客户端找不到这个TCP 服务端程序;listen后的套接字是被动套接字,只负责接收新的客户端的连接请求,不能收发消息;当TCP客户端程序和TCP服务端程序连接成功后﹐TCP服务器端程序会产生一个新的套接字﹐收发客户端消息使用该套接字。
关闭accept返回的套接字意味着和这个客户端已经通信完毕;关闭listen后的套接字意味着服务端的套接字关闭了﹐会导致新的客户端不能连接服务端﹐但是之前已经接成功的客户端还能正常通信;当客户端的套接字调用close后﹐服务器端的recv 会解阻塞﹐返回的数据长度为O﹐服务端可以通过返回数据的长度来判断客户端是否已经下线﹐反之服务端关闭套接字﹐客户端的recv也会解阻塞﹐返回的数据长度也为0。
案例:多任务TCP服务端程序开发
完成任务可以使用线程,比进程节省资源
步骤
:
编写一个TCP服务端程序,循环等待接受客户端的连接请求;
当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的请求,防止主线程阻塞;
把创建的子线程设置成为守护主线程,防止主线程无法退出
多任务TCP服务器
(函数client_task在threading.Thread的args参数中)
import socket
import threading
# 实现多任务功能函数
def client_task(client_socket, ip_port):
print(ip_port, '已连接...')
# 实现一个循环,持续接收客户消息
while True:
# 接收客户端数据
data = client_socket.recv(1024).decode('gbk')
if len(data) != 0:
print(f'客户端{ip_port[0]} 发来的消息是 {data}')
else:
print(f'客户端{ip_port[0]} 已断开...')
break
# 服务端回复的消息
data = '数据内容'.encode('gbk')
client_socket.send(data)
# 关闭客户端
client_socket.close()
if __name__ == '__main__':
# 创建TCP服务端套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立马释放
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口号
server_socket.bind(('', 8888))
# 设置监听
server_socket.listen(128)
# 循环接收客户连接,等待客户端
while True:
# 客户端连接后,该函数会返回两个数据,一个是客户端Socket对象,一个是客户端的地址信息
client_socket, ip_port = server_socket.accept()
print(f'客户端 {ip_port[0]} 使用端口 {ip_port[1]} 连接成功...')
# 实现多任务
t_client = threading.Thread(target=client_task, args=(client_socket, ip_port))
# 设置守护线程
# t_client.setDaemon(True)
# 启动多任务
t_client.start()
# 关闭服务端
server_socket.close()
面向对象多任务TCP服务器
import socket
import threading
# 设置一个类
class SocketServer(object):
# 实例化
def __init__(self, port):
# 创建TCP服务端套接字
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立马释放
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口号
self.server_socket.bind(('', port))
# 设置监听
self.server_socket.listen(128)
# 启动服务器
def start(self):
# 循环接收客户连接,等待客户端
while True:
# 客户端连接后,该函数会返回两个数据,一个是客户端Socket对象,一个是客户端的地址信息
client_socket, ip_port = self.server_socket.accept()
print(f'客户端 {ip_port[0]} 使用端口 {ip_port[1]} 连接成功...')
# 实现多任务
t_client = threading.Thread(target=self.client_task, args=(client_socket, ip_port))
# 设置守护线程
# t_client.setDaemon(True)
# 启动多任务
t_client.start()
# 关闭服务端
server_socket.close()
# 实现多任务功能函数
def client_task(self, client_socket, ip_port):
print(ip_port, '已连接...')
# 实现一个循环,持续接收客户消息
while True:
# 接收客户端数据
data = client_socket.recv(1024).decode('gbk')
if len(data) != 0:
print(f'客户端{ip_port[0]} 发来的消息是 {data}')
else:
print(f'客户端{ip_port[0]} 已断开...')
break
# 服务端回复的消息
data = '数据内容'.encode('gbk')
client_socket.send(data)
# 关闭客户端
client_socket.close()
if __name__ == '__main__':
# 创建SocketServer对象,并启动服务器
server = SocketServer(8888)
server.start()
send和recv都不是直接接收到对方的数据和发送数据到对方,发送数据刽写入到发送缓冲区,接收数据是从缓冲区来读取,发送数据和接收数据最终是由操作系统控制网卡来完成
三、HTTP协议和静态Web服务器
HTTP协议是超文本传输协议
。超文本是指超越文本限制或者超链接,比如:图片、音乐、视频、超链接等等都属于超文本。传输HTTP协议格式的数据是基于TCP 传输协议
的,发送数据之前需要先建立连接。HTTP协议规定了浏览器和 Web 服务器通信数据的格式,也就是说浏览器和web服务器通信需要使用http协议
URL是统一资源定位符,即网络资源地址,也就是我们常说的网址。URL由协议部分
(如http://、https://、ftp://)、域名部分
(如www.baidu.com)、资源路径部分
(如com后面的部分)组成,部分可能包含查询参数部分
(如?page=1&count=10)。域名就是IP地址的别名,使用域名就是方便的记住某台主机IP地址。
开发者工具按F12可以打开浏览器的开发者工具,开发者工具包括元素、控制台、源代码、网络
。元素 (Elements):用于查看或修改HTML标签;控制台 (Console执行js代码;源代码 (Sources):查看静态资源文件,断点调试JS代码;网络 (Netwiork):查看http协议的通信过程。开发者工具的Headers选项总共有三部分组成:General
: 主要信息;Response Headers
: 响应头;Request Headers
: 请求头
HTTP请求报文最常见的有两种:GET方式
的请求报文(获取web服务器数据);POST方式
的请求报文(向web服务器提交数据)
HTTP响应报文是由响应行、响应头、空行和响应体4个部分组成。响应行是由三部分组成:HTTP协议版本、状态码、状态描述,最常见的状态码是200
HTTP常见状态码
(开头为1表示消息,2成功,3重定向,4请求错误,5服务器错误)
状态码 | 说明 |
---|---|
200 | 请求成功 |
307 | 重定向 |
400 | 错误的请求,请求地址或参数有误 |
404 | 请求资源在服务器中不存在 |
500 | 服务器内部资源代码出现错误 |
四、搭建Python自带静态Web服务器
静态Web服务器可以为发出请求的浏览器提供静态文档,使用指令python3 -m http.server 端口号
搭建Python自带静态Web服务器。-m表示运行包里面的模块,执行这个命令的时候,需要进入你自己指定静态文件的目录,然后通过浏览器就能访问对应的 html文件了,这样一个静态的web服务器就搭建好了,然后可以输入IP地址访问本地服务器页面。
静态Web服务器-返回固定页面数据
实现步骤
:编写一个TCP服务端程序;获取浏览器发送的http请求报文数据;读取固定页面数据,把页面数据组装成HTTP响应报文数据发送给浏览器;HTTP响应报文数据发送完成以后,关闭服务于客户端的套接字。
实例一:在py文件同级目录创建文件static/index.html
文件,然后服务器代码如下,运行下面代码,在浏览器中搜索localhost:8888
,然后页面就会展示index.html
文件内容
import socket
if __name__ == '__main__':
# 创建TCP服务端套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立马释放
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口号 绑定没有给定IP时默认本地IP地址
server_socket.bind(('', 8888))
# 设置监听
server_socket.listen(128)
while True:
# 等待客户端 客户端连接后,该函数会返回两个数据,一个是客户端Socket对象,一个是客户端的地址信息
client_socket, ip_port = server_socket.accept()
print(f'客户端 {ip_port[0]} 使用端口 {ip_port[1]} 连接成功...')
# 接收客户端数据
data = client_socket.recv(1024).decode('gbk')
print(data)
# 读取文件数据
with open("static/index.html", 'rb') as file:
file_data = file.read()
# 响应行
response_line = "HTTP/1.1 200 ok\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("gbk") + response_body
# 发送数据
client_socket.send(response_data)
# 关闭客户端
client_socket.close()
# 关闭服务端
server_socket.close()
实例二:上面实例一中,不管用户搜索什么,返回的都是固定页面的数据,接下来需要根据用户的请求返回指定页面的数据(根据URL不同展示不同HTML文件
)
步骤:获取用户请求资源的路径;根据请求资源的路径,读取指定文件的数据;组装指定文件数据的响应报文,发送给浏览器;判断请求的文件在服务端不存在,组装404状态的响应报文,发送给浏览器
import socket
if __name__ == '__main__':
# 创建TCP服务端套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立马释放
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口号 绑定没有给定IP时默认本地IP地址
server_socket.bind(('', 8888))
# 设置监听
server_socket.listen(128)
while True:
# 等待客户端 客户端连接后,该函数会返回两个数据,一个是客户端Socket对象,一个是客户端的地址信息
client_socket, ip_port = server_socket.accept()
print(f'客户端 {ip_port[0]} 使用端口 {ip_port[1]} 连接成功...')
# 接收客户端数据
data = client_socket.recv(1024).decode('gbk')
# print(data)
# 对数据进行分隔,最大分隔次数为2
request_list = data.split(" ", maxsplit=2)
# 获取请求资源路劲
request_path = request_list[1]
print(request_path)
# 判断请求的是否是根目录,如果条件成立,指定首页数据返回
if request_path == "/":
request_path = "/index.html"
try:
# 读取文件数据
with open("static" + request_path, 'rb') as file:
file_data = file.read()
except Exception as e:
# 文件找不到,拼接404异常报文
# 响应行
response_line = "HTTP/1.1 404 NOT FOUND\r\n"
# 响应头
response_header = "Server: PWS1.1\r\n"
# 响应体
with open("static/error.html", 'rb') as file:
error_data = file.read()
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("gbk") + error_data
# 发送数据
client_socket.send(response_data)
else:
# 响应行
response_line = "HTTP/1.1 200 ok\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("gbk") + response_body
# 发送数据
client_socket.send(response_data)
# 关闭客户端
client_socket.close()
# 关闭服务端
server_socket.close()
多任务的静态Web服务器:上面的不支持多用户同时访问,只能一个一个的处理客户端请求,这里可以使用多线实现多任务模式,即支持多用户同时访问
步骤
:把创建的子线程设置成为守护主线程,防止主线程无法退出;当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的请求,防止主线程阻塞
import socket
import multiprocessing
def server_start(port):
# 创建TCP服务端套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立马释放
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口号 绑定没有给定IP时默认本地IP地址
server_socket.bind(('', port))
# 设置监听
server_socket.listen(128)
while True:
# 等待客户端 客户端连接后,该函数会返回两个数据,一个是客户端Socket对象,一个是客户端的地址信息
client_socket, ip_port = server_socket.accept()
print(f'客户端 {ip_port[0]} 使用端口 {ip_port[1]} 连接成功...')
p = multiprocessing.Process(target=task, args=(client_socket,))
p.start()
client_socket.close()
# 关闭服务端
server_socket.close()
def task(client_socket):
# 接收客户端数据
data = client_socket.recv(1024).decode('gbk')
# print(data)
# 对数据进行分隔,最大分隔次数为2
request_list = data.split(" ", maxsplit=2)
# 获取请求资源路劲
request_path = request_list[1]
print(request_path)
# 判断请求的是否是根目录,如果条件成立,指定首页数据返回
if request_path == "/":
request_path = "/index.html"
try:
# 读取文件数据
with open("static" + request_path, 'rb') as file:
file_data = file.read()
except Exception as e:
# 文件找不到,拼接404异常报文
# 响应行
response_line = "HTTP/1.1 404 NOT FOUND\r\n"
# 响应头
response_header = "Server: PWS1.1\r\n"
# 响应体
with open("static/error.html", 'rb') as file:
error_data = file.read()
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("gbk") + error_data
# 发送数据
client_socket.send(response_data)
else:
# 响应行
response_line = "HTTP/1.1 200 ok\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("gbk") + response_body
# 发送数据
client_socket.send(response_data)
finally:
# 关闭客户端
client_socket.close()
if __name__ == '__main__':
server_start(8888)
面向对象静态Web服务器是在上面的基础上将上面的方法封装成类
通过sys可以获取参数,比如,上面服务端的代码文件为main.py,运行指令python3 main.py 8888
可以开启服务器并且将8888传入到port中
if __name__ == '__main__':
# sys.argv属性用来接收命令行参数,返回一个列表
# 第一个参数是当前执行的程序文件,后面还可以跟其它数据,依次放到列表中
# 取出第二个参数,用来做为当前程序的端口号
port = int(sys.argv[1])
server_start(port)