Python基础-week07 Socket网络编程

一 客户端/服务器架构

  1.定义

    又称为C/S架构,S 指的是Server(服务端软件),C指的是Client(客户端软件)

    本章的中点就是教大写写一个c/s架构的软件,实现服务端软件和客户端软件基于网络的通信。

  2.互联网中的c/s架构应用

    腾讯作为服务端为你提供视频,你得下个腾讯视频客户端才能看它的视频)
        浏览网页,浏览器是客户端软件,服务端软件在后端服务器上

  3.C/S架构与socket的关系:

    我们学习socket就是为了完成C/S架构的开发,其中socket是一个Python给我们封装好的,方便开发不需要非常精通各种网络协议即可进行相关软件开发。

 

二 osi七层

  1.前言

  我们从拥有一台个人计算机开始,就有意无意的接触了一台小型,所以一个完整的计算机系统:其中由硬件,计算机操作系统,应用软件等组成,

  而我们要上网看视频、聊天等,需要使用基于操作系统之上的应用软件或者各种客户端。那这些应用软件是如何连接互联网,与互联网上的其他电脑进行通信的呢?它基层的通信原理是怎么样的呢?请看下面分解。

  2.互联网协议由来

  如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。

  所以能够我们能够连接互联网,核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语。

  人们按照分工不同把互联网协议从逻辑上划分了层级,有国际组织就制订了统一的标准 7层的OSI 网络模型,当然还有其他分类。

 

  3.网络通信原理详解

    请看并了解基本网络基础知识,这个很重要。

    https://www.cnblogs.com/Jame-mei/p/9571728.html

 

  4.学习socket为何要学习网络协议呢?

    1.首先:本节课程的目标就是教会你如何基于socket编程,来开发一款自己的C/S架构软件

    2.其次:C/S架构的软件(软件属于应用层)是基于网络进行通信的

    3.然后:网络的核心即一堆协议,协议即标准,你想开发一款基于网络通信的软件,就必须遵循这些标准。

    4.最后:就让我们从这些标准开始研究,开启我们的socket编程之旅

    

 

  


三 socket层

  在图1中,我们没有看到Socket的影子,那么它到底在哪里呢?还是用图来说话,一目了然。

  

 

四 socket是什么

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

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

  

  也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序 而程序的pid是同一台机器上不同进程或者线程的标识

 

五 套接字发展史及分类

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

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

  套接字家族的名字:AF_UNIX

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

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

  套接字家族的名字:AF_INET

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


六 套接字工作流程

  1.套接字工作原理介绍

    在生活中,你要打电话给你的GrilFriends,先拨号,gf听到电话铃声后是在考虑接电话还是不接电话,如果接电话,这时候你和你的gf就完成建立了连接,就可以讲话了。

    如果gf想要结束交流,则告知会告知你她要敷面膜洗澡睡觉了,你也回应了好的,晚安,然后彼此结束交谈。

    所以下图,就是这个原理。

    

 

    先从服务端说起:服务端初始化Socket,然后与端口绑定(bind()),对端口进行监听(listen()),调用accept() 阻塞,等待客户端连接。

    在这个时候如果有一个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这是客户端与服务端的连接就建立了。

    客户端发送数据请求,服务端接收请求并处理请求,然后回应数据发送给客户端,客户端读取数据,最后彼此关闭连接,一次交互结束。

 

    2.socket 模块函数用法

      

# @Time    : 2018/9/6 16:05
# @Author  : Jame
import socket

'''
socket.socket(socket_family,socket_type,protocal=0)

socket_family 可以是AF_UNIX 或AF_INET.
socket_type   可以是SOCK_STREAM 或SOCK_DGRAM.
protocal      一般不填写,默认值为0

'''

#获取tcp/ip套接字
tcpSock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#获取upd/ip套接字
udpSock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

'''
由于socket模块中有太多的属性。
所以我们可以破例使用 from socket import * 
这样我们就把socket模块里所有的属性都带到了我们的命名空间中,可以缩减一些重复代码。
例如:tcpSock=socket(AF_INET,SOCK_STREAM)
'''

 

 

  3.服务端套接字函数、客户端套接字函数

    *服务端:

      server.bind()  绑定(主机ip,端口号) 到套接字

      server.listen() 开始tcp监听(1...)  缓冲池数量1到n个

      server.accept()  被动接受tcp 客户端的连接,(阻塞式)等待连接的到来

 

    *客户端:

      client.connect()   主动初始化tcp服务器连接

      client.connect_ex()  connect() 函数的扩展版本,出错时候返回错码,而不是抛出异常

    

 

  4.公共用途的套接字函数

    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()           
