Loading

Python--网络编程

软件开发的架构

我们了解的涉及到两个程序之间通讯的应用大致分为两种:

第一种是应用类:qq、微信、网盘、优酷这一类是属于需要安装的桌面应用

第二种是web类:比如百度、知乎、博客园等使用浏览器访问就可以直接使用的应用

这些应用的本质其实都是两个程序之间的通讯。而这两个分类又对应了两个软件开发的架构~

C/S架构

C/S即:Client与Server,中文意思:客户端与服务端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。

这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。

B/S架构

B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的

Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要用户去安装什么应用程序,只需要在浏览器上通过HTTP请求服务端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查。

B/S架构和C/S的区别

C/S架构的优缺点:
*优点:
    1.客户端因为是独立设计,所以可以实现个性化
    2.因为客户端是需要进行安装的,可以不需要重复安装和加载
    3.因为客户端是独立开发的,所以有能力对客户端进行安全设计
4.如果遇到不同的操作系统,需要为不同的操作系统各开发一套客户端
*缺点:
    1.因为客户端是不需要重复安装,所以用户可以不更新与升级,增加了维护成本。
    2.因为需要开发客户端和服务器两套程序,所以开发成本会增加
B/S架构的优缺点:
*优点:
    1.因为B/S架构具备通用性,所以开发成本较低。
    2.因为不需要安装客户端,所以客户端不需要进行升级,只需要更新后台代码即可实现所有客户端的更新。
    3.因为B/S架构多用WEB网页进行开发,所以增、删功能也非常容易,只需要修改网页即可完成
*缺点:
    1.耗流量,每次都要加载全部的内容(不过有缓存可以降低流量损耗)
    2.因为没有独立的客户端,所以无法实现个性化(通过账号体系可以实现)
    3.因为没有独立设计客户端,所以客户端难以实现安全控制(HTTPS、控件)。
    4.难以实现特殊的操作(删本地文件),所以所有的杀毒软件都是C/S架构的。
B/S架构更多的时候是使用了HTTP协议、而C/S架构更多的时候使用的WinSocket协议(TCP、UDP)

socket 概念

socket层

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

socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。
也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。
所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。
通俗讲socket

套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。 

基于文件类型的套接字家族

套接字家族的名称:AF_UNIX

unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

基于网络类型的套接字家族

套接字家族的名称:AF_INET

(还有AF_INEF6被用于ipv6,还有一些其它的地址家族,不过,他们要么只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,Python支持很多地址家族,我们只关心网络编程,所以大部分时候我们只使用AF_INET) 

套接字工作流程

一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束后,挂断电话结束此次交谈。生活中的场景就解释了这工作原理。

先从服务器端说起。服务器端先初始化socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户连接。在这时如果有个客户端初始化一个socket,然后连接服务器(content),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接受请求并处理请求,然后把回应数据发送给客户端,客户端取数据,最后关闭连接,一次交互结束。

socket 模块使用方法

 1 import socket
 2 socket.socket(socket_family, socket_type, protocal=0)
 3 
 4     socket_faily:可以是 AF_UNIX 或 AF_INET; 
 5     socket_type:可以是 SOCK_STREAM 或 SOCK_DGRAM;
 6     protocol:一般不填,默认值为 0;
 7 
 8 获取tcp/ip套接字
 9 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 
11 获取udp/ip套接字
12 dupSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
13 
14 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
15 12 例如tcpSock = socket(AF_INET, SOCK_STREAM)
  • 服务端套接字函数
    s.bind()    绑定(主机,端口号)到套接字
    s.listen()  开始TCP监听
    s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来
  • 客户端套接字函数
    s.connect()     主动初始化TCP服务器连接
    s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
  • 公共用途的套接字函数
    s.recv()            接收TCP数据
    s.send()            发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
    s.sendall()         发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
    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()        创建一个与该套接字相关的文件

基于TCP协议的socket

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

tcp服务端

import socket
sk = socket.socket()            # 创建服务器套接字对象
sk.bind(('127.0.0.1', 8088))    # 把地址绑定到套接字
sk.listen()                     # 监听连接
conn, addr = sk.accept()        # 接受客户端连接
ret = conn.recv(1024)           # 接受客户端信息
print(ret)                      # 打印客户端信息
conn.send(b'hello')             # 向客户端发送信息
conn.close()                    # 关闭客户端套接字
sk.close()                      # 关闭服务器套接字(可选)

