一 socket编程

如上图所示,Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面对用户来说,一组简单的套接字相关函数就是全部,让Socket去组织数据,以符合指定的协议,如TCP或UDP就可以完成通信过程了。

所以,Socket不是通信协议,而是存在于应用层与传输层之间的接口,是方便开发者用于实现复杂的TCP/UDP协议所封装的一套函数

Python内置了2个常用模块提供给我们完成socket编程。

  • 低级别的网络服务支持基本的 Socket,它提供了标准的 BSD Sockets API,可以访问底层操作系统 Socket 接口的全部方法。

  • 高级别的网络服务模块 SocketServer, 它提供了服务器中心类,可以简化网络服务器的开发。

二 实现基于TCP协议的socket通信

服务端:

import socket

# 创建TCP套接字对象
# 套接字主要有两种类型:基于文件类型的AF_UNIX,基于网络类型的AF_INET(常用)
# 套接字数据传输格式:SOCK_STREAM 字节流传输方式,基于TCP通信(默认值),SOCK_DGRAM 数据报文传输方式,基于UDP通信
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定通信端口
# local 本地,本端
# remote 外地,远程,对端
laddr = ("127.0.0.1", 8000)
sk.bind(laddr)

# 监听通信端口,backlog参数设置阻塞等待的客户端连接数量,默认为1
sk.listen(5)

# 再次增加无限循环,实现让服务端可以和多个客户端不断的建立三次握手、收发消息、四次挥手
while True:
    # 接收客户端连接(三次握手)
    conn, raddr = sk.accept()
    print(f"客户端{raddr}连接了服务器!")
    # 增加无限循环,让服务端可以多次接收来自对端发送的数据
    while True:
        # 接收客户端返回数据,数据以bytes形式返回
        # recv 等待接收对端发送的数据,程序会阻塞,收到数据之后,才会继续执行后面的代码
        content = conn.recv(1024).decode('utf-8')
        # 如果客户端关闭连接,则服务端也要关闭与当前客户端的连接
        if content == "exit": break
        if content:
            print(f"客户端{raddr}连接过来数据: {content}")
            # 服务端也可以发送数据给客户端【将来就是通过数据库中的资源数据提供给客户端】
            message = input(">:")
            conn.send(message.encode("utf-8"))
    # 四次挥手
    conn.close()

客户端1:

import socket

# 创建TCP套接字对象
sk = socket.socket()

# 连接服务端
addr = ("127.0.0.1", 8000)
sk.connect(addr)

# 增加无限循环,让客户端可以多次发送数据给服务端
while True:
    message = input(">: ")
    sk.send(message.encode("utf-8"))
    if message == "exit": break  # 客户端退出
    # 接收服务端发送的数据
    content = sk.recv(1024).decode("utf-8")
    if content:
        print(f"服务端: {content}")

sk.close()

附:

1 TCP收发消息不为空原因

TCP是基于数据流的,所以收发的消息不能为空,这就需要在客户端和服务端都添加空消息的判断处理逻辑,防止程序卡住,而udp是基于数据报文的,即便是你输入的是空内容(直接回车),也可以被发送,
udp协议会帮你封装上消息头发送过去。对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。 下图UDP的消息头为保护边界
TCP:[三次握手] 数据xx xx xxxxx xxxx [四次挥手]

UDP: [8个字节的消息头]数据xxx [8个字节的消息头]xxx [8个字节的消息头]xxx

三 实现基于UDP协议的socket通信

服务端:

import socket

sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

laddr = ('127.0.0.1', 9000)
sk.bind(laddr)while True:
    content, raddr = sk.recvfrom(1024)
    print(f"来自[{raddr[0]}:{raddr[1]}]的一条消息:\033[1;44m{content.decode('utf-8')}\033[0m")
    message = input('>:').strip()
    sk.sendto(message.encode('utf-8'), raddr)

客户端1:

import socket

sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

friends = {
    'xiaoming': ('127.0.0.1', 9000),
    'xiaobai': ('127.0.0.1', 9000),
}

while True:
    name = input('请选择聊天对象: ').strip()
    if name == 'exit': break
    while True:
        content = input('>: ').strip()
        if content == 'exit': break
        # 如果内容为空,或者name为空,或者name不在friends字典中,则跳过本次循环,不要往下执行
        if not content or not name or name not in friends: continue
        sk.sendto(content.encode('utf-8'), friends[name])
        message, raddr = sk.recvfrom(1024)
        print(f"来自[{raddr[0]}:{raddr[1]}]的一条消息:\033[1;44m{message.decode('utf-8')}\033[0m")

sk.close()

 四 TCP通信过程中的粘包问题

1 粘包问题:

A端多次发送的数据,到达B端被接收时会存在一次性接受A端多次发送数据的情况。

2 粘包出现原因:

接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。粘包是由TCP协议本身是面向流造成的(三次握手建立连接后开始发送数据),所以只有TCP协议有粘包现象,而UDP是面向消息报文的协议,所以永远不会粘包。

3 粘包现象:

粘包现象一:
    在发送端, 由于两个数据短, 发送的时间隔较短, 所以在发送端形成粘包
粘包现象二:
    在接收端, 由于两个数据几乎同时被发送到对方的缓冲区中, 所有在接收端形成了粘包

总结:发送数据的时间间隔短 或者 接受数据不及时, 都会出现粘包问题。

4 粘包现象出现原因:

 