关闭套接字

  5.面向锁的套接字方法

    s.setblocking()  设置套接字的阻塞与非阻塞模式

    s.settimeout()   设置阻塞套接字操作的超时时间

    s.gettimeout()  得到阻塞套接字操作的超时时间

 

  6.面向文件的套接字的函数

    s.fileno()       套接字的文件描述符

    s.makefile()  创建一个与套接字相关的文件

     

# @Time    : 2018/9/6 16:43
# @Author  : Jame


# 1:用打电话的流程快速描述socket通信
# 2:服务端和客户端加上基于一次链接的循环通信
# 3:客户端发送空,卡主,证明是从哪个位置卡的
# 服务端:
from socket import *
phone=socket(AF_INET,SOCK_STREAM)
phone.bind(('127.0.0.1',8081))
phone.listen(5)

conn,addr=phone.accept()
while True:
    data=conn.recv(1024)
    print('server===>')
    print(data)
    conn.send(data.upper())
conn.close()
phone.close()



# 客户端:
from socket import *

phone=socket(AF_INET,SOCK_STREAM)
phone.connect(('127.0.0.1',8081))

while True:
    msg=input('>>: ').strip()
    phone.send(msg.encode('utf-8'))
    print('client====>')
    data=phone.recv(1024)
    print(data)




# 说明卡的原因:缓冲区为空recv就卡住,引出原理图
#
#
#
# 4.演示客户端断开链接,服务端的情况,提供解决方法
#
# 5.演示服务端不能重复接受链接,而服务器都是正常运行不断来接受客户链接的
#
tco套接字实例1
# 6:简单演示udp
# 服务端
from socket import *
phone=socket(AF_INET,SOCK_DGRAM)
phone.bind(('127.0.0.1',8082))
while True:
    msg,addr=phone.recvfrom(1024)
    phone.sendto(msg.upper(),addr)

# 客户端
from socket import *
phone=socket(AF_INET,SOCK_DGRAM)
while True:
    msg=input('>>: ')
    phone.sendto(msg.encode('utf-8'),('127.0.0.1',8082))
    msg,addr=phone.recvfrom(1024)
    print(msg)




# udp客户端可以并发演示
# udp客户端可以输入为空演示,说出recvfrom与recv的区别,暂且不提tcp流和udp报的概念,留到粘包去说
#
# 读者勿看:socket实验推演流程
udp套接字实例2

 

七 基于TCP的套接字

    Tcp是基于双向连接的,必须先启动服务端,然后启动客户端去连接服务端,建立初始连接。

  1.tcp服务端 和t cp客户端的主要步骤

    

ss = socket() #创建服务器套接字

ss.bind()      #把地址绑定到套接字

ss.listen()      #监听链接

inf_loop:      #服务器无限循环

    cs = ss.accept() #接受客户端链接

    comm_loop:         #通讯循环
        cs.recv()/cs.send() #对话(接收与发送)
    cs.close()    #关闭客户端套接字

ss.close()        #关闭服务器套接字(可选)
tcp服务端
cs = socket()    # 创建客户套接字

cs.connect()    # 尝试连接服务器

comm_loop:        # 通讯循环
     cs.send()/cs.recv()    # 对话(发送/接收)

cs.close()            # 关闭客户套接字
tcp客户端

  2.基于tcp的socket实现模拟打电话

    

# @Time    : 2018/9/6 16:55
# @Author  : Jame
import socket

ip_port=('127.0.0.1',8080)  #电话卡
BUFSIZE=1024                #收消息的大小
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
server.bind(ip_port) #手机插卡
server.listen(2) #手机待机等待接听


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

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


conn.send(msg.upper())  #发消息

conn.close()  #挂电话

server.close() #关机,防止骚扰

'''
接到来自127.0.0.1 的电话
b'jame is boy' <class 'bytes'>
'''
服务端实例
# @Time    : 2018/9/6 16:55
# @Author  : Jame

import socket

ip_port=('127.0.0.1',8080)  #电话卡
BUFSIZE=1024                #收消息的大小
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机

client.connect_ex(ip_port) #拨打电话



client.send('jame is boy'.encode('utf-8'))  #发消息,给服务端

