Python 全栈开发:网络编程

一 socket

1.什么是socket

看一看图中socket的位置

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

二 基于TCP的套接字

tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端

服务器端

#!/usr/bin/env python3
# encoding: utf-8
# by fixdq

import socket
# ip,端口
ip_prot = ('127.0.0.1', 8884)
# 缓冲区大小
buffer = 1024
# 创建 socket对象
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定 ip 端口
socket_server.bind(ip_prot)
# 服务端开始监听
socket_server.listen(5)
while True:
    #  连接对象,客户端地址(ip:port)
    conn, address = socket_server.accept()
    while True:
        try:
            # 接收信息 ()
            msg = conn.recv(buffer)
            # msg 字母变大写  发送个客户端
            conn.send(msg.upper())
        except ConnectionError:
            break
    # 关闭连接
    conn.close()
# 关闭socket服务
socket_server.close()

客户端

#!/usr/bin/env python3
# encoding: utf-8
# by fixdq
import socket

# ip,端口
ip_prot = ('127.0.0.1', 8884)
# 缓冲区大小
buffer = 1024
# 获取socket
socket_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 根据 ip_port 连接服务器
socket_client.connect(ip_prot)

while True:
    # 接受输入
    cmd = input("cmd>>:").strip()
    # 发送信息
    socket_client.send(cmd.encode('utf-8'))
    # 接受信息
    res = socket_client.recv(buffer)
    # 打印信息
    print(res.decode('utf-8'))
# 关闭socket
socket_client.close()

注意点: tcp协议是基于字节流传输数据的的,所以收发信息的格式都是字节(bytes)格式

三 基于UDP的套接字

服务端

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author  : Fixdq
# @File    : server.py
# @Software: PyCharm
import socket
# ip,端口
ip_port = ('127.122.114.15', 8080)
# 获取socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定IP ,端口
server.bind(ip_port)
# 循环执行
while True:
    # 接收信息
    res, addr = server.recvfrom(1024)
    # 打印信息
    print(res.decode('utf-8'))
    server.sendto('收到了'.encode('utf-8'), addr)
# 回收资源
server.close()

 

客户端

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author  : Fixdq
# @File    : client.py
# @Software: PyCharm

import socket
# IP  port
ip_port = ('127.122.114.15', 8080)
# 创建客户端
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 循环
while True:
    # 接收用户输入
    msg = input('>>>>>>>:').strip()
    if not msg: continue
    # 发送信息
    client.sendto(msg.encode('utf-8'), ip_port)
    # 接收信息
    res,addr = client.recvfrom(1024)
    # 打印信息
    print(res.decode('utf-8'),addr)
# 回收资源
client.close()

四 粘包现象以及处理

1.什么是粘包现象

须知:只有TCP有粘包现象,UDP永远不会粘包

 

两种情况下会发生粘包:

 

----发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

----接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

 

2.怎么处理粘包

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据

解决方案:

自定义报头,为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

需要用到  struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes

具体用法,参照官方Doc

https://docs.python.org/3/library/struct.html?highlight=struct#module-struct

3.具体示例

import json,struct
#假设通过客户端上传1T:1073741824000的文件a.txt

#为避免粘包,必须自定制报头
header={'file_size':1073741824000,
    'file_name':'/a/b/c/d/e/a.txt',  
    'md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值 #为了该报头能传送,需要序列化并且转为bytes head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度 #客户端开始发送 conn.send(head_len_bytes) #先发报头的长度,4个bytes conn.send(head_bytes) #再发报头的字节格式 conn.sendall(文件内容) #然后发真实内容的字节格式 #服务端开始接收 head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式 x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度 head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式 header=json.loads(json.dumps(header)) #提取报头 #最后根据报头的内容提取真实的数据,比如 real_data_len=s.recv(header['file_size']) s.recv(real_data_len)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author  : Fixdq
# @File    : tcp_server.py
# @Software: PyCharm
import json
from socket import *

import subprocess

import struct

ip_port = ('127.155.101.25', 8090)

server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(ip_port)

server.listen(5)

while True:
    conn, addr = server.accept()
    while True:
        cmd = conn.recv(1024)
        if not cmd: break
        print('--------->cmd----', cmd.decode('utf-8'))
        res = subprocess.Popen(cmd.decode('utf-8'),
                               shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
        ret = res.stdout.read()
        if ret:
            back = ret
        else:
            back = res.stderr.read()
        
        # 把真实数据的相关信息,包装成字典
        head = {
            'len': len(back)
        }
        # json 序列化 字典信息
        head_str = json.dumps(head)
        # 只包含报头长度 (报头的报头)
        # i 模式,产生一个固定4字节长度bytes
        head_head = struct.pack('i', len(head_str))   
        # 发送   (报头的报头)
        conn.send(head_head)
        # 报头数据
        conn.send(head_str.encode('utf-8'))
        # 真实数据
        conn.send(back)

    conn.close()
server.close()
服务端(自定义报头)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author  : Fixdq
# @File    : tcp_client.py
# @Software: PyCharm
import json
from socket import *
import struct

ip_port = ('127.155.101.25', 8090)
client = socket(AF_INET, SOCK_STREAM)

client.connect_ex(ip_port)

while True:
    cmd = input('>>>>>:').strip()
    client.send(cmd.encode('utf-8'))

    # 接收  报头的报头  (固定长度 4 字节)
    head_head = client.recv(4)
    # 反解出 报头的长度
    head_len = int(struct.unpack('i',head_head)[0])
    # 接收  自定义报头
    head = client.recv(head_len)
    # 拿到  自定义报头的长度
    body_len = json.loads(head.decode('utf-8'))['len']
    # 接收  真实的数据
    data = client.recv(body_len)
    print(data.decode('utf-8'))

client.close()
客户端(自定义报头)

自定义报头总结:

我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)

发送时:

先发报头长度

再编码报头内容然后发送

最后发真实内容

 

接收时:

先手报头长度,用struct取出来

根据取出的长度收取报头内容,然后解码,反序列化

从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

 

posted @ 2018-04-23 19:40  Fixdq  阅读(177)  评论(0编辑  收藏  举报