tcp客户端

import socket
sk = socket.socket()            # 创建客户套接字
sk.connect(('127.0.0.1', 8088)) # 尝试连接服务器
sk.send(b'Hai')                 # 向服务端发送信息
ret = sk.recv(1024)             # 接受服务端发送的信息
print(ret)                      # 打印服务端接受到的信息
sk.close()                      # 关闭客户套接字

socket通信流程与打电话流程类似,例子:

import socket
ip_port = ('127.0.0.1', 8088)   # 电话卡
buffer = 1024       # 收发消息的字节大小
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 买手机
sk.bind(ip_port)    # 手机插卡
sk.listen()     # 手机待机

conn, addr = sk.accept()        # 手机接电话
print('接到来自%s的电话' %addr[0])

msg = conn.recv(buffer)     # 听消息,听话
print(msg, type(msg))

conn.send(msg.upper())      # 发消息,说话

conn.close()        # 挂电话

sk.close()      # 手机关机
服务端
import socket
ip_port = ('127.0.0.1',8088)
buffer = 1024
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect_ex(ip_port)           # 拨电话

s.send('hello world'.encode('utf-8'))         # 发消息,说话(只能发送字节类型)

feedback=s.recv(buffer)                       # 收消息,听话
print(feedback.decode('utf-8'))

s.close()           # 挂电话
客户端

基于tcp实现的qq聊天

import socket
ip_port = ('127.0.0.1', 8088)
buffer = 1024
sk = socket.socket()
sk.bind(ip_port)
sk.listen()
coon, addr = sk.accept()

while True:
    msg = coon.recv(buffer).decode('utf-8')
    if msg == 'bye':
        break
    print(msg)
    info = input(">>>: ")
    if info == 'q':
        coon.send(b'bye')
        break
    coon.send(info.encode('utf-8'))

coon.close()
sk.close()
server端
import socket
ip_port = ('127.0.0.1', 8088)
buffer = 1024
sk = socket.socket()
sk.connect(ip_port)

while True:
    msg = input(">>>: ")
    if msg == 'q':
        sk.send(b'bye')
        break
    sk.send(msg.encode('utf-8'))
    info = sk.recv(buffer).decode('utf-8')
    if info == 'bye':
        break
    print(info)
sk.close()
客户端

基于UDP协议的socket

udp是无链接的,启动服务之后可以直接接收消息,不需要提前建立连接

udp服务端

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)  # 创建一个服务器的套接字
udp_sk.bind(('127.0.0.1', 8900))    # 绑定服务器套接字
msg, addr = udp_sk.recvfrom(1024)   # 接受消息
print(msg.decode('utf-8'))
udp_sk.sendto(b'bye', addr)     # 发送消息
udp_sk.close()  # 关闭服务器套接字

# udp的server 不需要监听也不需要建立连接
# 在启动服务后只能被动的等待客户端发送消息过来
# 客户端发送消息的同时还会自带地址信息
# 消息回复的时候,不仅需要发送消息,还需要把对方的地址填写上

udp客户端

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = ('127.0.0.1', 8900)
udp_sk.sendto(b'hello', ip_port)
ret, addr = udp_sk.recvfrom(1024)
print(ret.decode('utf-8'))
udp_sk.close()

基于udp实现的qq聊天

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)
udp_sk.bind(('127.0.0.1', 8090))
while True:
    msg, addr = udp_sk.recvfrom(1024)
    print(msg.decode('utf-8'))
    info = input("server:>>> ").encode('utf-8')
    udp_sk.sendto(info, addr)
sk.close()
服务端
# 客户端1
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = ('127.0.0.1', 8090)
while True:
    info = input("client1>>>: ")
    info = ('\033[34m 来自client1的消息: %s\033[0m' % info).encode('utf-8')
    udp_sk.sendto(info, ip_port)
    msg, addr = udp_sk.recvfrom(1024)
    print(msg.decode('utf-8'))
udp_sk.close()
客户端1
# 客户端2
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = ('127.0.0.1', 8090)
while True:
    info = input("client1>>>: ")
    info = ('\033[34m 来自client2的消息: %s\033[0m' % info).encode('utf-8')
    udp_sk.sendto(info, ip_port)
    msg, addr = udp_sk.recvfrom(1024)
    print(msg.decode('utf-8'))