msg=client.recv(BUFSIZE) #收消息,来自服务端回复
print(msg,type(msg))



client.close() #挂电话,不关机继续打给其他女孩,哈哈

'''
b'JAME IS BOY' <class 'bytes'>

'''
模拟客户端实例

 

  3.基于tcp的连接循环和通信循环的socket

    

# @Time    : 2018/8/27 10:45
# @Author  : Jame
import socket


#1买手机
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)


#2 插电话卡
server.bind(('127.0.0.1',8080))


#3.开机
server.listen(3)
print('server socket start...')

#4.等待电话连接中的请求,链接循环
while True:
    conn,client_addr=server.accept()
    print(conn,client_addr)


    #5.收/发信息,通信循环
    while True:
        try:
            data=conn.recv(1024)
            if len(data)==0:break  #针对linux max 系统
            print(data.decode('utf-8'))

            conn.send(data.upper())

        except ConnectionResetError: #针对windows 系统
            break


        conn.close()




server.close()
双向循环服务端实例
# @Time    : 2018/8/27 10:45
# @Author  : Jame
import socket


#1买手机
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)


#2 拨号
client.connect(('127.0.0.1',8080))

#3 发/收 信息
while True:
    msg=input('>>>:').strip()
    client.send(msg.encode('utf-8')) #必须是bytes类型

    data=client.recv(1024)
    print('服务端发来的消息:',data.decode('utf-8'))



client.close()
双向循环客户端实例

 

 

  4.解决服务端Address already in use

    这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的time_wait状态的优化方法)

    

#加入一条socket配置,重用ip和端口

phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))
方法1:调整代码
发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
vi /etc/sysctl.conf

编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
 
然后执行 /sbin/sysctl -p 让参数生效。
 
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;

net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;

net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。

net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
方法2:优化服务器参数

 

  

    


八 基于UDP的套接字

    UDP是不需要事先进行连接的,先启动哪一端都不会报错。

  1.udp服务端和udp客户端常用套路

   

  服务端: 

server=socket()  #创建一个服务器的套接字

server.bind()     #绑定服务器套接字

inf_loop:
    cs=server.recvfrom()  #对话(接送与发送)
    #或者 server.sendto() 


server.close()        #关闭服务器套接字

  

  客户端:

client=socket(...)  #创建套接字

comm_loop:       #通讯循环    
    client.sendto()  /client.recvfrom() #对话(发送、接收)


client.close()    #关闭客户端套接字    

 

 

  2.udp套接字简单实例

    

# @Time    : 2018/9/6 17:50
# @Author  : Jame
import socket

ip_port=('127.0.0.1',9090)
BUFSIZE=1024
udp_server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

udp_server.bind(ip_port)


while True:
    msg,client_addr=udp_server.recvfrom(BUFSIZE)
    print(msg,client_addr)


    udp_server.sendto(msg.upper(),client_addr)
udp服务端实例
# @Time    : 2018/9/6 17:50
# @Author  : Jame
import socket

ip_port=('127.0.0.1',9090)
BUFSIZE=1024
udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)




while True:
    msg=input('>>:').strip()
    if not msg:continue

    udp_client.sendto(msg.encode('utf-8'),ip_port)

    back_msg,addr=udp_client.recvfrom(BUFSIZE)
    print(back_msg.decode('utf-8'),addr)
udp客户端实例

 

  3.模拟qq聊天实例

     

# @Time    : 2018/9/6 18:00
# @Author  : Jame
import socket
ip_port=('127.0.0.1',9091)
Buffsize=1024
udp_server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
udp_server.bind(ip_port)



while True:
    qq_msg,addr=udp_server.recvfrom(Buffsize)
    print('来自[%s:%s]的一条信息:%s'%(addr[0],addr[1],qq_msg.decode('utf-8')))
    back_msg=input('回复信息:').strip()


    udp_server.sendto(back_msg.encode('utf-8'),addr)
模拟qq服务端实例
# @Time    : 2018/9/6 18:04
# @Author  : Jame
import socket
Buffsize=1024
udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

qq_name_dic={

    'dog':('127.0.0.1',9091),
    'cat':('127.0.0.1',9091),
    'pig':('127.0.0.1',9091)

}

