python网络编程

1.套接字

  套接字(Socket) 是实现网络编程进行数据传输的一种技术手段,网络上各种各样的网络服务大多都是基于Socket 来完成通信的。socket是传输层提供给应用层的编程接口。所以,套接字socket编程分为TCPUDP两类。在python中,通过Python套接字编程模块:import socket提供socket。通信时服务端需要建立一个socket,客户端也需要一个socket。这里借用网上一个图。

2.TCP Transmission Control Protocol ,传输控制协议)

传输特征 :

  1. 面向连接:TCP在通信双方之间建立一条连接,并且在连接保持期间维护这个连接。这种连接是可靠的,即传输过程中不会丢失数据包或发生错误。
  2. 可靠性:TCP通过使用确认、时间重传、流量控制和拥塞控制等机制来保证数据的可靠传输。确认机制确保接收方能够确认已接收的数据;时间重传机制在超时时重传丢失的数据;流量控制机制用来控制发送方的发送速率,以防止接收方无法及时处理数据;拥塞控制机制用来控制网络中的拥塞情况,以避免网络过载。
  3. 面向字节流:TCP将数据视为字节流进行传输,不区分其中的消息边界。发送方和接收方能够根据需要将字节流切分成自己的消息。
  4. 全双工通信:TCP连接中的双方可以同时发送和接收数据。
  5. 流量控制:TCP通过使用滑动窗口机制进行流量控制,即发送方根据接收方的处理能力来调整发送速率,以避免数据的丢失和阻塞。
  6. 拥塞控制:TCP通过使用拥塞窗口控制机制来调整发送速率,以避免网络拥塞。当网络中发生拥塞时,TCP会减小拥塞窗口,从而减少发送方的发送量。
  7. 头部信息:TCP在每个数据包的头部携带了一些必要的信息,如源端口号、目的端口号、序列号、确认号等,用来辅助连接的建立、数据的确认和流量控制等功能。
  8. 可靠性影响的效率:由于TCP保证了数据的可靠性,需要进行确认、重传等操作,因此会消耗额外的网络带宽和处理时间,使得数据传输的效率相对较低。

3. TCP服务端的创建

TCP服务端:

  创建套接字

  sock=socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)

  功能:创建套接字

  参数:family 网络地址类型 AF_INET表示ipv4

  type: 套接字类型, SOCK_STREAM 表示tcp套接字

  返回值: 套接字对象

  绑定地址

    本地地址 : '127.0.0.1'

    网络地址 : '172.40.91.185' (通过ifconfig查看)

    自动获取地址: '0.0.0.0'

    sock.bind(addr)

    功能: 绑定本机网络地址

    参数: 二元元组 (ip,port) ('0.0.0.0',8888)

  设置监听

  sock.listen()

  功能 : 将套接字设置为监听套接字

  处理客户端连接请求

  conn,addr = sock.accept()

  功能: 阻塞等待处理客户端请求

  返回值: conn 客户端连接套接字

  addr 连接的客户端地址

  消息收发

  data = conn.recv(buffersize)

  功能 : 接受客户端消息

  参数 :每次最多接收消息的大小

  返回值: 接收到的内容

  n = conn.send(data)

  功能 : 发送消息

  参数 :要发送的内容 bytes格式

  返回值: 发送的字节数

  关闭套接字

  sock.close()

  功能:关闭套接字

下面是服务端的测试程序。

# 创建tcp套接字
from socket import socket, AF_INET, SOCK_STREAM
tcp_socket = socket(AF_INET,SOCK_STREAM)
# 绑定地址
tcp_socket.bind(("0.0.0.0",8888))
# 设置为监听套接字
tcp_socket.listen(5)
# 等待客户端连接
print("Waiting for connect...")
connfd,addr = tcp_socket.accept()
print("Connect from",addr)
data = connfd.recv(1024)
print("收到:",data.decode())
connfd.send(b"Thanks/")
# 关闭套接字
connfd.close()
tcp_socket.close()

  这里用TCP调试助手测试一下:

  connfd.send(b"Thanks/")发送的是Thanks/的字节序列,图中框住的红色编码部分,将十六进制转ASCII后就是Thanks/了。

4. TCP客户端的创建

  这里直接给出测试程序:

from socket import *
ADDR = ("127.0.0.1",8888)#定义一个名为 ADDR 的元组,其中包含服务器的地址和端口号。
tcp_socket = socket()#创建一个 TCP 套接字对象,没给参数默认TCP
tcp_socket.connect(ADDR)#通过调用 connect() 方法,连接到指定的服务器地址和端口。
# 发送接收消息
msg = input(">>")
tcp_socket.send(msg.encode())
data = tcp_socket.recv(1024)
print("From server:",data.decode())
tcp_socket.close()

 

 5. TCP通信注意问题

  5.1粘包问题

  tcp以字节流方式进行数据传输,在接收时不区分消息边界。具体产生的原因有下面几种情况。

  产生原因:

  1. 缓冲区大小限制:在发送方和接收方的缓冲区有一定的大小限制,当连续发送的数据量超过缓冲区大小时,可能会导致粘包现象的发生。

  2. 数据传输延迟:发送方可能会在短时间内发送多个较小的数据包,而接收方可能会在同一时间间隔内接收多个数据包,从而导致数据粘连。

  3. 数据包的大小与MTU不匹配:当发送方发送的数据包大小小于MTU(最大传输单元)时,网络传输层将多个小的数据包合并成一个较大的数据包发送,接收方可能会将多个数据包解析成一个,导致粘包。
  4. 优化算法:TCP协议中的优化算法(如Nagle算法和延迟确认算法)会对数据包进行合并或延迟确认,从而提高网络传输的效率,但也会增加粘包的可能性。

  解决办法:

  1. 设置合适的缓冲区大小,确保缓冲区能够容纳发送方发送的数据量,避免因缓冲区溢出而导致粘包。
  2. 合适地控制发送方的发送速率,避免在较短时间内发送多个较小的数据包。
  3. 设置合适的优化算法参数,根据实际需求进行优化算法的配置,尽可能减少粘包的发生。
  4. 在应用层对数据包进行合理的分包和组包操作,确保接收方能够正确地解析出原始的数据包。
  5. 使用专门处理粘包问题的协议,如WebSocket等,这类协议在传输上有自己的解决粘包问题的机制。
  6. 消息格式化处理,如人为的添加消息边界,用作消息之间的分割以及控制发送的速度。

  下图为粘包的三种情况:

   5.2 粘包问题例子

# 服务器端代码
import socket

def handle_client(client_socket):
    while True:
        data = client_socket.recv(1024)  # 每次接收最多1024字节的数据
        if not data:
            break
        # 对接收到的数据进行处理,此处省略
        print("Received data:", data.decode())
    client_socket.close()

def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('localhost', 8888))
    server_socket.listen(5)
    while True:
        client_socket, addr = server_socket.accept()
        handle_client(client_socket)

    server_socket.close()

start_server()
# 客户端代码
import socket
# from time import sleep

def send_data(client_socket, data):
    client_socket.sendall(data.encode())#使用encode编码将字符串转换为字节序列,也就是二进制字节数据

def start_client():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(('localhost', 8888))
    # 发送多个数据包
    send_data(client_socket, "Hello")
    send_data(client_socket, "World")
    send_data(client_socket, "!")
    client_socket.close()

start_client()

  上面这个例子中,客户端与服务器建立连接后,使用三个send_data(xxx)向服务器发送了三个字符串。反复运行了程序多次,发现这种情况下出现第一种情况占多数,也就是很大概率发生粘包,客户端发送的太快了,服务端从自己的缓冲区去取数据时,Hello、World、!都在缓冲区了。此时打印输出就是第一种情况。至于最后一种情况没有发生粘包,运行了好多次才没发生粘包。粘包不粘包与客户端当时发送的速度,服务器处理的速度相关。

   5.3 解决粘包问题

  针对上面这个简单的例子,解决其粘包问题。由于我们发送的是Hello、World、!,最长的字节长度为5.我们可以设置接受的缓冲区数据最大为5字节。也就是将data = client_socket.recv(1024)改为data = client_socket.recv(5)。再次反复运行程序结果如下。也就是解决了粘包问题了。实时上,这些包仍然按顺序排列在缓冲区,可以看作仍然粘包了,我们使用client_socket.recv(5)后,每次只是从缓冲区拿了最前面的五个字节数据,所以可以顺利换行输出。针对我们发送的Hello、World、!。我们也可以考虑设置缓冲区大小为5个字节,我没有去试过,但是猜想估计是可以的,只要Hello发送到服务器高速缓冲区了,恰好占满了整个缓冲区,客户端此时就会阻塞,等缓冲区被取走后,再发送World等。

   除了上面的方法,还可以设置延时发送,这种方法往往更通用,延时是为了让服务端有足够的时间取获取缓冲区的数据。更改5.2中的代码,添加sleep(0.01)方法如下所示,使用该方法后,不再发生粘包问题。

 6. TCP细节问题

  1. tcp连接中,当一端退出,另一端调用recv时会返回一个空字节串。
  2. tcp连接中如果一端已经不存在,仍然试图通过send向其发送数据则会产生BrokenPipeError
  3. 一个服务端可以同时连接多个客户端,也能够重复被连接。