udp_sk.close()
客户端2

基于udp实现的时间服务器

# 需求
    # 写一个时间同步的服务器
    # 服务端接收请求
    # 按照client端发送的时间格式,将服务器时间转换成对应格式
    # 发送给客户端

from socket import *
from time import strftime

ip_port = ('127.0.0.1', 9000)
bufsize = 1024

tcp_server = socket(AF_INET, SOCK_DGRAM)
tcp_server.bind(ip_port)

while True:
    msg, addr = tcp_server.recvfrom(bufsize)
    print('===>', msg)

    if not msg:
        time_fmt = '%Y-%m-%d %X'
    else:
        time_fmt = msg.decode('utf-8')
    back_msg = strftime(time_fmt)

    tcp_server.sendto(back_msg.encode('utf-8'), addr)

tcp_server.close()
服务端
# 发送时间格式
# client端每隔一段时间发送请求到服务端
# 发送时间的格式
from socket import *
ip_port=('127.0.0.1',9000)
bufsize=1024

tcp_client=socket(AF_INET,SOCK_DGRAM)

while True:
    msg=input('请输入时间格式(例%Y %m %d)>>: ').strip()
    tcp_client.sendto(msg.encode('utf-8'),ip_port)

    data=tcp_client.recv(bufsize)

    print(data.decode('utf-8'))

tcp_client.close()
客户端

socket参数的解释

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)

创建socket对象的参数说明:

family 地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。
(AF_UNIX 域实际上是使用本地 socket 文件来通信)
type 套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。
SOCK_STREAM 是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送。 
SOCK_DGRAM 是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播信息。
proto 协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。
fileno 如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。
与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。
这可能有助于使用socket.close()关闭一个独立的插座。

黏包

黏包现象

让我们基于tcp先创建一个远程执行命令的程序(命令ls -l;llll;pwd)

import subprocess
# 内置模块 和os模块的功能有相似之处
# 能执行操作系统的命令的功能
ret = subprocess.Popen('dir',    # 要执行的命令
                       shell=True,  # 表示要执行的是一条系统命令
                       stdout=subprocess.PIPE, # 存储执行结果的正常信息
                       stderr=subprocess.PIPE) # 存储执行结果的错误信息

的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码

且只能从管道里读一次结果
subprocess模块

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

基于tcp协议实现的黏包

from socket import *
import subprocess

ip_port=('127.0.0.1',8081)
BUFSIZE=1024
code = 'gbk'    # 在windows上面执行命令时候返回的是gbk, 所以这里全部使用gbk

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)

while True:
    conn,addr=tcp_socket_server.accept()
    print('客户端',addr)

    while True:
        cmd=conn.recv(BUFSIZE)
        if len(cmd) == 0:break

        res=subprocess.Popen(cmd.decode(code),shell=True,
                         stdout=subprocess.PIPE,
                         stdin=subprocess.PIPE,
                         stderr=subprocess.PIPE)

        stderr=res.stderr.read()
        stdout=res.stdout.read()
        conn.send(stderr)
        conn.send(stdout)
tcp - server
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8081)
code = 'gbk'    # 在windows上面执行命令时候返回的是gbk, 所以这里全部使用gbk

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode(code))
    act_res=s.recv(BUFSIZE)

    print(act_res.decode(code),end='')
tcp - client

基于udp协议实现的黏包

from socket import *
import subprocess

ip_port=('127.0.0.1',9000)
bufsize=1024
code = 'gbk'    # 在windows上面执行命令时候返回的是gbk, 所以这里全部使用gbk

udp_server=socket(AF_INET,SOCK_DGRAM)
udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
udp_server.bind(ip_port)

while True:
    #收消息
    cmd,addr=udp_server.recvfrom(bufsize)
    print('用户命令----->',cmd)

    #逻辑处理
    res=subprocess.Popen(cmd.decode(code),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE)
    stderr=res.stderr.read()
    stdout=res.stdout.read()

    #发消息
    udp_server.sendto(stderr,addr)
    udp_server.sendto(stdout,addr)
udp_server.close()
udp - server
from socket import *
ip_port=('127.0.0.1',9000)
bufsize=1024
code = 'gbk'    # 在windows上面执行命令时候返回的是gbk, 所以这里全部使用gbk

udp_client=socket(AF_INET,SOCK_DGRAM)