while True:
    qq_name=input('请选择聊天对象:').strip()
    while True:
        msg=input('请输入消息,回车发送:').strip()
        if msg=='quit':break
        if not msg or not qq_name or qq_name not in qq_name_dic:continue

        udp_client.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])


        back_msg,addr=udp_client.recvfrom(Buffsize)
        print('来自[%s:%s]的一条消息:%s'%(addr[0],addr[1],back_msg.decode('utf-8')))



udp_client.close()
模拟qq客户端实例

 

  4.应用:ntp时间服务器

    

#Author http://www.cnblogs.com/Jame-mei
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 or len(msg)<6:
        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()
服务端
#Author http://www.cnblogs.com/Jame-mei
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()
客户端

 


九 粘包现象

  1.粘包问题发现tcp

    让我们基于tcp先制作一个远程执行命令的程序(1:执行错误命令 2:执行ls 3:执行ifconfig)

#Author http://www.cnblogs.com/Jame-mei
import socket
import subprocess

#1 买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print(phone)

#2 插电话卡
phone.bind(('127.0.0.1',8080))

#3 开机
phone.listen(3)
print('server  端启动')

#4 等待电话连接中
while True:
    conn,client_addr=phone.accept()
    print('客户端连接信息:',conn,client_addr)

    #5接受信息
    while True:
        try:
            data=conn.recv(1024)
            if  len(data)==0:break  #针对linux mac 的异常处理,不能接受空消息,否则处于一直等待中
            print('客户端发送的信息:',data)
            data=data.decode('utf-8')
            obj=subprocess.Popen(data,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)

            #服务端回信息
            stdout=obj.stdout.read() #byes类型
            stderr=obj.stderr.read()
            conn.send(stdout+stderr)
        except ConnectionResetError: #针对windows 系统的异常处理
            break


    #6最后关闭连接
    conn.close()

#7 关机
phone.close()
模拟ssh服务端实例
#Author http://www.cnblogs.com/Jame-mei
import socket
#1 买手机
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#2 插电话卡,发信息
client.connect(('127.0.0.1',8080))

while True:
    msg=input('Please input>>:').strip()
    if len(msg)==0:continue
    client.send(msg.encode('utf-8'))

    #3 客户端收信息
    data=client.recv(1024)
    print('服务端回复信息:',data.decode('gbk'))


#4 关闭客户端连接
client.close()
模拟ssh客户端实例

 

   注意注意注意:

   subprocess.Popen 的结果的编码是以当前所在的系统为准的。

   如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码,且只能从管道里读一次结果。

 

  注意:命令ls -l ; lllllll ; pwd 的结果是既有正确stdout结果,又有错误stderr结果

 

 

  2.udp为什么发生粘包问题 ? 

    udp不会发生粘包问题,除了超过udp接收数据的大小,会报错。

    

#Author http://www.cnblogs.com/Jame-mei
from socket import *
import subprocess

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

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

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

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

    #发消息
    udp_server.sendto(stderr+stdout,addr)

udp_server.close()
udp模拟ssh服务端实例
#Author http://www.cnblogs.com/Jame-mei
from socket import *
ip_port=('127.0.0.1',9003)
bufsize=1024

udp_client=socket(AF_INET,SOCK_DGRAM)


while True:
    msg=input('>>: ').strip()
    udp_client.sendto(msg.encode('utf-8'),ip_port)

    data,addr=udp_client.recvfrom(bufsize)
    print(data.decode('gbk'),end='')



'''
udp一次接收的数据量较小,因而适合dns解析,网页聊天等。
OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。

'''
udp模拟ssh客户端实例

 

    

  


十 什么是粘包?

  1.前言  

    只有tcp有粘包现象,udp永远不会粘包。这是为什么呢,需要了解回顾socket收发消息的原理。

    

 

 

  2.粘包形成的原理详解

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

     发送端可以是1kb 1kb 地发送数据,而接收端的应用程序可以2kb 2kb的提走数据,当然也可能一次提走3kb或者6kb数据,或者一次只提走几个字节。也就是说,应用程序所看到的数据是一           个整体,或者是一个流(stream)。一条消息有多少字节对应用程序来说是不可见的,因此TCP协议是面向流的协议。这也就是容易粘包的原因。 

      所谓粘包问题,主要还是因为接收方不知消息之间的界限,不知道一次性提取多少字节数据造成的。

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

 

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

 

    *TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发(client/server)两端都要有一一成对的socket,因此发送端为了将多个数据发送到接收端的包,更    有效的到对方,使用了优化算法(Nagle算法),将多次间隔较小且数据量较小的数据,合并成一个大的数据流,然后进行封包。这样,接收端就很难分辨出从该数据包里的几段数据的始末。所以必须提    供科学的拆包机制(定制报头字典等方法) 。 所以面向流的通信是无消息保护边界的。

 

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

 

  总结:

    tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息处理机制,防止程序阻塞卡住。而udp时基于数据报的,即便输入的消息为空,那也不是空消息,udp会帮你    封装上消息头,详情请九.2 udp ssh模拟实例。

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

    tcp的协议数据不会丢失,没有收完,下次会继续接收上次的,收取端收到ack才会清除缓冲区内容。数据是可靠的,但是会产生粘包问题。

  

    

 

  3.发生粘包的两种情况

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

       

