socket编程

一、客户端/服务端架构

'''
硬件C/S架构: 打印机
软件C/S架构:
    即服务端通过socket提供服务,客户端通过对应的IP端口访问服务器资源

B/S架构,即浏览器/服务端架构,此架构是用现成的浏览器作为客户端工具

'''

二、OSI模型

互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层

每层运行的常见物理设备

TCP/IP五层模型
1、物理层
    主要基于电器特性发送高低电频信号,高电压对应的数字是1,低电压对应数字是0 
    
2、数据链路层
    由来:单纯的电信号0和1没有任何意义,必须规定电信号多少位一组 
    即:数据链路层定义了电信号的分组方式

'''
以太网协议
    早期的时候各个公司有自己的分组方式,后来形成统一标准,即以太网协议Ethernet
    Ethernet规定:
        一组电信号构成一个数据包,叫做帧
        每组数据帧分成:报头head和数据data两部分
        head(固定18字节)包含:
            发送者/源地址:6字节
            接收者/目标地址:6字节
            数据类型:6字节
        data(最短46字节,最长1500字节)
            包含具体数据内容
    head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送

mac地址(media access control)
    head中包含的源和目标地址由来:Ethernet规定接入Internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡地址,即mac地址
    mac地址:每块网卡出厂时被烧制上一个世界唯一的mac地址,长度为48位二进制,通常由12位16进制表示(前6位是厂商编号,后六位是流水线编号)
    28-F1-0E-2D-65-9A

广播
    有了mac地址,统一局域网的两台主机就可以通信了(一台主机通过arp协议获取另一台主机的mac地址)
    Ethernet采用了最原始的方式,广播进行通信,即计算机通信基本靠吼
'''

3、网络层
    由来:
        有了Ethernet、mac地址、广播的发送方式,世界上的计算机就可以彼此通信了?问题是世界范围的互联网是由一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网广播的方式
        那么一台机器发送的包全世界都会收到,这就不仅仅是效率问题了,而是一种灾难
    
    网络层功能:为了解决广播的方式通信带来的问题,引入了一套新的地址来区分不同广播域/子网,这套地址即是网络地址
    
'''
IP协议:
    规定网络地址的协议叫ip协议,它定义的地址称为ip地址,广泛采用v4版本,即ipv4,它规定网络地址有32位2进制表示
    范围:0.0.0.0-255.255.255.255
    一个ip地址通常写成四段十进制数,例如192.168.15.60   俗称点分十进制
    ip地址分为两部分:
        网络部分:标识子网
        主机部分:标识主机
        注意:单纯的ip地址段只是标识了ip地址的种类,从网络部分或主机部分都无法辨识一个ip所处的子网
        例:172.16.10.1与172.16.10.2并不能确定二者处于同一子网
    子网掩码
        所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。
        比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。
    
    知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。
    方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0)
    然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。
    ip协议的作用
        为每一台计算机分配IP地址
        确定哪些地址在同一个子网
'''    
    ip数据包
    ip数据包分为head+data两部分,无须为ip包定义单独的栏位,直接放入以太网包的data部分
        head:长度为20-60字节
        data:最长为65515字节
    而以太网数据包的数据部分,最长只有1500字节,因此ip数据包超过了1500字节,它就需要分割成几个以太网数据包,分开发送
'''
ARP协议(Address Resolution Proctol)
    由来:计算机通信基本靠吼,即广播方式,所有上层的包到最后都要封装上以太网头,然后通过以太网协议发送
    通信是基于mac的广播方式实现,计算机在发包时,获取自身的mac是容易的
    如何获取目标主机的mac,就需要通过ARP协议
'''
4、传输层
    由来:
        网络层的IP帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序
        那么通过ip和mac找到一台特定的主机,如何标识这台主机上的应用程序,答案就是端口,端口即应用程序的与网卡关联的编号
    功能:
        建立端口到端口的通信
    端口范围:
        0-65535   其中0-1023为系统占用端口
