Python Socket编程

news2024/11/13 10:12:41

Python Socket编程

文章目录

  • Python Socket编程
    • 1. 弄懂HTTP、Socket、TCP这几个概念
      • 五层网络模型
    • 2. client和server实现通信
      • Socket编程模式指南
      • 代码实现
    • 3. socket实现聊天和多用户连接
    • 4. socket模拟http请求
    • 5. socket使用I/O多路复用模式模拟http请求

1. 弄懂HTTP、Socket、TCP这几个概念

  • 整个计算机网络都是有协议组成的
  • 每一个应用程序只能占用1个端口

五层网络模型

  • OSI的标准网络模型是有7层的。应用层和传输层下面还有个表示层、还有个会话层。
  • 应用层中最常用的库就是requests这个第三方的包,走的就是HTTP协议;浏览器也是实现了HTTP协议。
  • HTTP协议是构建与TCP/IP协议之上的。
  • 例如我们要写一个聊天工具,这就不属于这里的应用层任何协议,那么我们如何与TCP/UDP这些底层协议打交道呢?如果我们要手动实现协议与TCP/UDP打交道是非常繁琐的,所以我们不会去实现协议与TCP/UDP打交道,这些与TCP/UDP协议打交道的活都是操作系统帮我们提供的。
  • 操作系统会提供给我们Socket,Socket我们可以理解为是一个API,Socket不属于任何协议,我们通过Socket就可以直接和TCP/UDP传输层打交道,因为聊天工具属于新的应用,而这里的应用层协议都无法满足我们的需求,所以我们就只需要使用操作系统提供的Socket编程即可,Windows、Linux、macOS等操作系统都给我们提供了Socket接口。
  • 所以我们只需要通过Socket就可以和底层的这些传输层、网络层、数据链路层、物理层进行打交道。
  • Socket在整个网络模型中,可以让我们直接脱离应用层而自动帮我们与底层协议打交道,这样我们就可以完成很多的功能哦。
  • Socket本身不属于计算机网络模型。Socket是用来连接我们的应用和TCP层,Socket使得我们自己的应用可以直接和TCP打交道,这样我们就可以实现自己的应用层协议。而这个我们自己定义的应用层协议就是使用Socket与TCP打交道,我们自己定义的应用层协议是和HTTP协议是同一层级的应用层协议。

在这里插入图片描述

2. client和server实现通信

Socket编程模式指南

  • 左侧就是server端:
    • 特点就是随时处于监听的状态,也就是服务的状态,因为不知道客户端什么时候会发送连接过来,所以server必须时刻处于等待状态。
    • accept():阻塞等待连接请求
    • recv():获取数据
    • send():服务端可以发送数据给客户端,这里就与HTTP有很大的区别了,即只要连接不断开,就可以一直向客户端发送数据。而HTTP请求一般就是发送请求等待响应返回数据后就停止了。
    • close():断开连接
  • 右侧就是client端

在这里插入图片描述

代码实现

# socket_server.py
import socket

# 新建server
# 参数1:是指明使用的类型,AF_INET就是指明使用IPv4,常用还有AF_INET6就是IPv6,AF_IPX是做linux系统上的进程间通信的
# 参数2:指明这个类型对应的协议,SOCK_STREAM就是TCP,SOCK_DGRAM就是UDP
# socket.AF_INET: 基于网络的socket通信,使用IPv4
# socket.SOCK_STREAM: 基于TCP的流式socket通信
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定IP地址和端口,必须传入一个tuple
# 如果你写127.0.0.1那么你通过局域网就访问不到了
server.bind(("0.0.0.0", 8000))

# 监听
server.listen()

# 等待连接
# 返回表示连接的新套接字以及客户端的地址,sock是一个新的套接字对象,addr是客户端的地址
sock, addr = server.accept()

# 接收数据,即获取从客户端发送的数据,指定最大的接收长度为1024字节(即一次获取1KB的数据),recv方法是一个阻塞方法,如果没有数据接收就会阻塞程序执行
data = sock.recv(1024)  # data是一个bytes对象
print(data.decode("utf8"))  # 把bytes对象转换成字符串

