python - 网络编程

Python 提供了两个访问网络服务的方式
底层网络接口socket,这个模块提供了访问BSD Socket的接口,在所有现代 Unix 系统、Windows、macOS 和其他一些平台上可用。
用于简化网络服务端编写的类 socketserver

什么是socket
socket又称 套接字,应用程序通常通过"套接字"向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯

Python TCP | UDP使用socket的通信过程TCP|UDP

套接字对象的创建

# socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
"""
family:
    AF_INET :一对 (host, port) 被用于 AF_INET 地址族
                  host: '127.0.0.1' or 'vn.asdadasaa.com'
                  port: 为一个整数,代表主机上的端口号
    AF_INET6:一个四元组 (host, port, flowinfo, scopeid)
                  flowinfo代表了 C 库 struct sockaddr_in6 中的 sin6_flowinfo
                  scopeid 代表了 C 库 struct sockaddr_in6 中的 sin6_scope_id
                  上面的两个参数都可以省略
    AF_UNIX: unix操作系统的通信方式,其它不祥
      ...

type:
    SOCK_STREAM: 基于TCP连接的套接字
    SOCK_DGRAM : 基于UDP连接的套接字
       ...

proto: 一般不写

fileno: 如果存在这个参数,则上面的默认参数将 不 会存在
"""
class socket(_socket.socket):

    """A subclass of _socket.socket adding the makefile() method."""

    __slots__ = ["__weakref__", "_io_refs", "_closed"]

    def __init__(self, family=-1, type=-1, proto=-1, fileno=None):
        # For user code address family and type values are IntEnum members, but
        # for the underlying _socket.socket they're just integers. The
        # constructor of _socket.socket converts the given argument to an
        # integer automatically.
        if fileno is None:
            if family == -1:
                family = AF_INET
            if type == -1:
                type = SOCK_STREAM
            if proto == -1:
                proto = 0
        _socket.socket.__init__(self, family, type, proto, fileno)
        self._io_refs = 0
        self._closed = False

tcp是基于链接的,必须先启动服务端,然后再启动客户端去连接服务端
下面是基于TCP协议的socket

import socket
# server端
sk = socket.socket() # 创建服务端socket对象
sk.bind(('localhost', 8081)) # 把地址,端口绑定到socket对象
sk.listen() # 监听连接
conn, addr = sk.accept() # 接受客户端连接

# 这里 收/发 数据都是可以的
str = '连接成功'.encode('utf-8')
conn.send(str) # 向客户端发生信息

conn.close() # 关闭客户端连接
sk.close() # 关闭服务端socket对象
import socket
# client 端
sk = socket.socket() # 创建客户端socket对象
sk.connect(('localhost', 8081)) # 尝试连接服务器

# 这里 收/发 数据都是可以的
res = sk.recv(1024).decode() # 接受服务端发生的数据
print(res)

sk.close() # 关闭客户端的ocket对象

这上面的代码多次运行可能会出现端口占用的错误
OSError: [Errno 98] Address already in use

解决方案(仅在测试环境添加下面):

from socket import SOL_SOCKET,SO_REUSEADDR
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)  #加在bind前加

udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接
下面是基于udp协议的socket

import socket
# server端
sk = socket.socket(type=socket.SOCK_DGRAM) # 创建服务端socket对象
sk.bind(('127.0.0.1',8000)) # 把地址,端口绑定到socket对象
msg, addr =  sk.recvfrom(1024) # 接收 UDP 数据,返回值是(data,address)
# 其中 data 是包含接收数据的字符串,address 是发送数据的套接字地址
print(msg, addr)
sk.sendto(b'Hi', addr) # 发送 UDP 数据,将数据发送到套接字
# addr 是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数
sk.close()
import socket
# client端
ip_port = ('localhost', 8000)

sk = socket.socket(type=socket.SOCK_DGRAM)
sk.sendto(b'Hi Server', ip_port)# 发送 UDP 数据,将数据发送到套接字
# ip_port 是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数
back_msg, addr = sk.recvfrom(1024)# 接收 UDP 数据,返回值是(back_msg,addr)
# 其中 back_msg 是包含接收数据的字符串,addr 是发送数据的套接字地址
print(back_msg.decode(), addr)
sk.close()
黏包:先看下面代码所出现的现象
import socket
# server端
sk = socket.socket()
sk.bind(('localhost', 8081))
sk.listen()
conn, addr = sk.accept()

conn.send(b'hello, ')
conn.send(b'world')


conn.close()
sk.close()
import socket
# client端
sk = socket.socket()
sk.connect(('localhost', 8081))


res = sk.recv(1024).decode()
print(res, '<====1====>')    # hello, world <====1====>

res = sk.recv(1024).decode()
print(res, '<====2====>')    # <====2====>


sk.close()

由上面的结果不难可以看到 在client端第一次接受到结果为server端两次发送的结果,而在client端第二次接受数据时,却没有了,这就被我们成为黏包。

同时执行多条代码时,得到的结果很可能只有一部分结果,在执行其它代码的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。

黏包现象

# tcp协议在发送数据时,会出现黏包现象.	
    (1)数据粘包是因为在客户端/服务器的发送端和接收端都会有一个数据缓冲区,缓冲区用来临时保存数据,默认空间都设置较大。