while True:
    msg=input('>>: ').strip()
    udp_client.sendto(msg.encode(code),ip_port)
    err,addr=udp_client.recvfrom(bufsize)
    out,addr=udp_client.recvfrom(bufsize)
    if err:
        print('error : %s'%err.decode(code),end='')
    if out:
        print(out.decode(code), end='')
udp - client

注意:只有TCP有黏包现象,UDP永远不会有黏包 

黏包成因

TCP协议中的数据传递

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协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
socket数据传输过程中的用户态与内核态说明

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

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

UDP不会发生黏包

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

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

补充说明:

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

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

会发生黏包的两种情况

情况一 发送方的缓存机制:

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

#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)


conn,addr=tcp_socket_server.accept()


data1=conn.recv(10)
data2=conn.recv(10)

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()
服务端
#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)


s.send('hello'.encode('utf-8'))
s.send('egg'.encode('utf-8'))
客户端

情况二 接受方的缓存机制:

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

#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)


conn,addr=tcp_socket_server.accept()


data1=conn.recv(2) #一次没有收完整
data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()
服务端
#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)


s.send('hello egg'.encode('utf-8'))
客户端

总结

黏包现象只发生在tcp协议中:

1、从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。

2、实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

粘包的解决方案

方案一

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

import socket

ip_port = ('127.0.0.1', 8088)
buffer = 1024

sk = socket.socket()
sk.bind(ip_port)
sk.listen()
coon, addr = sk.accept()

while True:
    cmd = input("命令>>>: ")
    if cmd == 'q':
        coon.send(b'bye')
        break
    coon.send(cmd.encode('gbk'))
    num = coon.recv(buffer).decode('utf-8') # 接受客户端计算出来的内容的长度
    coon.send(b'ok')    # 回复客户端表示已经收到
    ret = coon.recv(int(num)).decode('gbk') # 按照内容的长度来指定接受的字节大小
    print(ret)

coon.close()
sk.close()
服务端
import socket
import subprocess
ip_port = ('127.0.0.1', 8088)
buffer = 1024

sk_client = socket.socket()
sk_client.connect(ip_port)

while True:
    cmd = sk_client.recv(buffer).decode('gbk')
    if cmd == 'bye':
        break
    print("服务端命令————>:%s" % cmd)
    ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # 执行系统命令
    stdout = ret.stdout.read()
    stderr = ret.stderr.read()
    sk_client.send(str(len(stdout) + len(stderr)).encode('utf-8'))  # 发送计算出来的内容字节的长度
    sk_client.recv(buffer)
    # 发送命令执行完的数据
    sk_client.send(stdout)
    sk_client.send(stderr)

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

方案二

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

struct模块

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

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

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

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)
#_*_coding:utf-8_*_
#http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
import struct
import binascii
import ctypes

values1 = (1, 'abc'.encode('utf-8'), 2.7)
values2 = ('defg'.encode('utf-8'),101)
s1 = struct.Struct('I3sf')
s2 = struct.Struct('4sI')

print(s1.size,s2.size)
prebuffer=ctypes.create_string_buffer(s1.size+s2.size)
print('Before : ',binascii.hexlify(prebuffer))
# t=binascii.hexlify('asdfaf'.encode('utf-8'))
# print(t)


s1.pack_into(prebuffer,0,*values1)
s2.pack_into(prebuffer,s1.size,*values2)

print('After pack',binascii.hexlify(prebuffer))
print(s1.unpack_from(prebuffer,0))
print(s2.unpack_from(prebuffer,s1.size))

s3=struct.Struct('ii')
s3.pack_into(prebuffer,0,123,123)
print('After pack',binascii.hexlify(prebuffer))
print(s3.unpack_from(prebuffer,0))
关于struct的详细用法

使用struct解决黏包

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

发送时 接收时
先发送struct转换好的数据长度4字节 先接收4个字节使用struct模块转成数字来获取要接收的长度
再发送数据 再按照长度接收数据
import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加

phone.bind(('127.0.0.1',8080))

phone.listen(5)

while True:
    conn,addr=phone.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        err=res.stderr.read()
        print(err)
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()


        conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度
        conn.sendall(back_msg) #在发真实的内容

    conn.close()