'''
tcp协议:
    又叫做流式协议,可靠传输,
    tcp数据包没有长度限制,理论上可以无限长,但是为了保证网络效率
    通常tcp数据包的长度不能超过IP数据包的长度,以确保单个tcp数据包不必再分割
    以太网头   ip头    tcp头    数据

udp协议:
    不可靠传输,报头部分一共8个字节,总长度不超过65535字节,正好放进一个ip数据包
    以太网头   ip头    udp头      数据
'''
    
5、应用层
    由来:
        用户使用的都是应用程序,均工作于应用层
    功能:规定应用程序的数据格式
tcp/ip五层模型扫盲

 tcp三次握手及四次断开

三、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是同一台机器上不同进程或者线程的标识

分类:
    基于文件的套接字家族:AF_UNIX
        unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
    基于网络的套接字家族:AF_INET
        所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候只使用AF_INET
'''

套接字工作原理

四、基于tcp的套接字(入门)

######################服务端##############################
#
!/usr/bin/env python # -*- coding: utf-8 -*- # author: Fred_li import socket # step1 创建server套接字对象 # socket_family 可以是 AF_UNIX 或 AF_INET # socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM SOCK_STREAM tcp套接字 SOCK_DGRAM udp套接字 # proto 协议编号,默认是0,一般不需要指定 server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # step2 绑定IP和端口到server对象 server.bind(('127.0.0.1',8082)) # IP和端口以元组形式传入 # step3 开始tcp监听 server.listen(5) # backlog 半连接池:控制的是同一时刻客户端的请求连接数(只是控制客户端的请求连接数,并不影响已建立连接的个数) print('Server is running') # 此时server套接字对象已经启动了 # step4 接收客户端连接 # server.accept() 有客户端连接进来,accep()会生成本次会话的(套接字对象(用来接收和发送数据),客户端IP和端口) 元组形式 被动接收 ''' (<socket.socket fd=552, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8082), raddr=('127.0.0.1', 58929)>, ('127.0.0.1', 58929)) ''' conn,client_address=server.accept() # step5 接/收消息 data=conn.recv(1024) # 1024的单位是bytes 代表最大接收1024bytes print('收到客户端(%s)消息:' %client_address[0],data) conn.send(data.upper()) # 服务端处理完后,回给客户端 # step6 关闭与客户端连接 conn.close() # step7 关闭套接字对象 server.close()
#######################客户端###############################
#
!/usr/bin/env python # -*- coding: utf-8 -*- # author: Fred_li import socket # step1 创建客户端套接字对象 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # step2 尝试连接服务器 client.connect(('127.0.0.1',8082)) # 服务端IP和端口 # step3 发/收消息 client.send('hello'.encode('utf8')) # 网络上的数据都以bytes格式传输,所以此处需要编码 data=client.recv(1024) # 指定接收数据包的大小 print('收到服务端消息:',data) client.close() # 关闭客户端套接字对象

五、基于tcp套接字,加入通信循环

加入通信循环bug版:

##############服务端##########################
import
socket server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',8100)) server.listen() print('server is running') conn,client_address=server.accept() while True: data=conn.recv(1024) print('收到客户端(%s):' %client_address[0],data) conn.send(data.upper()) conn.close() server.close()
######################客户端#######################
import
socket client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8100)) while True: msg=input('请输入发送消息: ').strip() client.send(msg.encode('utf8')) data=client.recv(1024) print('收到服务端消息:',data) client.close

以上版本,如果客户端强制关掉连接或者网络断开

Windows系统下,服务端直接崩溃:

Linux或mac系统下,服务端会因为收到空消息而陷入死循环

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


import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8200))
server.listen()

print('server is running')

conn, client_address = server.accept()

while True:
    try:
        data = conn.recv(1024)
        if len(data) == 0: break  # 针对linux或者mac系统,客户端异常终止或者消息为空的情况下,服务端单方面关闭连接
        print('收到客户端(%s):' % client_address[0], data)
        conn.send(data.upper())
    except ConnectionResetError:  # 针对Windows系统,客户端异常终止或者消息为空的情况下,服务端单方面关闭连接
        break

conn.close()
server.close()
服务端针对以上问题修正

六、基于tcp套接字,加入通讯循环和链接循环

'''
上面的版本,一次连接结束后,服务端程序也跟着退出,这明显不是我们想要的(server端一定是一直提供服务才对,不应该跟着客户端断开而断开)
所以需要加入链接循环,即第一个客户端连接断开后,继续服务后面的客户端
'''
######################服务端##################################
#
!/usr/bin/env python # -*- coding: utf-8 -*- # author: Fred_li import socket
server
= socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('127.0.0.1', 8002)) server.listen() print('server is running') while True: conn, client = server.accept() while True: try: data = conn.recv(1024) if len(data) == 0: break print('收到来自客户端(%s:%s)的消息:' %(client[0],client[1]),data) conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
#!/usr/bin/env python
# -*- coding: utf-8 -*-  
# author: Fred_li

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8002))
while True:
    msg = input('请输入发给服务端的消息:').strip()
    client.send(msg.encode('utf8'))
    data = client.recv(1024)
    print('收到来自服务端的消息:', data)

client.close()
客户端1
#!/usr/bin/env python
# -*- coding: utf-8 -*-  
# author: Fred_li

import socket

client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8002))
while True:
    msg=input('请输入发给服务端的消息:').strip()
    client.send(msg.encode('utf8'))
    data=client.recv(1024)
    print('收到来自服务端的消息:',data)

client.close()
客户端2
验证:
单连接架构下:
D:Python36python.exe D:/python_study_new/day9/复习/server.py server is running 收到来自客户端(127.0.0.1:62619)的消息: b'i will send msg' # 只有当客户端1和服务端会话连接结束后,客户端2才能和服务端交互 收到来自客户端(127.0.0.1:62620)的消息: b'i will send msg' # 客户端1会话连接没结束前,客户端2一直出去阻塞状态
#!/usr/bin/env python
# -*- coding: utf-8 -*-  
# author: Fred_li

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8002))
while True:
    msg = input('请输入发给服务端的消息:').strip()
    if len(msg) == 0:continue        # 判断是否为空消息
    client.send(msg.encode('utf8'))
    print('消息已发送')
    data = client.recv(1024)
    print('收到来自服务端的消息:', data)

client.close()
解决客户端发送的空消息导致服务端收消息阻塞的bug

 端口被使用的问题处理:

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

#加入一条socket配置,重用ip和端口
import socket
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
server.bind(('127.0.0.1',8080))
server.listen()
print('server is running')
while True:
    conn,client=server.accept()
    while True:
        data=conn.recv(1024)
        conn.send(data.upper())
    conn.close()

server.close()
加入一条socket配置,重用IP和端口
发现系统存在大量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 时间
Linux系统下通过调整内核参数

七、模拟ssh远程执行命令

######################################服务端#######################################
import socket
import subprocess

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 就是它,在bind前加
server.bind(('127.0.0.1', 43300))
server.listen()
print('server is running')
while True:
    conn, client = server.accept()
    while True:
        try:
            data = conn.recv(1024).decode('gbk')
            if len(data) == 0: break
            cmd_obj = subprocess.Popen(
                data,
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
            )
            stdout = cmd_obj.stdout.read()
            stderr = cmd_obj.stderr.read()
            conn.send(stdout + stderr)
            print('%s:的执行结果已发送给==========%s:%s' % (data, client[0], client[1]))
        except ConnectionResetError:
            break
    conn.close()

server.close()
######################################客户端#######################################
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8002))
while True:
    msg = input('请输入发给服务端的消息:').strip()
    client.send(msg.encode('utf8'))
    data = client.recv(1024)
    print('收到来自服务端的消息:', data)

client.close()
######################################服务端#######################################
D:Python36python.exe D:/python_study_new/test.py
server is running
netstat -an |findstr 43300:的执行结果已发送给==========127.0.0.1:57411
ls:的执行结果已发送给==========127.0.0.1:57411

######################################客户端#######################################
D:Python36python.exe D:/python_study_new/day9/复习/client1.py
[user@fred_li]$netstat -an |findstr 43300
  TCP    127.0.0.1:43300        0.0.0.0:0              LISTENING
  TCP    127.0.0.1:43300        127.0.0.1:57411        ESTABLISHED
  TCP    127.0.0.1:57411        127.0.0.1:43300        ESTABLISHED

[user@fred_li]$ls
'ls' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
执行结果

八、基于udp的套接字

#####################################服务端######################################
import socket
import subprocess

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 9090))

while True:
    cmd, addr = server.recvfrom(1024)
    if len(cmd) == 0: break
    cmd_obj = subprocess.Popen(
        cmd.decode('gbk'),
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    stdout = cmd_obj.stdout.read()
    stderr = cmd_obj.stderr.read()
    msg = stdout + stderr
    print(len(msg))
    server.sendto(stderr + stdout, addr)

server.close()###################################客户端######################################import socket

client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
ip_port=('127.0.0.1',9090)

while True:
    msg=input('请输入要发送给服务端的消息: ').strip()
    client.sendto(msg.encode('gbk'),ip_port)
    back_msg,addr=client.recvfrom(1024)
    print(back_msg.decode('gbk'))

client.close()

# udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
# windows系统接收端会报如下错误,mac或者windows则不报,会丢弃多余的数据
OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。

UDP协议的特点:
  1、当发送的数据报大于接收数据包的缓冲区的大小时:
    Windows系统:接收端抛出异常
    Linux系统:接收端不会抛出异常,会丢弃多余的数据
  2、没有粘包问题
  3、能够稳定传输的最大数据量为512Bytes

九、什么是粘包

'''
如上图,socket收发消息原理
发送端可以是1k 1k的发送数据,而接收端应用程序可以2k 2k的取走数据,当然也能一次取走3k或6k的数据,或者一次只取走对应字节的数据,也就是说,应用程序所看到的的数据时一个整体
一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因