7. UDP介绍及其使用

  7.1 UDP简介与TCP对比

  UDP是一种无连接的协议。发送方直接发送数据包给接收方,相对于TCP(需要建立连接)而言,UDP不需要建立连接。因此,UDP的传输速度较快,但不保证数据传输的可靠性。UDP不提供可靠性保证。它只是简单地发送数据包给接收方,不进行确认和重传,如果在传输过程中有数据包丢失或出错,UDP不会进行纠正,因此可能导致数据丢失或乱序。UDP适用于对传输速度和实时性要求较高,且允许数据丢失的场景,如实时游戏、视频和音频流传输等。

  7.2 UDP网络编程

  先看个例子。

"""
udp 服务器端
"""
from socket import *

# 1. 创建套接字
udp_sock = socket(AF_INET, SOCK_DGRAM)#指定数据报套阶字为UDP套阶字。
# 2. 绑定地址
udp_sock.bind(('127.0.0.1', 8888))
# 3. 收发消息
while True:
    data, addr = udp_sock.recvfrom(128)
    print("from:", addr, "content:",
          data.decode())
    # 发送 返回值: 发了多少个字节
    n = udp_sock.sendto(b'Thanks', addr)
    print("发了%s个字节" % n)
# 4. 关闭
udp_sock.close()

 

"""
udp 的客户端基础示例
"""
from socket import *

# 明确服务器地址
ADDR = ("127.0.0.1", 8888)
# 1.创建套接字
sock = socket(AF_INET, SOCK_DGRAM)#
# 2.发 收 消息
while True:
    msg = input(">>:")
    if not msg:
        break
    sock.sendto(msg.encode(), ADDR)
    # if msg == "##":
    #     break
    data, addr = sock.recvfrom(128)#从 sock 套接字对象接收最多128个字节的数据,并返回一个包含接收到的数据和发送方地址的元组。
    print('From server:', data.decode())
# 3.关闭
sock.close()

  结果如下:

  似乎没有问题。但是我们输入长一点的数据,就报错了,比如:123456

   也就是说我们使用的是参数是5,而我们客户端发送了超过5字节的数据。就报错了,如何改呢?改成大一点或者和客户端的128保持一致就可以解决了。

   但是在前面的TCP中,似乎不会报错。因为TCP可以拆包,而UDP的数据报不能拆分。这个例子中,UDP服务端的缓冲区为123456(6个字节),程序获取5个字节的数据就会报错。

 8.  HTTP协议

  HTTP(Hypertext Transfer Protocol)即超文本传输协议,是一种用于传输超媒体文档(例如HTML)的通信协议。它是基于请求-响应模式,是一种无状态协议,即服务器不会保存与之前客户端请求相关的任何信息。

  HTTP协议基于TCP/IP协议族,使用端口号80进行通信。它定义了客户端与服务器之间的通信语法和语义,规定了客户端向服务器发起请求的方式,以及服务器如何应答客户端的请求。通常,HTTP的请求由客户端发起,服务器接收请求然后发送响应。请求由三个部分组成:请求方法、URL和协议版本,例如GET /index.html HTTP/1.1。而响应由三个部分组成:协议版本、状态码和状态消息,例如HTTP/1.1 200 OK。

  HTTP协议的常见特点包括无连接、无状态、可扩展、灵活、简单等。由于无连接和无状态的特点,HTTP协议对于一次请求需要多次连接的情况下的性能较低,并且需要使用额外的机制来维护会话。为了解决这个问题,引入了Cookie和Session等机制。此外,在HTTP协议的基础上发展出了各种扩展,如HTTPS、HTTP/2、HTTP/3等,用于增强安全性、提高传输效率和性能。

  除了http之外,常用的还有HTTPS协议,其通过SSL/TLS提供了一个安全的端到端的加密通道,传输效率较http慢。