# @Time    : 2018/9/7 15:02
# @Author  : Jame
from socket import *

ip_port=('127.0.0.1',8080)
tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(2)

conn,client_addr=tcp_server.accept()


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

print('第一条----->',data1.decode('utf-8'))
print('第二条---->',data2.decode('utf-8'))
server实例
# @Time    : 2018/9/7 15:02
# @Author  : Jame
from socket import *

ip_port=('127.0.0.1',8080)
BUFSIZE=1024

tcp_client=socket(AF_INET,SOCK_STREAM)

res=tcp_client.connect_ex(ip_port)


tcp_client.send('hello'.encode('utf-8'))
tcp_client.send('jame'.encode('utf-8'))
'''
第一条-----> hellojame
第二条----> 


第一条-----> hello
第二条----> jame

出现这种情况的原因就是2条数据量小,时间间隔短

'''
client实例

 

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

      

# @Time    : 2018/9/7 15:02
# @Author  : Jame
from socket import *

ip_port=('127.0.0.1',8081)
tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(2)

conn,client_addr=tcp_server.accept()


data1=conn.recv(2) #第一次设置只收2个字节 不收完
data2=conn.recv(10) #下次收的时候,会先收取旧的数据,然后取新的

print('第一条----->',data1.decode('utf-8'))
print('第二条---->',data2.decode('utf-8'))

'''
第一条-----> he
第二条----> llo  jame

#发生这种粘包问题是,收取放第一次只收取了2个字节,没有及时收取准确的大小,导致下一次收取了旧数据+新数据

'''


conn.close()
tcp_server.close()
server实例
# @Time    : 2018/9/7 15:02
# @Author  : Jame
from socket import *

ip_port=('127.0.0.1',8081)
BUFSIZE=1024

tcp_client=socket(AF_INET,SOCK_STREAM)

res=tcp_client.connect_ex(ip_port)


tcp_client.send('hello  jame'.encode('utf-8'))
client 实例

 

    拆包的情况发生:

      当发送端缓冲区的长度大于网卡的MTU时候,tcp会将这次发送的数据拆成几个数据包发送出去。

  4.补充tcp/udp  send/recv/sendall相关问题

    1):为何tcp是可靠的,udp是不可靠的?

      基于tcp的传输原理请参考:https://www.cnblogs.com/Jame-mei/p/9571728.html 

      因为tcp在传输的时候,发送端会把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所      以tcp是可靠的。

      udp发送数据的时候,对端是不会返回确认信息的,因此不可靠。

 

    2):send(字节流)和recv(1024)及sendall

      recv里指定1024意思是从缓存里一次拿出1024个字节的数据。

      send的字节流是先存入已端缓存,然后由协议控制将缓存内容发送对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。


十一 解决粘包问题的处理方法1

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

  

from socket import *
import subprocess
import struct

phone=socket(AF_INET,SOCK_STREAM)
phone.bind(('127.0.0.1',8081))
phone.listen(5)

print('服务的启动......')
# 连接循环
while True:
    conn,client_addr=phone.accept()
    print(client_addr)

    # 通信循环
    while True:
        try:
            cmd=conn.recv(1024)
            if not cmd:break

            obj=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE
                             )

            stdout=obj.stdout.read()
            stderr=obj.stderr.read()


            # 1、先发送固定长度的报头
            #目前报头里只包含数据的大小
            total_size=len(stdout) + len(stderr)
            conn.send(struct.pack('i',total_size))

            # 2、发送真实的数据
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break
    conn.close()

phone.close()
解决粘包server实例
from socket import *
import struct

phone=socket(AF_INET,SOCK_STREAM)
phone.connect(('127.0.0.1',8081))