# 发送数据给客户端
sock.send("Hello client".encode("utf8"))

# 关闭连接
# server.close()是关闭服务器套接字,停止接受新的客户端连接,sock.close()是关闭一个已经建立的客户端连接。
# server.close() # 关闭连接,真正的server一般是不会close的
# sock.close() # 关闭套接字,表示和客户端的连接也中断了
# socket_client.py
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 建立连接
# 参数是一个tuple,包含地址和端口号,这里是本机的地址,所以是127.0.0.1,端口号是8000
client.connect(("127.0.0.1", 8000))

# 发送数据,要求发送的数据必须是bytes格式
client.send("Hello Server".encode("utf8"))

# 接收数据
data = client.recv(1024)
print(data.decode("utf8"))

# 关闭连接,一般是先关闭发送数据的一方,然后再关闭接收数据的一方,这是一个好习惯
client.close()

先启动server端,再启动client端,发现server端打印了“Hello Server”,而后client端打印了“Hello client”,最后两者程序都退出了。

3. socket实现聊天和多用户连接

# socket_server.py
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 8000))
server.listen()
sock, addr = server.accept()

while True:
    data = sock.recv(1024)
    print(data.decode("utf8"))
    re_data = input() # 使用终端进行发送数据的输入
    sock.send(re_data.encode("utf8"))
# socket_client.py
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8000))
while True:
    re_data = input()
    client.send(re_data.encode("utf8"))
    data = client.recv(1024)
    print(data.decode("utf8"))

这样的代码有个问题:

  • server只能连接一个client,因为当客户端连接后就在while循环中出不来了。可以使用线程,每一个socket都做一个线程,在线程里面去循环,那么我们主线程就可以不停的接收其他client的连接请求
# socket_server.py
import socket
import threading

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 8000))
server.listen()


def handle_sock(sock, addr):
    while True:
        data = sock.recv(1024)
        print(data.decode("utf8"))
        re_data = input()
        sock.send(re_data.encode("utf8"))


while True:
    sock, addr = server.accept()
    # 用线程去处理新接收的连接(用户)
    client_thread = threading.Thread(target=handle_sock, args=(sock, addr))
    client_thread.start()

4. socket模拟http请求

  • requests包是基于urllib完成的,而urllib是基于socket完成的。所以说最底层就是socket的。
  • 凡事在网络相关的连接中,比如数据库连接、进程之间的通信等,这些网络请求最底层都是用socket完成的
  • 再次强调socket是操作系统所提供的接口,只是说不同的开发语言会根据不同的操作系统所提供的socket,再次封装成接口而已。
  • 下面我们将实现通过socket 去完成urllib当中的去获取请求的实现。
import socket

# 用来解析URL, 会将URL解析为下面6个部分
# <scheme>://<netloc>/<path>;<params>?<query>#<fragment>
from urllib.parse import urlparse


def get_url(url):
    """通过socket请求html"""
    url = urlparse(url)  # 解析url
    host = url.netloc  # 获取域名
    path = url.path  # 获取域名后的子路径
    if path == "":
        path = "/"  # 如果直接请求的域名则把子路径改成/

    # 建立socket连接
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((host, 80))  # HTTP是80端口

    # 发送数据,注意这里的数据的格式是要按照HTTP协议的规范来写
    client.send(f"GET {path} HTTP/1.1\r\nHost:{host}\r\nConnection:close\r\n\r\n".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]  # 丢弃响应头,只取HTML
    print(html_data)
    client.close()


if __name__ == "__main__":
    get_url("http://www.baidu.com")

  • 注意,这里每次发送请求都会先去建立socket连接,然后close。下次如果我们再发生请求的时候还要这样,这就比较耗时。这里只是体验socket编程的代码示例。而正常我们是应该保持长连接的。
  • socket编程是理解异步IO、协程的基础。是必须要掌握的。
  • socket编程过于底层,所以需要手动完成的功能比较多。