服务端(自定制报头)
#_*_coding:utf-8_*_
import socket,time,struct

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode('utf-8'))



    l=s.recv(4)
    x=struct.unpack('i',l)[0]
    print(type(x),x)
    # print(struct.unpack('I',l))
    r_s=0
    data=b''
    while r_s < x:
        r_d=s.recv(1024)
        data+=r_d
        r_s+=len(r_d)

    # print(data.decode('utf-8'))
    print(data.decode('gbk')) #windows默认gbk编码
客户端(自定制报头)

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

发送时 接收时
先发送报头长度 先收报头长度,用struct取出来
再编码报头内容然后发送 根据取出的长度收取报头内容,然后解码,反序列化
最后发送真实内容 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加

phone.bind(('127.0.0.1',8080))

phone.listen(5)

while True:
    conn,addr=phone.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        err=res.stderr.read()
        print(err)
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()

        headers={'data_size':len(back_msg)}
        head_json=json.dumps(headers)
        head_json_bytes=bytes(head_json,encoding='utf-8')

        conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度
        conn.send(head_json_bytes) #再发报头
        conn.sendall(back_msg) #在发真实的内容

    conn.close()
服务端:定制稍微复杂一点的报头
from socket import *
import struct,json

ip_port=('127.0.0.1',8080)
client=socket(AF_INET,SOCK_STREAM)
client.connect(ip_port)

while True:
    cmd=input('>>: ')
    if not cmd:continue
    client.send(bytes(cmd,encoding='utf-8'))

    head=client.recv(4)
    head_json_len=struct.unpack('i',head)[0]
    head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
    data_len=head_json['data_size']

    recv_size=0
    recv_data=b''
    while recv_size < data_len:
        recv_data+=client.recv(1024)
        recv_size+=len(recv_data)

    print(recv_data.decode('utf-8'))
    #print(recv_data.decode('gbk')) #windows默认gbk编码
客户端

实例:实现大文件的上传,自定义包头

初级版

# 实现一个大文件的上传或者下载
import socket
import struct
import json


IP_PORT = ('127.0.0.1', 8088)
BUFFER = 4096
sk_server = socket.socket()
sk_server.bind(IP_PORT)
sk_server.listen()

coon, addr = sk_server.accept()

head_len = coon.recv(4)    # 接受客户端处理过的head_len
head_len = struct.unpack('i', head_len)[0]
json_head = coon.recv(int(head_len)).decode('utf-8')
head = json.loads(json_head)    # 拿到报头
filesize = head['filesize']     # 从包头中取到文件大小

with open(head['filename'], 'wb') as f:
    while filesize:
        if filesize >= BUFFER:
            data = coon.recv(BUFFER)
            f.write(data)
            filesize -= BUFFER
        else:
            data = coon.recv(filesize)
            f.write(data)
            break
coon.close()
sk_server.close()
server端(接收)
# 发送端
import socket
import os
import json
import struct


IP_PORT = ('127.0.0.1', 8088)

BUFFER = 4096
sk_client = socket.socket()
sk_client.connect(IP_PORT)

# 发送文件
head = {'filename':'01 python  s9day31 复习和认识tcp的长连接.mp4',
        'filesize':None,
        'filepath': r'G:\第3部分-网络编程(9期(女神讲的,非常详细))\day31'
        }
file_path = os.path.join(head['filepath'],head['filename'])
print(file_path)
filesize = os.path.getsize(file_path)
head['filesize'] = filesize

json_head = json.dumps(head)    # 字典转成字符串
bytes_head = json_head.encode('utf-8')  # 字符串转成bytes

# 计算head的长度
head_len = len(bytes_head)   # 报头的长度
pack_len = struct.pack('i', head_len)   # 将报头的长度转成4个字节的长度
sk_client.send(pack_len)    # 先发送报头的长度
sk_client.send(bytes_head)  # 再发送bytes类型的报头

with open(file_path, 'rb') as f:
    while filesize:
        if filesize >= BUFFER:
            data = f.read(BUFFER)   # 每次读出来的大小
            sk_client.send(data)
            filesize-=BUFFER
        else:
            data = f.read(filesize)
            sk_client.send(data)
            break
sk_client.close()
client端(发送)

升级版

import socket
import struct
import json
import hashlib