9. HTTP协议组成

  http协议由请求和响应两部分组成。

  请求消息的组成部分:

   1.请求行(Request Line

    HTTP方法:标示请求类型的动词,例如 GET, POST, PUT, DELETE 等。每种方法代表不同的操作类型。

    URL:请求的资源地址,比如 /index.html

    HTTP版本:标示HTTP协议的版本,例如 HTTP/1.1 或 HTTP/2.0。        

  请求Request Headers)

  请求头包括了关于资源和客户端的更多信息,以下是一些常见的请求头字段:

    Host:请求的主机名和端口号。

    User-Agent:客户端浏览器和操作系统的信息。

    Accept:客户端可以处理的媒体类型。

    Connection:控制持久连接的参数,例如 keep-alive

    Content-Type:请求体的媒体类型,例如 application/json

    Content-Length:请求体的长度(字节数)。

    Authorization:包含身份验证信息等。

  空

    请求头和消息体之间的空行,用于分隔它们。

  请求Request Body

  不是所有请求都会包含请求体,例如GET请求通常没有请求体。请求体用于包含需要发送给服务器的数据,比如在POST请求中提交的表单数据。请求体的格式由 Content-Type 头部字段定义,可能是 application/x-www-form-urlencodedmultipart/form-dataapplication/json 等 。

  响应消息的组成部分:

  1. 响应行:包含协议版本、状态码和状态消息,例如:HTTP/1.1 200 OK。
  2. 响应头部:包含关于响应的一些附加信息,如Content-Type、Content-Length、Server等。
  3. 响应体:服务器返回给客户端的实际数据,可以是HTML文档、图片、音频等。

  这里给一个简单的http请求报文与响应报文的例子(借用网上的图):

  请求报文:

    请求报文格式如下:

    请求报文例子如下:

   响应报文:

    响应报文格式如下:

    响应报文例子如下

   注意上图中,HTTP响应空行由一个回车符(CR)和一个换行符(LF)组成,即"\r\n"。它表示HTTP响应头部的结束。

  上面介绍了HTTP,HTTPS和HTTP协议差不多。HTTPS(超文本传输安全协议)在HTTP之上增加了一层加密层,以确保数据的完整性和机密性。HTTPS请求的组成部分与HTTP请求基本相同,主要区别在于数据传输过程中的加密。其加密层由如下结构组成:

  SSL/TLS握手:在发送任何HTTP数据之前,客户端和服务器之间会进行SSLTLS握手,以建立安全连接。握手过程包括协商加密算法、交换公钥、验证证书等。

  数据加密:一旦安全连接建立,所有传输的数据(包括请求行、请求头和请求体)都将被加密。这确保了在传输过程中的数据安全性和隐私保护。

10. HTTP编程

  下面通过socket编程,实现浏览器输入http://127.0.0.1:8009/访问注册页面。

from socket import socket
from threading import Thread