while True:
    cmd=input('>>>: ').strip()
    if not cmd:continue
    phone.send(cmd.encode('utf-8'))

    #1、先收报头,从报头里取出对真实数据的描述信息
    header=phone.recv(4)
    total_size=struct.unpack('i',header)[0]

    #2、循环接收真实的数据,直到收干净为止
    recv_size=0
    res=b''
    while recv_size < total_size:
        recv_data=phone.recv(1024)
        res+=recv_data
        recv_size+=len(recv_data)

    print(res.decode('gbk'))


phone.close()
解决粘包client实例
import struct

obj=struct.pack('q',53112312311231223)
print(obj,len(obj))

#
# res=struct.unpack('i',obj)[0]
# print(res)
用到的struct模块

 

   

十二 解决粘包问题的终极方法

  思路:先发送指定的报头及报头内容,其中为字节流加上自定义的报头信息,包括字节流长度,md5值,文件名等等,对端先接受固定长度的报头,从中读取真实字节流长度及其他信息,然后循环收取,直到彻底收取完毕。

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

  常用的又struct('i',12345678),4个字节,l(long) 4个字节,q(longlong) 8个字节,具体请查阅帮助。

  

#_*_coding:utf-8_*_
#http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
__author__ = 'Linhaifeng'
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常用实例

 

 

  具体步骤:

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

 

    发送时:

 

    先发报头长度

 

    再编码报头内容然后发送

 

    最后发真实内容

 

 

 

    接收时:

 

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

 

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

 

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

 

from socket import *
import subprocess
import struct
import json

phone=socket(AF_INET,SOCK_STREAM)
phone.bind(('127.0.0.1',8081))
phone.listen(5)

print('服务的启动......')
# 连接循环
while True:
    conn,client_addr=phone.accept()
    print(client_addr)

    # 通信循环
    while True:
        try:
            cmd=conn.recv(1024)
            if not cmd:break

            obj=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE
                             )

            stdout=obj.stdout.read()
            stderr=obj.stderr.read()

            #制作报头
            header_dic={
                'filename':'a.txt',
                'total_size':len(stdout) + len(stderr),
                'md5':'xxxxxsadfasdf123234e123'
            }

            header_json = json.dumps(header_dic)
            header_bytes=header_json.encode('utf-8')

            #1、先发送报头的长度
            conn.send(struct.pack('i',len(header_bytes)))

            #2、再发送报头
            conn.send(header_bytes)

            #3、最后发送真实的数据
            conn.send(stdout)
            conn.send(stderr)


        except ConnectionResetError:
            break
    conn.close()

phone.close()
Server实例
from socket import *
import struct
import json

phone=socket(AF_INET,SOCK_STREAM)
phone.connect(('127.0.0.1',8081))

while True:
    cmd=input('>>>: ').strip()
    if not cmd:continue
    phone.send(cmd.encode('utf-8'))

    #1、先收报头的长度
    obj=phone.recv(4)
    header_size=struct.unpack('i',obj)[0]

    #2、再接收报头
    header_bytes=phone.recv(header_size)
    header_json=header_bytes.decode('utf-8')
    header_dic=json.loads(header_json)
    print(header_dic)

    total_size=header_dic['total_size']
    #3、循环接收真实的数据,直到收干净为止
    recv_size=0
    res=b''
    while recv_size < total_size:
        recv_data=phone.recv(1024)
        res+=recv_data
        recv_size+=len(recv_data)

    print(res.decode('gbk'))


phone.close()
Client实例

 


十三 认证客户端的链接合法性

  思路:在分布式系统中,简单的实现一个客户端认证功能,不像sll那么复杂的,利用hmac+验证的方式来实现。

# @Time    : 2018/9/7 16:02
# @Author  : Jame
from socket import *
import hmac,os


secret_key=b'jia zhuang shi secret key file are you ok?'