5. socket使用I/O多路复用模式模拟http请求

  • 这里的代码了解即可。采用了 “select或者epoll + 回调 + 事件循环” 的编程模式
  • 这样写的好处就是并发行高,而且这里没有线程的切换,都在一个线程里面执行。而且省内存,因为1个线程消耗的内存是远远高于回调函数的模式的。
  • 回调函数其实就是一个指向函数的句柄而已。
  • 这里的代码就很好的展示了协程的核心原理(事件循环 + I/O多路复用)。
  • 这种方式其实就是当我们代码进行下一步操作的时候是让 事件循环loop 去驱动我们的回调函数
  • 缺点会让代码割裂的四分五裂。
# socket_select_http.py
"""
通过I/O多路复用模型中的select模式或者epoll模式实现http请求
"""

import socket

# selectors包的作用是根据不同的操作系统自动选择最佳的I/O多路复用模型
# 我们不要使用select包,而是要使用基于select包再次封装的selectors包
# DefaultSelector会自动帮我们根据代码运行的操作系统去选择使用poll或者epoll
# 在Windows下就是select,在Linux下就是epoll
# 而且DefaultSelector还为我们提供了注册机制
from selectors import EVENT_READ, EVENT_WRITE, DefaultSelector
from urllib.parse import urlparse

selector = DefaultSelector()  # 创建一个selector对象(选择器)


class Fetcher:
    def __init__(self):
        self.host = ""  # 域名
        self.path = ""  # 路径
        self.data = b""  # 存储返回的数据
        self.client = None  # socket对象

    def get_url(self, url):
        """
        通过socket请求html
        """
        url = urlparse(url)  # 解析url
        self.host = url.netloc  # 获取域名
        self.path = url.path  # 获取域名后的子路径
        self.data = b""  # 存储返回的数据
        if self.path == "":
            self.path = "/"  # 如果直接请求的域名则把子路径改成/

        # 创建socket对象<class 'socket.socket'>, socket.AF_INET表示ipv4, socket.SOCK_STREAM表示TCP
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 将socket设置为阻塞(true)或非阻塞(false), 默认为阻塞
        self.client.setblocking(False)  # 使用非阻塞
        try:
            # 连接
            self.client.connect((self.host, 80))  # 注意这里是非阻塞的,所以不会等待连接成功,而是会立刻返回并继续执行后续代码,所以会抛出BlockingIOError异常
        except BlockingIOError as e:
            pass  # 抛出BlockingIOError异常是合理的,虽然抛出了异常,但是与host的建立连接请求已经发出去了。

        # 将socket注册到selector中,监听socket是否是可写的状态
        # 参数1: socket的文件描述符
        # 参数2: 事件,有EVENT_WRITE和EVENT_READ
        # 参数3: 回调函数,即当socket的状态变为可写的时候,就会调用此回调函数
        selector.register(self.client.fileno(), EVENT_WRITE, self.connected)  # 使用事件监听, 等待连接成功, 然后再执行后面的代码
        # 注意这后面不能再写代码了,如果写了代码就是阻塞式IO了,所以我们才要使用事件监听,采用回调的方式来处理

    def connected(self, key):
        """连接成功的回调函数,当socket的状态变为可写的时候,就会调用此回调函数,进行发送数据(发送HTTP请求)"""
        # 注销掉监控的事件,即取消注册,因为我们已经连接成功了,所以就不需要再监听socket是否是可写的状态了,所以要注销掉,否则会一直监听下去
        selector.unregister(key.fd)  # fd是file descriptor文件描述符
        # 发送数据,这里不用try...except...,是因为我们使用事件监听,当回调此方法的时候连接的状态一定是就绪的状态
        self.client.send(f"GET {self.path} HTTP/1.1\r\nHost:{self.host}\r\nConnection:close\r\n\r\n".encode("utf8"))
        # 注册,即使用事件监听,因为我们要接收服务器返回的响应数据,所以要再次监听socket是否是可读的状态,当socket的状态变为可读的时候,就会调用此回调readable方法接收数据,然后打印出来
        selector.register(self.client.fileno(), EVENT_READ, self.readable)

    def readable(self, key):
        """接收数据(接收HTTP响应),当socket的状态变为可读的时候,就会调用此回调函数,进行接收数据(接收HTTP响应),然后打印出来"""
        d = self.client.recv(1024)  # 接收数据
        if d:  # 当数据没有读完的时候,继续读取
            self.data += d
        else:  # 当数据读完的时候,就注销掉监控的事件,即取消注册,因为我们已经接收完了,所以就不需要再监听socket是否是可读的状态了,所以要注销掉,否则会一直监听下去
            selector.unregister(key.fd)
            data = self.data.decode("utf8")
            html_data = data.split("\r\n\r\n")[1]
            print(html_data)
            self.client.close()


