铁乐学Python_Day34_Socket模块2和黏包现象

铁乐学Python_Day34_Socket模块2和黏包现象

套接字

套接字是计算机网络数据结构,它体现了C/S结构中”通信端点”的概念。
在任何类型的通信开始之前,网络应用程序必須创建套接字。
可以将它们比作成电话插孔,没有它将无法进行通信。
套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。
这就是所谓的进程间通信(Inter Process Communication, IPC)。

有两种类型的套接字:基于文件的和面向网络的。

基于文件 AF_UNIX

地址家族(address family):UNIX(术语)
两个进程运行在同一台计算机上,所以这些套接字都是基于文件的,这意味着文件系统支持它们的底层基础结构。
这是显而易见的,因为文件系统是运行在同一主机上的多个进程之间的共享常量。

基于网络 AF_INET

地址家族:因特网

AF_NETLINK(无连接)

python 2.5引入了对特殊类型的Linux套接字的支持。
套接字的AF_NETLINK家族(无连接)允许使用标准的BSD套接字接口进行用户级别和内核级别代码之间的IPC。
例如,添加新系统调用、/proc支持,或者对一个操作系统的”IOCTL”。

AF_TIPC (透明的进程间通信)

python 2.6 新增针对linux的另一种特性,支持透明的进程间通信(TIPC)协议。
TIPC允许计算机集群之中的机器相互通信,而无須使用基于IP的寻址方式。

套接字地址: 主机-端口对

一个网络地址由主机名和端口号对组成,而这是网络通信所需要的。

面向连接的套接字与无连接的套接字

1、面向连接的套接字

意味着在进行通信之前必須先建立一个连接,例如,使用电话系统给一个朋友打电话。
这种类型的通信也称为虚拟电路或流套接字。
面向连接的通信提供序列化的、可靠的和不重复的数据交付,而没有记录边界。
意味着每条消息可以拆分成多个片段,并且每一条消息片段都确保能够到达目的地,
然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序。

实现这种连接类型的主要协议是传输控制协议(TCP)。
为了创建TCP套接字,必須使用SOCK_STREAM作为套接字类型。(staeam.溪流)

2、无连接的套接字

数据报类型,它是一种无连接的套接字。在通信开始之前并不需要建立连接。
在数据传输过程中并无法保证它的顺序性、可靠性或重复性。
数据报保存了记录边界,消息是以整体发送的,而并非首先分成多个片段。
使用数据报的消息传输可以比作邮政服务。
信件和包裹或许并不能以发送顺序到达。甚至可能不会到达。
为了将其添加到并发通信中,在网络中甚至有可能存在重复的消息。

实现这种连接类型的主要协议是用户数据报协议(UDP)。
为了创建UDP套接字,必須使用SOCK_DGRAM作为套接字类型。(datagram.数据报)

socket()模块函数

要创建套接字,必須使用socket.socket()函数,它一般的语法如下:
socket(socket_family, socket_type, protocol=0)
其中,socket_family 是AF_UNIX或AF_INET, socket_type 是SOCK_STREAM 或SOCK_DGRAM, 
protocol通常省略,默认为0。

创建TCP/IP套接字:
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
创建UDP/IP套接字:
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

因为有很多socket模块属性,所以此时使用'from module import *'这种导入方式可以简便许多,
使用'from socket import *',就把socket属性引入到命名空间中,虽然看起来有些麻烦,
但是通过这种方式将能够大大缩短代码,如:
tcpSock = socket(AF_INET, SOCK_STREAM)

一旦有了一个套接字对象,那么使用套接字对象的方法将可以进行进一步的交互。

黏包

执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,
这种现像就是黏包。

注意:只有TCP有黏包现象,UDP永远不会粘包。
(udp面向数据报,是有消息边界的。)

例:socket TCP仿ssh远程执行命令服务器
#!/usr/bin/env python
# _*_ coding: utf-8 _*_

from socket import *
import subprocess

'''
socket TCP仿ssh远程执行命令服务器
'''

HOST = 'localhost'
PORT = 9527
ADDR = (HOST, PORT)
BUFSIZ = 1024

tcpss = socket(AF_INET, SOCK_STREAM)
tcpss.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
tcpss.bind(ADDR)
tcpss.listen(5)

while True:
    conn, addr = tcpss.accept()
    print('接受客户端连接中...:', addr)
    while True:
        # 接收客户端发过来的cmd命令
        cmd = conn.recv(BUFSIZ)
        # 命令为空时退出循环
        if not cmd:break
        '''
        subprocess通过子进程来执行外部指令,shell参数为True时,直接传入命令;
        Popen方法用于先进入到某个输入环境,再执行一系列指令;
        stdin 程序的标准输入,stdout 标准输出 stderr 标准错误。
        '''
        result = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                  stdout = subprocess.PIPE,
                                  stdin  = subprocess.PIPE,
                                  stderr = subprocess.PIPE )

        stderr = result.stderr.read()
        stdout = result.stdout.read()
        # 将标准输出和标准错误发送给客户端
        conn.send(stderr)
        conn.send(stdout)