def con_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 con_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:
    :param bufsize:
    :param backlog:
    :return:
    '''
    tcp_socket=socket(AF_INET,SOCK_STREAM)
    tcp_socket.bind(ip_port)
    tcp_socket.listen(backlog)
    while True:
        conn,addr=tcp_socket.accept()
        print('新连接[%s :%s]'%(addr[0],addr[1]))
        data_handler(conn,bufsize)



if __name__ == '__main__':
    ip_port=('127.0.0.1',8080)
    bufsize=1024
    server_handler(ip_port,bufsize)
服务端
# @Time    : 2018/9/7 16:02
# @Author  : Jame
from socket import *
import hmac,os

secret_key=b'jia zhuang shi secret key file are you ok?'
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_client=socket(AF_INET,SOCK_STREAM)
    tcp_client.connect(ip_port)

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

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

    tcp_client.close()



if __name__ == '__main__':
    ip_port=('127.0.0.1',8080)
    bufsize=1024
    client_handler(ip_port,bufsize)





'''
server输出:
新连接[127.0.0.1 :8761]
开始验证新连接合法性?
链接合法,开始通信!



client输出:
>>>>:nihao
NIHAO
>>>>:


'''
客户端(合法验证通过)
# @Time    : 2018/9/7 16:02
# @Author  : Jame
from socket import *
import hmac,os

'''
非法客户端:不知道加密方式!!!
secret_key=b'jia zhuang shi secret key file are you ok?'
def conn_auth(conn):
   
    msg=conn.recv(32)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    conn.sendall(digest)
'''


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

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

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

    tcp_client.close()



if __name__ == '__main__':
    ip_port=('127.0.0.1',8080)
    bufsize=1024
    client_handler(ip_port,bufsize)



'''
server输出:
新连接[127.0.0.1 :8746]
开始验证新连接合法性?
该链接不合法,请关闭!


client输出:
>>>>:nihao
Traceback (most recent call last):
  File "E:/pythonwork/s14/day9/基于认证的客户端连接合法性/非法客户端.py", line 39, in <module>
    client_handler(ip_port,bufsize)
  File "E:/pythonwork/s14/day9/基于认证的客户端连接合法性/非法客户端.py", line 30, in client_handler
    print(respone.decode('utf-8'))
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc9 in position 1: invalid continuation byte

'''
客户端(非法没有加密方式)
# @Time    : 2018/9/7 16:02
# @Author  : Jame
from socket import *
import hmac,os

secret_key=b'bu zhi dao secret key file????'
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_client=socket(AF_INET,SOCK_STREAM)
    tcp_client.connect(ip_port)

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

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

    tcp_client.close()



if __name__ == '__main__':
    ip_port=('127.0.0.1',8080)
    bufsize=1024
    client_handler(ip_port,bufsize)


'''
server输出:
新连接[127.0.0.1 :8795]
开始验证新连接合法性?
该链接不合法,请关闭!




client输出:
>>>>:nihao
Traceback (most recent call last):
  File "E:/pythonwork/s14/day9/基于认证的客户端连接合法性/非法客户端2.py", line 41, in <module>
    client_handler(ip_port,bufsize)
  File "E:/pythonwork/s14/day9/基于认证的客户端连接合法性/非法客户端2.py", line 31, in client_handler
    respone=tcp_client.recv(bufsize)
ConnectionAbortedError: [WinError 10053] 您的主机中的软件中止了一个已建立的连接。