def loop():
    """回调+事件循环+select(poll/epoll)
    驱动整个程序运行,可以理解为心脏,这样不会去阻塞建立连接、等待请求等IO操作
    """
    # 事件循环,不停的请求socket的状态并调用对应的回调函数
    # 所以这是个主循环
    # 1.select本身是不支持register模式
    # 2.socket状态变化以后的回调是由程序员完成的
    while True:
        # 注意在Windows下使用的是select模式,而select(r,w,w,timeout)的参数如果为空列表会抛出OSError异常
        # 而在Linux或者macOS下会使用epoll模式,就不会抛出OSError异常,会一直阻塞在这里
        # 不停的向操作系统请求有哪些socket已经准备好了
        ready = selector.select()  # 返回的是SelectorKey数据类型,是namedtuple类型
        for key, mask in ready:
            # key是一个SelectorKey类型的对象,包含了文件描述符fd、事件event、回调函数data等属性
            # mask是一个事件的遮罩,表示事件是可读的还是可写的,EVENT_READ或EVENT_WRITE,取决于我们注册的时候监听的是可读还是可写,即key.events
            call_back = key.data  # 获取回调函数,<bound method Fetcher.connected of <__main__.Fetcher object at 0x1025d55b0>>
            call_back(key)  # 执行回调函数,即调用connected方法


if __name__ == "__main__":
    fetcher = Fetcher()
    fetcher.get_url("http://www.baidu.com")
    loop()

下面是解决Windows下使用的是select模式抛出OSError异常的问题

import socket

from selectors import EVENT_READ, EVENT_WRITE, DefaultSelector
from urllib.parse import urlparse

selector = DefaultSelector()

urls = ["http://www.baidu.com"]
stop = False


class Fetcher:
    def get_url(self, url):
        self.spider_url = url
        url = urlparse(url)
        self.host = url.netloc
        self.path = url.path
        self.data = b""
        if self.path == "":
            self.path = "/"

        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.setblocking(False)
        try:
            self.client.connect((self.host, 80))
        except BlockingIOError as e:
            pass

        selector.register(self.client.fileno(), EVENT_WRITE, self.connected)

    def connected(self, key):
        selector.unregister(key.fd)
        self.client.send(f"GET {self.path} HTTP/1.1\r\nHost:{self.host}\r\nConnection:close\r\n\r\n".encode("utf8"))
        selector.register(self.client.fileno(), EVENT_READ, self.readable)

    def readable(self, key):
        d = self.client.recv(1024)
        if d:
            self.data += d
        else:
            selector.unregister(key.fd)
            data = self.data.decode("utf8")
            html_data = data.split("\r\n\r\n")[1]
            print(html_data)
            self.client.close()
            urls.remove(self.spider_url)
            if not urls:
                global stop
                stop = True


def loop():
    while not stop:
        ready = selector.select()
        for key, mask in ready:
            call_back = key.data
            call_back(key)


