第十课 python进阶socket编程
tags:
- Docker
- 慕课网
categories:
- TCP/UDP
- HTTP
- SOCKET
- 网络编程
文章目录
第一节 协议知识
1.1 协议知识
- 推荐书籍了解TCP/IP协议,TCP/IP详解卷1 :协议(共三卷)。
- 操作系统给我们提供了一个socket接口, 用于和TCP和UDP打交道, socket它不属于任何协议。
- HTTP协议和TCP协议的区别点(面试题):
- TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。
- TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。HTTP请求客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。
- HTTP是建立在TCP之上的协议。TCP和UDP是高速公路上的“卡车”,它们携带的货物就是像HTTP,文件传输协议FTP这样的协议等。
- HTTP是一种无状态的协议。Web编程中的session,cookie就是解决这个问题的方案。TCP是有状态的,TCP状态转换是一个比较复杂。
1.2 TCP三次握手和四次挥手
- 有图有真相:https://blog.csdn.net/zzj244392657/article/details/92634754
- 三次握手
- 第一次握手**:客户端发送一个TCP的SYN标志位置1的包指明客户打算连接的服务器的端口**,以及初始序号X,保存在包头的序列号(Sequence Number)字段里,并进入SYN_SEND状态,等待服务器确认。
- 第二次握手:服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序号(Acknowledgement Number)设置为客户的ISN加1以.即X+1。此时服务器进入SYN_RECV状态。
- 第三次握手:客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1.并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段放写ISN的+1。客户端和服务器进入ESTABLISHED状态,完成三次握手。
- 四次挥手
- 第一次挥手:客户端发送一个FIN,用来关闭客户到服务器的数据传送,表示客户端没有数据要发送给服务器了。
- 第二次挥手:服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1,表示我也没有数据要发送了,可以关闭连接了。
- 第三次挥手:服务器关闭与客户端的连接,发送一个FIN给客户端,请求关闭连接,同时进CLOSE_WAIT状态。
- 第四次挥手:客户端发回ACK报文确认,并将确认序号设置为收到序号加1,同时进入TIME_WAIT状态。服务器收到ACK之后关闭连接,客户端等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,客户端也可以关闭连接了。
- 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?(确定数据发送完)
- 这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
第二节 socket编程
2.1 基本Socket实现
- 推荐博客:https://www.cnblogs.com/alex3714/articles/5830365.html
# socker_server.py
import socket
# 获得socket服务器端实例
server = socket.socket()
# 绑定ip port("IP", port)
server.bind(("0.0.0.0", 8888))
# 开始监听
server.listen()
print("等待客户端的连接...")
# 接受并建立与客户端的连接,程序在此处开始阻塞,只到有客户端连接进来...
conn, addr = server.accept()
print("新连接:", addr)
data = conn.recv(1024)
print("收到消息:", data.decode("utf-8"))
# 已经收到信息
conn.send("已经收到".encode("utf-8"))
server.close()
import socket
# 获得socket客户端实例
client = socket.socket()
client.connect(("localhost", 8888))
client.send("你好".encode("utf-8"))
# 接受服务器发送的信息
data = client.recv(1024)
print("服务器端发送的数据为:", data.decode("utf-8"))
client.close()
2.2 Socket多次交互
- 上面的实现,接收了一次客户端的data就退出。实际场景中,一个连接建立起来后,可能要进行多次往返的通信。
# socker_server.py
import socket
# 获得socket服务器端实例
server = socket.socket()
# 绑定ip port("IP", port)
server.bind(("0.0.0.0", 8888))
# 开始监听
server.listen()
while True:
print("等待客户端的连接...")
# 接受并建立与客户端的连接,程序在此处开始阻塞,只到有客户端连接进来...
conn, addr = server.accept()
print("新连接:", addr)
while True:
print("等待新指令")
data = conn.recv(1024)
if not data:
print("客户端断开了...")
break
print("收到消息:", data.decode("utf-8"))
# 已经收到信息
conn.send("已经收到".encode("utf-8"))
server.close()
import socket
# 获得socket客户端实例
client = socket.socket()
client.connect(("localhost", 8888))
while True:
msg = input("输入内容:>>").strip()
# 防止发空 卡住
if len(msg) == 0: continue
client.send(msg.encode("utf-8"))
# 接受服务器发送的信息
data = client.recv(1024)
print("接受服务器端数据为:", data.decode("utf-8"))
client.close()import socket
# 获得socket客户端实例
client = socket.socket()
client.connect(("localhost", 8888))
while True:
msg = input("输入内容:>>").strip()
# 防止发空 卡住
if len(msg) == 0: continue
client.send(msg.encode("utf-8"))
# 接受服务器发送的信息
data = client.recv(1024)
print("接受服务器端数据为:", data.decode("utf-8"))
client.close()
2.3 Socket粘包问题
- 用socket实现ssh。传输数据过大,可以先发送数据大小,在发送数据。
- 连续两次send会造成粘包,这里加上等待客户端恢复过程中阻塞。就不会产生粘包问题。
# ssh_server.py
import socket
import os
# 获得socket实例
server = socket.socket()
# 绑定ip port
server.bind(("localhost", 9999))
server.listen()
while True:
print("等待客户端的连接...")
conn, addr = server.accept()
print("新连接:", addr)
while True:
try:
data = conn.recv(1024)
# 这种方法已经不能判断客户端断开了。需要捕获异常
# if not data:
# print("客户端断开了...")
# break
print("收到命令:", data)
# py3 里socket发送的只有bytes,os.popen又只能接受str,所以要decode一下
res = os.popen(data.decode()).read()
if len(res) == 0:
res = "cmd exec success,has not output!".encode("utf-8")
# 因为中文字符占3个字节,所以传res.encode("utf-8")的长度而不是str的长度
conn.send(str(len(res.encode("utf-8"))).encode("utf-8"))
print("等待客户ack应答...")
# 等待客户端响应 解决粘包问题
client_final_ack = conn.recv(1024)
print("客户应答:", client_final_ack.decode())
print(type(res))
# 发送端也有最大数据量限制,所以这里用sendall,相当于重复循环调用conn.send,直至数据发送完毕
print(res)
conn.sendall(res.encode("utf-8"))
except ConnectionResetError as e:
print("客户端断开连接", e)
break
server.close()
# ssh_client.py
import socket
import sys
client = socket.socket()
client.connect(("localhost",9999))
while True:
msg = input(">>:").strip()
if len(msg) == 0: continue
client.send(msg.encode("utf-8"))
# 接收这条命令执行结果的大小
res_return_size = client.recv(1024)
print("getting cmd result , ", res_return_size)
total_rece_size = int(res_return_size)
print("total size:", res_return_size)
client.send("准备好接收了".encode("utf-8"))
# 已接收到的数据
received_size = 0
cmd_res = b''
# 代表还没收完
while received_size != total_rece_size:
data = client.recv(1024)
# 为什么不是直接1024,还判断len干嘛,注意,实际收到的data有可能比1024少
received_size += len(data)
cmd_res += data
else:
print("数据收完了", received_size)
print(cmd_res.decode())
client.close()
2.4 socket模拟http请求
- request 底层 —> urllib底层 —> socket
- 凡是网络之间的连接,数据库之间的连接,进程之间的连接通信网络请求实际上都是socket实现的。
- 模拟http请求
- 对url解析得到host和path(调用urllib)
- 使用socket进行连接
- 发送get请求, 然后接受数据
- 对数据进行解析处理
- 最后断开socket连接
- 一般socket编程我们都是在解决长连接的问题。不能发送一次就给他关了。
import socket
from urllib.parse import urlparse
def get_url(url):
url = urlparse(url)
host = url.netloc
path = url.path
if path == "":
path = "/"
# 建立socket连接
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# client.setblocking(False)
# 阻塞不会消耗cpu
client.connect((host, 80))
# 不停的询问连接是否建立好, 需要while循环不停的去检查状态
# 做计算任务或者再次发起其他的连接请求
client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
data = b""
while True:
d = client.recv(1024)
if d:
data += d
else:
break
data = data.decode("utf8")
# 获取网站正文 去掉头部
html_data = data.split("\r\n\r\n")[1]
print(html_data)
client.close()
if __name__ == "__main__":
import time
start_time = time.time()
get_url("https://www.baidu.com")
print(time.time()-start_time)
第三节 SocketServer
3.1 SocketServer介绍
- SocketServer是Python中用于并发处理的模块,功能强大,用法简单
- 推荐的手册:https://python3-cookbook.readthedocs.io/zh_CN/latest/c11/p02_creating_tcp_server.html
- SocketServer内有五个重要的类:
- BaseServer:这个类是模块里最基本的类,所有的类源头都来至于这个基类,但是他不是用于实例或使用的
- TCPServer:这个类用于TCP/ip的socket通讯
- UDPServer:这个类用于UDP的socket通讯
- UnixStreamServer 和UnixDatagramServer :使用的Unix - domain sockets通讯,并且只能Unix平台使用
- 使用方法:
- 创建自己的sockserver请求处理类,但必须继承sockeserver中的BaseRequestHandler类
- 必须重写父类中的handle()方法,并把你自己需要处理的逻辑写入。
- 实例化TCPServer,并且传递Server ip和上面创建的请求处理类。
- 然后你需要调用handle_request() 或者 serve_forever() 来使程序处理一个或者多个请求
- server. handle_ request() # 只处理一个请求后退出(一般不用)
- server. serve_forever() # 处理多个请求,一直挂起 每0.5秒检测一次
- 使用server_close()关闭Socket服务。
3.2 SocketServer实现
- socketserver服务器端简单实现。
import socketserver
# 定义自己的sockserver请求处理类
class MyTCPHandler(socketserver.BaseRequestHandler):
# 重写handle函数,跟客户端所有的交互都是在handle中写的
def handle(self):
while True:
try:
self.data = self.request.recv(1024).strip()
print("{} wrote:".format(self.client_address[0]))
print(self.data)
# 下面方法行不通。需要捕获异常
# if not self.data:
# print(self.client_address, "断开了")
# break
self.request.sendall(self.data.upper())
except ConnectionResetError as e:
print(self.client_address, "断开了")
break
if __name__ == "__main__":
HOST, PORT = "localhost", 9999
# 设置ip 和端口号 并把自定义的类填入
server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
server.serve_forever()
- 简单客户端实现,并测试通信
import socket
client = socket.socket()
client.connect(('localhost', 9999)) # 连接服务器
while True:
msg = input(">>:").strip()
if len(msg) == 0: continue
client.send(msg.encode()) # 发送数据
data = client.recv(1024) # 接收数据
print("返回数据:", data.decode())
client.close()
3.3 SocketServer并发
- 如果我们同时开启三个客户端。我们发现,必须等到一个客户端通信完成断开后,才去处理另外的连接请求。
- 但是我们只要把上面的TCPServer改成ThreadingTCPServer。那么每来一个请求服务端就会开启一个线程进行处理。
- ThreadingTCPServer中继承了ThreadingMixIn专门处理线程的事情,它的process_request_thread方法是具体实现。
- 当然ForkingTCPServer是开启多进程,处理请求。实现效果和多线程相同(但是开销肯定不同)
- 这里它在windows上不能用,因为它调用了os.fork()函数。
- 在liunx上可以用。
import socketserver
# 定义自己的sockserver请求处理类
class MyTCPHandler(socketserver.BaseRequestHandler):
# 重写handle函数,跟客户端所有的交互都是在handle中写的
def handle(self):
while True:
try:
self.data = self.request.recv(1024).strip()
print("{} wrote:".format(self.client_address[0]))
print(self.data)
# 下面方法行不通。需要捕获异常
# if not self.data:
# print(self.client_address, "断开了")
# break
self.request.sendall(self.data.upper())
except ConnectionResetError as e:
print(self.client_address, "断开了")
break
if __name__ == "__main__":
HOST, PORT = "localhost", 9999
# 设置ip 和端口号 并把自定义的类填入, 修改CPServer改成ThreadingTCPServer
server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
server.serve_forever()
3.4 SocketServer练习
- 开发一个支持多用户在线的FTP程序
- 用户加密认证
- 允许同时多用户登录
- 每个用户有自己的家目录 ,且只能访问自己的家目录
- 对用户进行磁盘配额,每个用户的可用空间不同
- 允许用户在ftp server上随意切换目录
- 允许用户查看当前目录下文件
- 允许上传和下载文件,保证文件一致性
- 文件传输过程中显示进度条
- 附加功能:支持文件的断点续传
- 推荐博客:https://cloud.tencent.com/developer/article/1569649
- 暂时不写,练手。