class Server_socket():
    def __init__(self, ip, port):
        self.ip = ip
        self.port = port
        self.server = socket.socket()
        self.server.bind((self.ip, self.port))
        self.server.listen(5)
        self.conn, self.addr = self.server.accept()

    def header(self):
        '''接受包头的函数'''
        head_len = self.conn.recv(4)    # 接受struct类型包头的长度
        head_len = struct.unpack('i', head_len)[0] # 进行解struct包头,得到包头的实际长度
        json_head = self.conn.recv(int(head_len)).decode('utf-8')   # 得到json类型的包头
        head = json.loads(json_head)    # 拿到真正的包头
        fileSize = head['file_size']    # 从包头取出文件的大小
        fileName = head['file_name']    # 从包头取出文件的名字
        return fileSize, fileName

    def recvFile(self):
        '''接收文件并计算hash值'''
        BUFFER = 1024
        hash_md5_obj = hashlib.md5()
        fileSize, fileName = self.header()
        with open(fileName, 'wb') as f:
            while fileSize:
                if fileSize >= BUFFER:
                    data = self.conn.recv(BUFFER)
                    f.write(data)
                    hash_md5_obj.update(data)
                    fileSize -= BUFFER
                else:
                    data = self.conn.recv(fileSize)
                    f.write(data)
                    hash_md5_obj.update(data)
                    break
        local_hash_value = hash_md5_obj.hexdigest()
        client_hash_value = self.recvHash()
        if local_hash_value == client_hash_value:
            self.conn.send("Upload success ...".encode('utf-8'))
        else:
            self.conn.send("Upload failure ...".encode('utf-8'))
        self.conn.close()
        self.server.close()

    def recvHash(self):
        '''接收客户端的hash值'''
        client_hash_len = self.conn.recv(4)
        client_hash_len = struct.unpack('i', client_hash_len)[0]
        client_json_hash = self.conn.recv(int(client_hash_len)).decode('utf-8')
        client_hash = json.loads(client_json_hash)
        return client_hash

server = Server_socket('127.0.0.1', 8088)
server.recvFile()
server端(接收)
import socket
import os
import json
import struct
import hashlib


class Client_socket():
    def __init__(self, ip, port):
        self.ip = ip
        self.port = port
        self.client = socket.socket()
        self.client.connect((self.ip, self.port))

    def header(self):
        '''发送包头的长度'''
        while True:
            filePath = input("input filepath >>>: ")
            if os.path.exists(filePath):
                fileName = os.path.basename(filePath)
                fileSize = os.path.getsize(filePath)
                header = {
                    "file_path": filePath,
                    "file_name": fileName,
                    "file_size": fileSize
                }
                json_head = json.dumps(header)  # 得到json类型的header
                bytes_head = json_head.encode('utf-8')  # 得到bytes类型的header
                head_len = len(bytes_head)  # 计算包头的长度
                pick_head_len = struct.pack('i', head_len)  # 转换成4个字节长度
                self.client.send(pick_head_len) # 先发送包头的长度
                self.client.send(bytes_head)    # 再发送bytes类型报头
                return fileSize, filePath

    def sendFile(self):
        '''发送文件并计算hash值'''
        BUFFER = 1024
        hash_md5_obj = hashlib.md5()
        fileSize, filePath = self.header()
        with open(filePath, 'rb') as f:
            while fileSize:
                if fileSize >= BUFFER:
                    data = f.read(BUFFER)
                    self.client.send(data)
                    hash_md5_obj.update(data)
                    fileSize -= BUFFER
                else:
                    data = f.read(fileSize)
                    hash_md5_obj.update(data)
                    self.client.send(data)
                    break
        hash_value = hash_md5_obj.hexdigest()   # 拿到hash值
        self.sendHash(hash_value)   # 发送给服务端
        ret = self.client.recv(1024).decode('utf-8')    # 拿到上传的结果
        print(ret)

    def sendHash(self, hashValue):
        '''发送hash值'''
        bytes_hash = json.dumps(hashValue).encode('utf-8')
        hash_len = len(bytes_hash)
        pick_hash_len = struct.pack('i', hash_len)
        self.client.send(pick_hash_len)
        self.client.send(bytes_hash)

client = Client_socket('127.0.0.1', 8088)
client.sendFile()
client端(发送)

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,失败则抛出异常。

故,下面两段代码是等价的:

#sock.sendall('Hello world\n')

#buffer = 'Hello world\n'
#while buffer:
#    bytes = sock.send(buffer)
#    buffer = buffer[bytes:]
send和sendall方法的区别