if __name__ == "__main__":
    fetcher = Fetcher()
    fetcher.get_url("http://www.baidu.com")
    loop()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1312319.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于node.js和Vue游戏商城系统设计

游戏商城系统是一个在线平台&#xff0c;旨在为用户提供购买和销售电子游戏、游戏内物品和其他相关服务。游戏商城系统的主要目标是为玩家提供一个方便、快捷、安全的购物环境&#xff0c;同时也为游戏开发商和发行商提供一个有效的销售渠道。通过这个平台&#xff0c;玩家可以…

【Spring Boot 】Spring Boot 常用配置总结

文章目录 前言1.多环境配置application.propertiesapplication.yaml 2.常用配置3.配置读取4.自定义配置 前言 在涉及项目开发时&#xff0c;通常我们会灵活地把一些配置项集中在一起&#xff0c;如果你的项目不是很大的情况下&#xff0c;那么通过配置文件集中不失为一个很好的…

Android studio 离线配置gradle

Gradle Distributions Gradle Distributions 查看gradle 文件夹下 gradle-wrapper.properties文件中的distributionUrl 版本号 然后在上边网站下载对应需要的gradle对应版本 下载后复制到 gradle wrapper文件下&#xff0c;同时修改 distributionUrl 指向本地文件 然后同步就…

ChatGPT热门项目

1.智能GPT 项目地址&#xff1a;智能GPT&#xff1a;你只要提供OpenAI的API Key&#xff0c;那么它就可以根据你设定的目标&#xff0c;采用Google搜索、浏览网站、执行脚本等方式 主要语言&#xff1a;Python 推荐理由&#xff1a;这是由开发者Significant Gravitas推出的项目…

每天一点python——day94

#每天一点Python——94 #面向对象的三大特征——封装 封装&#xff1a;隐藏内部细节&#xff0c;对外提供操作方式。【提高程序的安全性】 继承&#xff1a;在函数调用时&#xff0c;使用’形参名称值‘的方式进行传参&#xff0c;传递参数的顺序可以与定义时参数顺序不同【提高…

8086汇编程序:多位数码管动态扫描显示2023

实验目的 1.掌握8255A和8086CPU接口方法&#xff0c;掌握8255A的工作方式和编程原理。 2.掌握多位数码管动态扫描显示的工作原理和编程方法。 实验内容 设计8255A 与多位共阴极数码管接口电路&#xff0c;实现在四位LED共阴极数码管上显示数字2023 8255A工作在方式0基本输入输出…

做数据分析为何要学统计学(10)——什么是回归分析

​回归分析&#xff08;regression analysis)是量化两种或两种以上因素/变量间相互依赖关系的统计分析方法。回归分析根据因素的数量&#xff0c;分为一元回归和多元回归分析&#xff1b;按因素之间依赖关系的复杂程度&#xff0c;可分为线性回归分析和非线性回归分析。我们通过…

跑代码中遇到的错误合集(持续更新)

1.TypeError: dropout(): argument ‘input‘ (position 1) must be Tensor, not str 原因&#xff1a;dropout函数接收到的参数是一个字典类型(需手动设置其不要返回字典类型) 解决步骤: 1.根据代码定位到dropout函数 2.定位到函数中的参数 3.对给dropout函数参数赋值的函数的…

【C++练级之路】【Lv.4】类和对象(下)(初始化列表,友元,static成员,编译器的优化)

目录 一、再谈构造函数1.1 构造函数体赋值1.2 初始化列表1.3 explicit关键字 二、static成员2.1 概念2.2 特性 三、友元3.1 引入3.2 友元函数3.2.1 概念3.2.2 特性 3.3 友元类3.3.1 概念3.3.2 特性 四、内部类4.1 概念4.2 特性 五、匿名对象六、编译器的优化6.1 传参优化6.1.1 …

使用python做一个实用的工具,便捷的定位和操作文档

环境准备&#xff1a;totalcommander 我用的totalcommander&#xff0c;python脚本的快捷键也是在totalcommander中注册的&#xff0c;所以需要先在TC界面下&#xff0c;才能通过预定义的快捷键唤起 python 脚本。以下是我的 totalCommander界面&#xff0c;我定义的快捷键是 …