应用程序无法直接操作网络,需要操作系统去管理底层。应用程序会将数据放于应用程序自己的缓存区,操作系统去程序缓存区取数据放于操作系统自己的缓存区,然后调用硬件网卡传输数据到对端网卡,对端操作系统到其网卡取数据后放于系统缓存中,应用程序主动到操作系统缓存中取数据。期间数据的收发存在延时或发送数据时间间隔短,因此存在数据粘包问题。

5 解决方案

使用Struct模块来解决:

使用内置模块Struct可以针对数据进行打包和解包,不仅可以支持普通字符串数据,也支持对字节流数据进行打包和解包,使用struct模块为字节流加上自定义固定长度报头报头中包含字节流长度,然后就一次性把数据send到对端,对端在接收时,先从缓冲区中取出固定长度的报头,然后再取真实数据

struct模块常用方法:

普通数据基本方法:
    struct.pack()     打包
    struct.unpack     解包
二进制数据打包:
    struct.pack_into()       打包
    struct.unpack_from()     解包

示例:

5.1 基于struct模块,对普通数据进行打包和解包

import struct

"""函数式用法"""
# content = 'abcabc'.encode()

# # 组装数据
# source_data = (内容长度, 内容)
# source_data = (len(content), content)
# print("源数据:", source_data)
# # 源数据: (6, b'abcabc')
#
# # 打包数据
# packed_data = struct.pack('i6s', *source_data)  # i表示整数int,6s表示6个字节长度的字符串str
#
# # 打包后的结果得到字节流
# print(packed_data)
# # b'\x06\x00\x00\x00abcabc'
# # 解包
# source_data = struct.unpack("i6s", packed_data)
# # 解包的结果是一个元组
# print(source_data)
# # (6, b'abcabc')


"""面向对象用法"""
cmd = 101  # 假设101表示商品信息
# 消息体
content = '商品具体信息'.encode()
# 消息长度
length = len(content)
# 组装数据
source_data = (cmd, content, length)
print("源数据:", source_data)
# 源数据: (101, b'\xe5..', 18)

# 打包数据
st = struct.Struct(f'i{length}si')
packed_data = st.pack(*source_data)  # i表示整数int,6s表示6个字节长度的字符串str
# 打包后的结果得到字节流
print(packed_data) #源数据: (101, b'\xe5\x95\..', 18)
# 解包数据
source_data = st.unpack(packed_data)
# 解包的结果是一个元组
print(source_data) #(101, b'\xe5\x95\..', 18)
print(f"消息类型:{source_data[0]}, 消息体:{source_data[1].decode('utf-8')}, 消息长度: {source_data[2]}")
# 消息类型:101, 消息体:商品具体信息, 消息长度: 18

5.2 基于struct模块,对二进制数据进行打包和解包

import struct, binascii, ctypes

# 第一条数据
source_data1 = (1, 'abcabc'.encode(), 2.7)
s1 = struct.Struct('i6sf')  # i 表示int,6s表示三个字符长度的字符串,f 表示 float

# 第二条数据
source_data2 = ('defg'.encode(), 101)
s2 = struct.Struct('4si')

# 获取数据格式的字节长度
print(s1.size)
print(s2.size)
# 把数据转换成二进制,并保存到缓冲区(buffer)
prebuffer = ctypes.create_string_buffer(s1.size+s2.size) # 参数是缓冲区的内存大小[单位是字节]
print('打包前缓冲区:', binascii.hexlify(prebuffer)) # 打包前缓冲区 : b'0000000000000000000000000000000000000000'
# 从缓冲区中提取数据并进行打包
s1.pack_into(prebuffer, 0, *source_data1) # s1.size 因为前面数据位已经被第一段数据占用了,此处要空出s1.size的长度
print('打包第一段数据:', binascii.hexlify(prebuffer))
# 打包第一段数据: b'010000006162636162630000cdcc2c400000000000000000'
s2.pack_into(prebuffer, s1.size, *source_data2)
print('打包第二段数据:', binascii.hexlify(prebuffer))
# 打包第二段数据: b'010000006162636162630000cdcc2c406465666765000000'

"""解包数据"""
print(s1.unpack_from(prebuffer, 0))
print(s2.unpack_from(prebuffer, s1.size))

五 socketserver模块

socket模块使用的TCP通信是基于一对一的通信模式。socketserver是python的内置模块,是基于原有socket模块又进行了一层封裝,实现了一个TCP服务端可以同时与多个TCP客户端同时进行通信即实现了并发客户端

socketserver模块中分两大类:server类(解决与客户端并发连接的问题)和RequestHandler类(解决与客户端的并发通信问题)

基本使用

#socketserver主要提供的操作是给服务端实现并发连接客户端的,所以客户端代码还是使用socket即可
server端:
import socketserver

class TCPServer(socketserver.BaseRequestHandler):
    """自定义服务端Request类"""
    # handle 方法是每当有一个客户端发起connect申请建立连接时, 自动执行handle方法,所以方法名必须固定为handle
    def handle(self):
        print("建立连接")

if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(("127.0.0.1", 9000), TCPServer)
    # 启动服务器
    server.serve_forever()


client端:
import socket
sk = socket.socket()
sk.connect( ("127.0.0.1",9000) )
sk.close()

 

posted on 2022-05-13 23:05  大明花花  阅读(76)  评论(0编辑  收藏  举报