第十课 python进阶socket编程

第十课 python进阶socket编程

tags:

  • Docker
  • 慕课网

categories:

  • TCP/UDP
  • HTTP
  • SOCKET
  • 网络编程

文章目录

  • 第十课 python进阶socket编程
    • 第一节 协议知识
      • 1.1 协议知识
      • 1.2 TCP三次握手和四次挥手
    • 第二节 socket编程
      • 2.1 基本Socket实现
      • 2.2 Socket多次交互
      • 2.3 Socket粘包问题
      • 2.4 socket模拟http请求
    • 第三节 SocketServer
      • 3.1 SocketServer介绍
      • 3.2 SocketServer实现
      • 3.3 SocketServer并发
      • 3.4 SocketServer练习

第一节 协议知识

1.1 协议知识

  1. 推荐书籍了解TCP/IP协议,TCP/IP详解卷1 :协议(共三卷)。
  2. 操作系统给我们提供了一个socket接口, 用于和TCP和UDP打交道, socket它不属于任何协议
  3. HTTP协议和TCP协议的区别点(面试题):
    • TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。
    • TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。HTTP请求客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。
    • HTTP是建立在TCP之上的协议。TCP和UDP是高速公路上的“卡车”,它们携带的货物就是像HTTP,文件传输协议FTP这样的协议等。
    • HTTP是一种无状态的协议。Web编程中的session,cookie就是解决这个问题的方案。TCP是有状态的,TCP状态转换是一个比较复杂。
      第十课 python进阶socket编程_第1张图片

1.2 TCP三次握手和四次挥手

  1. 有图有真相:https://blog.csdn.net/zzj244392657/article/details/92634754
  2. 三次握手
    • 第一次握手**:客户端发送一个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状态,完成三次握手。
  3. 四次挥手
    • 第一次挥手:客户端发送一个FIN,用来关闭客户到服务器的数据传送,表示客户端没有数据要发送给服务器了。
    • 第二次挥手:服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1,表示我也没有数据要发送了,可以关闭连接了。
    • 第三次挥手:服务器关闭与客户端的连接,发送一个FIN给客户端请求关闭连接,同时进CLOSE_WAIT状态。
    • 第四次挥手:客户端发回ACK报文确认,并将确认序号设置为收到序号加1,同时进入TIME_WAIT状态。服务器收到ACK之后关闭连接,客户端等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,客户端也可以关闭连接了。
  4. 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?(确定数据发送完)
    • 这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

第二节 socket编程

第十课 python进阶socket编程_第2张图片

2.1 基本Socket实现

  1. 推荐博客: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多次交互

  1. 上面的实现,接收了一次客户端的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粘包问题

  1. 用socket实现ssh。传输数据过大,可以先发送数据大小,在发送数据。
  2. 连续两次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请求

  1. request 底层 —> urllib底层 —> socket
  2. 凡是网络之间的连接,数据库之间的连接,进程之间的连接通信网络请求实际上都是socket实现的。
  3. 模拟http请求
    • 对url解析得到host和path(调用urllib)
    • 使用socket进行连接
    • 发送get请求, 然后接受数据
    • 对数据进行解析处理
    • 最后断开socket连接
  4. 一般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介绍

  1. SocketServer是Python中用于并发处理的模块,功能强大,用法简单
  2. 推荐的手册:https://python3-cookbook.readthedocs.io/zh_CN/latest/c11/p02_creating_tcp_server.html
  3. SocketServer内有五个重要的类:
  • BaseServer:这个类是模块里最基本的类,所有的类源头都来至于这个基类,但是他不是用于实例或使用的
  • TCPServer:这个类用于TCP/ip的socket通讯
  • UDPServer:这个类用于UDP的socket通讯
  • UnixStreamServer 和UnixDatagramServer :使用的Unix - domain sockets通讯,并且只能Unix平台使用
    第十课 python进阶socket编程_第3张图片
  1. 使用方法:
  • 创建自己的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实现

  1. 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()
  1. 简单客户端实现,并测试通信
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并发

  1. 如果我们同时开启三个客户端。我们发现,必须等到一个客户端通信完成断开后,才去处理另外的连接请求。
  2. 但是我们只要把上面的TCPServer改成ThreadingTCPServer。那么每来一个请求服务端就会开启一个线程进行处理。
  3. ThreadingTCPServer中继承了ThreadingMixIn专门处理线程的事情,它的process_request_thread方法是具体实现。
  4. 当然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练习

  1. 开发一个支持多用户在线的FTP程序
    1. 用户加密认证
    2. 允许同时多用户登录
    3. 每个用户有自己的家目录 ,且只能访问自己的家目录
    4. 对用户进行磁盘配额,每个用户的可用空间不同
    5. 允许用户在ftp server上随意切换目录
    6. 允许用户查看当前目录下文件
    7. 允许上传和下载文件,保证文件一致性
    8. 文件传输过程中显示进度条
    9. 附加功能:支持文件的断点续传
  2. 推荐博客:https://cloud.tencent.com/developer/article/1569649
  3. 暂时不写,练手。

你可能感兴趣的:(python的语言总结学习)