jrebel debug 启动不起来

idea更新之后jrebel debug模式启动不起来。 将下面的设置取消之后就可以了&#xff0c;希望能帮到你们… 被卡了两天… jrebel信息。 idea IntelliJ IDEA 2023.3.1 (Ultimate Edition) Build #IU-233.11799.300, built on December 12, 2023 Licensed to Alexandra Martin…

VHDL实验:基于有限状态机实现秒表

题目要求&#xff1a; 利用有限状态机实现实现一个具有启动、停止、清零功能的秒表&#xff0c;显示格式&#xff1a;分&#xff1a;秒&#xff1a;十分秒。启动、停止、清零由一个按键控制&#xff0c;按键按下时&#xff0c;功能按启动、停止、清零顺序循环。 思路分析&…

搭配环境—Python解释器

对于一些库&#xff0c;需要创建虚拟环境&#xff08;就是给你电脑创建一个虚拟的地方来存&#xff0c;这个虚拟的地方有很多&#xff0c;需要自己找&#xff09; 对于人脸识别项目存在 使用的这个解释器&#xff0c;其他解释器可以去envs找找

智能高效的Go开发工具GoLand v2023.3发布,支持AI辅助编码!

GoLand 使 Go 代码的阅读、编写和更改变得非常容易。即时错误检测和修复建议&#xff0c;通过一步撤消快速安全重构&#xff0c;智能代码完成&#xff0c;死代码检测和文档提示帮助所有 Go 开发人员&#xff0c;从新手到经验丰富的专业人士&#xff0c;创建快速、高效、和可靠的…

黑豹程序员-原生JS拖动div到任何地方-自定义布局

效果图 代码html <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html xmlns"http://www.w3.org/1999/xhtml"> <head> <meta http-equiv"Content-Type" content"text/html; charsetutf-8" /…

【Docker】ES、Kibana及IK安装配置

目录 一.单节点安装部署 1.版本选择 2.推荐及总结 ​3.官网下载地址 4.创建网络 5.拉取镜像 6.创建文件夹 7.运行docker命令 二、安装kibana 1.安装kibana 2.浏览器访问 3.国际化 三、Elasticsearch查询 1.数据插入&#xff1a;POST或PUT 2.数据查询GET 3.分词…

使用java调用python批处理将pdf转为图片

你可以使用Java中的ProcessBuilder来调用Python脚本&#xff0c;并将PDF转换为图片。以下是一个简单的Java代码示例&#xff0c;假设你的Python脚本名为pdf2img.py&#xff1a; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader…

Linux arm架构下构建Electron安装包

上篇文章我们介绍 Electron 基本的运行开发与 windows 安装包构建简单流程&#xff0c;这篇文章我们从零到一构建 Linux arm 架构下安装包&#xff0c;实际上 Linux arm 的构建流程&#xff0c;同样适用于 Linux x86 环境&#xff0c;只不过需要各自的环境依赖&#xff0c;Linu…

图文教程:从0开始安装stable-diffusion

现在AI绘画还是挺火&#xff0c;Midjourney虽然不错&#xff0c;但是对于我来说还是挺贵的。今天我就来安一下开源的AI绘画stable-diffusion,它的缺点就是对电脑的要求比较高&#xff0c;尤其是显卡。 话不多说开搞。 访问sd的github&#xff0c;https://github.com/AUTOMATIC…

西南科技大学数字电子技术实验四(基本触发器逻辑功能测试及FPGA的实现)预习报告

一、计算/设计过程 说明:本实验是验证性实验,计算预测验证结果。是设计性实验一定要从系统指标计算出元件参数过程,越详细越好。用公式输入法完成相关公式内容,不得贴手写图片。(注意:从抽象公式直接得出结果,不得分,页数可根据内容调整) (1)D触发器 特征方程: Q…