conn.close()
tcpss.close()

socket TCP仿ssh远程执行命令客户端

#!/usr/bin/env python
# _*_ coding: utf-8 _*_

from socket import *

'''socket TCP仿ssh远程执行命令客户端'''

HOST = 'localhost'
PORT = 9527
BUFSIZ = 1024
ADDR = (HOST, PORT)

tcpsc = socket(AF_INET, SOCK_STREAM)
tcpsc.connect(ADDR)

while True:
    cmd = input('请输入要对服务端操作的命令>>>').strip()
    if not cmd:break
    # 当客户端输入quit时退出循环
    if cmd == 'quit':break

    tcpsc.send(cmd.encode('utf-8'))
    result = tcpsc.recv(BUFSIZ)
    # 注意,服务器操作系统为windows,所以解码的时候使用gbk
    print(result.decode('gbk'), end='')

tcpsc.close()

正常命令输出短消息时不黏包如下:
D:\PortableSoft\Python35\python.exe E:/Python/重要的代码/socket-远程执行命令/TCPscCmd.py
请输入要对服务端操作的命令>>>dir
 驱动器 E 中的卷是 VM
 卷的序列号是 4AE6-716D

 E:\Python\重要的代码\socket-远程执行命令 的目录

2018-05-10  17:24    <DIR>          .
2018-05-10  17:24    <DIR>          ..
2018-05-10  17:24               532 TCPscCmd.py
2018-05-10  17:24             1,375 TCPssCmd.py
               2 个文件          1,907 字节
               2 个目录 26,977,878,016 可用字节

命令输出长消息时(1024字节一次显示不全)黏包如下:
D:\PortableSoft\Python35\python.exe E:/Python/重要的代码/socket-远程执行命令/TCPscCmd.py
请输入要对服务端操作的命令>>>help
有关某个命令的详细信息,请键入 HELP 命令名
ASSOC          显示或修改文件扩展名关联。
ATTRIB         显示或更改文件属性。
BREAK          设置或清除扩展式 CTRL+C 检查。
BCDEDIT        设置启动数据库中的属性以控制启动加载。
CACLS          显示或修改文件的访问控制列表(ACL)。
CALL           从另一个批处理程序调用这一个。
CD             显示当前目录的名称或将其更改。
CHCP           显示或设置活动代码页数。
CHDIR          显示当前目录的名称或将其更改。
CHKDSK         检查磁盘并显示状态报告。
CHKNTFS        显示或修改启动时间磁盘检查。
CLS            清除屏幕。
CMD            打开另一个 Windows 命令解释程序窗口。
COLOR          设置默认控制台前景和背景颜色。
COMP           比较两个或两套文件的内容。
COMPACT        显示或更改 NTFS 分区上文件的压缩。
CONVERT        将 FAT 卷转换成 NTFS。您不能转换
               当前驱动器。
COPY           将至少一个文件复制到另一个位置。
DATE           显示或设置日期。
DEL            删除至少一个文件。
DIR            显示一个目录中的文件和子目录。
DISKCOMP      请输入要对服务端操作的命令>>>

此时继续输入一条比较短消息的命令会怎样,答案是之前未显示全的消息会继续发送过来接收:
DISKCOMP      请输入要对服务端操作的命令>>>DATE
 比较两个软盘的内容。
DISKCOPY       将一个软盘的内容复制到另一个软盘。
DISKPART       显示或配置磁盘分区属性。
DOSKEY         编辑命令行、调用 Windows 命令并创建宏。
DRIVERQUERY    显示当前设备驱动程序状态和属性。
ECHO           显示消息,或将命令回显打开或关上。
ENDLOCAL       结束批文件中环境更改的本地化。
ERASE          删除一个或多个文件。
EXIT           退出 CMD.EXE 程序(命令解释程序)。
FC             比较两个文件或两个文件集并显示它们之间的不同。
FIND           在一个或多个文件中搜索一个文本字符串。
FINDSTR        在多个文件中搜索字符串。
FOR            为一套文件中的每个文件运行一个指定的命令。
FORMAT         格式化磁盘,以便跟 Windows 使用。
FSUTIL         显示或配置文件系统的属性。
FTYPE          显示或修改用在文件扩展名关联的文件类型。
GOTO           将 Windows 命令解释程序指向批处理程序
               中某个带标签的行。
GPRESULT       显示机器或用户的组策略信息。
GRAFTABL       启用 Windows 在图形模式显示扩展字符集。
HELP           提供 Windows 命令的帮助信息。
ICACLS         请输入要对服务端操作的命令>>>

----------------------

黏包成因

tcp协议的拆包机制

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。
大部分网络设备的MTU都是1500。
如果本机的MTU比网关的MTU大,
大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。