验证用户合法性

如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么利用hmac+加盐的方式来实现

demo1:

import socket
import hmac
import os
secret_key = b'egg'     # 定义一个和客户端的secret_key
IP_PORT = ('127.0.0.1', 8088)

sk_server = socket.socket()

sk_server.bind(IP_PORT)
sk_server.listen()

def check_conn(conn):
    msg = os.urandom(32)
    conn.send(msg)
    h = hmac.new(secret_key, msg)
    digest = h.digest()
    client_digest = conn.recv(1024) # 拿到客户端加密后的digest
    return hmac.compare_digest(digest, client_digest)   # 服务端和客户端对比


conn, addr = sk_server.accept()
ret = check_conn(conn)
if ret:
    print("合法的客户端")
    conn.close()
else:
    print("不合法的客户端")
    conn.close()
sk_server.close()
server
import socket
import hmac
secret_key = b'egg'     # 定义一个和服务端的secret_key

IP_PORT = ('127.0.0.1', 8088)
sk_client = socket.socket()
sk_client.connect(IP_PORT)
msg = sk_client.recv(1024)  
h = hmac.new(secret_key, msg)
digest = h.digest()

sk_client.send(digest)

sk_client.close()
client

demo2:

#_*_coding:utf-8_*_
from socket import *
import hmac,os

secret_key=b'bang bang'
def conn_auth(conn):
    '''
    认证客户端链接
    :param conn:
    :return:
    '''
    print('开始验证新链接的合法性')
    msg=os.urandom(32)
    conn.sendall(msg)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    respone=conn.recv(len(digest))
    return hmac.compare_digest(respone,digest)

def data_handler(conn,bufsize=1024):
    if not conn_auth(conn):
        print('该链接不合法,关闭')
        conn.close()
        return
    print('链接合法,开始通信')
    while True:
        data=conn.recv(bufsize)
        if not data:break
        conn.sendall(data.upper())

def server_handler(ip_port,bufsize,backlog=5):
    '''
    只处理链接
    :param ip_port:
    :return:
    '''
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(backlog)
    while True:
        conn,addr=tcp_socket_server.accept()
        print('新连接[%s:%s]' %(addr[0],addr[1]))
        data_handler(conn,bufsize)

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    server_handler(ip_port,bufsize)
服务端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
import hmac,os

secret_key=b'bang bang'
def conn_auth(conn):
    '''
    验证客户端到服务器的链接
    :param conn:
    :return:
    '''
    msg=conn.recv(32)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    conn.sendall(digest)

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    conn_auth(tcp_socket_client)

    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break

        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)
客户端(合法)
#_*_coding:utf-8_*_
from socket import *

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break

        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)
客户端(非法:不知道加密方式)
#_*_coding:utf-8_*_
from socket import *
import hmac,os

secret_key=b'linhaifeng bang bang bang1111'
def conn_auth(conn):
    '''
    验证客户端到服务器的链接
    :param conn:
    :return:
    '''
    msg=conn.recv(32)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    conn.sendall(digest)

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    conn_auth(tcp_socket_client)

    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break

        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)
客户端(非法:不知道secret_key)

socketserver

socketserver实现多并发

import socketserver


class MySocketServer(socketserver.BaseRequestHandler):  # 自定义一个socket类,但是必须继承socketserver.BaseRequestHandler
    def handle(self):   # 所有的连接收发都写在handle里面,  方法名必须是handle
        msg = self.request.recv(1024).decode('utf-8')
        print(msg)
        self.request.send("收到啦".encode('utf-8'))

if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('127.0.0.1', 8088), MySocketServer) # 创建一个server,绑定IP和端口,socketserver.ThreadingTCPServer((IP, PORT), 自定义类的类名)
    server.serve_forever()  # 让server永远运行下去,除非强制停止程序
server端
import socket

IP_PORT = ('127.0.0.1', 8088)

sk_client = socket.socket()
sk_client.connect(IP_PORT)

sk_client.send("哈喽...".encode('utf-8'))
msg = sk_client.recv(1024).decode('utf-8')
print(msg)
sk_client.close()
client端

参考 http://www.cnblogs.com/Security-Darren/p/4594393.html

 

 

 

说明:部分内容摘自景女神的博客

 

posted @ 2018-12-22 15:53  别来无恙-  阅读(1142)  评论(0编辑  收藏  举报