socket模块、黏包现象、struct模块

一、socket模块(套接字)

​ socket上应用层与TCP/IP协议通信中间的软件抽象层,帮助我们编写基于网络进行数据交互的程序,否则意味着需要自己通过代码来控制OSI七层来进行数据传输。

image-20221116105441303

1.socket简介

基于文件类型的套接字(单机): AF_UNIX

基于网络类型的套接字(联网): AF_INET

二、代码实现:TCP协议

1.初步实现

(1)服务端:

import socket
from socket import SOL_SOCKET,SO_REUSEADDR
"""
以后要养成查看源码编写代码的思路
"""
# 1 产生一个socket对象并指定采用的通信协议TCP
server = socket.socket()  # 括号内不写参数,默认TCP协议 ,family = AF_INET基于网络的套接字, type = SOCK_STREAM流式协议即
# 2 绑定一个固定的地址(服务端必备的条件)
server.bind(('127.0.0.1', 8080))    # 127.0.0.1为本地回环地址,只有自己的电脑可以访问
# 3 设立半连接池(预防洪水攻击)
server.listen(5)
# 4 等待客人连接,如果没有客人则程序会停在这里
sock, addr = server.accept()  # accept方法返回sock, addr
print(sock, addr)  # sock就是双向通道,addr就是客户端地址
# 5 服务客人
data = sock.recv(1024)  # 接收客户端发送过来的消息,1024字节
print(data.decode('utf8'))
sock.send('你好好哈哈哈哈'.encode('utf8'))  # 给客户端发消息,消息必须是bytes类型
# 6 关闭双向通道
sock.close()  # 四次挥手
# 7 关闭服务器
server.close()  # 关闭服务器

(2)客户端

"只要想用网络通信就要使用socket模块"

import socket
# 1 产生一个socket对象并指定采用的通信协议TCP
client = socket.socket()
# 2 通过服务端的地址连接服务端
client.connect(('127.0.0.1', 8080))  # 参数是元组,ip地址是服务端的地址
# 3 直接给服务端发送消息
client.send('吃了没'.encode('utf8'))
# 4 接收服务端发送过来到的消息
data = client.recv(1024)
print(data.decode('utf8'))
# 5 断开与服务端的连接
client.close()

2.代码优化

(1)服务端

"代码优化"
import socket
from socket import SOL_SOCKET, SO_REUSEADDR

# 1 产生一个socket对象并指定采用的通信协议TCP
server = socket.socket()  # 括号内不写参数,默认TCP协议 ,family = AF_INET基于网络的套接字, type = SOCK_STREAM流式协议即
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8080))  # 127.0.0.1为本地回环地址,只有自己的电脑可以访问
server.listen(5)
while True:  # 添加循环,保证可以多客户端申请访问,在连接一个客户端的同时,其他客户端可以在半连接池中等待
    sock, addr = server.accept()  # accept方法返回sock, addr
    while True:  # 添加循环,可以循环接收客户端发送的消息
        try:  # 添加异常处理2 windows系统: 当当前客户端不发消息了,就重新接收别的客人的消息,防止win系统报错
            data = sock.recv(1024)  # 接收客户端发送过来的消息,1024字节
            if len(data) == 0:  # 异常处理1 macos系统:当客户端发送空消息,则重新接收下一条消息
                break
            print(f'来自客户端{addr}的字节>>>', data.decode('utf8'))
            msg = input('消息发送:').strip()
            sock.send(msg.encode('utf8'))  # 给客户端发消息,消息必须是bytes类型
        except BlockingIOError:  # 异常处理
            break

(2)客户端

client = socket.socket()

client.connect(('127.0.0.1', 8080))
while True:  # 添加循环:可以多次发消息
    msg = input('发送给服务端>>>:').strip()
    if len(msg) == 0:
        print('不能发空消息')
        continue
    client.send(msg.encode('utf8'))
    data = client.recv(1024)
    print('来自客户端的消息>>>', data.decode('utf8'))

优化内容:

1.聊天内容自定义:
    采用input语句来获取消息,可以自定义消息

2.重复发送消息:
    添加循环,将聊天的部分用循环包起来

3.用户发送消息不能为空:
    本质其实是两边不能都是recv或者send,一定是一方收一方发 
 
4.服务端多次重启可能报错:macos系统
    Address already in use 
  	解决方式1:改端口号
  	解决方式2:博客里面代码拷贝即可
    
5.当客户端异常断开的情况下 如何让服务端继续服务其他客人
	  windows服务端会直接报错
    mac服务端会有一段时间反复接收空消息延迟报错	
  	异常处理、空消息判断

三、黏包现象

1.黏包现象简介