在收发数据频繁时,由于tcp传输消息的无边界特点,不清楚应该截取多少长度,导致客户端/服务器端,都有可能把多条数据当成是一条数据进行截取,造成黏包
    
    (2)发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个数据包。
若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后在发送,这样接收方就收到了粘包数据。 
    
    (3)接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。
这是因为接收方先把收到的数据放在系统接缓冲区,用户进程从该缓冲区取数据,
若下一个数据包到达时,上一个数据包尚未被用户进程取走,则系统可能把多条数据当成是一条数据进行截取
    
# 总结: TCP协议是面向连接的无边界协议

    黏包现象一:
        在发送端,由于在缓冲区两个数据小,发送的时间隔短,TCP会根据优化算法把这些数据合成一个发送
        
    黏包现象二:
        在接收端,由于在缓冲区没及时接受数据,截取数据时把多次发送的数据截取成一条,形成了黏包
黏包对比:tcp和udp
#tcp协议:
缺点:接收时数据之间无边界,有可能粘合几条数据成一条数据,造成黏包 
优点:不限制数据包的大小,稳定传输不丢包

#udp协议:
优点:接收时候数据之间有边界,传输速度快,不黏包
缺点:限制数据包的大小(受带宽路由器等因素影响),传输不稳定,可能丢包

#tcp和udp对于数据包来说都可以进行拆包和解包,理论上来讲,无论多大都能分次发送
但是tcp一旦发送失败,对方无响应(对方无回执),tcp可以选择再发,直到对应响应完毕为止
而udp一旦发送失败,是不会询问对方是否有响应的,如果数据量过大,易丢包
解决黏包问题
#解决黏包场景:
	应用场景在实时通讯时,需要阅读此次发的消息是什么
#不需要解决黏包场景:
	下载或者上传文件的时候,最后要把包都结合在一起,黏包无所谓.
黏包的解决方案 struct 模块
该模块可以把一个类型,如数字,转成固定长度的bytes
# struct模块的基本使用
import struct

# 'i' format requires -2147483648 <= number <= 2147483647

# 这里的i代表整型 int
struct.pack('i', 1234) # b'\xd2\x04\x00\x00'
len(struct.pack('i', 1234)) # 4
struct.unpack('i' ,b'\xd2\x04\x00\x00') #(1234,)
import socket
import struct
# server端
sk = socket.socket()
sk.bind(('localhost', 8080))
sk.listen()
conn, addr = sk.accept()

for i in range(1, 11):
    string = '这是第{}次发送的数据'.format(i).encode()

    # 第一次发送数据的长度
    len_bytes = struct.pack('i', len(string))
    conn.send(len_bytes)

    # 第二次发送真实的数据
    conn.send(string)

conn.close()
sk.close()
import socket
import struct
# client端
sk = socket.socket()
sk.connect(('localhost', 8080))


for i in range(10):
    # 第一次接受到的数据为服务器接发送的长度
    len_bytes = sk.recv(4)
    length = struct.unpack('i', len_bytes)[0]
    # 第二次接受到的数据为真实的数据
    res = sk.recv(length).decode()
    print(res)

sk.close()
socketserver模块 (后续剖析)
import socketserver
# server端

class MyServer(socketserver.BaseRequestHandler):
    def handle(self) -> None:
        self.request.send('连接上了....'.encode())
        for i in range(10):
            msg = self.request.recv(1024).decode().replace('发送', '接收')
            print(msg)



server = socketserver.ThreadingTCPServer(('127.0.0.1', 8081), MyServer)
server.serve_forever()
import socket,time

with socket.socket() as sk:
    sk.connect(('localhost', 8081))

    res = sk.recv(1024).decode()
    print(res)
    for i in range(10):
        time.sleep(2)
        msg = '[客户端]这是我发的第{}条消息'.format(i)
        sk.send(msg.encode())
socket的更多方法介绍(了解)
服务端套接字函数
s.bind()    绑定(主机,端口号)到套接字
s.listen()  开始TCP监听
s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect()     主动初始化TCP服务器连接
s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
(等价于:异常处理+connect 一旦网络不通,作用:返回错误号而不是直接报错)

公共用途的套接字函数
s.recv()            接收TCP数据
s.send()       发送TCP数据,send返回值是发送的[字节数量],这个值可能小于要发送的string字节数
s.sendall()    发送TCP数据,sendall返回值是None,发送string所有数据
'''
# 下面两个代码等价:
    #sendall => sock.sendall('Hello world\n')
    #send => buffer = 'Hello world\n'
             while buffer:
        		n = sock.send(buffer)
        		buffer = buffer[n:] (切片)
'''
s.recvfrom()        接收UDP数据
s.sendto()          发送UDP数据
s.getpeername()     连接到当前套接字的远端的地址
s.getsockname()     当前套接字的地址
s.getsockopt()      返回指定套接字的参数
s.setsockopt()      设置指定套接字的参数
s.close()           关闭套接字

面向锁的套接字方法
s.setblocking()     设置套接字的阻塞与非阻塞模式
s.settimeout()      设置阻塞套接字操作的超时时间
s.gettimeout()      得到阻塞套接字操作的超时时间

面向文件的套接字的函数
s.fileno()          套接字的文件描述符
s.makefile()        创建一个与该套接字相关的文件

更多方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值