面向流的通信特点和Nagle算法

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,
因此,发送端为了将多个发往接收端的包,更有效的发到对方,
使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。

面向流的通信是无消息保护边界的。

对于空消息:
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,
防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,
udp协议会帮你封装上消息头发送过去。

可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

基于tcp协议特点的黏包现象成因 

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,
当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。

也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),
一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。

而UDP是面向消息的协议,每个UDP段都是一条消息,
应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,
需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,
TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,
在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束。

此外,发送方引起的粘包是由TCP协议本身造成的,
TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。
若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,
这样接收方就收到了粘包数据。

UDP不会发生黏包

UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。
不会使用块的合并优化算法, 由于UDP支持的是一对多的模式,
所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,
在每个UDP包中就有了消息头(消息来源地址,端口等信息),
这样,对于接收端来说,就容易进行区分处理了。

即面向消息的通信是有消息保护边界的。

对于空消息:
tcp是基于数据流的,于是收发的消息不能为空,
这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,
而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,
udp协议会帮你封装上消息头发送过去。

不可靠不黏包的udp协议:

udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),
收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。
用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送)

用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),
这是指在用send函数时,数据长度参数不受限制。
而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,
如果比较短,可能会等待和下一次数据一起发送。

会发生黏包的两种情况

情况一 发送方的缓存机制

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

情况二 接收方的缓存机制

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

总结

黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

黏包的解决方案

解决方案一

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

存在的问题:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,
这种方式会放大网络延迟带来的性能损耗。

解决方案二(进阶)

我们可以借助struct模块,这个模块可以把要发送的数据长度转换成固定长度的字节。
这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,
那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。

struct模块

该模块可以把一个类型,如数字,转成固定长度的bytes。

>>> struct.pack('i',1111111111111)

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 
#这个是范围

使用struct解决黏包

借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。
因此可以利用这个特点来预先发送数据长度。

发送时 先发送struct转换好的数据长度4字节 再发送数据
接收时 先接受4个字节,使用struct转换成数字来获取要接收的数据长度 再按照长度接收数据

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

发送时 先发报头长度 再编码报头内容然后发送 最后发真实内容。

接收时 先收报头长度,用struct取出来 根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,最后去取真实的数据内容。

例:传输大文件,解决黏包

#!/usr/bin/env python
# _*_ coding: utf-8 _*_
'''
socket struct 传输大文件 解决黏包 TCP服务端
'''
import os
import json
import socket
import struct

server = socket.socket()
server.bind(('127.0.0.1', 9527))
server.listen()
conn, addr = server.accept()

# 要传输的文件路径,文件名,文件大小
filepath = r'E:\Python\file\三体.txt'
filename = os.path.basename(filepath)
filesize = os.path.getsize(filepath)
dic = {'filename': filename,
       'filesize': filesize}
# 字典json并转码bytes字节
str_dic = json.dumps(dic).encode('utf-8')
# 计算出json的字节长度并固定为struct模块的四位模式
len_dic = struct.pack('i', len(str_dic))

conn.send(len_dic) # 发送json的长度
conn.send(str_dic) # 发送json

with open(filepath, 'rb') as f:
    while filesize:
        content = f.read(4096)
        conn.send(content)
        filesize -= len(content)

conn.close()
server.close()

客户端:
#!/usr/bin/env python
# _*_ coding: utf-8 _*_

'''
socket struct 传输大文件 解决黏包 TCP客户端
'''

import json
import struct
import socket

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

dic_len = client.recv(4)
dic_len = struct.unpack('i', dic_len)[0]
dic = client.recv(dic_len)
str_dic = dic.decode('utf-8')
dic = json.loads(str_dic)

with open(dic['filename'], 'wb') as f:
    while dic['filesize']:
        content = client.resv(4096)
        dic['filesize'] -= len(content)
        f.write(content)

client.close()

socket的方法

服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据
s.sendall() 发送TCP数据

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() 创建一个与该套接字相关的文件

官方文档对socket模块下的socket.send()和socket.sendall()解释如下:

socket.send(string[, flags])
Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Returns the number of bytes sent. Applications are responsible for checking that all data has been sent; if only some of the data was transmitted, the application needs to attempt delivery of the remaining data.

send()的返回值是发送的字节数量,这个数量值可能小于要发送的string的字节数,也就是说可能无法发送string中所有的数据。如果有错误则会抛出异常。

socket.sendall(string[, flags])
Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Unlike send(), this method continues to send data from string until either all data has been sent or an error occurs. None is returned on success. On error, an exception is raised, and there is no way to determine how much data, if any, was successfully sent.

尝试发送string的所有数据,成功则返回None,失败则抛出异常。

end
2018-5-10

参考:
http://www.cnblogs.com/Eva-J/
《python核心编程第四版》

发表评论

电子邮件地址不会被公开。 必填项已用*标注

4 × 1 =