基于tcp协议传输数据的时候,如果接收端单次接收的数据长度小于发送的总长度,则会导致部分数据无法接收全,在第二次接收的时候会继续接收上次没接收完的数据,导致数据接收变混乱,这种现象就叫做黏包

或者连续发送小数据的时候,接收端一次接收所有收据,也导致数据接收变混乱,也叫黏包现象。

2.黏包现象产生的原因

1.接收数据时无法知道发送的数据的长短

2.TCP也称为流式协议:数据就像水流一样,没有间隔(TCP会针对数据量较小且发送间隔较短的多余数据一次性合并打包发送),并且在发送的数据传输的过程中还有缓存机制来避免数据丢失。

3.避免黏包现象的关键

如何确定即将接收的数据的大小,就是我们解决黏包现象的关键

解决的办法就是,客户端在发送数据之前先发送一条数据告诉服务端这条数据长度是多少服务器接收到这个数据之后反馈成功,客户端再发送真实数据。这样服务端就可以通过之前接收到数据长度来接收数据。

四、struct模块

1.解决黏包问题初级:

客户端:

1.将真实数据转成bytes类型并计算长度

2.利用struct模块将真实长度制作一个固定长度的报头

3.将固定长度的报头发送给服务端,服务端只需要在recv括号内填写固定长度的报头数据即可

4.然后在发送真实数据

服务端:

1.服务端线接收固定长度的报头

2.利用struct模块反向解析出真实数据长度

3.recv接收真实数据长度即可

但是struct模块对与打包数据的长度是有上限的

2.终极方案:

客户端:
1 制作真实数据的信息字典(数据长度、简介、名称)
2 利用struct模块制作字典的报头
3 发送固定长度的报头(解析出来的是字典的长度)
4 发送字典数据
5 发送真实数据
服务端:
1 接收固定长度的字典报头
2 解析出字典的长度并接收
3 通过字典获取到真实的数据的各项信息
4 接收真实数据的长度

3.发送图片防止黏包现象

客户端:

import json
import os
import socket
import struct


client = socket.socket()
client.connect(('127.0.0.1', 9090))

# 1 获取真实文件的大小


file_size = os.path.getsize(r'/Users/duoduo/Pictures/球球.jpeg')
print(file_size)  # 242227
# 2 制作真实数据的信息字典
file_data_dict = {
    'file_name': '球球小狗1.jpeg',
    'file_size': file_size,
    'file_desc': '白色小狗开心笑脸照片',
    'file_info': '笑口常开好运自然来'
}
# 3 制作报头,也就是字典的信,利用json模块序列化,并转换二进制
data_head_to_bytes = json.dumps(file_data_dict).encode('utf8')
# 利用struct模块压缩为固定长度
data_head_pack = struct.pack('i', len(data_head_to_bytes))
# 4 发送字典报头
client.send(data_head_pack)  # 此时报头已经被压缩为长度为 4 bytes的二进制数据
# 5 发送字典
client.send(data_head_to_bytes)
# 6 最后发送真实数据
with open(r'/Users/duoduo/Pictures/球球.jpeg', 'rb') as f:
    for line in f:
        client.send(line)

服务端:

import json
import socket
import struct

server = socket.socket()
server.bind(('127.0.0.1', 9090))
server.listen(8)

sock, addr = server.accept()
# 1 接收固定长度的报头
data_dict_head = sock.recv(4)
# 2 根据报头解析出字典数据的长度,收到的是元组一定要索引
data_dict_len = struct.unpack('i', data_dict_head)[0]
# 3 接收字典数据
data_dict_bytes = sock.recv(data_dict_len)
data_dict = json.loads(data_dict_bytes)
# 4 获取真实数据的各项信息,由于真实数据可能非常大,所以最好不要在recv的参数内直接填写真实数据的大小,而是可以分次接收,每次接收1024或其倍数
total_size = data_dict.get('file_size')
recv_size = 0
with open(data_dict.get('file_name'), 'wb') as f:
    while recv_size < total_size:
        data = sock.recv(1024)
        f.write(data)
        recv_size += len(data)

五、UDP协议

客户端

import socket


client = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1', 8080)

while True:
    msg = input('>>>:').strip()
    client.sendto(msg.encode('utf8'), server_addr)
    data, addr = client.recvfrom(1024)
    print(data.decode('utf8'), addr)

服务端

import socket

server = socket.socket(type=socket.SOCK_DGRAM)  # 指定为UDP协议
server.bind(('127.0.0.1', 8080))

while True:
    data, addr = server.recvfrom(1024)
    print('客户端地址>>>:', addr)
    print('上述地址发送的消息>>>:', data.decode('utf8'))
    msg = input('>>>:').strip()
    server.sendto(msg.encode('utf8'), addr)
posted @ 2022-11-16 17:12  Duosg  阅读(35)  评论(0编辑  收藏  举报