class ThreadSend(Thread):
    def __init__(self,client_socket):
        super().__init__()
        self.client_socket=client_socket


    #接收请求体 发送http响应体体
    def run(self) -> None:
        request = self.client_socket.recv(1024 * 10)
        #解析request
        #1.判断是get请求还是post请求
        requeststr=request.decode()
        print("requeststr",requeststr)
        http_type=requeststr.split(" ",2)[0]
        response_status = b"HTTP/1.1 200 OK\r\n"#状态行

        if http_type=='GET':#get请求信息在url中
            print("GET请求")
            http_url=requeststr.split(" ",2)[1]
            print("url",http_url)
            if http_url=="/" or http_url=='/register':
                #响应首页register
                response_head=b"Content-type:text/html\r\n"
                black=b"\r\n"
                response=response_status+ response_head+black
                with open("../static/register.html","rb") as file:
                    response=response+file.read()
                    self.client_socket.send(response)
            if http_url=="/css/base.css":
                #"text/css
                response_head = b"Content-type:text/css\r\n"
                black = b"\r\n"
                response = response_status + response_head + black
                with open("../static/css/base.css", "rb") as file:
                    response = response + file.read()
                    self.client_socket.send(response)
            if http_url=="/image/kj.jpg":
                response_head = b"Content-type:image/jpeg\r\n"
                black = b"\r\n"
                response = response_status + response_head + black
                with open("../static/image/kj.jpg", "rb") as file:
                    response = response + file.read()
                    self.client_socket.send(response)
            if http_url == "/js/register.js":
                response_head = b"Content-type:text/html\r\n"
                black = b"\r\n"
                response = response_status + response_head + black
                with open("../static/js/register.js", "rb") as file:
                    response = response + file.read()
                    self.client_socket.send(response)
            if http_url=="/image/backgound_image.jpg":
                response_head = b"Content-type:text/html\r\n"
                black = b"\r\n"
                response = response_status + response_head + black
                with open("../static/image/backgound_image.jpg", "rb") as file:
                    response = response + file.read()
                    self.client_socket.send(response)

        if http_type=='POST':
            print("POST请求")


class ThreadReceive(Thread):
    def __int__(self,client_socket):
        super().__int__()
        self.client_socket=client_socket
    #
    def run(self) -> None:
        pass

class Server: def __init__(self,host='127.0.0.1',port=8009): self.server_socket=socket() self.server_socket.bind((host,port)) self.server_socket.listen() self.getclient_socket() def getclient_socket(self): while True: client_socket,client_addr=self.server_socket.accept() print("已经接受了一个连接") print("id",id(client_socket)) self.__handle(client_socket) def __handle(self,client_socket): threadSend= ThreadSend(client_socket) threadSend.start() if __name__ == '__main__': server=Server()

 

   请求后,上面的服务端程序会将html页面原封不动作为响应体返回给浏览器客户端,浏览器客户端拿到页面解析,解析css文件 .jpg、.js等文件时,会再次主动给服务器发送Get请求,服务器再响应资源文件。如下图所示:

   上面是一个简单的访问注册页的例子,理论上,我们可以使用socket编程开发一个网站。但是,其小小的功能也需要大量的代码,结构层次不清晰,真实项目开发中,没有人会这么去做。这里只是介绍其原理,在python web中常常使用Django、Flask框架去开发。上面的整个代码(包括css图片等),会在文中最后给链接。

 

小结:http协议是基于TCP的协议,http使用默认端口80,https为443. 对于TCP粘包问题,下载与上传任务,可以忽略粘包问题,无论是否粘包,最后总会将包合并。另外,TCP协议常常用于传感设备的数据接受中,一般也采用边界符如$、::等去区分粘包问题的数据。在使用TCP传输数据时,所有数据(包括字符串、图片、音频等)都是以二进制字节序列传送过去的。所以常常使用了encode函数编码成字节串,另一端接受后,采用decode还原数据。另外UDP协议是无连接的,数据单独原样发送, 既不会拆分, 也不会合并。本文并没有对TCP组成,TCP三次挥手详细介绍,这一点已经在我的TCP协议与三次握手连接中讲的比较清楚了。

   若存在不足或错误之处,欢迎之处与评论!

http代码的链接:

链接:https://pan.baidu.com/s/1sC71LU1dGeXC_5yFTC-ovQ
提取码:r7lm

 

参考资料:

https://blog.csdn.net/weixin_44992737/article/details/125443207

https://blog.csdn.net/m0_58086930/article/details/125607675

https://zhuanlan.zhihu.com/p/468548864

https://www.python51.com/gongju/84322.html

https://www.runoob.com/python+/python-socket.html

https://blog.csdn.net/weixin_44992737/article/details/125443207

https://blog.csdn.net/dolly_baby/article/details/130478596

https://blog.csdn.net/apex_eixl/article/details/129327706

https://blog.csdn.net/2202_75483211/article/details/131908127

https://blog.csdn.net/weixin_57688764/article/details/129716757

https://blog.csdn.net/m0_70432049/article/details/128740854

  

posted @ 2023-08-25 21:18  wancy  阅读(104)  评论(0编辑  收藏  举报