'''
客户端(非法加密方式错误)

 

十四 socketserver实现并发

   1):模拟并发效果:连接循环+通信循环

    

# @Time    : 2018/8/27 10:45
# @Author  : Jame
import socket


#1买手机
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)


#2 插电话卡
server.bind(('127.0.0.1',8080))


#3.开机
server.listen(3)
print('server socket start...')

#4.等待电话连接中的请求,链接循环
while True:
    conn,client_addr=server.accept()
    print(conn,client_addr)


    #5.收/发信息,通信循环
    while True:
        try:
            data=conn.recv(1024)
            if len(data)==0:break  #针对linux max 系统
            print(data.decode('utf-8'))

            conn.send(data.upper())

        except ConnectionResetError: #针对windows 系统
            break


        conn.close()




server.close()
服务端实例
# @Time    : 2018/8/27 10:45
# @Author  : Jame
import socket


#1买手机
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)


#2 拨号
client.connect(('127.0.0.1',8080))

#3 发/收 信息
while True:
    msg=input('>>>:').strip()
    client.send(msg.encode('utf-8')) #必须是bytes类型

    data=client.recv(1024)
    print('服务端发来的消息:',data.decode('utf-8'))



client.close()
客户端实例

 

 

  2):socketserver模块使用

    (1).socketserver原理

      基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环

 

      socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)

      *server类图:

      

 

 

      *request类图:

           

 

       *继承关系:具体请分析源码

 

    (2).基于socketserver实例

 

# @Time    : 2018/8/28 11:14
# @Author  : Jame
import socketserver
import subprocess
import struct
import json

class MyTcphandler(socketserver.BaseRequestHandler):
    def handle(self):
        #通信循环
        while True:
            try:
                cmd=self.request.recv(1024)
                if len(cmd)==0:break
                print('收到:%s:%s的命令:%s'%(self.client_address[0],self.client_address[1],cmd.decode('utf-8')))

                obj = subprocess.Popen(
                    cmd.decode('utf-8'),
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE
                )
                stdout=obj.stdout.read()
                stderr=obj.stderr.read()

                #1制作报头
                header_dic={
                    'total_size':len(stdout)+len(stderr),
                    'filename':'a.txt'

                }
                #转换成json (str类型),再将str类型转化成bytes
                header_json=json.dumps(header_dic)
                header_bytes=header_json.encode('utf-8')

                #2 发送报头长度,报头的数据
                self.request.send(struct.pack('i',len(header_bytes)))
                self.request.send(header_bytes)

                #3 最后发送真是数据
                self.request.send(stderr)
                self.request.send(stdout)



            except ConnectionResetError:
                break





if __name__ == '__main__':
    server=socketserver.ThreadingTCPServer(('127.0.0.1',8088),MyTcphandler)
    socketserver.TCPServer.request_queue_size=3
    server.serve_forever()
服务端实例
# @Time    : 2018/8/28 11:20
# @Author  : Jame
import socket
import struct
import json

#1 买手机
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式协议=>SOCK_STREAM
#2 拨号
client.connect(('127.0.0.1',8088))


#3 发 收信息
while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    client.send(msg.encode('utf-8')) # b''



    #3.1 收取报头长度,报头的数据
    header_size=struct.unpack('i',client.recv(4))[0]

    header_bytes=client.recv(header_size)
    header_json=header_bytes.decode('utf-8')
    header_dic=json.loads(header_json)
    print(header_dic)

    #3.2 读取真是返回数据
    total_size=header_dic['total_size']
    res = b''
    recv_size =0
    while recv_size < total_size:
        data = client.recv(1024)
        res += data
        recv_size += len(data)

    print(res.decode('gbk'))


client.close()
客户端实例

 

十五.FTP 小程序支持用户

一.需求
  开发一个支持多用户在线的FTP程序
  要求:
      1.用户加密认证
      2.允许同时多个用户登陆
      3.每个用户有自己的家目录,且只能访问自己的家目录
      4.对用户进行磁盘配额,每个用户的可用空间不同

      5.运行用户在ftp server上随意切换目录(*)
      6.运行用户查看当前目录下的文件
      7.运行上传和下载文件,保证文件的一致性
      8.文件传输过程中显示进度条
      9.支持文件的断电续传功能(*)


  二.分析与思路

      角色1:用户consumer
         功能:
          *注册 *登陆(用hashlib md5进行加密,登陆用正向验证,因为md5反解难度大)

          *有自己的家目录,只能访问自己的家目录内的文件及文件夹

          *不同用户磁盘空间大小不同{
              思路:
              初始化的时候分配随机额度0-50m,并且用户上传的时候跟空间额度比对,会员vip充值,充值后可以提升存储空间。
          }

          *上传 put ,下载get ,并显示进度条,保证文件一致性(md5)
          {
              思路:
              上传:可以上传本地路径存在的文件,到服务端的默认保存位置或者家目录。

              下载:下载当前家目录里的某个路径的文件,到客户端本地的默认保存位置。
          }

          *断点续传(
          思路:
          创建临时文件,客户端上传时,服务器检查临时文件,有就发大小发给客户端,客户端seek到文件断点处给服务器端发送数据。
          )

          *用户在ftp server上随意切换目录(
              思路:
              用户操作使用cd命令,可以切换到家目录的任意目录,再用ls查看会显示切换后的目录,下次登陆会记住切换后的地址,直接登陆改地址
          )



      角色2:管理员 admin
        功能:
         *查看用户列表信息
         *修改用户密码
         *删除用户(删除状态,而不是真正删除数据)
         *可以对用户的磁盘额度进行调整
         *设置黑名单,禁止用于登陆和使用
  https://gitee.com/meijinmeng/Ftp_system_v0.1.git
项目思路及gitee地址

 

 

 

 

posted @ 2018-09-06 17:15  meijinmeng  阅读(272)  评论(0编辑  收藏  举报