而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位取走数据,不能提取任意字节的数据,这一点和TCP是不相同的

如何定义消息
    可以认为对方一次性的write/send的数据为一个消息,需要明白的是当对方send一条消息的时候,无论底层怎样分段分片,tcp协议层会把构成整条消息的数据排序完成后才呈现在应用系统内存中

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

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

此外,发送方引起的粘包是由tcp协议本身造成的,TCP为了提高传输效率,发送方往往要收集足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少
通常TCP会把根据优化算法把这些数据合成一个TCP段后一次性发出去,这样接收方就收到了粘包数据
'''
'''
TCP(Transport Control Protocol 传输控制协议)是面向连接的
    面向流的提供可靠性服务收发两端都是成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发给对方,使用了优化算法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样接收端就难以分辨出来,必须提供科学的拆包机制,即面向流的通信是无消息边界的

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

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

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

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

'''

十、两种情况下tcp会发生粘包

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

#####################################服务端######################################
import socket

server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',9200))
server.listen()
print('server is running')
conn,client=server.accept()
data1=conn.recv(20)
data2=conn.recv(20)
print(data1)
print(data2)
conn.send(data1+data2.upper())
conn.close()
server.close()
服务端
###################################客户端######################################
import socket

client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',9200))
client.send('this is first msg'.encode('utf8'))
client.send('this is second msg'.encode('utf8'))
data=client.recv(1024)
print(data)
client.close()
客户端
##########################验证结果#############################################3

#
客户端本来发送了两条消息: client.send('this is first msg'.encode('utf8')) client.send('this is second msg'.encode('utf8')) 服务端收到的消息: D:Python36python.exe D:/python_study_new/day9/udp-server.py server is running 第一次接收: b'this is first msgthi' 第二次接收: b's is second msg' # 很明显前后两条消息粘包了

2、接收方不及时(或者发送的包大于接收方的缓冲区大小)接收缓冲区的数据,接收方下次还会接收上次残留在缓冲区的数据,造成粘包

import socket
import subprocess

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 43400))
server.listen()
print('server is running')
while True:
    conn, client = server.accept()
    while True:
        try:
            data = conn.recv(1024).decode('gbk')
            if len(data) == 0: break
            cmd_obj = subprocess.Popen(
                data,
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
            )
            stdout = cmd_obj.stdout.read()
            stderr = cmd_obj.stderr.read()
            print(len(stdout.decode('gbk'))+len(stderr.decode('gbk')))
            conn.send(stderr+stdout)
            print('%s:的执行结果已发送给==========%s:%s' % (data, client[0], client[1]))
        except ConnectionResetError:
            break
    conn.close()

server.close()
服务端
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 43400))
while True:
    cmd = input('[user@fred_li]$').strip()
    if len(cmd) == 0:continue        # 判断是否为空消息
    client.send(cmd.encode('gbk'))
    data = client.recv(100)
    print(data.decode('gbk'))

client.close()
客户端
执行结果:
服务端:
D:Python36python.exe D:/python_study_new/day9/server.py
server is running
202
netstat -an |findstr 43400:的执行结果已发送给==========127.0.0.1:50222
35
ls:的执行结果已发送给==========127.0.0.1:50222
35
ls:的执行结果已发送给==========127.0.0.1:50222

客户端
D:Python36python.exe D:/python_study_new/day9/复习/client1.py
[user@fred_li]$netstat -an |findstr 43400 
  TCP    127.0.0.1:43400        0.0.0.0:0              LISTENING
  TCP    127.0.0.1:43400        12
[user@fred_li]$ls
7.0.0.1:50222        ESTABLISHED
  TCP    127.0.0.1:50222        127.0.0.1:43400        ESTABLISHED
[user@fred_li]$ls

'ls' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

# 1、客户端第一条命令:netstat -an |findstr 43400 服务端会给客户端的结果大小为202,但是客户端一次只能收100字节,即还有102字节的数据在客户端缓存中
# 2、当客户端执行第二条命令:ls时,结果本应该是”
'ls' 不是内部或外部命令,也不是可运行的程序或批处理文件。”,但是实际是客户端要先收自己缓存中上一条命令的残留的数据,即netstat -an |findstr 43400的残留结果
# 3、客户端执行第三条命令:ls执行的结果,其实是收上一条命令的残留数据,以此下去,数据就全乱了。
**************造成此结果的罪魁祸首就是粘包造成的***********************************

 十一、自定义报头解决粘包问题

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

import socket
import subprocess
import struct

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 43200))
server.listen()
print('server is running')

while True:
    conn, client = server.accept()

    while True:
        try:
            cmd = conn.recv(1024)
            if len(cmd) == 0: break  # 解决mac 或 Linux系统由于客户端异常终止或者发送空消息的情况,服务端单方面终止连接,避免死循环
            # 执行系统命令并且拿到命令的结果
            cmd_obj = subprocess.Popen(
                cmd.decode('utf8'),
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
            )
            stdout = cmd_obj.stdout.read()
            stderr = cmd_obj.stderr.read()
            # 制作固定长度的报头
            header = struct.pack('i', len(stdout) + len(stderr))
            # 对于文件,可通过os.path.getsize('filename')得到文件大小
            # header=struct.pack('i',os.path.getsize('test.txt'))

            # 首先发送报头给客户端
            conn.send(header)
            print('发送报头给%s:%s' % (client[0], client[1]))

            # 再发送真是数据
            conn.send(stdout + stderr)
            # 对于文件,可通过rb模式打开,然后一行一行发给客户端
            # with open('a.txt','rb') as f:
            #     for line in f:
            #         conn.send(line)
            print('发送真实数据完成')
        except ConnectionResetError:  # 针对Windows系统,客户端异常终止,服务端单方面断开,避免报错
            break

    conn.close()

server.close()
###########################################客户端######################################################
import
socket import struct client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 43200)) while True: cmd = input('[user@host ~]$').strip() if len(cmd) == 0: continue client.send(cmd.encode('utf8')) header = client.recv(4) # 只收4bytes的报头 total_size = struct.unpack('i', header)[0] res = b'' # 定义bytes类型的空数据 recv_size = 0 # 定义初始接收size while recv_size < total_size: data = client.recv(1024) res += data recv_size += len(data) print(res.decode('gbk')) # windows系统编码为gbk client.close()

 十二、简单ftp上传功能

#!/usr/bin/env python
# -*- coding: utf-8 -*-  
# author: Fred_li
#####################服务端########################

import json
import struct
import socket
import os
import sys


class MYFTPServer:
    family = socket.AF_INET  # 定义socket协议族
    tcp_type = socket.SOCK_STREAM  # 定义socket类别(tcp或udp)
    allow_reuse_address = False  # 是否允许重用IP和端口    正式环境这个参数是False的,可能跟别的程序端口冲突
    request_queue_numbs = 5  # 定义半连接池的大小  控制的是同一时刻客户端的请求连接数(只是控制客户端的请求连接数,并不影响已建立连接的个数)
    upload_dir = os.path.join(os.path.dirname(__file__), 'upload')  # 定义客户端上传文件的目录名称
    coding = 'utf8'
    max_package_size = 4096  # 定义recv的大小(最大接收大小)

    def __init__(self, server_address, bind_and_active=True):
        self.server_address = server_address
        self.socket = socket.socket(self.family, self.tcp_type)
        if bind_and_active:
            try:
                self.server_bind()
                self.server_active()
            except:
                self.server_close()
                raise

    def server_bind(self):
        if self.allow_reuse_address:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)
        self.server_address = self.socket.getsockname()

    def server_active(self):
        self.socket.listen(self.request_queue_numbs)
        print('server is running')

    def get_request(self):
        return self.socket.accept()

    def server_close(self):
        self.socket.close()

    def close_request(self, request):
        request.close()

    def run(self):
        while True:
            self.conn, self.client = self.get_request()
            print(self.client)
            while True:
                try:
                    header = self.conn.recv(4)
                    if not header: break

                    header_size = struct.unpack('i', header)[0]  # 得到报头的大小
                    header_json = self.conn.recv(header_size).decode(self.coding)  # # 得到报头的json数据
                    header_dic = json.loads(header_json)  # json反序列化成Python数据类型
                    '''
                    header_dic = {
                        'cmd': cmd,
                        'filename': filename,
                        'filesize': filesize,
                         }
                    '''
                    print(header_dic)

                    cmd = header_dic['cmd']
                    if hasattr(self, cmd):
                        func = getattr(self, cmd)
                        func(header_dic)
                except Exception:
                    break

    def humanbytes(self, B):
        'Return the given bytes as a human friendly KB, MB, GB, or TB string'
        B = float(B)
        KB = float(1024)
        MB = float(KB ** 2)  # 1048576
        GB = float(KB ** 3)  # 1073741824
        TB = float(KB ** 4)  # 1099511627776

        if B < KB:
            return '{0} {1}'.format(B, 'Bytes')
        elif KB <= B < MB:
            # 0:整数位   2f:小数位 ,不四舍五入
            return '{0:.2f} KB'.format(B / KB)  #
        elif MB <= B < GB:
            return '{0:.2f} MB'.format(B / MB)
        elif GB <= B < TB:
            return '{0:.2f} GB'.format(B / GB)
        elif TB <= B:
            return '{0:.2f} TB'.format(B / TB)

    def progres(self, num, Sum):
        """
        显示上传进度条
        num:已上传大小
        Sum:文件总大小
        #l:定义进度条大小
        """
        bar_length = 50  # 定义进度条大小
        percent = float(num) / float(Sum)
        hashes = '=' * int(percent * bar_length)  # 定义进度显示的数量长度百分比
        spaces = ' ' * (bar_length - len(hashes))  # 定义空格的数量=总长度-显示长度

        # sys.stdout.write(
        #     "
传输中: [%s] %d%%  %s/%s " % (hashes + spaces, percent * 100, self.humanbytes(num), self.humanbytes(Sum)))  # 输出显示进度条
        # # %d%%  %d是能传数字 %%:第二个%号代表取消第一个%的特殊意义
        # sys.stdout.flush()  # 强制刷新到屏幕
        print("
传输中: [%s] %d%%  %s/%s " % (hashes + spaces, percent * 100, self.humanbytes(num), self.humanbytes(Sum)),
              file=sys.stdout, flush=True, end='')

    def put(self, args):
        # 服务端存放路径
        if not os.path.exists(self.upload_dir):
            os.mkdir(self.upload_dir)
        filepath = os.path.normpath(os.path.join(
            self.upload_dir,
            args['filename']
        ))

        filesize = args['filesize']
        recv_size = 0  # 定义初始接收大小
        print('接收文件中:%s' % filepath)
        with open(filepath, 'wb') as f:
            while recv_size < filesize:
                recv_data = self.conn.recv(self.max_package_size)
                f.write(recv_data)
                recv_size += len(recv_data)
                self.progres(recv_size, filesize)  # 调用进度条功能
            else:
                print()  # 这里很low   因为进度条不换行,所以这里只能强制换行
                print('%s 接收完成  总大小:%s  已接收大小:%s' % (args['filename'], filesize, recv_size))


server = MYFTPServer(('127.0.0.1', 9800))
server.run()
服务端
#!/usr/bin/env python
# -*- coding: utf-8 -*-  
# author: Fred_li

######################################################客户端############################################################
import os
import socket
import struct
import json


class MYFTPClint:
    family = socket.AF_INET
    tcp_type = socket.SOCK_STREAM
    download_dir = 'download'
    Conding = 'utf8'

    def __init__(self, server_address, connect=True):
        self.socket = socket.socket(self.family, self.tcp_type)
        self.server = server_address

        if connect:
            try:
                self.connect_server()
            except:
                self.client_close()
                raise

    def connect_server(self):
        self.socket.connect(self.server)

    def client_close(self):
        self.socket.close()

    def run(self):
        while True:
            inp = input('>>>:').strip()  # 输入命令及文件路径
            if not inp: continue
            l = inp.split()  # 将inp按空格拆分
            cmd = l[0]
            if hasattr(self, cmd):  # 判断cmd是不是self对象的方法
                func = getattr(self,
                               cmd)  # 结果为True,则拿到对象的绑定方法 <bound method MYFTPClient.put of <__main__.MYFTPClient object at 0x03445610>>
                func(l)  # 方法加参数,就可以调用cmd(self,args)  此时args == l

    def put(self, args):
        cmd = args[0]
        filename = args[1]
        if not os.path.isfile(filename):
            print('file:%s is not exists' % filename)
            return
        else:
            filesize = os.path.getsize(filename)

        header_dic = {
            'cmd': cmd,
            'filename': os.path.basename(filename),
            'filesize': filesize,
        }
        print(header_dic)

        header_json = json.dumps(header_dic)
        header_json_bytes = bytes(header_json, encoding=self.Conding)  # 将json字符串使用utf8编码成bytes类型

        header_struct = struct.pack('i', len(header_json_bytes))  # 将len(header_json_bytes)使用struct打包

        # 发送报头大小给服务端
        self.socket.send(header_struct)

        # 发送真是报头数据
        self.socket.send(header_json_bytes)

        # 上传文件
        send_size = 0  # 定义初始发送大小

        with open(filename, 'rb') as f:
            for line in f:
                self.socket.send(line)
                send_size += len(line)
                # print(send_size)
            else:
                print('upload successful')


client = MYFTPClint(('127.0.0.1', 9800))
client.run()
客户端

十三、模拟ssl加密客户端提交明文密码

#!/usr/bin/env python
# -*- coding: utf-8 -*-  
# author: Fred_li
###################################################服务端##################################################

import socket

import hashlib
import struct
import json

user = 'fred'
password = '1234'

cert_key = '1PTCeD3MRw'


class MYServer:
    family = socket.AF_INET
    tcp_type = socket.SOCK_STREAM
    backlog = 5
    max_recv_size = 1024
    Coding = 'utf8'

    def __init__(self, server_address):
        self.server = server_address
        self.socket = socket.socket(self.family, self.tcp_type)
        self.server_bind()
        self.server_active()

    def server_bind(self):
        self.socket.bind(self.server)

    def server_active(self):
        self.socket.listen(self.backlog)
        print('server is running')

    def get_request(self):
        return self.socket.accept()

    def server_close(self):
        self.socket.close()

    def request_close(self, request):
        request.close()

    def run(self):
        while True:
            self.conn, self.client = self.get_request()
            print(self.client)
            while True:
                try:
                    header_struct = self.conn.recv(4)
                    if not header_struct: break

                    header_len = struct.unpack('i', header_struct)[0]
                    header_json = self.conn.recv(header_len).decode(self.Coding)
                    header_dic = json.loads(header_json)
                    m = hashlib.md5(cert_key.encode(self.Coding))
                    m.update(password.encode(self.Coding))
                    data_password = m.hexdigest()
                    print('客户端提交的密码:%s' % header_dic['password'])
                    print('服务器保存的密码:%s' % data_password)
                    if data_password == header_dic['password'] and user == header_dic['username']:
                        self.conn.send('账号密码验证成功'.encode(self.Coding))

                    else:
                        self.conn.send('账号密码错误'.encode(self.Coding))
                except Exception:
                    break


server = MYServer(('127.0.0.1', 50000))
server.run()
服务端
#!/usr/bin/env python
# -*- coding: utf-8 -*-  
# author: Fred_li
################################################客户端#####################################################

import socket
import hashlib
import struct
import json

cert_key = '1PTCeD3MRw'


class MYClient:
    family = socket.AF_INET
    tcp_type = socket.SOCK_STREAM
    max_recv_size = 1024
    Coding = 'utf8'

    def __init__(self, server_info):
        self.server = server_info
        self.socket = socket.socket(self.family, self.tcp_type)
        self.connect_server()

    def connect_server(self):
        self.socket.connect(self.server)

    def run(self):
        while True:
            username = input('请输入你的账号: ').strip()
            password = input('请输入你的密码:').strip()
            if not username or not password: continue
            m = hashlib.md5(cert_key.encode(self.Coding))
            m.update(password.encode(self.Coding))
            hashpasswd = m.hexdigest()
            msg_dic = {
                'username': username,
                'password': hashpasswd,
            }
            msg_json = json.dumps(msg_dic)
            msg_json_bytes = bytes(msg_json, encoding=self.Coding)
            header_struct = struct.pack('i', len(msg_json_bytes))
            self.socket.send(header_struct)
            self.socket.send(msg_json_bytes)
            res_msg = self.socket.recv(self.max_recv_size)
            print(res_msg.decode(self.Coding))


client = MYClient(('127.0.0.1', 50000))
client.run()
客户端
验证结果:

# 客户端:
D:Python36python.exe D:/python_study_new/day9/复习/认证客户端的链接合法性/client.py
请输入你的账号: fred
请输入你的密码:1234
账号密码验证成功
请输入你的账号: 

# 服务端:
D:Python36python.exe D:/python_study_new/day9/复习/认证客户端的链接合法性/server.py
server is running
('127.0.0.1', 64584)
客户端提交的密码:16e020ace2b75390529f2b6ea5055036
服务器保存的密码:16e020ace2b75390529f2b6ea5055036
原文地址:https://www.cnblogs.com/lichunke/p/9541701.html